Bug 1289974 - Device selection UI for presentation API; r?mconley draft
authorChun-Min Chang <chun.m.chang@gmail.com>
Tue, 16 Aug 2016 14:50:04 +0800
changeset 401063 84efdce27dc4bdd9e689105a8b32de734e6acec6
parent 400825 054d4856cea6150a6638e5daf7913713281af97d
child 528384 31204c402363e23077f43ecd7d7fae94755a4be6
push id26350
push userbmo:cchang@mozilla.com
push dateTue, 16 Aug 2016 06:55:35 +0000
reviewersmconley
bugs1289974
milestone51.0a1
Bug 1289974 - Device selection UI for presentation API; r?mconley MozReview-Commit-ID: 33YkVw5ifXA
browser/extensions/moz.build
browser/extensions/presentation/bootstrap.js
browser/extensions/presentation/content/DeviceMenu.jsm
browser/extensions/presentation/content/Presentation.jsm
browser/extensions/presentation/content/PresentationDevicePrompt.jsm
browser/extensions/presentation/content/ui/link.png
browser/extensions/presentation/content/ui/menu.css
browser/extensions/presentation/content/ui/menu.html
browser/extensions/presentation/content/ui/menu.js
browser/extensions/presentation/install.rdf.in
browser/extensions/presentation/jar.mn
browser/extensions/presentation/locale/en-US/presentation.properties
browser/extensions/presentation/moz.build
--- a/browser/extensions/moz.build
+++ b/browser/extensions/moz.build
@@ -4,15 +4,16 @@
 # 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/.
 
 DIRS += [
     'e10srollout',
     'pdfjs',
     'pocket',
     'webcompat',
+    'presentation',
 ]
 
 # Only include the following system add-ons if building Aurora or Nightly
 if 'a' in CONFIG['GRE_MILESTONE']:
     DIRS += [
         'flyweb',
     ]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/bootstrap.js
