|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ["webrtcUI"]; |
|
8 |
|
9 const Cu = Components.utils; |
|
10 const Cc = Components.classes; |
|
11 const Ci = Components.interfaces; |
|
12 |
|
13 Cu.import("resource://gre/modules/Services.jsm"); |
|
14 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
15 |
|
16 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", |
|
17 "resource://gre/modules/PluralForm.jsm"); |
|
18 |
|
19 XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService", |
|
20 "@mozilla.org/mediaManagerService;1", |
|
21 "nsIMediaManagerService"); |
|
22 |
|
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 }, |
|
29 |
|
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 }, |
|
35 |
|
36 showGlobalIndicator: false, |
|
37 |
|
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 } |
|
57 |
|
58 function getBrowserForWindowId(aWindowID) { |
|
59 return getBrowserForWindow(Services.wm.getOuterWindowWithId(aWindowID)); |
|
60 } |
|
61 |
|
62 function getBrowserForWindow(aContentWindow) { |
|
63 return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
|
64 .getInterface(Ci.nsIWebNavigation) |
|
65 .QueryInterface(Ci.nsIDocShell) |
|
66 .chromeEventHandler; |
|
67 } |
|
68 |
|
69 function handleRequest(aSubject, aTopic, aData) { |
|
70 let constraints = aSubject.getConstraints(); |
|
71 let secure = aSubject.isSecure; |
|
72 let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID); |
|
73 |
|
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 } |
|
87 |
|
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 } |
|
96 |
|
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 } |
|
113 |
|
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 } |
|
125 |
|
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 ]); |
|
133 |
|
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 }; |
|
143 |
|
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 ]; |
|
167 |
|
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 } |
|
178 |
|
179 let options = { |
|
180 eventCallback: function(aTopic, aNewBrowser) { |
|
181 if (aTopic == "swapping") |
|
182 return true; |
|
183 |
|
184 let chromeDoc = this.browser.ownerDocument; |
|
185 |
|
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 } |
|
191 |
|
192 if (aTopic != "showing") |
|
193 return false; |
|
194 |
|
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; |
|
200 |
|
201 let micPerm = perms.testExactPermission(uri, "microphone"); |
|
202 if (micPerm == perms.PROMPT_ACTION) |
|
203 micPerm = perms.UNKNOWN_ACTION; |
|
204 |
|
205 let camPerm = perms.testExactPermission(uri, "camera"); |
|
206 if (camPerm == perms.PROMPT_ACTION) |
|
207 camPerm = perms.UNKNOWN_ACTION; |
|
208 |
|
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 } |
|
226 |
|
227 function listDevices(menupopup, devices) { |
|
228 while (menupopup.lastChild) |
|
229 menupopup.removeChild(menupopup.lastChild); |
|
230 |
|
231 let deviceIndex = 0; |
|
232 for (let device of devices) { |
|
233 addDeviceToList(menupopup, device.name, deviceIndex); |
|
234 deviceIndex++; |
|
235 } |
|
236 } |
|
237 |
|
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 } |
|
245 |
|
246 chromeDoc.getElementById("webRTC-selectCamera").hidden = !videoDevices.length; |
|
247 chromeDoc.getElementById("webRTC-selectMicrophone").hidden = !audioDevices.length; |
|
248 |
|
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 } |
|
258 |
|
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 } |
|
283 |
|
284 if (allowedDevices.Count() == 0) { |
|
285 denyRequest(aCallID); |
|
286 return; |
|
287 } |
|
288 |
|
289 Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID); |
|
290 }; |
|
291 return false; |
|
292 } |
|
293 }; |
|
294 |
|
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 } |
|
300 |
|
301 function updateIndicators() { |
|
302 webrtcUI.showGlobalIndicator = |
|
303 MediaManagerService.activeMediaCaptureWindows.Count() > 0; |
|
304 |
|
305 let e = Services.wm.getEnumerator("navigator:browser"); |
|
306 while (e.hasMoreElements()) |
|
307 e.getNext().WebrtcIndicator.updateButton(); |
|
308 |
|
309 for (let {browser: browser} of webrtcUI.activeStreams) |
|
310 showBrowserSpecificIndicator(browser); |
|
311 } |
|
312 |
|
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 } |
|
329 |
|
330 let chromeWin = aBrowser.ownerDocument.defaultView; |
|
331 let stringBundle = chromeWin.gNavigatorBundle; |
|
332 |
|
333 let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2"); |
|
334 |
|
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"); |
|
357 |
|
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 } |
|
378 |
|
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 } |