Bumping manifests a=b2g-bump
[gecko.git] / browser / modules / webrtcUI.jsm
blobb3ad5c7e505331f4809c40e4b563ffe99a211c60
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 this.webrtcUI = {
20   init: function () {
21     Services.obs.addObserver(maybeAddMenuIndicator, "browser-delayed-startup-finished", false);
23     let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
24                  .getService(Ci.nsIMessageBroadcaster);
25     ppmm.addMessageListener("webrtc:UpdatingIndicators", this);
26     ppmm.addMessageListener("webrtc:UpdateGlobalIndicators", this);
28     let mm = Cc["@mozilla.org/globalmessagemanager;1"]
29                .getService(Ci.nsIMessageListenerManager);
30     mm.addMessageListener("webrtc:Request", this);
31     mm.addMessageListener("webrtc:CancelRequest", this);
32     mm.addMessageListener("webrtc:UpdateBrowserIndicators", this);
33   },
35   uninit: function () {
36     Services.obs.removeObserver(maybeAddMenuIndicator, "browser-delayed-startup-finished");
38     let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
39                  .getService(Ci.nsIMessageBroadcaster);
40     ppmm.removeMessageListener("webrtc:UpdatingIndicators", this);
41     ppmm.removeMessageListener("webrtc:UpdateGlobalIndicators", this);
43     let mm = Cc["@mozilla.org/globalmessagemanager;1"]
44                .getService(Ci.nsIMessageListenerManager);
45     mm.removeMessageListener("webrtc:Request", this);
46     mm.removeMessageListener("webrtc:CancelRequest", this);
47     mm.removeMessageListener("webrtc:UpdateBrowserIndicators", this);
48   },
50   showGlobalIndicator: false,
51   showCameraIndicator: false,
52   showMicrophoneIndicator: false,
53   showScreenSharingIndicator: "", // either "Application", "Screen" or "Window"
55   _streams: [],
56   // The boolean parameters indicate which streams should be included in the result.
57   getActiveStreams: function(aCamera, aMicrophone, aScreen) {
58     return webrtcUI._streams.filter(aStream => {
59       let state = aStream.state;
60       return aCamera && state.camera ||
61              aMicrophone && state.microphone ||
62              aScreen && state.screen;
63     }).map(aStream => {
64       let state = aStream.state;
65       let types = {camera: state.camera, microphone: state.microphone,
66                    screen: state.screen};
67       let browser = aStream.browser;
68       let browserWindow = browser.ownerDocument.defaultView;
69       let tab = browserWindow.gBrowser &&
70                 browserWindow.gBrowser.getTabForBrowser(browser);
71       return {uri: state.documentURI, tab: tab, browser: browser, types: types};
72     });
73   },
75   swapBrowserForNotification: function(aOldBrowser, aNewBrowser) {
76     for (let stream of this._streams) {
77       if (stream.browser == aOldBrowser)
78         stream.browser = aNewBrowser;
79     };
80   },
82   showSharingDoorhanger: function(aActiveStream, aType) {
83     let browserWindow = aActiveStream.browser.ownerDocument.defaultView;
84     if (aActiveStream.tab) {
85       browserWindow.gBrowser.selectedTab = aActiveStream.tab;
86     } else {
87       aActiveStream.browser.focus();
88     }
89     browserWindow.focus();
90     let PopupNotifications = browserWindow.PopupNotifications;
91     let notif = PopupNotifications.getNotification("webRTC-sharing" + aType,
92                                                    aActiveStream.browser);
93 #ifdef XP_MACOSX
94     if (!Services.focus.activeWindow) {
95       browserWindow.addEventListener("activate", function onActivate() {
96         browserWindow.removeEventListener("activate", onActivate);
97         Services.tm.mainThread.dispatch(function() {
98           notif.reshow();
99         }, Ci.nsIThread.DISPATCH_NORMAL);
100       });
101       Cc["@mozilla.org/widget/macdocksupport;1"].getService(Ci.nsIMacDockSupport)
102         .activateApplication(true);
103       return;
104     }
105 #endif
106     notif.reshow();
107   },
109   updateMainActionLabel: function(aMenuList) {
110     let type = aMenuList.selectedItem.getAttribute("devicetype");
111     let document = aMenuList.ownerDocument;
112     document.getElementById("webRTC-all-windows-shared").hidden = type != "Screen";
114     // If we are also requesting audio in addition to screen sharing,
115     // always use a generic label.
116     if (!document.getElementById("webRTC-selectMicrophone").hidden)
117       type = "";
119     let bundle = document.defaultView.gNavigatorBundle;
120     let stringId = "getUserMedia.share" + (type || "SelectedItems") + ".label";
121     let popupnotification = aMenuList.parentNode.parentNode;
122     popupnotification.setAttribute("buttonlabel", bundle.getString(stringId));
123   },
125   receiveMessage: function(aMessage) {
126     switch (aMessage.name) {
127       case "webrtc:Request":
128         prompt(aMessage.target, aMessage.data);
129         break;
130       case "webrtc:CancelRequest":
131         removePrompt(aMessage.target, aMessage.data);
132         break;
133       case "webrtc:UpdatingIndicators":
134         webrtcUI._streams = [];
135         break;
136       case "webrtc:UpdateGlobalIndicators":
137         updateIndicators(aMessage.data)
138         break;
139       case "webrtc:UpdateBrowserIndicators":
140         webrtcUI._streams.push({browser: aMessage.target, state: aMessage.data});
141         updateBrowserSpecificIndicator(aMessage.target, aMessage.data);
142         break;
143     }
144   }
147 function getBrowserForWindow(aContentWindow) {
148   return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
149                        .getInterface(Ci.nsIWebNavigation)
150                        .QueryInterface(Ci.nsIDocShell)
151                        .chromeEventHandler;
154 function denyRequest(aBrowser, aRequest) {
155   aBrowser.messageManager.sendAsyncMessage("webrtc:Deny",
156                                            {callID: aRequest.callID,
157                                             windowID: aRequest.windowID});
160 function getHost(uri, href) {
161   let host;
162   try {
163     if (!uri) {
164       uri = Services.io.newURI(href, null, null);
165     }
166     host = uri.host;
167   } catch (ex) {};
168   if (!host) {
169     if (uri && uri.scheme.toLowerCase() == "about") {
170       // For about URIs, just use the full spec, without any #hash parts
171       host = uri.specIgnoringRef;
172     } else {
173       // This is unfortunate, but we should display *something*...
174       const kBundleURI = "chrome://browser/locale/browser.properties";
175       let bundle = Services.strings.createBundle(kBundleURI);
176       host = bundle.GetStringFromName("getUserMedia.sharingMenuUnknownHost");
177     }
178   }
179   return host;
182 function prompt(aBrowser, aRequest) {
183   let {audioDevices: audioDevices, videoDevices: videoDevices,
184        sharingScreen: sharingScreen, requestTypes: requestTypes} = aRequest;
185   let uri = Services.io.newURI(aRequest.documentURI, null, null);
186   let host = getHost(uri);
187   let chromeDoc = aBrowser.ownerDocument;
188   let chromeWin = chromeDoc.defaultView;
189   let stringBundle = chromeWin.gNavigatorBundle;
190   let stringId = "getUserMedia.share" + requestTypes.join("And") + ".message";
191   let message = stringBundle.getFormattedString(stringId, [host]);
193   let mainLabel;
194   if (sharingScreen) {
195     mainLabel = stringBundle.getString("getUserMedia.shareSelectedItems.label");
196   }
197   else {
198     let string = stringBundle.getString("getUserMedia.shareSelectedDevices.label");
199     mainLabel = PluralForm.get(requestTypes.length, string);
200   }
202   let notification; // Used by action callbacks.
203   let mainAction = {
204     label: mainLabel,
205     accessKey: stringBundle.getString("getUserMedia.shareSelectedDevices.accesskey"),
206     // The real callback will be set during the "showing" event. The
207     // empty function here is so that PopupNotifications.show doesn't
208     // reject the action.
209     callback: function() {}
210   };
212   let secondaryActions = [
213     {
214       label: stringBundle.getString("getUserMedia.denyRequest.label"),
215       accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"),
216       callback: function () {
217         denyRequest(notification.browser, aRequest);
218       }
219     }
220   ];
222   if (!sharingScreen) { // Bug 1037438: implement 'never' for screen sharing.
223     secondaryActions.push({
224       label: stringBundle.getString("getUserMedia.never.label"),
225       accessKey: stringBundle.getString("getUserMedia.never.accesskey"),
226       callback: function () {
227         denyRequest(notification.browser, aRequest);
228         // Let someone save "Never" for http sites so that they can be stopped from
229         // bothering you with doorhangers.
230         let perms = Services.perms;
231         if (audioDevices.length)
232           perms.add(uri, "microphone", perms.DENY_ACTION);
233         if (videoDevices.length)
234           perms.add(uri, "camera", perms.DENY_ACTION);
235       }
236     });
237   }
239   if (aRequest.secure && !sharingScreen) {
240     // Don't show the 'Always' action if the connection isn't secure, or for
241     // screen sharing (because we can't guess which window the user wants to
242     // share without prompting).
243     secondaryActions.unshift({
244       label: stringBundle.getString("getUserMedia.always.label"),
245       accessKey: stringBundle.getString("getUserMedia.always.accesskey"),
246       callback: function () {
247         mainAction.callback(true);
248       }
249     });
250   }
252   let options = {
253     eventCallback: function(aTopic, aNewBrowser) {
254       if (aTopic == "swapping")
255         return true;
257       let chromeDoc = this.browser.ownerDocument;
259       if (aTopic == "shown") {
260         let PopupNotifications = chromeDoc.defaultView.PopupNotifications;
261         let popupId = "Devices";
262         if (requestTypes.length == 1 && requestTypes[0] == "Microphone")
263           popupId = "Microphone";
264         if (requestTypes.indexOf("Screen") != -1)
265           popupId = "Screen";
266         PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-share" + popupId);
267       }
269       if (aTopic != "showing")
270         return false;
272       // DENY_ACTION is handled immediately by MediaManager, but handling
273       // of ALLOW_ACTION is delayed until the popupshowing event
274       // to avoid granting permissions automatically to background tabs.
275       if (aRequest.secure) {
276         let perms = Services.perms;
278         let micPerm = perms.testExactPermission(uri, "microphone");
279         if (micPerm == perms.PROMPT_ACTION)
280           micPerm = perms.UNKNOWN_ACTION;
282         let camPerm = perms.testExactPermission(uri, "camera");
283         if (camPerm == perms.PROMPT_ACTION)
284           camPerm = perms.UNKNOWN_ACTION;
286         // Screen sharing shouldn't follow the camera permissions.
287         if (videoDevices.length && sharingScreen)
288           camPerm = perms.UNKNOWN_ACTION;
290         // We don't check that permissions are set to ALLOW_ACTION in this
291         // test; only that they are set. This is because if audio is allowed
292         // and video is denied persistently, we don't want to show the prompt,
293         // and will grant audio access immediately.
294         if ((!audioDevices.length || micPerm) && (!videoDevices.length || camPerm)) {
295           // All permissions we were about to request are already persistently set.
296           let allowedDevices = [];
297           if (videoDevices.length && camPerm == perms.ALLOW_ACTION)
298             allowedDevices.push(videoDevices[0].deviceIndex);
299           if (audioDevices.length && micPerm == perms.ALLOW_ACTION)
300             allowedDevices.push(audioDevices[0].deviceIndex);
301           let mm = this.browser.messageManager;
302           mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
303                                                windowID: aRequest.windowID,
304                                                devices: allowedDevices});
305           this.remove();
306           return true;
307         }
308       }
310       function listDevices(menupopup, devices) {
311         while (menupopup.lastChild)
312           menupopup.removeChild(menupopup.lastChild);
314         for (let device of devices)
315           addDeviceToList(menupopup, device.name, device.deviceIndex);
316       }
318       function listScreenShareDevices(menupopup, devices) {
319         while (menupopup.lastChild)
320           menupopup.removeChild(menupopup.lastChild);
322         let type = devices[0].mediaSource;
323         let typeName = type.charAt(0).toUpperCase() + type.substr(1);
325         let label = chromeDoc.getElementById("webRTC-selectWindow-label");
326         let stringId = "getUserMedia.select" + typeName;
327         label.setAttribute("value",
328                            stringBundle.getString(stringId + ".label"));
329         label.setAttribute("accesskey",
330                            stringBundle.getString(stringId + ".accesskey"));
332         // "No <type>" is the default because we can't pick a
333         // 'default' window to share.
334         addDeviceToList(menupopup,
335                         stringBundle.getString("getUserMedia.no" + typeName + ".label"),
336                         "-1");
337         menupopup.appendChild(chromeDoc.createElement("menuseparator"));
339         // Build the list of 'devices'.
340         for (let i = 0; i < devices.length; ++i) {
341           let name;
342           // Screen has a special treatment because we currently only support
343           // sharing the primary screen and want to display a localized string.
344           if (type == "screen") {
345             name = stringBundle.getString("getUserMedia.shareEntireScreen.label");
346           }
347           else {
348             name = devices[i].name;
349             if (type == "application") {
350               // The application names returned by the platform are of the form:
351               // <window count>\x1e<application name>
352               let sepIndex = name.indexOf("\x1e");
353               let count = name.slice(0, sepIndex);
354               let stringId = "getUserMedia.shareApplicationWindowCount.label";
355               name = PluralForm.get(parseInt(count), stringBundle.getString(stringId))
356                                .replace("#1", name.slice(sepIndex + 1))
357                                .replace("#2", count);
358             }
359           }
360           addDeviceToList(menupopup, name, i, typeName);
361         }
363         // Always re-select the "No <type>" item.
364         chromeDoc.getElementById("webRTC-selectWindow-menulist").removeAttribute("value");
365         chromeDoc.getElementById("webRTC-all-windows-shared").hidden = true;
366       }
368       function addDeviceToList(menupopup, deviceName, deviceIndex, type) {
369         let menuitem = chromeDoc.createElement("menuitem");
370         menuitem.setAttribute("value", deviceIndex);
371         menuitem.setAttribute("label", deviceName);
372         menuitem.setAttribute("tooltiptext", deviceName);
373         if (type)
374           menuitem.setAttribute("devicetype", type);
375         menupopup.appendChild(menuitem);
376       }
378       chromeDoc.getElementById("webRTC-selectCamera").hidden = !videoDevices.length || sharingScreen;
379       chromeDoc.getElementById("webRTC-selectWindowOrScreen").hidden = !sharingScreen || !videoDevices.length;
380       chromeDoc.getElementById("webRTC-selectMicrophone").hidden = !audioDevices.length;
382       let camMenupopup = chromeDoc.getElementById("webRTC-selectCamera-menupopup");
383       let windowMenupopup = chromeDoc.getElementById("webRTC-selectWindow-menupopup");
384       let micMenupopup = chromeDoc.getElementById("webRTC-selectMicrophone-menupopup");
385       if (sharingScreen)
386         listScreenShareDevices(windowMenupopup, videoDevices);
387       else
388         listDevices(camMenupopup, videoDevices);
389       listDevices(micMenupopup, audioDevices);
390       if (requestTypes.length == 2) {
391         let stringBundle = chromeDoc.defaultView.gNavigatorBundle;
392         if (!sharingScreen)
393           addDeviceToList(camMenupopup, stringBundle.getString("getUserMedia.noVideo.label"), "-1");
394         addDeviceToList(micMenupopup, stringBundle.getString("getUserMedia.noAudio.label"), "-1");
395       }
397       this.mainAction.callback = function(aRemember) {
398         let allowedDevices = [];
399         let perms = Services.perms;
400         if (videoDevices.length) {
401           let listId = "webRTC-select" + (sharingScreen ? "Window" : "Camera") + "-menulist";
402           let videoDeviceIndex = chromeDoc.getElementById(listId).value;
403           let allowCamera = videoDeviceIndex != "-1";
404           if (allowCamera)
405             allowedDevices.push(videoDeviceIndex);
406           if (aRemember) {
407             perms.add(uri, "camera",
408                       allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION);
409           }
410         }
411         if (audioDevices.length) {
412           let audioDeviceIndex = chromeDoc.getElementById("webRTC-selectMicrophone-menulist").value;
413           let allowMic = audioDeviceIndex != "-1";
414           if (allowMic)
415             allowedDevices.push(audioDeviceIndex);
416           if (aRemember) {
417             perms.add(uri, "microphone",
418                       allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION);
419           }
420         }
422         if (!allowedDevices.length) {
423           denyRequest(notification.browser, aRequest);
424           return;
425         }
427         let mm = notification.browser.messageManager
428         mm.sendAsyncMessage("webrtc:Allow", {callID: aRequest.callID,
429                                              windowID: aRequest.windowID,
430                                              devices: allowedDevices});
431       };
432       return false;
433     }
434   };
436   let anchorId = "webRTC-shareDevices-notification-icon";
437   if (requestTypes.length == 1 && requestTypes[0] == "Microphone")
438     anchorId = "webRTC-shareMicrophone-notification-icon";
439   if (requestTypes.indexOf("Screen") != -1)
440     anchorId = "webRTC-shareScreen-notification-icon";
441   notification =
442     chromeWin.PopupNotifications.show(aBrowser, "webRTC-shareDevices", message,
443                                       anchorId, mainAction, secondaryActions,
444                                       options);
445   notification.callID = aRequest.callID;
448 function removePrompt(aBrowser, aCallId) {
449   let chromeWin = aBrowser.ownerDocument.defaultView;
450   let notification =
451     chromeWin.PopupNotifications.getNotification("webRTC-shareDevices", aBrowser);
452   if (notification && notification.callID == aCallId)
453     notification.remove();
456 function getGlobalIndicator() {
457 #ifndef XP_MACOSX
458   const INDICATOR_CHROME_URI = "chrome://browser/content/webrtcIndicator.xul";
459   const features = "chrome,dialog=yes,titlebar=no,popup=yes";
461   return Services.ww.openWindow(null, INDICATOR_CHROME_URI, "_blank", features, []);
462 #else
463   let indicator = {
464     _camera: null,
465     _microphone: null,
466     _screen: null,
468     _hiddenDoc: Cc["@mozilla.org/appshell/appShellService;1"]
469                   .getService(Ci.nsIAppShellService)
470                   .hiddenDOMWindow.document,
471     _statusBar: Cc["@mozilla.org/widget/macsystemstatusbar;1"]
472                   .getService(Ci.nsISystemStatusBar),
474     _command: function(aEvent) {
475       let type = this.getAttribute("type");
476       if (type == "Camera" || type == "Microphone")
477         type = "Devices";
478       else if (type == "Window" || type == "Application")
479         type = "Screen";
480       webrtcUI.showSharingDoorhanger(aEvent.target.stream, type);
481     },
483     _popupShowing: function(aEvent) {
484       let type = this.getAttribute("type");
485       let activeStreams;
486       if (type == "Camera") {
487         activeStreams = webrtcUI.getActiveStreams(true, false, false);
488       }
489       else if (type == "Microphone") {
490         activeStreams = webrtcUI.getActiveStreams(false, true, false);
491       }
492       else if (type == "Screen") {
493         activeStreams = webrtcUI.getActiveStreams(false, false, true);
494         type = webrtcUI.showScreenSharingIndicator;
495       }
497       let bundle =
498         Services.strings.createBundle("chrome://browser/locale/webrtcIndicator.properties");
500       if (activeStreams.length == 1) {
501         let stream = activeStreams[0];
503         let menuitem = this.ownerDocument.createElement("menuitem");
504         let labelId = "webrtcIndicator.sharing" + type + "With.menuitem";
505         let label = stream.browser.contentTitle || stream.uri;
506         menuitem.setAttribute("label", bundle.formatStringFromName(labelId, [label], 1));
507         menuitem.setAttribute("disabled", "true");
508         this.appendChild(menuitem);
510         menuitem = this.ownerDocument.createElement("menuitem");
511         menuitem.setAttribute("label",
512                               bundle.GetStringFromName("webrtcIndicator.controlSharing.menuitem"));
513         menuitem.setAttribute("type", type);
514         menuitem.stream = stream;
515         menuitem.addEventListener("command", indicator._command);
517         this.appendChild(menuitem);
518         return true;
519       }
521       // We show a different menu when there are several active streams.
522       let menuitem = this.ownerDocument.createElement("menuitem");
523       let labelId = "webrtcIndicator.sharing" + type + "WithNTabs.menuitem";
524       let count = activeStreams.length;
525       let label = PluralForm.get(count, bundle.GetStringFromName(labelId)).replace("#1", count);
526       menuitem.setAttribute("label", label);
527       menuitem.setAttribute("disabled", "true");
528       this.appendChild(menuitem);
530       for (let stream of activeStreams) {
531         let item = this.ownerDocument.createElement("menuitem");
532         let labelId = "webrtcIndicator.controlSharingOn.menuitem";
533         let label = stream.browser.contentTitle || stream.uri;
534         item.setAttribute("label", bundle.formatStringFromName(labelId, [label], 1));
535         item.setAttribute("type", type);
536         item.stream = stream;
537         item.addEventListener("command", indicator._command);
538         this.appendChild(item);
539       }
541       return true;
542     },
544     _popupHiding: function(aEvent) {
545       while (this.firstChild)
546         this.firstChild.remove();
547     },
549     _setIndicatorState: function(aName, aState) {
550       let field = "_" + aName.toLowerCase();
551       if (aState && !this[field]) {
552         let menu = this._hiddenDoc.createElement("menu");
553         menu.setAttribute("id", "webRTC-sharing" + aName + "-menu");
555         // The CSS will only be applied if the menu is actually inserted in the DOM.
556         this._hiddenDoc.documentElement.appendChild(menu);
558         this._statusBar.addItem(menu);
560         let menupopup = this._hiddenDoc.createElement("menupopup");
561         menupopup.setAttribute("type", aName);
562         menupopup.addEventListener("popupshowing", this._popupShowing);
563         menupopup.addEventListener("popuphiding", this._popupHiding);
564         menupopup.addEventListener("command", this._command);
565         menu.appendChild(menupopup);
567         this[field] = menu;
568       }
569       else if (this[field] && !aState) {
570         this._statusBar.removeItem(this[field]);
571         this[field].remove();
572         this[field] = null
573       }
574     },
575     updateIndicatorState: function() {
576       this._setIndicatorState("Camera", webrtcUI.showCameraIndicator);
577       this._setIndicatorState("Microphone", webrtcUI.showMicrophoneIndicator);
578       this._setIndicatorState("Screen", webrtcUI.showScreenSharingIndicator);
579     },
580     close: function() {
581       this._setIndicatorState("Camera", false);
582       this._setIndicatorState("Microphone", false);
583       this._setIndicatorState("Screen", false);
584     }
585   };
587   indicator.updateIndicatorState();
588   return indicator;
589 #endif
592 function onTabSharingMenuPopupShowing(e) {
593   let streams = webrtcUI.getActiveStreams(true, true, true);
594   for (let streamInfo of streams) {
595     let stringName = "getUserMedia.sharingMenu";
596     let types = streamInfo.types;
597     if (types.camera)
598       stringName += "Camera";
599     if (types.microphone)
600       stringName += "Microphone";
601     if (types.screen)
602       stringName += types.screen;
604     let doc = e.target.ownerDocument;
605     let bundle = doc.defaultView.gNavigatorBundle;
607     let origin = getHost(null, streamInfo.uri);
608     let menuitem = doc.createElement("menuitem");
609     menuitem.setAttribute("label", bundle.getFormattedString(stringName, [origin]));
610     menuitem.stream = streamInfo;
612     // We can only open 1 doorhanger at a time. Guessing that users would be
613     // most eager to control screen/window/app sharing, and only then
614     // camera/microphone sharing, in that (decreasing) order of priority.
615     let doorhangerType;
616     if ((/Screen|Window|Application/).test(stringName)) {
617       doorhangerType = "Screen";
618     } else {
619       doorhangerType = "Devices";
620     }
621     menuitem.setAttribute("doorhangertype", doorhangerType);
622     menuitem.addEventListener("command", onTabSharingMenuPopupCommand);
623     e.target.appendChild(menuitem);
624   }
627 function onTabSharingMenuPopupHiding(e) {
628   while (this.lastChild)
629     this.lastChild.remove();
632 function onTabSharingMenuPopupCommand(e) {
633   let type = e.target.getAttribute("doorhangertype");
634   webrtcUI.showSharingDoorhanger(e.target.stream, type);
637 function showOrCreateMenuForWindow(aWindow) {
638   let document = aWindow.document;
639   let menu = document.getElementById("tabSharingMenu");
640   if (!menu) {
641     let stringBundle = aWindow.gNavigatorBundle;
642     menu = document.createElement("menu");
643     menu.id = "tabSharingMenu";
644     let labelStringId = "getUserMedia.sharingMenu.label";
645     menu.setAttribute("label", stringBundle.getString(labelStringId));
646 #ifdef XP_MACOSX
647     let container = document.getElementById("windowPopup");
648     let insertionPoint = document.getElementById("sep-window-list");
649     let separator = document.createElement("menuseparator");
650     separator.id = "tabSharingSeparator";
651     container.insertBefore(separator, insertionPoint);
652 #else
653     let accesskeyStringId = "getUserMedia.sharingMenu.accesskey";
654     menu.setAttribute("accesskey", stringBundle.getString(accesskeyStringId));
655     let container = document.getElementById("main-menubar");
656     let insertionPoint = document.getElementById("helpMenu");
657 #endif
658     let popup = document.createElement("menupopup");
659     popup.id = "tabSharingMenuPopup";
660     popup.addEventListener("popupshowing", onTabSharingMenuPopupShowing);
661     popup.addEventListener("popuphiding", onTabSharingMenuPopupHiding);
662     menu.appendChild(popup);
663     container.insertBefore(menu, insertionPoint);
664   } else {
665     menu.hidden = false;
666 #ifdef XP_MACOSX
667     document.getElementById("tabSharingSeparator").hidden = false;
668 #endif
669   }
672 function maybeAddMenuIndicator(window) {
673   if (webrtcUI.showGlobalIndicator) {
674     showOrCreateMenuForWindow(window);
675   }
678 var gIndicatorWindow = null;
680 function updateIndicators(data) {
681   webrtcUI.showGlobalIndicator = data.showGlobalIndicator;
682   webrtcUI.showCameraIndicator = data.showCameraIndicator;
683   webrtcUI.showMicrophoneIndicator = data.showMicrophoneIndicator;
684   webrtcUI.showScreenSharingIndicator = data.showScreenSharingIndicator;
686   let browserWindowEnum = Services.wm.getEnumerator("navigator:browser");
687   while (browserWindowEnum.hasMoreElements()) {
688     let chromeWin = browserWindowEnum.getNext();
689     if (webrtcUI.showGlobalIndicator) {
690       showOrCreateMenuForWindow(chromeWin);
691     } else {
692       let doc = chromeWin.document;
693       let existingMenu = doc.getElementById("tabSharingMenu");
694       if (existingMenu) {
695         existingMenu.hidden = true;
696       }
697 #ifdef XP_MACOSX
698       let separator = doc.getElementById("tabSharingSeparator");
699       if (separator) {
700         separator.hidden = true;
701       }
702 #endif
703     }
704   }
706   if (webrtcUI.showGlobalIndicator) {
707     if (!gIndicatorWindow)
708       gIndicatorWindow = getGlobalIndicator();
709     else
710       gIndicatorWindow.updateIndicatorState();
711   } else if (gIndicatorWindow) {
712     gIndicatorWindow.close();
713     gIndicatorWindow = null;
714   }
717 function updateBrowserSpecificIndicator(aBrowser, aState) {
718   let captureState;
719   if (aState.camera && aState.microphone) {
720     captureState = "CameraAndMicrophone";
721   } else if (aState.camera) {
722     captureState = "Camera";
723   } else if (aState.microphone) {
724     captureState = "Microphone";
725   }
727   let chromeWin = aBrowser.ownerDocument.defaultView;
728   let stringBundle = chromeWin.gNavigatorBundle;
730   let windowId = aState.windowId;
731   let notification; // Used by action callbacks.
732   let mainAction = {
733     label: stringBundle.getString("getUserMedia.continueSharing.label"),
734     accessKey: stringBundle.getString("getUserMedia.continueSharing.accesskey"),
735     callback: function () {},
736     dismiss: true
737   };
738   let secondaryActions = [{
739     label: stringBundle.getString("getUserMedia.stopSharing.label"),
740     accessKey: stringBundle.getString("getUserMedia.stopSharing.accesskey"),
741     callback: function () {
742       let uri = Services.io.newURI(aState.documentURI, null, null);
743       let host = getHost(uri);
744       let perms = Services.perms;
745       if (aState.camera &&
746           perms.testExactPermission(uri, "camera") == perms.ALLOW_ACTION)
747         perms.remove(host, "camera");
748       if (aState.microphone &&
749           perms.testExactPermission(uri, "microphone") == perms.ALLOW_ACTION)
750         perms.remove(host, "microphone");
752       let mm = notification.browser.messageManager;
753       mm.sendAsyncMessage("webrtc:StopSharing", windowId);
754     }
755   }];
756   let options = {
757     hideNotNow: true,
758     dismissed: true,
759     eventCallback: function(aTopic, aNewBrowser) {
760       if (aTopic == "shown") {
761         let PopupNotifications = this.browser.ownerDocument.defaultView.PopupNotifications;
762         let popupId = captureState == "Microphone" ? "Microphone" : "Devices";
763         PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-sharing" + popupId);
764       }
766       if (aTopic == "swapping") {
767         webrtcUI.swapBrowserForNotification(this.browser, aNewBrowser);
768         return true;
769       }
771       return false;
772     }
773   };
774   if (captureState) {
775     let anchorId = captureState == "Microphone" ? "webRTC-sharingMicrophone-notification-icon"
776                                                 : "webRTC-sharingDevices-notification-icon";
777     let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2");
778     notification =
779       chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingDevices", message,
780                                         anchorId, mainAction, secondaryActions, options);
781   }
782   else {
783     removeBrowserNotification(aBrowser,"webRTC-sharingDevices");
784   }
786   // Now handle the screen sharing indicator.
787   if (!aState.screen) {
788     removeBrowserNotification(aBrowser,"webRTC-sharingScreen");
789     return;
790   }
792   let screenSharingNotif; // Used by action callbacks.
793   options = {
794     hideNotNow: true,
795     dismissed: true,
796     eventCallback: function(aTopic, aNewBrowser) {
797       if (aTopic == "shown") {
798         let PopupNotifications = this.browser.ownerDocument.defaultView.PopupNotifications;
799         PopupNotifications.panel.firstChild.setAttribute("popupid", "webRTC-sharingScreen");
800       }
802       if (aTopic == "swapping") {
803         webrtcUI.swapBrowserForNotification(this.browser, aNewBrowser);
804         return true;
805       }
807       return false;
808     }
809   };
810   secondaryActions = [{
811     label: stringBundle.getString("getUserMedia.stopSharing.label"),
812     accessKey: stringBundle.getString("getUserMedia.stopSharing.accesskey"),
813     callback: function () {
814       let mm = screenSharingNotif.browser.messageManager;
815       mm.sendAsyncMessage("webrtc:StopSharing", "screen:" + windowId);
816     }
817   }];
818   // If we are sharing both a window and the screen, we show 'Screen'.
819   let stringId = "getUserMedia.sharing" + aState.screen;
820   screenSharingNotif =
821     chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingScreen",
822                                       stringBundle.getString(stringId + ".message"),
823                                       "webRTC-sharingScreen-notification-icon",
824                                       mainAction, secondaryActions, options);
827 function removeBrowserNotification(aBrowser, aNotificationId) {
828   let win = aBrowser.ownerDocument.defaultView;
829   let notification =
830     win.PopupNotifications.getNotification(aNotificationId, aBrowser);
831   if (notification)
832     win.PopupNotifications.remove(notification);