Bug 1612006 - getDisplayMedia NotFoundError in firefox after upgrade to MacOS 10.15.3 r=spohl a=RyanVM
authorHaik Aftandilian <haftandilian@mozilla.com>
Thu, 13 Feb 2020 23:36:23 +0000
changeset 575734 d3d67724628a858ad075567b4701f31d3bc05c6f
parent 575733 9cf0faa2144b87c885c76641c1ad881f94bb05a8
child 575735 5d58ff09f3caab845b39864c0a75e3ee13408606
push id12708
push usermalexandru@mozilla.com
push dateFri, 14 Feb 2020 19:49:29 +0000
treeherdermozilla-beta@5d58ff09f3ca [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersspohl, RyanVM
bugs1612006
milestone74.0
Bug 1612006 - getDisplayMedia NotFoundError in firefox after upgrade to MacOS 10.15.3 r=spohl a=RyanVM Update the heuristic-based screen recording permission check to be more reliable but still imperfect. Add pref "media.macos.screenrecording.oscheck.enabled" (true by default) to allow bypassing the permission check as a workaround and for testing. i.e., when the pref is set, nsIOSPermissionRequest::getScreenCapturePermissionState() always returns PERMISSION_STATE_AUTHORIZED on macOS. Differential Revision: https://phabricator.services.mozilla.com/D61909
modules/libpref/init/StaticPrefList.yaml
widget/cocoa/nsCocoaUtils.mm
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -6771,16 +6771,23 @@
   value: 0
   mirror: always
 
 - name: media.mediacontrol.testingevents.enabled
   type: bool
   value: false
   mirror: always
 
+#if defined(XP_MACOSX)
+- name: media.macos.screenrecording.oscheck.enabled
+  type: bool
+  value: true
+  mirror: always
+#endif
+
 #---------------------------------------------------------------------------
 # Prefs starting with "mousewheel."
 #---------------------------------------------------------------------------
 
 # This affects how line scrolls from wheel events will be accelerated.
 # Factor to be multiplied for constant acceleration.
 - name: mousewheel.acceleration.factor
   type: RelaxedAtomicInt32
--- a/widget/cocoa/nsCocoaUtils.mm
+++ b/widget/cocoa/nsCocoaUtils.mm
@@ -31,16 +31,17 @@
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/Logging.h"
 #include "mozilla/MiscEvents.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/TextEvents.h"
 #include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPrefs_media.h"
 
 using namespace mozilla;
 using namespace mozilla::widget;
 
 using mozilla::dom::Promise;
 using mozilla::gfx::BackendType;
 using mozilla::gfx::DataSourceSurface;
 using mozilla::gfx::DrawTarget;
@@ -1234,70 +1235,95 @@ nsresult nsCocoaUtils::GetAudioCapturePe
 // PERMISSION_STATE_DENIED. Returns NS_ERROR_NOT_IMPLEMENTED on macOS 10.14
 // and earlier.
 nsresult nsCocoaUtils::GetScreenCapturePermissionState(uint16_t& aPermissionState) {
   aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_NOTDETERMINED;
 
   // Only attempt to check screen recording authorization status on 10.15+.
   // On earlier macOS versions, screen recording is allowed by default.
   if (@available(macOS 10.15, *)) {
+    if (!StaticPrefs::media_macos_screenrecording_oscheck_enabled()) {
+      aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_AUTHORIZED;
+      LOG("screen authorization status: authorized (test disabled via pref)");
+      return NS_OK;
+    }
+
     // Unlike with camera and microphone capture, there is no support for
     // checking the screen recording permission status. Instead, an application
     // can use the presence of window names (which are privacy sensitive) in
     // the window info list as an indication. The list only includes window
     // names if the calling application has been authorized to record the
-    // screen. However, this does not allow us to differentiate between the
-    // denied and not-yet-decided state which is what is what
-    // AVAuthorizationStatusNotDetermined indicates for camera and microphone.
+    // screen. We use the window name, window level, and owning PID as
+    // heuristics to determine if we have screen recording permission.
     AutoCFRelease<CFArrayRef> windowArray =
         CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
     if (!windowArray) {
       LOG("GetScreenCapturePermissionState() ERROR: got NULL window info list");
       return NS_ERROR_UNEXPECTED;
     }
 
+    int32_t windowLevelDock = CGWindowLevelForKey(kCGDockWindowLevelKey);
+    int32_t windowLevelNormal = CGWindowLevelForKey(kCGNormalWindowLevelKey);
+    LOG("GetScreenCapturePermissionState(): DockWindowLevel: %d, "
+        "NormalWindowLevel: %d",
+        windowLevelDock, windowLevelNormal);
+
+    int32_t thisPid = [[NSProcessInfo processInfo] processIdentifier];
+
     CFIndex windowCount = CFArrayGetCount(windowArray);
     LOG("GetScreenCapturePermissionState() returned %ld windows", windowCount);
     if (windowCount == 0) {
       return NS_ERROR_UNEXPECTED;
     }
 
-    uint32_t windowsWithNames = 0;
     for (CFIndex i = 0; i < windowCount; i++) {
       CFDictionaryRef windowDict =
           reinterpret_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(windowArray, i));
 
-      // Check for the presence of the window name
+      // Get the window owner's PID
+      int32_t windowOwnerPid = -1;
+      CFNumberRef windowPidRef =
+          reinterpret_cast<CFNumberRef>(CFDictionaryGetValue(windowDict, kCGWindowOwnerPID));
+      if (!windowPidRef || !CFNumberGetValue(windowPidRef, kCFNumberIntType, &windowOwnerPid)) {
+        LOG("GetScreenCapturePermissionState() ERROR: failed to get window owner");
+        continue;
+      }
+
+      // Our own window names are always readable and
+      // therefore not relevant to the heuristic.
+      if (thisPid == windowOwnerPid) {
+        continue;
+      }
+
       CFStringRef windowName =
           reinterpret_cast<CFStringRef>(CFDictionaryGetValue(windowDict, kCGWindowName));
-      if (windowName) {
-        windowsWithNames++;
-      } else {
-        // We encountered a window with no name property indicating we
-        // don't have screen recording permission. No need to continue.
-        break;
+      if (!windowName) {
+        continue;
+      }
+
+      CFNumberRef windowLayerRef =
+          reinterpret_cast<CFNumberRef>(CFDictionaryGetValue(windowDict, kCGWindowLayer));
+      int32_t windowLayer;
+      if (!windowLayerRef || !CFNumberGetValue(windowLayerRef, kCFNumberIntType, &windowLayer)) {
+        LOG("GetScreenCapturePermissionState() ERROR: failed to get layer");
+        continue;
+      }
+
+      // If we have a window name and the window is in the dock or normal window
+      // level, and for another process, assume we have screen recording access.
+      LOG("GetScreenCapturePermissionState(): windowLayer: %d", windowLayer);
+      if (windowLayer == windowLevelDock || windowLayer == windowLevelNormal) {
+        aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_AUTHORIZED;
+        LOG("screen authorization status: authorized");
+        return NS_OK;
       }
     }
 
-    LOG("GetScreenCapturePermissionState(): %d/%d named windows", windowsWithNames,
-        (uint32_t)windowCount);
-
-    if (windowsWithNames == windowCount) {
-      // All windows in the list have the name property. We have already
-      // been given permission to record the screen.
-      LOG("screen authorization status: authorized");
-      aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_AUTHORIZED;
-    } else {
-      // We don't have permission to record the screen and we can't
-      // differntiate between the scenario when the user explicitly
-      // denied permission and when the user has not been asked yet.
-      LOG("screen authorization status: not authorized");
-      aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_DENIED;
-    }
-
+    aPermissionState = nsIOSPermissionRequest::PERMISSION_STATE_DENIED;
+    LOG("screen authorization status: not authorized");
     return NS_OK;
   }
 
   LOG("GetScreenCapturePermissionState(): nothing to do, not on 10.15+");
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 nsresult nsCocoaUtils::RequestVideoCapturePermission(RefPtr<Promise>& aPromise) {