Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 this.EXPORTED_SYMBOLS = ["webrtcUI"];
9 const Cu = Components.utils;
10 const Cc = Components.classes;
11 const Ci = Components.interfaces;
13 Cu.import("resource://gre/modules/Services.jsm");
14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
16 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
17 "resource://gre/modules/PluralForm.jsm");
19 XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
20 "@mozilla.org/mediaManagerService;1",
21 "nsIMediaManagerService");
23 this.webrtcUI = {
24 init: function () {
25 Services.obs.addObserver(handleRequest, "getUserMedia:request", false);
26 Services.obs.addObserver(updateIndicators, "recording-device-events", false);
27 Services.obs.addObserver(removeBrowserSpecificIndicator, "recording-window-ended", false);
28 },
30 uninit: function () {
31 Services.obs.removeObserver(handleRequest, "getUserMedia:request");
32 Services.obs.removeObserver(updateIndicators, "recording-device-events");
33 Services.obs.removeObserver(removeBrowserSpecificIndicator, "recording-window-ended");
34 },
36 showGlobalIndicator: false,
38 get activeStreams() {
39 let contentWindowSupportsArray = MediaManagerService.activeMediaCaptureWindows;
40 let count = contentWindowSupportsArray.Count();
41 let activeStreams = [];
42 for (let i = 0; i < count; i++) {
43 let contentWindow = contentWindowSupportsArray.GetElementAt(i);
44 let browser = getBrowserForWindow(contentWindow);
45 let browserWindow = browser.ownerDocument.defaultView;
46 let tab = browserWindow.gBrowser &&
47 browserWindow.gBrowser._getTabForContentWindow(contentWindow.top);
48 activeStreams.push({
49 uri: contentWindow.location.href,
50 tab: tab,
51 browser: browser
52 });
53 }
54 return activeStreams;
55 }
56 }
58 function getBrowserForWindowId(aWindowID) {
59 return getBrowserForWindow(Services.wm.getOuterWindowWithId(aWindowID));
60 }
62 function getBrowserForWindow(aContentWindow) {
63 return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
64 .getInterface(Ci.nsIWebNavigation)
65 .QueryInterface(Ci.nsIDocShell)
66 .chromeEventHandler;
67 }
69 function handleRequest(aSubject, aTopic, aData) {
70 let constraints = aSubject.getConstraints();
71 let secure = aSubject.isSecure;
72 let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID);
74 contentWindow.navigator.mozGetUserMediaDevices(
75 constraints,
76 function (devices) {
77 prompt(contentWindow, aSubject.callID, constraints.audio,
78 constraints.video || constraints.picture, devices, secure);
79 },
80 function (error) {
81 // bug 827146 -- In the future, the UI should catch NO_DEVICES_FOUND
82 // and allow the user to plug in a device, instead of immediately failing.
83 denyRequest(aSubject.callID, error);
84 },
85 aSubject.innerWindowID);
86 }
88 function denyRequest(aCallID, aError) {
89 let msg = null;
90 if (aError) {
91 msg = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
92 msg.data = aError;
93 }
94 Services.obs.notifyObservers(msg, "getUserMedia:response:deny", aCallID);
95 }
97 function prompt(aContentWindow, aCallID, aAudioRequested, aVideoRequested, aDevices, aSecure) {
98 let audioDevices = [];
99 let videoDevices = [];
100 for (let device of aDevices) {
101 device = device.QueryInterface(Ci.nsIMediaDevice);
102 switch (device.type) {
103 case "audio":
104 if (aAudioRequested)
105 audioDevices.push(device);
106 break;
107 case "video":
108 if (aVideoRequested)
109 videoDevices.push(device);
110 break;
111 }
112 }
114 let requestType;
115 if (audioDevices.length && videoDevices.length)
116 requestType = "CameraAndMicrophone";
117 else if (audioDevices.length)
118 requestType = "Microphone";
119 else if (videoDevices.length)
120 requestType = "Camera";
121 else {
122 denyRequest(aCallID, "NO_DEVICES_FOUND");
123 return;
124 }
126 let uri = aContentWindow.document.documentURIObject;
127 let browser = getBrowserForWindow(aContentWindow);
128 let chromeDoc = browser.ownerDocument;
129 let chromeWin = chromeDoc.defaultView;
130 let stringBundle = chromeWin.gNavigatorBundle;
131 let message = stringBundle.getFormattedString("getUserMedia.share" + requestType + ".message",
132 [ uri.host ]);
134 let mainAction = {
135 label: PluralForm.get(requestType == "CameraAndMicrophone" ? 2 : 1,
136 stringBundle.getString("getUserMedia.shareSelectedDevices.label")),
137 accessKey: stringBundle.getString("getUserMedia.shareSelectedDevices.accesskey"),
138 // The real callback will be set during the "showing" event. The
139 // empty function here is so that PopupNotifications.show doesn't
140 // reject the action.
141 callback: function() {}
142 };
144 let secondaryActions = [
145 {
146 label: stringBundle.getString("getUserMedia.denyRequest.label"),
147 accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"),
148 callback: function () {
149 denyRequest(aCallID);
150 }
151 },
152 {
153 label: stringBundle.getString("getUserMedia.never.label"),
154 accessKey: stringBundle.getString("getUserMedia.never.accesskey"),
155 callback: function () {
156 denyRequest(aCallID);
157 // Let someone save "Never" for http sites so that they can be stopped from
158 // bothering you with doorhangers.
159 let perms = Services.perms;
160 if (audioDevices.length)
161 perms.add(uri, "microphone", perms.DENY_ACTION);
162 if (videoDevices.length)
163 perms.add(uri, "camera", perms.DENY_ACTION);
164 }
165 }
166 ];
168 if (aSecure) {
169 // Don't show the 'Always' action if the connection isn't secure.
170 secondaryActions.unshift({
171 label: stringBundle.getString("getUserMedia.always.label"),
172 accessKey: stringBundle.getString("getUserMedia.always.accesskey"),
173 callback: function () {
174 mainAction.callback(true);
175 }
176 });
177 }
179 let options = {
180 eventCallback: function(aTopic, aNewBrowser) {
181 if (aTopic == "swapping")
182 return true;
184 let chromeDoc = this.browser.ownerDocument;
186 if (aTopic == "shown") {
187 let PopupNotifications = chromeDoc.defaultView.PopupNotifications;
188 let popupId = requestType == "Microphone" ? "Microphone" : "Devices";
189 PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-share" + popupId);
190 }
192 if (aTopic != "showing")
193 return false;
195 // DENY_ACTION is handled immediately by MediaManager, but handling
196 // of ALLOW_ACTION is delayed until the popupshowing event
197 // to avoid granting permissions automatically to background tabs.
198 if (aSecure) {
199 let perms = Services.perms;
201 let micPerm = perms.testExactPermission(uri, "microphone");
202 if (micPerm == perms.PROMPT_ACTION)
203 micPerm = perms.UNKNOWN_ACTION;
205 let camPerm = perms.testExactPermission(uri, "camera");
206 if (camPerm == perms.PROMPT_ACTION)
207 camPerm = perms.UNKNOWN_ACTION;
209 // We don't check that permissions are set to ALLOW_ACTION in this
210 // test; only that they are set. This is because if audio is allowed
211 // and video is denied persistently, we don't want to show the prompt,
212 // and will grant audio access immediately.
213 if ((!audioDevices.length || micPerm) && (!videoDevices.length || camPerm)) {
214 // All permissions we were about to request are already persistently set.
215 let allowedDevices = Cc["@mozilla.org/supports-array;1"]
216 .createInstance(Ci.nsISupportsArray);
217 if (videoDevices.length && camPerm == perms.ALLOW_ACTION)
218 allowedDevices.AppendElement(videoDevices[0]);
219 if (audioDevices.length && micPerm == perms.ALLOW_ACTION)
220 allowedDevices.AppendElement(audioDevices[0]);
221 Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID);
222 this.remove();
223 return true;
224 }
225 }
227 function listDevices(menupopup, devices) {
228 while (menupopup.lastChild)
229 menupopup.removeChild(menupopup.lastChild);
231 let deviceIndex = 0;
232 for (let device of devices) {
233 addDeviceToList(menupopup, device.name, deviceIndex);
234 deviceIndex++;
235 }
236 }
238 function addDeviceToList(menupopup, deviceName, deviceIndex) {
239 let menuitem = chromeDoc.createElement("menuitem");
240 menuitem.setAttribute("value", deviceIndex);
241 menuitem.setAttribute("label", deviceName);
242 menuitem.setAttribute("tooltiptext", deviceName);
243 menupopup.appendChild(menuitem);
244 }
246 chromeDoc.getElementById("webRTC-selectCamera").hidden = !videoDevices.length;
247 chromeDoc.getElementById("webRTC-selectMicrophone").hidden = !audioDevices.length;
249 let camMenupopup = chromeDoc.getElementById("webRTC-selectCamera-menupopup");
250 let micMenupopup = chromeDoc.getElementById("webRTC-selectMicrophone-menupopup");
251 listDevices(camMenupopup, videoDevices);
252 listDevices(micMenupopup, audioDevices);
253 if (requestType == "CameraAndMicrophone") {
254 let stringBundle = chromeDoc.defaultView.gNavigatorBundle;
255 addDeviceToList(camMenupopup, stringBundle.getString("getUserMedia.noVideo.label"), "-1");
256 addDeviceToList(micMenupopup, stringBundle.getString("getUserMedia.noAudio.label"), "-1");
257 }
259 this.mainAction.callback = function(aRemember) {
260 let allowedDevices = Cc["@mozilla.org/supports-array;1"]
261 .createInstance(Ci.nsISupportsArray);
262 let perms = Services.perms;
263 if (videoDevices.length) {
264 let videoDeviceIndex = chromeDoc.getElementById("webRTC-selectCamera-menulist").value;
265 let allowCamera = videoDeviceIndex != "-1";
266 if (allowCamera)
267 allowedDevices.AppendElement(videoDevices[videoDeviceIndex]);
268 if (aRemember) {
269 perms.add(uri, "camera",
270 allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION);
271 }
272 }
273 if (audioDevices.length) {
274 let audioDeviceIndex = chromeDoc.getElementById("webRTC-selectMicrophone-menulist").value;
275 let allowMic = audioDeviceIndex != "-1";
276 if (allowMic)
277 allowedDevices.AppendElement(audioDevices[audioDeviceIndex]);
278 if (aRemember) {
279 perms.add(uri, "microphone",
280 allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION);
281 }
282 }
284 if (allowedDevices.Count() == 0) {
285 denyRequest(aCallID);
286 return;
287 }
289 Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID);
290 };
291 return false;
292 }
293 };
295 let anchorId = requestType == "Microphone" ? "webRTC-shareMicrophone-notification-icon"
296 : "webRTC-shareDevices-notification-icon";
297 chromeWin.PopupNotifications.show(browser, "webRTC-shareDevices", message,
298 anchorId, mainAction, secondaryActions, options);
299 }
301 function updateIndicators() {
302 webrtcUI.showGlobalIndicator =
303 MediaManagerService.activeMediaCaptureWindows.Count() > 0;
305 let e = Services.wm.getEnumerator("navigator:browser");
306 while (e.hasMoreElements())
307 e.getNext().WebrtcIndicator.updateButton();
309 for (let {browser: browser} of webrtcUI.activeStreams)
310 showBrowserSpecificIndicator(browser);
311 }
313 function showBrowserSpecificIndicator(aBrowser) {
314 let hasVideo = {};
315 let hasAudio = {};
316 MediaManagerService.mediaCaptureWindowState(aBrowser.contentWindow,
317 hasVideo, hasAudio);
318 let captureState;
319 if (hasVideo.value && hasAudio.value) {
320 captureState = "CameraAndMicrophone";
321 } else if (hasVideo.value) {
322 captureState = "Camera";
323 } else if (hasAudio.value) {
324 captureState = "Microphone";
325 } else {
326 Cu.reportError("showBrowserSpecificIndicator: got neither video nor audio access");
327 return;
328 }
330 let chromeWin = aBrowser.ownerDocument.defaultView;
331 let stringBundle = chromeWin.gNavigatorBundle;
333 let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2");
335 let uri = aBrowser.contentWindow.document.documentURIObject;
336 let windowId = aBrowser.contentWindow
337 .QueryInterface(Ci.nsIInterfaceRequestor)
338 .getInterface(Ci.nsIDOMWindowUtils)
339 .currentInnerWindowID;
340 let mainAction = {
341 label: stringBundle.getString("getUserMedia.continueSharing.label"),
342 accessKey: stringBundle.getString("getUserMedia.continueSharing.accesskey"),
343 callback: function () {},
344 dismiss: true
345 };
346 let secondaryActions = [{
347 label: stringBundle.getString("getUserMedia.stopSharing.label"),
348 accessKey: stringBundle.getString("getUserMedia.stopSharing.accesskey"),
349 callback: function () {
350 let perms = Services.perms;
351 if (hasVideo.value &&
352 perms.testExactPermission(uri, "camera") == perms.ALLOW_ACTION)
353 perms.remove(uri.host, "camera");
354 if (hasAudio.value &&
355 perms.testExactPermission(uri, "microphone") == perms.ALLOW_ACTION)
356 perms.remove(uri.host, "microphone");
358 Services.obs.notifyObservers(null, "getUserMedia:revoke", windowId);
359 }
360 }];
361 let options = {
362 hideNotNow: true,
363 dismissed: true,
364 eventCallback: function(aTopic) {
365 if (aTopic == "shown") {
366 let PopupNotifications = this.browser.ownerDocument.defaultView.PopupNotifications;
367 let popupId = captureState == "Microphone" ? "Microphone" : "Devices";
368 PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-sharing" + popupId);
369 }
370 return aTopic == "swapping";
371 }
372 };
373 let anchorId = captureState == "Microphone" ? "webRTC-sharingMicrophone-notification-icon"
374 : "webRTC-sharingDevices-notification-icon";
375 chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingDevices", message,
376 anchorId, mainAction, secondaryActions, options);
377 }
379 function removeBrowserSpecificIndicator(aSubject, aTopic, aData) {
380 let browser = getBrowserForWindowId(aData);
381 let PopupNotifications = browser.ownerDocument.defaultView.PopupNotifications;
382 let notification = PopupNotifications &&
383 PopupNotifications.getNotification("webRTC-sharingDevices",
384 browser);
385 if (notification)
386 PopupNotifications.remove(notification);
387 }