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/. */
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(
18 "MediaManagerService",
19 "@mozilla.org/mediaManagerService;1",
20 "nsIMediaManagerService"
23 const kBrowserURL = AppConstants.BROWSER_CHROME_URL;
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.
31 const GlobalMuteListener = {
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
41 Services.cpmm.sharedData.addEventListener("change", this);
42 this._updateCameraMuteState();
43 this._updateMicrophoneMuteState();
49 if (event.changedKeys.includes("WebRTC:GlobalCameraMute")) {
50 this._updateCameraMuteState();
52 if (event.changedKeys.includes("WebRTC:GlobalMicrophoneMute")) {
53 this._updateMicrophoneMuteState();
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);
65 _updateMicrophoneMuteState() {
66 let shouldMute = Services.cpmm.sharedData.get(
67 "WebRTC:GlobalMicrophoneMute"
69 let topic = shouldMute
70 ? "getUserMedia:muteAudio"
71 : "getUserMedia:unmuteAudio";
73 Services.obs.notifyObservers(null, topic);
77 class WebRTCChild extends JSWindowActorChild {
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.
85 // If the user chooses to silence notifications during screen
86 // share, this will get set to true.
87 this.suppressNotifications = false;
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);
95 for (let key of contentWindow.pendingGetUserMediaRequests.keys()) {
96 actor.sendAsyncMessage("webrtc:CancelRequest", key);
98 for (let key of contentWindow.pendingPeerConnectionRequests.keys()) {
99 actor.sendAsyncMessage("rtcpeer:CancelRequest", key);
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) {
108 case "getUserMedia:request":
109 handleGUMRequest(aSubject, aTopic, aData);
111 case "recording-device-stopped":
112 handleGUMStop(aSubject, aTopic, aData);
114 case "PeerConnection:request":
115 handlePCRequest(aSubject, aTopic, aData);
117 case "recording-device-events":
118 updateIndicators(aSubject, aTopic, aData);
120 case "recording-window-ended":
121 removeBrowserSpecificIndicator(aSubject, aTopic, aData);
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
134 forgetPCRequest(contentWindow, callID);
136 aMessage.name == "rtcpeer:Allow"
137 ? "PeerConnection:response:allow"
138 : "PeerConnection:response:deny";
139 Services.obs.notifyObservers(null, topic, callID);
142 case "webrtc:Allow": {
143 let callID = aMessage.data.callID;
144 let contentWindow = Services.wm.getOuterWindowWithId(
145 aMessage.data.windowID
147 let devices = contentWindow.pendingGetUserMediaRequests.get(callID);
148 forgetGUMRequest(contentWindow, callID);
150 let allowedDevices = Cc["@mozilla.org/array;1"].createInstance(
153 for (let deviceIndex of aMessage.data.devices) {
154 allowedDevices.appendElement(devices[deviceIndex]);
157 Services.obs.notifyObservers(
159 "getUserMedia:response:allow",
163 this.suppressNotifications = !!aMessage.data.suppressNotifications;
168 denyGUMRequest(aMessage.data);
170 case "webrtc:StopSharing":
171 Services.obs.notifyObservers(
173 "getUserMedia:revoke",
177 case "webrtc:MuteCamera":
178 Services.obs.notifyObservers(
180 "getUserMedia:muteVideo",
184 case "webrtc:UnmuteCamera":
185 Services.obs.notifyObservers(
187 "getUserMedia:unmuteVideo",
191 case "webrtc:MuteMicrophone":
192 Services.obs.notifyObservers(
194 "getUserMedia:muteAudio",
198 case "webrtc:UnmuteMicrophone":
199 Services.obs.notifyObservers(
201 "getUserMedia:unmuteAudio",
209 function getActorForWindow(window) {
211 let windowGlobal = window.windowGlobalChild;
213 return windowGlobal.getActor("WebRTC");
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.
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);
229 contentWindow.pendingPeerConnectionRequests.add(callID);
235 documentURI: contentWindow.document.documentURI,
239 let actor = getActorForWindow(contentWindow);
241 actor.sendAsyncMessage("rtcpeer:Request", request);
245 function handleGUMStop(aSubject, aTopic, aData) {
246 let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
249 windowID: aSubject.windowID,
250 rawID: aSubject.rawID,
251 mediaSource: aSubject.mediaSource,
254 let actor = getActorForWindow(contentWindow);
256 actor.sendAsyncMessage("webrtc:StopRecording", request);
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);
279 aSubject.isHandlingUserInput
293 let audioInputDevices = [];
294 let videoInputDevices = [];
295 let audioOutputDevices = [];
298 // MediaStreamConstraints defines video as 'boolean or MediaTrackConstraints'.
299 let video = aConstraints.video || aConstraints.picture;
300 let audio = aConstraints.audio;
302 video && typeof video != "boolean" && video.mediaSource != "camera";
304 audio && typeof audio != "boolean" && audio.mediaSource != "microphone";
305 for (let device of aDevices) {
306 device = device.QueryInterface(Ci.nsIMediaDevice);
308 name: device.rawName, // unfiltered device name to show to the user
309 deviceIndex: devices.length,
311 mediaSource: device.mediaSource,
313 switch (device.type) {
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);
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) {
328 deviceObject.scary = true;
330 videoInputDevices.push(deviceObject);
331 devices.push(device);
335 if (aRequestType == "selectaudiooutput") {
336 audioOutputDevices.push(deviceObject);
337 devices.push(device);
343 let requestTypes = [];
344 if (videoInputDevices.length) {
345 requestTypes.push(sharingScreen ? "Screen" : "Camera");
347 if (audioInputDevices.length) {
348 requestTypes.push(sharingAudio ? "AudioCapture" : "Microphone");
350 if (audioOutputDevices.length) {
351 requestTypes.push("Speaker");
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 });
361 if (!aContentWindow.pendingGetUserMediaRequests) {
362 setupPendingListsInitially(aContentWindow);
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.
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
383 const shouldDelegatePermission =
384 permDelegateHandler.permissionDelegateFPEnabled;
386 let secondOrigin = undefined;
388 shouldDelegatePermission &&
389 permDelegateHandler.maybeUnsafePermissionDelegate(requestTypes)
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;
400 documentURI: aContentWindow.document.documentURI,
402 isHandlingUserInput: aIsHandlingUserInput,
403 shouldDelegatePermission,
412 let actor = getActorForWindow(aContentWindow);
414 actor.sendAsyncMessage("webrtc:Request", request);
418 function denyGUMRequest(aData) {
420 if (aData.noOSPermission) {
421 subject = "getUserMedia:response:noOSPermission";
423 subject = "getUserMedia:response:deny";
425 Services.obs.notifyObservers(null, subject, aData.callID);
427 if (!aData.windowID) {
430 let contentWindow = Services.wm.getOuterWindowWithId(aData.windowID);
431 if (contentWindow.pendingGetUserMediaRequests) {
432 forgetGUMRequest(contentWindow, aData.callID);
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) {
450 aContentWindow.pendingGetUserMediaRequests = new Map();
451 aContentWindow.pendingPeerConnectionRequests = new Set();
452 aContentWindow.addEventListener("unload", WebRTCChild.handleEvent);
455 function forgetPendingListsEventually(aContentWindow) {
457 aContentWindow.pendingGetUserMediaRequests.size ||
458 aContentWindow.pendingPeerConnectionRequests.size
462 aContentWindow.pendingGetUserMediaRequests = null;
463 aContentWindow.pendingPeerConnectionRequests = null;
464 aContentWindow.removeEventListener("unload", WebRTCChild.handleEvent);
467 function updateIndicators(aSubject, aTopic, aData) {
469 aSubject instanceof Ci.nsIPropertyBag &&
470 aSubject.getProperty("requestURL") == kBrowserURL
472 // Ignore notifications caused by the browser UI showing previews.
476 let contentWindow = aSubject.getProperty("window");
478 let actor = contentWindow ? getActorForWindow(contentWindow) : null;
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;
492 tabState.suppressNotifications = actor.suppressNotifications;
494 actor.sendAsyncMessage("webrtc:UpdateIndicators", tabState);
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.
505 let tabState = getTabStateForContentWindow(contentWindow, true);
507 tabState.windowId = aData;
509 let actor = getActorForWindow(contentWindow);
511 actor.sendAsyncMessage("webrtc:UpdateIndicators", tabState);
515 function getTabStateForContentWindow(aContentWindow, aForRemove = false) {
522 MediaManagerService.mediaCaptureWindowState(
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
539 return { remove: true };
543 return { remove: true };
546 let serializedDevices = [];
547 if (Array.isArray(devices.value)) {
548 serializedDevices = devices.value.map(device => {
551 mediaSource: device.mediaSource,
559 camera: camera.value,
560 microphone: microphone.value,
561 screen: screen.value,
562 window: window.value,
563 browser: browser.value,
564 devices: serializedDevices,
568 function getInnerWindowIDForWindow(aContentWindow) {
569 return aContentWindow.windowGlobalChild.innerWindowId;