Bug 1601301, rework webrtc permission granting UI around JSWindowActor, performing the work of determining the ultimate state for the tab icon and menu in the parent rather than the content process, r=johannh

Differential Revision: https://phabricator.services.mozilla.com/D56570

--HG--
rename : browser/modules/webrtcUI.jsm => browser/actors/WebRTCParent.jsm
extra : moz-landing-system : lando
This commit is contained in:
Neil Deakin 2020-01-10 15:09:59 +00:00
parent f4cc73018c
commit 16f95497b9
12 changed files with 1593 additions and 1412 deletions

View File

@ -10,9 +10,6 @@ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { ActorChild } = ChromeUtils.import(
"resource://gre/modules/ActorChild.jsm"
);
const { AppConstants } = ChromeUtils.import(
"resource://gre/modules/AppConstants.jsm"
);
@ -25,16 +22,18 @@ XPCOMUtils.defineLazyServiceGetter(
const kBrowserURL = AppConstants.BROWSER_CHROME_URL;
class WebRTCChild extends ActorChild {
class WebRTCChild extends JSWindowActorChild {
// Called only for 'unload' to remove pending gUM prompts in reloaded frames.
static handleEvent(aEvent) {
let contentWindow = aEvent.target.defaultView;
let mm = getMessageManagerForWindow(contentWindow);
for (let key of contentWindow.pendingGetUserMediaRequests.keys()) {
mm.sendAsyncMessage("webrtc:CancelRequest", key);
}
for (let key of contentWindow.pendingPeerConnectionRequests.keys()) {
mm.sendAsyncMessage("rtcpeer:CancelRequest", key);
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);
}
}
}
@ -112,6 +111,19 @@ class WebRTCChild extends ActorChild {
}
}
function getActorForWindow(window) {
let windowGlobal = window.getWindowGlobalChild();
try {
if (windowGlobal) {
return windowGlobal.getActor("WebRTC");
}
} catch (ex) {
// There might not be an actor for a parent process chrome URL.
}
return null;
}
function handlePCRequest(aSubject, aTopic, aData) {
let { windowID, innerWindowID, callID, isSecure } = aSubject;
let contentWindow = Services.wm.getOuterWindowWithId(windowID);
@ -140,7 +152,11 @@ function handlePCRequest(aSubject, aTopic, aData) {
documentURI: contentWindow.document.documentURI,
secure: isSecure,
};
mm.sendAsyncMessage("rtcpeer:Request", request);
let actor = getActorForWindow(contentWindow);
if (actor) {
actor.sendAsyncMessage("rtcpeer:Request", request);
}
}
function handleGUMStop(aSubject, aTopic, aData) {
@ -152,9 +168,9 @@ function handleGUMStop(aSubject, aTopic, aData) {
mediaSource: aSubject.mediaSource,
};
let mm = getMessageManagerForWindow(contentWindow);
if (mm) {
mm.sendAsyncMessage("webrtc:StopRecording", request);
let actor = getActorForWindow(contentWindow);
if (actor) {
actor.sendAsyncMessage("webrtc:StopRecording", request);
}
}
@ -270,11 +286,6 @@ function prompt(
}
aContentWindow.pendingGetUserMediaRequests.set(aCallID, devices);
// Record third party origins for telemetry.
let isThirdPartyOrigin =
aContentWindow.document.location.origin !=
aContentWindow.top.document.location.origin;
// 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
@ -295,10 +306,6 @@ function prompt(
const shouldDelegatePermission =
permDelegateHandler.permissionDelegateFPEnabled;
const origin = shouldDelegatePermission
? aContentWindow.top.document.nodePrincipal.origin
: aContentWindow.document.nodePrincipal.origin;
let secondOrigin = undefined;
if (
shouldDelegatePermission &&
@ -312,12 +319,10 @@ function prompt(
let request = {
callID: aCallID,
windowID: aWindowID,
origin,
secondOrigin,
documentURI: aContentWindow.document.documentURI,
secure: aSecure,
isHandlingUserInput: aIsHandlingUserInput,
isThirdPartyOrigin,
shouldDelegatePermission,
requestTypes,
sharingScreen,
@ -326,8 +331,10 @@ function prompt(
videoDevices,
};
let mm = getMessageManagerForWindow(aContentWindow);
mm.sendAsyncMessage("webrtc:Request", request);
let actor = getActorForWindow(aContentWindow);
if (actor) {
actor.sendAsyncMessage("webrtc:Request", request);
}
}
function denyGUMRequest(aData) {
@ -388,91 +395,35 @@ function updateIndicators(aSubject, aTopic, aData) {
return;
}
let contentWindowArray = MediaManagerService.activeMediaCaptureWindows;
let count = contentWindowArray.length;
let contentWindow = aSubject.getProperty("window");
let state = {
showGlobalIndicator: count > 0,
showCameraIndicator: false,
showMicrophoneIndicator: false,
showScreenSharingIndicator: "",
};
let actor = contentWindow ? getActorForWindow(contentWindow) : null;
if (actor) {
let tabState = getTabStateForContentWindow(contentWindow, false);
tabState.windowId = getInnerWindowIDForWindow(contentWindow);
Services.cpmm.sendAsyncMessage("webrtc:UpdatingIndicators");
// If several iframes in the same page use media streams, it's possible to
// have the same top level window several times. We use a Set to avoid
// sending duplicate notifications.
let contentWindows = new Set();
for (let i = 0; i < count; ++i) {
contentWindows.add(
contentWindowArray.queryElementAt(i, Ci.nsISupports).top
);
actor.sendAsyncMessage("webrtc:UpdateIndicators", tabState);
}
for (let contentWindow of contentWindows) {
if (contentWindow.document.documentURI == kBrowserURL) {
// There may be a preview shown at the same time as other streams.
continue;
}
let tabState = getTabStateForContentWindow(contentWindow);
if (
tabState.camera == MediaManagerService.STATE_CAPTURE_ENABLED ||
tabState.camera == MediaManagerService.STATE_CAPTURE_DISABLED
) {
state.showCameraIndicator = true;
}
if (
tabState.microphone == MediaManagerService.STATE_CAPTURE_ENABLED ||
tabState.microphone == MediaManagerService.STATE_CAPTURE_DISABLED
) {
state.showMicrophoneIndicator = true;
}
if (tabState.screen) {
if (tabState.screen.startsWith("Screen")) {
state.showScreenSharingIndicator = "Screen";
} else if (tabState.screen.startsWith("Window")) {
if (state.showScreenSharingIndicator != "Screen") {
state.showScreenSharingIndicator = "Window";
}
} else if (tabState.screen.startsWith("Browser")) {
if (!state.showScreenSharingIndicator) {
state.showScreenSharingIndicator = "Browser";
}
}
}
let mm = getMessageManagerForWindow(contentWindow);
mm.sendAsyncMessage("webrtc:UpdateBrowserIndicators", tabState);
}
Services.cpmm.sendAsyncMessage("webrtc:UpdateGlobalIndicators", state);
}
function removeBrowserSpecificIndicator(aSubject, aTopic, aData) {
let contentWindow = Services.wm.getOuterWindowWithId(aData).top;
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);
if (
tabState.camera == MediaManagerService.STATE_NOCAPTURE &&
tabState.microphone == MediaManagerService.STATE_NOCAPTURE &&
!tabState.screen
) {
tabState = { windowId: tabState.windowId };
}
let tabState = getTabStateForContentWindow(contentWindow, true);
let mm = getMessageManagerForWindow(contentWindow);
if (mm) {
mm.sendAsyncMessage("webrtc:UpdateBrowserIndicators", tabState);
tabState.windowId = aData;
let actor = getActorForWindow(contentWindow);
if (actor) {
actor.sendAsyncMessage("webrtc:UpdateIndicators", tabState);
}
}
function getTabStateForContentWindow(aContentWindow) {
function getTabStateForContentWindow(aContentWindow, aForRemove = false) {
let camera = {},
microphone = {},
screen = {},
@ -485,55 +436,31 @@ function getTabStateForContentWindow(aContentWindow) {
screen,
window,
browser,
true
false
);
let tabState = { camera: camera.value, microphone: microphone.value };
if (screen.value == MediaManagerService.STATE_CAPTURE_ENABLED) {
tabState.screen = "Screen";
} else if (window.value == MediaManagerService.STATE_CAPTURE_ENABLED) {
tabState.screen = "Window";
} else if (browser.value == MediaManagerService.STATE_CAPTURE_ENABLED) {
tabState.screen = "Browser";
} else if (screen.value == MediaManagerService.STATE_CAPTURE_DISABLED) {
tabState.screen = "ScreenPaused";
} else if (window.value == MediaManagerService.STATE_CAPTURE_DISABLED) {
tabState.screen = "WindowPaused";
} else if (browser.value == MediaManagerService.STATE_CAPTURE_DISABLED) {
tabState.screen = "BrowserPaused";
if (
camera.value == MediaManagerService.STATE_NOCAPTURE &&
microphone.value == MediaManagerService.STATE_NOCAPTURE &&
screen.value == MediaManagerService.STATE_NOCAPTURE &&
window.value == MediaManagerService.STATE_NOCAPTURE &&
browser.value == MediaManagerService.STATE_NOCAPTURE
) {
return {};
}
let screenEnabled = tabState.screen && !tabState.screen.includes("Paused");
let cameraEnabled =
tabState.camera == MediaManagerService.STATE_CAPTURE_ENABLED;
let microphoneEnabled =
tabState.microphone == MediaManagerService.STATE_CAPTURE_ENABLED;
// tabState.sharing controls which global indicator should be shown
// for the tab. It should always be set to the _enabled_ device which
// we consider most intrusive (screen > camera > microphone).
if (screenEnabled) {
tabState.sharing = "screen";
} else if (cameraEnabled) {
tabState.sharing = "camera";
} else if (microphoneEnabled) {
tabState.sharing = "microphone";
} else if (tabState.screen) {
tabState.sharing = "screen";
} else if (tabState.camera) {
tabState.sharing = "camera";
} else if (tabState.microphone) {
tabState.sharing = "microphone";
if (aForRemove) {
return {};
}
// The stream is considered paused when we're sharing something
// but all devices are off or set to disabled.
tabState.paused =
tabState.sharing && !screenEnabled && !cameraEnabled && !microphoneEnabled;
tabState.windowId = getInnerWindowIDForWindow(aContentWindow);
tabState.documentURI = aContentWindow.document.documentURI;
return tabState;
return {
camera: camera.value,
microphone: microphone.value,
screen: screen.value,
window: window.value,
browser: browser.value,
documentURI: aContentWindow.document.documentURI,
};
}
function getInnerWindowIDForWindow(aContentWindow) {

File diff suppressed because it is too large Load Diff

View File

@ -57,4 +57,5 @@ FINAL_TARGET_FILES.actors += [
'SwitchDocumentDirectionChild.jsm',
'URIFixupChild.jsm',
'WebRTCChild.jsm',
'WebRTCParent.jsm',
]

View File

@ -1670,10 +1670,11 @@ var gIdentityHandler = {
}
}
}
browser.messageManager.sendAsyncMessage(
"webrtc:StopSharing",
windowId
);
let bc = this._sharingState.webRTC.browsingContext;
bc.currentWindowGlobal
.getActor("WebRTC")
.sendAsyncMessage("webrtc:StopSharing", windowId);
webrtcUI.forgetActivePermissionsFromBrowser(gBrowser.selectedBrowser);
}
}

View File

@ -6121,7 +6121,6 @@ var TabsProgressListener = {
if (tab && tab._sharingState) {
gBrowser.resetBrowserSharing(aBrowser);
}
webrtcUI.forgetStreamsFromBrowser(aBrowser);
gBrowser.getNotificationBox(aBrowser).removeTransientNotifications();

View File

@ -2457,7 +2457,7 @@
if (aTab._sharingState) {
this.resetBrowserSharing(browser);
}
webrtcUI.forgetStreamsFromBrowser(browser);
webrtcUI.forgetStreamsFromBrowserContext(browser.browsingContext);
// Set browser parameters for when browser is restored. Also remove
// listeners and set up lazy restore data in SessionStore. This must

View File

@ -760,6 +760,7 @@ var gTests = [
// Request devices and expect a prompt despite the saved 'Allow' permission.
observerPromise = expectObserverCalled("getUserMedia:request");
promise = promisePopupNotificationShown("webRTC-shareDevices");
await promiseRequestDevice(false, true, null, "screen");
await promise;
await observerPromise;

View File

@ -1,4 +1,4 @@
browser/base/content/test/webrtc/head.jsconst { AppConstants } = ChromeUtils.import(
const { AppConstants } = ChromeUtils.import(
"resource://gre/modules/AppConstants.jsm"
);
@ -367,45 +367,97 @@ function activateSecondaryAction(aAction) {
}
}
function getMediaCaptureState() {
return SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function() {
let mediaManagerService = Cc[
"@mozilla.org/mediaManagerService;1"
].getService(Ci.nsIMediaManagerService);
async function getMediaCaptureState() {
function gatherBrowsingContexts(aBrowsingContext) {
let list = [aBrowsingContext];
let hasCamera = {};
let hasMicrophone = {};
let hasScreenShare = {};
let hasWindowShare = {};
let hasBrowserShare = {};
mediaManagerService.mediaCaptureWindowState(
content,
hasCamera,
hasMicrophone,
hasScreenShare,
hasWindowShare,
hasBrowserShare,
true
);
let result = {};
if (hasCamera.value != mediaManagerService.STATE_NOCAPTURE) {
result.video = true;
}
if (hasMicrophone.value != mediaManagerService.STATE_NOCAPTURE) {
result.audio = true;
let children = aBrowsingContext.getChildren();
for (let child of children) {
list.push(...gatherBrowsingContexts(child));
}
if (hasScreenShare.value != mediaManagerService.STATE_NOCAPTURE) {
result.screen = "Screen";
} else if (hasWindowShare.value != mediaManagerService.STATE_NOCAPTURE) {
result.screen = "Window";
} else if (hasBrowserShare.value != mediaManagerService.STATE_NOCAPTURE) {
result.screen = "Browser";
}
return list;
}
return result;
});
function combine(x, y) {
if (
x == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED ||
y == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED
) {
return Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED;
}
if (
x == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED ||
y == Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED
) {
return Ci.nsIMediaManagerService.STATE_CAPTURE_DISABLED;
}
return Ci.nsIMediaManagerService.STATE_NOCAPTURE;
}
let video = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
let audio = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
let screen = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
let window = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
let browser = Ci.nsIMediaManagerService.STATE_NOCAPTURE;
for (let bc of gatherBrowsingContexts(
gBrowser.selectedBrowser.browsingContext
)) {
let state = await SpecialPowers.spawn(bc, [], async function() {
let mediaManagerService = Cc[
"@mozilla.org/mediaManagerService;1"
].getService(Ci.nsIMediaManagerService);
let hasCamera = {};
let hasMicrophone = {};
let hasScreenShare = {};
let hasWindowShare = {};
let hasBrowserShare = {};
mediaManagerService.mediaCaptureWindowState(
content,
hasCamera,
hasMicrophone,
hasScreenShare,
hasWindowShare,
hasBrowserShare,
false
);
return {
video: hasCamera.value,
audio: hasMicrophone.value,
screen: hasScreenShare.value,
window: hasWindowShare.value,
browser: hasBrowserShare.value,
};
});
video = combine(state.video, video);
audio = combine(state.audio, audio);
screen = combine(state.screen, screen);
window = combine(state.window, window);
browser = combine(state.browser, browser);
}
let result = {};
if (video != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
result.video = true;
}
if (audio != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
result.audio = true;
}
if (screen != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
result.screen = "Screen";
} else if (window != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
result.screen = "Window";
} else if (browser != Ci.nsIMediaManagerService.STATE_NOCAPTURE) {
result.screen = "Browser";
}
return result;
}
async function stopSharing(aType = "camera", aShouldKeepSharing = false) {

View File

@ -330,6 +330,17 @@ let ACTORS = {
},
},
},
WebRTC: {
parent: {
moduleURI: "resource:///actors/WebRTCParent.jsm",
},
child: {
moduleURI: "resource:///actors/WebRTCChild.jsm",
},
allFrames: true,
},
};
let LEGACY_ACTORS = {
@ -392,19 +403,6 @@ let LEGACY_ACTORS = {
observers: ["keyword-uri-fixup"],
},
},
WebRTC: {
child: {
module: "resource:///actors/WebRTCChild.jsm",
messages: [
"rtcpeer:Allow",
"rtcpeer:Deny",
"webrtc:Allow",
"webrtc:Deny",
"webrtc:StopSharing",
],
},
},
};
(function earlyBlankFirstPaint() {
@ -583,7 +581,6 @@ let initializedModules = {};
],
["ContentSearch", "resource:///modules/ContentSearch.jsm", "init"],
["UpdateListener", "resource://gre/modules/UpdateListener.jsm", "init"],
["webrtcUI", "resource:///modules/webrtcUI.jsm", "init"],
].forEach(([name, resource, init]) => {
XPCOMUtils.defineLazyGetter(this, name, () => {
ChromeUtils.import(resource, initializedModules);
@ -639,9 +636,6 @@ const listeners = {
"AsyncPrefs:SetPref": ["AsyncPrefs"],
"AsyncPrefs:ResetPref": ["AsyncPrefs"],
// PLEASE KEEP THIS LIST IN SYNC WITH THE LISTENERS ADDED IN AsyncPrefs.init
"webrtc:UpdateGlobalIndicators": ["webrtcUI"],
"webrtc:UpdatingIndicators": ["webrtcUI"],
},
mm: {
@ -665,12 +659,6 @@ const listeners = {
ContentSearch: ["ContentSearch"],
"Reader:FaviconRequest": ["ReaderParent"],
"Reader:UpdateReaderButton": ["ReaderParent"],
"rtcpeer:CancelRequest": ["webrtcUI"],
"rtcpeer:Request": ["webrtcUI"],
"webrtc:CancelRequest": ["webrtcUI"],
"webrtc:Request": ["webrtcUI"],
"webrtc:StopRecording": ["webrtcUI"],
"webrtc:UpdateBrowserIndicators": ["webrtcUI"],
},
observe(subject, topic, data) {

View File

@ -87,18 +87,28 @@ class FxrWebRTCPrompt extends FxrPermissionPromptPrototype {
allowedDevices.push(videoDevices[0].deviceIndex);
}
this.targetBrowser.messageManager.sendAsyncMessage("webrtc:Allow", {
callID: this.request.callID,
windowID: this.request.windowID,
devices: allowedDevices,
});
// WebRTCChild doesn't currently care which actor
// this is sent to and just uses the windowID.
this.targetBrowser.sendMessageToActor(
"webrtc:Allow",
{
callID: this.request.callID,
windowID: this.request.windowID,
devices: allowedDevices,
},
"WebRTC"
);
}
deny() {
this.targetBrowser.messageManager.sendAsyncMessage("webrtc:Deny", {
callID: this.request.callID,
windowID: this.request.windowID,
});
this.targetBrowser.sendMessageToActor(
"webrtc:Deny",
{
callID: this.request.callID,
windowID: this.request.windowID,
},
"WebRTC"
);
}
}

View File

@ -45,7 +45,8 @@ function getMessageManagerForWindow(aContentWindow) {
Services.obs.addObserver(gEMEUIObserver, "mediakeys-request");
Services.obs.addObserver(gDecoderDoctorObserver, "decoder-doctor-notification");
// WebRTCChild observer registration.
// WebRTCChild observer registration. Actor observers require the subject
// to be a window, so they are registered here instead.
const kWebRTCObserverTopics = [
"getUserMedia:request",
"recording-device-stopped",

File diff suppressed because it is too large Load Diff