Repository: electron/electron
Pull request: fix: video and audio capture should be separate

Backport of #42775

See that PR for details.


Notes: Fixes an issue where `navigator.mediaDevices.enumerateDevices`  could return broken results in some cases after calling `session.setPermissionCheckHandler`

Changed files:
filename: shell/browser/web_contents_permission_helper.cc
patch: @@ -282,10 +282,10 @@ bool WebContentsPermissionHelper::CheckMediaAccessPermission(
   base::Value::Dict details;
   details.Set("securityOrigin", security_origin.GetURL().spec());
   details.Set("mediaType", MediaStreamTypeToString(type));
-  // The permission type doesn't matter here, AUDIO_CAPTURE/VIDEO_CAPTURE
-  // are presented as same type in content_converter.h.
-  return CheckPermission(blink::PermissionType::AUDIO_CAPTURE,
-                         std::move(details));
+  auto blink_type = type == blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE
+                        ? blink::PermissionType::AUDIO_CAPTURE
+                        : blink::PermissionType::VIDEO_CAPTURE;
+  return CheckPermission(blink_type, std::move(details));
 }
 
 bool WebContentsPermissionHelper::CheckSerialAccessPermission(

filename: spec/chromium-spec.ts
patch: @@ -1765,14 +1765,48 @@ describe('chromium features', () => {
       expect(labels.some((l: any) => l)).to.be.true();
     });
 
-    it('does not return labels of enumerated devices when permission denied', async () => {
+    it('does not return labels of enumerated devices when all permission denied', async () => {
       session.defaultSession.setPermissionCheckHandler(() => false);
       const w = new BrowserWindow({ show: false });
       w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
       const labels = await w.webContents.executeJavaScript('navigator.mediaDevices.enumerateDevices().then(ds => ds.map(d => d.label))');
       expect(labels.some((l: any) => l)).to.be.false();
     });
 
+    it('does not return labels of enumerated audio devices when permission denied', async () => {
+      session.defaultSession.setPermissionCheckHandler((wc, permission, origin, { mediaType }) => {
+        return permission !== 'media' || mediaType !== 'audio';
+      });
+      const w = new BrowserWindow({ show: false });
+      w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
+      const devices = await w.webContents.executeJavaScript(
+        `navigator.mediaDevices.enumerateDevices().then(ds => ds.map(d => {
+          return ({ label: d.label, kind: d.kind })
+        }));
+      `);
+      const audioDevices = devices.filter((d: any) => d.kind === 'audioinput');
+      expect(audioDevices.some((d: any) => d.label)).to.be.false();
+      const videoDevices = devices.filter((d: any) => d.kind === 'videoinput');
+      expect(videoDevices.some((d: any) => d.label)).to.be.true();
+    });
+
+    it('does not return labels of enumerated video devices when permission denied', async () => {
+      session.defaultSession.setPermissionCheckHandler((wc, permission, origin, { mediaType }) => {
+        return permission !== 'media' || mediaType !== 'video';
+      });
+      const w = new BrowserWindow({ show: false });
+      w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
+      const devices = await w.webContents.executeJavaScript(
+        `navigator.mediaDevices.enumerateDevices().then(ds => ds.map(d => {
+          return ({ label: d.label, kind: d.kind })
+        }));
+      `);
+      const audioDevices = devices.filter((d: any) => d.kind === 'audioinput');
+      expect(audioDevices.some((d: any) => d.label)).to.be.true();
+      const videoDevices = devices.filter((d: any) => d.kind === 'videoinput');
+      expect(videoDevices.some((d: any) => d.label)).to.be.false();
+    });
+
     it('returns the same device ids across reloads', async () => {
       const ses = session.fromPartition('persist:media-device-id');
       const w = new BrowserWindow({