Bug 1270572 - allow un-prompted gUM access if the page has a live track connected to the same device; r=florian, gcp
MozReview-Commit-ID: EvATqR4NNTH
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -4929,18 +4929,18 @@ var TabsProgressListener = {
// for this window has already been initialized (i.e. its getter no
// longer exists)
if (!Object.getOwnPropertyDescriptor(window, "PopupNotifications").get)
PopupNotifications.locationChange(aBrowser);
let tab = gBrowser.getTabForBrowser(aBrowser);
if (tab && tab._sharingState) {
gBrowser.setBrowserSharing(aBrowser, {});
- webrtcUI.forgetStreamsFromBrowser(aBrowser);
- }
+ }
+ webrtcUI.forgetStreamsFromBrowser(aBrowser);
gBrowser.getNotificationBox(aBrowser).removeTransientNotifications();
FullZoom.onLocationChange(aLocationURI, false, aBrowser);
},
}
function nsBrowserAccess() { }
@@ -7498,16 +7498,17 @@ var gIdentityHandler = {
perm.scope == SitePermissions.SCOPE_PERSISTENT) {
SitePermissions.remove(uri, id);
}
}
}
}
}
browser.messageManager.sendAsyncMessage("webrtc:StopSharing", windowId);
+ webrtcUI.forgetActivePermissionsFromBrowser(gBrowser.selectedBrowser);
}
SitePermissions.remove(gBrowser.currentURI, aPermission.id, browser);
this._permissionReloadHint.removeAttribute("hidden");
// Set telemetry values for clearing a permission
let histogram = Services.telemetry.getKeyedHistogramById("WEB_PERMISSION_CLEARED");
--- a/browser/modules/ContentWebRTC.jsm
+++ b/browser/modules/ContentWebRTC.jsm
@@ -20,26 +20,28 @@ this.ContentWebRTC = {
_initialized: false,
init() {
if (this._initialized)
return;
this._initialized = true;
Services.obs.addObserver(handleGUMRequest, "getUserMedia:request", false);
+ Services.obs.addObserver(handleGUMStop, "recording-device-stopped", false);
Services.obs.addObserver(handlePCRequest, "PeerConnection:request", false);
Services.obs.addObserver(updateIndicators, "recording-device-events", false);
Services.obs.addObserver(removeBrowserSpecificIndicator, "recording-window-ended", false);
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT)
Services.obs.addObserver(processShutdown, "content-child-shutdown", false);
},
uninit() {
Services.obs.removeObserver(handleGUMRequest, "getUserMedia:request");
+ Services.obs.removeObserver(handleGUMStop, "recording-device-stopped");
Services.obs.removeObserver(handlePCRequest, "PeerConnection:request");
Services.obs.removeObserver(updateIndicators, "recording-device-events");
Services.obs.removeObserver(removeBrowserSpecificIndicator, "recording-window-ended");
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT)
Services.obs.removeObserver(processShutdown, "content-child-shutdown");
this._initialized = false;
@@ -119,16 +121,29 @@ function handlePCRequest(aSubject, aTopi
innerWindowID,
callID,
documentURI: contentWindow.document.documentURI,
secure: isSecure,
};
mm.sendAsyncMessage("rtcpeer:Request", request);
}
+function handleGUMStop(aSubject, aTopic, aData) {
+ let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
+
+ let request = {
+ windowID: aSubject.windowID,
+ rawID: aSubject.rawID,
+ mediaSource: aSubject.mediaSource,
+ };
+
+ let mm = getMessageManagerForWindow(contentWindow);
+ mm.sendAsyncMessage("webrtc:StopRecording", request);
+}
+
function handleGUMRequest(aSubject, aTopic, aData) {
let constraints = aSubject.getConstraints();
let secure = aSubject.isSecure;
let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
contentWindow.navigator.mozGetUserMediaDevices(
constraints,
function(devices) {
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -40,16 +40,17 @@ this.webrtcUI = {
ppmm.addMessageListener("webrtc:UpdateGlobalIndicators", this);
ppmm.addMessageListener("child-process-shutdown", this);
let mm = Cc["@mozilla.org/globalmessagemanager;1"]
.getService(Ci.nsIMessageListenerManager);
mm.addMessageListener("rtcpeer:Request", this);
mm.addMessageListener("rtcpeer:CancelRequest", this);
mm.addMessageListener("webrtc:Request", this);
+ mm.addMessageListener("webrtc:StopRecording", this);
mm.addMessageListener("webrtc:CancelRequest", this);
mm.addMessageListener("webrtc:UpdateBrowserIndicators", this);
},
uninit() {
Services.obs.removeObserver(maybeAddMenuIndicator, "browser-delayed-startup-finished");
let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
@@ -57,26 +58,28 @@ this.webrtcUI = {
ppmm.removeMessageListener("webrtc:UpdatingIndicators", this);
ppmm.removeMessageListener("webrtc:UpdateGlobalIndicators", this);
let mm = Cc["@mozilla.org/globalmessagemanager;1"]
.getService(Ci.nsIMessageListenerManager);
mm.removeMessageListener("rtcpeer:Request", this);
mm.removeMessageListener("rtcpeer:CancelRequest", this);
mm.removeMessageListener("webrtc:Request", this);
+ mm.removeMessageListener("webrtc:StopRecording");
mm.removeMessageListener("webrtc:CancelRequest", this);
mm.removeMessageListener("webrtc:UpdateBrowserIndicators", this);
if (gIndicatorWindow) {
gIndicatorWindow.close();
gIndicatorWindow = null;
}
},
processIndicators: new Map(),
+ activePerms: new Map(),
get showGlobalIndicator() {
for (let [, indicators] of this.processIndicators) {
if (indicators.showGlobalIndicator)
return true;
}
return false;
},
@@ -137,18 +140,23 @@ this.webrtcUI = {
swapBrowserForNotification(aOldBrowser, aNewBrowser) {
for (let stream of this._streams) {
if (stream.browser == aOldBrowser)
stream.browser = aNewBrowser;
}
},
+ forgetActivePermissionsFromBrowser(aBrowser) {
+ webrtcUI.activePerms.delete(aBrowser.outerWindowID);
+ },
+
forgetStreamsFromBrowser(aBrowser) {
this._streams = this._streams.filter(stream => stream.browser != aBrowser);
+ webrtcUI.forgetActivePermissionsFromBrowser(aBrowser);
},
showSharingDoorhanger(aActiveStream) {
let browserWindow = aActiveStream.browser.ownerGlobal;
if (aActiveStream.tab) {
browserWindow.gBrowser.selectedTab = aActiveStream.tab;
} else {
aActiveStream.browser.focus();
@@ -260,16 +268,19 @@ this.webrtcUI = {
callID: aMessage.data
});
this.emitter.emit("peer-request-cancel", params);
break;
}
case "webrtc:Request":
prompt(aMessage.target, aMessage.data);
break;
+ case "webrtc:StopRecording":
+ stopRecording(aMessage.target, aMessage.data);
+ break;
case "webrtc:CancelRequest":
removePrompt(aMessage.target, aMessage.data);
break;
case "webrtc:UpdatingIndicators":
webrtcUI._streams = [];
break;
case "webrtc:UpdateGlobalIndicators":
updateIndicators(aMessage.data, aMessage.target);
@@ -329,16 +340,31 @@ function getHost(uri, href) {
const kBundleURI = "chrome://browser/locale/browser.properties";
let bundle = Services.strings.createBundle(kBundleURI);
host = bundle.GetStringFromName("getUserMedia.sharingMenuUnknownHost");
}
}
return host;
}
+function stopRecording(aBrowser, aRequest) {
+ let outerWindowID = aBrowser.outerWindowID;
+
+ if (!webrtcUI.activePerms.has(outerWindowID)) {
+ return;
+ }
+
+ if (!aRequest.rawID) {
+ webrtcUI.activePerms.delete(outerWindowID);
+ } else {
+ let set = webrtcUI.activePerms.get(outerWindowID);
+ set.delete(aRequest.windowID + aRequest.mediaSource + aRequest.rawID);
+ }
+}
+
function prompt(aBrowser, aRequest) {
let { audioDevices, videoDevices, sharingScreen, sharingAudio,
requestTypes } = aRequest;
// If the user has already denied access once in this tab,
// deny again without even showing the notification icon.
if ((audioDevices.length && SitePermissions
.get(null, "microphone", aBrowser).state == SitePermissions.BLOCK) ||
@@ -471,28 +497,47 @@ function prompt(aBrowser, aRequest) {
if (mediaManagerPerm) {
perms.remove(uri, "MediaManagerVideo");
}
// Screen sharing shouldn't follow the camera permissions.
if (videoDevices.length && sharingScreen)
camAllowed = false;
- if ((!audioDevices.length || micAllowed) &&
- (!videoDevices.length || camAllowed)) {
- // All permissions we were about to request are already persistently set.
+ let activeCamera;
+ let activeMic;
+
+ for (let device of videoDevices) {
+ let set = webrtcUI.activePerms.get(aBrowser.outerWindowID);
+ if (set && set.has(aRequest.windowID + device.mediaSource + device.id)) {
+ activeCamera = device;
+ break;
+ }
+ }
+
+ for (let device of audioDevices) {
+ let set = webrtcUI.activePerms.get(aBrowser.outerWindowID);
+ if (set && set.has(aRequest.windowID + device.mediaSource + device.id)) {
+ activeMic = device;
+ break;
+ }
+ }
+
+ if ((!audioDevices.length || micAllowed || activeMic) &&
+ (!videoDevices.length || camAllowed || activeCamera)) {
let allowedDevices = [];
- if (videoDevices.length && camAllowed) {
- allowedDevices.push(videoDevices[0].deviceIndex);
+ if (videoDevices.length) {
+ allowedDevices.push((activeCamera || videoDevices[0]).deviceIndex);
Services.perms.add(uri, "MediaManagerVideo",
Services.perms.ALLOW_ACTION,
Services.perms.EXPIRE_SESSION);
}
- if (audioDevices.length && micAllowed)
- allowedDevices.push(audioDevices[0].deviceIndex);
+ if (audioDevices.length) {
+ allowedDevices.push((activeMic || audioDevices[0]).deviceIndex);
+ }
// Remember on which URIs we found persistent permissions so that we
// can remove them if the user clicks 'Stop Sharing'. There's no
// other way for the stop sharing code to know the hostnames of frames
// using devices until bug 1066082 is fixed.
let browser = this.browser;
browser._devicePermissionURIs = browser._devicePermissionURIs || [];
browser._devicePermissionURIs.push(uri);
@@ -675,29 +720,51 @@ function prompt(aBrowser, aRequest) {
let videoDeviceIndex = doc.getElementById(listId).value;
let allowCamera = videoDeviceIndex != "-1";
if (allowCamera) {
allowedDevices.push(videoDeviceIndex);
// Session permission will be removed after use
// (it's really one-shot, not for the entire session)
perms.add(uri, "MediaManagerVideo", perms.ALLOW_ACTION,
perms.EXPIRE_SESSION);
+ if (!webrtcUI.activePerms.has(aBrowser.outerWindowID)) {
+ webrtcUI.activePerms.set(aBrowser.outerWindowID, new Set());
+ }
+
+ for (let device of videoDevices) {
+ if (device.deviceIndex == videoDeviceIndex) {
+ webrtcUI.activePerms.get(aBrowser.outerWindowID)
+ .add(aRequest.windowID + device.mediaSource + device.id);
+ break;
+ }
+ }
if (remember)
SitePermissions.set(uri, "camera", SitePermissions.ALLOW);
} else {
let scope = remember ? SitePermissions.SCOPE_PERSISTENT : SitePermissions.SCOPE_TEMPORARY;
SitePermissions.set(uri, "camera", SitePermissions.BLOCK, scope, aBrowser);
}
}
if (audioDevices.length) {
if (!sharingAudio) {
let audioDeviceIndex = doc.getElementById("webRTC-selectMicrophone-menulist").value;
let allowMic = audioDeviceIndex != "-1";
if (allowMic) {
allowedDevices.push(audioDeviceIndex);
+ if (!webrtcUI.activePerms.has(aBrowser.outerWindowID)) {
+ webrtcUI.activePerms.set(aBrowser.outerWindowID, new Set());
+ }
+
+ for (let device of audioDevices) {
+ if (device.deviceIndex == audioDeviceIndex) {
+ webrtcUI.activePerms.get(aBrowser.outerWindowID)
+ .add(aRequest.windowID + device.mediaSource + device.id);
+ break;
+ }
+ }
if (remember)
SitePermissions.set(uri, "microphone", SitePermissions.ALLOW);
} else {
let scope = remember ? SitePermissions.SCOPE_PERSISTENT : SitePermissions.SCOPE_TEMPORARY;
SitePermissions.set(uri, "microphone", SitePermissions.BLOCK, scope, aBrowser);
}
} else {
// Only one device possible for audio capture.
--- a/dom/media/GetUserMediaRequest.cpp
+++ b/dom/media/GetUserMediaRequest.cpp
@@ -20,16 +20,28 @@ GetUserMediaRequest::GetUserMediaRequest
: mInnerWindowID(aInnerWindow->WindowID())
, mOuterWindowID(aInnerWindow->GetOuterWindow()->WindowID())
, mCallID(aCallID)
, mConstraints(new MediaStreamConstraints(aConstraints))
, mIsSecure(aIsSecure)
{
}
+GetUserMediaRequest::GetUserMediaRequest(
+ nsPIDOMWindowInner* aInnerWindow,
+ const nsAString& aRawId,
+ const nsAString& aMediaSource)
+ : mRawID(aRawId)
+ , mMediaSource(aMediaSource)
+{
+ if (aInnerWindow && aInnerWindow->GetOuterWindow()) {
+ mOuterWindowID = aInnerWindow->GetOuterWindow()->WindowID();
+ }
+}
+
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(GetUserMediaRequest)
NS_IMPL_CYCLE_COLLECTING_ADDREF(GetUserMediaRequest)
NS_IMPL_CYCLE_COLLECTING_RELEASE(GetUserMediaRequest)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GetUserMediaRequest)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
@@ -44,16 +56,26 @@ nsISupports* GetUserMediaRequest::GetPar
return nullptr;
}
void GetUserMediaRequest::GetCallID(nsString& retval)
{
retval = mCallID;
}
+void GetUserMediaRequest::GetRawID(nsString& retval)
+{
+ retval = mRawID;
+}
+
+void GetUserMediaRequest::GetMediaSource(nsString& retval)
+{
+ retval = mMediaSource;
+}
+
uint64_t GetUserMediaRequest::WindowID()
{
return mOuterWindowID;
}
uint64_t GetUserMediaRequest::InnerWindowID()
{
return mInnerWindowID;
--- a/dom/media/GetUserMediaRequest.h
+++ b/dom/media/GetUserMediaRequest.h
@@ -19,34 +19,41 @@ struct MediaStreamConstraints;
class GetUserMediaRequest : public nsISupports, public nsWrapperCache
{
public:
GetUserMediaRequest(nsPIDOMWindowInner* aInnerWindow,
const nsAString& aCallID,
const MediaStreamConstraints& aConstraints,
bool aIsSecure);
+ GetUserMediaRequest(nsPIDOMWindowInner* aInnerWindow,
+ const nsAString& aRawId,
+ const nsAString& aMediaSource);
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(GetUserMediaRequest)
JSObject* WrapObject(JSContext* cx, JS::Handle<JSObject*> aGivenProto) override;
nsISupports* GetParentObject();
uint64_t WindowID();
uint64_t InnerWindowID();
bool IsSecure();
void GetCallID(nsString& retval);
+ void GetRawID(nsString& retval);
+ void GetMediaSource(nsString& retval);
void GetConstraints(MediaStreamConstraints &result);
private:
virtual ~GetUserMediaRequest() {}
uint64_t mInnerWindowID, mOuterWindowID;
const nsString mCallID;
+ const nsString mRawID;
+ const nsString mMediaSource;
nsAutoPtr<MediaStreamConstraints> mConstraints;
bool mIsSecure;
};
} // namespace dom
} // namespace mozilla
#endif // GetUserMediaRequest_h__
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -170,16 +170,17 @@ HostIsHttps(nsIURI &docURI)
/**
* This class is an implementation of MediaStreamListener. This is used
* to Start() and Stop() the underlying MediaEngineSource when MediaStreams
* are assigned and deassigned in content.
*/
class GetUserMediaCallbackMediaStreamListener : public MediaStreamListener
{
+ friend MediaManager;
public:
// Create in an inactive state
GetUserMediaCallbackMediaStreamListener(base::Thread *aThread,
uint64_t aWindowID,
const PrincipalHandle& aPrincipalHandle)
: mMediaThread(aThread)
, mMainThreadCheck(nullptr)
, mWindowID(aWindowID)
@@ -2744,25 +2745,107 @@ MediaManager::RemoveWindowID(uint64_t aW
}
void
MediaManager::RemoveFromWindowList(uint64_t aWindowID,
GetUserMediaCallbackMediaStreamListener *aListener)
{
MOZ_ASSERT(NS_IsMainThread());
+ nsString videoRawId;
+ nsString audioRawId;
+ nsString videoSourceType;
+ nsString audioSourceType;
+ bool hasVideoDevice = aListener->mVideoDevice;
+ bool hasAudioDevice = aListener->mAudioDevice;
+
+ if (hasVideoDevice) {
+ aListener->mVideoDevice->GetRawId(videoRawId);
+ aListener->mVideoDevice->GetMediaSource(videoSourceType);
+ }
+ if (hasAudioDevice) {
+ aListener->mAudioDevice->GetRawId(audioRawId);
+ aListener->mAudioDevice->GetMediaSource(audioSourceType);
+ }
+
// This is defined as safe on an inactive GUMCMSListener
aListener->Remove(); // really queues the remove
StreamListeners* listeners = GetWindowListeners(aWindowID);
if (!listeners) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ auto* globalWindow = nsGlobalWindow::GetInnerWindowWithId(aWindowID);
+ RefPtr<nsPIDOMWindowInner> window = globalWindow ? globalWindow->AsInner()
+ : nullptr;
+ if (window != nullptr) {
+ RefPtr<GetUserMediaRequest> req =
+ new GetUserMediaRequest(window, NullString(), NullString());
+ obs->NotifyObservers(req, "recording-device-stopped", nullptr);
+ }
return;
}
listeners->RemoveElement(aListener);
- if (listeners->Length() == 0) {
+
+ uint32_t length = listeners->Length();
+
+ if (hasVideoDevice) {
+ bool revokeVideoPermission = true;
+
+ for (uint32_t i = 0; i < length; ++i) {
+ RefPtr<GetUserMediaCallbackMediaStreamListener> listener =
+ listeners->ElementAt(i);
+ if (hasVideoDevice && listener->mVideoDevice) {
+ nsString rawId;
+ listener->mVideoDevice->GetRawId(rawId);
+ if (videoRawId.Equals(rawId)) {
+ revokeVideoPermission = false;
+ break;
+ }
+ }
+ }
+
+ if (revokeVideoPermission) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ auto* globalWindow = nsGlobalWindow::GetInnerWindowWithId(aWindowID);
+ RefPtr<nsPIDOMWindowInner> window = globalWindow ? globalWindow->AsInner()
+ : nullptr;
+ RefPtr<GetUserMediaRequest> req =
+ new GetUserMediaRequest(window, videoRawId, videoSourceType);
+ obs->NotifyObservers(req, "recording-device-stopped", nullptr);
+ }
+ }
+
+ if (hasAudioDevice) {
+ bool revokeAudioPermission = true;
+
+ for (uint32_t i = 0; i < length; ++i) {
+ RefPtr<GetUserMediaCallbackMediaStreamListener> listener =
+ listeners->ElementAt(i);
+ if (hasAudioDevice && listener->mAudioDevice) {
+ nsString rawId;
+ listener->mAudioDevice->GetRawId(rawId);
+ if (audioRawId.Equals(rawId)) {
+ revokeAudioPermission = false;
+ break;
+ }
+ }
+ }
+
+ if (revokeAudioPermission) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ auto* globalWindow = nsGlobalWindow::GetInnerWindowWithId(aWindowID);
+ RefPtr<nsPIDOMWindowInner> window = globalWindow ? globalWindow->AsInner()
+ : nullptr;
+ RefPtr<GetUserMediaRequest> req =
+ new GetUserMediaRequest(window, audioRawId, audioSourceType);
+ obs->NotifyObservers(req, "recording-device-stopped", nullptr);
+ }
+ }
+
+ if (length == 0) {
RemoveWindowID(aWindowID);
// listeners has been deleted here
}
}
void
MediaManager::GetPref(nsIPrefBranch *aBranch, const char *aPref,
const char *aData, int32_t *aVal)
--- a/dom/webidl/GetUserMediaRequest.webidl
+++ b/dom/webidl/GetUserMediaRequest.webidl
@@ -1,16 +1,25 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/.
*
* This is an internal IDL file
*/
+// for gUM request start (getUserMedia:request) notification,
+// rawID and mediaSource won't be set.
+// for gUM request stop (recording-device-stopped) notification due to page reload,
+// only windowID will be set.
+// for gUM request stop (recording-device-stopped) notification due to track stop,
+// only windowID, rawID and mediaSource will be set
+
[NoInterfaceObject]
interface GetUserMediaRequest {
readonly attribute unsigned long long windowID;
readonly attribute unsigned long long innerWindowID;
readonly attribute DOMString callID;
+ readonly attribute DOMString rawID;
+ readonly attribute DOMString mediaSource;
MediaStreamConstraints getConstraints();
readonly attribute boolean isSecure;
};