Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
XPCOMUtils.defineLazyServiceGetter(
lazy,
"MediaManagerService",
"@mozilla.org/mediaManagerService;1",
"nsIMediaManagerService"
);
const kBrowserURL = AppConstants.BROWSER_CHROME_URL;
/**
* GlobalMuteListener is a process-global object that listens for changes to
* the global mute state of the camera and microphone. When it notices a
* change in that state, it tells the underlying platform code to mute or
* unmute those devices.
*/
const GlobalMuteListener = {
_initted: false,
/**
* Initializes the listener if it hasn't been already. This will also
* ensure that the microphone and camera are initially in the right
* muting state.
*/
init() {
if (!this._initted) {
Services.cpmm.sharedData.addEventListener("change", this);
this._updateCameraMuteState();
this._updateMicrophoneMuteState();
this._initted = true;
}
},
handleEvent(event) {
if (event.changedKeys.includes("WebRTC:GlobalCameraMute")) {
this._updateCameraMuteState();
}
if (event.changedKeys.includes("WebRTC:GlobalMicrophoneMute")) {
this._updateMicrophoneMuteState();
}
},
_updateCameraMuteState() {
let shouldMute = Services.cpmm.sharedData.get("WebRTC:GlobalCameraMute");
let topic = shouldMute
? "getUserMedia:muteVideo"
: "getUserMedia:unmuteVideo";
Services.obs.notifyObservers(null, topic);
},
_updateMicrophoneMuteState() {
let shouldMute = Services.cpmm.sharedData.get(
"WebRTC:GlobalMicrophoneMute"
);
let topic = shouldMute
? "getUserMedia:muteAudio"
: "getUserMedia:unmuteAudio";
Services.obs.notifyObservers(null, topic);
},
};
export class WebRTCChild extends JSWindowActorChild {
actorCreated() {
// The user might request that DOM notifications be silenced
// when sharing the screen. There doesn't seem to be a great
// way of storing that state in any of the objects going into
// the WebRTC API or coming out via the observer notification
// service, so we store it here on the actor.
//
// If the user chooses to silence notifications during screen
// share, this will get set to true.
this.suppressNotifications = false;
}
// Called only for 'unload' to remove pending gUM prompts in reloaded frames.
static handleEvent(aEvent) {
let contentWindow = aEvent.target.defaultView;
let actor = getActorForWindow(contentWindow);
if (actor) {
for (let key of contentWindow.pendingGetUserMediaRequests.keys()) {
actor.sendAsyncMessage("webrtc:CancelRequest", key);
}
for (let key of contentWindow.pendingPeerConnectionRequests.keys()) {
actor.sendAsyncMessage("rtcpeer:CancelRequest", key);
}
}
}
// This observer is called from BrowserProcessChild to avoid
// loading this module when WebRTC is not in use.
static observe(aSubject, aTopic, aData) {
switch (aTopic) {
case "getUserMedia:request":
handleGUMRequest(aSubject, aTopic, aData);
break;
case "recording-device-stopped":
handleGUMStop(aSubject, aTopic, aData);
break;
case "PeerConnection:request":
handlePCRequest(aSubject, aTopic, aData);
break;
case "recording-device-events":
updateIndicators(aSubject, aTopic, aData);
break;
case "recording-window-ended":
removeBrowserSpecificIndicator(aSubject, aTopic, aData);
break;
}
}
receiveMessage(aMessage) {
switch (aMessage.name) {
case "rtcpeer:Allow":
case "rtcpeer:Deny": {
let callID = aMessage.data.callID;
let contentWindow = Services.wm.getOuterWindowWithId(
aMessage.data.windowID
);
forgetPCRequest(contentWindow, callID);
let topic =
aMessage.name == "rtcpeer:Allow"
? "PeerConnection:response:allow"
: "PeerConnection:response:deny";
Services.obs.notifyObservers(null, topic, callID);
break;
}
case "webrtc:Allow": {
let callID = aMessage.data.callID;
let contentWindow = Services.wm.getOuterWindowWithId(
aMessage.data.windowID
);
let devices = contentWindow.pendingGetUserMediaRequests.get(callID);
forgetGUMRequest(contentWindow, callID);
let allowedDevices = Cc["@mozilla.org/array;1"].createInstance(
Ci.nsIMutableArray
);
for (let deviceIndex of aMessage.data.devices) {
allowedDevices.appendElement(devices[deviceIndex]);
}
Services.obs.notifyObservers(
allowedDevices,
"getUserMedia:response:allow",
callID
);
this.suppressNotifications = !!aMessage.data.suppressNotifications;
break;
}
case "webrtc:Deny":
denyGUMRequest(aMessage.data);
break;
case "webrtc:StopSharing":
Services.obs.notifyObservers(
null,
"getUserMedia:revoke",
aMessage.data
);
break;
case "webrtc:MuteCamera":
Services.obs.notifyObservers(
null,
"getUserMedia:muteVideo",
aMessage.data
);
break;
case "webrtc:UnmuteCamera":
Services.obs.notifyObservers(
null,
"getUserMedia:unmuteVideo",
aMessage.data
);
break;
case "webrtc:MuteMicrophone":
Services.obs.notifyObservers(
null,
"getUserMedia:muteAudio",
aMessage.data
);
break;
case "webrtc:UnmuteMicrophone":
Services.obs.notifyObservers(
null,
"getUserMedia:unmuteAudio",
aMessage.data
);
break;
}
}
}
function getActorForWindow(window) {
try {
let windowGlobal = window.windowGlobalChild;
if (windowGlobal) {
return windowGlobal.getActor("WebRTC");
}
} catch (ex) {
// There might not be an actor for a parent process chrome URL,
// and we may not even be allowed to access its windowGlobalChild.
}
return null;
}
function handlePCRequest(aSubject) {
let { windowID, innerWindowID, callID, isSecure } = aSubject;
let contentWindow = Services.wm.getOuterWindowWithId(windowID);
if (!contentWindow.pendingPeerConnectionRequests) {
setupPendingListsInitially(contentWindow);
}
contentWindow.pendingPeerConnectionRequests.add(callID);
let request = {
windowID,
innerWindowID,
callID,
documentURI: contentWindow.document.documentURI,
secure: isSecure,
};
let actor = getActorForWindow(contentWindow);
if (actor) {
actor.sendAsyncMessage("rtcpeer:Request", request);
}
}
function handleGUMStop(aSubject) {
let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
let request = {
windowID: aSubject.windowID,
rawID: aSubject.rawID,
mediaSource: aSubject.mediaSource,
};
let actor = getActorForWindow(contentWindow);
if (actor) {
actor.sendAsyncMessage("webrtc:StopRecording", request);
}
}
function handleGUMRequest(aSubject) {
// Now that a getUserMedia request has been created, we should check
// to see if we're supposed to have any devices muted. This needs
// to occur after the getUserMedia request is made, since the global
// mute state is associated with the GetUserMediaWindowListener, which
// is only created after a getUserMedia request.
GlobalMuteListener.init();
let constraints = aSubject.getConstraints();
let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
prompt(
aSubject.type,
contentWindow,
aSubject.windowID,
aSubject.callID,
constraints,
aSubject.getAudioOutputOptions(),
aSubject.devices,
aSubject.isSecure,
aSubject.isHandlingUserInput
);
}
function prompt(
aRequestType,
aContentWindow,
aWindowID,
aCallID,
aConstraints,
aAudioOutputOptions,
aDevices,
aSecure,
aIsHandlingUserInput
) {
let audioInputDevices = [];
let videoInputDevices = [];
let audioOutputDevices = [];
let devices = [];
// MediaStreamConstraints defines video as 'boolean or MediaTrackConstraints'.
let video = aConstraints.video || aConstraints.picture;
let audio = aConstraints.audio;
let sharingScreen =
video && typeof video != "boolean" && video.mediaSource != "camera";
let sharingAudio =
audio && typeof audio != "boolean" && audio.mediaSource != "microphone";
const hasInherentConstraints = ({ facingMode, groupId, deviceId }) => {
const id = [deviceId].flat()[0];
return facingMode || groupId || (id && id != "default"); // flock workaround
};
let hasInherentAudioConstraints =
audio &&
!sharingAudio &&
[audio, ...(audio.advanced || [])].some(hasInherentConstraints);
let hasInherentVideoConstraints =
video &&
!sharingScreen &&
[video, ...(video.advanced || [])].some(hasInherentConstraints);
for (let device of aDevices) {
device = device.QueryInterface(Ci.nsIMediaDevice);
let deviceObject = {
name: device.rawName, // unfiltered device name to show to the user
deviceIndex: devices.length,
rawId: device.rawId,
id: device.id,
mediaSource: device.mediaSource,
canRequestOsLevelPrompt: device.canRequestOsLevelPrompt,
};
switch (device.type) {
case "audioinput":
// Check that if we got a microphone, we have not requested an audio
// capture, and if we have requested an audio capture, we are not
// getting a microphone instead.
if (audio && (device.mediaSource == "microphone") != sharingAudio) {
audioInputDevices.push(deviceObject);
devices.push(device);
}
break;
case "videoinput":
// Verify that if we got a camera, we haven't requested a screen share,
// or that if we requested a screen share we aren't getting a camera.
if (video && (device.mediaSource == "camera") != sharingScreen) {
if (device.scary) {
deviceObject.scary = true;
}
videoInputDevices.push(deviceObject);
devices.push(device);
}
break;
case "audiooutput":
if (aRequestType == "selectaudiooutput") {
audioOutputDevices.push(deviceObject);
devices.push(device);
}
break;
}
}
let requestTypes = [];
if (videoInputDevices.length) {
requestTypes.push(sharingScreen ? "Screen" : "Camera");
}
if (audioInputDevices.length) {
requestTypes.push(sharingAudio ? "AudioCapture" : "Microphone");
}
if (audioOutputDevices.length) {
requestTypes.push("Speaker");
}
if (!requestTypes.length) {
// Device enumeration is done ahead of handleGUMRequest, so we're not
// responsible for handling the NotFoundError spec case.
denyGUMRequest({ callID: aCallID });
return;
}
if (!aContentWindow.pendingGetUserMediaRequests) {
setupPendingListsInitially(aContentWindow);
}
aContentWindow.pendingGetUserMediaRequests.set(aCallID, devices);
// WebRTC prompts have a bunch of special requirements, such as being able to
// grant two permissions (microphone and camera), selecting devices and showing
// a screen sharing preview. All this could have probably been baked into
// nsIContentPermissionRequest prompts, but the team that implemented this back
// then chose to just build their own prompting mechanism instead.
//
// So, what you are looking at here is not a real nsIContentPermissionRequest, but
// something that looks really similar and will be transmitted to webrtcUI.sys.mjs
// for showing the prompt.
// Note that we basically do the permission delegate check in
// nsIContentPermissionRequest, but because webrtc uses their own prompting
// system, we should manually apply the delegate policy here. Permission
// should be delegated using Feature Policy and top principal
const permDelegateHandler =
aContentWindow.document.permDelegateHandler.QueryInterface(
Ci.nsIPermissionDelegateHandler
);
let secondOrigin = undefined;
if (permDelegateHandler.maybeUnsafePermissionDelegate(requestTypes)) {
// We are going to prompt both first party and third party origin.
// SecondOrigin should be third party
secondOrigin = aContentWindow.document.nodePrincipal.origin;
}
let request = {
callID: aCallID,
windowID: aWindowID,
secondOrigin,
documentURI: aContentWindow.document.documentURI,
secure: aSecure,
isHandlingUserInput: aIsHandlingUserInput,
requestTypes,
sharingScreen,
sharingAudio,
audioInputDevices,
videoInputDevices,
audioOutputDevices,
hasInherentAudioConstraints,
hasInherentVideoConstraints,
audioOutputId: aAudioOutputOptions.deviceId,
};
let actor = getActorForWindow(aContentWindow);
if (actor) {
actor.sendAsyncMessage("webrtc:Request", request);
}
}
function denyGUMRequest(aData) {
let subject;
if (aData.noOSPermission) {
subject = "getUserMedia:response:noOSPermission";
} else {
subject = "getUserMedia:response:deny";
}
Services.obs.notifyObservers(null, subject, aData.callID);
if (!aData.windowID) {
return;
}
let contentWindow = Services.wm.getOuterWindowWithId(aData.windowID);
if (contentWindow.pendingGetUserMediaRequests) {
forgetGUMRequest(contentWindow, aData.callID);
}
}
function forgetGUMRequest(aContentWindow, aCallID) {
aContentWindow.pendingGetUserMediaRequests.delete(aCallID);
forgetPendingListsEventually(aContentWindow);
}
function forgetPCRequest(aContentWindow, aCallID) {
aContentWindow.pendingPeerConnectionRequests.delete(aCallID);
forgetPendingListsEventually(aContentWindow);
}
function setupPendingListsInitially(aContentWindow) {
if (aContentWindow.pendingGetUserMediaRequests) {
return;
}
aContentWindow.pendingGetUserMediaRequests = new Map();
aContentWindow.pendingPeerConnectionRequests = new Set();
aContentWindow.addEventListener("unload", WebRTCChild.handleEvent);
}
function forgetPendingListsEventually(aContentWindow) {
if (
aContentWindow.pendingGetUserMediaRequests.size ||
aContentWindow.pendingPeerConnectionRequests.size
) {
return;
}
aContentWindow.pendingGetUserMediaRequests = null;
aContentWindow.pendingPeerConnectionRequests = null;
aContentWindow.removeEventListener("unload", WebRTCChild.handleEvent);
}
function updateIndicators(aSubject) {
if (
aSubject instanceof Ci.nsIPropertyBag &&
aSubject.getProperty("requestURL") == kBrowserURL
) {
// Ignore notifications caused by the browser UI showing previews.
return;
}
let contentWindow = aSubject.getProperty("window");
let actor = contentWindow ? getActorForWindow(contentWindow) : null;
if (actor) {
let tabState = getTabStateForContentWindow(contentWindow, false);
tabState.windowId = getInnerWindowIDForWindow(contentWindow);
// If we were silencing DOM notifications before, but we've updated
// state such that we're no longer sharing one of our displays, then
// reset the silencing state.
if (actor.suppressNotifications) {
if (!tabState.screen && !tabState.window && !tabState.browser) {
actor.suppressNotifications = false;
}
}
tabState.suppressNotifications = actor.suppressNotifications;
actor.sendAsyncMessage("webrtc:UpdateIndicators", tabState);
}
}
function removeBrowserSpecificIndicator(aSubject, aTopic, aData) {
let contentWindow = Services.wm.getOuterWindowWithId(aData);
if (contentWindow.document.documentURI == kBrowserURL) {
// Ignore notifications caused by the browser UI showing previews.
return;
}
let tabState = getTabStateForContentWindow(contentWindow, true);
tabState.windowId = aData;
let actor = getActorForWindow(contentWindow);
if (actor) {
actor.sendAsyncMessage("webrtc:UpdateIndicators", tabState);
}
}
function getTabStateForContentWindow(aContentWindow, aForRemove = false) {
let camera = {},
microphone = {},
screen = {},
window = {},
browser = {},
devices = {};
lazy.MediaManagerService.mediaCaptureWindowState(
aContentWindow,
camera,
microphone,
screen,
window,
browser,
devices
);
if (
camera.value == lazy.MediaManagerService.STATE_NOCAPTURE &&
microphone.value == lazy.MediaManagerService.STATE_NOCAPTURE &&
screen.value == lazy.MediaManagerService.STATE_NOCAPTURE &&
window.value == lazy.MediaManagerService.STATE_NOCAPTURE &&
browser.value == lazy.MediaManagerService.STATE_NOCAPTURE
) {
return { remove: true };
}
if (aForRemove) {
return { remove: true };
}
let serializedDevices = [];
if (Array.isArray(devices.value)) {
serializedDevices = devices.value.map(device => {
return {
type: device.type,
mediaSource: device.mediaSource,
rawId: device.rawId,
scary: device.scary,
};
});
}
return {
camera: camera.value,
microphone: microphone.value,
screen: screen.value,
window: window.value,
browser: browser.value,
devices: serializedDevices,
};
}
function getInnerWindowIDForWindow(aContentWindow) {
return aContentWindow.windowGlobalChild.innerWindowId;
}