Bug 1270572 - allow un-prompted gUM access if the page has a live track connected to the same device; r=florian, gcp draft
authorMunro Chiang <mchiang@mozilla.com>
Fri, 13 Jan 2017 10:30:02 +0800
changeset 462485 889b1be7057c6b5fb8d7eae0b9fe64aee5db3cd7
parent 460419 b1c31c4a0a678194931779e0f13fba7b508eb109
child 462486 b9f565351005e48fd1316fbe0a17f0c9bde5385f
push id41771
push usermchiang@mozilla.com
push dateTue, 17 Jan 2017 15:35:06 +0000
reviewersflorian, gcp
bugs1270572
milestone53.0a1
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: AuhXym6qtXF
browser/base/content/browser.js
browser/modules/ContentWebRTC.jsm
browser/modules/webrtcUI.jsm
dom/media/GetUserMediaRequest.cpp
dom/media/GetUserMediaRequest.h
dom/media/MediaManager.cpp
dom/webidl/GetUserMediaRequest.webidl
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -4913,16 +4913,17 @@ var TabsProgressListener = {
     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.forgetActivePermissionsFromBrowser(aBrowser);
 
     gBrowser.getNotificationBox(aBrowser).removeTransientNotifications();
 
     FullZoom.onLocationChange(aLocationURI, false, aBrowser);
   },
 }
 
 function nsBrowserAccess() { }
@@ -7469,16 +7470,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._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
@@ -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;
   },
@@ -135,16 +138,20 @@ 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);
   },
 
   showSharingDoorhanger(aActiveStream) {
     let browserWindow = aActiveStream.browser.ownerGlobal;
     if (aActiveStream.tab) {
       browserWindow.gBrowser.selectedTab = aActiveStream.tab;
@@ -258,16 +265,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 +337,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: audioDevices, videoDevices: videoDevices,
        sharingScreen: sharingScreen, sharingAudio: sharingAudio,
        requestTypes: requestTypes} = aRequest;
   let uri = Services.io.newURI(aRequest.documentURI);
   let host = getHost(uri);
   let chromeDoc = aBrowser.ownerDocument;
   let stringBundle = chromeDoc.defaultView.gNavigatorBundle;
@@ -442,31 +467,47 @@ 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;
 
-        // 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 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 || micPerm == perms.ALLOW_ACTION || activeMic) &&
+            (!videoDevices.length || camPerm == perms.ALLOW_ACTION || activeCamera)) {
           let allowedDevices = [];
-          if (videoDevices.length && camPerm == perms.ALLOW_ACTION) {
-            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 && micPerm == perms.ALLOW_ACTION)
-            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);
@@ -649,28 +690,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)
@@ -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;
 };