Bug 1724541 [wpt PR 29937] - App history: implement reload() instead of no-arg naviga...
[gecko.git] / browser / actors / WebRTCChild.jsm
blob6be9b630cd79ab339f16f310e62a1071ebfbc810
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 var EXPORTED_SYMBOLS = ["WebRTCChild"];
9 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
10 const { XPCOMUtils } = ChromeUtils.import(
11   "resource://gre/modules/XPCOMUtils.jsm"
13 const { AppConstants } = ChromeUtils.import(
14   "resource://gre/modules/AppConstants.jsm"
16 XPCOMUtils.defineLazyServiceGetter(
17   this,
18   "MediaManagerService",
19   "@mozilla.org/mediaManagerService;1",
20   "nsIMediaManagerService"
23 const kBrowserURL = AppConstants.BROWSER_CHROME_URL;
25 /**
26  * GlobalMuteListener is a process-global object that listens for changes to
27  * the global mute state of the camera and microphone. When it notices a
28  * change in that state, it tells the underlying platform code to mute or
29  * unmute those devices.
30  */
31 const GlobalMuteListener = {
32   _initted: false,
34   /**
35    * Initializes the listener if it hasn't been already. This will also
36    * ensure that the microphone and camera are initially in the right
37    * muting state.
38    */
39   init() {
40     if (!this._initted) {
41       Services.cpmm.sharedData.addEventListener("change", this);
42       this._updateCameraMuteState();
43       this._updateMicrophoneMuteState();
44       this._initted = true;
45     }
46   },
48   handleEvent(event) {
49     if (event.changedKeys.includes("WebRTC:GlobalCameraMute")) {
50       this._updateCameraMuteState();
51     }
52     if (event.changedKeys.includes("WebRTC:GlobalMicrophoneMute")) {
53       this._updateMicrophoneMuteState();
54     }
55   },
57   _updateCameraMuteState() {
58     let shouldMute = Services.cpmm.sharedData.get("WebRTC:GlobalCameraMute");
59     let topic = shouldMute
60       ? "getUserMedia:muteVideo"
61       : "getUserMedia:unmuteVideo";
62     Services.obs.notifyObservers(null, topic);
63   },
65   _updateMicrophoneMuteState() {
66     let shouldMute = Services.cpmm.sharedData.get(
67       "WebRTC:GlobalMicrophoneMute"
68     );
69     let topic = shouldMute
70       ? "getUserMedia:muteAudio"
71       : "getUserMedia:unmuteAudio";
73     Services.obs.notifyObservers(null, topic);
74   },
77 class WebRTCChild extends JSWindowActorChild {
78   actorCreated() {
79     // The user might request that DOM notifications be silenced
80     // when sharing the screen. There doesn't seem to be a great
81     // way of storing that state in any of the objects going into
82     // the WebRTC API or coming out via the observer notification
83     // service, so we store it here on the actor.
84     //
85     // If the user chooses to silence notifications during screen
86     // share, this will get set to true.
87     this.suppressNotifications = false;
88   }
90   // Called only for 'unload' to remove pending gUM prompts in reloaded frames.
91   static handleEvent(aEvent) {
92     let contentWindow = aEvent.target.defaultView;
93     let actor = getActorForWindow(contentWindow);
94     if (actor) {
95       for (let key of contentWindow.pendingGetUserMediaRequests.keys()) {
96         actor.sendAsyncMessage("webrtc:CancelRequest", key);
97       }
98       for (let key of contentWindow.pendingPeerConnectionRequests.keys()) {
99         actor.sendAsyncMessage("rtcpeer:CancelRequest", key);
100       }
101     }
102   }
104   // This observer is called from BrowserProcessChild to avoid
105   // loading this .jsm when WebRTC is not in use.
106   static observe(aSubject, aTopic, aData) {
107     switch (aTopic) {
108       case "getUserMedia:request":
109         handleGUMRequest(aSubject, aTopic, aData);
110         break;
111       case "recording-device-stopped":
112         handleGUMStop(aSubject, aTopic, aData);
113         break;
114       case "PeerConnection:request":
115         handlePCRequest(aSubject, aTopic, aData);
116         break;
117       case "recording-device-events":
118         updateIndicators(aSubject, aTopic, aData);
119         break;
120       case "recording-window-ended":
121         removeBrowserSpecificIndicator(aSubject, aTopic, aData);
122         break;
123     }
124   }
126   receiveMessage(aMessage) {
127     switch (aMessage.name) {
128       case "rtcpeer:Allow":
129       case "rtcpeer:Deny": {
130         let callID = aMessage.data.callID;
131         let contentWindow = Services.wm.getOuterWindowWithId(
132           aMessage.data.windowID
133         );
134         forgetPCRequest(contentWindow, callID);
135         let topic =
136           aMessage.name == "rtcpeer:Allow"
137             ? "PeerConnection:response:allow"
138             : "PeerConnection:response:deny";
139         Services.obs.notifyObservers(null, topic, callID);
140         break;
141       }
142       case "webrtc:Allow": {
143         let callID = aMessage.data.callID;
144         let contentWindow = Services.wm.getOuterWindowWithId(
145           aMessage.data.windowID
146         );
147         let devices = contentWindow.pendingGetUserMediaRequests.get(callID);
148         forgetGUMRequest(contentWindow, callID);
150         let allowedDevices = Cc["@mozilla.org/array;1"].createInstance(
151           Ci.nsIMutableArray
152         );
153         for (let deviceIndex of aMessage.data.devices) {
154           allowedDevices.appendElement(devices[deviceIndex]);
155         }
157         Services.obs.notifyObservers(
158           allowedDevices,
159           "getUserMedia:response:allow",
160           callID
161         );
163         this.suppressNotifications = !!aMessage.data.suppressNotifications;
165         break;
166       }
167       case "webrtc:Deny":
168         denyGUMRequest(aMessage.data);
169         break;
170       case "webrtc:StopSharing":
171         Services.obs.notifyObservers(
172           null,
173           "getUserMedia:revoke",
174           aMessage.data
175         );
176         break;
177       case "webrtc:MuteCamera":
178         Services.obs.notifyObservers(
179           null,
180           "getUserMedia:muteVideo",
181           aMessage.data
182         );
183         break;
184       case "webrtc:UnmuteCamera":
185         Services.obs.notifyObservers(
186           null,
187           "getUserMedia:unmuteVideo",
188           aMessage.data
189         );
190         break;
191       case "webrtc:MuteMicrophone":
192         Services.obs.notifyObservers(
193           null,
194           "getUserMedia:muteAudio",
195           aMessage.data
196         );
197         break;
198       case "webrtc:UnmuteMicrophone":
199         Services.obs.notifyObservers(
200           null,
201           "getUserMedia:unmuteAudio",
202           aMessage.data
203         );
204         break;
205     }
206   }
209 function getActorForWindow(window) {
210   try {
211     let windowGlobal = window.windowGlobalChild;
212     if (windowGlobal) {
213       return windowGlobal.getActor("WebRTC");
214     }
215   } catch (ex) {
216     // There might not be an actor for a parent process chrome URL,
217     // and we may not even be allowed to access its windowGlobalChild.
218   }
220   return null;
223 function handlePCRequest(aSubject, aTopic, aData) {
224   let { windowID, innerWindowID, callID, isSecure } = aSubject;
225   let contentWindow = Services.wm.getOuterWindowWithId(windowID);
226   if (!contentWindow.pendingPeerConnectionRequests) {
227     setupPendingListsInitially(contentWindow);
228   }
229   contentWindow.pendingPeerConnectionRequests.add(callID);
231   let request = {
232     windowID,
233     innerWindowID,
234     callID,
235     documentURI: contentWindow.document.documentURI,
236     secure: isSecure,
237   };
239   let actor = getActorForWindow(contentWindow);
240   if (actor) {
241     actor.sendAsyncMessage("rtcpeer:Request", request);
242   }
245 function handleGUMStop(aSubject, aTopic, aData) {
246   let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
248   let request = {
249     windowID: aSubject.windowID,
250     rawID: aSubject.rawID,
251     mediaSource: aSubject.mediaSource,
252   };
254   let actor = getActorForWindow(contentWindow);
255   if (actor) {
256     actor.sendAsyncMessage("webrtc:StopRecording", request);
257   }
260 function handleGUMRequest(aSubject, aTopic, aData) {
261   // Now that a getUserMedia request has been created, we should check
262   // to see if we're supposed to have any devices muted. This needs
263   // to occur after the getUserMedia request is made, since the global
264   // mute state is associated with the GetUserMediaWindowListener, which
265   // is only created after a getUserMedia request.
266   GlobalMuteListener.init();
268   let constraints = aSubject.getConstraints();
269   let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
271   prompt(
272     aSubject.type,
273     contentWindow,
274     aSubject.windowID,
275     aSubject.callID,
276     constraints,
277     aSubject.devices,
278     aSubject.isSecure,
279     aSubject.isHandlingUserInput
280   );
283 function prompt(
284   aRequestType,
285   aContentWindow,
286   aWindowID,
287   aCallID,
288   aConstraints,
289   aDevices,
290   aSecure,
291   aIsHandlingUserInput
292 ) {
293   let audioInputDevices = [];
294   let videoInputDevices = [];
295   let audioOutputDevices = [];
296   let devices = [];
298   // MediaStreamConstraints defines video as 'boolean or MediaTrackConstraints'.
299   let video = aConstraints.video || aConstraints.picture;
300   let audio = aConstraints.audio;
301   let sharingScreen =
302     video && typeof video != "boolean" && video.mediaSource != "camera";
303   let sharingAudio =
304     audio && typeof audio != "boolean" && audio.mediaSource != "microphone";
305   for (let device of aDevices) {
306     device = device.QueryInterface(Ci.nsIMediaDevice);
307     let deviceObject = {
308       name: device.rawName, // unfiltered device name to show to the user
309       deviceIndex: devices.length,
310       id: device.rawId,
311       mediaSource: device.mediaSource,
312     };
313     switch (device.type) {
314       case "audioinput":
315         // Check that if we got a microphone, we have not requested an audio
316         // capture, and if we have requested an audio capture, we are not
317         // getting a microphone instead.
318         if (audio && (device.mediaSource == "microphone") != sharingAudio) {
319           audioInputDevices.push(deviceObject);
320           devices.push(device);
321         }
322         break;
323       case "videoinput":
324         // Verify that if we got a camera, we haven't requested a screen share,
325         // or that if we requested a screen share we aren't getting a camera.
326         if (video && (device.mediaSource == "camera") != sharingScreen) {
327           if (device.scary) {
328             deviceObject.scary = true;
329           }
330           videoInputDevices.push(deviceObject);
331           devices.push(device);
332         }
333         break;
334       case "audiooutput":
335         if (aRequestType == "selectaudiooutput") {
336           audioOutputDevices.push(deviceObject);
337           devices.push(device);
338         }
339         break;
340     }
341   }
343   let requestTypes = [];
344   if (videoInputDevices.length) {
345     requestTypes.push(sharingScreen ? "Screen" : "Camera");
346   }
347   if (audioInputDevices.length) {
348     requestTypes.push(sharingAudio ? "AudioCapture" : "Microphone");
349   }
350   if (audioOutputDevices.length) {
351     requestTypes.push("Speaker");
352   }
354   if (!requestTypes.length) {
355     // Device enumeration is done ahead of handleGUMRequest, so we're not
356     // responsible for handling the NotFoundError spec case.
357     denyGUMRequest({ callID: aCallID });
358     return;
359   }
361   if (!aContentWindow.pendingGetUserMediaRequests) {
362     setupPendingListsInitially(aContentWindow);
363   }
364   aContentWindow.pendingGetUserMediaRequests.set(aCallID, devices);
366   // WebRTC prompts have a bunch of special requirements, such as being able to
367   // grant two permissions (microphone and camera), selecting devices and showing
368   // a screen sharing preview. All this could have probably been baked into
369   // nsIContentPermissionRequest prompts, but the team that implemented this back
370   // then chose to just build their own prompting mechanism instead.
371   //
372   // So, what you are looking at here is not a real nsIContentPermissionRequest, but
373   // something that looks really similar and will be transmitted to webrtcUI.jsm
374   // for showing the prompt.
375   // Note that we basically do the permission delegate check in
376   // nsIContentPermissionRequest, but because webrtc uses their own prompting
377   // system, we should manually apply the delegate policy here. Permission
378   // should be delegated using Feature Policy and top principal
379   const permDelegateHandler = aContentWindow.document.permDelegateHandler.QueryInterface(
380     Ci.nsIPermissionDelegateHandler
381   );
383   const shouldDelegatePermission =
384     permDelegateHandler.permissionDelegateFPEnabled;
386   let secondOrigin = undefined;
387   if (
388     shouldDelegatePermission &&
389     permDelegateHandler.maybeUnsafePermissionDelegate(requestTypes)
390   ) {
391     // We are going to prompt both first party and third party origin.
392     // SecondOrigin should be third party
393     secondOrigin = aContentWindow.document.nodePrincipal.origin;
394   }
396   let request = {
397     callID: aCallID,
398     windowID: aWindowID,
399     secondOrigin,
400     documentURI: aContentWindow.document.documentURI,
401     secure: aSecure,
402     isHandlingUserInput: aIsHandlingUserInput,
403     shouldDelegatePermission,
404     requestTypes,
405     sharingScreen,
406     sharingAudio,
407     audioInputDevices,
408     videoInputDevices,
409     audioOutputDevices,
410   };
412   let actor = getActorForWindow(aContentWindow);
413   if (actor) {
414     actor.sendAsyncMessage("webrtc:Request", request);
415   }
418 function denyGUMRequest(aData) {
419   let subject;
420   if (aData.noOSPermission) {
421     subject = "getUserMedia:response:noOSPermission";
422   } else {
423     subject = "getUserMedia:response:deny";
424   }
425   Services.obs.notifyObservers(null, subject, aData.callID);
427   if (!aData.windowID) {
428     return;
429   }
430   let contentWindow = Services.wm.getOuterWindowWithId(aData.windowID);
431   if (contentWindow.pendingGetUserMediaRequests) {
432     forgetGUMRequest(contentWindow, aData.callID);
433   }
436 function forgetGUMRequest(aContentWindow, aCallID) {
437   aContentWindow.pendingGetUserMediaRequests.delete(aCallID);
438   forgetPendingListsEventually(aContentWindow);
441 function forgetPCRequest(aContentWindow, aCallID) {
442   aContentWindow.pendingPeerConnectionRequests.delete(aCallID);
443   forgetPendingListsEventually(aContentWindow);
446 function setupPendingListsInitially(aContentWindow) {
447   if (aContentWindow.pendingGetUserMediaRequests) {
448     return;
449   }
450   aContentWindow.pendingGetUserMediaRequests = new Map();
451   aContentWindow.pendingPeerConnectionRequests = new Set();
452   aContentWindow.addEventListener("unload", WebRTCChild.handleEvent);
455 function forgetPendingListsEventually(aContentWindow) {
456   if (
457     aContentWindow.pendingGetUserMediaRequests.size ||
458     aContentWindow.pendingPeerConnectionRequests.size
459   ) {
460     return;
461   }
462   aContentWindow.pendingGetUserMediaRequests = null;
463   aContentWindow.pendingPeerConnectionRequests = null;
464   aContentWindow.removeEventListener("unload", WebRTCChild.handleEvent);
467 function updateIndicators(aSubject, aTopic, aData) {
468   if (
469     aSubject instanceof Ci.nsIPropertyBag &&
470     aSubject.getProperty("requestURL") == kBrowserURL
471   ) {
472     // Ignore notifications caused by the browser UI showing previews.
473     return;
474   }
476   let contentWindow = aSubject.getProperty("window");
478   let actor = contentWindow ? getActorForWindow(contentWindow) : null;
479   if (actor) {
480     let tabState = getTabStateForContentWindow(contentWindow, false);
481     tabState.windowId = getInnerWindowIDForWindow(contentWindow);
483     // If we were silencing DOM notifications before, but we've updated
484     // state such that we're no longer sharing one of our displays, then
485     // reset the silencing state.
486     if (actor.suppressNotifications) {
487       if (!tabState.screen && !tabState.window && !tabState.browser) {
488         actor.suppressNotifications = false;
489       }
490     }
492     tabState.suppressNotifications = actor.suppressNotifications;
494     actor.sendAsyncMessage("webrtc:UpdateIndicators", tabState);
495   }
498 function removeBrowserSpecificIndicator(aSubject, aTopic, aData) {
499   let contentWindow = Services.wm.getOuterWindowWithId(aData);
500   if (contentWindow.document.documentURI == kBrowserURL) {
501     // Ignore notifications caused by the browser UI showing previews.
502     return;
503   }
505   let tabState = getTabStateForContentWindow(contentWindow, true);
507   tabState.windowId = aData;
509   let actor = getActorForWindow(contentWindow);
510   if (actor) {
511     actor.sendAsyncMessage("webrtc:UpdateIndicators", tabState);
512   }
515 function getTabStateForContentWindow(aContentWindow, aForRemove = false) {
516   let camera = {},
517     microphone = {},
518     screen = {},
519     window = {},
520     browser = {},
521     devices = {};
522   MediaManagerService.mediaCaptureWindowState(
523     aContentWindow,
524     camera,
525     microphone,
526     screen,
527     window,
528     browser,
529     devices
530   );
532   if (
533     camera.value == MediaManagerService.STATE_NOCAPTURE &&
534     microphone.value == MediaManagerService.STATE_NOCAPTURE &&
535     screen.value == MediaManagerService.STATE_NOCAPTURE &&
536     window.value == MediaManagerService.STATE_NOCAPTURE &&
537     browser.value == MediaManagerService.STATE_NOCAPTURE
538   ) {
539     return { remove: true };
540   }
542   if (aForRemove) {
543     return { remove: true };
544   }
546   let serializedDevices = [];
547   if (Array.isArray(devices.value)) {
548     serializedDevices = devices.value.map(device => {
549       return {
550         type: device.type,
551         mediaSource: device.mediaSource,
552         rawId: device.rawId,
553         scary: device.scary,
554       };
555     });
556   }
558   return {
559     camera: camera.value,
560     microphone: microphone.value,
561     screen: screen.value,
562     window: window.value,
563     browser: browser.value,
564     devices: serializedDevices,
565   };
568 function getInnerWindowIDForWindow(aContentWindow) {
569   return aContentWindow.windowGlobalChild.innerWindowId;