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: EJ5qvcJwqUO
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -4906,16 +4906,17 @@ var TabsProgressListener = {
// or history.push/pop/replaceState.
if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
// Reader mode actually cares about these:
let mm = gBrowser.selectedBrowser.messageManager;
mm.sendAsyncMessage("Reader:PushState", {isArticle: gBrowser.selectedBrowser.isArticle});
return;
}
+ webrtcUI.forgetActivePermissionsFromBrowser(aBrowser);
// Filter out location changes in sub documents.
if (!aWebProgress.isTopLevel)
return;
// Only need to call locationChange if the PopupNotifications object
// for this window has already been initialized (i.e. its getter no
// longer exists)
if (!Object.getOwnPropertyDescriptor(window, "PopupNotifications").get)
@@ -7484,16 +7485,17 @@ var gIdentityHandler = {
if (this._sharingState[id] &&
SitePermissions.get(uri, id) == SitePermissions.ALLOW)
SitePermissions.remove(uri, id);
}
}
}
let mm = gBrowser.selectedBrowser.messageManager;
mm.sendAsyncMessage("webrtc:StopSharing", windowId);
+ webrtcUI.forgetActivePermissionsFromBrowser(gBrowser.selectedBrowser);
}
SitePermissions.remove(gBrowser.currentURI, aPermission.id);
this._permissionJustRemoved = true;
this.updatePermissionHint();
// 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
@@ -38,16 +38,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"]
@@ -55,26 +56,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;
},
@@ -139,16 +142,22 @@ this.webrtcUI = {
stream.browser = aNewBrowser;
}
},
forgetStreamsFromBrowser(aBrowser) {
this._streams = this._streams.filter(stream => stream.browser != aBrowser);
},
+ forgetActivePermissionsFromBrowser(aBrowser) {
+ if (webrtcUI.activePerms.has(aBrowser.outerWindowID)) {
+ webrtcUI.activePerms.delete(aBrowser.outerWindowID);
+ }
+ },
+
showSharingDoorhanger(aActiveStream) {
let browserWindow = aActiveStream.browser.ownerGlobal;
if (aActiveStream.tab) {
browserWindow.gBrowser.selectedTab = aActiveStream.tab;
} else {
aActiveStream.browser.focus();
}
browserWindow.focus();
@@ -258,16 +267,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);
@@ -327,16 +339,30 @@ 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) {
+ if (webrtcUI.activePerms.has(aBrowser.outerWindowID)) {
+ if (!aRequest.rawID) {
+ webrtcUI.activePerms.delete(aBrowser.outerWindowID);
+ } else {
+ let set = webrtcUI.activePerms.get(aBrowser.outerWindowID);
+ if (set.has(aRequest.windowID + aRequest.mediaSource + aRequest.rawID)) {
+ webrtcUI.activePerms.get(aBrowser.outerWindowID).
+ delete(aRequest.windowID + aRequest.mediaSource + aRequest.rawID);
+ }
+ }
+ }
+}
+
function prompt(aBrowser, aRequest) {
let {audioDevices: audioDevices, videoDevices: videoDevices,
sharingScreen: sharingScreen, sharingAudio: sharingAudio,
requestTypes: requestTypes} = aRequest;
let uri = Services.io.newURI(aRequest.documentURI, null, null);
let host = getHost(uri);
let chromeDoc = aBrowser.ownerDocument;
let stringBundle = chromeDoc.defaultView.gNavigatorBundle;
@@ -442,31 +468,69 @@ function prompt(aBrowser, aRequest) {
if (camPerm == perms.PROMPT_ACTION)
camPerm = perms.UNKNOWN_ACTION;
// Screen sharing shouldn't follow the camera permissions.
if (videoDevices.length && sharingScreen)
camPerm = perms.UNKNOWN_ACTION;
+ let tmpCamPerm = perms.UNKNOWN_ACTION;
+ let tmpMicPerm = perms.UNKNOWN_ACTION;
+ let allowedTmpDevices = [];
+
+ for (let device of videoDevices) {
+ if (webrtcUI.activePerms.has(aBrowser.outerWindowID)) {
+ if (webrtcUI.activePerms.get(aBrowser.outerWindowID).
+ has(aRequest.windowID + device.mediaSource + device.id)) {
+ tmpCamPerm = perms.ALLOW_ACTION;
+ allowedTmpDevices.push(device.deviceIndex);
+ break;
+ }
+ }
+ }
+
+ for (let device of audioDevices) {
+ if (webrtcUI.activePerms.has(aBrowser.outerWindowID)) {
+ if (webrtcUI.activePerms.get(aBrowser.outerWindowID).
+ has(aRequest.windowID + device.mediaSource + device.id)) {
+ tmpMicPerm = perms.ALLOW_ACTION;
+ allowedTmpDevices.push(device.deviceIndex);
+ break;
+ }
+ }
+ }
+
// We don't check that permissions are set to ALLOW_ACTION in this
// test; only that they are set. This is because if audio is allowed
// and video is denied persistently, we don't want to show the prompt,
// and will grant audio access immediately.
- if ((!audioDevices.length || micPerm) && (!videoDevices.length || camPerm)) {
- // All permissions we were about to request are already persistently set.
+ let isAlwaysShare = (!audioDevices.length || micPerm) && (!videoDevices.length || camPerm);
+ let isTemporalShare = (!audioDevices.length || tmpMicPerm) && (!videoDevices.length || tmpCamPerm);
+
+ if ( isAlwaysShare || isTemporalShare ) {
let allowedDevices = [];
- if (videoDevices.length && camPerm == perms.ALLOW_ACTION) {
- allowedDevices.push(videoDevices[0].deviceIndex);
- Services.perms.add(uri, "MediaManagerVideo",
- Services.perms.ALLOW_ACTION,
- Services.perms.EXPIRE_SESSION);
+ if (isAlwaysShare) {
+ // All permissions we were about to request are already persistently set.
+ if (videoDevices.length && camPerm == perms.ALLOW_ACTION) {
+ allowedDevices.push(videoDevices[0].deviceIndex);
+ Services.perms.add(uri, "MediaManagerVideo",
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_SESSION);
+ }
+ if (audioDevices.length && micPerm == perms.ALLOW_ACTION)
+ allowedDevices.push(audioDevices[0].deviceIndex);
+ } else { // isTemporalShare == true
+ allowedDevices = allowedTmpDevices;
+ if (tmpCamPerm) {
+ Services.perms.add(uri, "MediaManagerVideo",
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_SESSION);
+ }
}
- if (audioDevices.length && micPerm == perms.ALLOW_ACTION)
- allowedDevices.push(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);
@@ -649,28 +713,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) {
perms.add(uri, "camera",
allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION);
}
}
if (audioDevices.length) {
if (!sharingAudio) {
let audioDeviceIndex = doc.getElementById("webRTC-selectMicrophone-menulist").value;
let allowMic = audioDeviceIndex != "-1";
- if (allowMic)
+ 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) {
perms.add(uri, "microphone",
allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION);
}
} else {
// Only one device possible for audio capture.
allowedDevices.push(0);
}
--- 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)
@@ -2752,25 +2753,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
@@ -6,11 +6,13 @@
* This is an internal IDL file
*/
[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;
};