@@ -0,0 +1,34 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// Presentation API
+XPCOMUtils.defineLazyModuleGetter(this, "Presentation",
+                                  "chrome://presentation.api/content/Presentation.jsm");
+
+function log(aMsg) {
+  // dump("@ presentation add-on: " + aMsg + "\n");
+}
+
+function install(aData, aReason) {
+}
+
+function uninstall(aData, aReason) {
+}
+
+function startup(aData, aReason) {
+  log("startup");
+  // Initialize Presentation API
+  Presentation && Presentation.init();
+}
+
+function shutdown(aData, aReason) {
+  log("shutdown");
+  // Uninitialize Presentation API
+  Presentation && Presentation.uninit();
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/content/DeviceMenu.jsm
@@ -0,0 +1,265 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+'use strict';
+
+var EXPORTED_SYMBOLS = ['DeviceMenu'];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+// An string bundle for localization.
+XPCOMUtils.defineLazyGetter(this, "Strings", function() {
+  return Services.strings.createBundle("chrome://presentation.api/locale/presentation.properties");
+});
+
+/*
+ * Utils
+ */
+function log(aMsg) {
+  // dump("@ DeviceMenu: " + aMsg + "\n");
+}
+
+/*
+ * DeviceMenu
+ */
+const MAIN_POPUPSET = "mainPopupSet";
+const PANEL_ID = "presentation-devices-panel";
+const PANEL_UI_URL = "chrome://presentation.api/content/ui/menu.html"
+
+const LOAD_PRESENTATION_DATA = "Presentation:LoadData";
+const REQUIRE_PRESENTATION_DATA = "Presentation:RequireData";
+const SELECT_PRESENTATION_DEVICE = "Presentation:SelectDevice";
+
+const UI_WIDTH = 320;
+const UI_HEIGHT = 350;
+
+function DeviceMenu() {};
+
+DeviceMenu.prototype = {
+  _selectionPanel: null,
+  _devices: [],
+
+  init: function() {
+    log("init");
+
+    // Register observers.
+    //   REQUIRE_PRESENTATION_DATA will be called only once when the ui page
+    //   is created. We will set 'onpopupshowing' callback to reload the devices
+    //   into menu. However, onpopupshowing/onpopupshown is called before
+    //   DOMContentLoaded event of the page, so the devices can't be loaded into
+    //   menu because the menu is not created at that time.
+    Services.obs.addObserver(this, REQUIRE_PRESENTATION_DATA, false);
+    Services.obs.addObserver(this, SELECT_PRESENTATION_DEVICE, false);
+
+    // The following function will initialize _selectionPanel.
+    this._createSelectionPanel();
+  },
+
+  uninit: function() {
+    log("uninit");
+    // Unregister observers.
+    Services.obs.removeObserver(this, REQUIRE_PRESENTATION_DATA);
+    Services.obs.removeObserver(this, SELECT_PRESENTATION_DEVICE);
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    log("observe:" + aTopic + ", data: " + aData);
+    switch(aTopic) {
+      case REQUIRE_PRESENTATION_DATA:
+        if (this._devices.length) {
+          this._refreshMenu();
+        }
+        break;
+      case SELECT_PRESENTATION_DEVICE:
+        if (this._onSelect && typeof this._onSelect == 'function') {
+          let device = JSON.parse(aData);
+          this._onSelect(device);
+        }
+        break;
+      default:
+        break;
+    }
+  },
+
+  // Pass nsIPresentationDevice array here to generate device selection UI.
+  select: function(aDevices) {
+    log("select");
+    let self = this;
+    let responsed = false;
+    return new Promise(function(aResolve, aReject) {
+      if (!aDevices.length) {
+        aReject("No available device");
+        return;
+      }
+
+      if (!self._selectionPanel) {
+        aReject("No available selection panel");
+        return;
+      }
+
+      self._devices = aDevices;
+
+      // Setup the callback that will be called when device is selected.
+      // This will be called when we receive SELECT_PRESENTATION_DEVICE signal.
+      self._onSelect = function(aDevice) {
+        if (aDevice.index === undefined) {
+          return;
+        }
+
+        aDevice.index = Number(aDevice.index);
+        aResolve(aDevice.index);
+
+        // Close the popup
+        responsed = true;
+        self._selectionPanel.hidePopup();
+      };
+
+      // Setup callback that will be called when the panel is dismissed.
+      // This will be called when user click somewhere out of the popup panel.
+      self._selectionPanel.addEventListener("popuphidden", function onPopupShown() {
+        self._selectionPanel.removeEventListener("popuphidden", onPopupShown);
+        // If it's not caused by _onSelect, then it's canceled by user.
+        if (!responsed) {
+          aReject("Dismiss by user");
+        }
+      });
+
+      let { selectedBrowser } = self._getBrowserData();
+      let anchor = selectedBrowser.contentWindow.top;
+
+      // Show the device selection panel in the window's center.
+      self._selectionPanel.hidden = false;
+      self._selectionPanel.openPopupAtScreen(
+        anchor.screenX + (anchor.outerWidth - UI_WIDTH) / 2,
+        anchor.screenY + (anchor.outerHeight - UI_HEIGHT) / 2,
+        true);
+    });
+  },
+
+  _refreshMenu: function() {
+    log("_refreshMenu");
+    if (!this._devices.length) {
+      log("No available devices to be loaded");
+      return;
+    }
+
+    let { currentURL } = this._getBrowserData();
+    let siteName = this._getWebsiteName(currentURL);
+    let self = this;
+    let data = {
+      title: self._getString("presentation.title"),
+      description: self._getString("presentation.message", siteName),
+      devices: self._devices,
+    }
+    this._loadDataToMenu(data);
+  },
+
+  _loadDataToMenu: function(aData) {
+    if (!aData) {
+      log("No available data to be loaded");
+      return;
+    }
+    let data = JSON.stringify(aData);
+    Services.obs.notifyObservers(null, LOAD_PRESENTATION_DATA, data);
+  },
+
+  _getBrowserData: function() {
+    let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+    let selectedBrowser = browserWindow.gBrowser.selectedBrowser;
+    let currentURL = selectedBrowser.currentURI.spec;
+    let chromeWin = selectedBrowser.ownerGlobal;
+    let chromeDoc = selectedBrowser.ownerDocument;
+
+    return { browserWindow: browserWindow,
+             selectedBrowser: selectedBrowser,
+             currentURL : currentURL,
+             chromeWindow: chromeWin,
+             chromeDocument: chromeDoc };
+  },
+
+  _getString: function(aName, aStr) {
+    return (aStr) ? Strings.formatStringFromName(aName, [aStr], 1) :
+                    Strings.GetStringFromName(aName);
+  },
+
+  _getWebsiteName: function(aURL) {
+    let name;
+    let strs = aURL.split('/');
+    for (let i = 1 ; i < strs.length ; i++) { // strs[0] is protocol name
+      if (strs[i]) {
+        name = strs[i];
+        break;
+      }
+    }
+    return name;
+  },
+
+  // We will insert a Panel like:
+  //
+  // <panel id="presentation-devices-panel"
+  //        type="autocomplete"
+  //        position="after_start"
+  //        hidden="true"
+  //        orient="vertical"
+  //        noautofocus="true">
+  //   <iframe src="path/to/device_menu.html" flex="2" type="chrome">
+  // </panel>
+  //
+  _createSelectionPanel: function() {
+    log("_createSelectionPanel");
+    let { currentURL, chromeDocument } = this._getBrowserData();
+
+    let mainPopupSet = chromeDocument.getElementById(MAIN_POPUPSET);
+
+    this._selectionPanel = this._createPanel(chromeDocument, PANEL_ID);
+
+    let iframe = this._createIframe(chromeDocument, PANEL_UI_URL);
+    this._selectionPanel.appendChild(iframe);
+
+    // Refresh the menu every time when the popup is prompted.
+    let self = this;
+    this._selectionPanel.addEventListener("popupshowing", function() {
+      self._refreshMenu();
+    });
+
+    mainPopupSet.appendChild(this._selectionPanel);
+  },
+
+  _createPanel: function(aDocument, aId) {
+    let panel = aDocument.createElement("panel");
+    let properties = {
+      id: aId,
+      type: "autocomplete",
+      position: "after_start",
+      hidden: true,
+      noautofocus: true,
+      orient: "vertical",
+    };
+    for (let p in properties) {
+      panel.setAttribute(p, properties[p]);
+    }
+    return panel;
+  },
+
+  _createIframe: function(aDocument, aSrc) {
+    let iframe = aDocument.createElement("iframe");
+    let properties = {
+      src: aSrc,
+      flex: 2,
+      type: "chrome",
+      marginwidth: 0,
+      marginheight: 0,
+      width: UI_WIDTH,
+      height: UI_HEIGHT,
+    };
+    for (var p in properties) {
+      iframe.setAttribute(p, properties[p]);
+    }
+    return iframe;
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/content/Presentation.jsm
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+/*
+ * Presentation
+ */
+
+'use strict';
+
+var EXPORTED_SYMBOLS = ['Presentation'];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cm = Components.manager;
+const Cu = Components.utils;
+
+const PRESENTATIONDEVICEPROMPT_PATH = 'chrome://presentation.api/content/PresentationDevicePrompt.jsm';
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+function log(aMsg) {
+  // dump("@ Presentation: " + aMsg + "\n");
+}
+
+// Register/unregister a constructor as a factory.
+function Factory() {}
+Factory.prototype = {
+  register: function f_register(targetConstructor) {
+    let proto = targetConstructor.prototype;
+    this._classID = proto.classID;
+
+    let factory = XPCOMUtils._getFactory(targetConstructor);
+    this._factory = factory;
+
+    let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+    registrar.registerFactory(proto.classID, proto.classDescription,
+                              proto.contractID, factory);
+  },
+
+  unregister: function unregister() {
+    let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+    registrar.unregisterFactory(this._classID, this._factory);
+    this._factory = null;
+    this._classID = null;
+  },
+};
+
+var Presentation = {
+  // PUBLIC APIs
+  init: function p_init() {
+    log('init');
+    this._register();
+  },
+
+  uninit: function p_uninit() {
+    log('uninit');
+    this._unregister();
+  },
+
+  // PRIVATE APIs
+  _register: function p_register() {
+    log('_register');
+    this._devicePromptFactory = new Factory();
+    Cu.import(PRESENTATIONDEVICEPROMPT_PATH);
+    // Register PresentationDevicePrompt into a XPCOM component.
+    this._devicePromptFactory.register(PresentationDevicePrompt);
+  },
+
+  _unregister: function p_unregister() {
+    log('_unregister');
+    this._devicePromptFactory.unregister();
+    Cu.unload(PRESENTATIONDEVICEPROMPT_PATH);
+    // Unregister PresentationDevicePrompt XPCOM component.
+    delete this._devicePromptFactory;
+  },
+};
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/content/PresentationDevicePrompt.jsm
@@ -0,0 +1,106 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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 the implementation of nsIPresentationDevicePrompt XPCOM.
+ * It will be registered into a XPCOM component by Presntation.jsm.
+ *
+ * This component will prompt a device selection UI for users to choose which
+ * devices they want to connect, when PresentationRequest is started.
+ */
+
+'use strict';
+
+var EXPORTED_SYMBOLS = ['PresentationDevicePrompt'];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+// DeviceMenu module
+//   Used to prompt a device selection UI to user. The module need to be
+//   initialized when it's created. Then we can use "select" function to prompt
+//   UI to user. The "select" returns Promise. It may be resolved with a given
+//   value which is the index of the array containing current available devices,
+//   or it may be rejected with a reason.
+// -----------------------------
+XPCOMUtils.defineLazyGetter(this, "DeviceMenu", function() {
+  let sandbox = {};
+  Services.scriptloader.loadSubScript("chrome://presentation.api/content/DeviceMenu.jsm", sandbox);
+  return sandbox["DeviceMenu"];
+});
+
+/*
+ * Utils
+ */
+function log(aMsg) {
+  // dump("@ PresentationDevicePrompt: " + aMsg + "\n");
+}
+
+/*
+ * nsIPresentationDevicePrompt
+ */
+// For XPCOM registration
+const PRESENTATIONDEVICEPROMPT_CONTRACTID = "@mozilla.org/presentation-device/prompt;1";
+const PRESENTATIONDEVICEPROMPT_CID        = Components.ID("{388bd149-c919-4a43-b646-d7ec57877689}");
+
+function PresentationDevicePrompt() {}
+
+PresentationDevicePrompt.prototype = {
+  // properties required for XPCOM registration:
+  classID: PRESENTATIONDEVICEPROMPT_CID,
+  classDescription: "Firefox Desktop Presentation Device Prompt",
+  contractID: PRESENTATIONDEVICEPROMPT_CONTRACTID,
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevicePrompt]),
+
+  // This will be fired when window.PresentationRequest(URL).start() is called.
+  promptDeviceSelection: function(aRequest) {
+    log("promptDeviceSelection - request to present " + aRequest.requestURL);
+
+    let devices = this._loadDevices();
+
+    // Cancel request if no available device.
+    if (!devices.length) {
+      log("No available device, cancel the request.");
+      aRequest.cancel();
+      return;
+    }
+
+    // Call nsIPresentationDeviceRequest.select if user choose a device.
+    // Otherwise, cancel the request.
+    this.deviceMenu.select(devices).then(function(aIndex) {
+      log("Select: " + devices[aIndex].name + "(" + devices[aIndex].id + ")");
+      aRequest.select(devices[aIndex]);
+    }, function(aReason) {
+      log("Cancel request: " + aReason);
+      aRequest.cancel();
+    });
+  },
+
+  _loadDevices: function() {
+    let deviceManager = Cc["@mozilla.org/presentation-device/manager;1"]
+                      .getService(Ci.nsIPresentationDeviceManager);
+    let devices = deviceManager.getAvailableDevices().QueryInterface(Ci.nsIArray);
+    let list = [];
+    for (let i = 0; i < devices.length; i++) {
+      let device = devices.queryElementAt(i, Ci.nsIPresentationDevice);
+      list.push(device);
+    }
+
+    return list;
+  },
+
+  // Get the deviceMenu instance. Create a new one if it doesn't exist.
+  // Otherwise, return the existing one.
+  get deviceMenu() {
+    if (!this._deviceMenu) {
+      this._deviceMenu = new DeviceMenu();
+      this._deviceMenu.init();
+    }
+
+    return this._deviceMenu;
+  },
+};
new file mode 100644
index 0000000000000000000000000000000000000000..c4a668acdcfe7b073a40f9c86fae5ab9ee24252b
GIT binary patch
literal 15559
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
T&|Q&W{i|_wgiKNurYQIiE<2?H
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/content/ui/menu.css
@@ -0,0 +1,75 @@
+body {
+  margin: 0;
+  padding: 0;
+  font-family: Arial, sans-serif;
+}
+
+section {
+  font-size: 15px;
+}
+
+/*
+The whole menu is restricted in 320 x 350 (This is set in DeviceMenu.jsm),
+so we have some hard coding here.
+*/
+
+/* Menu Header */
+#menu-header {
+  padding: 16px;
+  max-height: 100px;
+  border-bottom: 1px solid rgba(200, 200, 200, 0.2);
+  margin-bottom: 16px;
+}
+
+#menu-header > .header-title-container {
+  display: flex;
+  align-items: center;
+  max-height: 32px;
+}
+
+#menu-header > .header-title-container > img {
+  height: 18px;
+  width: auto;
+  margin-right: 10px;
+}
+
+#menu-header > .header-title-container > h1 {
+  font-size: 20px;
+  font-weight: normal;
+  color: #555;
+}
+
+#menu-header > p {
+  text-overflow: ellipsis;
+  overflow: hidden;
+  max-height: 56px;
+}
+
+/* Menu Content */
+#menu-container {
+  min-width: 160px;
+  max-height: 200px;
+  overflow-y: auto; /* Show scroll bar when it needs */
+}
+
+#menu-container ul {
+  margin: 0;
+  padding: 0;
+  list-style-type: none;
+  line-height: 10px;
+}
+
+#menu-container a {
+  padding: 12px 16px;
+  text-decoration: none;
+  display: block;
+}
+
+#menu-container a:hover {
+  background-color: #0099ff;
+}
+
+#menu-container img {
+  vertical-align: middle;
+  margin-right: 10px;
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/content/ui/menu.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+  <title>Device Menu</title>
+  <link rel="stylesheet" type="text/css" href="menu.css">
+  <script src=menu.js></script>
+</head>
+<body>
+<section>
+  <div id="menu-header">
+    <div class="header-title-container">
+      <img src="link.png">
+      <h1>Devices</h1>
+    </div>
+    <p>Pick one device:</p>
+  </div>
+  <div id="menu-container">
+    <ul>
+      <!-- We will append something like the following items into the list
+      <li><a href="#" data-name="FxOS TV" data-id="Android.local." data-index="0">
+        <img src="path/to/fxostv-picture" />
+        FxOS TV
+      </a></li>
+      <li><a href="#" data-name="ChromeCast" data-id="chromecast.local." data-index="1">
+        <img src="path/to/chromecast-picture" />
+        ChromeCast
+      </a></li>
+      <li><a href="#" data-name="Roku" data-id="roku.local." data-index="2">
+        <img src="path/to/roku-picture" />
+        Roku
+      </a></li> -->
+    </ul>
+  </div>
+</section>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/content/ui/menu.js
@@ -0,0 +1,106 @@
+'use strict';
+
+// Get Services to use Services.obs.notifyObservers/addObserver/removeObserver
+// to communicate with DeviceMenu.jsm
+// Notice the menu.html page should be loaded as chrome://path/to/menu.html
+// (it can be set in jar.mn) to make the 'Components' and 'Services' works.
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+
+const LOAD_PRESENTATION_DATA = 'Presentation:LoadData';
+const REQUIRE_PRESENTATION_DATA = 'Presentation:RequireData';
+const SELECT_PRESENTATION_DEVICE = 'Presentation:SelectDevice';
+
+const FX_DEVICE_ICON = 'firefox.png';
+
+var menuLoader = {
+  // PUBLIC APIs
+  init: function() {
+    // Register observers
+    Services.obs.addObserver(this, LOAD_PRESENTATION_DATA, false);
+    // Require devices' information from DeviceMenu.jsm
+    Services.obs.notifyObservers(null, REQUIRE_PRESENTATION_DATA, null);
+  },
+
+  uninit: function() {
+    // Unregister observers
+    Services.obs.removeObserver(this, LOAD_PRESENTATION_DATA);
+  },
+
+  observe: function(subject, topic, data) {
+    if (topic != 'Presentation:LoadData') {
+      return;
+    }
+
+    let info = JSON.parse(data);
+    this._title.innerText = info.title;
+    this._description.innerText = info.description;
+    this._loadDevices(info.devices);
+  },
+
+  // PRIVATE APIs
+  _select: function(evt) {
+    let index = evt.target.getAttribute('data-index');
+    if (!index) {
+      return;
+    }
+    let device = {
+      index: index,
+      name: evt.target.getAttribute('data-name'),
+      id: evt.target.getAttribute('data-id')
+    };
+    let data = JSON.stringify(device);
+    Services.obs.notifyObservers(null, SELECT_PRESENTATION_DEVICE, data);
+  },
+
+  _loadDevices: function(devices) {
+    // Remove the items created last time
+    while (this._list.lastChild) {
+      this._list.removeChild(this._list.lastChild);
+    }
+
+    let self = this;
+    devices.forEach(function(device, index) {
+      let item = document.createElement('li');
+      let link = document.createElement('a');
+      // Save the device information in data-xxx attributes.
+      link.setAttribute('data-id', device.id);
+      link.setAttribute('data-name', device.name);
+      link.setAttribute('data-index', index);
+      // Set the text shown on menu item.
+      let text = document.createTextNode(device.name);
+      link.appendChild(text);
+      // Set the callback when user selects a device.
+      link.onclick = self._select;
+      item.appendChild(link);
+      self._list.appendChild(item);
+    });
+  },
+
+  // If the layout of menu.html is changed, then we just need to modify the
+  // following selectors here.
+  get _title() {
+    return document.querySelector('#menu-header > .header-title-container > h1');
+  },
+
+  get _description() {
+    return document.querySelector('#menu-header > p');
+  },
+
+  get _container() {
+    return document.querySelector('#menu-container');
+  },
+
+  get _list() {
+    return document.querySelector('#menu-container ul');
+  },
+};
+
+// Initialize and uninitialize when page is loaded and unloaded.
+document.addEventListener('DOMContentLoaded', function() {
+  menuLoader.init();
+});
+
+document.addEventListener('unload', function() {
+  menuLoader.uninit();
+});
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/install.rdf.in
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+#filter substitution
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>presentation@mozilla.org</em:id>
+    <em:version>1.0.0</em:version>
+    <em:type>2</em:type>
+    <em:bootstrap>true</em:bootstrap>
+
+    <!-- Target Application this theme can install into,
+        with minimum and maximum supported versions. -->
+    <em:targetApplication>
+      <Description>
+        <!-- Firefox GUID -->
+        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+        <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
+        <em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+    <!-- Front End MetaData -->
+    <em:name>Presentation</em:name>
+    <em:description>Discover nearby devices in the browser</em:description>
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/jar.mn
@@ -0,0 +1,5 @@
+[features/presentation@mozilla.org] presentation.jar:
+% content presentation.api %content/
+  content/  (content/*)
+% locale presentation.api en-US %locale/en-US/
+  locale/  (locale/*)
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/locale/en-US/presentation.properties
@@ -0,0 +1,2 @@
+presentation.title=Nearby Devices
+presentation.message=Pick one device to send video/audio content from %S
new file mode 100644
--- /dev/null
+++ b/browser/extensions/presentation/moz.build
@@ -0,0 +1,12 @@
+DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
+
+FINAL_TARGET_FILES.features['presentation@mozilla.org'] += [
+  'bootstrap.js'
+]
+
+FINAL_TARGET_PP_FILES.features['presentation@mozilla.org'] += [
+  'install.rdf.in'
+]
+
+JAR_MANIFESTS += ['jar.mn']