Merge mozilla-inbound to mozilla-central. a=merge
authorDorel Luca <dluca@mozilla.com>
Sat, 04 Aug 2018 01:01:35 +0300
changeset 430080 ff0d3184379365cf5e9ab2bee57d7e561b7c2482
parent 429998 c4c1a90df788b7634da5a89f417bf55198172640 (current diff)
parent 430079 67c4ed3c73d7bb770edcbdffebfb5688a3822a5b (diff)
child 430081 4e56a2f51ad739ca52046723448f3129a58f1666
child 430099 8f556cbf014c1711e8ddcb1ddbcb9244be544885
child 430149 72be95f82a93c62aab67814e1a5babd37a28ae68
push id34381
push userdluca@mozilla.com
push dateFri, 03 Aug 2018 22:01:59 +0000
treeherdermozilla-central@ff0d31843793 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-inbound to mozilla-central. a=merge
browser/app/profile/firefox.js
chrome/nsChromeRegistry.cpp
chrome/nsChromeRegistry.h
chrome/nsIChromeRegistry.idl
dom/base/nsGlobalWindowInner.cpp
dom/base/nsGlobalWindowOuter.cpp
dom/base/nsPIDOMWindow.h
testing/web-platform/meta/MANIFEST.json
testing/web-platform/meta/css/css-images/idlharness.html.ini
testing/web-platform/meta/webmessaging/with-options/broken-origin.tentative.html.ini
testing/web-platform/tests/background-fetch/credentials-in-url.https.window.js
testing/web-platform/tests/background-fetch/get-ids.https.js
testing/web-platform/tests/html/webappapis/scripting/events/body-exposed-window-event-handlers.html
testing/web-platform/tests/html/webappapis/scripting/events/event-handler-spec-example.html
testing/web-platform/tests/html/webappapis/scripting/events/invalid-uncompiled-raw-handler-compiled-late.html
testing/web-platform/tests/html/webappapis/scripting/events/invalid-uncompiled-raw-handler-compiled-once.html
testing/web-platform/tests/webgl/idlharness.window.js
testing/web-platform/tests/webmessaging/with-options/invalid-args.tentative.html
testing/web-platform/tests/webmessaging/with-options/null-arg-two.tentative.html
testing/web-platform/tests/webmessaging/with-options/two-arg.tentative.html
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -65,17 +65,20 @@ pref("extensions.geckoProfiler.getSymbol
 pref("extensions.geckoProfiler.getSymbolRules", "localBreakpad,dump_syms.exe");
 #endif
 
 
 // Add-on content security policies.
 pref("extensions.webextensions.base-content-security-policy", "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; object-src 'self' https://* moz-extension: blob: filesystem:;");
 pref("extensions.webextensions.default-content-security-policy", "script-src 'self'; object-src 'self';");
 
+#if defined(XP_WIN) || defined(XP_MACOSX)
 pref("extensions.webextensions.remote", true);
+#endif
+
 pref("extensions.webextensions.background-delayed-startup", true);
 
 // Extensions that should not be flagged as legacy in about:addons
 pref("extensions.legacy.exceptions", "testpilot@cliqz.com,@testpilot-containers,jid1-NeEaf3sAHdKHPA@jetpack,@activity-streams,pulse@mozilla.com,@testpilot-addon,@min-vid,tabcentertest1@mozilla.com,snoozetabs@mozilla.com,speaktome@mozilla.com,hoverpad@mozilla.com");
 
 // Require signed add-ons by default
 pref("extensions.langpacks.signatures.required", true);
 pref("xpinstall.signatures.required", true);
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -74,18 +74,16 @@
     <command id="Browser:Stop"    oncommand="BrowserStop();" disabled="true"/>
     <command id="Browser:Reload"  oncommand="if (event.shiftKey) BrowserReloadSkipCache(); else BrowserReload()" disabled="true"/>
     <command id="Browser:ReloadOrDuplicate" oncommand="BrowserReloadOrDuplicate(event)" disabled="true">
       <observes element="Browser:Reload" attribute="disabled"/>
     </command>
     <command id="Browser:ReloadSkipCache" oncommand="BrowserReloadSkipCache()" disabled="true">
       <observes element="Browser:Reload" attribute="disabled"/>
     </command>
-    <command id="Browser:NextTab" oncommand="gBrowser.tabContainer.advanceSelectedTab(1, true);"/>
-    <command id="Browser:PrevTab" oncommand="gBrowser.tabContainer.advanceSelectedTab(-1, true);"/>
     <command id="Browser:ShowAllTabs" oncommand="gTabsPanel.showAllTabsPanel();"/>
     <command id="cmd_fullZoomReduce"  oncommand="FullZoom.reduce()"/>
     <command id="cmd_fullZoomEnlarge" oncommand="FullZoom.enlarge()"/>
     <command id="cmd_fullZoomReset"   oncommand="FullZoom.reset()"/>
     <command id="cmd_fullZoomToggle"  oncommand="ZoomManager.toggleZoom();"/>
     <command id="cmd_gestureRotateLeft" oncommand="gGestureSupport.rotate(event.sourceEvent)"/>
     <command id="cmd_gestureRotateRight" oncommand="gGestureSupport.rotate(event.sourceEvent)"/>
     <command id="cmd_gestureRotateEnd" oncommand="gGestureSupport.rotateEnd()"/>
--- a/browser/base/content/test/contextMenu/browser_contextmenu_touch.js
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_touch.js
@@ -71,13 +71,12 @@ add_task(async function test_toolbar_con
   let target = document.getElementById("PanelUI-menu-button");
   await openAndCheckContextMenu(toolbarContextMenu, target);
 });
 
 // Test the urlbar input context menu.
 add_task(async function test_urlbar_contextmenu_touch() {
   let urlbar = document.getElementById("urlbar");
   let textBox = document.getAnonymousElementByAttribute(urlbar,
-                                      "anonid", "textbox-input-box");
-  let menu = document.getAnonymousElementByAttribute(textBox,
-                                      "anonid", "input-box-contextmenu");
+                                      "anonid", "moz-input-box");
+  let menu = textBox.menupopup;
   await openAndCheckContextMenu(menu, textBox);
 });
--- a/browser/base/content/test/performance/browser_appmenu.js
+++ b/browser/base/content/test/performance/browser_appmenu.js
@@ -38,17 +38,17 @@ const EXPECTED_APPMENU_OPEN_REFLOWS = [
     maxCount: 7, // This number should only ever go down - never up.
   },
 ];
 
 add_task(async function() {
   await ensureNoPreloadedBrowser();
 
   let textBoxRect = document.getAnonymousElementByAttribute(gURLBar,
-    "anonid", "textbox-input-box").getBoundingClientRect();
+    "anonid", "moz-input-box").getBoundingClientRect();
   let menuButtonRect =
     document.getElementById("PanelUI-menu-button").getBoundingClientRect();
   let frameExpectations = {
     filter: rects => rects.filter(r => !(
       // We expect the menu button to get into the active state.
       r.y1 >= menuButtonRect.top && r.y2 <= menuButtonRect.bottom &&
       r.x1 >= menuButtonRect.left && r.x2 <= menuButtonRect.right
       // XXX For some reason the menu panel isn't in our screenshots,
--- a/browser/base/content/test/performance/browser_tabopen.js
+++ b/browser/base/content/test/performance/browser_tabopen.js
@@ -30,17 +30,17 @@ add_task(async function() {
   await ensureFocusedUrlbar();
 
   let tabStripRect = gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
   let firstTabRect = gBrowser.selectedTab.getBoundingClientRect();
   let firstTabLabelRect =
     document.getAnonymousElementByAttribute(gBrowser.selectedTab, "anonid", "tab-label")
             .getBoundingClientRect();
   let textBoxRect = document.getAnonymousElementByAttribute(gURLBar,
-    "anonid", "textbox-input-box").getBoundingClientRect();
+    "anonid", "moz-input-box").getBoundingClientRect();
   let inRange = (val, min, max) => min <= val && val <= max;
 
   // Add a reflow observer and open a new tab.
   await withPerfObserver(async function() {
     let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
     BrowserOpenTab();
     await BrowserTestUtils.waitForEvent(gBrowser.selectedTab, "transitionend",
                                         false, e => e.propertyName === "max-width");
--- a/browser/base/content/test/performance/browser_tabopen_squeeze.js
+++ b/browser/base/content/test/performance/browser_tabopen_squeeze.js
@@ -30,17 +30,17 @@ add_task(async function() {
   const TAB_COUNT_FOR_SQUEEZE = computeMaxTabCount() - 1;
 
   await createTabs(TAB_COUNT_FOR_SQUEEZE);
 
   await ensureFocusedUrlbar();
 
   let tabStripRect = gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
   let textBoxRect = document.getAnonymousElementByAttribute(gURLBar,
-    "anonid", "textbox-input-box").getBoundingClientRect();
+    "anonid", "moz-input-box").getBoundingClientRect();
 
   await withPerfObserver(async function() {
     let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone");
     BrowserOpenTab();
     await BrowserTestUtils.waitForEvent(gBrowser.selectedTab, "transitionend",
       false, e => e.propertyName === "max-width");
     await switchDone;
   }, {expectedReflows: EXPECTED_REFLOWS,
--- a/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js
+++ b/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js
@@ -34,17 +34,17 @@ add_task(async function() {
   const TAB_COUNT_FOR_OVERFLOW = computeMaxTabCount();
 
   await createTabs(TAB_COUNT_FOR_OVERFLOW);
 
   await ensureFocusedUrlbar();
 
   let tabStripRect = gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
   let textBoxRect = document.getAnonymousElementByAttribute(gURLBar,
-    "anonid", "textbox-input-box").getBoundingClientRect();
+    "anonid", "moz-input-box").getBoundingClientRect();
   let ignoreTabstripRects = {
     filter: rects => rects.filter(r => !(
       // We expect plenty of changed rects within the tab strip.
       r.y1 >= tabStripRect.top && r.y2 <= tabStripRect.bottom &&
       r.x1 >= tabStripRect.left && r.x2 <= tabStripRect.right
     )),
     exceptions: [
       {name: "the urlbar placeolder moves up and down by a few pixels",
--- a/browser/base/content/test/performance/browser_urlbar_keyed_search.js
+++ b/browser/base/content/test/performance/browser_urlbar_keyed_search.js
@@ -138,17 +138,17 @@ add_task(async function() {
   let popup = URLBar.popup;
 
   URLBar.focus();
   URLBar.value = "";
 
   let dropmarkerRect = document.getAnonymousElementByAttribute(gURLBar,
     "anonid", "historydropmarker").getBoundingClientRect();
   let textBoxRect = document.getAnonymousElementByAttribute(gURLBar,
-    "anonid", "textbox-input-box").getBoundingClientRect();
+    "anonid", "moz-input-box").getBoundingClientRect();
 
   await withPerfObserver(async function() {
     let oldInvalidate = popup.invalidate.bind(popup);
     let oldResultsAdded = popup.onResultsAdded.bind(popup);
 
     // We need to invalidate the frame tree outside of the normal
     // mechanism since invalidations and result additions to the
     // URL bar occur without firing JS events (which is how we
--- a/browser/base/content/test/performance/browser_urlbar_search.js
+++ b/browser/base/content/test/performance/browser_urlbar_search.js
@@ -158,17 +158,17 @@ add_task(async function() {
     let hiddenPromise = BrowserTestUtils.waitForEvent(URLBar.popup, "popuphidden");
     EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
     await hiddenPromise;
   };
 
   let dropmarkerRect = document.getAnonymousElementByAttribute(gURLBar,
     "anonid", "historydropmarker").getBoundingClientRect();
   let textBoxRect = document.getAnonymousElementByAttribute(gURLBar,
-    "anonid", "textbox-input-box").getBoundingClientRect();
+    "anonid", "moz-input-box").getBoundingClientRect();
   let expectedRects = {
     filter: rects => rects.filter(r => !(
       // We put text into the urlbar so expect its textbox to change.
       (r.x1 >= textBoxRect.left && r.x2 <= textBoxRect.right &&
        r.y1 >= textBoxRect.top && r.y2 <= textBoxRect.bottom) ||
       // The dropmarker is displayed as active during some of the test.
       // dropmarkerRect.left isn't always an integer, hence the - 1 and + 1
       (r.x1 >= dropmarkerRect.left - 1 && r.x2 <= dropmarkerRect.right + 1 &&
--- a/browser/base/content/test/urlbar/browser_pasteAndGo.js
+++ b/browser/base/content/test/urlbar/browser_pasteAndGo.js
@@ -14,24 +14,22 @@ add_task(async function() {
       await new Promise((resolve, reject) => {
         waitForClipboard(url, function() {
           clipboardHelper.copyString(url);
         }, resolve,
           () => reject(new Error(`Failed to copy string '${url}' to clipboard`))
         );
       });
       let textBox = document.getAnonymousElementByAttribute(gURLBar,
-        "anonid", "textbox-input-box");
-      let cxmenu = document.getAnonymousElementByAttribute(textBox,
-        "anonid", "input-box-contextmenu");
+        "anonid", "moz-input-box");
+      let cxmenu = textBox.menupopup;
       let cxmenuPromise = BrowserTestUtils.waitForEvent(cxmenu, "popupshown");
       EventUtils.synthesizeMouseAtCenter(gURLBar, {type: "contextmenu", button: 2});
       await cxmenuPromise;
-      let menuitem = document.getAnonymousElementByAttribute(textBox,
-        "anonid", "paste-and-go");
+      let menuitem = textBox.getMenuItem("paste-and-go");
       let browserLoadedPromise = BrowserTestUtils.browserLoaded(browser, false, url.replace(/\n/g, ""));
       EventUtils.synthesizeMouseAtCenter(menuitem, {});
       // Using toSource in order to get the newlines escaped:
       info("Paste and go, loading " + url.toSource());
       await browserLoadedPromise;
       ok(true, "Successfully loaded " + url);
     });
   }
@@ -44,24 +42,22 @@ add_task(async function() {
     await new Promise((resolve, reject) => {
       waitForClipboard(url, function() {
         clipboardHelper.copyString(url);
       }, resolve,
         () => reject(new Error(`Failed to copy string '${url}' to clipboard`))
       );
     });
     let textBox = document.getAnonymousElementByAttribute(gURLBar,
-      "anonid", "textbox-input-box");
-    let cxmenu = document.getAnonymousElementByAttribute(textBox,
-      "anonid", "input-box-contextmenu");
+      "anonid", "moz-input-box");
+    let cxmenu = textBox.menupopup;
     let cxmenuPromise = BrowserTestUtils.waitForEvent(cxmenu, "popupshown");
     EventUtils.synthesizeMouseAtCenter(gURLBar, {type: "contextmenu", button: 2});
     await cxmenuPromise;
-    let menuitem = document.getAnonymousElementByAttribute(textBox,
-      "anonid", "paste-and-go");
+    let menuitem = textBox.getMenuItem("paste-and-go");
     let browserLoadedPromise = BrowserTestUtils.browserLoaded(browser, false, url.replace(/\u2028/g, ""));
     EventUtils.synthesizeMouseAtCenter(menuitem, {});
     // Using toSource in order to get the newlines escaped:
     info("Paste and go, loading " + url.toSource());
     await browserLoadedPromise;
     ok(true, "Successfully loaded " + url);
   });
 });
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -21,30 +21,30 @@ file, You can obtain one at http://mozil
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:xbl="http://www.mozilla.org/xbl">
 
   <binding id="urlbar" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete">
 
     <content sizetopopup="pref">
       <xul:hbox flex="1" class="urlbar-textbox-container">
         <children includes="image|deck|stack|box"/>
-        <xul:hbox anonid="textbox-input-box"
-                  class="textbox-input-box urlbar-input-box"
+        <xul:moz-input-box anonid="moz-input-box"
+                  class="urlbar-input-box"
                   flex="1" xbl:inherits="tooltiptext=inputtooltiptext">
           <children/>
           <html:input anonid="scheme"
                       class="urlbar-scheme textbox-input"
                       required="required"
                       xbl:inherits="textoverflow,focused"/>
           <html:input anonid="input"
                       class="autocomplete-textbox urlbar-input textbox-input"
                       allowevents="true"
                       inputmode="mozAwesomebar"
                       xbl:inherits="tooltiptext=inputtooltiptext,value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,focused,textoverflow"/>
-        </xul:hbox>
+        </xul:moz-input-box>
         <xul:image anonid="urlbar-go-button"
                    class="urlbar-go-button urlbar-icon"
                    onclick="gURLBar.handleCommand(event);"
                    tooltiptext="&goEndCap.tooltip;"
                    xbl:inherits="pageproxystate,parentfocused=focused,usertyping"/>
         <xul:dropmarker anonid="historydropmarker"
                         class="urlbar-history-dropmarker urlbar-icon chromeclass-toolbar-additional"
                         tooltiptext="&urlbar.openHistoryPopup.tooltip;"
@@ -90,19 +90,20 @@ file, You can obtain one at http://mozil
         this.inputField.addEventListener("mousedown", this);
         this.inputField.addEventListener("mousemove", this);
         this.inputField.addEventListener("mouseout", this);
         this.inputField.addEventListener("overflow", this);
         this.inputField.addEventListener("underflow", this);
         this.inputField.addEventListener("scrollend", this);
 
         var textBox = document.getAnonymousElementByAttribute(this,
-                                                "anonid", "textbox-input-box");
-        var cxmenu = document.getAnonymousElementByAttribute(textBox,
-                                            "anonid", "input-box-contextmenu");
+                                                "anonid", "moz-input-box");
+        // Force the Custom Element to upgrade until Bug 1470242 handles this:
+        customElements.upgrade(textBox);
+        var cxmenu = textBox.menupopup;
         var pasteAndGo;
         cxmenu.addEventListener("popupshowing", function() {
           if (!pasteAndGo)
             return;
           var controller = document.commandDispatcher.getControllerForCommand("cmd_paste");
           var enabled = controller.isCommandEnabled("cmd_paste");
           if (enabled)
             pasteAndGo.removeAttribute("disabled");
--- a/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js
+++ b/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js
@@ -40,18 +40,18 @@ add_task(async function searchbar_in_pan
   await SpecialPowers.pushPrefEnv({set: [["browser.search.suggest.enabled", false]]});
   let dontShowPopup = e => e.preventDefault();
   let searchbarPopup = searchbar.textbox.popup;
   searchbarPopup.addEventListener("popupshowing", dontShowPopup);
 
   searchbar.value = "foo";
   searchbar.focus();
   // Reaching into this context menu is pretty evil, but hey... it's a test.
-  let textbox = document.getAnonymousElementByAttribute(searchbar.textbox, "anonid", "textbox-input-box");
-  let contextmenu = document.getAnonymousElementByAttribute(textbox, "anonid", "input-box-contextmenu");
+  let textbox = document.getAnonymousElementByAttribute(searchbar.textbox, "anonid", "moz-input-box");
+  let contextmenu = textbox.menupopup;
   let contextMenuShown = promisePanelElementShown(window, contextmenu);
   EventUtils.synthesizeMouseAtCenter(searchbar, {type: "contextmenu", button: 2});
   await contextMenuShown;
 
   ok(isOverflowOpen(), "Panel should still be open");
 
   let selectAll = contextmenu.querySelector("[cmd='cmd_selectAll']");
   let contextMenuHidden = promisePanelElementHidden(window, contextmenu);
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload.js
@@ -197,17 +197,15 @@ add_task(async function testBrowserActio
   // Make sure the mouse isn't hovering over the browserAction widget.
   EventUtils.synthesizeMouseAtCenter(win.gURLBar, {type: "mouseover"}, win);
 
   await extension.startup();
 
   let widget = getBrowserActionWidget(extension).forWindow(win);
   EventUtils.synthesizeMouseAtCenter(widget.node, {type: "mousedown", button: 0}, win);
 
-  await new Promise(resolve => setTimeout(resolve, 100));
+  await extension.awaitMessage("tabTitle");
 
   EventUtils.synthesizeMouseAtCenter(widget.node, {type: "mouseup", button: 0}, win);
 
-  await extension.awaitMessage("tabTitle");
-
   await extension.unload();
   await BrowserTestUtils.closeWindow(win);
 });
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -498,19 +498,21 @@
         if (document.getBindingParent(this).parentNode.parentNode.localName ==
             "toolbarpaletteitem")
           return;
 
         if (Services.prefs.getBoolPref("browser.urlbar.clickSelectsAll"))
           this.setAttribute("clickSelectsAll", true);
 
         var textBox = document.getAnonymousElementByAttribute(this,
-                                              "anonid", "textbox-input-box");
-        var cxmenu = document.getAnonymousElementByAttribute(textBox,
-                                          "anonid", "input-box-contextmenu");
+                                              "anonid", "moz-input-box");
+
+        // Force the Custom Element to upgrade until Bug 1470242 handles this:
+        customElements.upgrade(textBox);
+        var cxmenu = textBox.menupopup;
         cxmenu.addEventListener("popupshowing",
                                 () => { this.initContextMenu(cxmenu); },
                                 {capture: true, once: true});
 
         this.setAttribute("aria-owns", this.popup.id);
         document.getBindingParent(this)._textboxInitialized = true;
       ]]></constructor>
 
--- a/browser/components/search/test/browser_searchbar_openpopup.js
+++ b/browser/components/search/test/browser_searchbar_openpopup.js
@@ -195,17 +195,17 @@ add_task(async function click_opens_popu
   textbox.value = "";
 });
 
 // Right clicking in a non-empty search box when unfocused should open the edit context menu.
 add_no_popup_task(async function right_click_doesnt_open_popup() {
   gURLBar.focus();
   textbox.value = "foo";
 
-  let contextPopup = document.getAnonymousElementByAttribute(textbox.inputField.parentNode, "anonid", "input-box-contextmenu");
+  let contextPopup = textbox.inputField.parentNode.menupopup;
   let promise = promiseEvent(contextPopup, "popupshown");
   context_click(textbox);
   await promise;
 
   is(Services.focus.focusedElement, textbox.inputField, "Should have focused the search bar");
   is(textbox.selectionStart, 0, "Should have selected all of the text");
   is(textbox.selectionEnd, 3, "Should have selected all of the text");
 
@@ -297,19 +297,17 @@ add_task(async function contextmenu_clos
 
   promise = promiseEvent(searchPopup, "popuphidden");
 
   // synthesizeKey does not work with VK_CONTEXT_MENU (bug 1127368)
   EventUtils.synthesizeMouseAtCenter(textbox, { type: "contextmenu", button: null });
 
   await promise;
 
-  let contextPopup =
-    document.getAnonymousElementByAttribute(textbox.inputField.parentNode,
-                                            "anonid", "input-box-contextmenu");
+  let contextPopup = textbox.inputField.parentNode.menupopup;
   promise = promiseEvent(contextPopup, "popuphidden");
   contextPopup.hidePopup();
   await promise;
 
   textbox.value = "";
 });
 
 // Tabbing to the search box should open the popup if it contains text.
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -562,17 +562,17 @@ html|input.urlbar-input {
   }
 
   #editBookmarkPanel .expander-up > .button-box > .button-icon,
   #editBookmarkPanel .expander-down > .button-box > .button-icon {
     width: 9px;
   }
 }
 
-#editBMPanel_tagsField > .textbox-input-box > html|*.textbox-input::placeholder {
+#editBMPanel_tagsField > moz-input-box > html|*.textbox-input::placeholder {
   opacity: 1.0;
   color: #bbb;
 }
 
 /* ----- SIDEBAR ELEMENTS ----- */
 
 %include ../shared/sidebar.inc.css
 
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -542,17 +542,17 @@ menuitem.bookmark-item {
 @media (-moz-windows-default-theme: 0) {
   #urlbar:not(:-moz-lwtheme):not([focused="true"]),
   .searchbar-textbox:not(:-moz-lwtheme):not([focused="true"]) {
     border-color: ThreeDShadow;
   }
 }
 
 html|*.urlbar-input:-moz-lwtheme::placeholder,
-.searchbar-textbox:-moz-lwtheme > .urlbar-textbox-container > .textbox-input-box > html|*.textbox-input::placeholder {
+.searchbar-textbox:-moz-lwtheme > .urlbar-textbox-container > moz-input-box > html|*.textbox-input::placeholder {
   opacity: 1.0;
   color: #777;
 }
 
 /* ::::: URL Bar Zoom Reset Button ::::: */
 @keyframes urlbar-zoom-reset-pulse {
   0% {
     transform: scale(0);
--- a/chrome/nsChromeRegistry.cpp
+++ b/chrome/nsChromeRegistry.cpp
@@ -467,57 +467,16 @@ nsChromeRegistry::FlushAllCaches()
   nsCOMPtr<nsIObserverService> obsSvc =
     mozilla::services::GetObserverService();
   NS_ASSERTION(obsSvc, "Couldn't get observer service.");
 
   obsSvc->NotifyObservers((nsIChromeRegistry*) this,
                           NS_CHROME_FLUSH_TOPIC, nullptr);
 }
 
-// xxxbsmedberg Move me to nsIWindowMediator
-NS_IMETHODIMP
-nsChromeRegistry::ReloadChrome()
-{
-  FlushAllCaches();
-  // Do a reload of all top level windows.
-  nsresult rv = NS_OK;
-
-  // Get the window mediator
-  nsCOMPtr<nsIWindowMediator> windowMediator
-    (do_GetService(NS_WINDOWMEDIATOR_CONTRACTID));
-  if (windowMediator) {
-    nsCOMPtr<nsISimpleEnumerator> windowEnumerator;
-
-    rv = windowMediator->GetEnumerator(nullptr, getter_AddRefs(windowEnumerator));
-    if (NS_SUCCEEDED(rv)) {
-      // Get each dom window
-      bool more;
-      rv = windowEnumerator->HasMoreElements(&more);
-      if (NS_FAILED(rv)) return rv;
-      while (more) {
-        nsCOMPtr<nsISupports> protoWindow;
-        rv = windowEnumerator->GetNext(getter_AddRefs(protoWindow));
-        if (NS_SUCCEEDED(rv)) {
-          nsCOMPtr<nsPIDOMWindowOuter> domWindow = do_QueryInterface(protoWindow);
-          if (domWindow) {
-            Location* location = domWindow->GetLocation();
-            if (location) {
-              rv = location->Reload(false);
-              if (NS_FAILED(rv)) return rv;
-            }
-          }
-        }
-        rv = windowEnumerator->HasMoreElements(&more);
-        if (NS_FAILED(rv)) return rv;
-      }
-    }
-  }
-  return rv;
-}
-
 NS_IMETHODIMP
 nsChromeRegistry::AllowScriptsForPackage(nsIURI* aChromeURI, bool *aResult)
 {
   nsresult rv;
   *aResult = false;
 
 #ifdef DEBUG
   bool isChrome;
--- a/chrome/nsChromeRegistry.h
+++ b/chrome/nsChromeRegistry.h
@@ -34,17 +34,16 @@ class nsIURL;
 class nsChromeRegistry : public nsIToolkitChromeRegistry,
                          public nsIObserver,
                          public nsSupportsWeakReference
 {
 public:
   NS_DECL_ISUPPORTS
 
   // nsIXULChromeRegistry methods:
-  NS_IMETHOD ReloadChrome() override;
   NS_IMETHOD RefreshSkins() override;
   NS_IMETHOD AllowScriptsForPackage(nsIURI* url,
                                     bool* _retval) override;
   NS_IMETHOD AllowContentToAccess(nsIURI* url,
                                   bool* _retval) override;
   NS_IMETHOD CanLoadURLRemotely(nsIURI* url,
                                 bool* _retval) override;
   NS_IMETHOD MustLoadURLRemotely(nsIURI* url,
--- a/chrome/nsIChromeRegistry.idl
+++ b/chrome/nsIChromeRegistry.idl
@@ -43,19 +43,16 @@ interface nsIChromeRegistry : nsISupport
    * returns whether XPCNativeWrappers are enabled for aURI.
    */
   [notxpcom] boolean wrappersEnabled(in nsIURI aURI);
 };
 
 [scriptable, uuid(93251ddf-5e85-4172-ac2a-31780562974f)]
 interface nsIXULChromeRegistry : nsIChromeRegistry
 {
-  /* Should be called when locales change to reload all chrome (including XUL). */
-  void reloadChrome();
-
   // If the optional asBCP47 parameter is true, the locale code will be
   // converted to a BCP47 language tag; in particular, this means that
   // "ja-JP-mac" will be returned as "ja-JP-x-lvariant-mac", which can be
   // passed to ECMA402 Intl API methods without throwing a RangeError.
   ACString getSelectedLocale(in ACString packageName,
                              [optional] in boolean asBCP47);
   
   // Get whether the default writing direction of the locale is RTL
--- a/devtools/client/aboutdebugging/aboutdebugging.css
+++ b/devtools/client/aboutdebugging/aboutdebugging.css
@@ -223,16 +223,17 @@ button {
 }
 
 .addons-tip-icon {
   width: 24px;
   height: 24px;
   background-image: url(chrome://devtools/skin/images/help.svg);
   background-repeat: no-repeat;
   background-size: 24px;
+  flex-shrink: 0;
   margin-inline-end: 8px;
 }
 
 .error-page {
   display: flex;
   justify-content: center;
   align-items: center;
   flex-direction: column;
--- a/devtools/client/debugger/new/test/mochitest/browser.ini
+++ b/devtools/client/debugger/new/test/mochitest/browser.ini
@@ -623,16 +623,17 @@ support-files =
   examples/simple2.js
   examples/simple3.js
   examples/frames.js
   examples/pause-points.js
   examples/script-mutate.js
   examples/script-switching-02.js
   examples/script-switching-01.js
   examples/times2.js
+  examples/doc_rr_basic.html
 
 [browser_dbg-asm.js]
 [browser_dbg-async-stepping.js]
 [browser_dbg-sourcemapped-breakpoint-console.js]
 skip-if = (os == "win" && ccov) # Bug 1453549
 [browser_dbg-sourcemapped-scopes.js]
 skip-if = ccov || (verify && debug && (os == 'linux')) # Bug 1441545
 [browser_dbg-sourcemapped-stepping.js]
@@ -711,8 +712,10 @@ skip-if = os == 'linux' && !asan # bug 1
 [browser_dbg-sources-named-eval.js]
 [browser_dbg-stepping.js]
 skip-if = debug || (verify && (os == 'win')) || (os == "win" && os_version == "6.1")
 [browser_dbg-tabs.js]
 [browser_dbg-tabs-pretty-print.js]
 [browser_dbg-toggling-tools.js]
 [browser_dbg-wasm-sourcemaps.js]
 skip-if = true
+[browser_dbg_rr_breakpoints-01.js]
+skip-if = os != "mac" || debug
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg_rr_breakpoints-01.js
@@ -0,0 +1,43 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test basic breakpoint functionality in web replay.
+async function test() {
+  waitForExplicitFinish();
+
+  let tab = gBrowser.addTab(null, { recordExecution: "*" });
+  gBrowser.selectedTab = tab;
+  openTrustedLinkIn(EXAMPLE_URL + "doc_rr_basic.html", "current");
+  await once(Services.ppmm, "RecordingFinished");
+
+  let toolbox = await attachDebugger(tab), client = toolbox.threadClient;
+  await client.interrupt();
+  await setBreakpoint(client, "doc_rr_basic.html", 21);
+
+  // Visit a lot of breakpoints so that we are sure we have crossed major
+  // checkpoint boundaries.
+  await rewindToLine(client, 21);
+  await checkEvaluateInTopFrame(client, "number", 10);
+  await rewindToLine(client, 21);
+  await checkEvaluateInTopFrame(client, "number", 9);
+  await rewindToLine(client, 21);
+  await checkEvaluateInTopFrame(client, "number", 8);
+  await rewindToLine(client, 21);
+  await checkEvaluateInTopFrame(client, "number", 7);
+  await rewindToLine(client, 21);
+  await checkEvaluateInTopFrame(client, "number", 6);
+  await resumeToLine(client, 21);
+  await checkEvaluateInTopFrame(client, "number", 7);
+  await resumeToLine(client, 21);
+  await checkEvaluateInTopFrame(client, "number", 8);
+  await resumeToLine(client, 21);
+  await checkEvaluateInTopFrame(client, "number", 9);
+  await resumeToLine(client, 21);
+  await checkEvaluateInTopFrame(client, "number", 10);
+
+  await toolbox.destroy();
+  await gBrowser.removeTab(tab);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/doc_rr_basic.html
@@ -0,0 +1,38 @@
+<html lang="en" dir="ltr">
+<body>
+<div id="maindiv">Hello World!</div>
+</body>
+<script>
+const cpmm = SpecialPowers.Services.cpmm;
+function recordingFinished() {
+  cpmm.sendAsyncMessage("RecordingFinished");
+}
+var number = 0;
+function f() {
+  updateNumber();
+  if (number >= 10) {
+    window.setTimeout(recordingFinished);
+    return;
+  }
+  window.setTimeout(f, 1);
+}
+function updateNumber() {
+  number++;
+  document.getElementById("maindiv").innerHTML = "Number: " + number;
+  testStepping();
+}
+function testStepping() {
+  var a = 0;
+  testStepping2();
+  return a;
+}
+function testStepping2() {
+  var c = this; // Note: using 'this' causes the script to have a prologue.
+  c++;
+  c--;
+}
+window.setTimeout(f, 1);
+// Simulate a longer recording by marking major checkpoints whenever possible.
+SpecialPowers.Cu.recordReplayDirective(/* AlwaysMarkMajorCheckpoints */ 4);
+</script>
+</html>
--- a/devtools/client/framework/components/MeatballMenu.js
+++ b/devtools/client/framework/components/MeatballMenu.js
@@ -12,16 +12,28 @@ const MenuItem = createFactory(
 const MenuList = createFactory(
   require("devtools/client/shared/components/menu/MenuList")
 );
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const { hr } = dom;
 const { openDocLink } = require("devtools/client/shared/link");
 const { assert } = require("devtools/shared/DevToolsUtils");
 
+const openDevToolsDocsLink = () => {
+  openDocLink(
+    "https://developer.mozilla.org/docs/Tools?utm_source=devtools&utm_medium=tabbar-menu"
+  );
+};
+
+const openCommunityLink = () => {
+  openDocLink(
+    "https://discourse.mozilla.org/c/devtools?utm_source=devtools&utm_medium=tabbar-menu"
+  );
+};
+
 class MeatballMenu extends PureComponent {
   static get propTypes() {
     return {
       // The id of the currently selected tool, e.g. "inspector"
       currentToolId: PropTypes.string,
 
       // List of possible docking options.
       hostTypes: PropTypes.arrayOf(
@@ -118,101 +130,99 @@ class MeatballMenu extends PureComponent
         default:
           assert(false, `Unexpected hostType.position: ${hostType.position}`);
           break;
       }
 
       items.push(
         MenuItem({
           id: `toolbox-meatball-menu-dock-${hostType.position}`,
+          key: `dock-${hostType.position}`,
           label: this.props.L10N.getStr(l10nkey),
-          onClick: () => hostType.switchHost(),
+          onClick: hostType.switchHost,
           checked: hostType.position === this.props.currentHostType,
           className: "iconic",
         })
       );
     }
 
     if (items.length) {
-      items.push(hr());
+      items.push(hr({ key: "dock-separator" }));
     }
 
     // Split console
     if (this.props.currentToolId !== "webconsole") {
       const l10nkey = this.props.isSplitConsoleActive
         ? "toolbox.meatballMenu.hideconsole.label"
         : "toolbox.meatballMenu.splitconsole.label";
       items.push(
         MenuItem({
           id: "toolbox-meatball-menu-splitconsole",
+          key: "splitconsole",
           label: this.props.L10N.getStr(l10nkey),
           accelerator: "Esc",
           onClick: this.props.toggleSplitConsole,
           className: "iconic",
         })
       );
     }
 
     // Disable pop-up autohide
     //
     // If |disableAutohide| is undefined, it means this feature is not available
     // in this context.
     if (typeof this.props.disableAutohide !== "undefined") {
       items.push(
         MenuItem({
           id: "toolbox-meatball-menu-noautohide",
+          key: "noautohide",
           label: this.props.L10N.getStr(
             "toolbox.meatballMenu.noautohide.label"
           ),
           type: "checkbox",
           checked: this.props.disableAutohide,
           onClick: this.props.toggleNoAutohide,
           className: "iconic",
         })
       );
     }
 
     // Settings
     items.push(
       MenuItem({
         id: "toolbox-meatball-menu-settings",
+        key: "settings",
         label: this.props.L10N.getStr("toolbox.meatballMenu.settings.label"),
         accelerator: this.props.L10N.getStr("toolbox.help.key"),
-        onClick: () => this.props.toggleOptions(),
+        onClick: this.props.toggleOptions,
         className: "iconic",
       })
     );
 
-    items.push(hr());
+    items.push(hr({ key: "docs-separator" }));
 
     // Getting started
     items.push(
       MenuItem({
         id: "toolbox-meatball-menu-documentation",
+        key: "documentation",
         label: this.props.L10N.getStr(
           "toolbox.meatballMenu.documentation.label"
         ),
-        onClick: () => {
-          openDocLink(
-            "https://developer.mozilla.org/docs/Tools?utm_source=devtools&utm_medium=tabbar-menu"
-          );
-        },
+        onClick: openDevToolsDocsLink,
       })
     );
 
     // Give feedback
     items.push(
       MenuItem({
         id: "toolbox-meatball-menu-community",
+        key: "community",
         label: this.props.L10N.getStr("toolbox.meatballMenu.community.label"),
-        onClick: () => {
-          openDocLink(
-            "https://discourse.mozilla.org/c/devtools?utm_source=devtools&utm_medium=tabbar-menu"
-          );
-        },
+        onClick: openCommunityLink,
       })
     );
 
     return MenuList({ id: "toolbox-meatball-menu" }, items);
   }
 }
 
 module.exports = MeatballMenu;
--- a/devtools/client/shared/components/menu/MenuButton.js
+++ b/devtools/client/shared/components/menu/MenuButton.js
@@ -12,16 +12,25 @@ const PropTypes = require("devtools/clie
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const Services = require("Services");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const { button } = dom;
 const {
   HTMLTooltip,
 } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
 
+// Return a copy of |obj| minus |fields|.
+const omit = (obj, fields) => {
+  const objCopy = { ...obj };
+  for (const field of fields) {
+    delete objCopy[field];
+  }
+  return objCopy;
+};
+
 class MenuButton extends PureComponent {
   static get propTypes() {
     return {
       // The document to be used for rendering the menu popup.
       doc: PropTypes.object.isRequired,
 
       // An optional ID to assign to the menu's container tooltip object.
       menuId: PropTypes.string,
@@ -261,17 +270,19 @@ class MenuButton extends PureComponent {
     //
     // Bug 1472942: Do this for all users of HTMLTooltip.
     const menu = ReactDOM.createPortal(
       this.props.children,
       this.tooltip.panel
     );
 
     const buttonProps = {
-      ...this.props,
+      // Pass through any props set on the button, except the ones we handle
+      // here.
+      ...omit(this.props, Object.keys(MenuButton.propTypes)),
       onClick: this.onClick,
       "aria-expanded": this.state.expanded,
       "aria-haspopup": "menu",
       ref: this.setButtonRef,
     };
 
     if (this.state.expanded) {
       buttonProps.onKeyDown = this.onKeyDown;
--- a/devtools/client/shared/components/menu/MenuItem.js
+++ b/devtools/client/shared/components/menu/MenuItem.js
@@ -113,24 +113,24 @@ class MenuItem extends PureComponent {
       if (this.props.checked) {
         attr["aria-checked"] = true;
       }
     } else {
       attr.role = "menuitem";
     }
 
     const textLabel = span(
-      { className: "label", ref: this.labelRef },
+      { key: "label", className: "label", ref: this.labelRef },
       this.props.label
     );
     const children = [textLabel];
 
     if (typeof this.props.accelerator !== "undefined") {
       const acceleratorLabel = span(
-        { className: "accelerator" },
+        { key: "accelerator", className: "accelerator" },
         this.props.accelerator
       );
       children.push(acceleratorLabel);
     }
 
     return li(
       { className: "menuitem", role: "presentation" },
       button(attr, children)
--- a/dom/abort/AbortController.cpp
+++ b/dom/abort/AbortController.cpp
@@ -51,17 +51,17 @@ AbortController::GetParentObject() const
 {
   return mGlobal;
 }
 
 AbortSignal*
 AbortController::Signal()
 {
   if (!mSignal) {
-    mSignal = new AbortSignal(this, mAborted);
+    mSignal = new AbortSignal(mGlobal, mAborted);
   }
 
   return mSignal;
 }
 
 void
 AbortController::Abort()
 {
--- a/dom/abort/AbortSignal.cpp
+++ b/dom/abort/AbortSignal.cpp
@@ -1,45 +1,41 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "AbortSignal.h"
 
-#include "AbortController.h"
 #include "mozilla/dom/Event.h"
 #include "mozilla/dom/AbortSignalBinding.h"
 
 namespace mozilla {
 namespace dom {
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(AbortSignal)
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(AbortSignal,
                                                   DOMEventTargetHelper)
-  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mController)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(AbortSignal,
                                                 DOMEventTargetHelper)
-  NS_IMPL_CYCLE_COLLECTION_UNLINK(mController)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AbortSignal)
 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
 
 NS_IMPL_ADDREF_INHERITED(AbortSignal, DOMEventTargetHelper)
 NS_IMPL_RELEASE_INHERITED(AbortSignal, DOMEventTargetHelper)
 
-AbortSignal::AbortSignal(AbortController* aController,
+AbortSignal::AbortSignal(nsIGlobalObject* aGlobalObject,
                          bool aAborted)
-  : DOMEventTargetHelper(aController->GetParentObject())
-  , mController(aController)
+  : DOMEventTargetHelper(aGlobalObject)
   , mAborted(aAborted)
 {}
 
 AbortSignal::AbortSignal(bool aAborted)
   : mAborted(aAborted)
 {}
 
 JSObject*
--- a/dom/abort/AbortSignal.h
+++ b/dom/abort/AbortSignal.h
@@ -7,17 +7,16 @@
 #ifndef mozilla_dom_AbortSignal_h
 #define mozilla_dom_AbortSignal_h
 
 #include "mozilla/DOMEventTargetHelper.h"
 
 namespace mozilla {
 namespace dom {
 
-class AbortController;
 class AbortSignal;
 
 // This class must be implemented by objects who want to follow a AbortSignal.
 class AbortFollower
 {
 public:
   virtual void Abort() = 0;
 
@@ -38,17 +37,17 @@ protected:
 
 class AbortSignal final : public DOMEventTargetHelper
                         , public AbortFollower
 {
 public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AbortSignal, DOMEventTargetHelper)
 
-  AbortSignal(AbortController* aController, bool aAborted);
+  AbortSignal(nsIGlobalObject* aGlobalObject, bool aAborted);
   explicit AbortSignal(bool aAborted);
 
   JSObject*
   WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   bool
   Aborted() const;
 
@@ -61,18 +60,16 @@ public:
   AddFollower(AbortFollower* aFollower);
 
   void
   RemoveFollower(AbortFollower* aFollower);
 
 private:
   ~AbortSignal() = default;
 
-  RefPtr<AbortController> mController;
-
   // Raw pointers. AbortFollower unregisters itself in the DTOR.
   nsTArray<AbortFollower*> mFollowers;
 
   bool mAborted;
 };
 
 } // dom namespace
 } // mozilla namespace
--- a/dom/base/Text.h
+++ b/dom/base/Text.h
@@ -11,16 +11,18 @@
 #include "mozilla/ErrorResult.h"
 
 namespace mozilla {
 namespace dom {
 
 class Text : public CharacterData
 {
 public:
+  NS_IMPL_FROMNODE_HELPER(Text, IsText())
+
   explicit Text(already_AddRefed<mozilla::dom::NodeInfo>& aNodeInfo)
     : CharacterData(aNodeInfo)
   {}
 
   explicit Text(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
     : CharacterData(aNodeInfo)
   {}
 
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -1686,20 +1686,26 @@ nsGlobalWindowInner::InnerSetNewDocument
   if (MOZ_LOG_TEST(gDOMLeakPRLogInner, LogLevel::Debug)) {
     nsIURI *uri = aDocument->GetDocumentURI();
     MOZ_LOG(gDOMLeakPRLogInner, LogLevel::Debug,
             ("DOMWINDOW %p SetNewDocument %s",
              this, uri ? uri->GetSpecOrDefault().get() : ""));
   }
 
   mDoc = aDocument;
-  ClearDocumentDependentSlots(aCx);
   mFocusedElement = nullptr;
   mLocalStorage = nullptr;
   mSessionStorage = nullptr;
+  mPerformance = nullptr;
+
+  // This must be called after nullifying the internal objects because here we
+  // could recreate them, calling the getter methods, and store them into the JS
+  // slots. If we nullify them after, the slot values and the objects will be
+  // out of sync.
+  ClearDocumentDependentSlots(aCx);
 
 #ifdef DEBUG
   mLastOpenedURI = aDocument->GetDocumentURI();
 #endif
 
   Telemetry::Accumulate(Telemetry::INNERWINDOWS_WITH_MUTATION_LISTENERS,
                         mMutationBits ? 1 : 0);
 
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -1915,17 +1915,22 @@ nsGlobalWindowOuter::SetNewDocument(nsID
 
       if (newInnerWindow->mDoc != aDocument) {
         newInnerWindow->mDoc = aDocument;
 
         // The storage objects contain the URL of the window. We have to
         // recreate them when the innerWindow is reused.
         newInnerWindow->mLocalStorage = nullptr;
         newInnerWindow->mSessionStorage = nullptr;
-
+        newInnerWindow->mPerformance = nullptr;
+
+        // This must be called after nullifying the internal objects because
+        // here we could recreate them, calling the getter methods, and store
+        // them into the JS slots. If we nullify them after, the slot values and
+        // the objects will be out of sync.
         newInnerWindow->ClearDocumentDependentSlots(cx);
 
         // When replacing an initial about:blank document we call
         // ExecutionReady again to update the client creation URL.
         rv = newInnerWindow->ExecutionReady();
         NS_ENSURE_SUCCESS(rv, rv);
       }
     } else {
@@ -6425,16 +6430,17 @@ nsGlobalWindowOuter::GetPrivateRoot()
         top = parent->GetTop();
       }
     }
   }
 
   return top;
 }
 
+// This has a caller in Windows-only code (nsNativeAppSupportWin).
 Location*
 nsGlobalWindowOuter::GetLocation()
 {
   // This method can be called on the outer window as well.
   FORWARD_TO_INNER(GetLocation, (), nullptr);
 }
 
 void
--- a/dom/file/BlobSet.cpp
+++ b/dom/file/BlobSet.cpp
@@ -54,16 +54,31 @@ BlobSet::AppendString(const nsAString& a
     StringBlobImpl::Create(utf8Str, EmptyString());
   return AppendBlobImpl(blobImpl);
 }
 
 nsresult
 BlobSet::AppendBlobImpl(BlobImpl* aBlobImpl)
 {
   NS_ENSURE_ARG_POINTER(aBlobImpl);
+
+  // If aBlobImpl is a MultipartBlobImpl, let's append the sub-blobImpls
+  // instead.
+  const nsTArray<RefPtr<BlobImpl>>* subBlobs = aBlobImpl->GetSubBlobImpls();
+  if (subBlobs) {
+    for (BlobImpl* subBlob : *subBlobs) {
+      nsresult rv = AppendBlobImpl(subBlob);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+    }
+
+    return NS_OK;
+  }
+
   if (NS_WARN_IF(!mBlobImpls.AppendElement(aBlobImpl, fallible))) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
   return NS_OK;
 }
 
 } // dom namespace
 } // mozilla namespace
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/crashtests/1480354.html
@@ -0,0 +1,14 @@
+<html>
+<body>
+  <script>
+function createBlob(blocksize) {
+  var blob = new Blob();
+  while (blob.size < 25 * 1024 * 1024) { // 25 MB
+    blob = new Blob([blob, new Uint8Array(blocksize)]);
+  }
+  URL.createObjectURL(blob);
+}
+createBlob(1024 * 25);
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/file/tests/crashtests/crashtests.list
@@ -0,0 +1,1 @@
+load 1480354.html
--- a/dom/media/AudioDeviceInfo.cpp
+++ b/dom/media/AudioDeviceInfo.cpp
@@ -74,42 +74,43 @@ AudioDeviceInfo::AudioDeviceInfo(AudioDe
   MOZ_ASSERT(mSupportedFormat & (FMT_S16LE | FMT_S16BE | FMT_F32LE | FMT_F32BE),
              "Wrong supported format");
   MOZ_ASSERT(mDefaultFormat == FMT_S16LE ||
              mDefaultFormat == FMT_S16BE ||
              mDefaultFormat == FMT_F32LE ||
              mDefaultFormat == FMT_F32BE, "Wrong default format");
 }
 
-Maybe<AudioDeviceID>
-AudioDeviceInfo::GetDeviceID()
+AudioDeviceID
+AudioDeviceInfo::DeviceID() const
 {
-  if (mDeviceId) {
-    return Some(mDeviceId);
-  }
-  return Nothing();
+  return mDeviceId;
 }
-
-const nsString& AudioDeviceInfo::FriendlyName()
+const nsString& AudioDeviceInfo::Name() const
 {
   return mName;
 }
-uint32_t AudioDeviceInfo::MaxChannels()
+uint32_t AudioDeviceInfo::MaxChannels() const
 {
   return mMaxChannels;
 }
-uint32_t AudioDeviceInfo::Type()
+uint32_t AudioDeviceInfo::Type() const
 {
   return mType;
 }
-uint32_t AudioDeviceInfo::State()
+uint32_t AudioDeviceInfo::State() const
 {
   return mState;
 }
 
+bool AudioDeviceInfo::Preferred() const
+{
+  return mPreferred;
+}
+
 /* readonly attribute DOMString name; */
 NS_IMETHODIMP
 AudioDeviceInfo::GetName(nsAString& aName)
 {
   aName = mName;
   return NS_OK;
 }
 
--- a/dom/media/AudioDeviceInfo.h
+++ b/dom/media/AudioDeviceInfo.h
@@ -3,17 +3,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/. */
 
 #ifndef MOZILLA_AudioDeviceInfo_H
 #define MOZILLA_AudioDeviceInfo_H
 
 #include "nsIAudioDeviceInfo.h"
 #include "CubebUtils.h"
-#include "nsString.h"
 #include "mozilla/Maybe.h"
 
 // This is mapped to the cubeb_device_info.
 class AudioDeviceInfo final : public nsIAudioDeviceInfo
 {
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIAUDIODEVICEINFO
@@ -30,25 +29,24 @@ public:
                   uint16_t aSupportedFormat,
                   uint16_t aDefaultFormat,
                   uint32_t aMaxChannels,
                   uint32_t aDefaultRate,
                   uint32_t aMaxRate,
                   uint32_t aMinRate,
                   uint32_t aMaxLatency,
                   uint32_t aMinLatency);
+  explicit AudioDeviceInfo(cubeb_device_info* aInfo);
 
-  explicit AudioDeviceInfo(cubeb_device_info* aInfo);
-  // It is possible to not know the device identifier here. It depends on which
-  // ctor this instance has been constructed with.
-  mozilla::Maybe<AudioDeviceID> GetDeviceID();
-  const nsString& FriendlyName();
-  uint32_t MaxChannels();
-  uint32_t Type();
-  uint32_t State();
+  AudioDeviceID DeviceID() const;
+  const nsString& Name() const;
+  uint32_t MaxChannels() const;
+  uint32_t Type() const;
+  uint32_t State() const;
+  bool Preferred() const;
 private:
   virtual ~AudioDeviceInfo() = default;
 
   const AudioDeviceID mDeviceId;
   const nsString mName;
   const nsString mGroupId;
   const nsString mVendor;
   const uint16_t mType;
--- a/dom/media/CubebUtils.cpp
+++ b/dom/media/CubebUtils.cpp
@@ -195,27 +195,27 @@ void PrefChanged(const char* aPref, void
     Preferences::GetCString(aPref, value);
     StaticMutexAutoLock lock(sMutex);
     if (value.IsEmpty()) {
       sVolumeScale = 1.0;
     } else {
       sVolumeScale = std::max<double>(0, PR_strtod(value.get(), nullptr));
     }
   } else if (strcmp(aPref, PREF_CUBEB_LATENCY_PLAYBACK) == 0) {
+    StaticMutexAutoLock lock(sMutex);
     // Arbitrary default stream latency of 100ms.  The higher this
     // value, the longer stream volume changes will take to become
     // audible.
     sCubebPlaybackLatencyPrefSet = Preferences::HasUserValue(aPref);
     uint32_t value = Preferences::GetUint(aPref, CUBEB_NORMAL_LATENCY_MS);
-    StaticMutexAutoLock lock(sMutex);
     sCubebPlaybackLatencyInMilliseconds = std::min<uint32_t>(std::max<uint32_t>(value, 1), 1000);
   } else if (strcmp(aPref, PREF_CUBEB_LATENCY_MSG) == 0) {
+    StaticMutexAutoLock lock(sMutex);
     sCubebMSGLatencyPrefSet = Preferences::HasUserValue(aPref);
     uint32_t value = Preferences::GetUint(aPref, CUBEB_NORMAL_LATENCY_FRAMES);
-    StaticMutexAutoLock lock(sMutex);
     // 128 is the block size for the Web Audio API, which limits how low the
     // latency can be here.
     // We don't want to limit the upper limit too much, so that people can
     // experiment.
     sCubebMSGLatencyInFrames = std::min<uint32_t>(std::max<uint32_t>(value, 128), 1e6);
   } else if (strcmp(aPref, PREF_CUBEB_FORCE_SAMPLE_RATE) == 0) {
     StaticMutexAutoLock lock(sMutex);
     sCubebForcedSampleRate = Preferences::GetUint(aPref);
@@ -229,16 +229,17 @@ void PrefChanged(const char* aPref, void
     } else if (value.EqualsLiteral("normal")) {
       cubeb_set_log_callback(CUBEB_LOG_NORMAL, CubebLogCallback);
       cubebLog->SetLevel(LogLevel::Error);
     } else if (value.IsEmpty()) {
       cubeb_set_log_callback(CUBEB_LOG_DISABLED, nullptr);
       cubebLog->SetLevel(LogLevel::Disabled);
     }
   } else if (strcmp(aPref, PREF_CUBEB_BACKEND) == 0) {
+    StaticMutexAutoLock lock(sMutex);
     nsAutoCString value;
     Preferences::GetCString(aPref, value);
     if (value.IsEmpty()) {
       sCubebBackendName = nullptr;
     } else {
       sCubebBackendName = new char[value.Length() + 1];
       PodCopy(sCubebBackendName.get(), value.get(), value.Length());
       sCubebBackendName[value.Length()] = 0;
@@ -295,16 +296,25 @@ double GetVolumeScale()
 }
 
 cubeb* GetCubebContext()
 {
   StaticMutexAutoLock lock(sMutex);
   return GetCubebContextUnlocked();
 }
 
+// This is only exported when running tests.
+void
+ForceSetCubebContext(cubeb* aCubebContext)
+{
+  StaticMutexAutoLock lock(sMutex);
+  sCubebContext = aCubebContext;
+  sCubebState = CubebState::Initialized;
+}
+
 bool InitPreferredSampleRate()
 {
   StaticMutexAutoLock lock(sMutex);
   if (sPreferredSampleRate != 0) {
     return true;
   }
 #ifdef MOZ_WIDGET_ANDROID
   sPreferredSampleRate = AndroidGetAudioOutputSampleRate();
--- a/dom/media/CubebUtils.h
+++ b/dom/media/CubebUtils.h
@@ -5,17 +5,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #if !defined(CubebUtils_h_)
 #define CubebUtils_h_
 
 #include "cubeb/cubeb.h"
 #include "nsString.h"
 #include "mozilla/RefPtr.h"
-#include "mozilla/Maybe.h"
 
 class AudioDeviceInfo;
 
 namespace mozilla {
 namespace CubebUtils {
 
 typedef cubeb_devid AudioDeviceID;
 
@@ -50,12 +49,17 @@ void GetCurrentBackend(nsAString& aBacke
 void GetDeviceCollection(nsTArray<RefPtr<AudioDeviceInfo>>& aDeviceInfos,
                          Side aSide);
 cubeb_stream_prefs GetDefaultStreamPrefs();
 
 #ifdef MOZ_WIDGET_ANDROID
 uint32_t AndroidGetAudioOutputSampleRate();
 uint32_t AndroidGetAudioOutputFramesPerBuffer();
 #endif
+
+#ifdef ENABLE_SET_CUBEB_BACKEND
+void
+ForceSetCubebContext(cubeb* aCubebContext);
+#endif
 } // namespace CubebUtils
 } // namespace mozilla
 
 #endif // CubebUtils_h_
--- a/dom/media/GraphDriver.cpp
+++ b/dom/media/GraphDriver.cpp
@@ -50,48 +50,58 @@ void GraphDriver::SetGraphTime(GraphDriv
   mIterationStart = aLastSwitchNextIterationStart;
   mIterationEnd = aLastSwitchNextIterationEnd;
 
   MOZ_ASSERT(!PreviousDriver());
   MOZ_ASSERT(aPreviousDriver);
   MOZ_DIAGNOSTIC_ASSERT(GraphImpl()->CurrentDriver() == aPreviousDriver);
 
   LOG(LogLevel::Debug,
-      ("Setting previous driver: %p (%s)",
+      ("%p: Setting previous driver: %p (%s)",
+       GraphImpl(),
        aPreviousDriver,
        aPreviousDriver->AsAudioCallbackDriver() ? "AudioCallbackDriver"
                                                 : "SystemClockDriver"));
 
   SetPreviousDriver(aPreviousDriver);
 }
 
 void GraphDriver::SwitchAtNextIteration(GraphDriver* aNextDriver)
 {
   MOZ_ASSERT(OnThread());
   MOZ_ASSERT(aNextDriver);
   GraphImpl()->GetMonitor().AssertCurrentThreadOwns();
 
   LOG(LogLevel::Debug,
-      ("Switching to new driver: %p (%s)",
+      ("%p: Switching to new driver: %p (%s)",
+       GraphImpl(),
        aNextDriver,
        aNextDriver->AsAudioCallbackDriver() ? "AudioCallbackDriver"
                                             : "SystemClockDriver"));
-
+  if (mNextDriver &&
+      mNextDriver != GraphImpl()->CurrentDriver()) {
+    LOG(LogLevel::Debug,
+        ("%p: Discarding previous next driver: %p (%s)",
+         GraphImpl(),
+         mNextDriver.get(),
+         mNextDriver->AsAudioCallbackDriver() ? "AudioCallbackDriver"
+                                              : "SystemClockDriver"));
+  }
   SetNextDriver(aNextDriver);
 }
 
 GraphTime
 GraphDriver::StateComputedTime() const
 {
-  return mGraphImpl->mStateComputedTime;
+  return GraphImpl()->mStateComputedTime;
 }
 
 void GraphDriver::EnsureNextIteration()
 {
-  mGraphImpl->EnsureNextIteration();
+  GraphImpl()->EnsureNextIteration();
 }
 
 bool GraphDriver::Switching()
 {
   MOZ_ASSERT(OnThread());
   GraphImpl()->GetMonitor().AssertCurrentThreadOwns();
   return mNextDriver || mPreviousDriver;
 }
@@ -267,16 +277,17 @@ ThreadedDriver::Revive()
 void
 ThreadedDriver::Shutdown()
 {
   NS_ASSERTION(NS_IsMainThread(), "Must be called on main thread");
   // mGraph's thread is not running so it's OK to do whatever here
   LOG(LogLevel::Debug, ("Stopping threads for MediaStreamGraph %p", this));
 
   if (mThread) {
+    LOG(LogLevel::Debug, ("%p: Stopping ThreadedDriver's %p thread", GraphImpl(), this));
     mThread->Shutdown();
     mThread = nullptr;
   }
 }
 
 SystemClockDriver::SystemClockDriver(MediaStreamGraphImpl* aGraphImpl)
   : ThreadedDriver(aGraphImpl),
     mInitialTimeStamp(TimeStamp::Now()),
@@ -305,169 +316,174 @@ ThreadedDriver::RunThread()
 {
   mThreadRunning = true;
   while (true) {
     mIterationStart = IterationEnd();
     mIterationEnd += GetIntervalForIteration();
 
     GraphTime stateComputedTime = StateComputedTime();
     if (stateComputedTime < mIterationEnd) {
-      LOG(LogLevel::Warning, ("Media graph global underrun detected"));
+      LOG(LogLevel::Warning, ("%p: Global underrun detected", GraphImpl()));
       mIterationEnd = stateComputedTime;
     }
 
     if (mIterationStart >= mIterationEnd) {
       NS_ASSERTION(mIterationStart == mIterationEnd ,
                    "Time can't go backwards!");
       // This could happen due to low clock resolution, maybe?
-      LOG(LogLevel::Debug, ("Time did not advance"));
+      LOG(LogLevel::Debug, ("%p: Time did not advance", GraphImpl()));
     }
 
     GraphTime nextStateComputedTime =
-      mGraphImpl->RoundUpToEndOfAudioBlock(
-        mIterationEnd + mGraphImpl->MillisecondsToMediaTime(AUDIO_TARGET_MS));
+      GraphImpl()->RoundUpToEndOfAudioBlock(
+        mIterationEnd + GraphImpl()->MillisecondsToMediaTime(AUDIO_TARGET_MS));
     if (nextStateComputedTime < stateComputedTime) {
       // A previous driver may have been processing further ahead of
       // iterationEnd.
       LOG(LogLevel::Warning,
-          ("Prevent state from going backwards. interval[%ld; %ld] state[%ld; "
+          ("%p: Prevent state from going backwards. interval[%ld; %ld] state[%ld; "
            "%ld]",
+           GraphImpl(),
            (long)mIterationStart,
            (long)mIterationEnd,
            (long)stateComputedTime,
            (long)nextStateComputedTime));
       nextStateComputedTime = stateComputedTime;
     }
     LOG(LogLevel::Verbose,
-        ("interval[%ld; %ld] state[%ld; %ld]",
+        ("%p: interval[%ld; %ld] state[%ld; %ld]",
+         GraphImpl(),
          (long)mIterationStart,
          (long)mIterationEnd,
          (long)stateComputedTime,
          (long)nextStateComputedTime));
 
-    bool stillProcessing = mGraphImpl->OneIteration(nextStateComputedTime);
+    bool stillProcessing = GraphImpl()->OneIteration(nextStateComputedTime);
 
     if (!stillProcessing) {
       // Enter shutdown mode. The stable-state handler will detect this
       // and complete shutdown if the graph does not get restarted.
-      mGraphImpl->SignalMainThreadCleanup();
+      GraphImpl()->SignalMainThreadCleanup();
       break;
     }
     MonitorAutoLock lock(GraphImpl()->GetMonitor());
     if (NextDriver()) {
-      LOG(LogLevel::Debug, ("Switching to AudioCallbackDriver"));
+      LOG(LogLevel::Debug, ("%p: Switching to AudioCallbackDriver", GraphImpl()));
       SwitchToNextDriver();
       break;
     }
   }
   mThreadRunning = false;
 }
 
 MediaTime
 SystemClockDriver::GetIntervalForIteration()
 {
   TimeStamp now = TimeStamp::Now();
   MediaTime interval =
-    mGraphImpl->SecondsToMediaTime((now - mCurrentTimeStamp).ToSeconds());
+    GraphImpl()->SecondsToMediaTime((now - mCurrentTimeStamp).ToSeconds());
   mCurrentTimeStamp = now;
 
   MOZ_LOG(gMediaStreamGraphLog, LogLevel::Verbose,
-          ("Updating current time to %f (real %f, StateComputedTime() %f)",
-           mGraphImpl->MediaTimeToSeconds(IterationEnd() + interval),
+          ("%p: Updating current time to %f (real %f, StateComputedTime() %f)",
+           GraphImpl(),
+           GraphImpl()->MediaTimeToSeconds(IterationEnd() + interval),
            (now - mInitialTimeStamp).ToSeconds(),
-           mGraphImpl->MediaTimeToSeconds(StateComputedTime())));
+           GraphImpl()->MediaTimeToSeconds(StateComputedTime())));
 
   return interval;
 }
 
 TimeStamp
 OfflineClockDriver::GetCurrentTimeStamp()
 {
   MOZ_CRASH("This driver does not support getting the current timestamp.");
   return TimeStamp();
 }
 
 void
 SystemClockDriver::WaitForNextIteration()
 {
-  mGraphImpl->GetMonitor().AssertCurrentThreadOwns();
+  GraphImpl()->GetMonitor().AssertCurrentThreadOwns();
 
   TimeDuration timeout = TimeDuration::Forever();
   TimeStamp now = TimeStamp::Now();
 
   // This lets us avoid hitting the Atomic twice when we know we won't sleep
-  bool another = mGraphImpl->mNeedAnotherIteration; // atomic
+  bool another = GraphImpl()->mNeedAnotherIteration; // atomic
   if (!another) {
-    mGraphImpl->mGraphDriverAsleep = true; // atomic
+    GraphImpl()->mGraphDriverAsleep = true; // atomic
     mWaitState = WAITSTATE_WAITING_INDEFINITELY;
   }
   // NOTE: mNeedAnotherIteration while also atomic may have changed before
   // we could set mGraphDriverAsleep, so we must re-test it.
   // (EnsureNextIteration sets mNeedAnotherIteration, then tests
   // mGraphDriverAsleep
-  if (another || mGraphImpl->mNeedAnotherIteration) { // atomic
+  if (another || GraphImpl()->mNeedAnotherIteration) { // atomic
     int64_t timeoutMS = MEDIA_GRAPH_TARGET_PERIOD_MS -
       int64_t((now - mCurrentTimeStamp).ToMilliseconds());
     // Make sure timeoutMS doesn't overflow 32 bits by waking up at
     // least once a minute, if we need to wake up at all
     timeoutMS = std::max<int64_t>(0, std::min<int64_t>(timeoutMS, 60*1000));
     timeout = TimeDuration::FromMilliseconds(timeoutMS);
     LOG(LogLevel::Verbose,
-        ("Waiting for next iteration; at %f, timeout=%f",
+        ("%p: Waiting for next iteration; at %f, timeout=%f",
+         GraphImpl(),
          (now - mInitialTimeStamp).ToSeconds(),
          timeoutMS / 1000.0));
     if (mWaitState == WAITSTATE_WAITING_INDEFINITELY) {
-      mGraphImpl->mGraphDriverAsleep = false; // atomic
+      GraphImpl()->mGraphDriverAsleep = false; // atomic
     }
     mWaitState = WAITSTATE_WAITING_FOR_NEXT_ITERATION;
   }
   if (!timeout.IsZero()) {
-    mGraphImpl->GetMonitor().Wait(timeout);
+    GraphImpl()->GetMonitor().Wait(timeout);
     LOG(LogLevel::Verbose,
-        ("Resuming after timeout; at %f, elapsed=%f",
+        ("%p: Resuming after timeout; at %f, elapsed=%f",
+         GraphImpl(),
          (TimeStamp::Now() - mInitialTimeStamp).ToSeconds(),
          (TimeStamp::Now() - now).ToSeconds()));
   }
 
   if (mWaitState == WAITSTATE_WAITING_INDEFINITELY) {
-    mGraphImpl->mGraphDriverAsleep = false; // atomic
+    GraphImpl()->mGraphDriverAsleep = false; // atomic
   }
   // Note: this can race against the EnsureNextIteration setting
   // WAITSTATE_RUNNING and setting mGraphDriverAsleep to false, so you can
   // have an iteration with WAITSTATE_WAKING_UP instead of RUNNING.
   mWaitState = WAITSTATE_RUNNING;
-  mGraphImpl->mNeedAnotherIteration = false; // atomic
+  GraphImpl()->mNeedAnotherIteration = false; // atomic
 }
 
 void SystemClockDriver::WakeUp()
 {
-  mGraphImpl->GetMonitor().AssertCurrentThreadOwns();
+  GraphImpl()->GetMonitor().AssertCurrentThreadOwns();
   // Note: this can race against the thread setting WAITSTATE_RUNNING and
   // setting mGraphDriverAsleep to false, so you can have an iteration
   // with WAITSTATE_WAKING_UP instead of RUNNING.
   mWaitState = WAITSTATE_WAKING_UP;
-  mGraphImpl->mGraphDriverAsleep = false; // atomic
-  mGraphImpl->GetMonitor().Notify();
+  GraphImpl()->mGraphDriverAsleep = false; // atomic
+  GraphImpl()->GetMonitor().Notify();
 }
 
 OfflineClockDriver::OfflineClockDriver(MediaStreamGraphImpl* aGraphImpl, GraphTime aSlice)
   : ThreadedDriver(aGraphImpl),
     mSlice(aSlice)
 {
 
 }
 
 OfflineClockDriver::~OfflineClockDriver()
 {
 }
 
 MediaTime
 OfflineClockDriver::GetIntervalForIteration()
 {
-  return mGraphImpl->MillisecondsToMediaTime(mSlice);
+  return GraphImpl()->MillisecondsToMediaTime(mSlice);
 }
 
 void
 OfflineClockDriver::WaitForNextIteration()
 {
   // No op: we want to go as fast as possible when we are offline
 }
 
@@ -495,28 +511,28 @@ AsyncCubebTask::~AsyncCubebTask()
 NS_IMETHODIMP
 AsyncCubebTask::Run()
 {
   MOZ_ASSERT(mDriver);
 
   switch(mOperation) {
     case AsyncCubebOperation::INIT: {
       LOG(LogLevel::Debug,
-          ("AsyncCubebOperation::INIT driver=%p", mDriver.get()));
+          ("%p: AsyncCubebOperation::INIT driver=%p", mDriver->GraphImpl(), mDriver.get()));
       if (!mDriver->Init()) {
         LOG(LogLevel::Warning,
             ("AsyncCubebOperation::INIT failed for driver=%p", mDriver.get()));
         return NS_ERROR_FAILURE;
       }
       mDriver->CompleteAudioContextOperations(mOperation);
       break;
     }
     case AsyncCubebOperation::SHUTDOWN: {
       LOG(LogLevel::Debug,
-          ("AsyncCubebOperation::SHUTDOWN driver=%p", mDriver.get()));
+          ("%p: AsyncCubebOperation::SHUTDOWN driver=%p", mDriver->GraphImpl(), mDriver.get()));
       mDriver->Stop();
 
       mDriver->CompleteAudioContextOperations(mOperation);
 
       mDriver = nullptr;
       mShutdownGrip = nullptr;
       break;
     }
@@ -533,37 +549,36 @@ StreamAndPromiseForOperation::StreamAndP
                                           dom::AudioContextOperation aOperation)
   : mStream(aStream)
   , mPromise(aPromise)
   , mOperation(aOperation)
 {
   // MOZ_ASSERT(aPromise);
 }
 
-AudioCallbackDriver::AudioCallbackDriver(MediaStreamGraphImpl* aGraphImpl)
+AudioCallbackDriver::AudioCallbackDriver(MediaStreamGraphImpl* aGraphImpl, uint32_t aInputChannelCount)
   : GraphDriver(aGraphImpl)
   , mOutputChannels(0)
   , mSampleRate(0)
-  , mInputChannels(1)
+  , mInputChannelCount(aInputChannelCount)
   , mIterationDurationMS(MEDIA_GRAPH_TARGET_PERIOD_MS)
   , mStarted(false)
-  , mAudioInput(nullptr)
   , mInitShutdownThread(SharedThreadPool::Get(NS_LITERAL_CSTRING("CubebOperation"), 1))
   , mAddedMixer(false)
   , mAudioThreadId(std::thread::id())
   , mAudioThreadRunning(false)
-  , mMicrophoneActive(false)
   , mShouldFallbackIfError(false)
   , mFromFallback(false)
 {
-  LOG(LogLevel::Debug, ("AudioCallbackDriver ctor for graph %p", aGraphImpl));
+  LOG(LogLevel::Debug, ("%p: AudioCallbackDriver ctor", GraphImpl()));
 
   const uint32_t kIdleThreadTimeoutMs = 2000;
   mInitShutdownThread->
       SetIdleThreadTimeout(PR_MillisecondsToInterval(kIdleThreadTimeoutMs));
+
 #if defined(XP_WIN)
   if (XRE_IsContentProcess()) {
     audio::AudioNotificationReceiver::Register(this);
   }
 #endif
 }
 
 AudioCallbackDriver::~AudioCallbackDriver()
@@ -603,17 +618,16 @@ bool IsMacbookOrMacbookAir()
   }
 #endif
   return false;
 }
 
 bool
 AudioCallbackDriver::Init()
 {
-  MOZ_ASSERT(!IsStarted() && OnCubebOperationThread());
   cubeb* cubebContext = CubebUtils::GetCubebContext();
   if (!cubebContext) {
     NS_WARNING("Could not get cubeb context.");
     LOG(LogLevel::Warning, ("%s: Could not get cubeb context", __func__));
     if (!mFromFallback) {
       CubebUtils::ReportCubebStreamInitFailure(true);
     }
     MonitorAutoLock lock(GraphImpl()->GetMonitor());
@@ -632,17 +646,17 @@ AudioCallbackDriver::Init()
 
   if (AUDIO_OUTPUT_FORMAT == AUDIO_FORMAT_S16) {
     output.format = CUBEB_SAMPLE_S16NE;
   } else {
     output.format = CUBEB_SAMPLE_FLOAT32NE;
   }
 
   // Query and set the number of channels this AudioCallbackDriver will use.
-  mOutputChannels = mGraphImpl->AudioChannelCount();
+  mOutputChannels = GraphImpl()->AudioOutputChannelCount();
   if (!mOutputChannels) {
     LOG(LogLevel::Warning, ("Output number of channels is 0."));
     MonitorAutoLock lock(GraphImpl()->GetMonitor());
     FallbackToSystemClockDriver();
     return true;
   }
 
   mBuffer = AudioCallbackBufferWrapper<AudioDataValue>(mOutputChannels);
@@ -656,91 +670,71 @@ AudioCallbackDriver::Init()
 
   // Macbook and MacBook air don't have enough CPU to run very low latency
   // MediaStreamGraphs, cap the minimal latency to 512 frames int this case.
   if (IsMacbookOrMacbookAir()) {
     latency_frames = std::max((uint32_t) 512, latency_frames);
   }
 
   input = output;
-  input.channels = mInputChannels;
+  input.channels = mInputChannelCount;
   input.layout = CUBEB_LAYOUT_UNDEFINED;
 
-#ifdef MOZ_WEBRTC
-  if (mGraphImpl->mInputWanted) {
-    StaticMutexAutoLock lock(AudioInputCubeb::Mutex());
-    uint32_t userChannels = 0;
-    AudioInputCubeb::GetUserChannelCount(mGraphImpl->mInputDeviceID, userChannels);
-    input.channels = mInputChannels = std::min<uint32_t>(8, userChannels);
-  }
-#endif
-
   cubeb_stream* stream = nullptr;
-  CubebUtils::AudioDeviceID input_id = nullptr, output_id = nullptr;
-  // We have to translate the deviceID values to cubeb devid's since those can be
-  // freed whenever enumerate is called.
-  {
-#ifdef MOZ_WEBRTC
-    StaticMutexAutoLock lock(AudioInputCubeb::Mutex());
-#endif
-    if ((!mGraphImpl->mInputWanted
-#ifdef MOZ_WEBRTC
-         || AudioInputCubeb::GetDeviceID(mGraphImpl->mInputDeviceID, input_id)
-#endif
-         ) &&
-        (mGraphImpl->mOutputDeviceID == -1 // pass nullptr for ID for default output
-#ifdef MOZ_WEBRTC
-         // XXX we should figure out how we would use a deviceID for output without webrtc.
-         // Currently we don't set this though, so it's ok
-         || AudioInputCubeb::GetDeviceID(mGraphImpl->mOutputDeviceID, output_id)
+  bool inputWanted = mInputChannelCount > 0;
+  CubebUtils::AudioDeviceID output_id = GraphImpl()->mOutputDeviceID;
+  CubebUtils::AudioDeviceID input_id = GraphImpl()->mInputDeviceID;
+
+  // XXX Only pass input input if we have an input listener.  Always
+  // set up output because it's easier, and it will just get silence.
+  if (cubeb_stream_init(cubebContext,
+                        &stream,
+                        "AudioCallbackDriver",
+                        input_id,
+                        inputWanted ? &input : nullptr,
+                        output_id,
+                        &output,
+                        latency_frames,
+                        DataCallback_s,
+                        StateCallback_s,
+                        this) == CUBEB_OK) {
+    mAudioStream.own(stream);
+    DebugOnly<int> rv =
+      cubeb_stream_set_volume(mAudioStream, CubebUtils::GetVolumeScale());
+    NS_WARNING_ASSERTION(
+      rv == CUBEB_OK,
+      "Could not set the audio stream volume in GraphDriver.cpp");
+    CubebUtils::ReportCubebBackendUsed();
+  } else {
+    NS_WARNING("Could not create a cubeb stream for MediaStreamGraph, falling "
+        "back to a SystemClockDriver");
+    // Only report failures when we're not coming from a driver that was
+    // created itself as a fallback driver because of a previous audio driver
+    // failure.
+    if (!mFromFallback) {
+      CubebUtils::ReportCubebStreamInitFailure(firstStream);
+    }
+    MonitorAutoLock lock(GraphImpl()->GetMonitor());
+    FallbackToSystemClockDriver();
+    return true;
+  }
+
+#ifdef XP_MACOSX
+   PanOutputIfNeeded(inputWanted);
 #endif
-         ) &&
-        // XXX Only pass input input if we have an input listener.  Always
-        // set up output because it's easier, and it will just get silence.
-        // XXX Add support for adding/removing an input listener later.
-        cubeb_stream_init(cubebContext, &stream,
-                          "AudioCallbackDriver",
-                          input_id,
-                          mGraphImpl->mInputWanted ? &input : nullptr,
-                          output_id,
-                          mGraphImpl->mOutputWanted ? &output : nullptr, latency_frames,
-                          DataCallback_s, StateCallback_s, this) == CUBEB_OK) {
-      mAudioStream.own(stream);
-      DebugOnly<int> rv = cubeb_stream_set_volume(mAudioStream, CubebUtils::GetVolumeScale());
-      NS_WARNING_ASSERTION(
-        rv == CUBEB_OK,
-        "Could not set the audio stream volume in GraphDriver.cpp");
-      CubebUtils::ReportCubebBackendUsed();
-    } else {
-#ifdef MOZ_WEBRTC
-      StaticMutexAutoUnlock unlock(AudioInputCubeb::Mutex());
-#endif
-      NS_WARNING("Could not create a cubeb stream for MediaStreamGraph, falling back to a SystemClockDriver");
-      // Only report failures when we're not coming from a driver that was
-      // created itself as a fallback driver because of a previous audio driver
-      // failure.
-      if (!mFromFallback) {
-        CubebUtils::ReportCubebStreamInitFailure(firstStream);
-      }
-      MonitorAutoLock lock(GraphImpl()->GetMonitor());
-      FallbackToSystemClockDriver();
-      return true;
-    }
-  }
-  SetMicrophoneActive(mGraphImpl->mInputWanted);
 
-  cubeb_stream_register_device_changed_callback(mAudioStream,
-                                                AudioCallbackDriver::DeviceChangedCallback_s);
+  cubeb_stream_register_device_changed_callback(
+    mAudioStream, AudioCallbackDriver::DeviceChangedCallback_s);
 
   if (!StartStream()) {
-    LOG(LogLevel::Warning, ("AudioCallbackDriver couldn't start stream."));
+    LOG(LogLevel::Warning, ("%p: AudioCallbackDriver couldn't start a cubeb stream.", GraphImpl()));
     return false;
   }
 
-  LOG(LogLevel::Debug, ("AudioCallbackDriver started."));
+  LOG(LogLevel::Debug, ("%p: AudioCallbackDriver started.", GraphImpl()));
   return true;
 }
 
 void
 AudioCallbackDriver::Start()
 {
   MOZ_ASSERT(!IsStarted());
   MOZ_ASSERT(NS_IsMainThread() || OnCubebOperationThread() ||
@@ -794,19 +788,19 @@ AudioCallbackDriver::Stop()
 }
 
 void
 AudioCallbackDriver::Revive()
 {
   MOZ_ASSERT(NS_IsMainThread() && !ThreadRunning());
   // Note: only called on MainThread, without monitor
   // We know were weren't in a running state
-  LOG(LogLevel::Debug, ("AudioCallbackDriver reviving."));
+  LOG(LogLevel::Debug, ("%p: AudioCallbackDriver reviving.", GraphImpl()));
   // If we were switching, switch now. Otherwise, start the audio thread again.
-  MonitorAutoLock mon(mGraphImpl->GetMonitor());
+  MonitorAutoLock mon(GraphImpl()->GetMonitor());
   if (NextDriver()) {
     SwitchToNextDriver();
   } else {
     LOG(LogLevel::Debug,
         ("Starting audio threads for MediaStreamGraph %p from a new thread.",
          mGraphImpl.get()));
     RefPtr<AsyncCubebTask> initEvent =
       new AsyncCubebTask(this, AsyncCubebOperation::INIT);
@@ -815,17 +809,17 @@ AudioCallbackDriver::Revive()
 }
 
 void
 AudioCallbackDriver::RemoveMixerCallback()
 {
   MOZ_ASSERT(OnThread() || !ThreadRunning());
 
   if (mAddedMixer) {
-    mGraphImpl->mMixer.RemoveCallback(this);
+    GraphImpl()->mMixer.RemoveCallback(this);
     mAddedMixer = false;
   }
 }
 
 void
 AudioCallbackDriver::AddMixerCallback()
 {
   MOZ_ASSERT(OnThread());
@@ -848,17 +842,17 @@ AudioCallbackDriver::WakeUp()
   mGraphImpl->GetMonitor().Notify();
 }
 
 void
 AudioCallbackDriver::Shutdown()
 {
   MOZ_ASSERT(NS_IsMainThread());
   LOG(LogLevel::Debug,
-      ("Releasing audio driver off main thread (GraphDriver::Shutdown)."));
+      ("%p: Releasing audio driver off main thread (GraphDriver::Shutdown).", GraphImpl()));
   RefPtr<AsyncCubebTask> releaseEvent =
     new AsyncCubebTask(this, AsyncCubebOperation::SHUTDOWN);
   releaseEvent->Dispatch(NS_DISPATCH_SYNC);
 }
 
 #if defined(XP_WIN)
 void
 AudioCallbackDriver::ResetDefaultDevice()
@@ -913,30 +907,33 @@ AudioCallbackDriver::DataCallback(const 
    TRACE_AUDIO_CALLBACK_BUDGET(aFrames, mSampleRate);
    TRACE_AUDIO_CALLBACK();
 
 #ifdef DEBUG
   AutoInCallback aic(this);
 #endif
 
   // Don't add the callback until we're inited and ready
-  AddMixerCallback();
+  if (!mAddedMixer) {
+    GraphImpl()->mMixer.AddCallback(this);
+    mAddedMixer = true;
+  }
 
   GraphTime stateComputedTime = StateComputedTime();
   if (stateComputedTime == 0) {
-    MonitorAutoLock mon(mGraphImpl->GetMonitor());
+    MonitorAutoLock mon(GraphImpl()->GetMonitor());
     // Because this function is called during cubeb_stream_init (to prefill the
     // audio buffers), it can be that we don't have a message here (because this
     // driver is the first one for this graph), and the graph would exit. Simply
     // return here until we have messages.
-    if (!mGraphImpl->MessagesQueued()) {
+    if (!GraphImpl()->MessagesQueued()) {
       PodZero(aOutputBuffer, aFrames * mOutputChannels);
       return aFrames;
     }
-    mGraphImpl->SwapMessageQueues();
+    GraphImpl()->SwapMessageQueues();
   }
 
   uint32_t durationMS = aFrames * 1000 / mSampleRate;
 
   // For now, simply average the duration with the previous
   // duration so there is some damping against sudden changes.
   if (!mIterationDurationMS) {
     mIterationDurationMS = durationMS;
@@ -949,81 +946,79 @@ AudioCallbackDriver::DataCallback(const 
   // fill part or all with leftover data from last iteration (since we
   // align to Audio blocks)
   mScratchBuffer.Empty(mBuffer);
 
   // State computed time is decided by the audio callback's buffer length. We
   // compute the iteration start and end from there, trying to keep the amount
   // of buffering in the graph constant.
   GraphTime nextStateComputedTime =
-    mGraphImpl->RoundUpToEndOfAudioBlock(
+    GraphImpl()->RoundUpToEndOfAudioBlock(
       stateComputedTime + mBuffer.Available());
 
   mIterationStart = mIterationEnd;
   // inGraph is the number of audio frames there is between the state time and
   // the current time, i.e. the maximum theoretical length of the interval we
   // could use as [mIterationStart; mIterationEnd].
   GraphTime inGraph = stateComputedTime - mIterationStart;
   // We want the interval [mIterationStart; mIterationEnd] to be before the
   // interval [stateComputedTime; nextStateComputedTime]. We also want
   // the distance between these intervals to be roughly equivalent each time, to
   // ensure there is no clock drift between current time and state time. Since
   // we can't act on the state time because we have to fill the audio buffer, we
   // reclock the current time against the state time, here.
   mIterationEnd = mIterationStart + 0.8 * inGraph;
 
   LOG(LogLevel::Verbose,
-      ("interval[%ld; %ld] state[%ld; %ld] (frames: %ld) (durationMS: %u) "
+      ("%p: interval[%ld; %ld] state[%ld; %ld] (frames: %ld) (durationMS: %u) "
        "(duration ticks: %ld)",
+       GraphImpl(),
        (long)mIterationStart,
        (long)mIterationEnd,
        (long)stateComputedTime,
        (long)nextStateComputedTime,
        (long)aFrames,
        (uint32_t)durationMS,
        (long)(nextStateComputedTime - stateComputedTime)));
 
   mCurrentTimeStamp = TimeStamp::Now();
 
   if (stateComputedTime < mIterationEnd) {
-    LOG(LogLevel::Error, ("Media graph global underrun detected"));
+    LOG(LogLevel::Error, ("%p: Media graph global underrun detected", GraphImpl()));
     MOZ_ASSERT_UNREACHABLE("We should not underrun in full duplex");
     mIterationEnd = stateComputedTime;
   }
 
   // Process mic data if any/needed
-  if (aInputBuffer) {
-    if (mAudioInput) { // for this specific input-only or full-duplex stream
-      mAudioInput->NotifyInputData(mGraphImpl, aInputBuffer,
-                                   static_cast<size_t>(aFrames),
-                                   mSampleRate, mInputChannels);
-    }
+  if (aInputBuffer && mInputChannelCount > 0) {
+    GraphImpl()->NotifyInputData(aInputBuffer, static_cast<size_t>(aFrames),
+                                 mSampleRate, mInputChannelCount);
   }
 
   bool stillProcessing;
   if (mBuffer.Available()) {
     // We totally filled the buffer (and mScratchBuffer isn't empty).
     // We don't need to run an iteration and if we do so we may overflow.
-    stillProcessing = mGraphImpl->OneIteration(nextStateComputedTime);
+    stillProcessing = GraphImpl()->OneIteration(nextStateComputedTime);
   } else {
     LOG(LogLevel::Verbose,
-        ("DataCallback buffer filled entirely from scratch "
-         "buffer, skipping iteration."));
+        ("%p: DataCallback buffer filled entirely from scratch "
+         "buffer, skipping iteration.", GraphImpl()));
     stillProcessing = true;
   }
 
   mBuffer.BufferFilled();
 
   // Callback any observers for the AEC speaker data.  Note that one
   // (maybe) of these will be full-duplex, the others will get their input
   // data off separate cubeb callbacks.  Take care with how stuff is
   // removed/added to this list and TSAN issues, but input and output will
   // use separate callback methods.
-  mGraphImpl->NotifyOutputData(aOutputBuffer, static_cast<size_t>(aFrames),
-                               mSampleRate, mOutputChannels);
+  GraphImpl()->NotifyOutputData(aOutputBuffer, static_cast<size_t>(aFrames),
+                                mSampleRate, mOutputChannels);
 
   if (!stillProcessing) {
     // About to hand over control of the graph.  Do not start a new driver if
     // StateCallback() receives an error for this stream while the main thread
     // or another driver has control of the graph.
     mShouldFallbackIfError = false;
     // Enter shutdown mode. The stable-state handler will detect this
     // and complete shutdown if the graph does not get restarted.
@@ -1031,29 +1026,29 @@ AudioCallbackDriver::DataCallback(const 
     RemoveMixerCallback();
     // Update the flag before go to drain
     mAudioThreadRunning = false;
     return aFrames - 1;
   }
 
   bool switching = false;
   {
-    MonitorAutoLock mon(mGraphImpl->GetMonitor());
+    MonitorAutoLock mon(GraphImpl()->GetMonitor());
     switching = !!NextDriver();
   }
 
   if (switching) {
     mShouldFallbackIfError = false;
     // If the audio stream has not been started by the previous driver or
     // the graph itself, keep it alive.
-    MonitorAutoLock mon(mGraphImpl->GetMonitor());
+    MonitorAutoLock mon(GraphImpl()->GetMonitor());
     if (!IsStarted()) {
       return aFrames;
     }
-    LOG(LogLevel::Debug, ("Switching to system driver."));
+    LOG(LogLevel::Debug, ("%p: Switching to system driver.", GraphImpl()));
     RemoveMixerCallback();
     SwitchToNextDriver();
     mAudioThreadRunning = false;
     // Returning less than aFrames starts the draining and eventually stops the
     // audio thread. This function will never get called again.
     return aFrames - 1;
   }
 
@@ -1145,31 +1140,19 @@ void AudioCallbackDriver::PanOutputIfNee
 
 void
 AudioCallbackDriver::DeviceChangedCallback()
 {
   MOZ_ASSERT(!OnThread());
   // Tell the audio engine the device has changed, it might want to reset some
   // state.
   MonitorAutoLock mon(mGraphImpl->GetMonitor());
-  if (mAudioInput) {
-    mAudioInput->DeviceChanged();
-  }
+  GraphImpl()->DeviceChanged();
 #ifdef XP_MACOSX
-  PanOutputIfNeeded(mMicrophoneActive);
-#endif
-}
-
-void
-AudioCallbackDriver::SetMicrophoneActive(bool aActive)
-{
-  mMicrophoneActive = aActive;
-
-#ifdef XP_MACOSX
-  PanOutputIfNeeded(mMicrophoneActive);
+  PanOutputIfNeeded(mInputChannelCount);
 #endif
 }
 
 uint32_t
 AudioCallbackDriver::IterationDuration()
 {
   MOZ_ASSERT(OnThread());
   // The real fix would be to have an API in cubeb to give us the number. Short
--- a/dom/media/GraphDriver.h
+++ b/dom/media/GraphDriver.h
@@ -192,17 +192,17 @@ public:
    */
   void EnsureNextIteration();
 
   /**
    * Same thing, but not locked.
    */
   void EnsureNextIterationLocked();
 
-  MediaStreamGraphImpl* GraphImpl() {
+  MediaStreamGraphImpl* GraphImpl() const {
     return mGraphImpl;
   }
 
   // True if the current thread is the GraphDriver's thread.
   // This is the thread that drives the MSG.
   virtual bool OnThread() = 0;
   // GraphDriver's thread has started and the thread is running.
   // This is the thread that drives the MSG.
@@ -258,24 +258,23 @@ public:
   void Revive() override;
   void Shutdown() override;
   /**
    * Runs main control loop on the graph thread. Normally a single invocation
    * of this runs for the entire lifetime of the graph thread.
    */
   void RunThread();
   friend class MediaStreamGraphInitThreadRunnable;
-  uint32_t IterationDuration() override
-  {
+  uint32_t IterationDuration() override {
     return MEDIA_GRAPH_TARGET_PERIOD_MS;
   }
 
   bool OnThread() override
   {
-    return mThread && mThread->EventTarget()->IsOnCurrentThread();
+    return !mThread || mThread->EventTarget()->IsOnCurrentThread();
   }
 
   bool ThreadRunning() override
   {
     return mThreadRunning;
   }
 
   /* When the graph wakes up to do an iteration, implementations return the
@@ -395,17 +394,18 @@ enum AsyncCubebOperation {
  */
 class AudioCallbackDriver : public GraphDriver,
                             public MixerCallbackReceiver
 #if defined(XP_WIN)
                             , public audio::DeviceChangeListener
 #endif
 {
 public:
-  explicit AudioCallbackDriver(MediaStreamGraphImpl* aGraphImpl);
+  /** If aInputChannelCount is zero, then this driver is output-only. */
+  AudioCallbackDriver(MediaStreamGraphImpl* aGraphImpl, uint32_t aInputChannelCount);
   virtual ~AudioCallbackDriver();
 
   void Start() override;
   void Revive() override;
   void WaitForNextIteration() override;
   void WakeUp() override;
   void Shutdown() override;
 #if defined(XP_WIN)
@@ -413,61 +413,56 @@ public:
 #endif
 
   /* Static wrapper function cubeb calls back. */
   static long DataCallback_s(cubeb_stream * aStream,
                              void * aUser,
                              const void * aInputBuffer,
                              void * aOutputBuffer,
                              long aFrames);
-  static void StateCallback_s(cubeb_stream* aStream, void * aUser,
-                              cubeb_state aState);
+  static void StateCallback_s(cubeb_stream* aStream, void * aUser, cubeb_state aState);
   static void DeviceChangedCallback_s(void * aUser);
   /* This function is called by the underlying audio backend when a refill is
    * needed. This is what drives the whole graph when it is used to output
    * audio. If the return value is exactly aFrames, this function will get
    * called again. If it is less than aFrames, the stream will go in draining
    * mode, and this function will not be called again. */
-  long DataCallback(const AudioDataValue* aInputBuffer, AudioDataValue* aOutputBuffer, long aFrames);
+  long DataCallback(const AudioDataValue* aInputBuffer,
+                    AudioDataValue* aOutputBuffer,
+                    long aFrames);
   /* This function is called by the underlying audio backend, but is only used
    * for informational purposes at the moment. */
   void StateCallback(cubeb_state aState);
   /* This is an approximation of the number of millisecond there are between two
    * iterations of the graph. */
   uint32_t IterationDuration() override;
 
   /* This function gets called when the graph has produced the audio frames for
    * this iteration. */
   void MixerCallback(AudioDataValue* aMixedBuffer,
                      AudioSampleFormat aFormat,
                      uint32_t aChannels,
                      uint32_t aFrames,
                      uint32_t aSampleRate) override;
 
-  // These are invoked on the MSG thread (we don't call this if not LIFECYCLE_RUNNING)
-  virtual void SetInputListener(AudioDataListener *aListener) {
-    MOZ_ASSERT(!IsStarted());
-    mAudioInput = aListener;
-  }
-  // XXX do we need the param?  probably no
-  virtual void RemoveInputListener(AudioDataListener *aListener) {
-    MOZ_ASSERT(OnThread());
-    mAudioInput = nullptr;
-  }
-
   AudioCallbackDriver* AsAudioCallbackDriver() override {
     return this;
   }
 
   uint32_t OutputChannelCount()
   {
     MOZ_ASSERT(mOutputChannels != 0 && mOutputChannels <= 8);
     return mOutputChannels;
   }
 
+  uint32_t InputChannelCount()
+  {
+    return mInputChannelCount;
+  }
+
   /* Enqueue a promise that is going to be resolved when a specific operation
    * occurs on the cubeb stream. */
   void EnqueueStreamAndPromiseForOperation(MediaStream* aStream,
                                          void* aPromise,
                                          dom::AudioContextOperation aOperation);
 
   bool OnThread() override
   {
@@ -478,20 +473,16 @@ public:
   {
     return mAudioThreadRunning;
   }
 
   /* Whether the underlying cubeb stream has been started. See comment for
    * mStarted for details. */
   bool IsStarted();
 
-  /* Tell the driver whether this process is using a microphone or not. This is
-   * thread safe. */
-  void SetMicrophoneActive(bool aActive);
-
   void CompleteAudioContextOperations(AsyncCubebOperation aOperation);
 
 private:
   /* Remove Mixer callbacks when switching */
   void RemoveMixerCallback();
   /* Add this driver in Mixer callbacks. */
   void AddMixerCallback();
   /**
@@ -531,19 +522,19 @@ private:
    * callback thread. */
   AudioCallbackBufferWrapper<AudioDataValue> mBuffer;
   /* cubeb stream for this graph. This is guaranteed to be non-null after Init()
    * has been called, and is synchronized internaly. */
   nsAutoRef<cubeb_stream> mAudioStream;
   /* The sample rate for the aforementionned cubeb stream. This is set on
    * initialization and can be read safely afterwards. */
   uint32_t mSampleRate;
-  /* The number of input channels from cubeb.  Should be set before opening cubeb
-   * and then be static. */
-  uint32_t mInputChannels;
+  /* The number of input channels from cubeb. Set before opening cubeb. If it is
+   * zero then the driver is output-only. */
+  const uint32_t mInputChannelCount;
   /* Approximation of the time between two callbacks. This is used to schedule
    * video frames. This is in milliseconds. Only even used (after
    * inizatialization) on the audio callback thread. */
   uint32_t mIterationDurationMS;
   /* cubeb_stream_init calls the audio callback to prefill the buffers. The
    * previous driver has to be kept alive until the audio stream has been
    * started, because it is responsible to call cubeb_stream_start, so we delay
    * the cleanup of the previous driver until it has started the audio stream.
@@ -553,18 +544,16 @@ private:
    * This is written on the previous driver's thread (if switching) or main
    * thread (if this driver is the first one).
    * This is read on previous driver's thread (during callbacks from
    * cubeb_stream_init) and the audio thread (when switching away from this
    * driver back to a SystemClockDriver).
    * This is synchronized by the Graph's monitor.
    * */
   Atomic<bool> mStarted;
-  /* Listener for mic input, if any. */
-  RefPtr<AudioDataListener> mAudioInput;
 
   struct AutoInCallback
   {
     explicit AutoInCallback(AudioCallbackDriver* aDriver);
     ~AutoInCallback();
     AudioCallbackDriver* mDriver;
   };
 
@@ -580,20 +569,16 @@ private:
   Atomic<bool> mAddedMixer;
 
   /* Contains the id of the audio thread for as long as the callback
    * is taking place, after that it is reseted to an invalid value. */
   std::atomic<std::thread::id> mAudioThreadId;
   /* True when audio thread is running. False before
    * starting and after stopping it the audio thread. */
   Atomic<bool> mAudioThreadRunning;
-  /**
-   * True if microphone is being used by this process. This is synchronized by
-   * the graph's monitor. */
-  Atomic<bool> mMicrophoneActive;
   /* Indication of whether a fallback SystemClockDriver should be started if
    * StateCallback() receives an error.  No mutex need be held during access.
    * The transition to true happens before cubeb_stream_start() is called.
    * After transitioning to false on the last DataCallback(), the stream is
    * not accessed from another thread until the graph thread either signals
    * main thread cleanup or dispatches an event to switch to another
    * driver. */
   bool mShouldFallbackIfError;
--- a/dom/media/MediaStreamGraph.cpp
+++ b/dom/media/MediaStreamGraph.cpp
@@ -89,29 +89,24 @@ MediaStreamGraphImpl::AddStreamGraphThre
     TimeStamp processedTimeStamp = currentTimeStamp +
       TimeDuration::FromSeconds(MediaTimeToSeconds(mProcessedTime - IterationEnd()));
     source->SetStreamTracksStartTimeStamp(processedTimeStamp);
   }
 
   if (aStream->IsSuspended()) {
     mSuspendedStreams.AppendElement(aStream);
     LOG(LogLevel::Debug,
-        ("Adding media stream %p to the graph, in the suspended stream array",
-         aStream));
+        ("%p: Adding media stream %p, in the suspended stream array",
+         this, aStream));
   } else {
     mStreams.AppendElement(aStream);
     LOG(LogLevel::Debug,
-        ("Adding media stream %p to graph %p, count %zu",
-         aStream,
+        ("%p:  Adding media stream %p, count %zu",
          this,
-         mStreams.Length()));
-    LOG(LogLevel::Debug,
-        ("Adding media stream %p to graph %p, count %zu",
          aStream,
-         this,
          mStreams.Length()));
   }
 
   SetStreamOrderDirty();
 }
 
 void
 MediaStreamGraphImpl::RemoveStreamGraphThread(MediaStream* aStream)
@@ -134,24 +129,19 @@ MediaStreamGraphImpl::RemoveStreamGraphT
 
   if (aStream->IsSuspended()) {
     mSuspendedStreams.RemoveElement(aStream);
   } else {
     mStreams.RemoveElement(aStream);
   }
 
   LOG(LogLevel::Debug,
-      ("Removed media stream %p from graph %p, count %zu",
-       aStream,
+      ("%p: Removed media stream %p, count %zu",
        this,
-       mStreams.Length()));
-  LOG(LogLevel::Debug,
-      ("Removed media stream %p from graph %p, count %zu",
        aStream,
-       this,
        mStreams.Length()));
 
   NS_RELEASE(aStream); // probably destroying it
 }
 
 StreamTime
 MediaStreamGraphImpl::GraphTimeToStreamTimeWithBlocking(const MediaStream* aStream,
                                                         GraphTime aTime) const
@@ -178,17 +168,18 @@ MediaStreamGraphImpl::UpdateCurrentTimeF
     bool isAnyUnblocked = stream->mStartBlocking > aPrevCurrentTime;
 
     // Calculate blocked time and fire Blocked/Unblocked events
     GraphTime blockedTime = mStateComputedTime - stream->mStartBlocking;
     NS_ASSERTION(blockedTime >= 0, "Error in blocking time");
     stream->AdvanceTimeVaryingValuesToCurrentTime(mStateComputedTime,
                                                   blockedTime);
     LOG(LogLevel::Verbose,
-        ("MediaStream %p bufferStartTime=%f blockedTime=%f",
+        ("%p: MediaStream %p bufferStartTime=%f blockedTime=%f",
+         this,
          stream,
          MediaTimeToSeconds(stream->mTracksStartTime),
          MediaTimeToSeconds(blockedTime)));
     stream->mStartBlocking = mStateComputedTime;
 
     if (isAnyUnblocked && stream->mNotifiedBlocked) {
       for (uint32_t j = 0; j < stream->mListeners.Length(); ++j) {
         MediaStreamListener* l = stream->mListeners[j];
@@ -249,18 +240,19 @@ MediaStreamGraphImpl::ProcessChunkMetada
     offset += chunk->GetDuration();
     if (chunk->IsNull() || offset < aStart) {
       continue;
     }
     const PrincipalHandle& principalHandle = chunk->GetPrincipalHandle();
     if (principalHandle != aSegment.GetLastPrincipalHandle()) {
       aSegment.SetLastPrincipalHandle(principalHandle);
       LOG(LogLevel::Debug,
-          ("MediaStream %p track %d, principalHandle "
+          ("%p: MediaStream %p track %d, principalHandle "
            "changed in %sChunk with duration %lld",
+           this,
            aStream,
            aTrackID,
            aSegment.GetType() == MediaSegment::AUDIO ? "Audio" : "Video",
            (long long)chunk->GetDuration()));
       for (const TrackBound<MediaStreamTrackListener>& listener :
            aStream->mTrackListeners) {
         if (listener.mTrackID == aTrackID) {
           listener.mListener->NotifyPrincipalHandleChanged(this, principalHandle);
@@ -310,19 +302,20 @@ MediaStreamGraphImpl::WillUnderrun(Media
   }
   // This stream isn't finished or suspended. We don't need to call
   // StreamTimeToGraphTime since an underrun is the only thing that can block
   // it.
   GraphTime bufferEnd = aStream->GetTracksEnd() + aStream->mTracksStartTime;
 #ifdef DEBUG
   if (bufferEnd < mProcessedTime) {
     LOG(LogLevel::Error,
-        ("MediaStream %p underrun, "
+        ("%p: MediaStream %p underrun, "
          "bufferEnd %f < mProcessedTime %f (%" PRId64 " < %" PRId64
          "), Streamtime %" PRId64,
+         this,
          aStream,
          MediaTimeToSeconds(bufferEnd),
          MediaTimeToSeconds(mProcessedTime),
          bufferEnd,
          mProcessedTime,
          aStream->GetTracksEnd()));
     aStream->DumpTrackInfo();
     NS_ASSERTION(bufferEnd >= mProcessedTime, "Buffer underran");
@@ -406,17 +399,17 @@ MediaStreamGraphImpl::UpdateStreamOrder(
     switching = CurrentDriver()->Switching();
   }
 
   if (audioTrackPresent && mRealtime &&
       !CurrentDriver()->AsAudioCallbackDriver() &&
       !switching) {
     MonitorAutoLock mon(mMonitor);
     if (LifecycleStateRef() == LIFECYCLE_RUNNING) {
-      AudioCallbackDriver* driver = new AudioCallbackDriver(this);
+      AudioCallbackDriver* driver = new AudioCallbackDriver(this, AudioInputChannelCount());
       CurrentDriver()->SwitchAtNextIteration(driver);
     }
   }
 
   if (!mStreamOrderDirty) {
     return;
   }
 
@@ -625,17 +618,17 @@ MediaStreamGraphImpl::CreateOrDestroyAud
   }
 
   if (!aStream->GetStreamTracks().GetAndResetTracksDirty() &&
       !aStream->mAudioOutputStreams.IsEmpty()) {
     return;
   }
 
   LOG(LogLevel::Debug,
-      ("Updating AudioOutputStreams for MediaStream %p", aStream));
+      ("%p: Updating AudioOutputStreams for MediaStream %p", this, aStream));
 
   AutoTArray<bool,2> audioOutputStreamsFound;
   for (uint32_t i = 0; i < aStream->mAudioOutputStreams.Length(); ++i) {
     audioOutputStreamsFound.AppendElement(false);
   }
 
   for (StreamTracks::TrackIter tracks(aStream->GetStreamTracks(), MediaSegment::AUDIO);
        !tracks.IsEnded(); tracks.Next()) {
@@ -661,17 +654,17 @@ MediaStreamGraphImpl::CreateOrDestroyAud
         MonitorAutoLock lock(mMonitor);
         switching = CurrentDriver()->Switching();
       }
 
       if (!CurrentDriver()->AsAudioCallbackDriver() &&
           !switching) {
         MonitorAutoLock mon(mMonitor);
         if (LifecycleStateRef() == LIFECYCLE_RUNNING) {
-          AudioCallbackDriver* driver = new AudioCallbackDriver(this);
+          AudioCallbackDriver* driver = new AudioCallbackDriver(this, AudioInputChannelCount());
           CurrentDriver()->SwitchAtNextIteration(driver);
         }
       }
     }
   }
 
   for (int32_t i = audioOutputStreamsFound.Length() - 1; i >= 0; --i) {
     if (!audioOutputStreamsFound[i]) {
@@ -717,32 +710,34 @@ MediaStreamGraphImpl::PlayAudio(MediaStr
       // Check how many ticks of sound we can provide if we are blocked some
       // time in the middle of this cycle.
       StreamTime toWrite = end - t;
 
       if (blocked) {
         output.InsertNullDataAtStart(toWrite);
         ticksWritten += toWrite;
         LOG(LogLevel::Verbose,
-            ("MediaStream %p writing %" PRId64 " blocking-silence samples for "
+            ("%p: MediaStream %p writing %" PRId64 " blocking-silence samples for "
              "%f to %f (%" PRId64 " to %" PRId64 ")",
+             this,
              aStream,
              toWrite,
              MediaTimeToSeconds(t),
              MediaTimeToSeconds(end),
              offset,
              offset + toWrite));
       } else {
         StreamTime endTicksNeeded = offset + toWrite;
         StreamTime endTicksAvailable = audio->GetDuration();
 
         if (endTicksNeeded <= endTicksAvailable) {
           LOG(LogLevel::Verbose,
-              ("MediaStream %p writing %" PRId64 " samples for %f to %f "
+              ("%p: MediaStream %p writing %" PRId64 " samples for %f to %f "
                "(samples %" PRId64 " to %" PRId64 ")",
+               this,
                aStream,
                toWrite,
                MediaTimeToSeconds(t),
                MediaTimeToSeconds(end),
                offset,
                endTicksNeeded));
           output.AppendSlice(*audio, offset, endTicksNeeded);
           ticksWritten += toWrite;
@@ -750,33 +745,35 @@ MediaStreamGraphImpl::PlayAudio(MediaStr
         } else {
           // MOZ_ASSERT(track->IsEnded(), "Not enough data, and track not ended.");
           // If we are at the end of the track, maybe write the remaining
           // samples, and pad with/output silence.
           if (endTicksNeeded > endTicksAvailable &&
               offset < endTicksAvailable) {
             output.AppendSlice(*audio, offset, endTicksAvailable);
             LOG(LogLevel::Verbose,
-                ("MediaStream %p writing %" PRId64 " samples for %f to %f "
+                ("%p: MediaStream %p writing %" PRId64 " samples for %f to %f "
                  "(samples %" PRId64 " to %" PRId64 ")",
+                 this,
                  aStream,
                  toWrite,
                  MediaTimeToSeconds(t),
                  MediaTimeToSeconds(end),
                  offset,
                  endTicksNeeded));
             uint32_t available = endTicksAvailable - offset;
             ticksWritten += available;
             toWrite -= available;
             offset = endTicksAvailable;
           }
           output.AppendNullData(toWrite);
           LOG(LogLevel::Verbose,
-              ("MediaStream %p writing %" PRId64 " padding slsamples for %f to "
+              ("%p MediaStream %p writing %" PRId64 " padding slsamples for %f to "
                "%f (samples %" PRId64 " to %" PRId64 ")",
+               this,
                aStream,
                toWrite,
                MediaTimeToSeconds(t),
                MediaTimeToSeconds(end),
                offset,
                endTicksNeeded));
           ticksWritten += toWrite;
         }
@@ -784,180 +781,291 @@ MediaStreamGraphImpl::PlayAudio(MediaStr
       }
       t = end;
     }
     audioOutput.mLastTickWritten = offset;
 
     // Need unique id for stream & track - and we want it to match the inserter
     output.WriteTo(LATENCY_STREAM_ID(aStream, track->GetID()),
                                      mMixer,
-                                     AudioChannelCount(),
+                                     AudioOutputChannelCount(),
                                      mSampleRate);
   }
   return ticksWritten;
 }
 
 void
-MediaStreamGraphImpl::OpenAudioInputImpl(int aID,
-                                         AudioDataListener *aListener)
+MediaStreamGraphImpl::OpenAudioInputImpl(CubebUtils::AudioDeviceID aID,
+                                         AudioDataListener* aListener)
 {
   MOZ_ASSERT(OnGraphThread());
-  // Bug 1238038 Need support for multiple mics at once
-  if (mInputDeviceUsers.Count() > 0 &&
-      !mInputDeviceUsers.Get(aListener, nullptr)) {
-    NS_ASSERTION(false, "Input from multiple mics not yet supported; bug 1238038");
-    // Need to support separate input-only AudioCallback drivers; they'll
-    // call us back on "other" threads.  We will need to echo-cancel them, though.
+  // Only allow one device per MSG (hence, per document), but allow opening a
+  // device multiple times
+  nsTArray<RefPtr<AudioDataListener>>& listeners = mInputDeviceUsers.GetOrInsert(aID);
+  if (listeners.IsEmpty() && mInputDeviceUsers.Count() > 1) {
+    // We don't support opening multiple input device in a graph for now.
+    listeners.RemoveElement(aID);
     return;
   }
-  mInputWanted = true;
-
-  // Add to count of users for this ID.
-  // XXX Since we can't rely on IDs staying valid (ugh), use the listener as
-  // a stand-in for the ID.  Fix as part of support for multiple-captures
-  // (Bug 1238038)
-  uint32_t count = 0;
-  mInputDeviceUsers.Get(aListener, &count); // ok if this fails
-  count++;
-  mInputDeviceUsers.Put(aListener, count); // creates a new entry in the hash if needed
-
-  if (count == 1) { // first open for this listener
-    // aID is a cubeb_devid, and we assume that opaque ptr is valid until
-    // we close cubeb.
+
+  MOZ_ASSERT(!listeners.Contains(aListener), "Don't add a listener twice.");
+
+  listeners.AppendElement(aListener);
+
+  if (listeners.Length() == 1) { // first open for this device
     mInputDeviceID = aID;
-    mAudioInputs.AppendElement(aListener); // always monitor speaker data
-
     // Switch Drivers since we're adding input (to input-only or full-duplex)
     MonitorAutoLock mon(mMonitor);
     if (LifecycleStateRef() == LIFECYCLE_RUNNING) {
-      AudioCallbackDriver* driver = new AudioCallbackDriver(this);
-      driver->SetMicrophoneActive(true);
+      AudioCallbackDriver* driver = new AudioCallbackDriver(this, AudioInputChannelCount());
       LOG(
         LogLevel::Debug,
-        ("OpenAudioInput: starting new AudioCallbackDriver(input) %p", driver));
-      LOG(
-        LogLevel::Debug,
-        ("OpenAudioInput: starting new AudioCallbackDriver(input) %p", driver));
-      driver->SetInputListener(aListener);
+        ("%p OpenAudioInput: starting new AudioCallbackDriver(input) %p", this, driver));
       CurrentDriver()->SwitchAtNextIteration(driver);
    } else {
      LOG(LogLevel::Error, ("OpenAudioInput in shutdown!"));
-     LOG(LogLevel::Debug, ("OpenAudioInput in shutdown!"));
-     NS_ASSERTION(false, "Can't open cubeb inputs in shutdown");
+     MOZ_ASSERT_UNREACHABLE("Can't open cubeb inputs in shutdown");
     }
   }
 }
 
 nsresult
-MediaStreamGraphImpl::OpenAudioInput(int aID,
-                                     AudioDataListener *aListener)
+MediaStreamGraphImpl::OpenAudioInput(CubebUtils::AudioDeviceID aID,
+                                     AudioDataListener* aListener)
 {
   // So, so, so annoying.  Can't AppendMessage except on Mainthread
   if (!NS_IsMainThread()) {
     RefPtr<nsIRunnable> runnable =
       WrapRunnable(this,
                    &MediaStreamGraphImpl::OpenAudioInput,
                    aID,
                    RefPtr<AudioDataListener>(aListener));
     mAbstractMainThread->Dispatch(runnable.forget());
     return NS_OK;
   }
   class Message : public ControlMessage {
   public:
-    Message(MediaStreamGraphImpl *aGraph, int aID,
-            AudioDataListener *aListener) :
+    Message(MediaStreamGraphImpl *aGraph, CubebUtils::AudioDeviceID aID,
+            AudioDataListener* aListener) :
       ControlMessage(nullptr), mGraph(aGraph), mID(aID), mListener(aListener) {}
     void Run() override
     {
       mGraph->OpenAudioInputImpl(mID, mListener);
     }
     MediaStreamGraphImpl *mGraph;
-    int mID;
+    CubebUtils::AudioDeviceID mID;
     RefPtr<AudioDataListener> mListener;
   };
   // XXX Check not destroyed!
   this->AppendMessage(MakeUnique<Message>(this, aID, aListener));
   return NS_OK;
 }
 
 void
-MediaStreamGraphImpl::CloseAudioInputImpl(AudioDataListener *aListener)
+MediaStreamGraphImpl::CloseAudioInputImpl(Maybe<CubebUtils::AudioDeviceID>& aID, AudioDataListener* aListener)
 {
-  MOZ_ASSERT(OnGraphThread());
-  uint32_t count;
-  DebugOnly<bool> result = mInputDeviceUsers.Get(aListener, &count);
-  MOZ_ASSERT(result);
-  if (--count > 0) {
-    mInputDeviceUsers.Put(aListener, count);
-    return; // still in use
+  MOZ_ASSERT(OnGraphThreadOrNotRunning());
+  // It is possible to not know the ID here, find it first.
+  if (aID.isNothing()) {
+    for (auto iter = mInputDeviceUsers.Iter(); !iter.Done(); iter.Next()) {
+      if (iter.Data().Contains(aListener)) {
+        aID = Some(iter.Key());
+      }
+    }
+    MOZ_ASSERT(aID.isSome(), "Closing an audio input that was not opened.");
   }
-  mInputDeviceUsers.Remove(aListener);
-  mInputDeviceID = -1;
-  mInputWanted = false;
-  AudioCallbackDriver *driver = CurrentDriver()->AsAudioCallbackDriver();
-  if (driver) {
-    driver->RemoveInputListener(aListener);
+
+  nsTArray<RefPtr<AudioDataListener>>* listeners = mInputDeviceUsers.GetValue(aID.value());
+
+  MOZ_ASSERT(listeners);
+  DebugOnly<bool> wasPresent = listeners->RemoveElement(aListener);
+  MOZ_ASSERT(wasPresent);
+
+  // Breaks the cycle between the MSG and the listener.
+  aListener->Disconnect(this);
+
+  if (!listeners->IsEmpty()) {
+    // There is still a consumer for this audio input device
+    return;
   }
-  mAudioInputs.RemoveElement(aListener);
+
+  mInputDeviceID = nullptr; // reset to default
+  mInputDeviceUsers.Remove(aID.value());
 
   // Switch Drivers since we're adding or removing an input (to nothing/system or output only)
   bool audioTrackPresent = AudioTrackPresent();
 
   MonitorAutoLock mon(mMonitor);
   if (LifecycleStateRef() == LIFECYCLE_RUNNING) {
     GraphDriver* driver;
     if (audioTrackPresent) {
       // We still have audio output
-      LOG(LogLevel::Debug, ("CloseInput: output present (AudioCallback)"));
-
-      driver = new AudioCallbackDriver(this);
+      LOG(LogLevel::Debug, ("%p: CloseInput: output present (AudioCallback)", this));
+
+      driver = new AudioCallbackDriver(this, AudioInputChannelCount());
       CurrentDriver()->SwitchAtNextIteration(driver);
     } else if (CurrentDriver()->AsAudioCallbackDriver()) {
       LOG(LogLevel::Debug,
-          ("CloseInput: no output present (SystemClockCallback)"));
+          ("%p: CloseInput: no output present (SystemClockCallback)", this));
 
       driver = new SystemClockDriver(this);
       CurrentDriver()->SwitchAtNextIteration(driver);
     } // else SystemClockDriver->SystemClockDriver, no switch
   }
 }
 
 void
-MediaStreamGraphImpl::CloseAudioInput(AudioDataListener *aListener)
+MediaStreamGraphImpl::CloseAudioInput(Maybe<CubebUtils::AudioDeviceID>& aID, AudioDataListener* aListener)
 {
   // So, so, so annoying.  Can't AppendMessage except on Mainthread
   if (!NS_IsMainThread()) {
     RefPtr<nsIRunnable> runnable =
       WrapRunnable(this,
                    &MediaStreamGraphImpl::CloseAudioInput,
+                   aID,
                    RefPtr<AudioDataListener>(aListener));
     mAbstractMainThread->Dispatch(runnable.forget());
     return;
   }
   class Message : public ControlMessage {
   public:
-    Message(MediaStreamGraphImpl *aGraph, AudioDataListener *aListener) :
-      ControlMessage(nullptr), mGraph(aGraph), mListener(aListener) {}
+    Message(MediaStreamGraphImpl *aGraph,
+            Maybe<CubebUtils::AudioDeviceID>& aID,
+            AudioDataListener* aListener)
+      : ControlMessage(nullptr),
+        mGraph(aGraph),
+        mID(aID),
+        mListener(aListener)
+    {}
     void Run() override
     {
-      mGraph->CloseAudioInputImpl(mListener);
+      mGraph->CloseAudioInputImpl(mID, mListener);
     }
     MediaStreamGraphImpl *mGraph;
+    Maybe<CubebUtils::AudioDeviceID> mID;
     RefPtr<AudioDataListener> mListener;
   };
-  this->AppendMessage(MakeUnique<Message>(this, aListener));
+  this->AppendMessage(MakeUnique<Message>(this, aID, aListener));
 }
 
 // All AudioInput listeners get the same speaker data (at least for now).
 void
-MediaStreamGraph::NotifyOutputData(AudioDataValue* aBuffer, size_t aFrames,
-                                   TrackRate aRate, uint32_t aChannels)
+MediaStreamGraphImpl::NotifyOutputData(AudioDataValue* aBuffer, size_t aFrames,
+                                       TrackRate aRate, uint32_t aChannels)
+{
+  if (!mInputDeviceID) {
+    return;
+  }
+  // When/if we decide to support multiple input devices per graph, this needs
+  // to loop over them.
+  nsTArray<RefPtr<AudioDataListener>>* listeners = mInputDeviceUsers.GetValue(mInputDeviceID);
+  MOZ_ASSERT(listeners);
+  for (auto& listener : *listeners) {
+    listener->NotifyOutputData(this, aBuffer, aFrames, aRate, aChannels);
+  }
+}
+
+void
+MediaStreamGraphImpl::NotifyInputData(const AudioDataValue* aBuffer, size_t aFrames,
+                                      TrackRate aRate, uint32_t aChannels)
+{
+#ifdef DEBUG
+  {
+    MonitorAutoLock lock(mMonitor);
+    // Either we have an audio input device, or we just removed the audio input
+    // this iteration, and we're switching back to an output-only driver next
+    // iteration.
+    MOZ_ASSERT(mInputDeviceID || CurrentDriver()->Switching());
+  }
+#endif
+  if (!mInputDeviceID) {
+    return;
+  }
+  nsTArray<RefPtr<AudioDataListener>>* listeners = mInputDeviceUsers.GetValue(mInputDeviceID);
+  MOZ_ASSERT(listeners);
+  for (auto& listener : *listeners) {
+    listener->NotifyInputData(this, aBuffer, aFrames, aRate, aChannels);
+  }
+}
+
+void MediaStreamGraphImpl::DeviceChangedImpl()
+{
+  MOZ_ASSERT(OnGraphThread());
+
+  if (!mInputDeviceID) {
+    return;
+  }
+
+  nsTArray<RefPtr<AudioDataListener>>* listeners =
+    mInputDeviceUsers.GetValue(mInputDeviceID);
+  for (auto& listener : *listeners) {
+    listener->DeviceChanged(this);
+  }
+}
+
+void MediaStreamGraphImpl::DeviceChanged()
 {
-  for (auto& listener : mAudioInputs) {
-    listener->NotifyOutputData(this, aBuffer, aFrames, aRate, aChannels);
+  // This is safe to be called from any thread: this message comes from an
+  // underlying platform API, and we don't have much guarantees. If it is not
+  // called from the main thread (and it probably will rarely be), it will post
+  // itself to the main thread, and the actual device change message will be ran
+  // and acted upon on the graph thread.
+  if (!NS_IsMainThread()) {
+    RefPtr<nsIRunnable> runnable =
+      WrapRunnable(this,
+                   &MediaStreamGraphImpl::DeviceChanged);
+    mAbstractMainThread->Dispatch(runnable.forget());
+    return;
+  }
+
+  class Message : public ControlMessage {
+  public:
+    explicit Message(MediaStreamGraph* aGraph)
+      : ControlMessage(nullptr)
+      , mGraphImpl(static_cast<MediaStreamGraphImpl*>(aGraph))
+    {}
+    void Run() override
+    {
+      mGraphImpl->DeviceChangedImpl();
+    }
+    // We know that this is valid, because the graph can't shutdown if it has
+    // messages.
+    MediaStreamGraphImpl* mGraphImpl;
+  };
+
+  AppendMessage(MakeUnique<Message>(this));
+}
+
+void MediaStreamGraphImpl::ReevaluateInputDevice()
+{
+  MOZ_ASSERT(OnGraphThread());
+  bool needToSwitch = false;
+
+  if (CurrentDriver()->AsAudioCallbackDriver()) {
+    AudioCallbackDriver* audioCallbackDriver = CurrentDriver()->AsAudioCallbackDriver();
+    if (audioCallbackDriver->InputChannelCount() != AudioInputChannelCount()) {
+      needToSwitch = true;
+    }
+  } else {
+    // We're already in the process of switching to a audio callback driver,
+    // which will happen at the next iteration.
+    // However, maybe it's not the correct number of channels. Re-query the
+    // correct channel amount at this time.
+#ifdef DEBUG
+    MonitorAutoLock lock(mMonitor);
+    MOZ_ASSERT(CurrentDriver()->Switching());
+#endif
+    needToSwitch = true;
+  }
+  if (needToSwitch) {
+    AudioCallbackDriver* newDriver = new AudioCallbackDriver(this, AudioInputChannelCount());
+    {
+      MonitorAutoLock lock(mMonitor);
+      CurrentDriver()->SwitchAtNextIteration(newDriver);
+    }
   }
 }
 
 bool
 MediaStreamGraph::OnGraphThreadOrNotRunning() const
 {
   // either we're on the right thread (and calling CurrentDriver() is safe),
   // or we're going to fail the assert anyway, so don't cross-check
@@ -1167,22 +1275,23 @@ MediaStreamGraphImpl::UpdateGraph(GraphT
     if (stream->mFinished) {
       // The stream's not suspended, and since it's finished, underruns won't
       // stop it playing out. So there's no blocking other than what we impose
       // here.
       GraphTime endTime = stream->GetStreamTracks().GetAllTracksEnd() +
           stream->mTracksStartTime;
       if (endTime <= mStateComputedTime) {
         LOG(LogLevel::Verbose,
-            ("MediaStream %p is blocked due to being finished", stream));
+            ("%p: MediaStream %p is blocked due to being finished", this, stream));
         stream->mStartBlocking = mStateComputedTime;
       } else {
         LOG(LogLevel::Verbose,
-            ("MediaStream %p is finished, but not blocked yet (end at %f, with "
+            ("%p: MediaStream %p is finished, but not blocked yet (end at %f, with "
              "blocking at %f)",
+             this,
              stream,
              MediaTimeToSeconds(stream->GetTracksEnd()),
              MediaTimeToSeconds(endTime)));
         // Data can't be added to a finished stream, so underruns are irrelevant.
         stream->mStartBlocking = std::min(endTime, aEndBlockingDecisions);
       }
     } else {
       stream->mStartBlocking = WillUnderrun(stream, aEndBlockingDecisions);
@@ -1190,18 +1299,19 @@ MediaStreamGraphImpl::UpdateGraph(GraphT
       SourceMediaStream* s = stream->AsSourceStream();
       if (s && s->mPullEnabled) {
         for (StreamTracks::TrackIter i(s->mTracks); !i.IsEnded(); i.Next()) {
           if (i->IsEnded()) {
             continue;
           }
           if (i->GetEnd() < stream->GraphTimeToStreamTime(aEndBlockingDecisions)) {
             LOG(LogLevel::Error,
-                ("SourceMediaStream %p track %u (%s) is live and pulled, but wasn't fed "
+                ("%p: SourceMediaStream %p track %u (%s) is live and pulled, but wasn't fed "
                  "enough data. Listeners=%zu. Track-end=%f, Iteration-end=%f",
+                 this,
                  stream,
                  i->GetID(),
                  (i->GetType() == MediaSegment::AUDIO ? "audio" : "video"),
                  stream->mListeners.Length(),
                  MediaTimeToSeconds(i->GetEnd()),
                  MediaTimeToSeconds(stream->GraphTimeToStreamTime(aEndBlockingDecisions))));
             MOZ_DIAGNOSTIC_ASSERT(false,
                                   "A non-finished SourceMediaStream wasn't fed "
@@ -1385,17 +1495,17 @@ MediaStreamGraphImpl::ApplyStreamUpdate(
     stream->NotifyMainThreadListeners();
   }
 }
 
 void
 MediaStreamGraphImpl::ForceShutDown(media::ShutdownTicket* aShutdownTicket)
 {
   MOZ_ASSERT(NS_IsMainThread(), "Must be called on main thread");
-  LOG(LogLevel::Debug, ("MediaStreamGraph %p ForceShutdown", this));
+  LOG(LogLevel::Debug, ("%p: MediaStreamGraph::ForceShutdown", this));
 
   if (aShutdownTicket) {
     MOZ_ASSERT(!mForceShutdownTicket);
     // Avoid waiting forever for a graph to shut down
     // synchronously.  Reports are that some 3rd-party audio drivers
     // occasionally hang in shutdown (both for us and Chrome).
     NS_NewTimerWithCallback(getter_AddRefs(mShutdownTimer),
                             this,
@@ -1449,17 +1559,17 @@ public:
     , mGraph(aGraph)
   {}
   NS_IMETHOD Run() override
   {
     MOZ_ASSERT(NS_IsMainThread());
     MOZ_ASSERT(mGraph->mDetectedNotRunning && mGraph->mDriver,
                  "We should know the graph thread control loop isn't running!");
 
-    LOG(LogLevel::Debug, ("Shutting down graph %p", mGraph.get()));
+    LOG(LogLevel::Debug, ("%p: Shutting down graph", mGraph.get()));
 
     // We've asserted the graph isn't running.  Use mDriver instead of CurrentDriver
     // to avoid thread-safety checks
 #if 0 // AudioCallbackDrivers are released asynchronously anyways
     // XXX a better test would be have setting mDetectedNotRunning make sure
     // any current callback has finished and block future ones -- or just
     // handle it all in Shutdown()!
     if (mGraph->mDriver->AsAudioCallbackDriver()) {
@@ -1600,17 +1710,17 @@ MediaStreamGraphImpl::RunInStableState(b
       "LIFECYCLE_RUNNING",
       "LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP",
       "LIFECYCLE_WAITING_FOR_THREAD_SHUTDOWN",
       "LIFECYCLE_WAITING_FOR_STREAM_DESTRUCTION"
     };
 
     if (LifecycleStateRef() != LIFECYCLE_RUNNING) {
       LOG(LogLevel::Debug,
-          ("Running %p in stable state. Current state: %s",
+          ("%p: Running stable state callback. Current state: %s",
            this,
            LifecycleState_str[LifecycleStateRef()]));
     }
 
     runnables.SwapElements(mUpdateRunnables);
     for (uint32_t i = 0; i < mStreamUpdates.Length(); ++i) {
       StreamUpdate* update = &mStreamUpdates[i];
       if (update->mStream) {
@@ -1623,21 +1733,21 @@ MediaStreamGraphImpl::RunInStableState(b
       if (LifecycleStateRef() == LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP && IsEmpty()) {
         // Complete shutdown. First, ensure that this graph is no longer used.
         // A new graph graph will be created if one is needed.
         // Asynchronously clean up old graph. We don't want to do this
         // synchronously because it spins the event loop waiting for threads
         // to shut down, and we don't want to do that in a stable state handler.
         LifecycleStateRef() = LIFECYCLE_WAITING_FOR_THREAD_SHUTDOWN;
         LOG(LogLevel::Debug,
-            ("Sending MediaStreamGraphShutDownRunnable %p", this));
+            ("%p: Sending MediaStreamGraphShutDownRunnable", this));
         nsCOMPtr<nsIRunnable> event = new MediaStreamGraphShutDownRunnable(this );
         mAbstractMainThread->Dispatch(event.forget());
 
-        LOG(LogLevel::Debug, ("Disconnecting MediaStreamGraph %p", this));
+        LOG(LogLevel::Debug, ("%p: Disconnecting MediaStreamGraph", this));
 
         // Find the graph in the hash table and remove it.
         for (auto iter = gGraphs.Iter(); !iter.Done(); iter.Next()) {
           if (iter.UserData() == this) {
             iter.Remove();
             break;
           }
         }
@@ -1656,20 +1766,20 @@ MediaStreamGraphImpl::RunInStableState(b
       if (LifecycleStateRef() == LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP &&
           mRealtime && !mForceShutDown) {
         LifecycleStateRef() = LIFECYCLE_RUNNING;
         // Revive the MediaStreamGraph since we have more messages going to it.
         // Note that we need to put messages into its queue before reviving it,
         // or it might exit immediately.
         {
           LOG(LogLevel::Debug,
-              ("Reviving a graph (%p) ! %s",
+              ("%p: Reviving this graph! %s",
                this,
-               CurrentDriver()->AsAudioCallbackDriver() ? "AudioDriver"
-                                                        : "SystemDriver"));
+               CurrentDriver()->AsAudioCallbackDriver() ? "AudioCallbackDriver"
+                                                        : "SystemClockDriver"));
           RefPtr<GraphDriver> driver = CurrentDriver();
           MonitorAutoUnlock unlock(mMonitor);
           driver->Revive();
         }
       }
     }
 
     // Don't start the thread for a non-realtime graph until it has been
@@ -1679,20 +1789,20 @@ MediaStreamGraphImpl::RunInStableState(b
       LifecycleStateRef() = LIFECYCLE_RUNNING;
       // Start the thread now. We couldn't start it earlier because
       // the graph might exit immediately on finding it has no streams. The
       // first message for a new graph must create a stream.
       {
         // We should exit the monitor for now, because starting a stream might
         // take locks, and we don't want to deadlock.
         LOG(LogLevel::Debug,
-            ("Starting a graph (%p) ! %s",
+            ("%p: Starting a graph with a %s",
              this,
-             CurrentDriver()->AsAudioCallbackDriver() ? "AudioDriver"
-                                                      : "SystemDriver"));
+             CurrentDriver()->AsAudioCallbackDriver() ? "AudioCallbackDriver"
+                                                      : "SystemClockDriver"));
         RefPtr<GraphDriver> driver = CurrentDriver();
         MonitorAutoUnlock unlock(mMonitor);
         driver->Start();
         // It's not safe to Shutdown() a thread from StableState, and
         // releasing this may shutdown a SystemClockDriver thread.
         // Proxy the release to outside of StableState.
         NS_ReleaseOnMainThreadSystemGroup(
           "MediaStreamGraphImpl::CurrentDriver", driver.forget(),
@@ -1770,17 +1880,17 @@ MediaStreamGraphImpl::SignalMainThreadCl
 {
   MOZ_ASSERT(mDriver->OnThread());
 
   MonitorAutoLock lock(mMonitor);
   // LIFECYCLE_THREAD_NOT_STARTED is possible when shutting down offline
   // graphs that have not started.
   MOZ_DIAGNOSTIC_ASSERT(mLifecycleState <= LIFECYCLE_RUNNING);
   LOG(LogLevel::Debug,
-      ("MediaStreamGraph %p waiting for main thread cleanup", this));
+      ("%p: MediaStreamGraph waiting for main thread cleanup", this));
   LifecycleStateRef() =
     MediaStreamGraphImpl::LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP;
   EnsureStableStateEventPosted();
 }
 
 void
 MediaStreamGraphImpl::AppendMessage(UniquePtr<ControlMessage> aMessage)
 {
@@ -2702,40 +2812,41 @@ SourceMediaStream::SourceMediaStream()
   , mUpdateKnownTracksTime(0)
   , mPullEnabled(false)
   , mFinishPending(false)
   , mNeedsMixing(false)
 {
 }
 
 nsresult
-SourceMediaStream::OpenAudioInput(int aID,
+SourceMediaStream::OpenAudioInput(CubebUtils::AudioDeviceID aID,
                                   AudioDataListener *aListener)
 {
-  if (GraphImpl()) {
-    mInputListener = aListener;
-    return GraphImpl()->OpenAudioInput(aID, aListener);
-  }
-  return NS_ERROR_FAILURE;
+  MOZ_ASSERT(GraphImpl());
+  mInputListener = aListener;
+  return GraphImpl()->OpenAudioInput(aID, aListener);
 }
 
 void
-SourceMediaStream::CloseAudioInput()
+SourceMediaStream::CloseAudioInput(Maybe<CubebUtils::AudioDeviceID>& aID,
+                                   AudioDataListener* aListener)
 {
+  MOZ_ASSERT(mInputListener == aListener);
   // Destroy() may have run already and cleared this
   if (GraphImpl() && mInputListener) {
-    GraphImpl()->CloseAudioInput(mInputListener);
+    GraphImpl()->CloseAudioInput(aID, aListener);
   }
   mInputListener = nullptr;
 }
 
 void
 SourceMediaStream::DestroyImpl()
 {
-  CloseAudioInput();
+  Maybe<CubebUtils::AudioDeviceID> id = Nothing();
+  CloseAudioInput(id, mInputListener);
 
   GraphImpl()->AssertOnGraphThreadOrNotRunning();
   for (int32_t i = mConsumers.Length() - 1; i >= 0; --i) {
     // Disconnect before we come under mMutex's lock since it can call back
     // through RemoveDirectTrackListenerImpl() and deadlock.
     mConsumers[i]->Disconnect();
   }
 
@@ -2774,17 +2885,18 @@ SourceMediaStream::PullNewData(StreamTim
   if (!mPullEnabled || mFinished) {
     return false;
   }
   // Compute how much stream time we'll need assuming we don't block
   // the stream at all.
   StreamTime t = GraphTimeToStreamTime(aDesiredUpToTime);
   StreamTime current = mTracks.GetEnd();
   LOG(LogLevel::Verbose,
-      ("Calling NotifyPull aStream=%p t=%f current end=%f",
+      ("%p: Calling NotifyPull aStream=%p t=%f current end=%f",
+        GraphImpl(),
         this,
         GraphImpl()->MediaTimeToSeconds(t),
         GraphImpl()->MediaTimeToSeconds(current)));
   if (t <= current) {
     return false;
   }
   for (uint32_t j = 0; j < mListeners.Length(); ++j) {
     MediaStreamListener* l = mListeners[j];
@@ -2875,35 +2987,37 @@ SourceMediaStream::ExtractPendingInput()
       b.mListener->NotifyQueuedChanges(GraphImpl(), offset, *data->mData);
       if (data->mCommands & SourceMediaStream::TRACK_END) {
         b.mListener->NotifyEnded();
       }
     }
     if (data->mCommands & SourceMediaStream::TRACK_CREATE) {
       MediaSegment* segment = data->mData.forget();
       LOG(LogLevel::Debug,
-          ("SourceMediaStream %p creating track %d, start %" PRId64
+          ("%p: SourceMediaStream %p creating track %d, start %" PRId64
             ", initial end %" PRId64,
+            GraphImpl(),
             this,
             data->mID,
             int64_t(data->mStart),
             int64_t(segment->GetDuration())));
 
       data->mEndOfFlushedData += segment->GetDuration();
       mTracks.AddTrack(data->mID, data->mStart, segment);
       // The track has taken ownership of data->mData, so let's replace
       // data->mData with an empty clone.
       data->mData = segment->CreateEmptyClone();
       data->mCommands &= ~SourceMediaStream::TRACK_CREATE;
       shouldNotifyTrackCreated = true;
     } else if (data->mData->GetDuration() > 0) {
       MediaSegment* dest = mTracks.FindTrack(data->mID)->GetSegment();
       LOG(LogLevel::Verbose,
-          ("SourceMediaStream %p track %d, advancing end from %" PRId64
+          ("%p: SourceMediaStream %p track %d, advancing end from %" PRId64
             " to %" PRId64,
+            GraphImpl(),
             this,
             data->mID,
             int64_t(dest->GetDuration()),
             int64_t(dest->GetDuration() + data->mData->GetDuration())));
       data->mEndOfFlushedData += data->mData->GetDuration();
       dest->AppendFrom(data->mData);
     }
     if (data->mCommands & SourceMediaStream::TRACK_END) {
@@ -2933,17 +3047,18 @@ void
 SourceMediaStream::AddTrackInternal(TrackID aID, TrackRate aRate, StreamTime aStart,
                                     MediaSegment* aSegment, uint32_t aFlags)
 {
   MutexAutoLock lock(mMutex);
   nsTArray<TrackData> *track_data = (aFlags & ADDTRACK_QUEUED) ?
                                     &mPendingTracks : &mUpdateTracks;
   TrackData* data = track_data->AppendElement();
   LOG(LogLevel::Debug,
-      ("AddTrackInternal: %lu/%lu",
+      ("%p: AddTrackInternal: %lu/%lu",
+       GraphImpl(),
        (long)mPendingTracks.Length(),
        (long)mUpdateTracks.Length()));
   data->mID = aID;
   data->mInputRate = aRate;
   data->mResamplerChannelCount = 0;
   data->mStart = aStart;
   data->mEndOfFlushedData = aStart;
   data->mCommands = TRACK_CREATE;
@@ -2962,17 +3077,18 @@ SourceMediaStream::AddAudioTrack(TrackID
 }
 
 void
 SourceMediaStream::FinishAddTracks()
 {
   MutexAutoLock lock(mMutex);
   mUpdateTracks.AppendElements(std::move(mPendingTracks));
   LOG(LogLevel::Debug,
-      ("FinishAddTracks: %lu/%lu",
+      ("%p: FinishAddTracks: %lu/%lu",
+       GraphImpl(),
        (long)mPendingTracks.Length(),
        (long)mUpdateTracks.Length()));
   if (GraphImpl()) {
     GraphImpl()->EnsureNextIteration();
   }
 }
 
 void
@@ -3093,38 +3209,40 @@ void
 SourceMediaStream::AddDirectTrackListenerImpl(already_AddRefed<DirectMediaStreamTrackListener> aListener,
                                               TrackID aTrackID)
 {
   MOZ_ASSERT(IsTrackIDExplicit(aTrackID));
   MutexAutoLock lock(mMutex);
 
   RefPtr<DirectMediaStreamTrackListener> listener = aListener;
   LOG(LogLevel::Debug,
-      ("Adding direct track listener %p bound to track %d to source stream %p",
+      ("%p: Adding direct track listener %p bound to track %d to source stream %p",
+       GraphImpl(),
        listener.get(),
        aTrackID,
        this));
 
   StreamTracks::Track* track = FindTrack(aTrackID);
 
   if (!track) {
     LOG(LogLevel::Warning,
-        ("Couldn't find source track for direct track listener %p",
+        ("%p: Couldn't find source track for direct track listener %p",
+         GraphImpl(),
          listener.get()));
     listener->NotifyDirectListenerInstalled(
       DirectMediaStreamTrackListener::InstallationResult::TRACK_NOT_FOUND_AT_SOURCE);
     return;
   }
 
   bool isAudio = track->GetType() == MediaSegment::AUDIO;
   bool isVideo = track->GetType() == MediaSegment::VIDEO;
   if (!isAudio && !isVideo) {
     LOG(
       LogLevel::Warning,
-      ("Source track for direct track listener %p is unknown", listener.get()));
+      ("%p: Source track for direct track listener %p is unknown", GraphImpl(), listener.get()));
     MOZ_ASSERT(false);
     return;
   }
 
   for (auto entry : mDirectTrackListeners) {
     if (entry.mListener == listener &&
         (entry.mTrackID == TRACK_ANY || entry.mTrackID == aTrackID)) {
       listener->NotifyDirectListenerInstalled(
@@ -3133,17 +3251,17 @@ SourceMediaStream::AddDirectTrackListene
     }
   }
 
   TrackBound<DirectMediaStreamTrackListener>* sourceListener =
     mDirectTrackListeners.AppendElement();
   sourceListener->mListener = listener;
   sourceListener->mTrackID = aTrackID;
 
-  LOG(LogLevel::Debug, ("Added direct track listener %p", listener.get()));
+  LOG(LogLevel::Debug, ("%p: Added direct track listener %p", GraphImpl(), listener.get()));
   listener->NotifyDirectListenerInstalled(
     DirectMediaStreamTrackListener::InstallationResult::SUCCESS);
 
   // Pass buffered data to the listener
   AudioSegment bufferedAudio;
   VideoSegment bufferedVideo;
   MediaSegment& bufferedData =
     isAudio ? static_cast<MediaSegment&>(bufferedAudio)
@@ -3234,25 +3352,27 @@ SourceMediaStream::SetTrackEnabledImpl(T
     for (TrackBound<DirectMediaStreamTrackListener>& l: mDirectTrackListeners) {
       if (l.mTrackID != aTrackID) {
         continue;
       }
       DisabledTrackMode oldMode = GetDisabledTrackMode(aTrackID);
       bool oldEnabled = oldMode == DisabledTrackMode::ENABLED;
       if (!oldEnabled && aMode == DisabledTrackMode::ENABLED) {
         LOG(LogLevel::Debug,
-            ("SourceMediaStream %p track %d setting "
+            ("%p: SourceMediaStream %p track %d setting "
              "direct listener enabled",
+             GraphImpl(),
              this,
              aTrackID));
         l.mListener->DecreaseDisabled(oldMode);
       } else if (oldEnabled && aMode != DisabledTrackMode::ENABLED) {
         LOG(LogLevel::Debug,
-            ("SourceMediaStream %p track %d setting "
+            ("%p: SourceMediaStream %p track %d setting "
              "direct listener disabled",
+             GraphImpl(),
              this,
              aTrackID));
         l.mListener->IncreaseDisabled(aMode);
       }
     }
   }
   MediaStream::SetTrackEnabledImpl(aTrackID, aMode);
 }
@@ -3311,60 +3431,22 @@ SourceMediaStream::HasPendingAudioTrack(
       audioTrackPresent = true;
       break;
     }
   }
 
   return audioTrackPresent;
 }
 
-bool
-SourceMediaStream::OpenNewAudioCallbackDriver(AudioDataListener * aListener)
-{
-  // Can't AppendMessage except on Mainthread. This is an ungly trick
-  // to bounce the message in mainthread and then in MSG thread.
-  if (!NS_IsMainThread()) {
-    RefPtr<nsIRunnable> runnable =
-      WrapRunnable(this,
-                   &SourceMediaStream::OpenNewAudioCallbackDriver,
-                   aListener);
-    GraphImpl()->mAbstractMainThread->Dispatch(runnable.forget());
-    return true;
-  }
-
-  AudioCallbackDriver* nextDriver = new AudioCallbackDriver(GraphImpl());
-  nextDriver->SetInputListener(aListener);
-
-  class Message : public ControlMessage {
-  public:
-    Message(MediaStream* aStream, AudioCallbackDriver* aNextDriver)
-    : ControlMessage(aStream)
-    , mNextDriver(aNextDriver)
-    {MOZ_ASSERT(mNextDriver);}
-    void Run() override
-    {
-       MediaStreamGraphImpl* graphImpl = mNextDriver->GraphImpl();
-       MonitorAutoLock mon(graphImpl->GetMonitor());
-       MOZ_ASSERT(graphImpl->LifecycleStateRef() ==
-                  MediaStreamGraphImpl::LifecycleState::LIFECYCLE_RUNNING);
-       graphImpl->CurrentDriver()->SwitchAtNextIteration(mNextDriver);
-    }
-    AudioCallbackDriver* mNextDriver;
-  };
-  GraphImpl()->AppendMessage(MakeUnique<Message>(this, nextDriver));
-
-  return true;
-}
-
-
 void
 MediaInputPort::Init()
 {
   LOG(LogLevel::Debug,
-      ("Adding MediaInputPort %p (from %p to %p) to the graph",
+      ("%p: Adding MediaInputPort %p (from %p to %p)",
+       mSource->GraphImpl(),
        this,
        mSource,
        mDest));
   mSource->AddConsumer(this);
   mDest->AddInput(this);
   // mPortCount decremented via MediaInputPort::Destroy's message
   ++mDest->GraphImpl()->mPortCount;
 }
@@ -3606,20 +3688,18 @@ ProcessedMediaStream::DestroyImpl()
 }
 
 MediaStreamGraphImpl::MediaStreamGraphImpl(GraphDriverType aDriverRequested,
                                            TrackRate aSampleRate,
                                            AbstractThread* aMainThread)
   : MediaStreamGraph(aSampleRate)
   , mFirstCycleBreaker(0)
   , mPortCount(0)
-  , mInputWanted(false)
-  , mInputDeviceID(-1)
-  , mOutputWanted(true)
-  , mOutputDeviceID(-1)
+  , mInputDeviceID(nullptr)
+  , mOutputDeviceID(nullptr)
   , mNeedAnotherIteration(false)
   , mGraphDriverAsleep(false)
   , mMonitor("MediaStreamGraphImpl")
   , mLifecycleState(LIFECYCLE_THREAD_NOT_STARTED)
   , mEndTime(GRAPH_TIME_MAX)
   , mForceShutDown(false)
   , mPostedRunInStableStateEvent(false)
   , mDetectedNotRunning(false)
@@ -3632,18 +3712,18 @@ MediaStreamGraphImpl::MediaStreamGraphIm
   , mSelfRef(this)
   , mOutputChannels(std::min<uint32_t>(8, CubebUtils::MaxNumberOfChannels()))
 #ifdef DEBUG
   , mCanRunMessagesSynchronously(false)
 #endif
 {
   if (mRealtime) {
     if (aDriverRequested == AUDIO_THREAD_DRIVER) {
-      AudioCallbackDriver* driver = new AudioCallbackDriver(this);
-      mDriver = driver;
+      // Always start with zero input channels.
+      mDriver = new AudioCallbackDriver(this, 0);
     } else {
       mDriver = new SystemClockDriver(this);
     }
 
 #ifdef TRACING
     // This is a noop if the logger has not been enabled.
     gMSGTraceLogger.Start();
     gMSGTraceLogger.Log("[");
@@ -3758,17 +3838,17 @@ MediaStreamGraph::GetInstance(MediaStrea
     graph = new MediaStreamGraphImpl(aGraphDriverRequested,
                                      sampleRate,
                                      mainThread);
 
     uint32_t hashkey = WindowToHash(aWindow, sampleRate);
     gGraphs.Put(hashkey, graph);
 
     LOG(LogLevel::Debug,
-        ("Starting up MediaStreamGraph %p for window %p for sample rate %d", graph, aWindow, sampleRate));
+        ("Starting up MediaStreamGraph %p for window %p", graph, aWindow));
   }
 
   return graph;
 }
 
 MediaStreamGraph*
 MediaStreamGraph::CreateNonRealtimeInstance(TrackRate aSampleRate,
                                             nsPIDOMWindowInner* aWindow)
@@ -3787,16 +3867,22 @@ MediaStreamGraph::CreateNonRealtimeInsta
 
 void
 MediaStreamGraph::DestroyNonRealtimeInstance(MediaStreamGraph* aGraph)
 {
   MOZ_ASSERT(NS_IsMainThread(), "Main thread only");
   MOZ_ASSERT(aGraph->IsNonRealtime(), "Should not destroy the global graph here");
 
   MediaStreamGraphImpl* graph = static_cast<MediaStreamGraphImpl*>(aGraph);
+
+  if (!graph->mNonRealtimeProcessing) {
+    // Start the graph, but don't produce anything
+    graph->StartNonRealtimeProcessing(0);
+  }
+
   graph->ForceShutDown(nullptr);
 }
 
 NS_IMPL_ISUPPORTS(MediaStreamGraphImpl, nsIMemoryReporter, nsITimerCallback,
                   nsINamed)
 
 NS_IMETHODIMP
 MediaStreamGraphImpl::CollectReports(nsIHandleReportCallback* aHandleReport,
@@ -4156,17 +4242,17 @@ MediaStreamGraphImpl::ApplyAudioContextO
   // anyways, but doing this now save some time.
   if (aOperation == AudioContextOperation::Resume) {
     if (!CurrentDriver()->AsAudioCallbackDriver()) {
       AudioCallbackDriver* driver;
       if (switching) {
         MOZ_ASSERT(nextDriver->AsAudioCallbackDriver());
         driver = nextDriver->AsAudioCallbackDriver();
       } else {
-        driver = new AudioCallbackDriver(this);
+        driver = new AudioCallbackDriver(this, AudioInputChannelCount());
         MonitorAutoLock lock(mMonitor);
         CurrentDriver()->SwitchAtNextIteration(driver);
       }
       driver->EnqueueStreamAndPromiseForOperation(aDestinationStream,
           aPromise, aOperation);
     } else {
       // We are resuming a context, but we are already using an
       // AudioCallbackDriver, we can resolve the promise now.
--- a/dom/media/MediaStreamGraph.h
+++ b/dom/media/MediaStreamGraph.h
@@ -7,16 +7,17 @@
 #define MOZILLA_MEDIASTREAMGRAPH_H_
 
 #include "AudioStream.h"
 #include "MainThreadUtils.h"
 #include "MediaStreamTypes.h"
 #include "StreamTracks.h"
 #include "VideoSegment.h"
 #include "mozilla/LinkedList.h"
+#include "mozilla/Maybe.h"
 #include "mozilla/Mutex.h"
 #include "mozilla/TaskQueue.h"
 #include "nsAutoPtr.h"
 #include "nsAutoRef.h"
 #include "nsIRunnable.h"
 #include "nsTArray.h"
 #include <speex/speex_resampler.h>
 
@@ -100,31 +101,41 @@ protected:
 
 public:
   /* These are for cubeb audio input & output streams: */
   /**
    * Output data to speakers, for use as the "far-end" data for echo
    * cancellation.  This is not guaranteed to be in any particular size
    * chunks.
    */
-  virtual void NotifyOutputData(MediaStreamGraph* aGraph,
+  virtual void NotifyOutputData(MediaStreamGraphImpl* aGraph,
                                 AudioDataValue* aBuffer, size_t aFrames,
                                 TrackRate aRate, uint32_t aChannels) = 0;
   /**
    * Input data from a microphone (or other audio source.  This is not
    * guaranteed to be in any particular size chunks.
    */
-  virtual void NotifyInputData(MediaStreamGraph* aGraph,
+  virtual void NotifyInputData(MediaStreamGraphImpl* aGraph,
                                const AudioDataValue* aBuffer, size_t aFrames,
                                TrackRate aRate, uint32_t aChannels) = 0;
 
   /**
+   * Number of audio input channels.
+   */
+  virtual uint32_t RequestedInputChannelCount(MediaStreamGraphImpl* aGraph) = 0;
+
+  /**
    * Called when the underlying audio device has changed.
    */
-  virtual void DeviceChanged() = 0;
+  virtual void DeviceChanged(MediaStreamGraphImpl* aGraph) = 0;
+
+  /**
+   * Called when the underlying audio device is being closed.
+   */
+  virtual void Disconnect(MediaStreamGraphImpl* aGraph) = 0;
 };
 
 class AudioDataListener : public AudioDataListenerInterface {
 protected:
   // Protected destructor, to discourage deletion outside of Release():
   virtual ~AudioDataListener() {}
 
 public:
@@ -698,20 +709,21 @@ public:
    * it is still possible for a NotifyPull to occur.
    */
   void SetPullEnabled(bool aEnabled);
 
   // Users of audio inputs go through the stream so it can track when the
   // last stream referencing an input goes away, so it can close the cubeb
   // input.  Also note: callable on any thread (though it bounces through
   // MainThread to set the command if needed).
-  nsresult OpenAudioInput(int aID,
-                          AudioDataListener *aListener);
+  nsresult OpenAudioInput(CubebUtils::AudioDeviceID aID,
+                          AudioDataListener* aListener);
   // Note: also implied when Destroy() happens
-  void CloseAudioInput();
+  void CloseAudioInput(Maybe<CubebUtils::AudioDeviceID>& aID,
+                       AudioDataListener* aListener);
 
   // MediaStreamGraph thread only
   void DestroyImpl() override;
 
   // Call these on any thread.
   /**
    * Call all MediaStreamListeners to request new data via the NotifyPull API
    * (if enabled).
@@ -828,18 +840,16 @@ public:
   bool HasPendingAudioTrack();
 
   TimeStamp GetStreamTracksStrartTimeStamp()
   {
     MutexAutoLock lock(mMutex);
     return mStreamTracksStartTimeStamp;
   }
 
-  bool OpenNewAudioCallbackDriver(AudioDataListener *aListener);
-
   // XXX need a Reset API
 
   friend class MediaStreamGraphImpl;
 
 protected:
   enum TrackCommands : uint32_t;
 
   virtual ~SourceMediaStream();
@@ -1314,23 +1324,20 @@ public:
 
   // Return the correct main thread for this graph. This always returns
   // something that is valid. Thread safe.
   AbstractThread* AbstractMainThread();
 
   // Idempotent
   static void DestroyNonRealtimeInstance(MediaStreamGraph* aGraph);
 
-  virtual nsresult OpenAudioInput(int aID,
-                                  AudioDataListener *aListener)
-  {
-    return NS_ERROR_FAILURE;
-  }
-  virtual void CloseAudioInput(AudioDataListener *aListener) {}
-
+  virtual nsresult OpenAudioInput(CubebUtils::AudioDeviceID aID,
+                                  AudioDataListener* aListener) = 0;
+  virtual void CloseAudioInput(Maybe<CubebUtils::AudioDeviceID>& aID,
+                               AudioDataListener* aListener) = 0;
   // Control API.
   /**
    * Create a stream that a media decoder (or some other source of
    * media data, such as a camera) can write to.
    */
   SourceMediaStream* CreateSourceStream();
   /**
    * Create a stream that will form the union of the tracks of its input
@@ -1398,23 +1405,16 @@ public:
   TrackRate GraphRate() const { return mSampleRate; }
 
   void RegisterCaptureStreamForWindow(uint64_t aWindowId,
                                       ProcessedMediaStream* aCaptureStream);
   void UnregisterCaptureStreamForWindow(uint64_t aWindowId);
   already_AddRefed<MediaInputPort> ConnectToCaptureStream(
     uint64_t aWindowId, MediaStream* aMediaStream);
 
-  /**
-   * Data going to the speakers from the GraphDriver's DataCallback
-   * to notify any listeners (for echo cancellation).
-   */
-  void NotifyOutputData(AudioDataValue* aBuffer, size_t aFrames,
-                        TrackRate aRate, uint32_t aChannels);
-
   void AssertOnGraphThreadOrNotRunning() const
   {
     MOZ_ASSERT(OnGraphThreadOrNotRunning());
   }
 
 protected:
   explicit MediaStreamGraph(TrackRate aSampleRate)
     : mSampleRate(aSampleRate)
@@ -1435,18 +1435,13 @@ protected:
   nsTArray<nsCOMPtr<nsIRunnable> > mPendingUpdateRunnables;
 
   /**
    * Sample rate at which this graph runs. For real time graphs, this is
    * the rate of the audio mixer. For offline graphs, this is the rate specified
    * at construction.
    */
   TrackRate mSampleRate;
-
-  /**
-   * CloseAudioInput is async, so hold a reference here.
-   */
-  nsTArray<RefPtr<AudioDataListener>> mAudioInputs;
 };
 
 } // namespace mozilla
 
 #endif /* MOZILLA_MEDIASTREAMGRAPH_H_ */
--- a/dom/media/MediaStreamGraphImpl.h
+++ b/dom/media/MediaStreamGraphImpl.h
@@ -12,17 +12,17 @@
 #include "GraphDriver.h"
 #include "Latency.h"
 #include "mozilla/Atomics.h"
 #include "mozilla/Monitor.h"
 #include "mozilla/Services.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/WeakPtr.h"
-#include "nsDataHashtable.h"
+#include "nsClassHashtable.h"
 #include "nsIMemoryReporter.h"
 #include "nsINamed.h"
 #include "nsIRunnable.h"
 #include "nsIThread.h"
 #include "nsITimer.h"
 #include "AsyncLogger.h"
 
 namespace mozilla {
@@ -373,27 +373,54 @@ public:
    * If aStream doesn't need an audio stream but has one, destroy it.
    */
   void CreateOrDestroyAudioStreams(MediaStream* aStream);
   /**
    * Queue audio (mix of stream audio and silence for blocked intervals)
    * to the audio output stream. Returns the number of frames played.
    */
   StreamTime PlayAudio(MediaStream* aStream);
-  /**
-   * No more data will be forthcoming for aStream. The stream will end
-   * at the current buffer end point. The StreamTracks's tracks must be
-   * explicitly set to finished by the caller.
-   */
-  void OpenAudioInputImpl(int aID,
-                          AudioDataListener *aListener);
-  virtual nsresult OpenAudioInput(int aID,
-                                  AudioDataListener *aListener) override;
-  void CloseAudioInputImpl(AudioDataListener *aListener);
-  virtual void CloseAudioInput(AudioDataListener *aListener) override;
+  /* Runs off a message on the graph thread when something requests audio from
+   * an input audio device of ID aID, and delivers the input audio frames to
+   * aListener. */
+  void OpenAudioInputImpl(CubebUtils::AudioDeviceID aID,
+                          AudioDataListener* aListener);
+  /* Called on the main thread when something requests audio from an input
+   * audio device aID. */
+  virtual nsresult OpenAudioInput(CubebUtils::AudioDeviceID aID,
+                                  AudioDataListener* aListener) override;
+  /* Runs off a message on the graph when input audio from aID is not needed
+   * anymore, for a particular stream. It can be that other streams still need
+   * audio from this audio input device. */
+  void CloseAudioInputImpl(Maybe<CubebUtils::AudioDeviceID>& aID,
+                           AudioDataListener* aListener);
+  /* Called on the main thread when input audio from aID is not needed
+   * anymore, for a particular stream. It can be that other streams still need
+   * audio from this audio input device. */
+  virtual void CloseAudioInput(Maybe<CubebUtils::AudioDeviceID>& aID,
+                               AudioDataListener* aListener) override;
+  /* Called on the graph thread when the input device settings should be
+   * reevaluated, for example, if the channel count of the input stream should
+   * be changed. */
+  void ReevaluateInputDevice();
+  /* Called on the graph thread when there is new output data for listeners.
+   * This is the mixed audio output of this MediaStreamGraph. */
+  void NotifyOutputData(AudioDataValue* aBuffer, size_t aFrames,
+                        TrackRate aRate, uint32_t aChannels);
+  /* Called on the graph thread when there is new input data for listeners. This
+   * is the raw audio input for this MediaStreamGraph. */
+  void NotifyInputData(const AudioDataValue* aBuffer, size_t aFrames,
+                       TrackRate aRate, uint32_t aChannels);
+  /* Called every time there are changes to input/output audio devices like
+   * plug/unplug etc. This can be called on any thread, and posts a message to
+   * the main thread so that it can post a message to the graph thread. */
+  void DeviceChanged();
+  /* Called every time there are changes to input/output audio devices. This is
+   * called on the graph thread. */
+  void DeviceChangedImpl();
 
   /**
    * Compute how much stream data we would like to buffer for aStream.
    */
   StreamTime GetDesiredBufferEnd(MediaStream* aStream);
   /**
    * Returns true when there are no active streams.
    */
@@ -422,21 +449,56 @@ public:
    * Mark the media stream order as dirty.
    */
   void SetStreamOrderDirty()
   {
     MOZ_ASSERT(OnGraphThreadOrNotRunning());
     mStreamOrderDirty = true;
   }
 
-  uint32_t AudioChannelCount() const
+  uint32_t AudioOutputChannelCount() const
   {
     return mOutputChannels;
   }
 
+  /**
+   * The audio input channel count for a MediaStreamGraph is the max of all the
+   * channel counts requested by the listeners. The max channel count is
+   * delivered to the listeners themselves, and they take care of downmixing.
+   */
+  uint32_t AudioInputChannelCount()
+  {
+    MOZ_ASSERT(OnGraphThreadOrNotRunning());
+
+    if (!mInputDeviceID) {
+#ifndef ANDROID
+      MOZ_ASSERT(mInputDeviceUsers.Count() == 0,
+        "If running on a platform other than android,"
+        "an explicit device id should be present");
+#endif
+      return 0;
+    }
+    uint32_t maxInputChannels = 0;
+    // When/if we decide to support multiple input device per graph, this needs
+    // loop over them.
+    nsTArray<RefPtr<AudioDataListener>>* listeners =
+      mInputDeviceUsers.GetValue(mInputDeviceID);
+    MOZ_ASSERT(listeners);
+    for (const auto& listener : *listeners) {
+      maxInputChannels =
+        std::max(maxInputChannels, listener->RequestedInputChannelCount(this));
+    }
+    return maxInputChannels;
+  }
+
+  CubebUtils::AudioDeviceID InputDeviceID()
+  {
+    return mInputDeviceID;
+  }
+
   double MediaTimeToSeconds(GraphTime aTime) const
   {
     NS_ASSERTION(aTime > -STREAM_TIME_MAX && aTime <= STREAM_TIME_MAX,
                  "Bad time");
     return static_cast<double>(aTime)/GraphRate();
   }
 
   GraphTime SecondsToMediaTime(double aS) const
@@ -621,26 +683,32 @@ public:
    */
   TimeStamp mLastMainThreadUpdate;
   /**
    * Number of active MediaInputPorts
    */
   int32_t mPortCount;
 
   /**
-   * Devices to use for cubeb input & output, or NULL for no input (void*),
-   * and boolean to control if we want input/output
+   * Devices to use for cubeb input & output, or nullptr for default device.
+   * A MediaStreamGraph always has an output (even if silent).
+   * If `mInputDeviceUsers.Count() != 0`, this MediaStreamGraph wants audio
+   * input.
+   *
+   * In any case, the number of channels to use can be queried (on the graph
+   * thread) by AudioInputChannelCount() and AudioOutputChannelCount().
    */
-  bool mInputWanted;
-  int mInputDeviceID;
-  bool mOutputWanted;
-  int mOutputDeviceID;
-  // Maps AudioDataListeners to a usecount of streams using the listener
-  // so we can know when it's no longer in use.
-  nsDataHashtable<nsPtrHashKey<AudioDataListener>, uint32_t> mInputDeviceUsers;
+  CubebUtils::AudioDeviceID mInputDeviceID;
+  CubebUtils::AudioDeviceID mOutputDeviceID;
+  // Maps AudioDeviceID to an array of their users (that are listeners). This is
+  // used to deliver audio input frames and to notify the listeners that the
+  // audio device that delivers the audio frames has changed.
+  // This is only touched on the graph thread.
+  nsDataHashtable<nsVoidPtrHashKey,
+                  nsTArray<RefPtr<AudioDataListener>>> mInputDeviceUsers;
 
   // True if the graph needs another iteration after the current iteration.
   Atomic<bool> mNeedAnotherIteration;
   // GraphDriver may need a WakeUp() if something changes
   Atomic<bool> mGraphDriverAsleep;
 
   // mMonitor guards the data below.
   // MediaStreamGraph normally does its work without holding mMonitor, so it is
new file mode 100644
--- /dev/null
+++ b/dom/media/gtest/TestAudioDeviceEnumerator.cpp
@@ -0,0 +1,574 @@
+/* -*- Mode: C++; 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/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Attributes.h"
+#include "nsTArray.h"
+#define ENABLE_SET_CUBEB_BACKEND 1
+#include "CubebUtils.h"
+#include "MediaEngineWebRTC.h"
+
+using namespace mozilla;
+
+const bool DEBUG_PRINTS = false;
+
+// Keep those and the struct definition in sync with cubeb.h and
+// cubeb-internal.h
+void
+cubeb_mock_destroy(cubeb* context);
+static int
+cubeb_mock_enumerate_devices(cubeb* context,
+                             cubeb_device_type type,
+                             cubeb_device_collection* out);
+
+static int
+cubeb_mock_device_collection_destroy(cubeb* context,
+                                     cubeb_device_collection* collection);
+
+static int
+cubeb_mock_register_device_collection_changed(
+  cubeb* context,
+  cubeb_device_type devtype,
+  cubeb_device_collection_changed_callback callback,
+  void* user_ptr);
+
+struct cubeb_ops
+{
+  int (*init)(cubeb** context, char const* context_name);
+  char const* (*get_backend_id)(cubeb* context);
+  int (*get_max_channel_count)(cubeb* context, uint32_t* max_channels);
+  int (*get_min_latency)(cubeb* context,
+                         cubeb_stream_params params,
+                         uint32_t* latency_ms);
+  int (*get_preferred_sample_rate)(cubeb* context, uint32_t* rate);
+  int (*enumerate_devices)(cubeb* context,
+                           cubeb_device_type type,
+                           cubeb_device_collection* collection);
+  int (*device_collection_destroy)(cubeb* context,
+                                   cubeb_device_collection* collection);
+  void (*destroy)(cubeb* context);
+  int (*stream_init)(cubeb* context,
+                     cubeb_stream** stream,
+                     char const* stream_name,
+                     cubeb_devid input_device,
+                     cubeb_stream_params* input_stream_params,
+                     cubeb_devid output_device,
+                     cubeb_stream_params* output_stream_params,
+                     unsigned int latency,
+                     cubeb_data_callback data_callback,
+                     cubeb_state_callback state_callback,
+                     void* user_ptr);
+  void (*stream_destroy)(cubeb_stream* stream);
+  int (*stream_start)(cubeb_stream* stream);
+  int (*stream_stop)(cubeb_stream* stream);
+  int (*stream_reset_default_device)(cubeb_stream* stream);
+  int (*stream_get_position)(cubeb_stream* stream, uint64_t* position);
+  int (*stream_get_latency)(cubeb_stream* stream, uint32_t* latency);
+  int (*stream_set_volume)(cubeb_stream* stream, float volumes);
+  int (*stream_set_panning)(cubeb_stream* stream, float panning);
+  int (*stream_get_current_device)(cubeb_stream* stream,
+                                   cubeb_device** const device);
+  int (*stream_device_destroy)(cubeb_stream* stream, cubeb_device* device);
+  int (*stream_register_device_changed_callback)(
+    cubeb_stream* stream,
+    cubeb_device_changed_callback device_changed_callback);
+  int (*register_device_collection_changed)(
+    cubeb* context,
+    cubeb_device_type devtype,
+    cubeb_device_collection_changed_callback callback,
+    void* user_ptr);
+};
+
+// Mock cubeb impl, only supports device enumeration for now.
+cubeb_ops const mock_ops = {
+  /*.init =*/NULL,
+  /*.get_backend_id =*/NULL,
+  /*.get_max_channel_count =*/NULL,
+  /*.get_min_latency =*/NULL,
+  /*.get_preferred_sample_rate =*/NULL,
+  /*.enumerate_devices =*/cubeb_mock_enumerate_devices,
+  /*.device_collection_destroy =*/cubeb_mock_device_collection_destroy,
+  /*.destroy =*/cubeb_mock_destroy,
+  /*.stream_init =*/NULL,
+  /*.stream_destroy =*/NULL,
+  /*.stream_start =*/NULL,
+  /*.stream_stop =*/NULL,
+  /*.stream_reset_default_device =*/NULL,
+  /*.stream_get_position =*/NULL,
+  /*.stream_get_latency =*/NULL,
+  /*.stream_set_volume =*/NULL,
+  /*.stream_set_panning =*/NULL,
+  /*.stream_get_current_device =*/NULL,
+  /*.stream_device_destroy =*/NULL,
+  /*.stream_register_device_changed_callback =*/NULL,
+  /*.register_device_collection_changed =*/
+  cubeb_mock_register_device_collection_changed
+};
+
+// This class has two facets: it is both a fake cubeb backend that is intended
+// to be used for testing, and passed to Gecko code that expects a normal
+// backend, but is also controllable by the test code to decide what the backend
+// should do, depending on what is being tested.
+class MockCubeb
+{
+public:
+  MockCubeb()
+    : ops(&mock_ops)
+    , mDeviceCollectionChangeCallback(nullptr)
+    , mDeviceCollectionChangeType(CUBEB_DEVICE_TYPE_UNKNOWN)
+    , mDeviceCollectionChangeUserPtr(nullptr)
+    , mSupportsDeviceCollectionChangedCallback(true)
+  {
+  }
+  // Cubeb backend implementation
+  // This allows passing this class as a cubeb* instance.
+  cubeb* AsCubebContext() { return reinterpret_cast<cubeb*>(this); }
+  // Fill in the collection parameter with all devices of aType.
+  int EnumerateDevices(cubeb_device_type aType,
+                       cubeb_device_collection* collection)
+  {
+#ifdef ANDROID
+    EXPECT_TRUE(false) << "This is not to be called on Android.";
+#endif
+    size_t count = 0;
+    if (aType & CUBEB_DEVICE_TYPE_INPUT) {
+      count += mInputDevices.Length();
+    }
+    if (aType & CUBEB_DEVICE_TYPE_OUTPUT) {
+      count += mOutputDevices.Length();
+    }
+    collection->device = new cubeb_device_info[count];
+    collection->count = count;
+
+    uint32_t collection_index = 0;
+    if (aType & CUBEB_DEVICE_TYPE_INPUT) {
+      for (auto& device : mInputDevices) {
+        collection->device[collection_index] = device;
+        collection_index++;
+      }
+    }
+    if (aType & CUBEB_DEVICE_TYPE_OUTPUT) {
+      for (auto& device : mOutputDevices) {
+        collection->device[collection_index] = device;
+        collection_index++;
+      }
+    }
+
+    return CUBEB_OK;
+  }
+
+  // For a given device type, add a callback, called with a user pointer, when
+  // the device collection for this backend changes (i.e. a device has been
+  // removed or added).
+  int RegisterDeviceCollectionChangeCallback(
+    cubeb_device_type aDevType,
+    cubeb_device_collection_changed_callback aCallback,
+    void* aUserPtr)
+  {
+    if (!mSupportsDeviceCollectionChangedCallback) {
+      return CUBEB_ERROR;
+    }
+
+    mDeviceCollectionChangeType = aDevType;
+    mDeviceCollectionChangeCallback = aCallback;
+    mDeviceCollectionChangeUserPtr = aUserPtr;
+
+    return CUBEB_OK;
+  }
+
+  // Control API
+
+  // Add an input or output device to this backend. This calls the device
+  // collection invalidation callback if needed.
+  void AddDevice(cubeb_device_info aDevice)
+  {
+    bool needToCall = false;
+
+    if (aDevice.type == CUBEB_DEVICE_TYPE_INPUT) {
+      mInputDevices.AppendElement(aDevice);
+    } else if (aDevice.type == CUBEB_DEVICE_TYPE_OUTPUT) {
+      mOutputDevices.AppendElement(aDevice);
+    } else {
+      MOZ_CRASH("bad device type when adding a device in mock cubeb backend");
+    }
+
+    bool isInput = aDevice.type & CUBEB_DEVICE_TYPE_INPUT;
+
+    needToCall |=
+      isInput && mDeviceCollectionChangeType & CUBEB_DEVICE_TYPE_INPUT;
+    needToCall |=
+      !isInput && mDeviceCollectionChangeType & CUBEB_DEVICE_TYPE_OUTPUT;
+
+    if (needToCall && mDeviceCollectionChangeCallback) {
+      mDeviceCollectionChangeCallback(AsCubebContext(),
+                                      mDeviceCollectionChangeUserPtr);
+    }
+  }
+  // Remove a specific input or output device to this backend, returns true if
+  // a device was removed. This calls the device collection invalidation
+  // callback if needed.
+  bool RemoveDevice(cubeb_devid aId)
+  {
+    bool foundInput = false;
+    bool foundOutput = false;
+    mInputDevices.RemoveElementsBy(
+      [aId, &foundInput](cubeb_device_info& aDeviceInfo) {
+        bool foundThisTime = aDeviceInfo.devid == aId;
+        foundInput |= foundThisTime;
+        return foundThisTime;
+      });
+    mOutputDevices.RemoveElementsBy(
+      [aId, &foundOutput](cubeb_device_info& aDeviceInfo) {
+        bool foundThisTime = aDeviceInfo.devid == aId;
+        foundOutput |= foundThisTime;
+        return foundThisTime;
+      });
+
+    bool needToCall = false;
+
+    needToCall |=
+      foundInput && mDeviceCollectionChangeType & CUBEB_DEVICE_TYPE_INPUT;
+    needToCall |=
+      foundOutput && mDeviceCollectionChangeType & CUBEB_DEVICE_TYPE_OUTPUT;
+
+    if (needToCall && mDeviceCollectionChangeCallback) {
+      mDeviceCollectionChangeCallback(AsCubebContext(),
+                                      mDeviceCollectionChangeUserPtr);
+    }
+
+    // If the device removed was a default device, set another device as the
+    // default, if there are still devices available.
+    bool foundDefault = false;
+    for (uint32_t i = 0; i < mInputDevices.Length(); i++) {
+      foundDefault |= mInputDevices[i].preferred != CUBEB_DEVICE_PREF_NONE;
+    }
+
+    if (!foundDefault) {
+      if (!mInputDevices.IsEmpty()) {
+        mInputDevices[mInputDevices.Length() - 1].preferred =
+          CUBEB_DEVICE_PREF_ALL;
+      }
+    }
+
+    foundDefault = false;
+    for (uint32_t i = 0; i < mOutputDevices.Length(); i++) {
+      foundDefault |= mOutputDevices[i].preferred != CUBEB_DEVICE_PREF_NONE;
+    }
+
+    if (!foundDefault) {
+      if (!mOutputDevices.IsEmpty()) {
+        mOutputDevices[mOutputDevices.Length() - 1].preferred =
+          CUBEB_DEVICE_PREF_ALL;
+      }
+    }
+
+    return foundInput | foundOutput;
+  }
+  // Remove all input or output devices from this backend, without calling the
+  // callback. This is meant to clean up in between tests.
+  void ClearDevices(cubeb_device_type aType)
+  {
+    mInputDevices.Clear();
+    mOutputDevices.Clear();
+  }
+
+  // This allows simulating a backend that does not support setting a device
+  // collection invalidation callback, to be able to test the fallback path.
+  void SetSupportDeviceChangeCallback(bool aSupports)
+  {
+    mSupportsDeviceCollectionChangedCallback = aSupports;
+  }
+
+private:
+  // This needs to have the exact same memory layout as a real cubeb backend.
+  // It's very important for this `ops` member to be the very first member of
+  // the class, and to not have any virtual members (to avoid having a vtable).
+  const cubeb_ops* ops;
+  // The callback to call when the device list has been changed.
+  cubeb_device_collection_changed_callback mDeviceCollectionChangeCallback;
+  // For which device type to call the callback.
+  cubeb_device_type mDeviceCollectionChangeType;
+  // The pointer to pass in the callback.
+  void* mDeviceCollectionChangeUserPtr;
+  // Whether or not this backed supports device collection change notification
+  // via a system callback. If not, Gecko is expected to re-query the list every
+  // time.
+  bool mSupportsDeviceCollectionChangedCallback;
+  // Our input and output devices.
+  nsTArray<cubeb_device_info> mInputDevices;
+  nsTArray<cubeb_device_info> mOutputDevices;
+};
+
+void
+cubeb_mock_destroy(cubeb* context)
+{
+  delete reinterpret_cast<MockCubeb*>(context);
+}
+
+static int
+cubeb_mock_enumerate_devices(cubeb* context,
+                             cubeb_device_type type,
+                             cubeb_device_collection* out)
+{
+  MockCubeb* mock = reinterpret_cast<MockCubeb*>(context);
+  return mock->EnumerateDevices(type, out);
+}
+
+int
+cubeb_mock_device_collection_destroy(cubeb* context,
+                                     cubeb_device_collection* collection)
+{
+  delete[] collection->device;
+  return CUBEB_OK;
+}
+
+int
+cubeb_mock_register_device_collection_changed(
+  cubeb* context,
+  cubeb_device_type devtype,
+  cubeb_device_collection_changed_callback callback,
+  void* user_ptr)
+{
+  MockCubeb* mock = reinterpret_cast<MockCubeb*>(context);
+  return mock->RegisterDeviceCollectionChangeCallback(
+    devtype, callback, user_ptr);
+  return CUBEB_OK;
+}
+
+void
+PrintDevice(cubeb_device_info aInfo)
+{
+  printf("id: %zu\n"
+         "device_id: %s\n"
+         "friendly_name: %s\n"
+         "group_id: %s\n"
+         "vendor_name: %s\n"
+         "type: %d\n"
+         "state: %d\n"
+         "preferred: %d\n"
+         "format: %d\n"
+         "default_format: %d\n"
+         "max_channels: %d\n"
+         "default_rate: %d\n"
+         "max_rate: %d\n"
+         "min_rate: %d\n"
+         "latency_lo: %d\n"
+         "latency_hi: %d\n",
+         reinterpret_cast<uintptr_t>(aInfo.devid),
+         aInfo.device_id,
+         aInfo.friendly_name,
+         aInfo.group_id,
+         aInfo.vendor_name,
+         aInfo.type,
+         aInfo.state,
+         aInfo.preferred,
+         aInfo.format,
+         aInfo.default_format,
+         aInfo.max_channels,
+         aInfo.default_rate,
+         aInfo.max_rate,
+         aInfo.min_rate,
+         aInfo.latency_lo,
+         aInfo.latency_hi);
+}
+
+void
+PrintDevice(AudioDeviceInfo* aInfo)
+{
+  cubeb_devid id;
+  nsString name;
+  nsString groupid;
+  nsString vendor;
+  uint16_t type;
+  uint16_t state;
+  uint16_t preferred;
+  uint16_t supportedFormat;
+  uint16_t defaultFormat;
+  uint32_t maxChannels;
+  uint32_t defaultRate;
+  uint32_t maxRate;
+  uint32_t minRate;
+  uint32_t maxLatency;
+  uint32_t minLatency;
+
+  id = aInfo->DeviceID();
+  aInfo->GetName(name);
+  aInfo->GetGroupId(groupid);
+  aInfo->GetVendor(vendor);
+  aInfo->GetType(&type);
+  aInfo->GetState(&state);
+  aInfo->GetPreferred(&preferred);
+  aInfo->GetSupportedFormat(&supportedFormat);
+  aInfo->GetDefaultFormat(&defaultFormat);
+  aInfo->GetMaxChannels(&maxChannels);
+  aInfo->GetDefaultRate(&defaultRate);
+  aInfo->GetMaxRate(&maxRate);
+  aInfo->GetMinRate(&minRate);
+  aInfo->GetMinLatency(&minLatency);
+  aInfo->GetMaxLatency(&maxLatency);
+
+  printf("device id: %zu\n"
+         "friendly_name: %s\n"
+         "group_id: %s\n"
+         "vendor_name: %s\n"
+         "type: %d\n"
+         "state: %d\n"
+         "preferred: %d\n"
+         "format: %d\n"
+         "default_format: %d\n"
+         "max_channels: %d\n"
+         "default_rate: %d\n"
+         "max_rate: %d\n"
+         "min_rate: %d\n"
+         "latency_lo: %d\n"
+         "latency_hi: %d\n",
+         reinterpret_cast<uintptr_t>(id),
+         NS_LossyConvertUTF16toASCII(name).get(),
+         NS_LossyConvertUTF16toASCII(groupid).get(),
+         NS_LossyConvertUTF16toASCII(vendor).get(),
+         type,
+         state,
+         preferred,
+         supportedFormat,
+         defaultFormat,
+         maxChannels,
+         defaultRate,
+         maxRate,
+         minRate,
+         minLatency,
+         maxLatency);
+}
+
+cubeb_device_info
+InputDeviceTemplate(cubeb_devid aId)
+{
+  // A fake input device
+  cubeb_device_info device;
+  device.devid = aId;
+  device.device_id = "nice name";
+  device.friendly_name = "an even nicer name";
+  device.group_id = "the physical device";
+  device.vendor_name = "mozilla";
+  device.type = CUBEB_DEVICE_TYPE_INPUT;
+  device.state = CUBEB_DEVICE_STATE_ENABLED;
+  device.preferred = CUBEB_DEVICE_PREF_NONE;
+  device.format = CUBEB_DEVICE_FMT_F32NE;
+  device.default_format = CUBEB_DEVICE_FMT_F32NE;
+  device.max_channels = 2;
+  device.default_rate = 44100;
+  device.max_rate = 44100;
+  device.min_rate = 16000;
+  device.latency_lo = 256;
+  device.latency_hi = 1024;
+
+  return device;
+}
+
+enum DeviceOperation
+{
+  ADD,
+  REMOVE
+};
+
+void
+TestEnumeration(MockCubeb* aMock,
+                uint32_t aExpectedDeviceCount,
+                DeviceOperation aOperation)
+{
+  CubebDeviceEnumerator enumerator;
+
+  nsTArray<RefPtr<AudioDeviceInfo>> inputDevices;
+
+  enumerator.EnumerateAudioInputDevices(inputDevices);
+
+  EXPECT_EQ(inputDevices.Length(), aExpectedDeviceCount)
+    << "Device count is correct when enumerating";
+
+  if (DEBUG_PRINTS) {
+    for (uint32_t i = 0; i < inputDevices.Length(); i++) {
+      printf("=== Before removal\n");
+      PrintDevice(inputDevices[i]);
+    }
+  }
+
+  if (aOperation == DeviceOperation::REMOVE) {
+    aMock->RemoveDevice(reinterpret_cast<cubeb_devid>(1));
+  } else {
+    aMock->AddDevice(InputDeviceTemplate(reinterpret_cast<cubeb_devid>(123)));
+  }
+
+  enumerator.EnumerateAudioInputDevices(inputDevices);
+
+  uint32_t newExpectedDeviceCount = aOperation == DeviceOperation::REMOVE
+                                      ? aExpectedDeviceCount - 1
+                                      : aExpectedDeviceCount + 1;
+
+  EXPECT_EQ(inputDevices.Length(), newExpectedDeviceCount)
+    << "Device count is correct when enumerating after operation";
+
+  if (DEBUG_PRINTS) {
+    for (uint32_t i = 0; i < inputDevices.Length(); i++) {
+      printf("=== After removal\n");
+      PrintDevice(inputDevices[i]);
+    }
+  }
+}
+
+#ifndef ANDROID
+TEST(CubebDeviceEnumerator, EnumerateSimple)
+{
+  // It looks like we're leaking this object, but in fact it will be freed by
+  // gecko sometime later: `cubeb_destroy` is called when layout statics are
+  // shutdown and we cast back to a MockCubeb* and call the dtor.
+  MockCubeb* mock = new MockCubeb();
+  mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext());
+
+  // We want to test whether CubebDeviceEnumerator works with and without a
+  // backend that can notify of a device collection change via callback.
+  // Additionally, we're testing that both adding and removing a device
+  // invalidates the list correctly.
+  bool supportsDeviceChangeCallback[2] = { true, false };
+  DeviceOperation operations[2] = { DeviceOperation::ADD,
+                                    DeviceOperation::REMOVE };
+
+  for (DeviceOperation op : operations) {
+    for (bool supports : supportsDeviceChangeCallback) {
+      mock->ClearDevices(CUBEB_DEVICE_TYPE_INPUT);
+      // Add a few input devices (almost all the same but it does not really
+      // matter as long as they have distinct IDs and only one is the default
+      // devices)
+      uint32_t device_count = 4;
+      for (uintptr_t i = 0; i < device_count; i++) {
+        cubeb_device_info device =
+          InputDeviceTemplate(reinterpret_cast<void*>(i + 1));
+        // Make it so that the last device is the default input device.
+        if (i == device_count - 1) {
+          device.preferred = CUBEB_DEVICE_PREF_ALL;
+        }
+        mock->AddDevice(device);
+      }
+
+      mock->SetSupportDeviceChangeCallback(supports);
+      TestEnumeration(mock, device_count, op);
+    }
+  }
+}
+#else // building for Android, which has no device enumeration support
+TEST(CubebDeviceEnumerator, EnumerateAndroid)
+{
+  MockCubeb* mock = new MockCubeb();
+  mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext());
+
+  CubebDeviceEnumerator enumerator;
+
+  nsTArray<RefPtr<AudioDeviceInfo>> inputDevices;
+  enumerator.EnumerateAudioInputDevices(inputDevices);
+  EXPECT_EQ(inputDevices.Length(), 1u) <<  "Android always exposes a single input device.";
+  EXPECT_EQ(inputDevices[0]->MaxChannels(), 1u) << "With a single channel.";
+  EXPECT_EQ(inputDevices[0]->DeviceID(), nullptr) << "It's always the default device.";
+  EXPECT_TRUE(inputDevices[0]->Preferred()) << "it's always the prefered device.";
+}
+#endif
--- a/dom/media/gtest/moz.build
+++ b/dom/media/gtest/moz.build
@@ -1,18 +1,27 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
+include('/media/webrtc/webrtc.mozbuild')
+
+
+LOCAL_INCLUDES += [
+    '/media/webrtc/signaling/src/common',
+    '/media/webrtc/trunk'
+]
+
 UNIFIED_SOURCES += [
     'MockMediaResource.cpp',
     'TestAudioBuffers.cpp',
     'TestAudioCompactor.cpp',
+    'TestAudioDeviceEnumerator.cpp',
     'TestAudioMixer.cpp',
     'TestAudioPacketizer.cpp',
     'TestAudioSegment.cpp',
     'TestAudioTrackEncoder.cpp',
     'TestBitWriter.cpp',
     'TestBlankVideoDataCreator.cpp',
     'TestCDMStorage.cpp',
     'TestDataMutex.cpp',
@@ -73,9 +82,9 @@ LOCAL_INCLUDES += [
     '/dom/media/platforms/agnostic',
     '/security/certverifier',
     '/security/pkix/include',
 ]
 
 FINAL_LIBRARY = 'xul-gtest'
 
 if CONFIG['CC_TYPE'] in ('clang', 'gcc'):
-    CXXFLAGS += ['-Wno-error=shadow']
+    CXXFLAGS += ['-Wno-error=shadow', '-Wno-unused-private-field']
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -49,16 +49,20 @@ skip-if = os == 'android' || verify
 [test_getUserMedia_audioCapture.html]
 skip-if = toolkit == 'android' # android(Bug 1189784, timeouts on 4.3 emulator), android(Bug 1264333)
 [test_getUserMedia_addTrackRemoveTrack.html]
 skip-if = android_version == '18' || os == 'linux' # android(Bug 1189784, timeouts on 4.3 emulator), linux bug 1377450
 [test_getUserMedia_addtrack_removetrack_events.html]
 skip-if = os == 'linux' && debug # Bug 1389983
 [test_getUserMedia_audioConstraints.html]
 skip-if = os == 'mac' || os == 'win' || toolkit == 'android' # Bug 1404995, no loopback devices on some platforms
+[test_getUserMedia_audioConstraints_concurrentIframes.html]
+skip-if = os == 'mac' || os == 'win' || toolkit == 'android' # Bug 1404995, no loopback devices on some platforms
+[test_getUserMedia_audioConstraints_concurrentStreams.html]
+skip-if = os == 'mac' || os == 'win' || toolkit == 'android' # Bug 1404995, no loopback devices on some platforms
 [test_getUserMedia_basicAudio_loopback.html]
 skip-if = os == 'mac' || os == 'win' || toolkit == 'android' # Bug 1404995, no loopback devices on some platforms
 [test_getUserMedia_basicAudio.html]
 [test_getUserMedia_basicVideo.html]
 [test_getUserMedia_basicVideo_playAfterLoadedmetadata.html]
 [test_getUserMedia_basicScreenshare.html]
 skip-if = toolkit == 'android' # no screenshare on android
 [test_getUserMedia_basicTabshare.html]
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_audioConstraints_concurrentIframes.html
@@ -0,0 +1,153 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+  title: "getUserMedia in multiple iframes with different constraints",
+  bug: "1404977"
+});
+/**
+  * Verify that we can successfully call getUserMedia for the same device in
+  * multiple iframes concurrently. This is checked by creating a number of
+  * iframes and performing a separate getUserMedia call in each. We verify the
+  * stream returned by that call has the same constraints as requested both
+  * immediately after the call and after all gUM calls have been made. The test
+  * then verifies the streams can be played.
+  */
+runTest(async function() {
+  // Compare constraints and return a string with the differences in
+  // echoCancellation, autoGainControl, and noiseSuppression. The string
+  // will be empty if there are no differences.
+  function getConstraintDifferenceString(constraints, otherConstraints) {
+    let diffString = "";
+    if (constraints.echoCancellation != otherConstraints.echoCancellation) {
+      diffString += "echoCancellation different: " +
+                    `${constraints.echoCancellation} != ${otherConstraints.echoCancellation}, `;
+    }
+    if (constraints.autoGainControl != otherConstraints.autoGainControl) {
+      diffString += "autoGainControl different: " +
+                    `${constraints.autoGainControl} != ${otherConstraints.autoGainControl}, `;
+    }
+    if (constraints.noiseSuppression != otherConstraints.noiseSuppression) {
+      diffString += "noiseSuppression different: " +
+                    `${constraints.noiseSuppression} != ${otherConstraints.noiseSuppression}, `;
+    }
+    // Replace trailing comma and space if any
+    return diffString.replace(/, $/, "");
+  }
+
+  // We need a real device to get a MediaEngine supporting constraints
+  let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+  if (!audioDevice) {
+    todo(false, "No device set by framework. Try --use-test-media-devices");
+    return;
+  }
+
+  let egn = (e, g, n) => ({
+    echoCancellation: e,
+    autoGainControl: g,
+    noiseSuppression: n
+  });
+
+  let allConstraintCombinations = [
+    egn(false, false, false),
+    egn(true,  false, false),
+    egn(false, true,  false),
+    egn(false, false, true),
+    egn(true,  true,  false),
+    egn(true,  false, true),
+    egn(false, true,  true),
+    egn(true,  true,  true),
+  ];
+
+  // TODO: We would like to be able to perform an arbitrary number of gUM calls
+  // at once, but issues with pulse and audio IPC mean on some systems we're
+  // limited to as few as 2 concurrent calls. To avoid issues we chunk test runs
+  // to only two calls at a time. The while and the splice lines can be removed,
+  // and allConstraintCombinations can be renamed to constraintCombinations once
+  // this issue is resolved. See bug 1480489
+  while (allConstraintCombinations.length) {
+    let constraintCombinations = allConstraintCombinations.splice(0, 2);
+    // Array to store objects that associate information used in our test such as
+    // constraints, iframes, gum streams, and various promises.
+    let testCases = [];
+
+    for (let constraints of constraintCombinations) {
+      let testCase = {requestedConstraints: constraints};
+      // Provide an id for logging, labeling related elements.
+      testCase.id = `testCase.` +
+                    `e=${constraints.echoCancellation}.` +
+                    `g=${constraints.noiseSuppression}.` +
+                    `n=${constraints.noiseSuppression}`;
+      testCases.push(testCase);
+      testCase.iframe = document.createElement("iframe");
+      testCase.iframeLoadedPromise = new Promise((resolve, reject) => {
+        testCase.iframe.onload = () => { resolve(); };
+      });
+      document.body.appendChild(testCase.iframe);
+    }
+    is(testCases.length,
+      constraintCombinations.length,
+      "Should have created a testcase for each constraint");
+
+    // Wait for all iframes to be loaded
+    await Promise.all(testCases.map(tc => tc.iframeLoadedPromise));
+
+    // Start a tone at our top level page so the gUM calls will record something
+    // should we wish to verify their recording in future.
+    let tone = new LoopbackTone(new AudioContext, TEST_AUDIO_FREQ);
+    tone.start();
+
+    // One by one see if we can grab a gUM stream per iframe
+    for (let testCase of testCases) {
+      // Use normal gUM rather than our test helper as the test harness was
+      // not made to be used inside iframes.
+      testCase.gumStream =
+        await testCase.iframe.contentWindow.navigator.mediaDevices.getUserMedia({audio: testCase.requestedConstraints})
+        .catch(e => Promise.reject(`getUserMedia calls should not fail! Failed at ${testCase.id} with: ${e}!`));
+      let differenceString = getConstraintDifferenceString(
+        testCase.requestedConstraints,
+        testCase.gumStream.getAudioTracks()[0].getSettings());
+      ok(!differenceString,
+        `gUM stream for ${testCase.id} should have the same constraints as were ` +
+        `requested from gUM. Differences: ${differenceString}`);
+    }
+
+    // Once all streams are collected, make sure the constraints haven't been
+    // mutated by another gUM call.
+    for (let testCase of testCases) {
+      let differenceString = getConstraintDifferenceString(
+        testCase.requestedConstraints,
+        testCase.gumStream.getAudioTracks()[0].getSettings());
+      ok(!differenceString,
+        `gUM stream for ${testCase.id} should not have had constraints altered after ` +
+        `all gUM calls are done. Differences: ${differenceString}`);
+    }
+
+    // We do not currently have tests to verify the behaviour of the different
+    // constraints. Once we do we should do further verification here. See
+    // bug 1406372, bug 1406376, and bug 1406377.
+
+    for (let testCase of testCases) {
+      let testAudio = createMediaElement("audio", `testAudio.${testCase.id}`);
+      let playback = new LocalMediaStreamPlayback(testAudio, testCase.gumStream);
+      await playback.playMediaWithoutStoppingTracks(false);
+    }
+
+    // Stop the tracks for each stream, we left them running above via
+    // playMediaWithoutStoppingTracks to make sure they can play concurrently.
+    for (let testCase of testCases) {
+      testCase.gumStream.getTracks().map(t => t.stop());
+    }
+
+    tone.stop();
+  }
+});
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_audioConstraints_concurrentStreams.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+  title: "getUserMedia multiple times, concurrently, and with different constraints",
+  bug: "1404977"
+});
+/**
+  * Verify that we can successfully call getUserMedia multiple times for the
+  * same device, concurrently. This is checked by calling getUserMedia a number
+  * of times with different constraints. We verify that the stream returned by
+  * that call has the same constraints as requested both immediately after the
+  * call and after all gUM calls have been made. The test then verifies the
+  * streams can be played.
+  */
+runTest(async function() {
+  // Compare constraints and return a string with the differences in
+  // echoCancellation, autoGainControl, and noiseSuppression. The string
+  // will be empty if there are no differences.
+  function getConstraintDifferenceString(constraints, otherConstraints) {
+    let diffString = "";
+    if (constraints.echoCancellation != otherConstraints.echoCancellation) {
+      diffString += "echoCancellation different: " +
+                    `${constraints.echoCancellation} != ${otherConstraints.echoCancellation}, `;
+    }
+    if (constraints.autoGainControl != otherConstraints.autoGainControl) {
+      diffString += "autoGainControl different: " +
+                    `${constraints.autoGainControl} != ${otherConstraints.autoGainControl}, `;
+    }
+    if (constraints.noiseSuppression != otherConstraints.noiseSuppression) {
+      diffString += "noiseSuppression different: " +
+                    `${constraints.noiseSuppression} != ${otherConstraints.noiseSuppression}, `;
+    }
+    // Replace trailing comma and space if any
+    return diffString.replace(/, $/, "");
+  }
+
+  // We need a real device to get a MediaEngine supporting constraints
+  let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+  if (!audioDevice) {
+    todo(false, "No device set by framework. Try --use-test-media-devices");
+    return;
+  }
+
+  let egn = (e, g, n) => ({
+    echoCancellation: e,
+    autoGainControl: g,
+    noiseSuppression: n
+  });
+
+  let constraintCombinations = [
+    egn(false, false, false),
+    egn(true,  false, false),
+    egn(false, true,  false),
+    egn(false, false, true),
+    egn(true,  true,  false),
+    egn(true,  false, true),
+    egn(false, true,  true),
+    egn(true,  true,  true),
+  ];
+
+  // Array to store objects that associate information used in our test such as
+  // constraints, gum streams, and various promises.
+  let testCases = [];
+
+  for (let constraints of constraintCombinations) {
+    let testCase = {requestedConstraints: constraints};
+    // Provide an id for logging, labeling related elements.
+    testCase.id = `testCase.` +
+                  `e=${constraints.echoCancellation}.` +
+                  `g=${constraints.noiseSuppression}.` +
+                  `n=${constraints.noiseSuppression}`;
+    testCases.push(testCase);
+    testCase.gumStream =
+      await getUserMedia({audio: testCase.requestedConstraints})
+      .catch(e => Promise.reject(`getUserMedia calls should not fail! Failed at ${testCase.id} with: ${e}!`));
+    let differenceString = getConstraintDifferenceString(
+      testCase.requestedConstraints,
+      testCase.gumStream.getAudioTracks()[0].getSettings());
+      ok(!differenceString,
+        `gUM stream for ${testCase.id} should have the same constraints as were ` +
+        `requested from gUM. Differences: ${differenceString}`);
+  }
+  is(testCases.length,
+    constraintCombinations.length,
+    "Should have a stream for each constraint");
+
+  // Once all streams are collected, make sure the constraints haven't been
+  // mutated by another gUM call.
+  for (let testCase of testCases) {
+    let differenceString = getConstraintDifferenceString(
+      testCase.requestedConstraints,
+      testCase.gumStream.getAudioTracks()[0].getSettings());
+    ok(!differenceString,
+      `gUM stream for ${testCase.id} should not have had constraints altered after ` +
+      `all gUM calls are done. Differences: ${differenceString}`);
+  }
+
+  // We do not currently have tests to verify the behaviour of the different
+  // constraints. Once we do we should do further verificaiton here. See
+  // bug 1406372, bug 1406376, and bug 1406377.
+
+  for (let testCase of testCases) {
+    let testAudio = createMediaElement("audio", `testAudio.${testCase.id}`);
+    let playback = new LocalMediaStreamPlayback(testAudio, testCase.gumStream);
+    await playback.playMediaWithoutStoppingTracks(false);
+  }
+
+  // Stop the tracks for each stream, we left them running above via
+  // playMediaWithoutStoppingTracks to make sure they can play concurrently.
+  for (let testCase of testCases) {
+    testCase.gumStream.getTracks().map(t => t.stop());
+  }
+});
+</script>
+</pre>
+</body>
+</html>
--- a/dom/media/webrtc/MediaEngineWebRTC.cpp
+++ b/dom/media/webrtc/MediaEngineWebRTC.cpp
@@ -21,94 +21,23 @@
 #include "prenv.h"
 
 static mozilla::LazyLogModule sGetUserMediaLog("GetUserMedia");
 #undef LOG
 #define LOG(args) MOZ_LOG(sGetUserMediaLog, mozilla::LogLevel::Debug, args)
 
 namespace mozilla {
 
-// statics from AudioInputCubeb
-nsTArray<int>* AudioInputCubeb::mDeviceIndexes;
-int AudioInputCubeb::mDefaultDevice = -1;
-nsTArray<nsCString>* AudioInputCubeb::mDeviceNames;
-cubeb_device_collection AudioInputCubeb::mDevices = { nullptr, 0 };
-bool AudioInputCubeb::mAnyInUse = false;
-StaticMutex AudioInputCubeb::sMutex;
-uint32_t AudioInputCubeb::sUserChannelCount = 0;
-
-// AudioDeviceID is an annoying opaque value that's really a string
-// pointer, and is freed when the cubeb_device_collection is destroyed
-
-void AudioInputCubeb::UpdateDeviceList()
-{
-  // We keep all the device names, but wipe the mappings and rebuild them.
-  // Do this first so that if cubeb has failed we've unmapped our devices
-  // before we early return. Otherwise we'd keep the old list.
-  for (auto& device_index : (*mDeviceIndexes)) {
-    device_index = -1; // unmapped
-  }
-
-  cubeb* cubebContext = CubebUtils::GetCubebContext();
-  if (!cubebContext) {
-    return;
-  }
-
-  cubeb_device_collection devices = { nullptr, 0 };
-
-  if (CUBEB_OK != cubeb_enumerate_devices(cubebContext,
-                                          CUBEB_DEVICE_TYPE_INPUT,
-                                          &devices)) {
-    return;
-  }
-
-  // Calculate translation from existing mDevices to new devices. Note we
-  // never end up with less devices than before, since people have
-  // stashed indexes.
-  // For some reason the "fake" device for automation is marked as DISABLED,
-  // so white-list it.
-  mDefaultDevice = -1;
-  for (uint32_t i = 0; i < devices.count; i++) {
-    LOG(("Cubeb device %u: type 0x%x, state 0x%x, name %s, id %p",
-         i, devices.device[i].type, devices.device[i].state,
-         devices.device[i].friendly_name, devices.device[i].device_id));
-    if (devices.device[i].type == CUBEB_DEVICE_TYPE_INPUT && // paranoia
-        devices.device[i].state == CUBEB_DEVICE_STATE_ENABLED )
-    {
-      auto j = mDeviceNames->IndexOf(devices.device[i].device_id);
-      if (j != nsTArray<nsCString>::NoIndex) {
-        // match! update the mapping
-        (*mDeviceIndexes)[j] = i;
-      } else {
-        // new device, add to the array
-        mDeviceIndexes->AppendElement(i);
-        mDeviceNames->AppendElement(devices.device[i].device_id);
-        j = mDeviceIndexes->Length()-1;
-      }
-      if (devices.device[i].preferred & CUBEB_DEVICE_PREF_VOICE) {
-        // There can be only one... we hope
-        NS_ASSERTION(mDefaultDevice == -1, "multiple default cubeb input devices!");
-        mDefaultDevice = j;
-      }
-    }
-  }
-  LOG(("Cubeb default input device %d", mDefaultDevice));
-  StaticMutexAutoLock lock(sMutex);
-  // swap state
-  cubeb_device_collection_destroy(cubebContext, &mDevices);
-  mDevices = devices;
-}
+using namespace CubebUtils;
 
 MediaEngineWebRTC::MediaEngineWebRTC(MediaEnginePrefs &aPrefs)
-  : mMutex("MediaEngineWebRTC::mMutex"),
-    mAudioInput(nullptr),
-    mFullDuplex(aPrefs.mFullDuplex),
-    mDelayAgnostic(aPrefs.mDelayAgnostic),
-    mExtendedFilter(aPrefs.mExtendedFilter),
-    mHasTabVideoSource(false)
+  : mMutex("mozilla::MediaEngineWebRTC")
+  , mDelayAgnostic(aPrefs.mDelayAgnostic)
+  , mExtendedFilter(aPrefs.mExtendedFilter)
+  , mHasTabVideoSource(false)
 {
   nsCOMPtr<nsIComponentRegistrar> compMgr;
   NS_GetComponentRegistrar(getter_AddRefs(compMgr));
   if (compMgr) {
     compMgr->IsContractIDRegistered(NS_TABSOURCESERVICE_CONTRACTID, &mHasTabVideoSource);
   }
 
   camera::GetChildAndCall(
@@ -250,102 +179,97 @@ MediaEngineWebRTC::EnumerateVideoDevices
 }
 
 void
 MediaEngineWebRTC::EnumerateMicrophoneDevices(uint64_t aWindowId,
                                               nsTArray<RefPtr<MediaDevice> >* aDevices)
 {
   mMutex.AssertCurrentThreadOwns();
 
-  if (!mAudioInput) {
-    if (!SupportsDuplex()) {
-      return;
-    }
-    mAudioInput = new mozilla::AudioInputCubeb();
+  if (!mEnumerator) {
+    mEnumerator.reset(new CubebDeviceEnumerator());
   }
 
-  int nDevices = 0;
-  mAudioInput->GetNumOfRecordingDevices(nDevices);
-  int i;
-#if defined(MOZ_WIDGET_ANDROID)
-  i = 0; // Bug 1037025 - let the OS handle defaulting for now on android/b2g
-#else
-  // -1 is "default communications device" depending on OS in webrtc.org code
-  i = -1;
+  nsTArray<RefPtr<AudioDeviceInfo>> devices;
+  mEnumerator->EnumerateAudioInputDevices(devices);
+
+  DebugOnly<bool> foundPreferredDevice = false;
+
+  for (uint32_t i = 0; i < devices.Length(); i++) {
+#ifndef ANDROID
+    MOZ_ASSERT(devices[i]->DeviceID());
 #endif
-  for (; i < nDevices; i++) {
-    // We use constants here because GetRecordingDeviceName takes char[128].
-    char deviceName[128];
-    char uniqueId[128];
-    // paranoia; jingle doesn't bother with this
-    deviceName[0] = '\0';
-    uniqueId[0] = '\0';
-
-    int error = mAudioInput->GetRecordingDeviceName(i, deviceName, uniqueId);
-    if (error) {
-      LOG((" AudioInput::GetRecordingDeviceName: Failed %d", error));
-      continue;
-    }
+    LOG(("Cubeb device %u: type 0x%x, state 0x%x, name %s, id %p",
+          i,
+          devices[i]->Type(),
+          devices[i]->State(),
+          NS_ConvertUTF16toUTF8(devices[i]->Name()).get(),
+          devices[i]->DeviceID()));
 
-    if (uniqueId[0] == '\0') {
-      // Mac and Linux don't set uniqueId!
-      strcpy(uniqueId, deviceName); // safe given assert and initialization/error-check
+    if (devices[i]->State() == CUBEB_DEVICE_STATE_ENABLED) {
+      MOZ_ASSERT(devices[i]->Type() == CUBEB_DEVICE_TYPE_INPUT);
+      RefPtr<MediaEngineSource> source =
+        new MediaEngineWebRTCMicrophoneSource(
+            devices[i],
+            devices[i]->Name(),
+            // Lie and provide the name as UUID
+            NS_ConvertUTF16toUTF8(devices[i]->Name()),
+            devices[i]->MaxChannels(),
+            mDelayAgnostic,
+            mExtendedFilter);
+      RefPtr<MediaDevice> device = MakeRefPtr<MediaDevice>(
+                                     source,
+                                     source->GetName(),
+                                     NS_ConvertUTF8toUTF16(source->GetUUID()));
+      if (devices[i]->Preferred()) {
+#ifdef DEBUG
+        if (!foundPreferredDevice) {
+          foundPreferredDevice = true;
+        } else {
+          MOZ_ASSERT(!foundPreferredDevice,
+              "Found more than one preferred audio input device"
+              "while enumerating");
+        }
+#endif
+        aDevices->InsertElementAt(0, device);
+      } else {
+        aDevices->AppendElement(device);
+      }
     }
-
-
-    RefPtr<MediaEngineSource> micSource;
-    NS_ConvertUTF8toUTF16 uuid(uniqueId);
-
-    nsRefPtrHashtable<nsStringHashKey, MediaEngineSource>*
-      devicesForThisWindow = mAudioSources.LookupOrAdd(aWindowId);
-
-    bool alreadySeenThisDeviceBefore = devicesForThisWindow->Get(uuid, getter_AddRefs(micSource)) &&
-                                       micSource->RequiresSharing();
-    if (!alreadySeenThisDeviceBefore) {
-      micSource = new MediaEngineWebRTCMicrophoneSource(
-          new mozilla::AudioInputCubeb(i),
-          i, deviceName, uniqueId,
-          mDelayAgnostic, mExtendedFilter);
-      devicesForThisWindow->Put(uuid, micSource);
-    }
-    aDevices->AppendElement(MakeRefPtr<MediaDevice>(
-                              micSource,
-                              micSource->GetName(),
-                              NS_ConvertUTF8toUTF16(micSource->GetUUID())));
   }
 }
 
 void
 MediaEngineWebRTC::EnumerateSpeakerDevices(uint64_t aWindowId,
                                            nsTArray<RefPtr<MediaDevice> >* aDevices)
 {
   nsTArray<RefPtr<AudioDeviceInfo>> devices;
   CubebUtils::GetDeviceCollection(devices, CubebUtils::Output);
   for (auto& device : devices) {
-    MOZ_ASSERT(device->GetDeviceID().isSome());
     if (device->State() == CUBEB_DEVICE_STATE_ENABLED) {
       MOZ_ASSERT(device->Type() == CUBEB_DEVICE_TYPE_OUTPUT);
-      nsString uuid(device->FriendlyName());
+      nsString uuid(device->Name());
       // If, for example, input and output are in the same device, uuid
       // would be the same for both which ends up to create the same
       // deviceIDs (in JS).
       uuid.Append(NS_LITERAL_STRING("_Speaker"));
       aDevices->AppendElement(MakeRefPtr<MediaDevice>(
-                                device->FriendlyName(),
+                                device->Name(),
                                 dom::MediaDeviceKind::Audiooutput,
                                 uuid));
     }
   }
 }
 
+
 void
 MediaEngineWebRTC::EnumerateDevices(uint64_t aWindowId,
                                     dom::MediaSourceEnum aMediaSource,
                                     MediaSinkEnum aMediaSink,
-                                    nsTArray<RefPtr<MediaDevice> >* aDevices)
+                                    nsTArray<RefPtr<MediaDevice>>* aDevices)
 {
   MOZ_ASSERT(aMediaSource != dom::MediaSourceEnum::Other ||
              aMediaSink != MediaSinkEnum::Other);
   // We spawn threads to handle gUM runnables, so we must protect the member vars
   MutexAutoLock lock(mMutex);
   if (MediaEngineSource::IsVideo(aMediaSource)) {
     EnumerateVideoDevices(aWindowId, aMediaSource, aDevices);
   } else if (aMediaSource == dom::MediaSourceEnum::AudioCapture) {
@@ -360,22 +284,16 @@ MediaEngineWebRTC::EnumerateDevices(uint
     EnumerateMicrophoneDevices(aWindowId, aDevices);
   }
 
   if (aMediaSink == MediaSinkEnum::Speaker) {
     EnumerateSpeakerDevices(aWindowId, aDevices);
   }
 }
 
-bool
-MediaEngineWebRTC::SupportsDuplex()
-{
-  return mFullDuplex;
-}
-
 void
 MediaEngineWebRTC::ReleaseResourcesForWindow(uint64_t aWindowId)
 {
   {
     nsRefPtrHashtable<nsStringHashKey, MediaEngineSource>*
       audioDevicesForThisWindow = mAudioSources.Get(aWindowId);
 
     if (audioDevicesForThisWindow) {
@@ -430,13 +348,117 @@ MediaEngineWebRTC::Shutdown()
   }
 
   LOG(("%s", __FUNCTION__));
   // Shutdown all the sources, since we may have dangling references to the
   // sources in nsDOMUserMediaStreams waiting for GC/CC
   ShutdownSources(mVideoSources);
   ShutdownSources(mAudioSources);
 
+  mEnumerator = nullptr;
+
   mozilla::camera::Shutdown();
-  AudioInputCubeb::CleanupGlobalData();
+}
+
+CubebDeviceEnumerator::CubebDeviceEnumerator()
+  : mMutex("CubebDeviceListMutex")
+  , mManualInvalidation(false)
+{
+  int rv = cubeb_register_device_collection_changed(GetCubebContext(),
+     CUBEB_DEVICE_TYPE_INPUT,
+     &mozilla::CubebDeviceEnumerator::AudioDeviceListChanged_s,
+     this);
+
+  if (rv != CUBEB_OK) {
+    NS_WARNING("Could not register the audio input"
+               " device collection changed callback.");
+    mManualInvalidation = true;
+  }
+}
+
+CubebDeviceEnumerator::~CubebDeviceEnumerator()
+{
+  int rv = cubeb_register_device_collection_changed(GetCubebContext(),
+                                                    CUBEB_DEVICE_TYPE_INPUT,
+                                                    nullptr,
+                                                    this);
+  if (rv != CUBEB_OK) {
+    NS_WARNING("Could not unregister the audio input"
+               " device collection changed callback.");
+  }
+}
+
+void
+CubebDeviceEnumerator::EnumerateAudioInputDevices(nsTArray<RefPtr<AudioDeviceInfo>>& aOutDevices)
+{
+  aOutDevices.Clear();
+
+#ifdef ANDROID
+  // Bug 1473346: enumerating devices is not supported on Android in cubeb,
+  // simply state that there is a single mic, that it is the default, and has a
+  // single channel. All the other values are made up and are not to be used.
+  RefPtr<AudioDeviceInfo> info = new AudioDeviceInfo(nullptr,
+                                                     NS_ConvertUTF8toUTF16(""),
+                                                     NS_ConvertUTF8toUTF16(""),
+                                                     NS_ConvertUTF8toUTF16(""),
+                                                     CUBEB_DEVICE_TYPE_INPUT,
+                                                     CUBEB_DEVICE_STATE_ENABLED,
+                                                     CUBEB_DEVICE_PREF_ALL,
+                                                     CUBEB_DEVICE_FMT_ALL,
+                                                     CUBEB_DEVICE_FMT_S16NE,
+                                                     1,
+                                                     44100,
+                                                     44100,
+                                                     41000,
+                                                     410,
+                                                     128);
+  if (mDevices.IsEmpty()) {
+    mDevices.AppendElement(info);
+  }
+  aOutDevices.AppendElements(mDevices);
+#else
+  cubeb* context = GetCubebContext();
+
+  if (!context) {
+    return;
+  }
+
+  MutexAutoLock lock(mMutex);
+
+  if (mDevices.IsEmpty() || mManualInvalidation) {
+    mDevices.Clear();
+    CubebUtils::GetDeviceCollection(mDevices, CubebUtils::Input);
+  }
+
+  aOutDevices.AppendElements(mDevices);
+#endif
+}
+
+already_AddRefed<AudioDeviceInfo>
+CubebDeviceEnumerator::DeviceInfoFromID(CubebUtils::AudioDeviceID aID)
+{
+  MutexAutoLock lock(mMutex);
+
+  for (uint32_t i  = 0; i < mDevices.Length(); i++) {
+    if (mDevices[i]->DeviceID() == aID) {
+      RefPtr<AudioDeviceInfo> other = mDevices[i];
+      return other.forget();
+    }
+  }
+  return nullptr;
+}
+
+void
+CubebDeviceEnumerator::AudioDeviceListChanged_s(cubeb* aContext, void* aUser)
+{
+  CubebDeviceEnumerator* self = reinterpret_cast<CubebDeviceEnumerator*>(aUser);
+  self->AudioDeviceListChanged();
+}
+
+void
+CubebDeviceEnumerator::AudioDeviceListChanged()
+{
+  MutexAutoLock lock(mMutex);
+
+  mDevices.Clear();
 }
 
 }
--- a/dom/media/webrtc/MediaEngineWebRTC.h
+++ b/dom/media/webrtc/MediaEngineWebRTC.h
@@ -117,289 +117,104 @@ public:
   uint32_t GetBestFitnessDistance(
     const nsTArray<const NormalizedConstraintSet*>& aConstraintSets,
     const nsString& aDeviceId) const override;
 
 protected:
   virtual ~MediaEngineWebRTCAudioCaptureSource() = default;
 };
 
-// Small subset of VoEHardware
-class AudioInput
-{
-public:
-  AudioInput() = default;
-  // Threadsafe because it's referenced from an MicrophoneSource, which can
-  // had references to it on other threads.
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioInput)
-
-  virtual int GetNumOfRecordingDevices(int& aDevices) = 0;
-  virtual int GetRecordingDeviceName(int aIndex, char (&aStrNameUTF8)[128],
-                                     char aStrGuidUTF8[128]) = 0;
-  virtual int GetRecordingDeviceStatus(bool& aIsAvailable) = 0;
-  virtual void GetChannelCount(uint32_t& aChannels) = 0;
-  virtual int GetMaxAvailableChannels(uint32_t& aChannels) = 0;
-  virtual void StartRecording(SourceMediaStream *aStream, AudioDataListener *aListener) = 0;
-  virtual void StopRecording(SourceMediaStream *aStream) = 0;
-  virtual int SetRecordingDevice(int aIndex) = 0;
-  virtual void SetUserChannelCount(uint32_t aChannels) = 0;
-
-protected:
-  // Protected destructor, to discourage deletion outside of Release():
-  virtual ~AudioInput() = default;
-};
-
-class AudioInputCubeb final : public AudioInput
+// This class implements a cache for accessing the audio device list. It can be
+// accessed on any thread.
+class CubebDeviceEnumerator final
 {
 public:
-  explicit AudioInputCubeb(int aIndex = 0) :
-    AudioInput(), mSelectedDevice(aIndex), mInUseCount(0)
-  {
-    if (!mDeviceIndexes) {
-      mDeviceIndexes = new nsTArray<int>;
-      mDeviceNames = new nsTArray<nsCString>;
-      mDefaultDevice = -1;
-    }
-  }
-
-  static void CleanupGlobalData()
-  {
-    if (mDevices.device) {
-      cubeb_device_collection_destroy(CubebUtils::GetCubebContext(), &mDevices);
-    }
-    delete mDeviceIndexes;
-    mDeviceIndexes = nullptr;
-    delete mDeviceNames;
-    mDeviceNames = nullptr;
-  }
-
-  int GetNumOfRecordingDevices(int& aDevices)
-  {
-#ifdef MOZ_WIDGET_ANDROID
-    // OpenSL ES does not support enumerate device.
-    aDevices = 1;
-#else
-    UpdateDeviceList();
-    aDevices = mDeviceIndexes->Length();
-#endif
-    return 0;
-  }
-
-  static int32_t DeviceIndex(int aIndex)
-  {
-    // -1 = system default if any
-    if (aIndex == -1) {
-      if (mDefaultDevice == -1) {
-        aIndex = 0;
-      } else {
-        aIndex = mDefaultDevice;
-      }
-    }
-    MOZ_ASSERT(mDeviceIndexes);
-    if (aIndex < 0 || aIndex >= (int) mDeviceIndexes->Length()) {
-      return -1;
-    }
-    // Note: if the device is gone, this will be -1
-    return (*mDeviceIndexes)[aIndex]; // translate to mDevices index
-  }
-
-  static StaticMutex& Mutex()
-  {
-    return sMutex;
-  }
-
-  static bool GetDeviceID(int aDeviceIndex, CubebUtils::AudioDeviceID &aID)
-  {
-    // Assert sMutex is held
-    sMutex.AssertCurrentThreadOwns();
-#ifdef MOZ_WIDGET_ANDROID
-    aID = nullptr;
-    return true;
-#else
-    int dev_index = DeviceIndex(aDeviceIndex);
-    if (dev_index != -1) {
-      aID = mDevices.device[dev_index].devid;
-      return true;
-    }
-    return false;
-#endif
-  }
-
-  int GetRecordingDeviceName(int aIndex, char (&aStrNameUTF8)[128],
-                             char aStrGuidUTF8[128])
-  {
-#ifdef MOZ_WIDGET_ANDROID
-    aStrNameUTF8[0] = '\0';
-    aStrGuidUTF8[0] = '\0';
-#else
-    int32_t devindex = DeviceIndex(aIndex);
-    if (mDevices.count == 0 || devindex < 0) {
-      return 1;
-    }
-    SprintfLiteral(aStrNameUTF8, "%s%s", aIndex == -1 ? "default: " : "",
-                   mDevices.device[devindex].friendly_name);
-    aStrGuidUTF8[0] = '\0';
-#endif
-    return 0;
-  }
-
-  int GetRecordingDeviceStatus(bool& aIsAvailable)
-  {
-    // With cubeb, we only expose devices of type CUBEB_DEVICE_TYPE_INPUT,
-    // so unless it was removed, say it's available
-    aIsAvailable = true;
-    return 0;
-  }
-
-  void GetChannelCount(uint32_t& aChannels)
-  {
-    GetUserChannelCount(mSelectedDevice, aChannels);
-  }
-
-  static void GetUserChannelCount(int aDeviceIndex, uint32_t& aChannels)
-  {
-    aChannels = sUserChannelCount;
-  }
-
-  int GetMaxAvailableChannels(uint32_t& aChannels)
-  {
-    return GetDeviceMaxChannels(mSelectedDevice, aChannels);
-  }
-
-  static int GetDeviceMaxChannels(int aDeviceIndex, uint32_t& aChannels)
-  {
-#ifdef MOZ_WIDGET_ANDROID
-    aChannels = 1;
-#else
-    int32_t devindex = DeviceIndex(aDeviceIndex);
-    if (mDevices.count == 0 || devindex < 0) {
-      return 1;
-    }
-    aChannels = mDevices.device[devindex].max_channels;
-#endif
-    return 0;
-  }
-
-  void SetUserChannelCount(uint32_t aChannels)
-  {
-    if (GetDeviceMaxChannels(mSelectedDevice, sUserChannelCount)) {
-      sUserChannelCount = 1; // error capture mono
-      return;
-    }
-
-    if (aChannels && aChannels < sUserChannelCount) {
-      sUserChannelCount = aChannels;
-    }
-  }
-
-  void StartRecording(SourceMediaStream *aStream, AudioDataListener *aListener)
-  {
-#ifdef MOZ_WIDGET_ANDROID
-    // OpenSL ES does not support enumerating devices.
-    MOZ_ASSERT(mDevices.count == 0);
-#else
-    MOZ_ASSERT(mDevices.count > 0);
-#endif
-
-    mAnyInUse = true;
-    mInUseCount++;
-    // Always tell the stream we're using it for input
-    aStream->OpenAudioInput(mSelectedDevice, aListener);
-  }
-
-  void StopRecording(SourceMediaStream *aStream)
-  {
-    aStream->CloseAudioInput();
-    if (--mInUseCount == 0) {
-      mAnyInUse = false;
-    }
-  }
-
-  int SetRecordingDevice(int aIndex)
-  {
-    mSelectedDevice = aIndex;
-    return 0;
-  }
+  CubebDeviceEnumerator();
+  ~CubebDeviceEnumerator();
+  // This method returns a list of all the input and output audio devices
+  // available on this machine.
+  // This method is safe to call from all threads.
+  void EnumerateAudioInputDevices(nsTArray<RefPtr<AudioDeviceInfo>>& aOutDevices);
+  // From a cubeb device id, return the info for this device, if it's still a
+  // valid id, or nullptr otherwise.
+  // This method is safe to call from any thread.
+  already_AddRefed<AudioDeviceInfo>
+  DeviceInfoFromID(CubebUtils::AudioDeviceID aID);
 
 protected:
-  ~AudioInputCubeb() {
-    MOZ_RELEASE_ASSERT(mInUseCount == 0);
-  }
+
+  // Static function called by cubeb when the audio input device list changes
+  // (i.e. when a new device is made available, or non-available). This
+  // re-binds to the MediaEngineWebRTC that instantiated this
+  // CubebDeviceEnumerator, and simply calls `AudioDeviceListChanged` below.
+  static void AudioDeviceListChanged_s(cubeb* aContext, void* aUser);
+  // Invalidates the cached audio input device list, can be called on any
+  // thread.
+  void AudioDeviceListChanged();
 
 private:
-  // It would be better to watch for device-change notifications
-  void UpdateDeviceList();
-
-  // We have an array, which consists of indexes to the current mDevices
-  // list.  This is updated on mDevices updates.  Many devices in mDevices
-  // won't be included in the array (wrong type, etc), or if a device is
-  // removed it will map to -1 (and opens of this device will need to check
-  // for this - and be careful of threading access.  The mappings need to
-  // updated on each re-enumeration.
-  int mSelectedDevice;
-  uint32_t mInUseCount;
-
-  // pointers to avoid static constructors
-  static nsTArray<int>* mDeviceIndexes;
-  static int mDefaultDevice; // -1 == not set
-  static nsTArray<nsCString>* mDeviceNames;
-  static cubeb_device_collection mDevices;
-  static bool mAnyInUse;
-  static StaticMutex sMutex;
-  static uint32_t sUserChannelCount;
+  // Synchronize access to mDevices
+  Mutex mMutex;
+  nsTArray<RefPtr<AudioDeviceInfo>> mDevices;
+  // If mManualInvalidation is true, then it is necessary to query the device
+  // list each time instead of relying on automatic invalidation of the cache by
+  // cubeb itself. Set in the constructor and then can be access on any thread.
+  bool mManualInvalidation;
 };
 
+// This class is instantiated on the MediaManager thread, and is then sent and
+// only ever access again on the MediaStreamGraph.
 class WebRTCAudioDataListener : public AudioDataListener
 {
 protected:
   // Protected destructor, to discourage deletion outside of Release():
   virtual ~WebRTCAudioDataListener() {}
 
 public:
   explicit WebRTCAudioDataListener(MediaEngineWebRTCMicrophoneSource* aAudioSource)
-    : mMutex("WebRTCAudioDataListener::mMutex")
-    , mAudioSource(aAudioSource)
+    : mAudioSource(aAudioSource)
   {}
 
   // AudioDataListenerInterface methods
-  void NotifyOutputData(MediaStreamGraph* aGraph,
+  void NotifyOutputData(MediaStreamGraphImpl* aGraph,
                         AudioDataValue* aBuffer,
                         size_t aFrames,
                         TrackRate aRate,
                         uint32_t aChannels) override;
 
-  void NotifyInputData(MediaStreamGraph* aGraph,
+  void NotifyInputData(MediaStreamGraphImpl* aGraph,
                        const AudioDataValue* aBuffer,
                        size_t aFrames,
                        TrackRate aRate,
                        uint32_t aChannels) override;
 
-  void DeviceChanged() override;
+  uint32_t RequestedInputChannelCount(MediaStreamGraphImpl* aGraph) override;
 
-  void Shutdown();
+  void DeviceChanged(MediaStreamGraphImpl* aGraph) override;
+
+  void Disconnect(MediaStreamGraphImpl* aGraph) override;
 
 private:
-  Mutex mMutex;
   RefPtr<MediaEngineWebRTCMicrophoneSource> mAudioSource;
 };
 
 class MediaEngineWebRTCMicrophoneSource : public MediaEngineSource,
                                           public AudioDataListenerInterface
 {
 public:
-  MediaEngineWebRTCMicrophoneSource(mozilla::AudioInput* aAudioInput,
-                                    int aIndex,
-                                    const char* name,
-                                    const char* uuid,
+  MediaEngineWebRTCMicrophoneSource(RefPtr<AudioDeviceInfo> aInfo,
+                                    const nsString& name,
+                                    const nsCString& uuid,
+                                    uint32_t maxChannelCount,
                                     bool aDelayAgnostic,
                                     bool aExtendedFilter);
 
   bool RequiresSharing() const override
   {
-    return true;
+    return false;
   }
 
   nsString GetName() const override;
   nsCString GetUUID() const override;
 
   nsresult Allocate(const dom::MediaTrackConstraints &aConstraints,
                     const MediaEnginePrefs& aPrefs,
                     const nsString& aDeviceId,
@@ -427,24 +242,31 @@ public:
 
   void Pull(const RefPtr<const AllocationHandle>& aHandle,
             const RefPtr<SourceMediaStream>& aStream,
             TrackID aTrackID,
             StreamTime aDesiredTime,
             const PrincipalHandle& aPrincipalHandle) override;
 
   // AudioDataListenerInterface methods
-  void NotifyOutputData(MediaStreamGraph* aGraph,
+  void NotifyOutputData(MediaStreamGraphImpl* aGraph,
                         AudioDataValue* aBuffer, size_t aFrames,
                         TrackRate aRate, uint32_t aChannels) override;
-  void NotifyInputData(MediaStreamGraph* aGraph,
+  void NotifyInputData(MediaStreamGraphImpl* aGraph,
                        const AudioDataValue* aBuffer, size_t aFrames,
                        TrackRate aRate, uint32_t aChannels) override;
 
-  void DeviceChanged() override;
+  void DeviceChanged(MediaStreamGraphImpl* aGraph) override;
+
+  uint32_t RequestedInputChannelCount(MediaStreamGraphImpl* aGraph) override
+  {
+    return GetRequestedInputChannelCount(aGraph);
+  }
+
+  void Disconnect(MediaStreamGraphImpl* aGraph) override;
 
   dom::MediaSourceEnum GetMediaSource() const override
   {
     return dom::MediaSourceEnum::Microphone;
   }
 
   nsresult TakePhoto(MediaEnginePhotoCallback* aCallback) override
   {
@@ -539,68 +361,74 @@ private:
 
   bool HasEnabledTrack() const;
 
   template<typename T>
   void InsertInGraph(const T* aBuffer,
                      size_t aFrames,
                      uint32_t aChannels);
 
-  void PacketizeAndProcess(MediaStreamGraph* aGraph,
+  void PacketizeAndProcess(MediaStreamGraphImpl* aGraph,
                            const AudioDataValue* aBuffer,
                            size_t aFrames,
                            TrackRate aRate,
                            uint32_t aChannels);
 
 
   // This is true when all processing is disabled, we can skip
   // packetization, resampling and other processing passes.
   // Graph thread only.
-  bool PassThrough() const;
+  bool PassThrough(MediaStreamGraphImpl* aGraphImpl) const;
 
   // Graph thread only.
   void SetPassThrough(bool aPassThrough);
+  uint32_t GetRequestedInputChannelCount(MediaStreamGraphImpl* aGraphImpl);
+  void SetRequestedInputChannelCount(uint32_t aRequestedInputChannelCount);
 
-  // Owning thread only.
+  // mListener is created on the MediaManager thread, and then sent to the MSG
+  // thread. On shutdown, we send this pointer to the MSG thread again, telling
+  // it to clean up.
   RefPtr<WebRTCAudioDataListener> mListener;
 
-  // Note: shared across all microphone sources. Owning thread only.
-  static int sChannelsOpen;
+  // Can be shared on any thread.
+  const RefPtr<AudioDeviceInfo> mDeviceInfo;
 
-  const RefPtr<mozilla::AudioInput> mAudioInput;
   const UniquePtr<webrtc::AudioProcessing> mAudioProcessing;
 
   // accessed from the GraphDriver thread except for deletion.
   nsAutoPtr<AudioPacketizer<AudioDataValue, float>> mPacketizerInput;
   nsAutoPtr<AudioPacketizer<AudioDataValue, float>> mPacketizerOutput;
 
   // mMutex protects some of our members off the owning thread.
   Mutex mMutex;
 
   // We append an allocation in Allocate() and remove it in Deallocate().
   // Both the array and the Allocation members are modified under mMutex on
   // the owning thread. Accessed under one of the two.
   nsTArray<Allocation> mAllocations;
 
-  // Current state of the shared resource for this source.
-  // Set under mMutex on the owning thread. Accessed under one of the two
-  MediaEngineSourceState mState = kReleased;
+  // Current state of the shared resource for this source. Written on the
+  // owning thread, read on either the owning thread or the MSG thread.
+  Atomic<MediaEngineSourceState> mState;
 
-  int mCapIndex;
   bool mDelayAgnostic;
   bool mExtendedFilter;
   bool mStarted;
 
   const nsString mDeviceName;
   const nsCString mDeviceUUID;
 
   // The current settings for the underlying device.
   // Member access is main thread only after construction.
   const nsMainThreadPtrHandle<media::Refcountable<dom::MediaTrackSettings>> mSettings;
 
+  // The number of channels asked for by content, after clamping to the range of
+  // legal channel count for this particular device. This is the number of
+  // channels of the input buffer passed as parameter in NotifyInputData.
+  uint32_t mRequestedInputChannelCount;
   uint64_t mTotalFrames;
   uint64_t mLastLogFrames;
 
   // mSkipProcessing is true if none of the processing passes are enabled,
   // because of prefs or constraints. This allows simply copying the audio into
   // the MSG, skipping resampling and the whole webrtc.org code.
   // This is read and written to only on the MSG thread.
   bool mSkipProcessing;
@@ -644,24 +472,23 @@ private:
   void EnumerateVideoDevices(uint64_t aWindowId,
                              dom::MediaSourceEnum,
                              nsTArray<RefPtr<MediaDevice>>*);
   void EnumerateMicrophoneDevices(uint64_t aWindowId,
                                   nsTArray<RefPtr<MediaDevice>>*);
   void EnumerateSpeakerDevices(uint64_t aWindowId,
                                nsTArray<RefPtr<MediaDevice> >*);
 
-  nsCOMPtr<nsIThread> mThread;
-
   // gUM runnables can e.g. Enumerate from multiple threads
   Mutex mMutex;
-  RefPtr<mozilla::AudioInput> mAudioInput;
-  bool mFullDuplex;
-  bool mDelayAgnostic;
-  bool mExtendedFilter;
+  UniquePtr<mozilla::CubebDeviceEnumerator> mEnumerator;
+  const bool mDelayAgnostic;
+  const bool mExtendedFilter;
+  // This also is set in the ctor and then never changed, but we can't make it
+  // const because we pass it to a function that takes bool* in the ctor.
   bool mHasTabVideoSource;
 
   // Maps WindowID to a map of device uuid to their MediaEngineSource,
   // separately for audio and video.
   nsClassHashtable<nsUint64HashKey,
                     nsRefPtrHashtable<nsStringHashKey,
                                       MediaEngineSource>> mVideoSources;
   nsClassHashtable<nsUint64HashKey,
--- a/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
+++ b/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
@@ -18,17 +18,16 @@
 #include "mtransport/runnable_utils.h"
 #include "nsAutoPtr.h"
 #include "Tracing.h"
 
 // scoped_ptr.h uses FF
 #ifdef FF
 #undef FF
 #endif
-#include "webrtc/modules/audio_device/opensl/single_rw_fifo.h"
 #include "webrtc/voice_engine/voice_engine_defines.h"
 #include "webrtc/modules/audio_processing/include/audio_processing.h"
 #include "webrtc/common_audio/include/audio_util.h"
 
 using namespace webrtc;
 
 // These are restrictions from the webrtc.org code
 #define MAX_CHANNELS 2
@@ -48,103 +47,120 @@ LogModule* GetMediaManagerLog();
 #define LOG_FRAMES(msg) MOZ_LOG(GetMediaManagerLog(), mozilla::LogLevel::Verbose, msg)
 
 LogModule* AudioLogModule() {
   static mozilla::LazyLogModule log("AudioLatency");
   return static_cast<LogModule*>(log);
 }
 
 void
-WebRTCAudioDataListener::NotifyOutputData(MediaStreamGraph* aGraph,
+WebRTCAudioDataListener::NotifyOutputData(MediaStreamGraphImpl* aGraph,
                                           AudioDataValue* aBuffer,
                                           size_t aFrames,
                                           TrackRate aRate,
                                           uint32_t aChannels)
 {
-  MutexAutoLock lock(mMutex);
+  MOZ_ASSERT(aGraph->CurrentDriver()->OnThread());
   if (mAudioSource) {
     mAudioSource->NotifyOutputData(aGraph, aBuffer, aFrames, aRate, aChannels);
   }
 }
 
 void
-WebRTCAudioDataListener::NotifyInputData(MediaStreamGraph* aGraph,
+WebRTCAudioDataListener::NotifyInputData(MediaStreamGraphImpl* aGraph,
                                          const AudioDataValue* aBuffer,
                                          size_t aFrames,
                                          TrackRate aRate,
                                          uint32_t aChannels)
 {
-  MutexAutoLock lock(mMutex);
+  MOZ_ASSERT(aGraph->CurrentDriver()->OnThread());
   if (mAudioSource) {
     mAudioSource->NotifyInputData(aGraph, aBuffer, aFrames, aRate, aChannels);
   }
 }
 
 void
-WebRTCAudioDataListener::DeviceChanged()
+WebRTCAudioDataListener::DeviceChanged(MediaStreamGraphImpl* aGraph)
 {
-  MutexAutoLock lock(mMutex);
+  MOZ_ASSERT(aGraph->CurrentDriver()->OnThread());
   if (mAudioSource) {
-    mAudioSource->DeviceChanged();
+    mAudioSource->DeviceChanged(aGraph);
   }
 }
 
-void
-WebRTCAudioDataListener::Shutdown()
+uint32_t
+WebRTCAudioDataListener::RequestedInputChannelCount(MediaStreamGraphImpl* aGraph)
 {
-  MutexAutoLock lock(mMutex);
-  mAudioSource = nullptr;
+  MOZ_ASSERT(aGraph->CurrentDriver()->OnThread());
+  if (mAudioSource) {
+    return mAudioSource->RequestedInputChannelCount(aGraph);
+  }
+  return 0;
+}
+
+void
+WebRTCAudioDataListener::Disconnect(MediaStreamGraphImpl* aGraph)
+{
+  MOZ_ASSERT(aGraph->CurrentDriver()->OnThread());
+  if (mAudioSource) {
+    mAudioSource->Disconnect(aGraph);
+    mAudioSource = nullptr;
+  }
 }
 
 /**
  * WebRTC Microphone MediaEngineSource.
  */
-int MediaEngineWebRTCMicrophoneSource::sChannelsOpen = 0;
 
 MediaEngineWebRTCMicrophoneSource::Allocation::Allocation(
     const RefPtr<AllocationHandle>& aHandle)
   : mHandle(aHandle)
 {}
 
 MediaEngineWebRTCMicrophoneSource::Allocation::~Allocation() = default;
 
 MediaEngineWebRTCMicrophoneSource::MediaEngineWebRTCMicrophoneSource(
-    mozilla::AudioInput* aAudioInput,
-    int aIndex,
-    const char* aDeviceName,
-    const char* aDeviceUUID,
+    RefPtr<AudioDeviceInfo> aInfo,
+    const nsString& aDeviceName,
+    const nsCString& aDeviceUUID,
+    uint32_t aMaxChannelCount,
     bool aDelayAgnostic,
     bool aExtendedFilter)
-  : mAudioInput(aAudioInput)
+  : mDeviceInfo(std::move(aInfo))
   , mAudioProcessing(AudioProcessing::Create())
   , mMutex("WebRTCMic::Mutex")
-  , mCapIndex(aIndex)
   , mDelayAgnostic(aDelayAgnostic)
   , mExtendedFilter(aExtendedFilter)
   , mStarted(false)
-  , mDeviceName(NS_ConvertUTF8toUTF16(aDeviceName))
+  , mDeviceName(aDeviceName)
   , mDeviceUUID(aDeviceUUID)
   , mSettings(
       new nsMainThreadPtrHolder<media::Refcountable<dom::MediaTrackSettings>>(
         "MediaEngineWebRTCMicrophoneSource::mSettings",
         new media::Refcountable<dom::MediaTrackSettings>(),
         // Non-strict means it won't assert main thread for us.
         // It would be great if it did but we're already on the media thread.
         /* aStrict = */ false))
+  , mRequestedInputChannelCount(aMaxChannelCount)
   , mTotalFrames(0)
   , mLastLogFrames(0)
   , mSkipProcessing(false)
   , mInputDownmixBuffer(MAX_SAMPLING_FREQ * MAX_CHANNELS / 100)
 {
-  MOZ_ASSERT(aAudioInput);
+#ifndef ANDROID
+  MOZ_ASSERT(mDeviceInfo->DeviceID());
+#endif
+
+  // We'll init lazily as needed
   mSettings->mEchoCancellation.Construct(0);
   mSettings->mAutoGainControl.Construct(0);
   mSettings->mNoiseSuppression.Construct(0);
   mSettings->mChannelCount.Construct(0);
-  // We'll init lazily as needed
+
+  mState = kReleased;
 }
 
 nsString
 MediaEngineWebRTCMicrophoneSource::GetName() const
 {
   return mDeviceName;
 }
 
@@ -412,161 +428,197 @@ MediaEngineWebRTCMicrophoneSource::Updat
   AssertIsOnOwningThread();
 
   FlattenedConstraints c(aNetConstraints);
 
   MediaEnginePrefs prefs = aPrefs;
   prefs.mAecOn = c.mEchoCancellation.Get(prefs.mAecOn);
   prefs.mAgcOn = c.mAutoGainControl.Get(prefs.mAgcOn);
   prefs.mNoiseOn = c.mNoiseSuppression.Get(prefs.mNoiseOn);
-  uint32_t maxChannels = 1;
-  if (mAudioInput->GetMaxAvailableChannels(maxChannels) != 0) {
-    return NS_ERROR_FAILURE;
-  }
-  // Check channelCount violation
-  if (static_cast<int32_t>(maxChannels) < c.mChannelCount.mMin ||
-      static_cast<int32_t>(maxChannels) > c.mChannelCount.mMax) {
+
+  // Determine an actual channel count to use for this source. Three factors at
+  // play here: the device capabilities, the constraints passed in by content,
+  // and a pref that can force things (for testing)
+  int32_t maxChannels = mDeviceInfo->MaxChannels();
+
+  // First, check channelCount violation wrt constraints. This fails in case of
+  // error.
+  if (c.mChannelCount.mMin > maxChannels) {
     *aOutBadConstraint = "channelCount";
     return NS_ERROR_FAILURE;
   }
-  // Clamp channelCount to a valid value
+  // A pref can force the channel count to use. If the pref has a value of zero
+  // or lower, it has no effect.
   if (prefs.mChannels <= 0) {
-    prefs.mChannels = static_cast<int32_t>(maxChannels);
+    prefs.mChannels = maxChannels;
   }
+
+  // Get the number of channels asked for by content, and clamp it between the
+  // pref and the maximum number of channels that the device supports.
   prefs.mChannels = c.mChannelCount.Get(std::min(prefs.mChannels,
-                                        static_cast<int32_t>(maxChannels)));
-  // Clamp channelCount to a valid value
-  prefs.mChannels = std::max(1, std::min(prefs.mChannels, static_cast<int32_t>(maxChannels)));
+                                        maxChannels));
+  prefs.mChannels = std::max(1, std::min(prefs.mChannels, maxChannels));
 
   LOG(("Audio config: aec: %d, agc: %d, noise: %d, channels: %d",
       prefs.mAecOn ? prefs.mAec : -1,
       prefs.mAgcOn ? prefs.mAgc : -1,
       prefs.mNoiseOn ? prefs.mNoise : -1,
       prefs.mChannels));
 
   switch (mState) {
     case kReleased:
       MOZ_ASSERT(aHandle);
-      if (sChannelsOpen != 0) {
-        // Until we fix (or wallpaper) support for multiple mic input
-        // (Bug 1238038) fail allocation for a second device
-        return NS_ERROR_FAILURE;
-      }
-      if (mAudioInput->SetRecordingDevice(mCapIndex)) {
-         return NS_ERROR_FAILURE;
-      }
-      mAudioInput->SetUserChannelCount(prefs.mChannels);
       {
         MutexAutoLock lock(mMutex);
         mState = kAllocated;
       }
-      sChannelsOpen++;
-      LOG(("Audio device %d allocated", mCapIndex));
-      {
-        // Update with the actual applied channelCount in order
-        // to store it in settings.
-        uint32_t channelCount = 0;
-        mAudioInput->GetChannelCount(channelCount);
-        MOZ_ASSERT(channelCount > 0);
-        prefs.mChannels = channelCount;
-      }
+      LOG(("Audio device %s allocated", NS_ConvertUTF16toUTF8(mDeviceInfo->Name()).get()));
       break;
 
     case kStarted:
     case kStopped:
-      if (prefs.mChannels != mNetPrefs.mChannels) {
-        // If the channel count changed, tell the MSG to open a new driver with
-        // the correct channel count.
-        MOZ_ASSERT(!mAllocations.IsEmpty());
-        RefPtr<SourceMediaStream> stream;
-        for (const Allocation& allocation : mAllocations) {
-          if (allocation.mStream && allocation.mStream->GraphImpl()) {
-            stream = allocation.mStream;
-            break;
-          }
-        }
-        MOZ_ASSERT(stream);
-
-        mAudioInput->SetUserChannelCount(prefs.mChannels);
-        // Get validated number of channel
-        uint32_t channelCount = 0;
-        mAudioInput->GetChannelCount(channelCount);
-        MOZ_ASSERT(channelCount > 0 && mNetPrefs.mChannels > 0);
-        if (!stream->OpenNewAudioCallbackDriver(mListener)) {
-          MOZ_LOG(GetMediaManagerLog(), LogLevel::Error, ("Could not open a new AudioCallbackDriver for input"));
-          return NS_ERROR_FAILURE;
-        }
+      if (prefs == mNetPrefs) {
+        LOG(("UpdateSingleSource: new prefs for %s are the same as the current prefs, returning.",
+             NS_ConvertUTF16toUTF8(mDeviceName).get()));
+        return NS_OK;
       }
       break;
 
     default:
-      LOG(("Audio device %d in ignored state %d", mCapIndex, mState));
+      LOG(("Audio device %s in ignored state %d", NS_ConvertUTF16toUTF8(mDeviceInfo->Name()).get(), MediaEngineSourceState(mState)));
       break;
   }
 
-  if (MOZ_LOG_TEST(GetMediaManagerLog(), LogLevel::Debug)) {
-    if (mAllocations.IsEmpty()) {
-      LOG(("Audio device %d reallocated", mCapIndex));
-    } else {
-      LOG(("Audio device %d allocated shared", mCapIndex));
-    }
-  }
-
-  if (sChannelsOpen > 0) {
+  if (mState != kReleased) {
     UpdateAGCSettingsIfNeeded(prefs.mAgcOn, static_cast<AgcModes>(prefs.mAgc));
     UpdateNSSettingsIfNeeded(prefs.mNoiseOn, static_cast<NsModes>(prefs.mNoise));
     UpdateAECSettingsIfNeeded(prefs.mAecOn, static_cast<EcModes>(prefs.mAec));
 
     webrtc::Config config;
     config.Set<webrtc::ExtendedFilter>(new webrtc::ExtendedFilter(mExtendedFilter));
     config.Set<webrtc::DelayAgnostic>(new webrtc::DelayAgnostic(mDelayAgnostic));
     mAudioProcessing->SetExtraOptions(config);
   }
   mNetPrefs = prefs;
   return NS_OK;
 }
 
 #undef HANDLE_APM_ERROR
 
+bool
+MediaEngineWebRTCMicrophoneSource::PassThrough(MediaStreamGraphImpl* aGraph) const
+{
+  MOZ_ASSERT(aGraph->CurrentDriver()->OnThread());
+  return mSkipProcessing;
+}
+void
+MediaEngineWebRTCMicrophoneSource::SetPassThrough(bool aPassThrough)
+{
+  {
+    MutexAutoLock lock(mMutex);
+    if (mAllocations.IsEmpty()) {
+      // This can be the case, for now, because we're mixing mutable shared state
+      // and linearization via message queue. This is temporary.
+      return;
+    }
+
+    // mStream is always valid because it's set right before ::Start is called.
+    // SetPassThrough cannot be called before that, because it's running on the
+    // graph thread, and this cannot happen before the source has been started.
+    MOZ_ASSERT(mAllocations.Length() == 1 &&
+               mAllocations[0].mStream &&
+               mAllocations[0].mStream->GraphImpl()->CurrentDriver()->OnThread(),
+               "Wrong calling pattern, don't call this before ::SetTrack.");
+  }
+  mSkipProcessing = aPassThrough;
+}
+
+uint32_t
+MediaEngineWebRTCMicrophoneSource::GetRequestedInputChannelCount(MediaStreamGraphImpl* aGraphImpl)
+{
+  MOZ_ASSERT(aGraphImpl->CurrentDriver()->OnThread(),
+             "Wrong calling pattern, don't call this before ::SetTrack.");
+
+  if (mState == kReleased) {
+    // This source has been released, and is waiting for collection. Simply
+    // return 0, this source won't contribute to the channel count decision.
+    // Again, this is temporary.
+    return 0;
+  }
+
+  return mRequestedInputChannelCount;
+}
+
+void
+MediaEngineWebRTCMicrophoneSource::SetRequestedInputChannelCount(
+  uint32_t aRequestedInputChannelCount)
+{
+  MutexAutoLock lock(mMutex);
+
+  MOZ_ASSERT(mAllocations.Length() <= 1);
+
+  if (mAllocations.IsEmpty()) {
+      return;
+  }
+  MOZ_ASSERT(mAllocations.Length() == 1 &&
+             mAllocations[0].mStream &&
+             mAllocations[0].mStream->GraphImpl()->CurrentDriver()->OnThread(),
+             "Wrong calling pattern, don't call this before ::SetTrack.");
+  mRequestedInputChannelCount = aRequestedInputChannelCount;
+  mAllocations[0].mStream->GraphImpl()->ReevaluateInputDevice();
+}
+
 void
 MediaEngineWebRTCMicrophoneSource::ApplySettings(const MediaEnginePrefs& aPrefs,
                                                  RefPtr<MediaStreamGraphImpl> aGraph)
 {
   AssertIsOnOwningThread();
   MOZ_DIAGNOSTIC_ASSERT(aGraph);
+#ifdef DEBUG
+  {
+    MutexAutoLock lock(mMutex);
+    MOZ_ASSERT(mAllocations.Length() <= 1);
+  }
+#endif
 
   RefPtr<MediaEngineWebRTCMicrophoneSource> that = this;
   NS_DispatchToMainThread(media::NewRunnableFrom([that, graph = std::move(aGraph), aPrefs]() mutable {
     that->mSettings->mEchoCancellation.Value() = aPrefs.mAecOn;
     that->mSettings->mAutoGainControl.Value() = aPrefs.mAgcOn;
     that->mSettings->mNoiseSuppression.Value() = aPrefs.mNoiseOn;
     that->mSettings->mChannelCount.Value() = aPrefs.mChannels;
 
     class Message : public ControlMessage {
     public:
       Message(MediaEngineWebRTCMicrophoneSource* aSource,
-              bool aPassThrough)
+              bool aPassThrough,
+              uint32_t aRequestedInputChannelCount)
         : ControlMessage(nullptr)
         , mMicrophoneSource(aSource)
         , mPassThrough(aPassThrough)
+        , mRequestedInputChannelCount(aRequestedInputChannelCount)
         {}
 
       void Run() override
       {
         mMicrophoneSource->SetPassThrough(mPassThrough);
+        mMicrophoneSource->SetRequestedInputChannelCount(mRequestedInputChannelCount);
       }
 
     protected:
       RefPtr<MediaEngineWebRTCMicrophoneSource> mMicrophoneSource;
       bool mPassThrough;
+      uint32_t mRequestedInputChannelCount;
     };
 
     bool passThrough = !(aPrefs.mAecOn || aPrefs.mAgcOn || aPrefs.mNoiseOn);
     if (graph) {
-      graph->AppendMessage(MakeUnique<Message>(that, passThrough));
+      graph->AppendMessage(MakeUnique<Message>(that,
+                                               passThrough,
+                                               aPrefs.mChannels));
     }
 
     return NS_OK;
   }));
 }
 
 nsresult
 MediaEngineWebRTCMicrophoneSource::Allocate(const dom::MediaTrackConstraints &aConstraints,
@@ -576,92 +628,94 @@ MediaEngineWebRTCMicrophoneSource::Alloc
                                             AllocationHandle** aOutHandle,
                                             const char** aOutBadConstraint)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(aOutHandle);
   auto handle = MakeRefPtr<AllocationHandle>(aConstraints, aPrincipalInfo,
                                              aDeviceId);
 
+#ifdef DEBUG
+  {
+    MutexAutoLock lock(mMutex);
+    MOZ_ASSERT(mAllocations.Length() <= 1);
+  }
+#endif
   LOG(("Mic source %p allocation %p Allocate()", this, handle.get()));
 
   nsresult rv = ReevaluateAllocation(handle, nullptr, aPrefs, aDeviceId,
                                      aOutBadConstraint);
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   {
     MutexAutoLock lock(mMutex);
+    MOZ_ASSERT(mAllocations.IsEmpty(), "Only allocate once.");
     mAllocations.AppendElement(Allocation(handle));
   }
 
   handle.forget(aOutHandle);
   return NS_OK;
 }
 
 nsresult
 MediaEngineWebRTCMicrophoneSource::Deallocate(const RefPtr<const AllocationHandle>& aHandle)
 {
   AssertIsOnOwningThread();
 
+  MOZ_ASSERT(mState == kStopped);
+
   size_t i = mAllocations.IndexOf(aHandle, 0, AllocationHandleComparator());
   MOZ_DIAGNOSTIC_ASSERT(i != mAllocations.NoIndex);
   MOZ_DIAGNOSTIC_ASSERT(!mAllocations[i].mEnabled,
                         "Source should be stopped for the track before removing");
 
-  LOG(("Mic source %p allocation %p Deallocate()", this, aHandle.get()));
-
   if (mAllocations[i].mStream && IsTrackIDExplicit(mAllocations[i].mTrackID)) {
     mAllocations[i].mStream->EndTrack(mAllocations[i].mTrackID);
   }
 
   {
     MutexAutoLock lock(mMutex);
+    MOZ_ASSERT(mAllocations.Length() == 1, "Only allocate once.");
     mAllocations.RemoveElementAt(i);
   }
 
   if (mAllocations.IsEmpty()) {
     // If empty, no callbacks to deliver data should be occuring
     MOZ_ASSERT(mState != kReleased, "Source not allocated");
     MOZ_ASSERT(mState != kStarted, "Source not stopped");
-    MOZ_ASSERT(sChannelsOpen > 0);
-    --sChannelsOpen;
 
     MutexAutoLock lock(mMutex);
     mState = kReleased;
-    LOG(("Audio device %d deallocated", mCapIndex));
+    LOG(("Audio device %s deallocated", NS_ConvertUTF16toUTF8(mDeviceName).get()));
   } else {
-    LOG(("Audio device %d deallocated but still in use", mCapIndex));
+    LOG(("Audio device %s deallocated but still in use", NS_ConvertUTF16toUTF8(mDeviceName).get()));
   }
   return NS_OK;
 }
 
 nsresult
 MediaEngineWebRTCMicrophoneSource::SetTrack(const RefPtr<const AllocationHandle>& aHandle,
                                             const RefPtr<SourceMediaStream>& aStream,
                                             TrackID aTrackID,
                                             const PrincipalHandle& aPrincipal)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(aStream);
   MOZ_ASSERT(IsTrackIDExplicit(aTrackID));
 
-  LOG(("Mic source %p allocation %p SetTrack() stream=%p, track=%" PRId32,
-       this, aHandle.get(), aStream.get(), aTrackID));
-
-  // Until we fix bug 1400488 we need to block a second tab (OuterWindow)
-  // from opening an already-open device.  If it's the same tab, they
-  // will share a Graph(), and we can allow it.
-  if (!mAllocations.IsEmpty() &&
+  if (mAllocations.Length() == 1 &&
       mAllocations[0].mStream &&
       mAllocations[0].mStream->Graph() != aStream->Graph()) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
+  MOZ_ASSERT(mAllocations.Length() == 1, "Only allocate once.");
+
   size_t i = mAllocations.IndexOf(aHandle, 0, AllocationHandleComparator());
   MOZ_DIAGNOSTIC_ASSERT(i != mAllocations.NoIndex);
   MOZ_ASSERT(!mAllocations[i].mStream);
   MOZ_ASSERT(mAllocations[i].mTrackID == TRACK_NONE);
   MOZ_ASSERT(mAllocations[i].mPrincipal == PRINCIPAL_HANDLE_NONE);
   {
     MutexAutoLock lock(mMutex);
     mAllocations[i].mStream = aStream;
@@ -683,28 +737,35 @@ MediaEngineWebRTCMicrophoneSource::SetTr
   LOG(("Stream %p registered for microphone capture", aStream.get()));
   return NS_OK;
 }
 
 nsresult
 MediaEngineWebRTCMicrophoneSource::Start(const RefPtr<const AllocationHandle>& aHandle)
 {
   AssertIsOnOwningThread();
-
-  if (sChannelsOpen == 0) {
-    return NS_ERROR_FAILURE;
+  if (mState == kStarted) {
+    return NS_OK;
   }
 
-  LOG(("Mic source %p allocation %p Start()", this, aHandle.get()));
+  MOZ_ASSERT(mState == kAllocated || mState == kStopped);
 
   size_t i = mAllocations.IndexOf(aHandle, 0, AllocationHandleComparator());
   MOZ_DIAGNOSTIC_ASSERT(i != mAllocations.NoIndex,
                         "Can't start track that hasn't been added");
   Allocation& allocation = mAllocations[i];
 
+  CubebUtils::AudioDeviceID deviceID = mDeviceInfo->DeviceID();
+  if (allocation.mStream->GraphImpl()->InputDeviceID() &&
+      allocation.mStream->GraphImpl()->InputDeviceID() != deviceID) {
+    // For now, we only allow opening a single audio input device per document,
+    // because we can only have one MSG per document.
+    return NS_ERROR_FAILURE;
+  }
+
   MOZ_ASSERT(!allocation.mEnabled, "Source already started");
   {
     // This spans setting both the enabled state and mState.
     MutexAutoLock lock(mMutex);
     allocation.mEnabled = true;
 
 #ifdef DEBUG
     // Ensure that callback-tracking state is reset when callbacks start coming.
@@ -715,67 +776,65 @@ MediaEngineWebRTCMicrophoneSource::Start
 
     if (!mListener) {
       mListener = new WebRTCAudioDataListener(this);
     }
 
     // Make sure logger starts before capture
     AsyncLatencyLogger::Get(true);
 
-    // Must be *before* StartSend() so it will notice we selected external input (full_duplex)
-    mAudioInput->StartRecording(allocation.mStream, mListener);
+    allocation.mStream->OpenAudioInput(deviceID, mListener);
 
     MOZ_ASSERT(mState != kReleased);
     mState = kStarted;
   }
 
   ApplySettings(mNetPrefs, allocation.mStream->GraphImpl());
 
   return NS_OK;
 }
 
 nsresult
 MediaEngineWebRTCMicrophoneSource::Stop(const RefPtr<const AllocationHandle>& aHandle)
 {
+  MOZ_ASSERT(mAllocations.Length() <= 1);
   AssertIsOnOwningThread();
 
   LOG(("Mic source %p allocation %p Stop()", this, aHandle.get()));
 
   size_t i = mAllocations.IndexOf(aHandle, 0, AllocationHandleComparator());
   MOZ_DIAGNOSTIC_ASSERT(i != mAllocations.NoIndex,
                         "Cannot stop track that we don't know about");
   Allocation& allocation = mAllocations[i];
+  MOZ_ASSERT(allocation.mStream, "SetTrack must have been called before ::Stop");
 
   if (!allocation.mEnabled) {
     // Already stopped - this is allowed
     return NS_OK;
   }
 
   {
     // This spans setting both the enabled state and mState.
     MutexAutoLock lock(mMutex);
     allocation.mEnabled = false;
 
-    mAudioInput->StopRecording(allocation.mStream);
+    CubebUtils::AudioDeviceID deviceID = mDeviceInfo->DeviceID();
+    Maybe<CubebUtils::AudioDeviceID> id = Some(deviceID);
+    allocation.mStream->CloseAudioInput(id, mListener);
+    mListener = nullptr;
 
     if (HasEnabledTrack()) {
       // Another track is keeping us from stopping
       return NS_OK;
     }
 
     MOZ_ASSERT(mState == kStarted, "Should be started when stopping");
     mState = kStopped;
   }
 
-  if (mListener) {
-    // breaks a cycle, since the WebRTCAudioDataListener has a RefPtr to us
-    mListener->Shutdown();
-    mListener = nullptr;
-  }
-
   return NS_OK;
 }
 
 void
 MediaEngineWebRTCMicrophoneSource::GetSettings(dom::MediaTrackSettings& aOutSettings) const
 {
   MOZ_ASSERT(NS_IsMainThread());
   aOutSettings = *mSettings;
@@ -821,17 +880,18 @@ MediaEngineWebRTCMicrophoneSource::Pull(
       delta += WEBAUDIO_BLOCK_SIZE;
     }
 
     LOG_FRAMES(("Pulling %" PRId64 " frames of silence for allocation %p",
                 delta, mAllocations[i].mHandle.get()));
 
     // This assertion fails when we append silence here in the same iteration
     // as there were real audio samples already appended by the audio callback.
-    // Note that this is exempted until live samples and a subsequent chunk of silence have been appended to the track. This will cover cases like:
+    // Note that this is exempted until live samples and a subsequent chunk of
+    // silence have been appended to the track. This will cover cases like:
     // - After Start(), there is silence (maybe multiple times) appended before
     //   the first audio callback.
     // - After Start(), there is real data (maybe multiple times) appended
     //   before the first graph iteration.
     // And other combinations of order of audio sample sources.
     MOZ_ASSERT_IF(
       mAllocations[i].mEnabled &&
       mAllocations[i].mLiveFramesAppended &&
@@ -845,22 +905,24 @@ MediaEngineWebRTCMicrophoneSource::Pull(
   }
 
   AudioSegment audio;
   audio.AppendNullData(delta);
   aStream->AppendToTrack(aTrackID, &audio);
 }
 
 void
-MediaEngineWebRTCMicrophoneSource::NotifyOutputData(MediaStreamGraph* aGraph,
+MediaEngineWebRTCMicrophoneSource::NotifyOutputData(MediaStreamGraphImpl* aGraph,
                                                     AudioDataValue* aBuffer,
                                                     size_t aFrames,
                                                     TrackRate aRate,
                                                     uint32_t aChannels)
 {
+  MOZ_ASSERT(aGraph->CurrentDriver()->OnThread());
+
   if (!mPacketizerOutput ||
       mPacketizerOutput->PacketSize() != aRate/100u ||
       mPacketizerOutput->Channels() != aChannels) {
     // It's ok to drop the audio still in the packetizer here: if this changes,
     // we changed devices or something.
     mPacketizerOutput =
       new AudioPacketizer<AudioDataValue, float>(aRate/100, aChannels);
   }
@@ -938,23 +1000,23 @@ MediaEngineWebRTCMicrophoneSource::Notif
                                              deinterleavedPacketDataChannelPointers.Elements());
 
     MOZ_ASSERT(!err, "Could not process the reverse stream.");
   }
 }
 
 // Only called if we're not in passthrough mode
 void
-MediaEngineWebRTCMicrophoneSource::PacketizeAndProcess(MediaStreamGraph* aGraph,
+MediaEngineWebRTCMicrophoneSource::PacketizeAndProcess(MediaStreamGraphImpl* aGraph,
                                                        const AudioDataValue* aBuffer,
                                                        size_t aFrames,
                                                        TrackRate aRate,
                                                        uint32_t aChannels)
 {
-  MOZ_ASSERT(!PassThrough(), "This should be bypassed when in PassThrough mode.");
+  MOZ_ASSERT(!PassThrough(aGraph), "This should be bypassed when in PassThrough mode.");
   size_t offset = 0;
 
   if (!mPacketizerInput ||
       mPacketizerInput->PacketSize() != aRate/100u ||
       mPacketizerInput->Channels() != aChannels) {
     // It's ok to drop the audio still in the packetizer here.
     mPacketizerInput =
       new AudioPacketizer<AudioDataValue, float>(aRate/100, aChannels);
@@ -1066,43 +1128,31 @@ MediaEngineWebRTCMicrophoneSource::Packe
                            processedOutputChannelPointersConst,
                            mPacketizerInput->PacketSize(),
                            allocation.mPrincipal);
       allocation.mStream->AppendToTrack(allocation.mTrackID, &segment);
     }
   }
 }
 
-bool
-MediaEngineWebRTCMicrophoneSource::PassThrough() const
-{
-  return mSkipProcessing;
-}
-
-void
-MediaEngineWebRTCMicrophoneSource::SetPassThrough(bool aPassThrough)
-{
-  mSkipProcessing = aPassThrough;
-}
-
 template<typename T>
 void
 MediaEngineWebRTCMicrophoneSource::InsertInGraph(const T* aBuffer,
                                                  size_t aFrames,
                                                  uint32_t aChannels)
 {
   MutexAutoLock lock(mMutex);
 
   if (mState != kStarted) {
     return;
   }
 
   if (MOZ_LOG_TEST(AudioLogModule(), LogLevel::Debug)) {
     mTotalFrames += aFrames;
-    if (!mAllocations.IsEmpty() && mAllocations[0].mStream &&
+    if (mAllocations[0].mStream &&
         mTotalFrames > mLastLogFrames +
                        mAllocations[0].mStream->GraphRate()) { // ~ 1 second
       MOZ_LOG(AudioLogModule(), LogLevel::Debug,
               ("%p: Inserting %zu samples into graph, total frames = %" PRIu64,
                (void*)this, aFrames, mTotalFrames));
       mLastLogFrames = mTotalFrames;
     }
   }
@@ -1175,26 +1225,40 @@ MediaEngineWebRTCMicrophoneSource::Inser
 
     allocation.mStream->AppendToTrack(allocation.mTrackID, &segment);
   }
 }
 
 // Called back on GraphDriver thread!
 // Note this can be called back after ::Shutdown()
 void
-MediaEngineWebRTCMicrophoneSource::NotifyInputData(MediaStreamGraph* aGraph,
+MediaEngineWebRTCMicrophoneSource::NotifyInputData(MediaStreamGraphImpl* aGraph,
                                                    const AudioDataValue* aBuffer,
                                                    size_t aFrames,
                                                    TrackRate aRate,
                                                    uint32_t aChannels)
 {
+  MOZ_ASSERT(aGraph->CurrentDriver()->OnThread());
   TRACE_AUDIO_CALLBACK();
+
+  {
+    MutexAutoLock lock(mMutex);
+    if (mAllocations.IsEmpty()) {
+      // This can happen because mAllocations is not yet using message passing, and
+      // is access both on the media manager thread and the MSG thread. This is to
+      // be fixed soon.
+      // When deallocating, the listener is removed via message passing, while the
+      // allocation is removed immediately, so there can be a few iterations where
+      // we need to return early here.
+      return;
+    }
+  }
   // If some processing is necessary, packetize and insert in the WebRTC.org
   // code. Otherwise, directly insert the mic data in the MSG, bypassing all processing.
-  if (PassThrough()) {
+  if (PassThrough(aGraph)) {
     InsertInGraph<AudioDataValue>(aBuffer, aFrames, aChannels);
   } else {
     PacketizeAndProcess(aGraph, aBuffer, aFrames, aRate, aChannels);
   }
 }
 
 #define ResetProcessingIfNeeded(_processing)                        \
 do {                                                                \
@@ -1213,36 +1277,38 @@ do {                                    
       #_processing " on device change.");                           \
       return;                                                       \
     }                                                               \
                                                                     \
   }                                                                 \
 }  while(0)
 
 void
-MediaEngineWebRTCMicrophoneSource::DeviceChanged()
+MediaEngineWebRTCMicrophoneSource::DeviceChanged(MediaStreamGraphImpl* aGraph)
 {
+  MOZ_ASSERT(aGraph->CurrentDriver()->OnThread());
   // Reset some processing
   ResetProcessingIfNeeded(gain_control);
   ResetProcessingIfNeeded(echo_cancellation);
   ResetProcessingIfNeeded(noise_suppression);
 }
 
 void
+MediaEngineWebRTCMicrophoneSource::Disconnect(MediaStreamGraphImpl* aGraph)
+{
+  // This method is just for asserts.
+  MOZ_ASSERT(aGraph->CurrentDriver()->OnThread());
+  MOZ_ASSERT(!mListener);
+}
+
+void
 MediaEngineWebRTCMicrophoneSource::Shutdown()
 {
   AssertIsOnOwningThread();
 
-  if (mListener) {
-    // breaks a cycle, since the WebRTCAudioDataListener has a RefPtr to us
-    mListener->Shutdown();
-    // Don't release the webrtc.org pointers yet until the Listener is (async) shutdown
-    mListener = nullptr;
-  }
-
   if (mState == kStarted) {
     for (const Allocation& allocation : mAllocations) {
       if (allocation.mEnabled) {
         Stop(allocation.mHandle);
       }
     }
     MOZ_ASSERT(mState == kStopped);
   }
--- a/dom/performance/PerformanceMainThread.cpp
+++ b/dom/performance/PerformanceMainThread.cpp
@@ -315,17 +315,17 @@ PerformanceMainThread::EnsureDocEntry()
 {
   if (!mDocEntry && nsContentUtils::IsPerformanceNavigationTimingEnabled()) {
 
     UniquePtr<PerformanceTimingData> timing(
       new PerformanceTimingData(mChannel, nullptr, 0));
 
     nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
     if (httpChannel) {
-      timing->SetPropertiesFromHttpChannel(httpChannel);
+      timing->SetPropertiesFromHttpChannel(httpChannel, mChannel);
     }
 
     nsAutoString name;
     GetURLSpecFromChannel(mChannel, name);
 
     mDocEntry = new PerformanceNavigationTiming(std::move(timing), this, name);
   }
 }
--- a/dom/performance/PerformanceTiming.cpp
+++ b/dom/performance/PerformanceTiming.cpp
@@ -202,40 +202,41 @@ PerformanceTimingData::PerformanceTiming
   }
 
   // The aHttpChannel argument is null if this PerformanceTiming object is
   // being used for navigation timing (which is only relevant for documents).
   // It has a non-null value if this PerformanceTiming object is being used
   // for resource timing, which can include document loads, both toplevel and
   // in subframes, and resources linked from a document.
   if (aHttpChannel) {
-    mTimingAllowed = CheckAllowedOrigin(aHttpChannel, aChannel);
-    bool redirectsPassCheck = false;
-    aChannel->GetAllRedirectsPassTimingAllowCheck(&redirectsPassCheck);
-    mReportCrossOriginRedirect = mTimingAllowed && redirectsPassCheck;
-
-    SetPropertiesFromHttpChannel(aHttpChannel);
+    SetPropertiesFromHttpChannel(aHttpChannel, aChannel);
   }
 }
 
 void
-PerformanceTimingData::SetPropertiesFromHttpChannel(nsIHttpChannel* aHttpChannel)
+PerformanceTimingData::SetPropertiesFromHttpChannel(nsIHttpChannel* aHttpChannel,
+                                                    nsITimedChannel* aChannel)
 {
   MOZ_ASSERT(aHttpChannel);
 
   nsAutoCString protocol;
   Unused << aHttpChannel->GetProtocolVersion(protocol);
   mNextHopProtocol = NS_ConvertUTF8toUTF16(protocol);
 
   Unused << aHttpChannel->GetEncodedBodySize(&mEncodedBodySize);
   Unused << aHttpChannel->GetTransferSize(&mTransferSize);
   Unused << aHttpChannel->GetDecodedBodySize(&mDecodedBodySize);
   if (mDecodedBodySize == 0) {
     mDecodedBodySize = mEncodedBodySize;
   }
+
+  mTimingAllowed = CheckAllowedOrigin(aHttpChannel, aChannel);
+  bool redirectsPassCheck = false;
+  aChannel->GetAllRedirectsPassTimingAllowCheck(&redirectsPassCheck);
+  mReportCrossOriginRedirect = mTimingAllowed && redirectsPassCheck;
 }
 
 PerformanceTiming::~PerformanceTiming()
 {
 }
 
 DOMHighResTimeStamp
 PerformanceTimingData::FetchStartHighRes(Performance* aPerformance)
--- a/dom/performance/PerformanceTiming.h
+++ b/dom/performance/PerformanceTiming.h
@@ -36,17 +36,18 @@ public:
          nsAString& aInitiatorType,
          nsAString& aEntryName);
 
   PerformanceTimingData(nsITimedChannel* aChannel,
                         nsIHttpChannel* aHttpChannel,
                         DOMHighResTimeStamp aZeroTime);
 
   void
-  SetPropertiesFromHttpChannel(nsIHttpChannel* aHttpChannel);
+  SetPropertiesFromHttpChannel(nsIHttpChannel* aHttpChannel,
+                               nsITimedChannel* aChannel);
 
   bool IsInitialized() const
   {
     return mInitialized;
   }
 
   const nsString& NextHopProtocol() const
   {
--- a/gfx/2d/Factory.cpp
+++ b/gfx/2d/Factory.cpp
@@ -616,25 +616,27 @@ Factory::CreateScaledFontForNativeFont(c
 
 already_AddRefed<NativeFontResource>
 Factory::CreateNativeFontResource(uint8_t *aData, uint32_t aSize, BackendType aBackendType, FontType aFontType, void* aFontContext)
 {
   switch (aFontType) {
 #ifdef WIN32
   case FontType::DWRITE:
     {
-      bool needsCairo = aBackendType == BackendType::CAIRO ||
-                        aBackendType == BackendType::SKIA;
+      bool needsCairo = aBackendType == BackendType::CAIRO;
       return NativeFontResourceDWrite::Create(aData, aSize, needsCairo);
     }
   case FontType::GDI:
     return NativeFontResourceGDI::Create(aData, aSize);
 #elif defined(XP_DARWIN)
   case FontType::MAC:
-    return NativeFontResourceMac::Create(aData, aSize);
+    {
+      bool needsCairo = aBackendType == BackendType::CAIRO;
+      return NativeFontResourceMac::Create(aData, aSize, needsCairo);
+    }
 #elif defined(MOZ_WIDGET_GTK)
   case FontType::FONTCONFIG:
     return NativeFontResourceFontconfig::Create(aData, aSize,
                                                 static_cast<FT_Library>(aFontContext));
 #elif defined(MOZ_WIDGET_ANDROID)
   case FontType::FREETYPE:
     return NativeFontResourceFreeType::Create(aData, aSize,
                                               static_cast<FT_Library>(aFontContext));
--- a/gfx/2d/NativeFontResourceMac.cpp
+++ b/gfx/2d/NativeFontResourceMac.cpp
@@ -16,17 +16,17 @@
 
 #include "nsCocoaFeatures.h"
 
 namespace mozilla {
 namespace gfx {
 
 /* static */
 already_AddRefed<NativeFontResourceMac>
-NativeFontResourceMac::Create(uint8_t *aFontData, uint32_t aDataLength)
+NativeFontResourceMac::Create(uint8_t *aFontData, uint32_t aDataLength, bool aNeedsCairo)
 {
   // copy font data
   CFDataRef data = CFDataCreate(kCFAllocatorDefault, aFontData, aDataLength);
   if (!data) {
     return nullptr;
   }
 
   // create a provider
@@ -42,25 +42,25 @@ NativeFontResourceMac::Create(uint8_t *a
   CGDataProviderRelease(provider);
 
   if (!fontRef) {
     return nullptr;
   }
 
   // passes ownership of fontRef to the NativeFontResourceMac instance
   RefPtr<NativeFontResourceMac> fontResource =
-    new NativeFontResourceMac(fontRef);
+    new NativeFontResourceMac(fontRef, aNeedsCairo);
 
   return fontResource.forget();
 }
 
 already_AddRefed<UnscaledFont>
 NativeFontResourceMac::CreateUnscaledFont(uint32_t aIndex,
                                           const uint8_t* aInstanceData,
                                           uint32_t aInstanceDataLength)
 {
-  RefPtr<UnscaledFont> unscaledFont = new UnscaledFontMac(mFontRef, true);
+  RefPtr<UnscaledFont> unscaledFont = new UnscaledFontMac(mFontRef, true, mNeedsCairo);
 
   return unscaledFont.forget();
 }
 
 } // gfx
 } // mozilla
--- a/gfx/2d/NativeFontResourceMac.h
+++ b/gfx/2d/NativeFontResourceMac.h
@@ -15,30 +15,34 @@ namespace mozilla {
 namespace gfx {
 
 class NativeFontResourceMac final : public NativeFontResource
 {
 public:
   MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(NativeFontResourceMac, override)
 
   static already_AddRefed<NativeFontResourceMac>
-    Create(uint8_t *aFontData, uint32_t aDataLength);
+    Create(uint8_t *aFontData, uint32_t aDataLength, bool aNeedsCairo);
 
   already_AddRefed<UnscaledFont>
     CreateUnscaledFont(uint32_t aIndex,
                        const uint8_t* aInstanceData,
                        uint32_t aInstanceDataLength) final;
 
   ~NativeFontResourceMac()
   {
     CFRelease(mFontRef);
   }
 
 private:
-  explicit NativeFontResourceMac(CGFontRef aFontRef) : mFontRef(aFontRef) {}
+  NativeFontResourceMac(CGFontRef aFontRef, bool aNeedsCairo)
+    : mFontRef(aFontRef)
+    , mNeedsCairo(aNeedsCairo)
+  {}
 
   CGFontRef mFontRef;
+  bool mNeedsCairo;
 };
 
 } // gfx
 } // mozilla
 
 #endif // mozilla_gfx_NativeFontResourceMac_h
--- a/gfx/2d/ScaledFontMac.cpp
+++ b/gfx/2d/ScaledFontMac.cpp
@@ -590,17 +590,17 @@ UnscaledFontMac::CreateScaledFont(Float 
     if (varFont) {
       fontRef = varFont;
     }
   }
 
   RefPtr<ScaledFontMac> scaledFont =
     new ScaledFontMac(fontRef, this, aGlyphSize, fontRef != mFont);
 
-  if (!scaledFont->PopulateCairoScaledFont()) {
+  if (mNeedsCairo && !scaledFont->PopulateCairoScaledFont()) {
     gfxWarning() << "Unable to create cairo scaled Mac font.";
     return nullptr;
   }
 
   return scaledFont.forget();
 }
 
 #ifdef USE_CAIRO_SCALED_FONT
--- a/gfx/2d/UnscaledFontMac.h
+++ b/gfx/2d/UnscaledFontMac.h
@@ -18,19 +18,22 @@
 
 namespace mozilla {
 namespace gfx {
 
 class UnscaledFontMac final : public UnscaledFont
 {
 public:
   MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(UnscaledFontMac, override)
-  explicit UnscaledFontMac(CGFontRef aFont, bool aIsDataFont = false)
+  explicit UnscaledFontMac(CGFontRef aFont,
+                           bool aIsDataFont = false,
+                           bool aNeedsCairo = false)
     : mFont(aFont)
     , mIsDataFont(aIsDataFont)
+    , mNeedsCairo(aNeedsCairo)
   {
     CFRetain(mFont);
   }
   ~UnscaledFontMac()
   {
     CFRelease(mFont);
   }
 
@@ -54,15 +57,16 @@ public:
                                uint32_t aVariationCount,
                                const FontVariation* aVariations);
 
   bool GetWRFontDescriptor(WRFontDescriptorOutput aCb, void* aBaton) override;
 
 private:
   CGFontRef mFont;
   bool mIsDataFont;
+  bool mNeedsCairo;
 };
 
 } // namespace gfx
 } // namespace mozilla
 
 #endif /* MOZILLA_GFX_UNSCALEDFONTMAC_H_ */
 
--- a/gfx/skia/skia/src/gpu/GrColorSpaceXform.cpp
+++ b/gfx/skia/skia/src/gpu/GrColorSpaceXform.cpp
@@ -176,17 +176,17 @@ bool GrColorSpaceXform::Equals(const GrC
         return false;
     }
 
     if (SkToBool(a->fFlags & kApplyTransferFn_Flag) &&
         0 != memcmp(&a->fSrcTransferFn, &b->fSrcTransferFn, sizeof(SkColorSpaceTransferFn))) {
         return false;
     }
 
-    if (SkToBool(a->fFlags && kApplyGamutXform_Flag) && a->fGamutXform != b->fGamutXform) {
+    if (SkToBool(a->fFlags & kApplyGamutXform_Flag) && a->fGamutXform != b->fGamutXform) {
         return false;
     }
 
     return true;
 }
 
 GrColor4f GrColorSpaceXform::unclampedXform(const GrColor4f& srcColor) {
     // This transform step should only happen with textures (not CPU xform of individual values)
--- a/js/public/RefCounted.h
+++ b/js/public/RefCounted.h
@@ -52,32 +52,38 @@ class RefCounted
 
   private:
     mutable MozRefCountType mRefCnt;
 };
 
 template <typename T>
 class AtomicRefCounted
 {
-    static const MozRefCountType DEAD = 0xffffdead;
+    // On 64-bit systems, if the refcount type is small (say, 32 bits), there's
+    // a risk that it could overflow.  So require it to be large enough.
+
+    static_assert(sizeof(MozRefCountType) == sizeof(uintptr_t),
+                  "You're at risk for ref count overflow.");
+
+    static const MozRefCountType DEAD = ~MozRefCountType(0xffff) | 0xdead;
 
   protected:
     AtomicRefCounted() : mRefCnt(0) {}
     ~AtomicRefCounted() { MOZ_ASSERT(mRefCnt == DEAD); }
 
   public:
     void AddRef() const
     {
-        MOZ_ASSERT(int32_t(mRefCnt) >= 0);
         ++mRefCnt;
+        MOZ_ASSERT(mRefCnt != DEAD);
     }
 
     void Release() const
     {
-        MOZ_ASSERT(int32_t(mRefCnt) > 0);
+        MOZ_ASSERT(mRefCnt != 0);
         MozRefCountType cnt = --mRefCnt;
         if (0 == cnt) {
 #ifdef DEBUG
             mRefCnt = DEAD;
 #endif
             js_delete(const_cast<T*>(static_cast<const T*>(this)));
         }
     }
--- a/js/src/builtin/Array.cpp
+++ b/js/src/builtin/Array.cpp
@@ -521,17 +521,17 @@ DeleteArrayElement(JSContext* cx, Handle
     {
         ArrayObject* aobj = &obj->as<ArrayObject>();
         if (index <= UINT32_MAX) {
             uint32_t idx = uint32_t(index);
             if (idx < aobj->getDenseInitializedLength()) {
                 if (!aobj->maybeCopyElementsForWrite(cx))
                     return false;
                 if (idx+1 == aobj->getDenseInitializedLength()) {
-                    aobj->setDenseInitializedLength(idx);
+                    aobj->setDenseInitializedLengthMaybeNonExtensible(cx, idx);
                 } else {
                     aobj->markDenseElementsNotPacked(cx);
                     aobj->setDenseElement(idx, MagicValue(JS_ELEMENTS_HOLE));
                 }
                 if (!SuppressDeletedElement(cx, obj, idx))
                     return false;
             }
         }
@@ -785,17 +785,17 @@ js::ArraySetLength(JSContext* cx, Handle
         if (!arr->isIndexed() && !MaybeInIteration(arr, cx) && !arr->denseElementsAreSealed()) {
             if (!arr->maybeCopyElementsForWrite(cx))
                 return false;
 
             uint32_t oldCapacity = arr->getDenseCapacity();
             uint32_t oldInitializedLength = arr->getDenseInitializedLength();
             MOZ_ASSERT(oldCapacity >= oldInitializedLength);
             if (oldInitializedLength > newLen)
-                arr->setDenseInitializedLength(newLen);
+                arr->setDenseInitializedLengthMaybeNonExtensible(cx, newLen);
             if (oldCapacity > newLen)
                 arr->shrinkElements(cx, newLen);
 
             // We've done the work of deleting any dense elements needing
             // deletion, and there are no sparse elements.  Thus we can skip
             // straight to defining the length.
             break;
         }
@@ -925,16 +925,19 @@ js::ArraySetLength(JSContext* cx, Handle
     // so that all element fields remain properly synchronized.
 
     // Trim the initialized length, if needed, to preserve the <= length
     // invariant.  (Capacity was already reduced during element deletion, if
     // necessary.)
     ObjectElements* header = arr->getElementsHeader();
     header->initializedLength = Min(header->initializedLength, newLen);
 
+    if (!arr->isExtensible())
+        arr->shrinkCapacityToInitializedLength(cx);
+
     if (attrs & JSPROP_READONLY)
         arr->setNonWritableLength(cx);
 
     if (!succeeded)
         return result.fail(JSMSG_CANT_TRUNCATE_ARRAY);
 
     return result.succeed();
 }
@@ -2390,28 +2393,29 @@ js::ArrayShiftMoveElements(NativeObject*
 
     if (!obj->tryShiftDenseElements(1))
         obj->moveDenseElementsNoPreBarrier(0, 1, initlen - 1);
 }
 
 static inline void
 SetInitializedLength(JSContext* cx, NativeObject* obj, size_t initlen)
 {
+    MOZ_ASSERT(obj->isExtensible());
+
     size_t oldInitlen = obj->getDenseInitializedLength();
     obj->setDenseInitializedLength(initlen);
     if (initlen < oldInitlen)
         obj->shrinkElements(cx, initlen);
 }
 
 static DenseElementResult
 MoveDenseElements(JSContext* cx, NativeObject* obj, uint32_t dstStart, uint32_t srcStart,
                   uint32_t length)
 {
-    if (!obj->isExtensible())
-        return DenseElementResult::Incomplete;
+    MOZ_ASSERT(obj->isExtensible());
 
     if (!obj->maybeCopyElementsForWrite(cx))
         return DenseElementResult::Failure;
     obj->moveDenseElements(dstStart, srcStart, length);
 
     return DenseElementResult::Success;
 }
 
@@ -2419,16 +2423,19 @@ static DenseElementResult
 ArrayShiftDenseKernel(JSContext* cx, HandleObject obj, MutableHandleValue rval)
 {
     if (!IsPackedArray(obj) && ObjectMayHaveExtraIndexedProperties(obj))
         return DenseElementResult::Incomplete;
 
     if (MaybeInIteration(obj, cx))
         return DenseElementResult::Incomplete;
 
+    if (!obj->as<NativeObject>().isExtensible())
+        return DenseElementResult::Incomplete;
+
     size_t initlen = obj->as<NativeObject>().getDenseInitializedLength();
     if (initlen == 0)
         return DenseElementResult::Incomplete;
 
     rval.set(obj->as<NativeObject>().getDenseElement(0));
     if (rval.isMagic(JS_ELEMENTS_HOLE))
         rval.setUndefined();
 
--- a/js/src/builtin/TypedObject.cpp
+++ b/js/src/builtin/TypedObject.cpp
@@ -542,16 +542,17 @@ ArrayMetaTypeDescr::create(JSContext* cx
 
     obj->initReservedSlot(JS_DESCR_SLOT_KIND, Int32Value(ArrayTypeDescr::Kind));
     obj->initReservedSlot(JS_DESCR_SLOT_STRING_REPR, StringValue(stringRepr));
     obj->initReservedSlot(JS_DESCR_SLOT_ALIGNMENT, Int32Value(elementType->alignment()));
     obj->initReservedSlot(JS_DESCR_SLOT_SIZE, Int32Value(size));
     obj->initReservedSlot(JS_DESCR_SLOT_OPAQUE, BooleanValue(elementType->opaque()));
     obj->initReservedSlot(JS_DESCR_SLOT_ARRAY_ELEM_TYPE, ObjectValue(*elementType));
     obj->initReservedSlot(JS_DESCR_SLOT_ARRAY_LENGTH, Int32Value(length));
+    obj->initReservedSlot(JS_DESCR_SLOT_FLAGS, Int32Value(JS_DESCR_FLAG_ALLOW_CONSTRUCT));
 
     RootedValue elementTypeVal(cx, ObjectValue(*elementType));
     if (!DefineDataProperty(cx, obj, cx->names().elementType, elementTypeVal,
                             JSPROP_READONLY | JSPROP_PERMANENT))
     {
         return nullptr;
     }
 
@@ -678,17 +679,17 @@ js::IsTypedObjectArray(JSObject& obj)
 static const ClassOps StructTypeDescrClassOps = {
     nullptr, /* addProperty */
     nullptr, /* delProperty */
     nullptr, /* enumerate */
     nullptr, /* newEnumerate */
     nullptr, /* resolve */
     nullptr, /* mayResolve */
     TypeDescr::finalize,
-    nullptr, /* call */
+    StructTypeDescr::call,
     nullptr, /* hasInstance */
     TypedObject::construct
 };
 
 const Class StructTypeDescr::class_ = {
     "StructType",
     JSCLASS_HAS_RESERVED_SLOTS(JS_DESCR_SLOTS) | JSCLASS_BACKGROUND_FINALIZE,
     &StructTypeDescrClassOps
@@ -762,16 +763,18 @@ StructMetaTypeDescr::create(JSContext* c
         return nullptr;
 
     // Iterate through each field. Collect values for the various
     // vectors below and also track total size and alignment. Be wary
     // of overflow!
     AutoValueVector fieldTypeObjs(cx); // Type descriptor of each field.
     bool opaque = false;               // Opacity of struct.
 
+    Vector<bool> fieldMutabilities(cx);
+
     RootedValue fieldTypeVal(cx);
     RootedId id(cx);
     Rooted<TypeDescr*> fieldType(cx);
 
     for (unsigned int i = 0; i < ids.length(); i++) {
         id = ids[i];
 
         // Check that all the property names are non-numeric strings.
@@ -791,38 +794,46 @@ StructMetaTypeDescr::create(JSContext* c
             ReportCannotConvertTo(cx, fieldTypeVal, "StructType field specifier");
             return nullptr;
         }
 
         // Collect field type object
         if (!fieldTypeObjs.append(ObjectValue(*fieldType)))
             return nullptr;
 
+        // Along this path everything is mutable
+        if (!fieldMutabilities.append(true))
+            return nullptr;
+
         // Struct is opaque if any field is opaque
         if (fieldType->opaque())
             opaque = true;
     }
 
     RootedObject structTypePrototype(cx, GetPrototype(cx, metaTypeDescr));
     if (!structTypePrototype)
         return nullptr;
 
-    return createFromArrays(cx, structTypePrototype, opaque, ids, fieldTypeObjs);
+    return createFromArrays(cx, structTypePrototype, opaque, /* allowConstruct= */ true, ids,
+                            fieldTypeObjs, fieldMutabilities);
 }
 
 /* static */ StructTypeDescr*
 StructMetaTypeDescr::createFromArrays(JSContext* cx,
                                       HandleObject structTypePrototype,
                                       bool opaque,
+                                      bool allowConstruct,
                                       AutoIdVector& ids,
-                                      AutoValueVector& fieldTypeObjs)
+                                      AutoValueVector& fieldTypeObjs,
+                                      Vector<bool>& fieldMutabilities)
 {
     StringBuffer stringBuffer(cx);     // Canonical string repr
     AutoValueVector fieldNames(cx);    // Name of each field.
     AutoValueVector fieldOffsets(cx);  // Offset of each field field.
+    AutoValueVector fieldMuts(cx);     // Mutability of each field.
     RootedObject userFieldOffsets(cx); // User-exposed {f:offset} object
     RootedObject userFieldTypes(cx);   // User-exposed {f:descr} object.
     Layout layout;                     // Field offsetter
 
     userFieldOffsets = NewBuiltinClassInstance<PlainObject>(cx, TenuredObject);
     if (!userFieldOffsets)
         return nullptr;
 
@@ -867,16 +878,19 @@ StructMetaTypeDescr::createFromArrays(JS
         if (!offset.isValid()) {
             JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPEDOBJECT_TOO_BIG);
             return nullptr;
         }
         MOZ_ASSERT(offset.value() >= 0);
         if (!fieldOffsets.append(Int32Value(offset.value())))
             return nullptr;
 
+        if (!fieldMuts.append(BooleanValue(fieldMutabilities[i])))
+            return nullptr;
+
         // userFieldOffsets[id] = offset
         RootedValue offsetValue(cx, Int32Value(offset.value()));
         if (!DefineDataProperty(cx, userFieldOffsets, id, offsetValue,
                                 JSPROP_READONLY | JSPROP_PERMANENT))
         {
             return nullptr;
         }
     }
@@ -903,16 +917,18 @@ StructMetaTypeDescr::createFromArrays(JS
     if (!descr)
         return nullptr;
 
     descr->initReservedSlot(JS_DESCR_SLOT_KIND, Int32Value(type::Struct));
     descr->initReservedSlot(JS_DESCR_SLOT_STRING_REPR, StringValue(stringRepr));
     descr->initReservedSlot(JS_DESCR_SLOT_ALIGNMENT, Int32Value(AssertedCast<int32_t>(alignment)));
     descr->initReservedSlot(JS_DESCR_SLOT_SIZE, Int32Value(totalSize.value()));
     descr->initReservedSlot(JS_DESCR_SLOT_OPAQUE, BooleanValue(opaque));
+    descr->initReservedSlot(JS_DESCR_SLOT_FLAGS,
+                            Int32Value(allowConstruct ? JS_DESCR_FLAG_ALLOW_CONSTRUCT : 0));
 
     // Construct for internal use an array with the name for each field.
     {
         RootedObject fieldNamesVec(cx);
         fieldNamesVec = NewDenseCopiedArray(cx, fieldNames.length(),
                                             fieldNames.begin(), nullptr,
                                             TenuredObject);
         if (!fieldNamesVec)
@@ -935,17 +951,29 @@ StructMetaTypeDescr::createFromArrays(JS
         fieldOffsetsVec = NewDenseCopiedArray(cx, fieldOffsets.length(),
                                               fieldOffsets.begin(), nullptr,
                                               TenuredObject);
         if (!fieldOffsetsVec)
             return nullptr;
         descr->initReservedSlot(JS_DESCR_SLOT_STRUCT_FIELD_OFFSETS, ObjectValue(*fieldOffsetsVec));
     }
 
+    // Construct for internal use an array with the mutability for each field.
+    {
+        RootedObject fieldMutsVec(cx);
+        fieldMutsVec = NewDenseCopiedArray(cx, fieldMuts.length(),
+                                           fieldMuts.begin(), nullptr,
+                                           TenuredObject);
+        if (!fieldMutsVec)
+            return nullptr;
+        descr->initReservedSlot(JS_DESCR_SLOT_STRUCT_FIELD_MUTS, ObjectValue(*fieldMutsVec));
+    }
+
     // Create data properties fieldOffsets and fieldTypes
+    // TODO: Probably also want to track mutability here, but not important yet.
     if (!FreezeObject(cx, userFieldOffsets))
         return nullptr;
     if (!FreezeObject(cx, userFieldTypes))
         return nullptr;
     RootedValue userFieldOffsetsValue(cx, ObjectValue(*userFieldOffsets));
     if (!DefineDataProperty(cx, descr, cx->names().fieldOffsets, userFieldOffsetsValue,
                             JSPROP_READONLY | JSPROP_PERMANENT))
     {
@@ -1036,24 +1064,41 @@ StructTypeDescr::fieldName(size_t index)
 size_t
 StructTypeDescr::fieldOffset(size_t index) const
 {
     ArrayObject& fieldOffsets = fieldInfoObject(JS_DESCR_SLOT_STRUCT_FIELD_OFFSETS);
     MOZ_ASSERT(index < fieldOffsets.getDenseInitializedLength());
     return AssertedCast<size_t>(fieldOffsets.getDenseElement(index).toInt32());
 }
 
+bool
+StructTypeDescr::fieldIsMutable(size_t index) const
+{
+    ArrayObject& fieldMuts = fieldInfoObject(JS_DESCR_SLOT_STRUCT_FIELD_MUTS);
+    MOZ_ASSERT(index < fieldMuts.getDenseInitializedLength());
+    return fieldMuts.getDenseElement(index).toBoolean();
+}
+
 TypeDescr&
 StructTypeDescr::fieldDescr(size_t index) const
 {
     ArrayObject& fieldDescrs = fieldInfoObject(JS_DESCR_SLOT_STRUCT_FIELD_TYPES);
     MOZ_ASSERT(index < fieldDescrs.getDenseInitializedLength());
     return fieldDescrs.getDenseElement(index).toObject().as<TypeDescr>();
 }
 
+bool
+StructTypeDescr::call(JSContext* cx, unsigned argc, Value* vp)
+{
+    // A structure type is a constructor and hence callable, but at present the
+    // call always throws.
+    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPEDOBJECT_STRUCTTYPE_NOT_CALLABLE);
+    return false;
+}
+
 /******************************************************************************
  * Creating the TypedObject "module"
  *
  * We create one global, `TypedObject`, which contains the following
  * members:
  *
  * 1. uint8, uint16, etc
  * 2. ArrayType
@@ -1119,16 +1164,17 @@ DefineSimpleTypeDescr(JSContext* cx,
         return false;
 
     descr->initReservedSlot(JS_DESCR_SLOT_KIND, Int32Value(T::Kind));
     descr->initReservedSlot(JS_DESCR_SLOT_STRING_REPR, StringValue(className));
     descr->initReservedSlot(JS_DESCR_SLOT_ALIGNMENT, Int32Value(T::alignment(type)));
     descr->initReservedSlot(JS_DESCR_SLOT_SIZE, Int32Value(AssertedCast<int32_t>(T::size(type))));
     descr->initReservedSlot(JS_DESCR_SLOT_OPAQUE, BooleanValue(T::Opaque));
     descr->initReservedSlot(JS_DESCR_SLOT_TYPE, Int32Value(int32_t(type)));
+    descr->initReservedSlot(JS_DESCR_SLOT_FLAGS, Int32Value(0));
 
     if (!CreateUserSizeAndAlignmentProperties(cx, descr))
         return false;
 
     if (!JS_DefineFunctions(cx, descr, T::typeObjectMethods))
         return false;
 
     // Create the typed prototype for the scalar type. This winds up
@@ -1860,16 +1906,22 @@ TypedObject::obj_setProperty(JSContext* 
 
       case type::Struct: {
         Rooted<StructTypeDescr*> descr(cx, &typedObj->typeDescr().as<StructTypeDescr>());
 
         size_t fieldIndex;
         if (!descr->fieldIndex(id, &fieldIndex))
             break;
 
+        if (!descr->fieldIsMutable(fieldIndex)) {
+            JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                                      JSMSG_TYPEDOBJECT_SETTING_IMMUTABLE);
+            return false;
+        }
+
         if (!receiver.isObject() || obj != &receiver.toObject())
             return SetPropertyByDefining(cx, id, v, receiver, result);
 
         size_t offset = descr->fieldOffset(fieldIndex);
         Rooted<TypeDescr*> fieldType(cx, &descr->fieldDescr(fieldIndex));
         RootedAtom fieldName(cx, &descr->fieldName(fieldIndex));
         if (!ConvertAndCopyTo(cx, fieldType, typedObj, offset, fieldName, v))
             return false;
@@ -2230,32 +2282,40 @@ TypedObject::construct(JSContext* cx, un
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     MOZ_ASSERT(args.callee().is<TypeDescr>());
     Rooted<TypeDescr*> callee(cx, &args.callee().as<TypeDescr>());
 
     MOZ_ASSERT(cx->realm() == callee->realm());
 
+    // Types created by Wasm may not be constructible from JS due to field types
+    // that are not expressible in the current TypedObject system.
+    if (callee->is<ComplexTypeDescr>() && !callee->as<ComplexTypeDescr>().allowConstruct()) {
+        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TYPEDOBJECT_NOT_CONSTRUCTIBLE);
+        return false;
+    }
+
     // Typed object constructors are overloaded in two ways:
     //
     //   new TypeObj()
     //   new TypeObj(data)
 
     // Zero argument constructor:
     if (args.length() == 0) {
         Rooted<TypedObject*> obj(cx, createZeroed(cx, callee));
         if (!obj)
             return false;
         args.rval().setObject(*obj);
         return true;
     }
 
     // Data constructor.
     if (args[0].isObject()) {
+
         // Create the typed object.
         Rooted<TypedObject*> obj(cx, createZeroed(cx, callee));
         if (!obj)
             return false;
 
         // Initialize from `arg`.
         if (!ConvertAndCopyTo(cx, obj, args[0]))
             return false;
--- a/js/src/builtin/TypedObject.h
+++ b/js/src/builtin/TypedObject.h
@@ -323,16 +323,20 @@ class ReferenceTypeDescr : public Simple
 class ComplexTypeDescr : public TypeDescr
 {
   public:
     // Returns the prototype that instances of this type descriptor
     // will have.
     TypedProto& instancePrototype() const {
         return getReservedSlot(JS_DESCR_SLOT_TYPROTO).toObject().as<TypedProto>();
     }
+
+    bool allowConstruct() const {
+        return getReservedSlot(JS_DESCR_SLOT_FLAGS).toInt32() & JS_DESCR_FLAG_ALLOW_CONSTRUCT;
+    }
 };
 
 bool IsTypedObjectClass(const Class* clasp); // Defined below
 bool IsTypedObjectArray(JSObject& obj);
 
 MOZ_MUST_USE bool CreateUserSizeAndAlignmentProperties(JSContext* cx, HandleTypeDescr obj);
 
 class ArrayTypeDescr;
@@ -411,18 +415,20 @@ class StructMetaTypeDescr : public Nativ
 
   public:
     // The prototype cannot be null.
     // The names in `ids` must all be non-numeric.
     // The type objects in `fieldTypeObjs` must all be TypeDescr objects.
     static StructTypeDescr* createFromArrays(JSContext* cx,
                                              HandleObject structTypePrototype,
                                              bool opaque,
+                                             bool allowConstruct,
                                              AutoIdVector& ids,
-                                             AutoValueVector& fieldTypeObjs);
+                                             AutoValueVector& fieldTypeObjs,
+                                             Vector<bool>& fieldMutabilities);
 
     // Properties and methods to be installed on StructType.prototype,
     // and hence inherited by all struct type objects:
     static const JSPropertySpec typeObjectProperties[];
     static const JSFunctionSpec typeObjectMethods[];
 
     // Properties and methods to be installed on StructType.prototype.prototype,
     // and hence inherited by all struct *typed* objects:
@@ -471,16 +477,21 @@ class StructTypeDescr : public ComplexTy
     JSAtom& fieldName(size_t index) const;
 
     // Return the type descr of the field at index `index`.
     TypeDescr& fieldDescr(size_t index) const;
 
     // Return the offset of the field at index `index`.
     size_t fieldOffset(size_t index) const;
 
+    // Return the mutability of the field at index `index`.
+    bool fieldIsMutable(size_t index) const;
+
+    static bool call(JSContext* cx, unsigned argc, Value* vp);
+
   private:
     ArrayObject& fieldInfoObject(size_t slot) const {
         return getReservedSlot(slot).toObject().as<ArrayObject>();
     }
 };
 
 typedef Handle<StructTypeDescr*> HandleStructTypeDescr;
 
--- a/js/src/builtin/TypedObjectConstants.h
+++ b/js/src/builtin/TypedObjectConstants.h
@@ -47,31 +47,35 @@
 #define JS_DESCR_SLOT_KIND               0  // Atomized string representation
 #define JS_DESCR_SLOT_STRING_REPR        1  // Atomized string representation
 #define JS_DESCR_SLOT_ALIGNMENT          2  // Alignment in bytes
 #define JS_DESCR_SLOT_SIZE               3  // Size in bytes, else 0
 #define JS_DESCR_SLOT_OPAQUE             4  // Atomized string representation
 #define JS_DESCR_SLOT_TYPROTO            5  // Prototype for instances, if any
 #define JS_DESCR_SLOT_ARRAYPROTO         6  // Lazily created prototype for arrays
 #define JS_DESCR_SLOT_TRACE_LIST         7  // List of references for use in tracing
+#define JS_DESCR_SLOT_FLAGS              8  // int32 bitvector of JS_DESCR_FLAG_*
 
 // Slots on scalars, references
-#define JS_DESCR_SLOT_TYPE               8  // Type code
+#define JS_DESCR_SLOT_TYPE               9  // Type code
 
 // Slots on array descriptors
-#define JS_DESCR_SLOT_ARRAY_ELEM_TYPE    8
-#define JS_DESCR_SLOT_ARRAY_LENGTH       9
+#define JS_DESCR_SLOT_ARRAY_ELEM_TYPE    9
+#define JS_DESCR_SLOT_ARRAY_LENGTH       10
 
 // Slots on struct type objects
-#define JS_DESCR_SLOT_STRUCT_FIELD_NAMES 8
-#define JS_DESCR_SLOT_STRUCT_FIELD_TYPES 9
-#define JS_DESCR_SLOT_STRUCT_FIELD_OFFSETS 10
+#define JS_DESCR_SLOT_STRUCT_FIELD_NAMES 9
+#define JS_DESCR_SLOT_STRUCT_FIELD_TYPES 10
+#define JS_DESCR_SLOT_STRUCT_FIELD_OFFSETS 11
+#define JS_DESCR_SLOT_STRUCT_FIELD_MUTS  12
 
 // Maximum number of slots for any descriptor
-#define JS_DESCR_SLOTS                   11
+#define JS_DESCR_SLOTS                   13
+
+#define JS_DESCR_FLAG_ALLOW_CONSTRUCT    1 // Allow structure to be constructed
 
 // These constants are for use exclusively in JS code. In C++ code,
 // prefer TypeRepresentation::Scalar etc, which allows you to
 // write a switch which will receive a warning if you omit a case.
 #define JS_TYPEREPR_SCALAR_KIND         1
 #define JS_TYPEREPR_REFERENCE_KIND      2
 #define JS_TYPEREPR_STRUCT_KIND         3
 #define JS_TYPEREPR_ARRAY_KIND          4
--- a/js/src/jit-test/tests/atomics/mutual-exclusion.js
+++ b/js/src/jit-test/tests/atomics/mutual-exclusion.js
@@ -1,13 +1,13 @@
 // Let a few threads hammer on memory with atomics to provoke errors
 // in exclusion work.  This test is not 100% fail-safe: the test may
 // pass despite a bug, but this is unlikely.
 
-if (!(this.SharedArrayBuffer && this.getSharedArrayBuffer && this.setSharedArrayBuffer && this.evalInWorker))
+if (!(this.SharedArrayBuffer && this.getSharedObject && this.setSharedObject && this.evalInWorker))
     quit(0);
 
 try {
     // This will fail with --no-threads.
     evalInWorker("37");
 }
 catch (e) {
     quit(0);
@@ -32,27 +32,27 @@ const oddResult = (function () {
         v |= (iterCount << (8 * j));
     return v;
 })();
 
 const evenResult = 0;
 
 const sab = new SharedArrayBuffer(sabLength);
 
-setSharedArrayBuffer(sab);
+setSharedObject(sab);
 
 const iab = new Int32Array(sab);
 
 function testRun(limit) {
     console.log("Limit = " + limit);
 
     // Fork off workers to hammer on memory.
     for ( var i=0 ; i < numWorkers ; i++ ) {
         evalInWorker(`
-                     const iab = new Int32Array(getSharedArrayBuffer());
+                     const iab = new Int32Array(getSharedObject());
                      const v = 1 << (8 * ${i});
                      for ( var i=0 ; i < ${limit} ; i++ ) {
                          for ( var k=0 ; k < ${iterCount} ; k++ ) {
                              if (i & 1) {
                                  for ( var j=1 ; j < iab.length ; j++ )
                                      Atomics.sub(iab, j, v);
                              }
                              else {
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/non-extensible-elements9.js
@@ -0,0 +1,187 @@
+function testNonExtensibleStoreFallibleT() {
+    // Create an array with initialized-length = capacity = 2.
+    var x = [8, 0];
+
+    // Make it non-extensible.
+    Object.preventExtensions(x);
+
+    // Now reduce initialized-length by one, so that initialized-length < capacity is true.
+    x.length = 1;
+
+    // There's enough capacity in the elements storage to save the new element,
+    // but we still need to reject the store since the object is non-extensible.
+    x[1] = 4;
+
+    assertEq(x.length, 1);
+    assertEq(x[0], 8);
+}
+
+for (var i = 0; i < 15; ++i)
+    testNonExtensibleStoreFallibleT();
+
+// Repeat testNonExtensibleStoreFallibleT for the MIRType::Value specialization.
+function testNonExtensibleStoreFallibleV(i) {
+    // Create an array with initialized-length = capacity = 2.
+    var x = [8, ""];
+
+    // Make it non-extensible.
+    Object.preventExtensions(x);
+
+    // Now reduce initialized-length by one, so that initialized-length < capacity is true.
+    x.length = 1;
+
+    // There's enough capacity in the elements storage to save the new element,
+    // but we still need to reject the store since the object is non-extensible.
+    x[1] = [true, 1][i & 1];
+
+    assertEq(x.length, 1);
+    assertEq(x[0], 8);
+}
+
+for (var i = 0; i < 15; ++i)
+    testNonExtensibleStoreFallibleV(i);
+
+function testForInIterationNonExtensibleStoreFallibleT() {
+    // Create an array with initialized-length = capacity = 2.
+    var x = [8, 0];
+
+    // Make it non-extensible.
+    Object.preventExtensions(x);
+
+    // Modifying an array's length takes a different path during for-in
+    // iteration of the array.
+    for (var k in x) {
+        // Now reduce initialized-length by one, so that initialized-length < capacity is true.
+        x.length = 1;
+    }
+
+    // There's enough capacity in the elements storage to save the new element,
+    // but we still need to reject the store since the object is non-extensible.
+    x[1] = 4;
+
+    assertEq(x.length, 1);
+    assertEq(x[0], 8);
+}
+
+for (var i = 0; i < 15; ++i)
+    testForInIterationNonExtensibleStoreFallibleT();
+
+// Repeat testForInIterationNonExtensibleStoreFallibleT for the MIRType::Value specialization.
+function testForInIterationNonExtensibleStoreFallibleV(i) {
+    // Create an array with initialized-length = capacity = 2.
+    var x = [8, ""];
+
+    // Make it non-extensible.
+    Object.preventExtensions(x);
+
+    // Modifying an array's length takes a different path during for-in
+    // iteration of the array.
+    for (var k in x) {
+        // Now reduce initialized-length by one, so that initialized-length < capacity is true.
+        x.length = 1;
+        break;
+    }
+
+    // There's enough capacity in the elements storage to save the new element,
+    // but we still need to reject the store since the object is non-extensible.
+    x[1] = [true, 1][i & 1];
+
+    assertEq(x.length, 1);
+    assertEq(x[0], 8);
+}
+
+for (var i = 0; i < 15; ++i)
+    testForInIterationNonExtensibleStoreFallibleV(i);
+
+function testNonExtensibleArrayPop() {
+    // Create an array with initialized-length = capacity = 2.
+    var x = [8, 0];
+
+    // Make it non-extensible.
+    Object.preventExtensions(x);
+
+    // Now reduce initialized-length by one, so that initialized-length < capacity is true.
+    x.pop();
+
+    // There's enough capacity in the elements storage to save the new element,
+    // but we still need to reject the store since the object is non-extensible.
+    x[1] = 4;
+
+    assertEq(x.length, 1);
+    assertEq(x[0], 8);
+}
+
+for (var i = 0; i < 15; ++i)
+    testNonExtensibleArrayPop();
+
+function testNonExtensibleArrayPopNonWritable() {
+    // Create an array with initialized-length = capacity = 2.
+    var x = [8, 0];
+
+    // Make it non-extensible.
+    Object.preventExtensions(x);
+
+    // And make the "length" property non-writable.
+    Object.defineProperty(x, "length", {writable: false});
+
+    // Now reduce initialized-length by one, so that initialized-length < capacity is true.
+    // This doesn't update |x.length|, because the "length" property is non-writable.
+    try {
+        x.pop();
+    } catch {}
+
+    // There's enough capacity in the elements storage to save the new element,
+    // but we still need to reject the store since the object is non-extensible.
+    for (var i = 0; i < 100; ++i)
+        x[1] = 4;
+
+    assertEq(x.length, 2);
+    assertEq(x[0], 8);
+    assertEq(x[1], undefined);
+    assertEq(1 in x, false);
+}
+
+for (var i = 0; i < 15; ++i)
+    testNonExtensibleArrayPopNonWritable();
+
+function testNonExtensibleArrayShift() {
+    // Create an array with initialized-length = capacity = 2.
+    var x = [8, 0];
+
+    // Make it non-extensible.
+    Object.preventExtensions(x);
+
+    // Now reduce initialized-length by one, so that initialized-length < capacity is true.
+    x.shift();
+
+    // There's enough capacity in the elements storage to save the new element,
+    // but we still need to reject the store since the object is non-extensible.
+    x[1] = 4;
+
+    assertEq(x.length, 1);
+    assertEq(x[0], 0);
+}
+
+for (var i = 0; i < 15; ++i)
+    testNonExtensibleArrayShift();
+
+function testNonExtensibleArraySplice() {
+    // Create an array with initialized-length = capacity = 2.
+    var x = [8, 0];
+
+    // Make it non-extensible.
+    Object.preventExtensions(x);
+
+    // Now reduce initialized-length by one, so that initialized-length < capacity is true.
+    x.splice(1, 1);
+
+    // There's enough capacity in the elements storage to save the new element,
+    // but we still need to reject the store since the object is non-extensible.
+    x[1] = 4;
+
+    assertEq(x.length, 1);
+    assertEq(x[0], 8);
+}
+
+for (var i = 0; i < 15; ++i)
+    testNonExtensibleArraySplice();
--- a/js/src/jit-test/tests/sharedbuf/slice-same-memory.js
+++ b/js/src/jit-test/tests/sharedbuf/slice-same-memory.js
@@ -1,17 +1,17 @@
 if (!this.SharedArrayBuffer)
     quit(0);
 
 load(libdir + "asserts.js");
 
 var sab = new SharedArrayBuffer(1 * Int32Array.BYTES_PER_ELEMENT);
 
 // Make a copy, sharing the same memory
-var sab2 = (setSharedArrayBuffer(sab), getSharedArrayBuffer());
+var sab2 = (setSharedObject(sab), getSharedObject());
 
 // Assert it's not the same object
 assertEq(sab === sab2, false);
 
 // Assert they're sharing memory
 new Int32Array(sab)[0] = 0x12345678;
 assertEq(new Int32Array(sab2)[0], 0x12345678)
 
--- a/js/src/jit/IonBuilder.cpp
+++ b/js/src/jit/IonBuilder.cpp
@@ -10569,17 +10569,18 @@ IonBuilder::getPropTryNotDefined(bool* e
 AbortReasonOr<Ok>
 IonBuilder::getPropTryTypedObject(bool* emitted,
                                   MDefinition* obj,
                                   PropertyName* name)
 {
     TypedObjectPrediction fieldPrediction;
     size_t fieldOffset;
     size_t fieldIndex;
-    if (!typedObjectHasField(obj, name, &fieldOffset, &fieldPrediction, &fieldIndex))
+    bool fieldMutable;
+    if (!typedObjectHasField(obj, name, &fieldOffset, &fieldPrediction, &fieldIndex, &fieldMutable))
         return Ok();
 
     switch (fieldPrediction.kind()) {
       case type::Struct:
       case type::Array:
         return getPropTryComplexPropOfTypedObject(emitted,
                                                   obj,
                                                   fieldOffset,
@@ -11711,17 +11712,21 @@ IonBuilder::setPropTryCommonDOMSetter(bo
 
 AbortReasonOr<Ok>
 IonBuilder::setPropTryTypedObject(bool* emitted, MDefinition* obj,
                                   PropertyName* name, MDefinition* value)
 {
     TypedObjectPrediction fieldPrediction;
     size_t fieldOffset;
     size_t fieldIndex;
-    if (!typedObjectHasField(obj, name, &fieldOffset, &fieldPrediction, &fieldIndex))
+    bool fieldMutable;
+    if (!typedObjectHasField(obj, name, &fieldOffset, &fieldPrediction, &fieldIndex, &fieldMutable))
+        return Ok();
+
+    if (!fieldMutable)
         return Ok();
 
     switch (fieldPrediction.kind()) {
       case type::Reference:
         return setPropTryReferencePropOfTypedObject(emitted, obj, fieldOffset,
                                                     value, fieldPrediction, name);
 
       case type::Scalar:
@@ -13481,33 +13486,34 @@ IonBuilder::loadTypedObjectElements(MDef
 // set `objTypes` of the field owner. If a field is found, returns true
 // and sets *fieldOffset, *fieldPrediction, and *fieldIndex. Returns false
 // otherwise. Infallible.
 bool
 IonBuilder::typedObjectHasField(MDefinition* typedObj,
                                 PropertyName* name,
                                 size_t* fieldOffset,
                                 TypedObjectPrediction* fieldPrediction,
-                                size_t* fieldIndex)
+                                size_t* fieldIndex,
+                                bool* fieldMutable)
 {
     TypedObjectPrediction objPrediction = typedObjectPrediction(typedObj);
     if (objPrediction.isUseless()) {
         trackOptimizationOutcome(TrackedOutcome::AccessNotTypedObject);
         return false;
     }
 
     // Must be accessing a struct.
     if (objPrediction.kind() != type::Struct) {
         trackOptimizationOutcome(TrackedOutcome::NotStruct);
         return false;
     }
 
     // Determine the type/offset of the field `name`, if any.
     if (!objPrediction.hasFieldNamed(NameToId(name), fieldOffset,
-                                     fieldPrediction, fieldIndex))
+                                     fieldPrediction, fieldIndex, fieldMutable))
     {
         trackOptimizationOutcome(TrackedOutcome::StructNoField);
         return false;
     }
 
     return true;
 }
 
--- a/js/src/jit/IonBuilder.h
+++ b/js/src/jit/IonBuilder.h
@@ -352,17 +352,18 @@ class IonBuilder
 
     // binary data lookup helpers.
     TypedObjectPrediction typedObjectPrediction(MDefinition* typedObj);
     TypedObjectPrediction typedObjectPrediction(TemporaryTypeSet* types);
     bool typedObjectHasField(MDefinition* typedObj,
                              PropertyName* name,
                              size_t* fieldOffset,
                              TypedObjectPrediction* fieldTypeReprs,
-                             size_t* fieldIndex);
+                             size_t* fieldIndex,
+                             bool* fieldMutable);
     MDefinition* loadTypedObjectType(MDefinition* value);
     AbortReasonOr<Ok> loadTypedObjectData(MDefinition* typedObj,
                                           MDefinition** owner,
                                           LinearSum* ownerOffset);
     AbortReasonOr<Ok> loadTypedObjectElements(MDefinition* typedObj,
                                               const LinearSum& byteOffset,
                                               uint32_t scale,
                                               MDefinition** ownerElements,
--- a/js/src/jit/MCallOptimize.cpp
+++ b/js/src/jit/MCallOptimize.cpp
@@ -669,28 +669,28 @@ IonBuilder::inlineArrayPopShift(CallInfo
     if (returnType == MIRType::Undefined || returnType == MIRType::Null)
         return InliningStatus_NotInlined;
     if (callInfo.thisArg()->type() != MIRType::Object)
         return InliningStatus_NotInlined;
 
     // Pop and shift are only handled for dense arrays that have never been
     // used in an iterator: popping elements does not account for suppressing
     // deleted properties in active iterators.
+    // Don't optimize shift if the array may be non-extensible (this matters
+    // when there are holes). Don't optimize pop if the array may be
+    // non-extensible, so we don't need to adjust the capacity for
+    // non-extensible arrays (non-extensible objects always have a capacity
+    // equal to their initialized length). We check this here because there's
+    // no non-extensible ObjectElements flag so we would need an extra guard
+    // on the BaseShape flags.
     ObjectGroupFlags unhandledFlags =
         OBJECT_FLAG_SPARSE_INDEXES |
         OBJECT_FLAG_LENGTH_OVERFLOW |
-        OBJECT_FLAG_ITERATED;
-
-    // Don't optimize shift if the array may be non-extensible (this matters
-    // when there are holes). We check this here because there's no
-    // non-extensible ObjectElements flag so we would need an extra guard on the
-    // BaseShape flags. For pop this doesn't matter, guarding on the SEALED
-    // ObjectElements flag in JIT code is sufficient.
-    if (mode == MArrayPopShift::Shift)
-        unhandledFlags |= OBJECT_FLAG_NON_EXTENSIBLE_ELEMENTS;
+        OBJECT_FLAG_ITERATED |
+        OBJECT_FLAG_NON_EXTENSIBLE_ELEMENTS;
 
     MDefinition* obj = convertUnboxedObjects(callInfo.thisArg());
     TemporaryTypeSet* thisTypes = obj->resultTypeSet();
     if (!thisTypes)
         return InliningStatus_NotInlined;
     const Class* clasp = thisTypes->getKnownClass(constraints());
     if (clasp != &ArrayObject::class_)
         return InliningStatus_NotInlined;
--- a/js/src/jit/TypedObjectPrediction.cpp
+++ b/js/src/jit/TypedObjectPrediction.cpp
@@ -251,51 +251,54 @@ TypedObjectPrediction::arrayElementType(
 }
 
 bool
 TypedObjectPrediction::hasFieldNamedPrefix(const StructTypeDescr& descr,
                                            size_t fieldCount,
                                            jsid id,
                                            size_t* fieldOffset,
                                            TypedObjectPrediction* out,
-                                           size_t* index) const
+                                           size_t* index,
+                                           bool* isMutable) const
 {
     // Find the index of the field |id| if any.
     if (!descr.fieldIndex(id, index))
         return false;
 
     // Check whether the index falls within our known safe prefix.
     if (*index >= fieldCount)
         return false;
 
     // Load the offset and type.
     *fieldOffset = descr.fieldOffset(*index);
     *out = TypedObjectPrediction(descr.fieldDescr(*index));
+    *isMutable = descr.fieldIsMutable(*index);
     return true;
 }
 
 bool
 TypedObjectPrediction::hasFieldNamed(jsid id,
                                      size_t* fieldOffset,
                                      TypedObjectPrediction* fieldType,
-                                     size_t* fieldIndex) const
+                                     size_t* fieldIndex,
+                                     bool* fieldMutable) const
 {
     MOZ_ASSERT(kind() == type::Struct);
 
     switch (predictionKind()) {
       case TypedObjectPrediction::Empty:
       case TypedObjectPrediction::Inconsistent:
         return false;
 
       case TypedObjectPrediction::Descr:
         return hasFieldNamedPrefix(
             descr().as<StructTypeDescr>(), ALL_FIELDS,
-            id, fieldOffset, fieldType, fieldIndex);
+            id, fieldOffset, fieldType, fieldIndex, fieldMutable);
 
       case TypedObjectPrediction::Prefix:
         return hasFieldNamedPrefix(
             *prefix().descr, prefix().fields,
-            id, fieldOffset, fieldType, fieldIndex);
+            id, fieldOffset, fieldType, fieldIndex, fieldMutable);
 
       default:
         MOZ_CRASH("Bad prediction kind");
     }
 }
--- a/js/src/jit/TypedObjectPrediction.h
+++ b/js/src/jit/TypedObjectPrediction.h
@@ -105,17 +105,18 @@ class TypedObjectPrediction {
     template<typename T>
     typename T::Type extractType() const;
 
     bool hasFieldNamedPrefix(const StructTypeDescr& descr,
                              size_t fieldCount,
                              jsid id,
                              size_t* fieldOffset,
                              TypedObjectPrediction* out,
-                             size_t* index) const;
+                             size_t* index,
+                             bool* isMutable) const;
 
   public:
 
     ///////////////////////////////////////////////////////////////////////////
     // Constructing a prediction. Generally, you start with an empty
     // prediction and invoke addDescr() repeatedly.
 
     TypedObjectPrediction()
@@ -187,15 +188,16 @@ class TypedObjectPrediction {
 
     // Returns true if the predicted type includes a field named |id|
     // and sets |*fieldOffset|, |*fieldType|, and |*fieldIndex| with
     // the offset (in bytes), type, and index of the field
     // respectively.  Otherwise returns false.
     bool hasFieldNamed(jsid id,
                        size_t* fieldOffset,
                        TypedObjectPrediction* fieldType,
-                       size_t* fieldIndex) const;
+                       size_t* fieldIndex,
+                       bool* fieldMutable) const;
 };
 
 } // namespace jit
 } // namespace js
 
 #endif
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -536,17 +536,20 @@ MSG_DEF(JSMSG_DEFAULT_LOCALE_ERROR,    0
 MSG_DEF(JSMSG_NO_SUCH_SELF_HOSTED_PROP,1, JSEXN_ERR, "No such property on self-hosted object: {0}")
 
 // Typed object
 MSG_DEF(JSMSG_INVALID_PROTOTYPE,       0, JSEXN_TYPEERR, "prototype field is not an object")
 MSG_DEF(JSMSG_TYPEDOBJECT_BAD_ARGS,    0, JSEXN_TYPEERR, "invalid arguments")
 MSG_DEF(JSMSG_TYPEDOBJECT_BINARYARRAY_BAD_INDEX, 0, JSEXN_RANGEERR, "invalid or out-of-range index")
 MSG_DEF(JSMSG_TYPEDOBJECT_HANDLE_UNATTACHED, 0, JSEXN_TYPEERR, "handle unattached")
 MSG_DEF(JSMSG_TYPEDOBJECT_STRUCTTYPE_BAD_ARGS, 0, JSEXN_RANGEERR, "invalid field descriptor")
+MSG_DEF(JSMSG_TYPEDOBJECT_STRUCTTYPE_NOT_CALLABLE, 0, JSEXN_TYPEERR, "not callable")
 MSG_DEF(JSMSG_TYPEDOBJECT_TOO_BIG,     0, JSEXN_ERR, "Type is too large to allocate")
+MSG_DEF(JSMSG_TYPEDOBJECT_SETTING_IMMUTABLE, 0, JSEXN_ERR, "setting immutable field")
+MSG_DEF(JSMSG_TYPEDOBJECT_NOT_CONSTRUCTIBLE, 0, JSEXN_TYPEERR, "not constructible")
 
 // Array
 MSG_DEF(JSMSG_TOO_LONG_ARRAY,         0, JSEXN_TYPEERR, "Too long array")
 
 // Typed array
 MSG_DEF(JSMSG_BAD_INDEX,               0, JSEXN_RANGEERR, "invalid or out-of-range index")
 MSG_DEF(JSMSG_NON_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected ArrayBuffer, but species constructor returned non-ArrayBuffer")
 MSG_DEF(JSMSG_SAME_ARRAY_BUFFER_RETURNED, 0, JSEXN_TYPEERR, "expected different ArrayBuffer, but species constructor returned same ArrayBuffer")
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -108,16 +108,17 @@
 #include "vm/MutexIDs.h"
 #include "vm/Printer.h"
 #include "vm/Shape.h"
 #include "vm/SharedArrayObject.h"
 #include "vm/Time.h"
 #include "vm/TypedArrayObject.h"
 #include "vm/WrapperObject.h"
 #include "wasm/WasmJS.h"
+#include "wasm/WasmModule.h"
 
 #include "vm/Compartment-inl.h"
 #include "vm/ErrorObject-inl.h"
 #include "vm/Interpreter-inl.h"
 #include "vm/JSObject-inl.h"
 #include "vm/Realm-inl.h"
 #include "vm/Stack-inl.h"
 
@@ -3201,17 +3202,17 @@ DisassWithSrc(JSContext* cx, unsigned ar
         }
 
         FILE* file = fopen(script->filename(), "rb");
         if (!file) {
             /* FIXME: script->filename() should become UTF-8 (bug 987069). */
             ReportCantOpenErrorUnknownEncoding(cx, script->filename());
             return false;
         }
-        auto closeFile = MakeScopeExit([file]() { fclose(file); });
+        auto closeFile = MakeScopeExit([file] { fclose(file); });
 
         jsbytecode* pc = script->code();
         jsbytecode* end = script->codeEnd();
 
         Sprinter sprinter(cx);
         if (!sprinter.init())
             return false;
 
@@ -5740,129 +5741,246 @@ DisableGeckoProfiling(JSContext* cx, uns
 
     if (!cx->runtime()->geckoProfiler().enabled())
         return true;
 
     cx->runtime()->geckoProfiler().enable(false);
     return true;
 }
 
-// Global mailbox that is used to communicate a SharedArrayBuffer
-// value from one worker to another.
+// Global mailbox that is used to communicate a shareable object value from one
+// worker to another.
+//
+// These object types are shareable:
+//
+//   - SharedArrayBuffer
+//   - WasmMemoryObject (when constructed with shared:true)
+//   - WasmModuleObject
 //
-// For simplicity we store only the SharedArrayRawBuffer; retaining
-// the SAB object would require per-runtime storage, and would have no
-// real benefits.
+// For the SharedArrayBuffer and WasmMemoryObject we transmit the underlying
+// SharedArrayRawBuffer ("SARB"). For the WasmModuleObject we transmit the
+// underlying wasm::Module.  The transmitted types are refcounted.  When they
+// are in the mailbox their reference counts are at least 1, accounting for the
+// reference from the mailbox.
 //
-// Invariant: when a SARB is in the mailbox its reference count is at
-// least 1, accounting for the reference from the mailbox.
+// The lock guards the mailbox variable and prevents a race where two workers
+// try to set the mailbox at the same time to replace an object that is only
+// referenced from the mailbox: the workers will both decrement the reference
+// count on the old object, and one of those decrements will be on a garbage
+// object.  We could implement this with atomics and a CAS loop but it's not
+// worth the bother.
 //
-// The lock guards the mailbox variable and prevents a race where two
-// workers try to set the mailbox at the same time to replace a SARB
-// that is only referenced from the mailbox: the workers will both
-// decrement the reference count on the old SARB, and one of those
-// decrements will be on a garbage object.  We could implement this
-// with atomics and a CAS loop but it's not worth the bother.
-
-struct SharedArrayBufferMailbox
-{
-    SharedArrayBufferMailbox() : buffer(nullptr), length(0) {}
-
-    SharedArrayRawBuffer* buffer;
-    uint32_t              length;
+// Note that if a thread reads the mailbox repeatedly it will get distinct
+// objects on each read.  The alternatives are to cache created objects locally,
+// but this retains storage we don't need to retain, or to somehow clear the
+// mailbox locally, but this creates a coordination headache.  Buyer beware.
+
+enum class MailboxTag {
+    Empty,
+    SharedArrayBuffer,
+    WasmMemory,
+    WasmModule
 };
 
-typedef ExclusiveData<SharedArrayBufferMailbox> SABMailbox;
+struct SharedObjectMailbox
+{
+    union Value {
+        struct {
+            SharedArrayRawBuffer* buffer;
+            uint32_t              length;
+        } sarb;
+        wasm::Module*             module;
+    };
+
+    SharedObjectMailbox() : tag(MailboxTag::Empty) {}
+
+    MailboxTag tag;
+    Value      val;
+};
+
+typedef ExclusiveData<SharedObjectMailbox> SOMailbox;
 
 // Never null after successful initialization.
-static SABMailbox* sharedArrayBufferMailbox;
-
-static bool
-InitSharedArrayBufferMailbox()
-{
-    sharedArrayBufferMailbox = js_new<SABMailbox>(mutexid::ShellArrayBufferMailbox);
-    return sharedArrayBufferMailbox != nullptr;
+static SOMailbox* sharedObjectMailbox;
+
+static bool
+InitSharedObjectMailbox()
+{
+    sharedObjectMailbox = js_new<SOMailbox>(mutexid::ShellObjectMailbox);
+    return sharedObjectMailbox != nullptr;
 }
 
 static void
-DestructSharedArrayBufferMailbox()
+DestructSharedObjectMailbox()
 {
     // All workers need to have terminated at this point.
 
     {
-        auto mbx = sharedArrayBufferMailbox->lock();
-        if (mbx->buffer)
-            mbx->buffer->dropReference();
-    }
-
-    js_delete(sharedArrayBufferMailbox);
-    sharedArrayBufferMailbox = nullptr;
-}
-
-static bool
-GetSharedArrayBuffer(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-    JSObject* newObj = nullptr;
+        auto mbx = sharedObjectMailbox->lock();
+        switch (mbx->tag) {
+          case MailboxTag::Empty:
+            break;
+          case MailboxTag::SharedArrayBuffer:
+          case MailboxTag::WasmMemory:
+            mbx->val.sarb.buffer->dropReference();
+            break;
+          case MailboxTag::WasmModule:
+            mbx->val.module->Release();
+            break;
+          default:
+            MOZ_CRASH();
+        }
+    }
+
+    js_delete(sharedObjectMailbox);
+    sharedObjectMailbox = nullptr;
+}
+
+static bool
+GetSharedObject(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+    RootedObject newObj(cx);
 
     {
-        auto mbx = sharedArrayBufferMailbox->lock();
-
-        if (SharedArrayRawBuffer* buf = mbx->buffer) {
+        auto mbx = sharedObjectMailbox->lock();
+        switch (mbx->tag) {
+          case MailboxTag::Empty: {
+            break;
+          }
+          case MailboxTag::SharedArrayBuffer:
+          case MailboxTag::WasmMemory: {
+            // Flag was set in the sender; ensure it is set in the receiver.
+            MOZ_ASSERT(cx->realm()->creationOptions().getSharedMemoryAndAtomicsEnabled());
+
+            SharedArrayRawBuffer* buf = mbx->val.sarb.buffer;
+            uint32_t length = mbx->val.sarb.length;
             if (!buf->addReference()) {
                 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_SC_SAB_REFCNT_OFLO);
                 return false;
             }
-
-            // Shared memory is enabled globally in the shell: there can't be a worker
-            // that does not enable it if the main thread has it.
+            auto dropBuf = MakeScopeExit([buf] { buf->dropReference(); });
+
+            Rooted<ArrayBufferObjectMaybeShared*> maybesab(cx, SharedArrayBufferObject::New(cx, buf, length));
+            if (!maybesab)
+                return false;
+            if (mbx->tag == MailboxTag::SharedArrayBuffer) {
+                newObj = maybesab;
+            } else {
+                if (!GlobalObject::ensureConstructor(cx, cx->global(), JSProto_WebAssembly))
+                    return false;
+
+                RootedObject proto(cx, &cx->global()->getPrototype(JSProto_WasmMemory).toObject());
+                newObj = WasmMemoryObject::create(cx, maybesab, proto);
+                MOZ_ASSERT_IF(newObj, newObj->as<WasmMemoryObject>().isShared());
+            }
+            if (!newObj)
+                return false;
+
+            dropBuf.release();
+
+            break;
+          }
+          case MailboxTag::WasmModule: {
+            // Flag was set in the sender; ensure it is set in the receiver.
             MOZ_ASSERT(cx->realm()->creationOptions().getSharedMemoryAndAtomicsEnabled());
 
-            newObj = SharedArrayBufferObject::New(cx, buf, mbx->length);
-            if (!newObj) {
-                buf->dropReference();
+            if (!GlobalObject::ensureConstructor(cx, cx->global(), JSProto_WebAssembly))
                 return false;
-            }
+
+            // WasmModuleObject::create() increments the refcount on the module
+            // and signals an error and returns null if that fails.
+            RootedObject proto(cx, &cx->global()->getPrototype(JSProto_WasmModule).toObject());
+            newObj = WasmModuleObject::create(cx, *mbx->val.module, proto);
+            if (!newObj)
+                return false;
+            break;
+          }
+          default: {
+            MOZ_CRASH();
+          }
         }
     }
 
     args.rval().setObjectOrNull(newObj);
     return true;
 }
 
 static bool
-SetSharedArrayBuffer(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-    SharedArrayRawBuffer* newBuffer = nullptr;
-    uint32_t newLength = 0;
-
-    if (args.get(0).isNullOrUndefined()) {
-        // Clear out the mailbox
-    }
-    else if (args.get(0).isObject() && args[0].toObject().is<SharedArrayBufferObject>()) {
-        newBuffer = args[0].toObject().as<SharedArrayBufferObject>().rawBufferObject();
-        newLength = args[0].toObject().as<SharedArrayBufferObject>().byteLength();
-        if (!newBuffer->addReference()) {
-            JS_ReportErrorASCII(cx, "Reference count overflow on SharedArrayBuffer");
-            return false;
-        }
+SetSharedObject(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    MailboxTag tag = MailboxTag::Empty;
+    SharedObjectMailbox::Value value;
+
+    // Increase refcounts when we obtain the value to avoid operating on dead
+    // storage during self-assignment.
+
+    if (args.get(0).isObject()) {
+        RootedObject obj(cx, &args[0].toObject());
+        if (obj->is<SharedArrayBufferObject>()) {
+            Rooted<SharedArrayBufferObject*> sab(cx, &obj->as<SharedArrayBufferObject>());
+            tag = MailboxTag::SharedArrayBuffer;
+            value.sarb.buffer = sab->rawBufferObject();
+            value.sarb.length = sab->byteLength();
+            if (!value.sarb.buffer->addReference()) {
+                JS_ReportErrorASCII(cx, "Reference count overflow on SharedArrayBuffer");
+                return false;
+            }
+        } else if (obj->is<WasmMemoryObject>()) {
+            // Here we must transmit sab.byteLength() as the length; the SARB has its
+            // own notion of the length which may be greater, and that's fine.
+            if (obj->as<WasmMemoryObject>().isShared()) {
+                Rooted<SharedArrayBufferObject*> sab(cx, &obj->as<WasmMemoryObject>().buffer().as<SharedArrayBufferObject>());
+                tag = MailboxTag::WasmMemory;
+                value.sarb.buffer = sab->rawBufferObject();
+                value.sarb.length = sab->byteLength();
+                if (!value.sarb.buffer->addReference()) {
+                    JS_ReportErrorASCII(cx, "Reference count overflow on SharedArrayBuffer");
+                    return false;
+                }
+            } else {
+                JS_ReportErrorASCII(cx, "Invalid argument to SetSharedObject");
+                return false;
+            }
+        } else if (obj->is<WasmModuleObject>()) {
+            tag = MailboxTag::WasmModule;
+            value.module = &obj->as<WasmModuleObject>().module();
+            value.module->AddRef();
+        } else {
+            JS_ReportErrorASCII(cx, "Invalid argument to SetSharedObject");
+            return false;
+        }
+    } else if (args.get(0).isNullOrUndefined()) {
+        // Nothing
     } else {
-        JS_ReportErrorASCII(cx, "Only a SharedArrayBuffer can be installed in the global mailbox");
+        JS_ReportErrorASCII(cx, "Invalid argument to SetSharedObject");
         return false;
     }
 
     {
-        auto mbx = sharedArrayBufferMailbox->lock();
-
-        if (SharedArrayRawBuffer* oldBuffer = mbx->buffer)
-            oldBuffer->dropReference();
-
-        mbx->buffer = newBuffer;
-        mbx->length = newLength;
+        auto mbx = sharedObjectMailbox->lock();
+
+        switch (mbx->tag) {
+          case MailboxTag::Empty:
+            break;
+          case MailboxTag::SharedArrayBuffer:
+          case MailboxTag::WasmMemory:
+            mbx->val.sarb.buffer->dropReference();
+            break;
+          case MailboxTag::WasmModule:
+            mbx->val.module->Release();
+            break;
+          default:
+            MOZ_CRASH();
+        }
+
+        mbx->tag = tag;
+        mbx->val = value;
     }
 
     args.rval().setUndefined();
     return true;
 }
 
 struct BufferStreamJob
 {
@@ -6953,27 +7071,35 @@ static const JSFunctionSpecWithHelp shel
 "  Evaluate s in optional sandbox object o.\n"
 "  if (s == '' && !o) return new o with eager standard classes\n"
 "  if (s == 'lazy' && !o) return new o with lazy standard classes"),
 
     JS_FN_HELP("evalInWorker", EvalInWorker, 1, 0,
 "evalInWorker(str)",
 "  Evaluate 'str' in a separate thread with its own runtime.\n"),
 
-    JS_FN_HELP("getSharedArrayBuffer", GetSharedArrayBuffer, 0, 0,
-"getSharedArrayBuffer()",
-"  Retrieve the SharedArrayBuffer object from the cross-worker mailbox.\n"
+    JS_FN_HELP("getSharedObject", GetSharedObject, 0, 0,
+"getSharedObject()",
+"  Retrieve the shared object from the cross-worker mailbox.\n"
 "  The object retrieved may not be identical to the object that was\n"
 "  installed, but it references the same shared memory.\n"
-"  getSharedArrayBuffer performs an ordering memory barrier.\n"),
-
-    JS_FN_HELP("setSharedArrayBuffer", SetSharedArrayBuffer, 0, 0,
-"setSharedArrayBuffer()",
-"  Install the SharedArrayBuffer object in the cross-worker mailbox.\n"
-"  setSharedArrayBuffer performs an ordering memory barrier.\n"),
+"  getSharedObject performs an ordering memory barrier.\n"),
+
+    JS_FN_HELP("setSharedObject", SetSharedObject, 0, 0,
+"setSharedObject(obj)",
+"  Install the shared object in the cross-worker mailbox.  The object\n"
+"  may be null.  setSharedObject performs an ordering memory barrier.\n"),
+
+    JS_FN_HELP("getSharedArrayBuffer", GetSharedObject, 0, 0,
+"getSharedArrayBuffer()",
+"  Obsolete alias for getSharedObject().\n"),
+
+    JS_FN_HELP("setSharedArrayBuffer", SetSharedObject, 0, 0,
+"setSharedArrayBuffer(obj)",
+"  Obsolete alias for setSharedObject(obj).\n"),
 
     JS_FN_HELP("shapeOf", ShapeOf, 1, 0,
 "shapeOf(obj)",
 "  Get the shape of obj (an implementation detail)."),
 
     JS_FN_HELP("groupOf", GroupOf, 1, 0,
 "groupOf(obj)",
 "  Get the group of obj (an implementation detail)."),
@@ -9148,17 +9274,17 @@ main(int argc, char** argv, char** envp)
 
     SetOutputFile("JS_STDOUT", &rcStdout, &gOutFile);
     SetOutputFile("JS_STDERR", &rcStderr, &gErrFile);
 
     // Start the engine.
     if (!JS_Init())
         return 1;
 
-    auto shutdownEngine = MakeScopeExit([]() { JS_ShutDown(); });
+    auto shutdownEngine = MakeScopeExit([] { JS_ShutDown(); });
 
     OptionParser op("Usage: {progname} [options] [[script] scriptArgs*]");
 
     op.setDescription("The SpiderMonkey shell provides a command line interface to the "
         "JavaScript engine. Code and file options provided via the command line are "
         "run left to right. If provided, the optional script argument is run after "
         "all options have been processed. Just-In-Time compilation modes may be enabled via "
         "command line options.");
@@ -9394,17 +9520,17 @@ main(int argc, char** argv, char** envp)
         char* path = dllPaths.front();
         loader.load(path);
         dllPaths.popFront();
     }
 
     if (op.getBoolOption("suppress-minidump"))
         js::NoteIntentionalCrash();
 
-    if (!InitSharedArrayBufferMailbox())
+    if (!InitSharedObjectMailbox())
         return 1;
 
     // The fake CPU count must be set before initializing the Runtime,
     // which spins up the thread pool.
     int32_t cpuCount = op.getIntOption("cpu-count"); // What we're really setting
     if (cpuCount < 0)
         cpuCount = op.getIntOption("thread-count");  // Legacy name
     if (cpuCount >= 0)
@@ -9505,15 +9631,15 @@ main(int argc, char** argv, char** envp)
 
     // Must clear out some of sc's pointer containers before JS_DestroyContext.
     sc->markObservers.reset();
 
     KillWatchdog(cx);
 
     KillWorkerThreads(cx);
 
-    DestructSharedArrayBufferMailbox();
+    DestructSharedObjectMailbox();
 
     CancelOffThreadJobsForRuntime(cx);
 
     JS_DestroyContext(cx);
     return result;
 }
--- a/js/src/tests/non262/TypedArray/slice-memcpy.js
+++ b/js/src/tests/non262/TypedArray/slice-memcpy.js
@@ -46,34 +46,34 @@ for (var constructor of typedArrayConstr
     let tb = ta.slice(0);
 
     assertEqArray(ta, [1, 2, 1, 2, 1, 2]);
     assertEqArray(tb, [1, 2, 1, 2, 1, 2]);
     assertEqArray(new constructor(ab), [1, 2, 1, 2, 1, 2, 1, 2, 9, 10]);
 }
 
 // Tricky: Same as above, but with SharedArrayBuffer and different compartments.
-if (typeof setSharedArrayBuffer === "function") {
+if (typeof setSharedObject === "function") {
     for (var constructor of typedArrayConstructors) {
         const elementSize = constructor.BYTES_PER_ELEMENT;
         let ts = new constructor(new SharedArrayBuffer(10 * elementSize));
         for (let i = 0; i < ts.length; i++) {
             ts[i] = i + 1;
         }
         let ab = ts.buffer;
         let ta = new constructor(ab, 0, 6);
         ta.constructor = {
             [Symbol.species]: function(len) {
-                setSharedArrayBuffer(ab);
+                setSharedObject(ab);
                 try {
                     return otherGlobal.eval(`
-                        new ${constructor.name}(getSharedArrayBuffer(), ${2 * elementSize}, ${len});
+                        new ${constructor.name}(getSharedObject(), ${2 * elementSize}, ${len});
                     `);
                 } finally {
-                    setSharedArrayBuffer(null);
+                    setSharedObject(null);
                 }
             }
         };
         let tb = ta.slice(0);
 
         assertEqArray(ta, [1, 2, 1, 2, 1, 2]);
         assertEqArray(tb, [1, 2, 1, 2, 1, 2]);
         assertEqArray(new constructor(ab), [1, 2, 1, 2, 1, 2, 1, 2, 9, 10]);
--- a/js/src/tests/non262/TypedObject/structtypereflection.js
+++ b/js/src/tests/non262/TypedObject/structtypereflection.js
@@ -13,32 +13,35 @@ var int16 = TypedObject.int16;
 var int32 = TypedObject.int32;
 var float32 = TypedObject.float32;
 var float64 = TypedObject.float64;
 
 function runTests() {
     print(BUGNUMBER + ": " + summary);
 
     var S = new StructType({x: int32, y: uint8, z: float64});
+    var T = new StructType({x: int32, y: uint8, z: float64});
     assertEq(S.__proto__, StructType.prototype);
     assertEq(S.prototype.__proto__, StructType.prototype.prototype);
     assertEq(S.toSource(), "new StructType({x: int32, y: uint8, z: float64})");
     assertEq(S.byteLength, 16);
     assertEq(S.byteAlignment, 8);
     var fieldNames = Object.getOwnPropertyNames(S.fieldTypes);
     assertEq(fieldNames[0], "x");
     assertEq(fieldNames[1], "y");
     assertEq(fieldNames[2], "z");
     assertEq(fieldNames.length, 3);
     assertEq(S.fieldTypes.x, int32);
     assertEq(S.fieldTypes.y, uint8);
     assertEq(S.fieldTypes.z, float64);
     assertEq(S.fieldOffsets.x, 0);
     assertEq(S.fieldOffsets.y, 4);
     assertEq(S.fieldOffsets.z, 8);
+    assertEq((new S({x:10, y:20, z:30})) instanceof S, true);
+    assertEq((new S({x:10, y:20, z:30})) instanceof T, false);
 
     // fieldTypes and fieldOffsets should be frozen
     assertEq(Object.isFrozen(S.fieldTypes), true);
     assertEq(Object.isFrozen(S.fieldOffsets), true);
 
     if (typeof reportCompare === "function")
         reportCompare(true, true);
     print("Tests complete");
--- a/js/src/tests/shell/futex.js
+++ b/js/src/tests/shell/futex.js
@@ -7,66 +7,18 @@
 
 var DEBUG = false;
 
 function dprint(s) {
     if (DEBUG) print(s);
 }
 
 var hasSharedArrayBuffer = !!(this.SharedArrayBuffer &&
-                              this.getSharedArrayBuffer &&
-                              this.setSharedArrayBuffer);
-
-if (hasSharedArrayBuffer) {
-
-// Tests the SharedArrayBuffer mailbox in the shell.
-// Tests the wait/wake functionality in the shell.
-
-var sab = new SharedArrayBuffer(12);
-var mem = new Int32Array(sab);
-
-// SharedArrayBuffer mailbox tests
-
-assertEq(getSharedArrayBuffer(), null); // Mbx starts empty
-
-assertEq(setSharedArrayBuffer(mem.buffer), undefined); // Setter returns undefined
-assertEq(getSharedArrayBuffer() == null, false);       // And then the mbx is not empty
-
-var v = getSharedArrayBuffer();
-assertEq(v.byteLength, mem.buffer.byteLength); // Looks like what we put in?
-var w = new Int32Array(v);
-mem[0] = 314159;
-assertEq(w[0], 314159);		// Shares memory (locally) with what we put in?
-mem[0] = 0;
-
-setSharedArrayBuffer(null);	// Setting to null clears to null
-assertEq(getSharedArrayBuffer(), null);
-
-setSharedArrayBuffer(mem.buffer);
-setSharedArrayBuffer(undefined); // Setting to undefined clears to null
-assertEq(getSharedArrayBuffer(), null);
-
-setSharedArrayBuffer(mem.buffer);
-setSharedArrayBuffer();		// Setting without arguments clears to null
-assertEq(getSharedArrayBuffer(), null);
-
-// Only SharedArrayBuffer can be stored in the mbx
-
-assertThrowsInstanceOf(() => setSharedArrayBuffer({x:10, y:20}), Error);
-assertThrowsInstanceOf(() => setSharedArrayBuffer([1,2]), Error);
-assertThrowsInstanceOf(() => setSharedArrayBuffer(new ArrayBuffer(10)), Error);
-assertThrowsInstanceOf(() => setSharedArrayBuffer(new Int32Array(10)), Error);
-assertThrowsInstanceOf(() => setSharedArrayBuffer(false), Error);
-assertThrowsInstanceOf(() => setSharedArrayBuffer(3.14), Error);
-assertThrowsInstanceOf(() => setSharedArrayBuffer(mem), Error);
-assertThrowsInstanceOf(() => setSharedArrayBuffer("abracadabra"), Error);
-assertThrowsInstanceOf(() => setSharedArrayBuffer(() => 37), Error);
-
-} // if (hasSharedArrayBuffer) { ... }
-
+                              this.getSharedObject &&
+                              this.setSharedObject);
 
 // Futex test
 
 // Only run if helper threads are available.
 if (hasSharedArrayBuffer && helperThreadCount() !== 0) {
 
 ////////////////////////////////////////////////////////////
 
@@ -84,47 +36,47 @@ assertEq(Atomics.wait(mem, 0, 42, 100), 
 
 // Main is sharing the buffer with the worker; the worker is clearing
 // the buffer.
 
 mem[0] = 42;
 mem[1] = 37;
 mem[2] = DEBUG;
 
-setSharedArrayBuffer(mem.buffer);
+setSharedObject(mem.buffer);
 
 evalInWorker(`
-var mem = new Int32Array(getSharedArrayBuffer());
+var mem = new Int32Array(getSharedObject());
 function dprint(s) {
     if (mem[2]) print(s);
 }
 assertEq(mem[0], 42);		// what was written in the main thread
 assertEq(mem[1], 37);		//   is read in the worker
 mem[1] = 1337;
 dprint("Sleeping for 2 seconds");
 sleep(2);
 dprint("Waking the main thread now");
-setSharedArrayBuffer(null);
+setSharedObject(null);
 assertEq(Atomics.wake(mem, 0, 1), 1); // Can fail spuriously but very unlikely
 `);
 
 var then = Date.now();
 assertEq(Atomics.wait(mem, 0, 42), "ok");
 dprint("Woke up as I should have in " + (Date.now() - then)/1000 + "s");
 assertEq(mem[1], 1337); // what was written in the worker is read in the main thread
-assertEq(getSharedArrayBuffer(), null); // The worker's clearing of the mbx is visible
+assertEq(getSharedObject(), null); // The worker's clearing of the mbx is visible
 
 ////////////////////////////////////////////////////////////
 
 // Test the default argument to atomics.wake()
 
-setSharedArrayBuffer(mem.buffer);
+setSharedObject(mem.buffer);
 
 evalInWorker(`
-var mem = new Int32Array(getSharedArrayBuffer());
+var mem = new Int32Array(getSharedObject());
 sleep(2);				// Probably long enough to avoid a spurious error next
 assertEq(Atomics.wake(mem, 0), 1);	// Last argument to wake should default to +Infinity
 `);
 
 var then = Date.now();
 dprint("Main thread waiting on wakeup (2s)");
 assertEq(Atomics.wait(mem, 0, 42), "ok");
 dprint("Woke up as I should have in " + (Date.now() - then)/1000 + "s");
new file mode 100644
--- /dev/null
+++ b/js/src/tests/shell/mailbox.js
@@ -0,0 +1,101 @@
+// |reftest| skip-if(!xulRuntime.shell)
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ */
+
+// Tests the shared-object mailbox in the shell.
+
+var hasSharedMemory = !!(this.SharedArrayBuffer &&
+                         this.getSharedObject &&
+                         this.setSharedObject);
+
+if (!hasSharedMemory) {
+    reportCompare(true, true);
+    quit(0);
+}
+
+var sab = new SharedArrayBuffer(12);
+var mem = new Int32Array(sab);
+
+// SharedArrayBuffer mailbox tests
+
+assertEq(getSharedObject(), null); // Mbx starts empty
+
+assertEq(setSharedObject(mem.buffer), undefined); // Setter returns undefined
+assertEq(getSharedObject() == null, false);       // And then the mbx is not empty
+
+var v = getSharedObject();
+assertEq(v.byteLength, mem.buffer.byteLength); // Looks like what we put in?
+var w = new Int32Array(v);
+mem[0] = 314159;
+assertEq(w[0], 314159);		// Shares memory (locally) with what we put in?
+mem[0] = 0;
+
+setSharedObject(null);	// Setting to null clears to null
+assertEq(getSharedObject(), null);
+
+setSharedObject(mem.buffer);
+setSharedObject(undefined); // Setting to undefined clears to null
+assertEq(getSharedObject(), null);
+
+setSharedObject(mem.buffer);
+setSharedObject();		// Setting without arguments clears to null
+assertEq(getSharedObject(), null);
+
+// Non-shared objects cannot be stored in the mbx
+
+assertThrowsInstanceOf(() => setSharedObject({x:10, y:20}), Error);
+assertThrowsInstanceOf(() => setSharedObject([1,2]), Error);
+assertThrowsInstanceOf(() => setSharedObject(new ArrayBuffer(10)), Error);
+assertThrowsInstanceOf(() => setSharedObject(new Int32Array(10)), Error);
+assertThrowsInstanceOf(() => setSharedObject(false), Error);
+assertThrowsInstanceOf(() => setSharedObject(3.14), Error);
+assertThrowsInstanceOf(() => setSharedObject(mem), Error);
+assertThrowsInstanceOf(() => setSharedObject("abracadabra"), Error);
+assertThrowsInstanceOf(() => setSharedObject(() => 37), Error);
+
+// We can store wasm shared memories, too
+
+if (!this.WebAssembly) {
+    reportCompare(true, true);
+    quit(0);
+}
+
+setSharedObject(null);
+
+var mem = new WebAssembly.Memory({initial: 1, maximum: 1, shared: true});
+setSharedObject(mem);
+var ia1 = new Int32Array(mem.buffer);
+
+var mem2 = getSharedObject();
+assertEq(mem2.buffer instanceof SharedArrayBuffer, true);
+assertEq(mem2.buffer.byteLength, 65536);
+var ia2 = new Int32Array(mem2.buffer);
+
+ia2[37] = 0x12345678;
+assertEq(ia1[37], 0x12345678);
+
+// Can't store a non-shared memory
+
+assertThrowsInstanceOf(() => setSharedObject(new WebAssembly.Memory({initial: 1, maximum: 1})), Error);
+
+// We can store wasm modules
+
+var mod = new WebAssembly.Module(wasmTextToBinary(`(module
+                                                    (func (export "hi") (result i32)
+                                                     (i32.const 37))
+                                                    (import "m" "f" (param i32) (result i32)))`));
+
+setSharedObject(mod);
+
+var mod2 = getSharedObject();
+
+// This should fail because we're not providing the correct import object
+assertThrowsInstanceOf(() => new WebAssembly.Instance(mod2, {m:{}}), WebAssembly.LinkError);
+
+// But this should work
+new WebAssembly.Instance(mod2, {m:{f:(x) => x}});
+
+reportCompare(true,true);
--- a/js/src/vm/MutexIDs.h
+++ b/js/src/vm/MutexIDs.h
@@ -13,17 +13,17 @@
 //
 // Mutexes can only be acquired in increasing order. This prevents the
 // possibility of deadlock.
 
 #define FOR_EACH_MUTEX(_)             \
   _(TestMutex,                   100) \
   _(ShellContextWatchdog,        100) \
   _(ShellWorkerThreads,          100) \
-  _(ShellArrayBufferMailbox,     100) \
+  _(ShellObjectMailbox,          100) \
                                       \
   _(AtomsTable,                  200) \
                                       \
   _(WasmInitBuiltinThunks,       250) \
   _(WasmLazyStubsTier1,          250) \
   _(WasmLazyStubsTier2,          251) \
                                       \
   _(GlobalHelperThreadState,     300) \
--- a/js/src/vm/NativeObject-inl.h
+++ b/js/src/vm/NativeObject-inl.h
@@ -197,33 +197,36 @@ NativeObject::initDenseElements(const Va
 
     memcpy(reinterpret_cast<Value*>(elements_), src, count * sizeof(Value));
     elementsRangeWriteBarrierPost(0, count);
 }
 
 inline bool
 NativeObject::tryShiftDenseElements(uint32_t count)
 {
+    MOZ_ASSERT(isExtensible());
+
     ObjectElements* header = getElementsHeader();
     if (header->initializedLength == count ||
         count > ObjectElements::MaxShiftedElements ||
         header->isCopyOnWrite() ||
-        !isExtensible() ||
         header->hasNonwritableArrayLength())
     {
         return false;
     }
 
     shiftDenseElementsUnchecked(count);
     return true;
 }
 
 inline void
 NativeObject::shiftDenseElementsUnchecked(uint32_t count)
 {
+    MOZ_ASSERT(isExtensible());
+
     ObjectElements* header = getElementsHeader();
     MOZ_ASSERT(count > 0);
     MOZ_ASSERT(count < header->initializedLength);
 
     if (MOZ_UNLIKELY(header->numShiftedElements() + count > ObjectElements::MaxShiftedElements)) {
         moveShiftedElements();
         header = getElementsHeader();
     }
@@ -315,16 +318,17 @@ NativeObject::reverseDenseElementsNoPreB
     elementsRangeWriteBarrierPost(0, length);
 }
 
 inline void
 NativeObject::ensureDenseInitializedLengthNoPackedCheck(uint32_t index, uint32_t extra)
 {
     MOZ_ASSERT(!denseElementsAreCopyOnWrite());
     MOZ_ASSERT(!denseElementsAreFrozen());
+    MOZ_ASSERT(isExtensible() || (containsDenseElement(index) && extra == 1));
 
     /*
      * Ensure that the array's contents have been initialized up to index, and
      * mark the elements through 'index + extra' as initialized in preparation
      * for a write.
      */
     MOZ_ASSERT(index + extra <= getDenseCapacity());
     uint32_t& initlen = getElementsHeader()->initializedLength;
@@ -341,16 +345,18 @@ NativeObject::ensureDenseInitializedLeng
         }
         initlen = index + extra;
     }
 }
 
 inline void
 NativeObject::ensureDenseInitializedLength(JSContext* cx, uint32_t index, uint32_t extra)
 {
+    MOZ_ASSERT(isExtensible());
+
     if (writeToIndexWouldMarkNotPacked(index))
         markDenseElementsNotPacked(cx);
     ensureDenseInitializedLengthNoPackedCheck(index, extra);
 }
 
 DenseElementResult
 NativeObject::extendDenseElements(JSContext* cx,
                                   uint32_t requiredCapacity, uint32_t extra)
@@ -380,16 +386,17 @@ NativeObject::extendDenseElements(JSCont
 
     return DenseElementResult::Success;
 }
 
 inline DenseElementResult
 NativeObject::ensureDenseElements(JSContext* cx, uint32_t index, uint32_t extra)
 {
     MOZ_ASSERT(isNative());
+    MOZ_ASSERT(isExtensible() || (containsDenseElement(index) && extra == 1));
 
     if (writeToIndexWouldMarkNotPacked(index))
         markDenseElementsNotPacked(cx);
 
     if (!maybeCopyElementsForWrite(cx))
         return DenseElementResult::Failure;
 
     uint32_t currentCapacity = getDenseCapacity();
--- a/js/src/vm/NativeObject.cpp
+++ b/js/src/vm/NativeObject.cpp
@@ -654,16 +654,18 @@ NativeObject::maybeDensifySparseElements
         return DenseElementResult::Failure;
 
     return DenseElementResult::Success;
 }
 
 void
 NativeObject::moveShiftedElements()
 {
+    MOZ_ASSERT(isExtensible());
+
     ObjectElements* header = getElementsHeader();
     uint32_t numShifted = header->numShiftedElements();
     MOZ_ASSERT(numShifted > 0);
 
     uint32_t initLength = header->initializedLength;
 
     ObjectElements* newHeader = static_cast<ObjectElements*>(getUnshiftedElementsHeader());
     memmove(newHeader, header, sizeof(ObjectElements));
@@ -686,43 +688,45 @@ NativeObject::moveShiftedElements()
     // make sure prepareElementRangeForOverwrite is called on the shifted
     // elements.
     setDenseInitializedLength(initLength);
 }
 
 void
 NativeObject::maybeMoveShiftedElements()
 {
+    MOZ_ASSERT(isExtensible());
+
     ObjectElements* header = getElementsHeader();
     MOZ_ASSERT(header->numShiftedElements() > 0);
 
     // Move the elements if less than a third of the allocated space is in use.
     if (header->capacity < header->numAllocatedElements() / 3)
         moveShiftedElements();
 }
 
 bool
 NativeObject::tryUnshiftDenseElements(uint32_t count)
 {
+    MOZ_ASSERT(isExtensible());
     MOZ_ASSERT(count > 0);
 
     ObjectElements* header = getElementsHeader();
     uint32_t numShifted = header->numShiftedElements();
 
     if (count > numShifted) {
         // We need more elements than are easily available. Try to make space
         // for more elements than we need (and shift the remaining ones) so
         // that unshifting more elements later will be fast.
 
         // Don't bother reserving elements if the number of elements is small.
         // Note that there's no technical reason for using this particular
         // limit.
         if (header->initializedLength <= 10 ||
             header->isCopyOnWrite() ||
-            !isExtensible() ||
             header->hasNonwritableArrayLength() ||
             MOZ_UNLIKELY(count > ObjectElements::MaxShiftedElements))
         {
             return false;
         }
 
         MOZ_ASSERT(header->capacity >= header->initializedLength);
         uint32_t unusedCapacity = header->capacity - header->initializedLength;
@@ -960,17 +964,17 @@ NativeObject::growElements(JSContext* cx
 
     return true;
 }
 
 void
 NativeObject::shrinkElements(JSContext* cx, uint32_t reqCapacity)
 {
     MOZ_ASSERT(canHaveNonEmptyElements());
-    MOZ_ASSERT(reqCapacity >= getDenseInitializedLength());
+    MOZ_ASSERT(reqCapacity >= getDenseInitializedLengthUnchecked());
 
     if (denseElementsAreCopyOnWrite())
         MOZ_CRASH();
 
     if (!hasDynamicElements())
         return;
 
     // If we have shifted elements, consider moving them.
--- a/js/src/vm/NativeObject.h
+++ b/js/src/vm/NativeObject.h
@@ -525,17 +525,30 @@ class NativeObject : public ShapedObject
     }
     const Value& getDenseElement(uint32_t idx) const {
         MOZ_ASSERT(idx < getDenseInitializedLength());
         return elements_[idx];
     }
     bool containsDenseElement(uint32_t idx) {
         return idx < getDenseInitializedLength() && !elements_[idx].isMagic(JS_ELEMENTS_HOLE);
     }
+
+  private:
+    uint32_t getDenseInitializedLengthUnchecked() const {
+        return getElementsHeader()->initializedLength;
+    }
+
+  public:
     uint32_t getDenseInitializedLength() const {
+        // If the following assertion fails, there's somewhere else a missing
+        // call to shrinkCapacityToInitializedLength(). Good luck for the hunt
+        // and finding the offender!
+        MOZ_ASSERT_IF(!isExtensible(),
+                      getElementsHeader()->initializedLength == getElementsHeader()->capacity);
+
         return getElementsHeader()->initializedLength;
     }
     uint32_t getDenseCapacity() const {
         return getElementsHeader()->capacity;
     }
 
     bool isSharedMemory() const {
         return getElementsHeader()->isSharedMemory();
@@ -1246,24 +1259,37 @@ class NativeObject : public ShapedObject
         uint32_t len = header->initializedLength;
         if (header->capacity > len) {
             shrinkElements(cx, len);
             header = getElementsHeader();
             header->capacity = len;
         }
     }
 
-    void setDenseInitializedLength(uint32_t length) {
+  private:
+    void setDenseInitializedLengthInternal(uint32_t length) {
         MOZ_ASSERT(length <= getDenseCapacity());
         MOZ_ASSERT(!denseElementsAreCopyOnWrite());
         MOZ_ASSERT(!denseElementsAreFrozen());
         prepareElementRangeForOverwrite(length, getElementsHeader()->initializedLength);
         getElementsHeader()->initializedLength = length;
     }
 
+  public:
+    void setDenseInitializedLength(uint32_t length) {
+        MOZ_ASSERT(isExtensible());
+        setDenseInitializedLengthInternal(length);
+    }
+
+    void setDenseInitializedLengthMaybeNonExtensible(JSContext* cx, uint32_t length) {
+        setDenseInitializedLengthInternal(length);
+        if (!isExtensible())
+            shrinkCapacityToInitializedLength(cx);
+    }
+
     inline void ensureDenseInitializedLength(JSContext* cx,
                                              uint32_t index, uint32_t extra);
 
     void setDenseElement(uint32_t index, const Value& val) {
         MOZ_ASSERT(index < getDenseInitializedLength());
         MOZ_ASSERT(!denseElementsAreCopyOnWrite());
         MOZ_ASSERT(!denseElementsAreFrozen());
         checkStoredValue(val);
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -952,22 +952,22 @@ public:
   private:
     nsFrameConstructorState& mState;
     PendingBinding* mPendingBinding;
   };
 
   /**
    * Add a new pending binding to the list
    */
-  void AddPendingBinding(PendingBinding* aPendingBinding)
+  void AddPendingBinding(UniquePtr<PendingBinding> aPendingBinding)
   {
     if (mCurrentPendingBindingInsertionPoint) {
-      mCurrentPendingBindingInsertionPoint->setPrevious(aPendingBinding);
+      mCurrentPendingBindingInsertionPoint->setPrevious(aPendingBinding.release());
     } else {
-      mPendingBindings.insertBack(aPendingBinding);
+      mPendingBindings.insertBack(aPendingBinding.release());
     }
   }
 
 protected:
   friend class nsFrameConstructorSaveState;
 
   /**
    * ProcessFrameInsertions takes the frames in aFrameItems and adds them as
@@ -3400,31 +3400,30 @@ FindAncestorWithGeneratedContentPseudo(n
 }
 
 #define SIMPLE_FCDATA(_func) FCDATA_DECL(0, _func)
 #define FULL_CTOR_FCDATA(_flags, _func)                             \
   { _flags | FCDATA_FUNC_IS_FULL_CTOR, { nullptr }, _func, nullptr }
 
 /* static */
 const nsCSSFrameConstructor::FrameConstructionData*
-nsCSSFrameConstructor::FindTextData(nsIFrame* aParentFrame,
-                                    nsIContent* aTextContent)
-{
-  MOZ_ASSERT(aTextContent, "How?");
+nsCSSFrameConstructor::FindTextData(const Text& aTextContent,
+                                    nsIFrame* aParentFrame)
+{
   if (aParentFrame && IsFrameForSVG(aParentFrame)) {
     nsIFrame* ancestorFrame =
       nsSVGUtils::GetFirstNonAAncestorFrame(aParentFrame);
     if (!ancestorFrame || !nsSVGUtils::IsInSVGTextSubtree(ancestorFrame)) {
       return nullptr;
     }
 
     // Don't render stuff in display: contents / Shadow DOM subtrees, because
     // TextCorrespondenceRecorder in the SVG text code doesn't really know how
     // to deal with it. This kinda sucks. :(
-    if (aParentFrame->GetContent() != aTextContent->GetParent()) {
+    if (aParentFrame->GetContent() != aTextContent.GetParent()) {
       return nullptr;
     }
 
     static const FrameConstructionData sSVGTextData =
       FCDATA_DECL(FCDATA_IS_LINE_PARTICIPANT | FCDATA_IS_SVG_TEXT,
                   NS_NewTextFrame);
     return &sSVGTextData;
   }
@@ -3548,22 +3547,17 @@ IsFrameForFieldSet(nsIFrame* aFrame)
 }
 
 /* static */
 const nsCSSFrameConstructor::FrameConstructionData*
 nsCSSFrameConstructor::FindHTMLData(const Element& aElement,
                                     nsIFrame* aParentFrame,
                                     ComputedStyle& aStyle)
 {
-  // Ignore the tag if it's not HTML content and if it doesn't extend (via XBL)
-  // a valid HTML namespace.  This check must match the one in
-  // ShouldHaveFirstLineStyle.
-  if (!aElement.IsHTMLElement()) {
-    return nullptr;
-  }
+  MOZ_ASSERT(aElement.IsHTMLElement());
 
   nsAtom* tag = aElement.NodeInfo()->NameAtom();
   NS_ASSERTION(!aParentFrame ||
                aParentFrame->Style()->GetPseudo() !=
                  nsCSSAnonBoxes::fieldsetContent ||
                aParentFrame->GetParent()->IsFieldSetFrame(),
                "Unexpected parent for fieldset content anon box");
   if (tag == nsGkAtoms::legend &&
@@ -4190,22 +4184,19 @@ nsIFrame* NS_NewGridBoxFrame(nsIPresShel
   NS_NewGridLayout2(aPresShell, getter_AddRefs(layout));
   return NS_NewBoxFrame(aPresShell, aComputedStyle, false, layout);
 }
 
 /* static */
 const nsCSSFrameConstructor::FrameConstructionData*
 nsCSSFrameConstructor::FindXULTagData(const Element& aElement,
                                       nsAtom* aTag,
-                                      int32_t aNameSpaceID,
                                       ComputedStyle& aStyle)
 {
-  if (aNameSpaceID != kNameSpaceID_XUL) {
-    return nullptr;
-  }
+  MOZ_ASSERT(aElement.IsXULElement());
 
   static const FrameConstructionDataByTag sXULTagData[] = {
 #ifdef MOZ_XUL
     SCROLLABLE_XUL_CREATE(button, NS_NewButtonBoxFrame),
     SCROLLABLE_XUL_CREATE(thumb, NS_NewButtonBoxFrame),
     SCROLLABLE_XUL_CREATE(checkbox, NS_NewButtonBoxFrame),
     SCROLLABLE_XUL_CREATE(radio, NS_NewButtonBoxFrame),
     SCROLLABLE_XUL_CREATE(titlebar, NS_NewTitleBarFrame),
@@ -4923,20 +4914,17 @@ nsCSSFrameConstructor::FlushAccumulatedB
                   FCDATA_FORCE_NULL_ABSPOS_CONTAINER |                  \
                   FCDATA_WRAP_KIDS_IN_BLOCKS, _func) }
 
 /* static */
 const nsCSSFrameConstructor::FrameConstructionData*
 nsCSSFrameConstructor::FindMathMLData(const Element& aElement,
                                       ComputedStyle& aStyle)
 {
-  // Make sure that we remain confined in the MathML world
-  if (!aElement.IsMathMLElement()) {
-    return nullptr;
-  }
+  MOZ_ASSERT(aElement.IsMathMLElement());
 
   nsAtom* tag = aElement.NodeInfo()->NameAtom();
 
   // Handle <math> specially, because it sometimes produces inlines
   if (tag == nsGkAtoms::math) {
     // This needs to match the test in EnsureBlockDisplay in
     // nsRuleNode.cpp.  Though the behavior here for the display:table
     // case is pretty weird...
@@ -5111,19 +5099,17 @@ IsFilterPrimitiveChildTag(const nsAtom* 
 /* static */
 const nsCSSFrameConstructor::FrameConstructionData*
 nsCSSFrameConstructor::FindSVGData(const Element& aElement,
                                    nsIFrame* aParentFrame,
                                    bool aIsWithinSVGText,
                                    bool aAllowsTextPathChild,
                                    ComputedStyle& aStyle)
 {
-  if (!aElement.IsSVGElement()) {
-    return nullptr;
-  }
+  MOZ_ASSERT(aElement.IsSVGElement());
 
   static const FrameConstructionData sSuppressData = SUPPRESS_FCDATA();
   static const FrameConstructionData sContainerData =
     SIMPLE_SVG_FCDATA(NS_NewSVGContainerFrame);
 
   bool parentIsSVG = aIsWithinSVGText;
   nsIContent* parentContent =
     aParentFrame ? aParentFrame->GetContent() : nullptr;
@@ -5427,139 +5413,270 @@ nsCSSFrameConstructor::AddFrameConstruct
 
 // Whether we should suppress frames for a child under a <select> frame.
 //
 // Never create frames for non-option/optgroup kids of <select> and non-option
 // kids of <optgroup> inside a <select>.
 // XXXbz it's not clear how this should best work with XBL.
 static bool
 ShouldSuppressFrameInSelect(const nsIContent* aParent,
-                            const nsIContent* aChild)
+                            const nsIContent& aChild)
 {
   if (!aParent ||
       !aParent->IsAnyOfHTMLElements(nsGkAtoms::select, nsGkAtoms::optgroup)) {
     return false;
   }
 
   // If we're in any display: contents subtree, just suppress the frame.
   //
   // We can't be regular NAC, since display: contents has no frame to generate
   // them off.
-  if (aChild->GetParent() != aParent) {
+  if (aChild.GetParent() != aParent) {
     return true;
   }
 
   // Option is always fine.
-  if (aChild->IsHTMLElement(nsGkAtoms::option)) {
+  if (aChild.IsHTMLElement(nsGkAtoms::option)) {
     return false;
   }
 
   // <optgroup> is OK in <select> but not in <optgroup>.
-  if (aChild->IsHTMLElement(nsGkAtoms::optgroup) &&
+  if (aChild.IsHTMLElement(nsGkAtoms::optgroup) &&
       aParent->IsHTMLElement(nsGkAtoms::select)) {
     return false;
   }
 
   // Allow native anonymous content no matter what.
-  if (aChild->IsRootOfAnonymousSubtree()) {
+  if (aChild.IsRootOfAnonymousSubtree()) {
     return false;
   }
 
   return true;
 }
 
 static bool
 ShouldSuppressFrameInNonOpenDetails(const HTMLDetailsElement* aDetails,
-                                    const nsIContent* aChild)
+                                    const nsIContent& aChild)
 {
   if (!aDetails || aDetails->Open()) {
     return false;
   }
 
-  if (aChild->GetParent() != aDetails) {
+  if (aChild.GetParent() != aDetails) {
     return true;
   }
 
   auto* summary = HTMLSummaryElement::FromNode(aChild);
   if (summary && summary->IsMainSummary()) {
     return false;
   }
 
   // Don't suppress NAC, unless it's ::before or ::after.
-  if (aChild->IsRootOfAnonymousSubtree() &&
-      !aChild->IsGeneratedContentContainerForBefore() &&
-      !aChild->IsGeneratedContentContainerForAfter()) {
+  if (aChild.IsRootOfAnonymousSubtree() &&
+      !aChild.IsGeneratedContentContainerForBefore() &&
+      !aChild.IsGeneratedContentContainerForAfter()) {
     return false;
   }
 
   return true;
 }
 
+const nsCSSFrameConstructor::FrameConstructionData*
+nsCSSFrameConstructor::FindDataForContent(nsIContent& aContent,
+                                          ComputedStyle& aStyle,
+                                          nsIFrame* aParentFrame,
+                                          nsAtom* aTag,
+                                          uint32_t aFlags)
+{
+  MOZ_ASSERT(aStyle.StyleDisplay()->mDisplay != StyleDisplay::None &&
+             aStyle.StyleDisplay()->mDisplay != StyleDisplay::Contents,
+             "These two special display values should be handled earlier");
+
+  if (auto* text = Text::FromNode(aContent)) {
+    return FindTextData(*text, aParentFrame);
+  }
+
+  return FindElementData(*aContent.AsElement(),
+                         aStyle,
+                         aParentFrame,
+                         aTag,
+                         aFlags);
+}
+
+const nsCSSFrameConstructor::FrameConstructionData*
+nsCSSFrameConstructor::FindElementData(const Element& aElement,
+                                       ComputedStyle& aStyle,
+                                       nsIFrame* aParentFrame,
+                                       nsAtom* aTag,
+                                       uint32_t aFlags)
+{
+  // Don't create frames for non-SVG element children of SVG elements.
+  if (!aElement.IsSVGElement()) {
+    if (aParentFrame && IsFrameForSVG(aParentFrame) &&
+        !aParentFrame->IsFrameOfType(nsIFrame::eSVGForeignObject)) {
+      return nullptr;
+    }
+    if (aFlags & ITEM_IS_WITHIN_SVG_TEXT) {
+      return nullptr;
+    }
+  }
+
+  if (auto* data = FindElementTagData(aElement, aStyle, aParentFrame, aTag, aFlags)) {
+    return data;
+  }
+
+  // Check for 'content: <image-url>' on the element (which makes us ignore
+  // 'display' values other than 'none' or 'contents').
+  if (ShouldCreateImageFrameForContent(aElement, aStyle)) {
+    static const FrameConstructionData sImgData =
+      SIMPLE_FCDATA(NS_NewImageFrameForContentProperty);
+    return &sImgData;
+  }
+
+  const auto& display = *aStyle.StyleDisplay();
+  if (auto* data = FindXULDisplayData(display, aElement)) {
+    return data;
+  }
+
+  return FindDisplayData(display, aElement);
+}
+
+const nsCSSFrameConstructor::FrameConstructionData*
+nsCSSFrameConstructor::FindElementTagData(const Element& aElement,
+                                          ComputedStyle& aStyle,
+                                          nsIFrame* aParentFrame,
+                                          nsAtom* aTag,
+                                          uint32_t aFlags)
+{
+  switch (aElement.GetNameSpaceID()) {
+    case kNameSpaceID_XHTML:
+      return FindHTMLData(aElement, aParentFrame, aStyle);
+    case kNameSpaceID_MathML:
+      return FindMathMLData(aElement, aStyle);
+    case kNameSpaceID_SVG:
+      return FindSVGData(aElement,
+                         aParentFrame,
+                         aFlags & ITEM_IS_WITHIN_SVG_TEXT,
+                         aFlags & ITEM_ALLOWS_TEXT_PATH_CHILD,
+                         aStyle);
+    case kNameSpaceID_XUL:
+      return FindXULTagData(aElement, aTag, aStyle);
+    default:
+      return nullptr;
+  }
+}
+
+nsCSSFrameConstructor::XBLBindingLoadInfo::XBLBindingLoadInfo(
+  already_AddRefed<ComputedStyle> aStyle,
+  mozilla::UniquePtr<PendingBinding> aPendingBinding,
+  nsAtom* aTag)
+  : mStyle(aStyle)
+  , mPendingBinding(std::move(aPendingBinding))
+  , mTag(aTag)
+{
+  MOZ_ASSERT(mTag);
+  MOZ_ASSERT(mStyle);
+}
+
+nsCSSFrameConstructor::XBLBindingLoadInfo::XBLBindingLoadInfo(nsIContent& aContent,
+                                                              ComputedStyle& aStyle)
+  : mStyle(&aStyle)
+  , mPendingBinding(nullptr)
+  , mTag(aContent.NodeInfo()->NameAtom())
+{
+}
+
+nsCSSFrameConstructor::XBLBindingLoadInfo::XBLBindingLoadInfo() = default;
+
+nsCSSFrameConstructor::XBLBindingLoadInfo
+nsCSSFrameConstructor::LoadXBLBindingIfNeeded(nsIContent& aContent,
+                                              ComputedStyle& aStyle,
+                                              uint32_t aFlags)
+{
+  if (!(aFlags & ITEM_ALLOW_XBL_BASE)) {
+    return { aContent, aStyle };
+  }
+  css::URLValue* binding = aStyle.StyleDisplay()->mBinding;
+  if (!binding) {
+    return { aContent, aStyle };
+  }
+
+  nsXBLService* xblService = nsXBLService::GetInstance();
+  if (!xblService) {
+    return { };
+  }
+
+  auto newPendingBinding = MakeUnique<PendingBinding>();
+
+  bool resolveStyle;
+  nsresult rv = xblService->LoadBindings(aContent.AsElement(),
+                                         binding->GetURI(),
+                                         binding->mExtraData->GetPrincipal(),
+                                         getter_AddRefs(newPendingBinding->mBinding),
+                                         &resolveStyle);
+  if (NS_FAILED(rv)) {
+    if (rv == NS_ERROR_XBL_BLOCKED) {
+      return { aContent, aStyle };
+    }
+    return { };
+  }
+
+  RefPtr<ComputedStyle> style = resolveStyle
+    ? mPresShell->StyleSet()->ResolveServoStyle(*aContent.AsElement())
+    : do_AddRef(&aStyle);
+
+  nsAtom* tag = aContent.NodeInfo()->NameAtom();
+  if (aContent.IsXULElement()) {
+    int32_t overridenNamespace;
+    nsAtom* overridenTag =
+      mDocument->BindingManager()->ResolveTag(&aContent, &overridenNamespace);
+    // Only allow overriding from & to XUL.
+    if (overridenNamespace == kNameSpaceID_XUL) {
+      tag = overridenTag;
+    }
+  }
+
+  return { style.forget(), std::move(newPendingBinding), tag };
+}
+
+
 void
 nsCSSFrameConstructor::AddFrameConstructionItemsInternal(nsFrameConstructorState& aState,
                                                          nsIContent* aContent,
                                                          nsContainerFrame* aParentFrame,
                                                          bool aSuppressWhiteSpaceOptimizations,
                                                          ComputedStyle* aComputedStyle,
                                                          uint32_t aFlags,
                                                          FrameConstructionItemList& aItems)
 {
   MOZ_ASSERT(aContent->IsText() || aContent->IsElement(),
              "Shouldn't get anything else here!");
   MOZ_ASSERT(aContent->IsInComposedDoc());
   MOZ_ASSERT(!aContent->GetPrimaryFrame() || aState.mCreatingExtraFrames ||
              aContent->NodeInfo()->NameAtom() == nsGkAtoms::area);
 
-  // The following code allows the user to specify the base tag of an element
-  // using XBL. XUL elements can then be extended arbitrarily.
-  RefPtr<ComputedStyle> style = aComputedStyle;
   PendingBinding* pendingBinding = nullptr;
-  nsAtom* tag = aContent->NodeInfo()->NameAtom();
-  int32_t namespaceId = aContent->GetNameSpaceID();
-  if (aFlags & ITEM_ALLOW_XBL_BASE) {
-    if (css::URLValue* binding = style->StyleDisplay()->mBinding) {
-      // Ensure that our XBL bindings are installed.
-
-      nsXBLService* xblService = nsXBLService::GetInstance();
-      if (!xblService) {
-        return;
-      }
-
-      auto newPendingBinding = MakeUnique<PendingBinding>();
-      bool resolveStyle;
-
-      nsresult rv = xblService->LoadBindings(
-        aContent->AsElement(), binding->GetURI(),
-        binding->mExtraData->GetPrincipal(),
-        getter_AddRefs(newPendingBinding->mBinding), &resolveStyle);
-      if (NS_FAILED(rv) && rv != NS_ERROR_XBL_BLOCKED)
-        return;
-
-      if (newPendingBinding->mBinding) {
-        pendingBinding = newPendingBinding.get();
-        // aState takes over owning newPendingBinding
-        aState.AddPendingBinding(newPendingBinding.release());
-      }
-
-      if (resolveStyle) {
-        style =
-          mPresShell->StyleSet()->ResolveServoStyle(*aContent->AsElement());
-      }
-
-      aComputedStyle = style;
-      if (namespaceId == kNameSpaceID_XUL) {
-        // Only allow overriding from & to XUL.
-        int32_t overridenNamespace;
-        nsAtom* overridenTag =
-          mDocument->BindingManager()->ResolveTag(aContent, &overridenNamespace);
-        if (overridenNamespace == kNameSpaceID_XUL) {
-          tag = overridenTag;
-        }
-      }
-    }
+  RefPtr<ComputedStyle> style;
+  nsAtom* tag;
+  {
+    XBLBindingLoadInfo xblInfo =
+      LoadXBLBindingIfNeeded(*aContent, *aComputedStyle, aFlags);
+    if (!xblInfo.mTag) {
+      return;
+    }
+
+    if (xblInfo.mPendingBinding && xblInfo.mPendingBinding->mBinding) {
+      pendingBinding = xblInfo.mPendingBinding.get();
+      aState.AddPendingBinding(std::move(xblInfo.mPendingBinding));
+    }
+
+    style = xblInfo.mStyle.forget();
+    aComputedStyle = style.get();
+
+    tag = xblInfo.mTag;
   }
 
   const bool isGeneratedContent = !!(aFlags & ITEM_IS_GENERATED_CONTENT);
   MOZ_ASSERT(!isGeneratedContent || style->GetPseudo(),
              "Generated content should be a pseudo-element");
 
   FrameConstructionItem* item = nullptr;
   auto cleanupGeneratedContent = mozilla::MakeScopeExit([&]() {
@@ -5594,105 +5711,52 @@ nsCSSFrameConstructor::AddFrameConstruct
     }
     aItems.SetParentHasNoXBLChildren(!iter.XBLInvolved());
 
     CreateGeneratedContentItem(aState, aParentFrame, *aContent->AsElement(),
                                *style, CSSPseudoElementType::after, aItems);
     return;
   }
 
+
   nsIContent* parent = aParentFrame ? aParentFrame->GetContent() : nullptr;
-  if (ShouldSuppressFrameInSelect(parent, aContent)) {
+  if (ShouldSuppressFrameInSelect(parent, *aContent)) {
     return;
   }
 
   // When constructing a child of a non-open <details>, create only the frame
   // for the main <summary> element, and skip other elements.  This only applies
   // to things that are not roots of native anonymous subtrees (except for
   // ::before and ::after); we always want to create "internal" anonymous
   // content.
   auto* details = HTMLDetailsElement::FromNodeOrNull(parent);
-  if (ShouldSuppressFrameInNonOpenDetails(details, aContent)) {
+  if (ShouldSuppressFrameInNonOpenDetails(details, *aContent)) {
+    return;
+  }
+
+  const FrameConstructionData* data =
+    FindDataForContent(*aContent, *style, aParentFrame, tag, aFlags);
+  if (!data || data->mBits & FCDATA_SUPPRESS_FRAME) {
     return;
   }
 
   bool isPopup = false;
-  const bool isText = !aContent->IsElement();
-  // Try to find frame construction data for this content
-  const FrameConstructionData* data;
-  if (isText) {
-    data = FindTextData(aParentFrame, aContent);
-    if (!data) {
-      // Nothing to do here; suppressed text inside SVG
-      return;
-    }
-  } else {
-    Element& element = *aContent->AsElement();
-
-    // Don't create frames for non-SVG element children of SVG elements.
-    if (namespaceId != kNameSpaceID_SVG &&
-        ((aParentFrame &&
-          IsFrameForSVG(aParentFrame) &&
-          !aParentFrame->IsFrameOfType(nsIFrame::eSVGForeignObject)) ||
-         (aFlags & ITEM_IS_WITHIN_SVG_TEXT))) {
-      return;
-    }
-
-    data = FindHTMLData(element, aParentFrame, *style);
-    if (!data) {
-      data = FindXULTagData(element, tag, namespaceId, *style);
-    }
-    if (!data) {
-      data = FindMathMLData(element, *style);
-    }
-    if (!data) {
-      data = FindSVGData(element,
-                         aParentFrame,
-                         aFlags & ITEM_IS_WITHIN_SVG_TEXT,
-                         aFlags & ITEM_ALLOWS_TEXT_PATH_CHILD,
-                         *style);
-    }
-
-    // Check for 'content: <image-url>' on the element (which makes us ignore
-    // 'display' values other than 'none' or 'contents').
-    if (!data && ShouldCreateImageFrameForContent(element, *style)) {
-      static const FrameConstructionData sImgData =
-        SIMPLE_FCDATA(NS_NewImageFrameForContentProperty);
-      data = &sImgData;
-    }
-
-    // Now check for XUL display types
-    if (!data) {
-      data = FindXULDisplayData(display, element);
-    }
-
-    // And general display types
-    if (!data) {
-      data = FindDisplayData(display, element);
-    }
-
-    MOZ_ASSERT(data, "Should have frame construction data now");
-
-    if (data->mBits & FCDATA_SUPPRESS_FRAME) {
-      return;
-    }
 
 #ifdef MOZ_XUL
-    if ((data->mBits & FCDATA_IS_POPUP) &&
-        (!aParentFrame || // Parent is inline
-         !aParentFrame->IsMenuFrame())) {
-      if (!aState.mPopupItems.containingBlock &&
-          !aState.mHavePendingPopupgroup) {
-        return;
-      }
-
-      isPopup = true;
-    }
+  if ((data->mBits & FCDATA_IS_POPUP) &&
+      (!aParentFrame || // Parent is inline
+       !aParentFrame->IsMenuFrame())) {
+    if (!aState.mPopupItems.containingBlock &&
+        !aState.mHavePendingPopupgroup) {
+      return;
+    }
+
+    isPopup = true;
+  }
 #endif /* MOZ_XUL */
-  }
 
   uint32_t bits = data->mBits;
 
   // Inside colgroups, suppress everything except columns.
   if (aParentFrame && aParentFrame->IsTableColGroupFrame() &&
       (!(bits & FCDATA_IS_TABLE_PART) ||
        display.mDisplay != StyleDisplay::TableColumn)) {
     return;
@@ -5719,46 +5783,44 @@ nsCSSFrameConstructor::AddFrameConstruct
     }
   }
 
   if (!item) {
     item = aItems.AppendItem(this, data, aContent, pendingBinding,
                              style.forget(),
                              aSuppressWhiteSpaceOptimizations);
   }
-  item->mIsText = isText;
+  item->mIsText = !aContent->IsElement();
   item->mIsGeneratedContent = isGeneratedContent;
   item->mIsAnonymousContentCreatorContent =
     aFlags & ITEM_IS_ANONYMOUSCONTENTCREATOR_CONTENT;
   if (isGeneratedContent) {
     // We need to keep this alive until the frame takes ownership.
     // This corresponds to the Release in ConstructFramesFromItem.
     item->mContent->AddRef();
   }
   item->mIsRootPopupgroup =
-    namespaceId == kNameSpaceID_XUL && tag == nsGkAtoms::popupgroup &&
-    aContent->IsRootOfNativeAnonymousSubtree();
+    aContent->IsRootOfNativeAnonymousSubtree() &&
+    aContent->IsXULElement() &&
+    tag == nsGkAtoms::popupgroup;
   if (item->mIsRootPopupgroup) {
     aState.mHavePendingPopupgroup = true;
   }
   item->mIsPopup = isPopup;
-  item->mIsForSVGAElement = namespaceId == kNameSpaceID_SVG &&
-                            tag == nsGkAtoms::a;
 
   if (canHavePageBreak && display.mBreakAfter) {
     AddPageBreakItem(aContent, aItems);
   }
 
   if (bits & FCDATA_IS_INLINE) {
     // To correctly set item->mIsAllInline we need to build up our child items
     // right now.
     BuildInlineChildItems(aState, *item,
                           aFlags & ITEM_IS_WITHIN_SVG_TEXT,
                           aFlags & ITEM_ALLOWS_TEXT_PATH_CHILD);
-    item->mHasInlineEnds = true;
     item->mIsBlock = false;
   } else {
     // Compute a boolean isInline which is guaranteed to be false for blocks
     // (but may also be false for some inlines).
     bool isInline =
       // Table-internal things are inline-outside if and only if they're kids of
       // inlines, since they'll trigger construction of inline-table
       // pseudos.
@@ -5769,30 +5831,29 @@ nsCSSFrameConstructor::AddFrameConstruct
       display.IsInlineOutsideStyle() ||
       // Popups that are certainly out of flow.
       isPopup;
 
     // Set mIsAllInline conservatively.  It just might be that even an inline
     // that has mIsAllInline false doesn't need an {ib} split.  So this is just
     // an optimization to keep from doing too much work in cases when we can
     // show that mIsAllInline is true..
-    item->mIsAllInline = item->mHasInlineEnds = isInline ||
+    item->mIsAllInline = isInline ||
       // Figure out whether we're guaranteed this item will be out of flow.
       // This is not a precise test, since one of our ancestor inlines might add
       // an absolute containing block (if it's relatively positioned) when there
       // wasn't such a containing block before.  But it's conservative in the
       // sense that anything that will really end up as an in-flow non-inline
       // will test false here.  In other words, if this test is true we're
       // guaranteed to be inline; if it's false we don't know what we'll end up
       // as.
       //
       // If we make this test precise, we can remove some of the code dealing
       // with the imprecision in ConstructInline and adjust the comments on
-      // mIsAllInline and mIsBlock in the header.  And probably remove mIsBlock
-      // altogether, since then it will always be equal to !mHasInlineEnds.
+      // mIsAllInline and mIsBlock in the header.
       (!(bits & FCDATA_DISALLOW_OUT_OF_FLOW) &&
        aState.GetGeometricParent(display, nullptr));
 
     // Set mIsBlock conservatively.  It's OK to set it false for some real
     // blocks, but not OK to set it true for things that aren't blocks.  Since
     // isOutOfFlow might be false even in cases when the frame will end up
     // out-of-flow, we can't use it here.  But we _can_ say that the frame will
     // for sure end up in-flow if it's not floated or absolutely positioned.
@@ -9276,17 +9337,17 @@ nsCSSFrameConstructor::CreateNeededAnonF
       new (this) FrameConstructionItem(&sBlockFormattingContextFCData,
                                 // Use the content of our parent frame
                                 parentContent,
                                 // no pending binding
                                 nullptr,
                                 wrapperStyle,
                                 true);
 
-    newItem->mIsAllInline = newItem->mHasInlineEnds =
+    newItem->mIsAllInline =
       newItem->mComputedStyle->StyleDisplay()->IsInlineOutsideStyle();
     newItem->mIsBlock = !newItem->mIsAllInline;
 
     MOZ_ASSERT(!newItem->mIsAllInline && newItem->mIsBlock,
                "expecting anonymous flex/grid items to be block-level "
                "(this will make a difference when we encounter "
                "'align-items: baseline')");
 
@@ -9784,18 +9845,17 @@ nsCSSFrameConstructor::WrapItemsInPseudo
                               true);
 
   const nsStyleDisplay* disp = newItem->mComputedStyle->StyleDisplay();
   // Here we're cheating a tad... technically, table-internal items should be
   // inline if aParentFrame is inline, but they'll get wrapped in an
   // inline-table in the end, so it'll all work out.  In any case, arguably
   // we don't need to maintain this state at this point... but it's better
   // to, I guess.
-  newItem->mIsAllInline = newItem->mHasInlineEnds =
-    disp->IsInlineOutsideStyle();
+  newItem->mIsAllInline = disp->IsInlineOutsideStyle();
 
   bool isRuby = disp->IsRubyDisplayType();
   // All types of ruby frames need a block frame to provide line layout,
   // hence they are always line participant.
   newItem->mIsLineParticipant = isRuby;
 
   if (!isRuby) {
     // Table pseudo frames always induce line boundaries around their
@@ -11211,17 +11271,18 @@ nsCSSFrameConstructor::BuildInlineChildI
                                *parentComputedStyle, CSSPseudoElementType::before,
                                aParentItem.mChildItems);
   }
 
   uint32_t flags = ITEM_ALLOW_XBL_BASE | ITEM_ALLOW_PAGE_BREAK;
   if (aItemIsWithinSVGText) {
     flags |= ITEM_IS_WITHIN_SVG_TEXT;
   }
-  if (aItemAllowsTextPathChild && aParentItem.mIsForSVGAElement) {
+  if (aItemAllowsTextPathChild &&
+      aParentItem.mContent->IsSVGElement(nsGkAtoms::a)) {
     flags |= ITEM_ALLOWS_TEXT_PATH_CHILD;
   }
 
   FlattenedChildIterator iter(parentContent);
   for (nsIContent* content = iter.GetNextChild(); content; content = iter.GetNextChild()) {
     // Manually check for comments/PIs, since we don't have a frame to pass to
     // AddFrameConstructionItems.  We know our parent is a non-replaced inline,
     // so there is no need to do the NeedFrameFor check.
--- a/layout/base/nsCSSFrameConstructor.h
+++ b/layout/base/nsCSSFrameConstructor.h
@@ -41,27 +41,29 @@ class nsFrameConstructorState;
 
 namespace mozilla {
 
 class ComputedStyle;
 
 namespace dom {
 
 class CharacterData;
+class Text;
 class FlattenedChildIterator;
 
 } // namespace dom
 } // namespace mozilla
 
 class nsCSSFrameConstructor final : public nsFrameManager
 {
 public:
   typedef mozilla::ComputedStyle ComputedStyle;
   typedef mozilla::CSSPseudoElementType CSSPseudoElementType;
   typedef mozilla::dom::Element Element;
+  typedef mozilla::dom::Text Text;
 
   // FIXME(emilio): Is this really needed?
   friend class mozilla::RestyleManager;
 
   nsCSSFrameConstructor(nsIDocument* aDocument, nsIPresShell* aPresShell);
   ~nsCSSFrameConstructor() {
     MOZ_ASSERT(mFCItemsInUse == 0);
   }
@@ -775,16 +777,64 @@ private:
   struct PseudoParentData {
     const FrameConstructionData mFCData;
     nsICSSAnonBoxPseudo * const * const mPseudoType;
   };
   /* Array of such structures that we use to properly construct table
      pseudo-frames as needed */
   static const PseudoParentData sPseudoParentData[eParentTypeCount];
 
+  // The information that concerns the frame constructor after loading an XBL
+  // binding.
+  //
+  // This is expected to just be used temporarily to aggregate the different
+  // objects that LoadXBLBindingIfNeeded returns.
+  struct MOZ_STACK_CLASS XBLBindingLoadInfo
+  {
+    RefPtr<ComputedStyle> mStyle;
+    mozilla::UniquePtr<PendingBinding> mPendingBinding;
+    nsAtom* mTag = nullptr;
+
+    // For the 'no binding loaded' case.
+    XBLBindingLoadInfo(nsIContent&, ComputedStyle&);
+
+    // For the case we actually load an XBL binding.
+    XBLBindingLoadInfo(already_AddRefed<ComputedStyle> aStyle,
+                       mozilla::UniquePtr<PendingBinding> aPendingBinding,
+                       nsAtom* aTag);
+
+    // For the error case.
+    XBLBindingLoadInfo();
+  };
+
+  // Returns null mStyle / mTag members to signal an error.
+  XBLBindingLoadInfo LoadXBLBindingIfNeeded(nsIContent&,
+                                            ComputedStyle&,
+                                            uint32_t aFlags);
+
+  const FrameConstructionData* FindDataForContent(nsIContent&,
+                                                  ComputedStyle&,
+                                                  nsIFrame* aParentFrame,
+                                                  nsAtom* aTag,
+                                                  uint32_t aFlags);
+
+  // aParentFrame might be null.  If it is, that means it was an inline frame.
+  static const FrameConstructionData* FindTextData(const Text&,
+                                                   nsIFrame* aParentFrame);
+  const FrameConstructionData* FindElementData(const Element&,
+                                               ComputedStyle&,
+                                               nsIFrame* aParentFrame,
+                                               nsAtom* aTag,
+                                               uint32_t aFlags);
+  const FrameConstructionData* FindElementTagData(const Element&,
+                                                  ComputedStyle&,
+                                                  nsIFrame* aParentFrame,
+                                                  nsAtom* aTag,
+                                                  uint32_t aFlags);
+
   /* A function that takes an integer, content, style, and array of
      FrameConstructionDataByInts and finds the appropriate frame construction
      data to use and returns it.  This can return null if none of the integers
      match or if the matching integer has a FrameConstructionDataGetter that
      returns null. */
   static const FrameConstructionData*
     FindDataByInt(int32_t aInt,
                   const Element&,
@@ -1099,18 +1149,18 @@ private:
                           already_AddRefed<ComputedStyle>& aComputedStyle,
                           bool aSuppressWhiteSpaceOptimizations)
     : mFCData(aFCData), mContent(aContent),
       mPendingBinding(aPendingBinding), mComputedStyle(aComputedStyle),
       mSuppressWhiteSpaceOptimizations(aSuppressWhiteSpaceOptimizations),
       mIsText(false), mIsGeneratedContent(false),
       mIsAnonymousContentCreatorContent(false),
       mIsRootPopupgroup(false), mIsAllInline(false), mIsBlock(false),
-      mHasInlineEnds(false), mIsPopup(false),
-      mIsLineParticipant(false), mIsForSVGAElement(false)
+      mIsPopup(false),
+      mIsLineParticipant(false)
     {
       MOZ_COUNT_CTOR(FrameConstructionItem);
     }
 
     void* operator new(size_t, nsCSSFrameConstructor* aFCtor)
     { return aFCtor->AllocateFCItem(); }
 
     void Delete(nsCSSFrameConstructor* aFCtor)
@@ -1181,28 +1231,21 @@ private:
     // they might still all be inline.
     bool mIsAllInline:1;
     // Whether construction from this item will create only frames that are
     // IsBlockOutside() in the principal child list.  This is not precise, but
     // conservative: if true the frames will really be blocks, whereas if false
     // they might still be blocks (and in particular, out-of-flows that didn't
     // find a containing block).
     bool mIsBlock:1;
-    // Whether construction from this item will give leading and trailing
-    // inline frames.  This is equal to mIsAllInline, except for inline frame
-    // items, where it's always true, whereas mIsAllInline might be false due
-    // to {ib} splits.
-    bool mHasInlineEnds:1;
     // Whether construction from this item will create a popup that needs to
     // go into the global popup items.
     bool mIsPopup:1;
     // Whether this item should be treated as a line participant
     bool mIsLineParticipant:1;
-    // Whether this item is for an SVG <a> element
-    bool mIsForSVGAElement:1;
 
   private:
     // Not allocated from the general heap - instead, use the new/Delete APIs
     // that take a nsCSSFrameConstructor* (which manages our arena allocation).
     void* operator new(size_t) = delete;
     void* operator new[](size_t) = delete;
 #ifdef _MSC_VER  /* Visual Studio */
     void operator delete(void*) { MOZ_CRASH("FrameConstructionItem::delete"); }
@@ -1380,21 +1423,16 @@ private:
   // ConstructDetailsFrame puts the new frame in aFrameItems and
   // handles the kids of the details.
   nsIFrame* ConstructDetailsFrame(nsFrameConstructorState& aState,
                                   FrameConstructionItem& aItem,
                                   nsContainerFrame* aParentFrame,
                                   const nsStyleDisplay* aStyleDisplay,
                                   nsFrameItems& aFrameItems);
 
-  // aParentFrame might be null.  If it is, that means it was an
-  // inline frame.
-  static const FrameConstructionData* FindTextData(nsIFrame* aParentFrame,
-                                                   nsIContent* aTextContent);
-
   void ConstructTextFrame(const FrameConstructionData* aData,
                           nsFrameConstructorState& aState,
                           nsIContent*              aContent,
                           nsContainerFrame*        aParentFrame,
                           ComputedStyle*          aComputedStyle,
                           nsFrameItems&            aFrameItems);
 
   // If aPossibleTextContent is a text node and doesn't have a frame, append a
@@ -1504,17 +1542,16 @@ private:
 
   // Function to find FrameConstructionData for an element.  Will return
   // null if the element is not XUL.
   //
   // NOTE(emilio): This gets the overloaded tag and namespace id since they can
   // be overriden by extends="" in XBL.
   static const FrameConstructionData* FindXULTagData(const Element&,
                                                      nsAtom* aTag,
-                                                     int32_t aNameSpaceID,
                                                      ComputedStyle&);
   // XUL data-finding helper functions and structures
 #ifdef MOZ_XUL
   static const FrameConstructionData* FindPopupGroupData(const Element&, ComputedStyle&);
   // sXULTextBoxData used for both labels and descriptions
   static const FrameConstructionData sXULTextBoxData;
   static const FrameConstructionData* FindXULLabelData(const Element&, ComputedStyle&);
   static const FrameConstructionData* FindXULDescriptionData(const Element&, ComputedStyle&);
--- a/mfbt/HashFunctions.h
+++ b/mfbt/HashFunctions.h
@@ -244,39 +244,26 @@ MOZ_MUST_USE inline HashNumber
 HashGeneric(Args... aArgs)
 {
   return AddToHash(0, aArgs...);
 }
 
 namespace detail {
 
 template<typename T>
-HashNumber
+constexpr HashNumber
 HashUntilZero(const T* aStr)
 {
   HashNumber hash = 0;
-  for (T c; (c = *aStr); aStr++) {
+  for (; T c = *aStr; aStr++) {
     hash = AddToHash(hash, c);
   }
   return hash;
 }
 
-// This is a `constexpr` alternative to HashUntilZero(const T*). It should
-// only be used for compile-time computation because it uses recursion.
-// XXX: once support for GCC 4.9 is dropped, this function should be removed
-// and HashUntilZero(const T*) should be made `constexpr`.
-template<typename T>
-constexpr HashNumber
-ConstExprHashUntilZero(const T* aStr, HashNumber aHash)
-{
-  return !*aStr
-       ? aHash
-       : ConstExprHashUntilZero(aStr + 1, AddToHash(aHash, *aStr));
-}
-
 template<typename T>
 HashNumber
 HashKnownLength(const T* aStr, size_t aLength)
 {
   HashNumber hash = 0;
   for (size_t i = 0; i < aLength; i++) {
     hash = AddToHash(hash, aStr[i]);
   }
@@ -305,37 +292,25 @@ HashString(const char* aStr, size_t aLen
 
 MOZ_MUST_USE
 inline HashNumber
 HashString(const unsigned char* aStr, size_t aLength)
 {
   return detail::HashKnownLength(aStr, aLength);
 }
 
-MOZ_MUST_USE inline HashNumber
+// You may need to use the
+// MOZ_{PUSH,POP}_DISABLE_INTEGRAL_CONSTANT_OVERFLOW_WARNING macros if you use
+// this function. See the comment on those macros' definitions for more detail.
+MOZ_MUST_USE constexpr HashNumber
 HashString(const char16_t* aStr)
 {
   return detail::HashUntilZero(aStr);
 }
 
-// This is a `constexpr` alternative to HashString(const char16_t*). It should
-// only be used for compile-time computation because it uses recursion.
-//
-// You may need to use the
-// MOZ_{PUSH,POP}_DISABLE_INTEGRAL_CONSTANT_OVERFLOW_WARNING macros if you use
-// this function. See the comment on those macros' definitions for more detail.
-//
-// XXX: once support for GCC 4.9 is dropped, this function should be removed
-// and HashString(const char16_t*) should be made `constexpr`.
-MOZ_MUST_USE constexpr HashNumber
-ConstExprHashString(const char16_t* aStr)
-{
-  return detail::ConstExprHashUntilZero(aStr, 0);
-}
-
 MOZ_MUST_USE inline HashNumber
 HashString(const char16_t* aStr, size_t aLength)
 {
   return detail::HashKnownLength(aStr, aLength);
 }
 
 /*
  * On Windows, wchar_t is not the same as char16_t, even though it's
--- a/mfbt/HashTable.h
+++ b/mfbt/HashTable.h
@@ -1723,28 +1723,33 @@ private:
 
   bool underloaded() { return wouldBeUnderloaded(capacity(), mEntryCount); }
 
   static MOZ_ALWAYS_INLINE bool match(Entry& aEntry, const Lookup& aLookup)
   {
     return HashPolicy::match(HashPolicy::getKey(aEntry.get()), aLookup);
   }
 
+  enum LookupReason
+  {
+    ForNonAdd,
+    ForAdd
+  };
+
   // Warning: in order for readonlyThreadsafeLookup() to be safe this
   // function must not modify the table in any way when |collisionBit| is 0.
   // (The use of the METER() macro to increment stats violates this
   // restriction but we will live with that for now because it's enabled so
   // rarely.)
+  template<LookupReason Reason>
   MOZ_ALWAYS_INLINE Entry& lookup(const Lookup& aLookup,
-                                  HashNumber aKeyHash,
-                                  uint32_t aCollisionBit) const
+                                  HashNumber aKeyHash) const
   {
     MOZ_ASSERT(isLiveHash(aKeyHash));
     MOZ_ASSERT(!(aKeyHash & sCollisionBit));
-    MOZ_ASSERT(aCollisionBit == 0 || aCollisionBit == sCollisionBit);
     MOZ_ASSERT(mTable);
     METER(mStats.mSearches++);
 
     // Compute the primary hash address.
     HashNumber h1 = hash1(aKeyHash);
     Entry* entry = &mTable[h1];
 
     // Miss: return space for a new entry.
@@ -1761,22 +1766,20 @@ private:
 
     // Collision: double hash.
     DoubleHash dh = hash2(aKeyHash);
 
     // Save the first removed entry pointer so we can recycle later.
     Entry* firstRemoved = nullptr;
 
     while (true) {
-      if (MOZ_UNLIKELY(entry->isRemoved())) {
-        if (!firstRemoved) {
+      if (Reason == ForAdd && !firstRemoved) {
+        if (MOZ_UNLIKELY(entry->isRemoved())) {
           firstRemoved = entry;
-        }
-      } else {
-        if (aCollisionBit == sCollisionBit) {
+        } else {
           entry->setCollision();
         }
       }
 
       METER(mStats.mSteps++);
       h1 = applyDoubleHash(h1, dh);
 
       entry = &mTable[h1];
@@ -2125,39 +2128,39 @@ public:
 
   MOZ_ALWAYS_INLINE Ptr lookup(const Lookup& aLookup) const
   {
     ReentrancyGuard g(*this);
     if (!HasHash<HashPolicy>(aLookup)) {
       return Ptr();
     }
     HashNumber keyHash = prepareHash(aLookup);
-    return Ptr(lookup(aLookup, keyHash, 0), *this);
+    return Ptr(lookup<ForNonAdd>(aLookup, keyHash), *this);
   }
 
   MOZ_ALWAYS_INLINE Ptr readonlyThreadsafeLookup(const Lookup& aLookup) const
   {
     if (!HasHash<HashPolicy>(aLookup)) {
       return Ptr();
     }
     HashNumber keyHash = prepareHash(aLookup);
-    return Ptr(lookup(aLookup, keyHash, 0), *this);
+    return Ptr(lookup<ForNonAdd>(aLookup, keyHash), *this);
   }
 
   MOZ_ALWAYS_INLINE AddPtr lookupForAdd(const Lookup& aLookup) const
   {
     ReentrancyGuard g(*this);
     if (!EnsureHash<HashPolicy>(aLookup)) {
       return AddPtr();
     }
     HashNumber keyHash = prepareHash(aLookup);
     // Directly call the constructor in the return statement to avoid
     // excess copying when building with Visual Studio 2017.
     // See bug 1385181.
-    return AddPtr(lookup(aLookup, keyHash, sCollisionBit), *this, keyHash);
+    return AddPtr(lookup<ForAdd>(aLookup, keyHash), *this, keyHash);
   }
 
   template<typename... Args>
   MOZ_MUST_USE bool add(AddPtr& aPtr, Args&&... aArgs)
   {
     ReentrancyGuard g(*this);
     MOZ_ASSERT(mTable);
     MOZ_ASSERT_IF(aPtr.isValid(), aPtr.mTable == this);
@@ -2249,17 +2252,17 @@ public:
 #ifdef DEBUG
     aPtr.mGeneration = generation();
     aPtr.mMutationCount = mMutationCount;
 #endif
     {
       ReentrancyGuard g(*this);
       // Check that aLookup has not been destroyed.
       MOZ_ASSERT(prepareHash(aLookup) == aPtr.mKeyHash);
-      aPtr.mEntry = &lookup(aLookup, aPtr.mKeyHash, sCollisionBit);
+      aPtr.mEntry = &lookup<ForAdd>(aLookup, aPtr.mKeyHash);
     }
     return aPtr.found() || add(aPtr, std::forward<Args>(aArgs)...);
   }
 
   void remove(Ptr aPtr)
   {
     MOZ_ASSERT(mTable);
     ReentrancyGuard g(*this);
--- a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/toolbars.py
+++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/toolbars.py
@@ -118,17 +118,17 @@ class LocationBar(UIBaseLib):
 
     @property
     def contextmenu(self):
         """Provides access to the urlbar context menu.
 
         :returns: Reference to the urlbar context menu.
         """
         # TODO: This method should be implemented via the menu API.
-        parent = self.urlbar.find_element(By.ANON_ATTRIBUTE, {'anonid': 'textbox-input-box'})
+        parent = self.urlbar.find_element(By.ANON_ATTRIBUTE, {'anonid': 'moz-input-box'})
         return parent.find_element(By.ANON_ATTRIBUTE, {'anonid': 'input-box-contextmenu'})
 
     @property
     def focused(self):
         """Checks the focus state of the location bar.
 
         :returns: `True` if focused, otherwise `False`
         """
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -498,28 +498,28 @@ Tester.prototype = {
 
     if (gConfig.jscovDirPrefix) {
       let coveragePath = gConfig.jscovDirPrefix;
       let {CoverageCollector} = ChromeUtils.import("resource://testing-common/CoverageUtils.jsm",
                                                    {});
       this._coverageCollector = new CoverageCollector(coveragePath);
     }
 
-    this.PerTestCoverageUtils.beforeTestSync();
-
     this.structuredLogger.info("*** Start BrowserChrome Test Results ***");
     Services.console.registerListener(this);
     this._globalProperties = Object.keys(window);
     this._globalPropertyWhitelist = [
       "navigator", "constructor", "top",
       "Application",
       "__SS_tabsToRestore", "__SSi",
       "webConsoleCommandController",
     ];
 
+    this.PerTestCoverageUtils.beforeTestSync();
+
     if (this.tests.length) {
       this.waitForWindowsReady().then(() => {
         this.nextTest();
       });
     } else {
       this.finish();
     }
   },
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -48316,16 +48316,28 @@
       [
        "/css/reference/ref-filled-green-200px-square.html",
        "=="
       ]
      ],
      {}
     ]
    ],
+   "css/CSS2/floats/new-fc-relayout.html": [
+    [
+     "/css/CSS2/floats/new-fc-relayout.html",
+     [
+      [
+       "/css/reference/ref-filled-green-100px-square-only.html",
+       "=="
+      ]
+     ],
+     {}
+    ]
+   ],
    "css/CSS2/floats/new-fc-separates-from-float-2.html": [
     [
      "/css/CSS2/floats/new-fc-separates-from-float-2.html",
      [
       [
        "/css/reference/ref-filled-green-200px-square.html",
        "=="
       ]
@@ -48352,16 +48364,28 @@
       [
        "/css/reference/ref-filled-green-200px-square.html",
        "=="
       ]
      ],
      {}
     ]
    ],
+   "css/CSS2/floats/zero-width-floats-positioning.html": [
+    [
+     "/css/CSS2/floats/zero-width-floats-positioning.html",
+     [
+      [
+       "/css/reference/ref-filled-green-100px-square-only.html",
+       "=="
+      ]
+     ],
+     {}
+    ]
+   ],
    "css/CSS2/floats/zero-width-floats.html": [
     [
      "/css/CSS2/floats/zero-width-floats.html",
      [
       [
        "/css/reference/ref-filled-green-200px-square.html",
        "=="
       ]
@@ -194171,21 +194195,16 @@
      {}
     ]
    ],
    "background-fetch/META.yml": [
     [
      {}
     ]
    ],
-   "background-fetch/get-ids.https.js": [
-    [
-     {}
-    ]
-   ],
    "background-fetch/resources/feature-name.txt": [
     [
      {}
     ]
    ],
    "background-fetch/resources/sw.js": [
     [
      {}
@@ -287091,16 +287110,21 @@
      {}
     ]
    ],
    "html/user-activat