Merge autoland to mozilla-central. a=merge
authorCsoregi Natalia <ncsoregi@mozilla.com>
Fri, 12 Oct 2018 19:55:37 +0300
changeset 489202 7fd59dc00149b33ede6efb0ed848eef227cae540
parent 489178 0ab221f0a99689d1cfe3a158ade3818a680ec26d (current diff)
parent 489201 bb32faa290f0b8871c7c72798dc1876b01e48a31 (diff)
child 489203 ede21c2f2f993c7bb00d37b1e52b62c9f0c4c671
push id246
push userfmarier@mozilla.com
push dateSat, 13 Oct 2018 00:15:40 +0000
reviewersmerge
milestone64.0a1
Merge autoland to mozilla-central. a=merge
dom/media/MediaManager.cpp
dom/media/webrtc/MediaTrackConstraints.cpp
dom/webidl/AudioStreamTrack.webidl
dom/webidl/VideoStreamTrack.webidl
testing/web-platform/meta/css/cssom/cssstyledeclaration-mutationrecord-002.html.ini
toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js
--- a/browser/components/distribution.js
+++ b/browser/components/distribution.js
@@ -160,25 +160,27 @@ DistributionCustomizer.prototype = {
 
         await PlacesUtils.bookmarks.insert({
           type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
           parentGuid, index,
         });
         break;
 
       case "livemark":
-        if (itemIndex < defaultIndex)
+        // Livemarks are no more supported, instead of a livemark we'll insert
+        // a bookmark pointing to the site uri, if available.
+        if (!item.siteLink) {
+          break;
+        }
+        if (itemIndex < defaultIndex) {
           index = prependIndex++;
+        }
 
-        // Don't bother updating the livemark contents on creation.
-        let parentId = await PlacesUtils.promiseItemId(parentGuid);
-        await PlacesUtils.livemarks.addLivemark({
-          feedURI: Services.io.newURI(item.feedLink),
-          siteURI: Services.io.newURI(item.siteLink),
-          parentId, index, title: item.title,
+        await PlacesUtils.bookmarks.insert({
+          parentGuid, index, title: item.title, url: item.siteLink,
         });
         break;
 
       case "bookmark":
       default:
         if (itemIndex < defaultIndex)
           index = prependIndex++;
 
--- a/browser/components/extensions/ExtensionControlledPopup.jsm
+++ b/browser/components/extensions/ExtensionControlledPopup.jsm
@@ -198,18 +198,26 @@ class ExtensionControlledPopup {
     // The item should have an extension and the user shouldn't have confirmed
     // the change here, but just to be sure check that it is still controlled
     // and the user hasn't already confirmed the change.
     // If there is no id, then the extension is no longer in control.
     if (!extensionId || this.userHasConfirmed(extensionId)) {
       return;
     }
 
+    let win = targetWindow || this.topWindow;
+    // If the window closes while waiting for focus, this might reject/throw,
+    // and we should stop trying to show the popup.
+    try {
+      await this._ensureWindowReady(win);
+    } catch (ex) {
+      return;
+    }
+
     // Find the elements we need.
-    let win = targetWindow || this.topWindow;
     let doc = win.document;
     let panel = doc.getElementById("extension-notification-panel");
     let popupnotification = doc.getElementById(this.popupnotificationId);
     let urlBarWasFocused = win.gURLBar.focused;
 
     if (!popupnotification) {
       throw new Error(`No popupnotification found for id "${this.popupnotificationId}"`);
     }
@@ -296,9 +304,58 @@ class ExtensionControlledPopup {
     }
 
     let link = doc.createXULElement("label");
     link.setAttribute("class", "learnMore text-link");
     link.href = Services.urlFormatter.formatURLPref("app.support.baseURL") + this.learnMoreLink;
     link.textContent = strBundle.GetStringFromName(this.learnMoreMessageId);
     description.appendChild(link);
   }
+
+  _ensureWindowReady(win) {
+    return new Promise(async (resolve, reject) => {
+      if (win.closed) {
+        reject();
+        return;
+      }
+      let promises = [];
+      let listenersToRemove = [];
+      function promiseEvent(type) {
+        promises.push(new Promise(resolve => {
+          let listener = () => {
+            win.removeEventListener(type, listener);
+            resolve();
+          };
+          win.addEventListener(type, listener);
+          listenersToRemove.push([type, listener]);
+        }));
+      }
+      let {focusedWindow, activeWindow} = Services.focus;
+      if (activeWindow != win) {
+        promiseEvent("activate");
+      }
+      if (focusedWindow) {
+        // We may have focused a non-remote child window, find the browser window:
+        let {rootTreeItem} = focusedWindow.docShell;
+        rootTreeItem.QueryInterface(Ci.nsIDocShell);
+        focusedWindow = rootTreeItem.contentViewer.DOMDocument.defaultView;
+      }
+      if (focusedWindow != win) {
+        promiseEvent("focus");
+      }
+      let unloadListener;
+      if (promises.length) {
+        unloadListener = () => {
+          for (let [type, listener] of listenersToRemove) {
+            win.removeEventListener(type, listener);
+          }
+          reject();
+        };
+        win.addEventListener("unload", unloadListener, {once: true});
+      }
+      await Promise.all(promises);
+      if (unloadListener) {
+        win.removeEventListener("unload", unloadListener);
+      }
+      resolve();
+    });
+  }
 }
--- a/browser/components/extensions/parent/ext-windows.js
+++ b/browser/components/extensions/parent/ext-windows.js
@@ -1,15 +1,14 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
-                                   "@mozilla.org/browser/aboutnewtab-service;1",
-                                   "nsIAboutNewTabService");
+ChromeUtils.defineModuleGetter(this, "HomePage",
+                               "resource:///modules/HomePage.jsm");
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
                                "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 var {
   promiseObserved,
 } = ExtensionUtils;
 
 /**
@@ -147,19 +146,16 @@ this.windows = class extends ExtensionAP
             let tab = tabTracker.getTab(createData.tabId);
 
             // Private browsing tabs can only be moved to private browsing
             // windows.
             let incognito = PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser);
             if (createData.incognito !== null && createData.incognito != incognito) {
               return Promise.reject({message: "`incognito` property must match the incognito state of tab"});
             }
-            if (createData.incognito && !PrivateBrowsingUtils.enabled) {
-              return Promise.reject({message: "`incognito` cannot be used if incognito mode is disabled"});
-            }
             createData.incognito = incognito;
 
             if (createData.cookieStoreId && createData.cookieStoreId !== getCookieStoreIdForTab(createData, tab)) {
               return Promise.reject({message: "`cookieStoreId` must match the tab's cookieStoreId"});
             }
 
             args.appendElement(tab);
           } else if (createData.url !== null) {
@@ -168,22 +164,24 @@ this.windows = class extends ExtensionAP
               for (let url of createData.url) {
                 array.appendElement(mkstr(url));
               }
               args.appendElement(array);
             } else {
               args.appendElement(mkstr(createData.url));
             }
           } else {
-            let url = aboutNewTabService.newTabURL;
+            let url = createData.incognito && !PrivateBrowsingUtils.permanentPrivateBrowsing ?
+              "about:privatebrowsing" : HomePage.get().split("|", 1)[0];
             args.appendElement(mkstr(url));
 
-            if (url === "about:newtab") {
-              // The extension principal cannot directly load about:newtab,
-              // so use the system principal instead.
+            if (url.startsWith("about:") &&
+                !context.checkLoadURL(url, {dontReportErrors: true})) {
+              // The extension principal cannot directly load about:-URLs,
+              // except for about:blank. So use the system principal instead.
               principal = Services.scriptSecurityManager.getSystemPrincipal();
             }
           }
 
           args.appendElement(null); // unused
           args.appendElement(null); // referrer
           args.appendElement(null); // postData
           args.appendElement(null); // allowThirdPartyFixup
@@ -208,16 +206,19 @@ this.windows = class extends ExtensionAP
             features.push("dialog=no", "all");
           } else {
             // All other types create "popup"-type windows by default.
             features.push("dialog", "resizable", "minimizable", "centerscreen", "titlebar", "close");
           }
 
           if (createData.incognito !== null) {
             if (createData.incognito) {
+              if (!PrivateBrowsingUtils.enabled) {
+                return Promise.reject({message: "`incognito` cannot be used if incognito mode is disabled"});
+              }
               features.push("private");
             } else {
               features.push("non-private");
             }
           }
 
           let {allowScriptsToClose, url} = createData;
           if (allowScriptsToClose === null) {
--- a/browser/components/extensions/test/browser/browser_ext_tabs_create_url.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_create_url.js
@@ -1,12 +1,32 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+async function runWithDisabledPrivateBrowsing(callback) {
+  const {
+    EnterprisePolicyTesting,
+    PoliciesPrefTracker,
+  } = ChromeUtils.import("resource://testing-common/EnterprisePolicyTesting.jsm", {});
+
+  PoliciesPrefTracker.start();
+  await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+    policies: {DisablePrivateBrowsing: true},
+  });
+
+  try {
+    await callback();
+  } finally {
+    await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+    EnterprisePolicyTesting.resetRunOnceState();
+    PoliciesPrefTracker.stop();
+  }
+}
+
 add_task(async function test_urlbar_focus() {
   // Disable preloaded new tab because the urlbar is automatically focused when
   // a preloaded new tab is opened, while this test is supposed to test that the
   // implementation of tabs.create automatically focuses the urlbar of new tabs.
   await SpecialPowers.pushPrefEnv({
     set: [["browser.newtab.preload", false]],
   });
 
@@ -55,8 +75,117 @@ add_task(async function test_urlbar_focu
   is(active.tagName, "html:input", "Input element focused");
   ok(active.classList.contains("urlbar-input"), "Urlbar focused");
 
   extension.sendMessage("remove", tab2.id);
   await extension.awaitMessage("result");
 
   await extension.unload();
 });
+
+add_task(async function default_url() {
+  const extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["tabs"],
+    },
+    background() {
+      function promiseNonBlankTab() {
+        return new Promise(resolve => {
+          browser.tabs.onUpdated.addListener(function listener(tabId, changeInfo, tab) {
+            if (changeInfo.status === "complete" && tab.url !== "about:blank") {
+              browser.tabs.onUpdated.removeListener(listener);
+              resolve(tab);
+            }
+          });
+        });
+      }
+
+      browser.test.onMessage.addListener(async (msg, {incognito, expectedNewWindowUrl, expectedNewTabUrl}) => {
+        browser.test.assertEq("start", msg, `Start test, incognito=${incognito}`);
+
+        let tabPromise = promiseNonBlankTab();
+        let win;
+        try {
+          win = await browser.windows.create({incognito});
+          browser.test.assertEq(1, win.tabs.length, "Expected one tab in the new window.");
+        } catch (e) {
+          browser.test.assertEq(expectedNewWindowUrl, e.message, "Expected error");
+          browser.test.sendMessage("done");
+          return;
+        }
+        let tab = await tabPromise;
+        browser.test.assertEq(expectedNewWindowUrl, tab.url, "Expected default URL of new window");
+
+        tabPromise = promiseNonBlankTab();
+        await browser.tabs.create({windowId: win.id});
+        tab = await tabPromise;
+        browser.test.assertEq(expectedNewTabUrl, tab.url, "Expected default URL of new tab");
+
+        await browser.windows.remove(win.id);
+        browser.test.sendMessage("done");
+      });
+    },
+  });
+
+  await extension.startup();
+
+  extension.sendMessage("start", {
+    incognito: false,
+    expectedNewWindowUrl: "about:home",
+    expectedNewTabUrl: "about:newtab",
+  });
+  await extension.awaitMessage("done");
+  extension.sendMessage("start", {
+    incognito: true,
+    expectedNewWindowUrl: "about:privatebrowsing",
+    expectedNewTabUrl: "about:privatebrowsing",
+  });
+  await extension.awaitMessage("done");
+
+  info("Testing with multiple homepages.");
+  await SpecialPowers.pushPrefEnv({set: [["browser.startup.homepage", "about:robots|about:blank|about:home"]]});
+  extension.sendMessage("start", {
+    incognito: false,
+    expectedNewWindowUrl: "about:robots",
+    expectedNewTabUrl: "about:newtab",
+  });
+  await extension.awaitMessage("done");
+  extension.sendMessage("start", {
+    incognito: true,
+    expectedNewWindowUrl: "about:privatebrowsing",
+    expectedNewTabUrl: "about:privatebrowsing",
+  });
+  await extension.awaitMessage("done");
+  await SpecialPowers.popPrefEnv();
+
+  info("Testing with perma-private browsing mode.");
+  await SpecialPowers.pushPrefEnv({set: [["browser.privatebrowsing.autostart", true]]});
+  extension.sendMessage("start", {
+    incognito: false,
+    expectedNewWindowUrl: "about:home",
+    expectedNewTabUrl: "about:newtab",
+  });
+  await extension.awaitMessage("done");
+  extension.sendMessage("start", {
+    incognito: true,
+    expectedNewWindowUrl: "about:home",
+    expectedNewTabUrl: "about:newtab",
+  });
+  await extension.awaitMessage("done");
+  await SpecialPowers.popPrefEnv();
+
+  info("Testing with disabled private browsing mode.");
+  await runWithDisabledPrivateBrowsing(async () => {
+    extension.sendMessage("start", {
+      incognito: false,
+      expectedNewWindowUrl: "about:home",
+      expectedNewTabUrl: "about:newtab",
+    });
+    await extension.awaitMessage("done");
+    extension.sendMessage("start", {
+      incognito: true,
+      expectedNewWindowUrl: "`incognito` cannot be used if incognito mode is disabled",
+    });
+    await extension.awaitMessage("done");
+  });
+
+  await extension.unload();
+});
--- a/browser/components/extensions/test/browser/browser_ext_windows_create_cookieStoreId.js
+++ b/browser/components/extensions/test/browser/browser_ext_windows_create_cookieStoreId.js
@@ -69,17 +69,17 @@ add_task(async function valid_cookieStor
     description: "no explicit URL",
     createParams: {
       cookieStoreId: "firefox-container-1",
     },
     expectedCookieStoreIds: [
       "firefox-container-1",
     ],
     expectedExecuteScriptResult: [
-      // Default URL is about:newtab, and extensions cannot run scripts in it.
+      // Default URL is about:home, and extensions cannot run scripts in it.
       "Missing host permission for the tab",
     ],
   }, {
     description: "one URL",
     createParams: {
       url: "about:blank",
       cookieStoreId: "firefox-container-1",
     },
@@ -164,17 +164,17 @@ add_task(async function valid_cookieStor
 
       for (let [i, expectedCookieStoreId] of Object.entries(expectedCookieStoreIds)) {
         browser.test.assertEq(expectedCookieStoreId, win.tabs[i].cookieStoreId, `expected cookieStoreId for tab ${i} (${description})`);
       }
 
       for (let [i, expectedResult] of Object.entries(expectedExecuteScriptResult)) {
         // Wait until the the tab can process the tabs.executeScript calls.
         // TODO: Remove this when bug 1418655 and bug 1397667 are fixed.
-        let expectedUrl = Array.isArray(createParams.url) ? createParams.url[i] : createParams.url || "about:newtab";
+        let expectedUrl = Array.isArray(createParams.url) ? createParams.url[i] : createParams.url || "about:home";
         await awaitTabReady(win.tabs[i].id, expectedUrl);
 
         let result = await executeScriptAndGetResult(win.tabs[i].id);
         browser.test.assertEq(expectedResult, result, `expected executeScript result for tab ${i} (${description})`);
       }
 
       await browser.windows.remove(win.id);
     }
--- a/browser/components/tests/unit/test_distribution.js
+++ b/browser/components/tests/unit/test_distribution.js
@@ -48,17 +48,26 @@ function installDistributionEngine() {
       aPersistent.value = true;
       if (aProp == XRE_APP_DISTRIBUTION_DIR)
         return distDir.clone();
       return null;
     },
   });
 }
 
-function run_test() {
+registerCleanupFunction(async function() {
+  // Remove the distribution dir, even if the test failed, otherwise all
+  // next tests will use it.
+  let folderPath = OS.Path.join(OS.Constants.Path.profileDir, "distribution");
+  await OS.File.removeDir(folderPath, { ignoreAbsent: true });
+  Assert.ok(!(await OS.File.exists(folderPath)));
+  Services.prefs.clearUserPref("distribution.testing.loadFromProfile");
+});
+
+add_task(async function() {
   // Set special pref to load distribution.ini from the profile folder.
   Services.prefs.setBoolPref("distribution.testing.loadFromProfile", true);
 
   // Copy distribution.ini file to the profile dir.
   let distroDir = gProfD.clone();
   distroDir.leafName = "distribution";
   let iniFile = distroDir.clone();
   iniFile.append("distribution.ini");
@@ -68,28 +77,16 @@ function run_test() {
   }
 
   let testDistributionFile = gTestDir.clone();
   testDistributionFile.append("distribution.ini");
   testDistributionFile.copyTo(distroDir, "distribution.ini");
   Assert.ok(testDistributionFile.exists());
 
   installDistributionEngine();
-
-  run_next_test();
-}
-
-registerCleanupFunction(function() {
-  // Remove the distribution dir, even if the test failed, otherwise all
-  // next tests will use it.
-  let distDir = gProfD.clone();
-  distDir.append("distribution");
-  distDir.remove(true);
-  Assert.ok(!distDir.exists());
-  Services.prefs.clearUserPref("distribution.testing.loadFromProfile");
 });
 
 add_task(async function() {
   // Force distribution.
   let glue = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver);
   glue.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_DISTRIBUTION_CUSTOMIZATION);
 
   var defaultBranch = Services.prefs.getDefaultBranch(null);
@@ -142,19 +139,19 @@ add_task(async function() {
     /NS_ERROR_UNEXPECTED/);
   // This value was overridden by a locale specific setting
   Assert.equal(defaultBranch.getComplexValue("distribution.test.locale.set", Ci.nsIPrefLocalizedString).data, "Locale Set");
   // This value was overridden by a language specific setting
   Assert.equal(defaultBranch.getComplexValue("distribution.test.language.set", Ci.nsIPrefLocalizedString).data, "Language Set");
   // Language should not override locale
   Assert.notEqual(defaultBranch.getComplexValue("distribution.test.locale.set", Ci.nsIPrefLocalizedString).data, "Language Set");
 
-  do_test_pending();
-
   Services.prefs.setCharPref("distribution.searchplugins.defaultLocale", "de-DE");
 
-  Services.search.init(function() {
-    Assert.equal(Services.search.isInitialized, true);
-    var engine = Services.search.getEngineByName("Google");
-    Assert.equal(engine.description, "override-de-DE");
-    do_test_finished();
+  await new Promise(resolve => {
+    Services.search.init(function() {
+      Assert.equal(Services.search.isInitialized, true);
+      var engine = Services.search.getEngineByName("Google");
+      Assert.equal(engine.description, "override-de-DE");
+      resolve();
+    });
   });
 });
--- a/devtools/client/inspector/boxmodel/test/browser_boxmodel_layout-accordion-state.js
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_layout-accordion-state.js
@@ -26,18 +26,18 @@ add_task(async function() {
   await testAccordionStateAfterClickingHeader(doc);
   await testAccordionStateAfterSwitchingSidebars(inspector, doc);
   await testAccordionStateAfterReopeningLayoutView(toolbox);
 
   Services.prefs.clearUserPref(BOXMODEL_OPENED_PREF);
 });
 
 function testAccordionStateAfterClickingHeader(doc) {
-  const header = doc.querySelector("#layout-container .box-model-pane ._header");
-  const bContent = doc.querySelector("#layout-container .box-model-pane ._content");
+  const header = doc.querySelector(".layout-container .box-model-pane ._header");
+  const bContent = doc.querySelector(".layout-container .box-model-pane ._content");
 
   info("Checking initial state of the box model panel.");
   is(bContent.style.display, "block", "The box model panel content is 'display: block'.");
   ok(Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF),
     `${BOXMODEL_OPENED_PREF} is pref on by default.`);
 
   info("Clicking the box model header to hide the box model panel.");
   header.click();
@@ -46,17 +46,17 @@ function testAccordionStateAfterClicking
   is(bContent.style.display, "none", "The box model panel content is 'display: none'.");
   ok(!Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF),
     `${BOXMODEL_OPENED_PREF} is pref off.`);
 }
 
 function testAccordionStateAfterSwitchingSidebars(inspector, doc) {
   info("Checking the box model accordion state is persistent after switching sidebars.");
 
-  const bContent = doc.querySelector("#layout-container .box-model-pane ._content");
+  const bContent = doc.querySelector(".layout-container .box-model-pane ._content");
 
   info("Selecting the computed view.");
   inspector.sidebar.select("computedview");
 
   info("Selecting the layout view.");
   inspector.sidebar.select("layoutview");
 
   info("Checking the state of the box model panel.");
@@ -70,15 +70,15 @@ async function testAccordionStateAfterRe
   + "re-opening the layout view.");
 
   info("Closing the toolbox.");
   await toolbox.destroy();
 
   info("Re-opening the layout view.");
   const { boxmodel } = await openLayoutView();
   const { document: doc } = boxmodel;
-  const bContent = doc.querySelector("#layout-container .box-model-pane ._content");
+  const bContent = doc.querySelector(".layout-container .box-model-pane ._content");
 
   info("Checking the state of the box model panel.");
   ok(!bContent, "The box model panel content is not rendered.");
   ok(!Services.prefs.getBoolPref(BOXMODEL_OPENED_PREF),
     `${BOXMODEL_OPENED_PREF} is pref off.`);
 }
--- a/devtools/client/inspector/flexbox/components/FlexItemSizingOutline.js
+++ b/devtools/client/inspector/flexbox/components/FlexItemSizingOutline.js
@@ -41,50 +41,60 @@ class FlexItemSizingOutline extends Pure
         className: "flex-outline-delta",
         style: {
           backgroundColor: colorUtils.setAlpha(this.props.color, 0.1)
         }
       })
     );
   }
 
-  renderFinalOutline(mainFinalSize, mainMaxSize, mainMinSize) {
-    const isClamped = mainFinalSize === mainMaxSize ||
-                      mainFinalSize === mainMinSize;
-
+  renderFinalOutline(mainFinalSize, mainMaxSize, mainMinSize, isClamped) {
     return (
       dom.div({
         className: "flex-outline-final" + (isClamped ? " clamped" : "")
       })
     );
   }
 
   renderPoint(name) {
     return dom.div({ className: `flex-outline-point ${name}`, "data-label": name });
   }
 
   render() {
     const {
+      flexItemSizing,
+      properties,
+    } = this.props.flexItem;
+    const {
       mainBaseSize,
       mainDeltaSize,
       mainMaxSize,
       mainMinSize,
-    } = this.props.flexItem.flexItemSizing;
+    } = flexItemSizing;
+
     const isRow = this.props.flexDirection.startsWith("row");
+    const dimension = isRow ? "width" : "height";
 
     // Calculate the final size. This is base + delta, then clamped by min or max.
     let mainFinalSize = mainBaseSize + mainDeltaSize;
     mainFinalSize = Math.max(mainFinalSize, mainMinSize);
     mainFinalSize = Math.min(mainFinalSize, mainMaxSize);
 
     // The max size is only interesting to show if it did clamp the item
+    // TODO: replace this with the new clamping state that the API will return once bug
+    // 1498273 is fixed.
     const showMax = mainMaxSize === mainFinalSize;
 
     // The min size is only really interesting if it actually clamped the item.
-    const showMin = mainMinSize === mainFinalSize;
+    // Just checking that the main size = final size isn't enough because this may be true
+    // if the max content size is the final size. So also check that min-width/height is
+    // set.
+    // TODO: replace this with the new clamping state that the API will return once bug
+    // 1498273 is fixed.
+    const showMin = mainMinSize === mainFinalSize && properties[`min-${dimension}`];
 
     // Sort all of the dimensions in order to come up with a grid track template.
     // Make mainDeltaSize start from the same point as the other ones so we can compare.
     let sizes = [
       { name: "basis-end", size: mainBaseSize },
       { name: "final-end", size: mainFinalSize }
     ];
 
@@ -132,16 +142,17 @@ class FlexItemSizingOutline extends Pure
             }
           },
           this.renderPoint("basis"),
           this.renderPoint("final"),
           showMin ? this.renderPoint("min") : null,
           showMax ? this.renderPoint("max") : null,
           this.renderBasisOutline(mainBaseSize),
           this.renderDeltaOutline(mainDeltaSize),
-          this.renderFinalOutline(mainFinalSize, mainMaxSize, mainMinSize)
+          this.renderFinalOutline(mainFinalSize, mainMaxSize, mainMinSize,
+                                  showMin || showMax)
         )
       )
     );
   }
 }
 
 module.exports = FlexItemSizingOutline;
--- a/devtools/client/inspector/flexbox/components/FlexItemSizingProperties.js
+++ b/devtools/client/inspector/flexbox/components/FlexItemSizingProperties.js
@@ -14,63 +14,338 @@ const Types = require("../types");
 class FlexItemSizingProperties extends PureComponent {
   static get propTypes() {
     return {
       flexDirection: PropTypes.string.isRequired,
       flexItem: PropTypes.shape(Types.flexItem).isRequired,
     };
   }
 
+  /**
+   * Rounds some dimension in pixels and returns a string to be displayed to the user.
+   * The string will end with 'px'. If the number is 0, the string "0" is returned.
+   *
+   * @param  {Number} value
+   *         The number to be rounded
+   * @return {String}
+   *         Representation of the rounded number
+   */
+  getRoundedDimension(value) {
+    if (value == 0) {
+      return "0";
+    }
+    return (Math.round(value * 100) / 100) + "px";
+  }
+
+  /**
+   * Format the flexibility value into a meaningful value for the UI.
+   * If the item grew, then prepend a + sign, if it shrank, prepend a - sign.
+   * If it didn't flex, return "0".
+   *
+   * @param  {Boolean} grew
+   *         Whether the item grew or not
+   * @param  {Number} value
+   *         The amount of pixels the item flexed
+   * @return {String}
+   *         Representation of the flexibility value
+   */
+  getFlexibilityValueString(grew, mainDeltaSize) {
+    const value = this.getRoundedDimension(mainDeltaSize);
+
+    if (grew) {
+      return "+" + value;
+    }
+
+    return value;
+  }
+
+  /**
+   * Render an authored CSS property.
+   *
+   * @param  {String} name
+   *         The name for this CSS property
+   * @param  {String} value
+   *         The property value
+   * @param  {Booleam} isDefaultValue
+   *         Whether the value come from the browser default style
+   * @return {Object}
+   *         The React component representing this CSS property
+   */
+  renderCssProperty(name, value, isDefaultValue) {
+    return (
+      dom.span({ className: "css-property-link" },
+        dom.span({ className: "theme-fg-color5" }, name),
+        ": ",
+        dom.span({ className: "theme-fg-color1" }, value),
+        ";"
+      )
+    );
+  }
+
+  /**
+   * Render a list of sentences to be displayed in the UI as reasons why a certain sizing
+   * value happened.
+   *
+   * @param  {Array} sentences
+   *         The list of sentences as Strings
+   * @return {Object}
+   *         The React component representing these sentences
+   */
+  renderReasons(sentences) {
+    return (
+      dom.ul({ className: "reasons" },
+        sentences.map(sentence => dom.li({}, sentence))
+      )
+    );
+  }
+
+  renderBaseSizeSection({ mainBaseSize, mainMinSize }, properties, dimension) {
+    const flexBasisValue = properties["flex-basis"];
+    const dimensionValue = properties[dimension];
+    const minDimensionValue = properties[`min-${dimension}`];
+    const hasMinClamping = mainMinSize && mainMinSize === mainBaseSize;
+
+    let property = null;
+    let reason = null;
+
+    if (hasMinClamping && minDimensionValue) {
+      // If min clamping happened, then the base size is going to be that value.
+      // TODO: this isn't going to be necessarily true after bug 1498273 is fixed.
+      property = this.renderCssProperty(`min-${dimension}`, minDimensionValue);
+    } else if (flexBasisValue && !hasMinClamping) {
+      // If flex-basis is defined, then that's what is used for the base size.
+      property = this.renderCssProperty("flex-basis", flexBasisValue);
+    } else if (dimensionValue) {
+      // If not and width/height is defined, then that's what defines the base size.
+      property = this.renderCssProperty(dimension, dimensionValue);
+    } else {
+      // Finally, if nothing is set, then the base size is the max-content size.
+      reason = this.renderReasons(
+        [getStr("flexbox.itemSizing.itemBaseSizeFromContent")]);
+    }
+
+    return (
+      dom.li({ className: property ? "section" : "section no-property" },
+        dom.span({ className: "name" },
+          getStr("flexbox.itemSizing.baseSizeSectionHeader")
+        ),
+        dom.span({ className: "value theme-fg-color1" },
+          this.getRoundedDimension(mainBaseSize)
+        ),
+        property,
+        reason
+      )
+    );
+  }
+
+  renderFlexibilitySection(flexItemSizing, properties) {
+    const {
+      mainDeltaSize,
+      mainBaseSize,
+      mainFinalSize,
+      lineGrowthState
+    } = flexItemSizing;
+
+    const flexGrow = properties["flex-grow"];
+    const flexGrow0 = parseFloat(flexGrow) === 0;
+    const flexShrink = properties["flex-shrink"];
+    const flexShrink0 = parseFloat(flexShrink) === 0;
+    const grew = mainDeltaSize > 0;
+    const shrank = mainDeltaSize < 0;
+    // TODO: replace this with the new clamping state that the API will return once bug
+    // 1498273 is fixed.
+    const wasClamped = mainDeltaSize + mainBaseSize !== mainFinalSize;
+
+    const reasons = [];
+
+    // First output a sentence for telling users about whether there was enough room or
+    // not on the line.
+    if (lineGrowthState === "growing") {
+      reasons.push(getStr("flexbox.itemSizing.extraRoomOnLine"));
+    } else if (lineGrowthState === "shrinking") {
+      reasons.push(getStr("flexbox.itemSizing.notEnoughRoomOnLine"));
+    }
+
+    // Then tell users whether the item was set to grow, shrink or none of them.
+    if (flexGrow && !flexGrow0 && lineGrowthState !== "shrinking") {
+      reasons.push(getStr("flexbox.itemSizing.setToGrow"));
+    }
+    if (flexShrink && !flexShrink0 && lineGrowthState !== "growing") {
+      reasons.push(getStr("flexbox.itemSizing.setToShrink"));
+    }
+    if (!grew && !shrank && lineGrowthState === "growing") {
+      reasons.push(getStr("flexbox.itemSizing.notSetToGrow"));
+    }
+    if (!grew && !shrank && lineGrowthState === "shrinking") {
+      reasons.push(getStr("flexbox.itemSizing.notSetToShrink"));
+    }
+
+    let property = null;
+
+    if (grew) {
+      // If the item grew.
+      if (flexGrow) {
+        // It's normally because it was set to grow (flex-grow is non 0).
+        property = this.renderCssProperty("flex-grow", flexGrow);
+      }
+
+      if (wasClamped) {
+        // It may have wanted to grow more than it did, because it was later max-clamped.
+        reasons.push(getStr("flexbox.itemSizing.growthAttemptWhenClamped"));
+      }
+    } else if (shrank) {
+      // If the item shrank.
+      if (flexShrink && !flexShrink0) {
+        // It's either because flex-shrink is non 0.
+        property = this.renderCssProperty("flex-shrink", flexShrink);
+      } else {
+        // Or also because it's default value is 1 anyway.
+        property = this.renderCssProperty("flex-shrink", "1", true);
+      }
+
+      if (wasClamped) {
+        // It might have wanted to shrink more (to accomodate all items) but couldn't
+        // because it was later min-clamped.
+        reasons.push(getStr("flexbox.itemSizing.shrinkAttemptWhenClamped"));
+      }
+    } else if (lineGrowthState === "growing" && flexGrow && !flexGrow0) {
+      // The item did not grow or shrink. There was room on the line and flex-grow was
+      // set, other items have likely used up all of the space.
+      property = this.renderCssProperty("flex-grow", flexGrow);
+      reasons.push(getStr("flexbox.itemSizing.growthAttemptButSiblings"));
+    } else if (lineGrowthState === "shrinking") {
+      // The item did not grow or shrink and there wasn't enough room on the line.
+      if (!flexShrink0) {
+        // flex-shrink was set (either defined in CSS, or via its default value of 1).
+        // but the item didn't shrink.
+        if (flexShrink) {
+          property = this.renderCssProperty("flex-shrink", flexShrink);
+        } else {
+          property = this.renderCssProperty("flex-shrink", 1, true);
+        }
+
+        reasons.push(getStr("flexbox.itemSizing.shrinkAttemptButCouldnt"));
+
+        if (wasClamped) {
+          // Maybe it was clamped.
+          reasons.push(getStr("flexbox.itemSizing.shrinkAttemptWhenClamped"));
+        }
+      } else {
+        // flex-shrink was set to 0, so it didn't shrink.
+        property = this.renderCssProperty("flex-shrink", flexShrink);
+      }
+    }
+
+    // Don't display the section at all if there's nothing useful to show users.
+    if (!property && !reasons.length) {
+      return null;
+    }
+
+    return (
+      dom.li({ className: property ? "section" : "section no-property" },
+        dom.span({ className: "name" },
+          getStr("flexbox.itemSizing.flexibilitySectionHeader")
+        ),
+        dom.span({ className: "value theme-fg-color1" },
+          this.getFlexibilityValueString(grew, mainDeltaSize)
+        ),
+        property,
+        this.renderReasons(reasons)
+      )
+    );
+  }
+
+  renderMinimumSizeSection({ mainMinSize, mainFinalSize }, properties, dimension) {
+    // We only display the minimum size when the item actually violates that size during
+    // layout & is clamped.
+    // For now, we detect this by checking that the min-size is the same as the final size
+    // and that a min-size is actually defined in CSS.
+    // TODO: replace this with the new clamping state that the API will return once bug
+    // 1498273 is fixed.
+    const minDimensionValue = properties[`min-${dimension}`];
+    if (mainMinSize !== mainFinalSize || !minDimensionValue) {
+      return null;
+    }
+
+    return (
+      dom.li({ className: "section" },
+        dom.span({ className: "name" },
+          getStr("flexbox.itemSizing.minSizeSectionHeader")
+        ),
+        dom.span({ className: "value theme-fg-color1" },
+          this.getRoundedDimension(mainMinSize)
+        ),
+        this.renderCssProperty(`min-${dimension}`, minDimensionValue)
+      )
+    );
+  }
+
+  renderMaximumSizeSection({ mainMaxSize, mainFinalSize }, properties, dimension) {
+    // TODO: replace this with the new clamping state that the API will return once bug
+    // 1498273 is fixed.
+    if (mainMaxSize !== mainFinalSize) {
+      return null;
+    }
+
+    const maxDimensionValue = properties[`max-${dimension}`];
+
+    return (
+      dom.li({ className: "section" },
+        dom.span({ className: "name" },
+          getStr("flexbox.itemSizing.maxSizeSectionHeader")
+        ),
+        dom.span({ className: "value theme-fg-color1" },
+          this.getRoundedDimension(mainMaxSize)
+        ),
+        this.renderCssProperty(`max-${dimension}`, maxDimensionValue)
+      )
+    );
+  }
+
+  renderFinalSizeSection({ mainFinalSize }) {
+    return (
+      dom.li({ className: "section no-property" },
+        dom.span({ className: "name" },
+          getStr("flexbox.itemSizing.finalSizeSectionHeader")
+        ),
+        dom.span({ className: "value theme-fg-color1" },
+          this.getRoundedDimension(mainFinalSize)
+        )
+      )
+    );
+  }
+
   render() {
     const {
       flexDirection,
       flexItem,
     } = this.props;
     const {
       flexItemSizing,
       properties,
     } = flexItem;
+    const {
+      mainBaseSize,
+      mainDeltaSize,
+      mainMaxSize,
+      mainMinSize,
+    } = flexItemSizing;
     const dimension = flexDirection.startsWith("row") ? "width" : "height";
-    const contentStr = dimension === "width" ?
-      getStr("flexbox.contentWidth") : getStr("flexbox.contentHeight");
-    const finalStr = dimension === "width" ?
-      getStr("flexbox.finalWidth") : getStr("flexbox.finalHeight");
+
+    // Calculate the final size. This is base + delta, then clamped by min or max.
+    let mainFinalSize = mainBaseSize + mainDeltaSize;
+    mainFinalSize = Math.max(mainFinalSize, mainMinSize);
+    mainFinalSize = Math.min(mainFinalSize, mainMaxSize);
+    flexItemSizing.mainFinalSize = mainFinalSize;
 
     return (
-      dom.ol(
-        {
-          id: "flex-item-sizing-properties",
-          className: "flex-item-list",
-        },
-        dom.li({},
-          dom.span({}, "flex-basis: "),
-          properties["flex-basis"]
-        ),
-        dom.li({},
-          dom.span({}, "flex-grow: "),
-          properties["flex-grow"]
-        ),
-        dom.li({},
-          dom.span({}, "flex-shrink: "),
-          properties["flex-shrink"]
-        ),
-        dom.li({},
-          dom.span({}, `${contentStr} `),
-          `${parseFloat(flexItemSizing.mainBaseSize.toPrecision(6))}px`
-        ),
-        dom.li({},
-          dom.span({}, `Min-${dimension}: `),
-          properties["min-" + dimension]
-        ),
-        dom.li({},
-          dom.span({}, `Max-${dimension}: `),
-          properties["max-" + dimension]
-        ),
-        dom.li({},
-          dom.span({}, `${finalStr} `),
-          `${parseFloat(properties[dimension].toPrecision(6))}px`
-        )
+      dom.ul({ className: "flex-item-sizing" },
+        this.renderBaseSizeSection(flexItemSizing, properties, dimension),
+        this.renderFlexibilitySection(flexItemSizing, properties),
+        this.renderMinimumSizeSection(flexItemSizing, properties, dimension),
+        this.renderMaximumSizeSection(flexItemSizing, properties, dimension),
+        this.renderFinalSizeSection(flexItemSizing)
       )
     );
   }
 }
 
 module.exports = FlexItemSizingProperties;
--- a/devtools/client/inspector/flexbox/test/browser.ini
+++ b/devtools/client/inspector/flexbox/test/browser.ini
@@ -4,10 +4,12 @@ subsuite = devtools
 support-files =
   doc_flexbox_simple.html
   head.js
   !/devtools/client/inspector/test/head.js
   !/devtools/client/inspector/test/shared-head.js
   !/devtools/client/shared/test/shared-head.js
 
 [browser_flexbox_item_outline_exists.js]
+[browser_flexbox_item_outline_has_correct_layout.js]
 [browser_flexbox_item_outline_rotates_for_column.js]
-[browser_flexbox_item_outline_has_correct_layout.js]
+[browser_flexbox_sizing_info_exists.js]
+[browser_flexbox_sizing_info_has_correct_sections.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_exists.js
@@ -0,0 +1,32 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the flex item sizing information exists when a flex item is selected.
+
+const TEST_URI = URL_ROOT + "doc_flexbox_simple.html";
+
+add_task(async function() {
+  await addTab(TEST_URI);
+  const { inspector, flexboxInspector } = await openLayoutView();
+  const { document: doc } = flexboxInspector;
+
+  // Select a flex item in the test document and wait for the sizing info to be rendered.
+  // Note that we select an item that has base, delta and final sizes, so we can check
+  // those sections exists.
+  const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing");
+  await selectNode(".container.growing .item", inspector);
+  const [flexSizingContainer] = await onFlexItemSizingRendered;
+
+  ok(flexSizingContainer, "The flex sizing exists in the DOM");
+
+  info("Check that the base, flexibility and final sizes are displayed");
+  const allSections = [...flexSizingContainer.querySelectorAll(".section .name")];
+  const allSectionTitles = allSections.map(el => el.textContent);
+  const expectedTitles = ["Base Size", "Flexibility", "Final Size"];
+
+  ok(expectedTitles.every(title => allSectionTitles.includes(title)),
+     "The 3 main sizing sections where found");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/flexbox/test/browser_flexbox_sizing_info_has_correct_sections.js
@@ -0,0 +1,53 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the flex item sizing UI contains the right sections, depending on which
+// element is selected. Some items may be clamped, others not, so not all sections are
+// visible at all times.
+
+const TEST_URI = URL_ROOT + "doc_flexbox_simple.html";
+
+const TEST_DATA = [{
+  selector: ".shrinking .item",
+  expectedSections: ["Base Size", "Flexibility", "Final Size"]
+}, {
+  selector: ".shrinking.is-clamped .item",
+  expectedSections: ["Base Size", "Flexibility", "Minimum Size", "Final Size"]
+}, {
+  selector: ".growing .item",
+  expectedSections: ["Base Size", "Flexibility", "Final Size"]
+}, {
+  selector: ".growing.is-clamped .item",
+  expectedSections: ["Base Size", "Flexibility", "Maximum Size", "Final Size"]
+}];
+
+add_task(async function() {
+  await addTab(TEST_URI);
+  const { inspector, flexboxInspector } = await openLayoutView();
+  const { document: doc } = flexboxInspector;
+
+  for (const { selector, expectedSections } of TEST_DATA) {
+    info(`Checking the list of sections for the flex item ${selector}`);
+    const sections = await selectNodeAndGetFlexSizingSections(selector, inspector, doc);
+
+    is(sections.length, expectedSections.length, "Correct number of sections found");
+    expectedSections.forEach((expectedSection, i) => {
+      is(sections[i], expectedSection, `The ${expectedSection} section was found`);
+    });
+  }
+});
+
+async function selectNodeAndGetFlexSizingSections(selector, inspector, doc) {
+  const onFlexItemSizingRendered = waitForDOM(doc, "ul.flex-item-sizing");
+  await selectNode(selector, inspector);
+  const [flexSizingContainer] = await onFlexItemSizingRendered;
+
+  info(`Getting the list of displayed sections for ${selector}`);
+  const allSections = [...flexSizingContainer.querySelectorAll(".section .name")];
+  const allSectionTitles = allSections.map(el => el.textContent);
+
+  return allSectionTitles;
+}
--- a/devtools/client/inspector/layout/components/LayoutApp.js
+++ b/devtools/client/inspector/layout/components/LayoutApp.js
@@ -134,16 +134,16 @@ class LayoutApp extends PureComponent {
             const opened =  Services.prefs.getBoolPref(FLEXBOX_OPENED_PREF);
             Services.prefs.setBoolPref(FLEXBOX_OPENED_PREF, !opened);
           }
         });
       }
     }
 
     return (
-      dom.div({ id: "layout-container" },
+      dom.div({ className: "layout-container" },
         Accordion({ items })
       )
     );
   }
 }
 
 module.exports = connect(state => state)(LayoutApp);
--- a/devtools/client/locales/en-US/layout.properties
+++ b/devtools/client/locales/en-US/layout.properties
@@ -27,16 +27,97 @@ flexbox.flexContainerProperties=Flex Con
 
 # LOCALIZATION NOTE (flexbox.contentWidth, flexbox.contentHeight, flexbox.finalWidth,
 # flexbox.finalHeight): Labels for the flex item sizing properties in the Flexbox panel.
 flexbox.contentWidth=Content width:
 flexbox.contentHeight=Content height:
 flexbox.finalWidth=Final width:
 flexbox.finalHeight=Final height:
 
+# LOCALIZATION NOTE (flexbox.itemSizing.baseSizeSectionHeader): Header label displayed
+# at the start of the flex item sizing Base Size section.
+flexbox.itemSizing.baseSizeSectionHeader=Base Size
+
+# LOCALIZATION NOTE (flexbox.itemSizing.flexibilitySectionHeader): Header label displayed
+# at the start of the flex item sizing Flexibility section.
+flexbox.itemSizing.flexibilitySectionHeader=Flexibility
+
+# LOCALIZATION NOTE (flexbox.itemSizing.minSizeSectionHeader): Header label displayed
+# at the start of the flex item sizing Minimum Size section.
+flexbox.itemSizing.minSizeSectionHeader=Minimum Size
+
+# LOCALIZATION NOTE (flexbox.itemSizing.maxSizeSectionHeader): Header label displayed at
+# the start of the flex item sizing Maximum Size section.
+flexbox.itemSizing.maxSizeSectionHeader=Maximum Size
+
+# LOCALIZATION NOTE (flexbox.itemSizing.finalSizeSectionHeader): Header label displayed at
+# the start of the flex item sizing Final Size section.
+flexbox.itemSizing.finalSizeSectionHeader=Final Size
+
+# LOCALIZATION NOTE (flexbox.itemSizing.itemBaseSizeFromContent): Label shown in the flex
+# item sizing panel. It tells users that a given item’s base size was calculated from its
+# content size when unconstrained.
+flexbox.itemSizing.itemBaseSizeFromContent=The item’s content size when unconstrained.
+
+# LOCALIZATION NOTE (flexbox.itemSizing.itemMinSizeFromItemMinContent): Label shown in the
+# flex item sizing panel. It tells users that a given item’s minimum size is coming from
+# its min-content size.
+flexbox.itemSizing.itemMinSizeFromItemMinContent=This is the element’s minimum content size.
+
+# LOCALIZATION NOTE (flexbox.itemSizing.extraRoomOnLine): Label shown in the flexbox item
+# sizing panel. It tells users that there was extra room to distribute inside a given flex
+# line.
+flexbox.itemSizing.extraRoomOnLine=There was extra room available on the flex line.
+
+# LOCALIZATION NOTE (flexbox.itemSizing.notEnoughRoomOnLine): Label shown in the flexbox
+# item sizing panel. It tells users that there wasn’t enough room inside a given flex line
+# for all of its items.
+flexbox.itemSizing.notEnoughRoomOnLine=There wasn’t enough room available on the flex line.
+
+# LOCALIZATION NOTE (flexbox.itemSizing.growthAttemptWhenClamped): Label shown in the
+# flexbox item sizing panel. It tells users that a given item attempted to grow by a
+# certain amount but ended up being clamped by a max size.
+# (note that clamp is a common word in flexbox terminology. It refers to constraining an
+# item's size to some defined min/max-width/height set on the element, even though there
+# might have been room for it to grow, or reason for it to shrink more).
+flexbox.itemSizing.growthAttemptWhenClamped=The item wanted to grow, but it was clamped.
+
+# LOCALIZATION NOTE (flexbox.itemSizing.shrinkAttemptWhenClamped): Label shown in the
+# flexbox item sizing panel. It tells users that a given item attempted to shrink by a
+# certain amount but ended up being clamped by a min size.
+flexbox.itemSizing.shrinkAttemptWhenClamped=The item wanted to shrink, but it was clamped.
+
+# LOCALIZATION NOTE (flexbox.itemSizing.shrinkAttemptButCouldnt): Label shown in the
+# flexbox item sizing panel. It tells users that a given item attempted to shrink by a
+# certain amount but could not
+flexbox.itemSizing.shrinkAttemptButCouldnt=Item was set to shrink but could not.
+
+# LOCALIZATION NOTE (flexbox.itemSizing.growthAttemptButSiblings): Label shown in the
+# flexbox item sizing panel. It tells users that a given item could not grow to occupy
+# extra space because its siblings have likely already used it.
+flexbox.itemSizing.growthAttemptButSiblings=Item could not grow, siblings have likely used the extra space.
+
+# LOCALIZATION NOTE (flexbox.itemSizing.setToGrow): Label shown in the flex item sizing
+# panel. It tells users that a given item was set to grow.
+flexbox.itemSizing.setToGrow=Item was set to grow.
+
+# LOCALIZATION NOTE (flexbox.itemSizing.setToShrink): Label shown in the flexbox item
+# sizing panel. It tells users that a given item was set to shrink.
+flexbox.itemSizing.setToShrink=Item was set to shrink.
+
+# LOCALIZATION NOTE (flexbox.itemSizing.notSetToGrow): Label shown in the
+# flexbox item sizing panel. It tells users that a given item was not set to grow, even
+# though there might have been space on the flex line for it to grow.
+flexbox.itemSizing.notSetToGrow=Item was not set to grow.
+
+# LOCALIZATION NOTE (flexbox.itemSizing.notSetToShrink): Label shown in the
+# flexbox item sizing panel. It tells users that a given item did not shrink even though
+# there might not have been enough space on the flex line for all items to fit.
+flexbox.itemSizing.notSetToShrink=Item was not set to shrink.
+
 # LOCALIZATION NOTE (layout.cannotShowGridOutline, layout.cannotSHowGridOutline.title):
 # In the case where the grid outline cannot be effectively displayed.
 layout.cannotShowGridOutline=Cannot show outline for this grid
 layout.cannotShowGridOutline.title=The selected grid’s outline cannot effectively fit inside the layout panel for it to be usable.
 
 # LOCALIZATION NOTE (layout.displayAreaNames): Label of the display area names setting
 # option in the CSS Grid panel.
 layout.displayAreaNames=Display area names
--- a/devtools/client/themes/layout.css
+++ b/devtools/client/themes/layout.css
@@ -1,45 +1,45 @@
 /* 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/. */
 
-#layout-container {
+.layout-container {
   height: 100%;
   width: 100%;
   overflow-y: auto;
   overflow-x: auto;
   min-width: 200px;
 }
 
-#layout-container .accordion ._content {
+.layout-container .accordion ._content {
   padding: 0;
 }
 
 #layout-container .accordion ._header {
   white-space: nowrap;
   text-overflow: ellipsis;
   overflow: hidden;
 }
 
 /**
  * Common styles for the layout container
  */
 
-#layout-container li {
+.layout-container li {
   padding: 3px 0;
   -moz-user-select: none;
 }
 
-#layout-container input {
+.layout-container input {
   margin-inline-end: 7px;
   vertical-align: middle;
 }
 
-#layout-container label {
+.layout-container label {
   margin-inline-start: -3px;
 }
 
 .layout-color-swatch {
   width: 12px;
   height: 12px;
   margin-inline-start: -1px;
   border: 1px solid var(--theme-highlight-gray);
@@ -358,22 +358,57 @@
 .flex-outline.shrinking .flex-outline-point.final::before {
   border-width: 0 1px 0 0;
 }
 
 /**
  * Flex Item Sizing Properties
  */
 
-#flex-item-sizing-properties {
-  padding-top: 0;
+.flex-item-sizing {
+  margin: 20px;
+  padding: 0;
+  list-style: none;
+}
+
+.flex-item-sizing .section {
+  --padding: 10px;
+  margin-block-start: var(--padding);
+  padding: var(--padding) 0 0 0;
+  border-block-start: 1px solid var(--theme-splitter-color);
+  display: grid;
+  grid-template-columns: 1fr max-content;
+  grid-column-gap: var(--padding);
+}
+
+.flex-item-sizing .section:first-child {
+  margin: 0;
 }
 
-#flex-item-sizing-properties span {
- font-weight: 600;
+.flex-item-sizing .name {
+  font-weight: 600;
+  grid-column: 1;
+}
+
+.flex-item-sizing .value {
+  text-align: end;
+  font-weight: 600;
+}
+
+.flex-item-sizing .css-property-link {
+  grid-column: 2;
+  text-align: end;
+}
+
+.flex-item-sizing .reasons,
+.flex-item-sizing .reasons li {
+  grid-column: 1 / 3;
+  margin: 0;
+  padding: 0;
+  list-style: none;
 }
 
 /**
  * Flex Container Properties
  */
 
 #flex-container-properties {
   border-block-start: 1px solid var(--theme-splitter-color);
--- a/devtools/server/actors/layout.js
+++ b/devtools/server/actors/layout.js
@@ -7,21 +7,21 @@
 const { Cu } = require("chrome");
 const { Actor, ActorClassWithSpec } = require("devtools/shared/protocol");
 const {
   flexboxSpec,
   flexItemSpec,
   gridSpec,
   layoutSpec,
 } = require("devtools/shared/specs/layout");
-const { ELEMENT_NODE } = require("devtools/shared/dom-node-constants");
 const { SHOW_ELEMENT } = require("devtools/shared/dom-node-filter-constants");
 const { getStringifiableFragments } =
   require("devtools/server/actors/utils/css-grid-utils");
 
+loader.lazyRequireGetter(this, "getCSSStyleRules", "devtools/shared/inspector/css-logic", true);
 loader.lazyRequireGetter(this, "CssLogic", "devtools/server/actors/inspector/css-logic", true);
 loader.lazyRequireGetter(this, "nodeConstants", "devtools/shared/dom-node-constants");
 
 /**
  * Set of actors the expose the CSS layout information to the devtools protocol clients.
  *
  * The |Layout| actor is the main entry point. It is used to get various CSS
  * layout-related information from the document.
@@ -98,27 +98,24 @@ const FlexboxActor = ActorClassWithSpec(
     if (!flex) {
       return [];
     }
 
     const flexItemActors = [];
 
     for (const line of flex.getLines()) {
       for (const item of line.getItems()) {
-        if (item.node.nodeType !== ELEMENT_NODE) {
-          continue;
-        }
-
         flexItemActors.push(new FlexItemActor(this, item.node, {
           crossMaxSize: item.crossMaxSize,
           crossMinSize: item.crossMinSize,
           mainBaseSize: item.mainBaseSize,
           mainDeltaSize: item.mainDeltaSize,
           mainMaxSize: item.mainMaxSize,
           mainMinSize: item.mainMinSize,
+          lineGrowthState: line.growthState,
         }));
       }
     }
 
     return flexItemActors;
   },
 });
 
@@ -152,42 +149,60 @@ const FlexItemActor = ActorClassWithSpec
     this.walker = null;
   },
 
   form(detail) {
     if (detail === "actorid") {
       return this.actorID;
     }
 
+    const { flexDirection } = CssLogic.getComputedStyle(this.containerEl);
+    const dimension = flexDirection.startsWith("row") ? "width" : "height";
+
+    // Find the authored sizing properties for this item.
+    const properties = {
+      "flex-basis": "",
+      "flex-grow": "",
+      "flex-shrink": "",
+      [`min-${dimension}`]: "",
+      [`max-${dimension}`]: "",
+      [dimension]: ""
+    };
+
+    if (this.element.nodeType === this.element.ELEMENT_NODE) {
+      for (const name in properties) {
+        let value = "";
+        // Look first on the element style.
+        if (this.element.style[name] && this.element.style[name] !== "auto") {
+          value = this.element.style[name];
+        } else {
+          // And then on the rules that apply to the element.
+          // getCSSStyleRules returns rules from least to most specific, so override
+          // values as we find them.
+          const cssRules = getCSSStyleRules(this.element);
+          for (const rule of cssRules) {
+            const rulePropertyValue = rule.style.getPropertyValue(name);
+            if (rulePropertyValue && rulePropertyValue !== "auto") {
+              value = rulePropertyValue;
+            }
+          }
+        }
+
+        properties[name] = value;
+      }
+    }
+
     const form = {
       actor: this.actorID,
       // The flex item sizing data.
       flexItemSizing: this.flexItemSizing,
+      // The authored style properties of the flex item.
+      properties,
     };
 
-    if (this.element.nodeType === ELEMENT_NODE) {
-      const { flexDirection } = CssLogic.getComputedStyle(this.containerEl);
-      const styles = CssLogic.getComputedStyle(this.element);
-      const clientRect = this.element.getBoundingClientRect();
-      const dimension = flexDirection.startsWith("row") ? "width" : "height";
-
-      // The computed style properties of the flex item.
-      form.properties = {
-        "flex-basis": styles.flexBasis,
-        "flex-grow": styles.flexGrow,
-        "flex-shrink": styles.flexShrink,
-        // min-width/height computed style.
-        [`min-${dimension}`]: styles[`min-${dimension}`],
-        // max-width/height computed style.
-        [`max-${dimension}`]: styles[`max-${dimension}`],
-        // Computed width/height of the flex item element.
-        [dimension]: parseFloat(clientRect[dimension.toLowerCase()].toPrecision(6)),
-      };
-    }
-
     // If the WalkerActor already knows the flex item element, then also return its
     // ActorID so we avoid the client from doing another round trip to get it in many
     // cases.
     if (this.walker.hasNode(this.element)) {
       form.nodeActorID = this.walker.getNode(this.element).actorID;
     }
 
     return form;
--- a/devtools/shared/event-emitter.js
+++ b/devtools/shared/event-emitter.js
@@ -60,17 +60,17 @@ class EventEmitter {
   static off(target, type, listener) {
     const length = arguments.length;
     const events = target[eventListeners];
 
     if (!events) {
       return;
     }
 
-    if (length === 3) {
+    if (length >= 3) {
       // Trying to remove from the `target` the `listener` specified for the
       // event's `type` given.
       const listenersForType = events.get(type);
 
       // If we don't have listeners for the event's type, we bail out.
       if (!listenersForType) {
         return;
       }
--- a/devtools/shared/fronts/layout.js
+++ b/devtools/shared/fronts/layout.js
@@ -65,17 +65,17 @@ const FlexItemFront = FrontClassWithSpec
     if (!this._form.nodeActorID) {
       return null;
     }
 
     return this.conn.getActor(this._form.nodeActorID);
   },
 
   /**
-   * Get the computed style properties for the flex item.
+   * Get the style properties for the flex item.
    */
   get properties() {
     return this._form.properties;
   },
 });
 
 const GridFront = FrontClassWithSpec(gridSpec, {
   form: function(form, detail) {
--- a/devtools/shared/tests/unit/test_eventemitter_static.js
+++ b/devtools/shared/tests/unit/test_eventemitter_static.js
@@ -318,16 +318,23 @@ const TESTS = {
 
     emit(target, "foo", "bar");
     emit(target, "foo", "baz");
     emit(target, "done", "");
 
     await Promise.all([pFoo, pDone]);
   },
 
+  testCallingOffWithMoreThan3Args() {
+    const target = { name: "target"};
+    on(target, "data", fail);
+    off(target, "data", fail, undefined);
+    emit(target, "data", "Listener should be removed");
+  }
+
 };
 
 /**
  * Create a runnable tests based on the tests descriptor given.
  *
  * @param {Object} tests
  *  The tests descriptor object, contains the tests to run.
  */
--- a/dom/base/nsStyledElement.cpp
+++ b/dom/base/nsStyledElement.cpp
@@ -127,18 +127,17 @@ nsStyledElement::SetInlineStyleDeclarati
                                          this);
 
   nsAttrValue attrValue(do_AddRef(&aDeclaration), nullptr);
   SetMayHaveStyle();
 
   nsIDocument* document = GetComposedDoc();
   mozAutoDocUpdate updateBatch(document, true);
   return SetAttrAndNotify(kNameSpaceID_None, nsGkAtoms::style, nullptr,
-                          aData.mOldValue.isSome() ?
-                            aData.mOldValue.ptr() : nullptr,
+                          aData.mOldValue.ptrOr(nullptr),
                           attrValue, nullptr, aData.mModType,
                           hasListeners, true, kDontCallAfterSetAttr,
                           document, updateBatch);
 }
 
 // ---------------------------------------------------------------
 // Others and helpers
 
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -541,20 +541,16 @@ DOMInterfaces = {
 'MediaStreamAudioDestinationNode': {
     'binaryNames': { 'stream': 'DOMStream' }
 },
 
 'MediaStreamList': {
     'headerFile': 'MediaStreamList.h',
 },
 
-'MediaStreamTrack': {
-    'concrete': False
-},
-
 'MediaRecorder': {
     'headerFile': 'MediaRecorder.h',
 },
 
 'MessageBroadcaster': {
     'concrete': False
 },
 
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -8,16 +8,17 @@
 #include "objbase.h"
 // Some Windows header defines this, so undef it as it conflicts with our
 // function of the same name.
 #undef GetCurrentTime
 #endif
 
 #include "mozilla/dom/HTMLMediaElement.h"
 #include "AudioChannelService.h"
+#include "AudioDeviceInfo.h"
 #include "AudioStreamTrack.h"
 #include "AutoplayPolicy.h"
 #include "ChannelMediaDecoder.h"
 #include "DOMMediaStream.h"
 #include "DecoderDoctorDiagnostics.h"
 #include "DecoderDoctorLogger.h"
 #include "DecoderTraits.h"
 #include "FrameStatistics.h"
@@ -26,16 +27,17 @@
 #include "HLSDecoder.h"
 #endif
 #include "HTMLMediaElement.h"
 #include "ImageContainer.h"
 #include "Layers.h"
 #include "MP4Decoder.h"
 #include "MediaContainerType.h"
 #include "MediaError.h"
+#include "MediaManager.h"
 #include "MediaMetadataManager.h"
 #include "MediaResource.h"
 #include "MediaSourceDecoder.h"
 #include "MediaStreamError.h"
 #include "MediaStreamGraph.h"
 #include "MediaStreamListener.h"
 #include "MediaTrackList.h"
 #include "SVGObserverUtils.h"
@@ -3880,16 +3882,17 @@ HTMLMediaElement::HTMLMediaElement(
   , mWatchManager(this, OwnerDoc()->AbstractMainThreadFor(TaskCategory::Other))
   , mMainThreadEventTarget(OwnerDoc()->EventTargetFor(TaskCategory::Other))
   , mAbstractMainThread(OwnerDoc()->AbstractMainThreadFor(TaskCategory::Other))
   , mShutdownObserver(new ShutdownObserver)
   , mPlayed(new TimeRanges(ToSupports(OwnerDoc())))
   , mPaused(true, "HTMLMediaElement::mPaused")
   , mErrorSink(new ErrorSink(this))
   , mAudioChannelWrapper(new AudioChannelAgentCallback(this))
+  , mSink(MakePair(nsString(), RefPtr<AudioDeviceInfo>()))
 {
   MOZ_ASSERT(mMainThreadEventTarget);
   MOZ_ASSERT(mAbstractMainThread);
 
   DecoderDoctorLogger::LogConstruction(this);
 
   mWatchManager.Watch(mPaused, &HTMLMediaElement::UpdateWakeLock);
 
@@ -5080,16 +5083,29 @@ HTMLMediaElement::FinishDecoderSetup(Med
 
   // Notify the decoder of the initial activity status.
   NotifyDecoderActivityChanges();
 
   // Update decoder principal before we start decoding, since it
   // can affect how we feed data to MediaStreams
   NotifyDecoderPrincipalChanged();
 
+  // Set sink device if we have one. Otherwise the default is used.
+  if (mSink.second()) {
+    mDecoder->SetSink(mSink.second())
+#ifdef DEBUG
+      ->Then(mAbstractMainThread, __func__,
+      [](const GenericPromise::ResolveOrRejectValue& aValue) {
+        MOZ_ASSERT(aValue.IsResolve() && !aValue.ResolveValue());
+      });
+#else
+    ;
+#endif
+  }
+
   for (OutputMediaStream& ms : mOutputStreams) {
     if (ms.mCapturingMediaStream) {
       MOZ_ASSERT(!ms.mCapturingDecoder);
       continue;
     }
 
     ms.mCapturingDecoder = true;
     aDecoder->AddOutputStream(ms.mStream->GetInputStream()->AsProcessedStream(),
@@ -5311,16 +5327,19 @@ HTMLMediaElement::UpdateSrcMediaStreamPl
     mSrcStreamPausedCurrentTime = -1;
 
     mMediaStreamListener =
       new StreamListener(this, "HTMLMediaElement::mMediaStreamListener");
     stream->AddListener(mMediaStreamListener);
 
     stream->AddAudioOutput(this);
     SetVolumeInternal();
+    if (mSink.second()) {
+      NS_WARNING("setSinkId() when playing a MediaStream is not supported yet and will be ignored");
+    }
 
     VideoFrameContainer* container = GetVideoFrameContainer();
     if (mSelectedVideoStreamTrack && container) {
       mSelectedVideoStreamTrack->AddVideoOutput(container);
     }
 
     SetCapturedOutputStreamsEnabled(true); // Unmute
     // If the input is a media stream, we don't check its data and always regard
@@ -8260,13 +8279,93 @@ HTMLMediaElement::ReportCanPlayTelemetry
             Telemetry::Accumulate(
               Telemetry::HistogramID::VIDEO_CAN_CREATE_H264_DECODER, h264);
             thread->AsyncShutdown();
           }));
       }),
     NS_DISPATCH_NORMAL);
 }
 
+already_AddRefed<Promise>
+HTMLMediaElement::SetSinkId(const nsAString& aSinkId, ErrorResult& aRv)
+{
+  nsCOMPtr<nsPIDOMWindowInner> win = OwnerDoc()->GetInnerWindow();
+  if (!win) {
+    aRv.Throw(NS_ERROR_UNEXPECTED);
+    return nullptr;
+  }
+
+  RefPtr<Promise> promise = Promise::Create(win->AsGlobal(), aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  if (mSink.first().Equals(aSinkId)) {
+    promise->MaybeResolveWithUndefined();
+    return promise.forget();
+  }
+
+  nsString sinkId(aSinkId);
+  MediaManager::Get()->GetSinkDevice(win, sinkId)
+    ->Then(mAbstractMainThread, __func__,
+           [self = RefPtr<HTMLMediaElement>(this)](RefPtr<AudioDeviceInfo>&& aInfo) {
+             // Sink found switch output device.
+             MOZ_ASSERT(aInfo);
+             if (self->mDecoder) {
+               RefPtr<SinkInfoPromise> p = self->mDecoder->SetSink(aInfo)
+                 ->Then(self->mAbstractMainThread, __func__,
+                       [aInfo] (const GenericPromise::ResolveOrRejectValue& aValue) {
+                         if (aValue.IsResolve()) {
+                           return SinkInfoPromise::CreateAndResolve(aInfo, __func__);
+                         }
+                         return SinkInfoPromise::CreateAndReject(aValue.RejectValue(), __func__);
+                       });
+               return p;
+             }
+             if (self->GetSrcMediaStream()) {
+               // Set Sink Id through MSG is not supported yet.
+               return SinkInfoPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
+             }
+             // No media attached to the element save it for later.
+             return SinkInfoPromise::CreateAndResolve(aInfo, __func__);
+           },
+           [](nsresult res){
+             // Promise is rejected, sink not found.
+             return SinkInfoPromise::CreateAndReject(res, __func__);
+           })
+    ->Then(mAbstractMainThread, __func__,
+           [promise, self = RefPtr<HTMLMediaElement>(this),
+           sinkId = std::move(sinkId)] (const SinkInfoPromise::ResolveOrRejectValue& aValue) {
+             if (aValue.IsResolve()) {
+               self->mSink = MakePair(sinkId, aValue.ResolveValue());
+               promise->MaybeResolveWithUndefined();
+             } else {
+               switch (aValue.RejectValue()) {
+                 case NS_ERROR_ABORT:
+                   promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+                   break;
+                 case NS_ERROR_NOT_AVAILABLE:
+                 {
+                   ErrorResult notFoundError;
+                   notFoundError.ThrowDOMException(
+                         NS_ERROR_DOM_NOT_FOUND_ERR,
+                         NS_LITERAL_CSTRING("The object can not be found here."));
+                   promise->MaybeReject(notFoundError);
+                   break;
+                 }
+                 case NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR:
+                   promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
+                   break;
+                 default:
+                   MOZ_ASSERT_UNREACHABLE("Invalid error.");
+               }
+             }
+           });
+
+  aRv = NS_OK;
+  return promise.forget();
+}
+
 } // namespace dom
 } // namespace mozilla
 
 #undef LOG
 #undef LOG_EVENT
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -61,16 +61,17 @@ class TextTrack;
 class TimeRanges;
 class WakeLock;
 class MediaTrack;
 class MediaStreamTrack;
 class VideoStreamTrack;
 } // namespace dom
 } // namespace mozilla
 
+class AudioDeviceInfo;
 class nsIChannel;
 class nsIHttpChannel;
 class nsILoadGroup;
 class nsIRunnable;
 class nsISerialEventTarget;
 class nsITimer;
 class nsRange;
 
@@ -822,16 +823,27 @@ public:
   void AsyncResolveSeekDOMPromiseIfExists() override;
   void AsyncRejectSeekDOMPromiseIfExists() override;
 
   nsISerialEventTarget* MainThreadEventTarget()
   {
     return mMainThreadEventTarget;
   }
 
+  // Set the sink id (of the output device) that the audio will play. If aSinkId
+  // is empty the default device will be set.
+  already_AddRefed<Promise> SetSinkId(const nsAString& aSinkId, ErrorResult& aRv);
+  // Get the sink id of the device that audio is being played. Initial value is
+  // empty and the default device is being used.
+  void GetSinkId(nsString& aSinkId)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    aSinkId = mSink.first();
+  }
+
 protected:
   virtual ~HTMLMediaElement();
 
   class AudioChannelAgentCallback;
   class ChannelLoader;
   class ErrorSink;
   class MediaLoadListener;
   class MediaStreamTracksAvailableCallback;
@@ -1888,16 +1900,24 @@ private:
   // AsyncRejectSeekDOMPromiseIfExists() methods.
   RefPtr<dom::Promise> mSeekDOMPromise;
 
   // For debugging bug 1407148.
   void AssertReadyStateIsNothing();
 
   // Attach UA Shadow Root if it is not attached.
   void AttachAndSetUAShadowRoot();
+
+  // Contains the unique id of the sink device and the device info.
+  // The initial value is ("", nullptr) and the default output device is used.
+  // It can contain an invalid id and info if the device has been
+  // unplugged. It can be set to ("", nullptr). It follows the spec attribute:
+  // https://w3c.github.io/mediacapture-output/#htmlmediaelement-extensions
+  // Read/Write from the main thread only.
+  Pair<nsString, RefPtr<AudioDeviceInfo>> mSink;
 };
 
 // Check if the context is chrome or has the debugger or tabs permission
 bool
 HasDebuggerOrTabsPrivilege(JSContext* aCx, JSObject* aObj);
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/media/AudioStream.cpp
+++ b/dom/media/AudioStream.cpp
@@ -332,26 +332,29 @@ int AudioStream::InvokeCubeb(Function aF
 {
   MonitorAutoUnlock mon(mMonitor);
   return aFunction(mCubebStream.get(), std::forward<Args>(aArgs)...);
 }
 
 nsresult
 AudioStream::Init(uint32_t aNumChannels,
                   AudioConfig::ChannelLayout::ChannelMap aChannelMap,
-                  uint32_t aRate)
+                  uint32_t aRate,
+                  AudioDeviceInfo* aSinkInfo)
 {
   auto startTime = TimeStamp::Now();
 
   LOG("%s channels: %d, rate: %d", __FUNCTION__, aNumChannels, aRate);
   mChannels = aNumChannels;
   mOutChannels = aNumChannels;
 
   mDumpFile = OpenDumpFile(aNumChannels, aRate);
 
+  mSinkInfo = aSinkInfo;
+
   cubeb_stream_params params;
   params.rate = aRate;
   params.channels = mOutChannels;
   params.layout = static_cast<uint32_t>(aChannelMap);
   params.format = ToCubebFormat<AUDIO_OUTPUT_FORMAT>::value;
   params.prefs = CubebUtils::GetDefaultStreamPrefs();
 
   mAudioClock.Init(aRate);
@@ -375,18 +378,22 @@ AudioStream::OpenCubeb(cubeb* aContext, 
                        TimeStamp aStartTime, bool aIsFirst)
 {
   MOZ_ASSERT(aContext);
 
   cubeb_stream* stream = nullptr;
   /* Convert from milliseconds to frames. */
   uint32_t latency_frames =
     CubebUtils::GetCubebPlaybackLatencyInMilliseconds() * aParams.rate / 1000;
+  cubeb_devid deviceID = nullptr;
+  if (mSinkInfo && mSinkInfo->DeviceID()) {
+    deviceID = mSinkInfo->DeviceID();
+  }
   if (cubeb_stream_init(aContext, &stream, "AudioStream",
-                        nullptr, nullptr, nullptr, &aParams,
+                        nullptr, nullptr, deviceID, &aParams,
                         latency_frames,
                         DataCallback_S, StateCallback_S, this) == CUBEB_OK) {
     mCubebStream.reset(stream);
     CubebUtils::ReportCubebBackendUsed();
   } else {
     LOGE("OpenCubeb() failed to init cubeb");
     CubebUtils::ReportCubebStreamInitFailure(aIsFirst);
     return NS_ERROR_FAILURE;
@@ -406,27 +413,31 @@ AudioStream::SetVolume(double aVolume)
 {
   MOZ_ASSERT(aVolume >= 0.0 && aVolume <= 1.0, "Invalid volume");
 
   if (cubeb_stream_set_volume(mCubebStream.get(), aVolume * CubebUtils::GetVolumeScale()) != CUBEB_OK) {
     LOGE("Could not change volume on cubeb stream.");
   }
 }
 
-void
+nsresult
 AudioStream::Start()
 {
   MonitorAutoLock mon(mMonitor);
   MOZ_ASSERT(mState == INITIALIZED);
   mState = STARTED;
   auto r = InvokeCubeb(cubeb_stream_start);
   if (r != CUBEB_OK) {
     mState = ERRORED;
   }
   LOG("started, state %s", mState == STARTED ? "STARTED" : mState == DRAINED ? "DRAINED" : "ERRORED");
+  if (mState == STARTED || mState == DRAINED) {
+    return NS_OK;
+  }
+  return NS_ERROR_FAILURE;
 }
 
 void
 AudioStream::Pause()
 {
   MonitorAutoLock mon(mMonitor);
   MOZ_ASSERT(mState != INITIALIZED, "Must be Start()ed.");
   MOZ_ASSERT(mState != STOPPED, "Already Pause()ed.");
--- a/dom/media/AudioStream.h
+++ b/dom/media/AudioStream.h
@@ -195,29 +195,30 @@ public:
   explicit AudioStream(DataSource& aSource);
 
   // Initialize the audio stream. aNumChannels is the number of audio
   // channels (1 for mono, 2 for stereo, etc), aChannelMap is the indicator for
   // channel layout(mono, stereo, 5.1 or 7.1 ) and aRate is the sample rate
   // (22050Hz, 44100Hz, etc).
   nsresult Init(uint32_t aNumChannels,
                 AudioConfig::ChannelLayout::ChannelMap aChannelMap,
-                uint32_t aRate);
+                uint32_t aRate,
+                AudioDeviceInfo* aSinkInfo);
 
   // Closes the stream. All future use of the stream is an error.
   void Shutdown();
 
   void Reset();
 
   // Set the current volume of the audio playback. This is a value from
   // 0 (meaning muted) to 1 (meaning full volume).  Thread-safe.
   void SetVolume(double aVolume);
 
   // Start the stream.
-  void Start();
+  nsresult Start();
 
   // Pause audio playback.
   void Pause();
 
   // Resume audio playback.
   void Resume();
 
 #if defined(XP_WIN)
@@ -312,13 +313,18 @@ private:
     SHUTDOWN     // Shutdown has been called
   };
 
   StreamState mState;
 
   DataSource& mDataSource;
 
   bool mPrefillQuirk;
+
+  // The device info of the current sink. If null
+  // the default device is used. It is set
+  // during the Init() in decoder thread.
+  RefPtr<AudioDeviceInfo> mSinkInfo;
 };
 
 } // namespace mozilla
 
 #endif
--- a/dom/media/AudioStreamTrack.cpp
+++ b/dom/media/AudioStreamTrack.cpp
@@ -2,27 +2,20 @@
 /* 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 "AudioStreamTrack.h"
 
 #include "nsContentUtils.h"
 
-#include "mozilla/dom/AudioStreamTrackBinding.h"
 
 namespace mozilla {
 namespace dom {
 
-JSObject*
-AudioStreamTrack::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
-{
-  return AudioStreamTrack_Binding::Wrap(aCx, this, aGivenProto);
-}
-
 void
 AudioStreamTrack::GetLabel(nsAString& aLabel, CallerType aCallerType)
 {
   if (nsContentUtils::ResistFingerprinting(aCallerType)) {
     aLabel.AssignLiteral("Internal Microphone");
     return;
   }
   MediaStreamTrack::GetLabel(aLabel, aCallerType);
--- a/dom/media/AudioStreamTrack.h
+++ b/dom/media/AudioStreamTrack.h
@@ -15,20 +15,17 @@ namespace dom {
 class AudioStreamTrack : public MediaStreamTrack {
 public:
   AudioStreamTrack(DOMMediaStream* aStream, TrackID aTrackID,
                    TrackID aInputTrackID,
                    MediaStreamTrackSource* aSource,
                    const MediaTrackConstraints& aConstraints = MediaTrackConstraints())
     : MediaStreamTrack(aStream, aTrackID, aInputTrackID, aSource, aConstraints) {}
 
-  JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
-
   AudioStreamTrack* AsAudioStreamTrack() override { return this; }
-
   const AudioStreamTrack* AsAudioStreamTrack() const override { return this; }
 
   // WebIDL
   void GetKind(nsAString& aKind) override { aKind.AssignLiteral("audio"); }
 
   void GetLabel(nsAString& aLabel, CallerType aCallerType) override;
 
 protected:
--- a/dom/media/DOMMediaStream.cpp
+++ b/dom/media/DOMMediaStream.cpp
@@ -652,38 +652,56 @@ DOMMediaStream::CountUnderlyingStreams(c
 
 void
 DOMMediaStream::GetId(nsAString& aID) const
 {
   aID = mID;
 }
 
 void
-DOMMediaStream::GetAudioTracks(nsTArray<RefPtr<AudioStreamTrack> >& aTracks) const
+DOMMediaStream::GetAudioTracks(nsTArray<RefPtr<AudioStreamTrack>>& aTracks) const
 {
   for (const RefPtr<TrackPort>& info : mTracks) {
-    AudioStreamTrack* t = info->GetTrack()->AsAudioStreamTrack();
-    if (t) {
+    if (AudioStreamTrack* t = info->GetTrack()->AsAudioStreamTrack()) {
       aTracks.AppendElement(t);
     }
   }
 }
 
 void
-DOMMediaStream::GetVideoTracks(nsTArray<RefPtr<VideoStreamTrack> >& aTracks) const
+DOMMediaStream::GetAudioTracks(nsTArray<RefPtr<MediaStreamTrack>>& aTracks) const
+{
+  for (const RefPtr<TrackPort>& info : mTracks) {
+    if (info->GetTrack()->AsAudioStreamTrack()) {
+      aTracks.AppendElement(info->GetTrack());
+    }
+  }
+}
+
+void
+DOMMediaStream::GetVideoTracks(nsTArray<RefPtr<VideoStreamTrack>>& aTracks) const
 {
   for (const RefPtr<TrackPort>& info : mTracks) {
-    VideoStreamTrack* t = info->GetTrack()->AsVideoStreamTrack();
-    if (t) {
+    if (VideoStreamTrack* t = info->GetTrack()->AsVideoStreamTrack()) {
       aTracks.AppendElement(t);
     }
   }
 }
 
 void
+DOMMediaStream::GetVideoTracks(nsTArray<RefPtr<MediaStreamTrack>>& aTracks) const
+{
+  for (const RefPtr<TrackPort>& info : mTracks) {
+    if (info->GetTrack()->AsVideoStreamTrack()) {
+      aTracks.AppendElement(info->GetTrack());
+    }
+  }
+}
+
+void
 DOMMediaStream::GetTracks(nsTArray<RefPtr<MediaStreamTrack> >& aTracks) const
 {
   for (const RefPtr<TrackPort>& info : mTracks) {
     aTracks.AppendElement(info->GetTrack());
   }
 }
 
 void
--- a/dom/media/DOMMediaStream.h
+++ b/dom/media/DOMMediaStream.h
@@ -362,17 +362,19 @@ public:
   double CurrentTime();
 
   static already_AddRefed<dom::Promise>
   CountUnderlyingStreams(const dom::GlobalObject& aGlobal, ErrorResult& aRv);
 
   void GetId(nsAString& aID) const;
 
   void GetAudioTracks(nsTArray<RefPtr<AudioStreamTrack> >& aTracks) const;
+  void GetAudioTracks(nsTArray<RefPtr<MediaStreamTrack> >& aTracks) const;
   void GetVideoTracks(nsTArray<RefPtr<VideoStreamTrack> >& aTracks) const;
+  void GetVideoTracks(nsTArray<RefPtr<MediaStreamTrack> >& aTracks) const;
   void GetTracks(nsTArray<RefPtr<MediaStreamTrack> >& aTracks) const;
   MediaStreamTrack* GetTrackById(const nsAString& aId) const;
   void AddTrack(MediaStreamTrack& aTrack);
   void RemoveTrack(MediaStreamTrack& aTrack);
 
   /** Identical to CloneInternal(TrackForwardingOption::EXPLICIT) */
   already_AddRefed<DOMMediaStream> Clone();
 
--- a/dom/media/MediaDecoder.cpp
+++ b/dom/media/MediaDecoder.cpp
@@ -164,16 +164,24 @@ MediaDecoder::Pause()
 void
 MediaDecoder::SetVolume(double aVolume)
 {
   MOZ_ASSERT(NS_IsMainThread());
   AbstractThread::AutoEnter context(AbstractMainThread());
   mVolume = aVolume;
 }
 
+RefPtr<GenericPromise>
+MediaDecoder::SetSink(AudioDeviceInfo* aSink)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  AbstractThread::AutoEnter context(AbstractMainThread());
+  return GetStateMachine()->InvokeSetSink(aSink);
+}
+
 void
 MediaDecoder::AddOutputStream(ProcessedMediaStream* aStream,
                               TrackID aNextAvailableTrackID,
                               bool aFinishWhenEnded)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(mDecoderStateMachine, "Must be called after Load().");
   AbstractThread::AutoEnter context(AbstractMainThread());
--- a/dom/media/MediaDecoder.h
+++ b/dom/media/MediaDecoder.h
@@ -152,16 +152,19 @@ public:
   virtual void Pause();
   // Adjust the speed of the playback, optionally with pitch correction,
   void SetVolume(double aVolume);
 
   void SetPlaybackRate(double aPlaybackRate);
   void SetPreservesPitch(bool aPreservesPitch);
   void SetLooping(bool aLooping);
 
+  // Set the given device as the output device.
+  RefPtr<GenericPromise> SetSink(AudioDeviceInfo* aSink);
+
   bool GetMinimizePreroll() const { return mMinimizePreroll; }
 
   // All MediaStream-related data is protected by mReentrantMonitor.
   // We have at most one DecodedStreamData per MediaDecoder. Its stream
   // is used as the input for each ProcessedMediaStream created by calls to
   // captureStream(UntilEnded). Seeking creates a new source stream, as does
   // replaying after the input as ended. In the latter case, the new source is
   // not connected to streams created by captureStreamUntilEnded.
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -3298,50 +3298,54 @@ MediaDecoderStateMachine::WaitForData(Me
       },
       [self] (const WaitForDataRejectValue& aRejection) {
         self->mVideoWaitRequest.Complete();
         self->DecodeError(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA);
       })->Track(mVideoWaitRequest);
   }
 }
 
-void
+nsresult
 MediaDecoderStateMachine::StartMediaSink()
 {
   MOZ_ASSERT(OnTaskQueue());
-  if (!mMediaSink->IsStarted()) {
-    mAudioCompleted = false;
-    mMediaSink->Start(GetMediaTime(), Info());
-
-    auto videoPromise = mMediaSink->OnEnded(TrackInfo::kVideoTrack);
-    auto audioPromise = mMediaSink->OnEnded(TrackInfo::kAudioTrack);
-
-    if (audioPromise) {
-      audioPromise->Then(
-        OwnerThread(), __func__, this,
-        &MediaDecoderStateMachine::OnMediaSinkAudioComplete,
-        &MediaDecoderStateMachine::OnMediaSinkAudioError)
-      ->Track(mMediaSinkAudioPromise);
-    }
-    if (videoPromise) {
-      videoPromise->Then(
-        OwnerThread(), __func__, this,
-        &MediaDecoderStateMachine::OnMediaSinkVideoComplete,
-        &MediaDecoderStateMachine::OnMediaSinkVideoError)
-      ->Track(mMediaSinkVideoPromise);
-    }
-    // Remember the initial offset when playback starts. This will be used
-    // to calculate the rate at which bytes are consumed as playback moves on.
-    RefPtr<MediaData> sample = mAudioQueue.PeekFront();
-    mPlaybackOffset = sample ? sample->mOffset : 0;
-    sample = mVideoQueue.PeekFront();
-    if (sample && sample->mOffset > mPlaybackOffset) {
-      mPlaybackOffset = sample->mOffset;
-    }
+
+  if (mMediaSink->IsStarted()) {
+    return NS_OK;
   }
+
+  mAudioCompleted = false;
+  nsresult rv = mMediaSink->Start(GetMediaTime(), Info());
+
+  auto videoPromise = mMediaSink->OnEnded(TrackInfo::kVideoTrack);
+  auto audioPromise = mMediaSink->OnEnded(TrackInfo::kAudioTrack);
+
+  if (audioPromise) {
+    audioPromise->Then(
+      OwnerThread(), __func__, this,
+      &MediaDecoderStateMachine::OnMediaSinkAudioComplete,
+      &MediaDecoderStateMachine::OnMediaSinkAudioError)
+    ->Track(mMediaSinkAudioPromise);
+  }
+  if (videoPromise) {
+    videoPromise->Then(
+      OwnerThread(), __func__, this,
+      &MediaDecoderStateMachine::OnMediaSinkVideoComplete,
+      &MediaDecoderStateMachine::OnMediaSinkVideoError)
+    ->Track(mMediaSinkVideoPromise);
+  }
+  // Remember the initial offset when playback starts. This will be used
+  // to calculate the rate at which bytes are consumed as playback moves on.
+  RefPtr<MediaData> sample = mAudioQueue.PeekFront();
+  mPlaybackOffset = sample ? sample->mOffset : 0;
+  sample = mVideoQueue.PeekFront();
+  if (sample && sample->mOffset > mPlaybackOffset) {
+    mPlaybackOffset = sample->mOffset;
+  }
+  return rv;
 }
 
 bool
 MediaDecoderStateMachine::HasLowDecodedAudio()
 {
   MOZ_ASSERT(OnTaskQueue());
   return IsAudioDecoding() && GetDecodedAudioDuration()
                               < EXHAUSTED_DATA_MARGIN.MultDouble(mPlaybackRate);
@@ -3659,16 +3663,70 @@ void
 MediaDecoderStateMachine::LoopingChanged()
 {
   MOZ_ASSERT(OnTaskQueue());
   if (mSeamlessLoopingAllowed) {
     mReader->SetSeamlessLoopingEnabled(mLooping);
   }
 }
 
+RefPtr<GenericPromise>
+MediaDecoderStateMachine::InvokeSetSink(RefPtr<AudioDeviceInfo> aSink)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aSink);
+
+  ++mSetSinkRequestsCount;
+  return InvokeAsync(
+           OwnerThread(), this, __func__,
+           &MediaDecoderStateMachine::SetSink, aSink);
+}
+
+RefPtr<GenericPromise>
+MediaDecoderStateMachine::SetSink(RefPtr<AudioDeviceInfo> aSink)
+{
+  MOZ_ASSERT(OnTaskQueue());
+  if (mAudioCaptured) {
+    // Not supported yet.
+    return GenericPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
+  }
+
+  // Backup current playback parameters.
+  bool wasPlaying = mMediaSink->IsPlaying();
+
+  if (--mSetSinkRequestsCount > 0) {
+    MOZ_ASSERT(mSetSinkRequestsCount > 0);
+    return GenericPromise::CreateAndResolve(wasPlaying, __func__);
+  }
+
+  MediaSink::PlaybackParams params = mMediaSink->GetPlaybackParams();
+  params.mSink = std::move(aSink);
+
+  if (!mMediaSink->IsStarted()) {
+    mMediaSink->SetPlaybackParams(params);
+    return GenericPromise::CreateAndResolve(false, __func__);
+  }
+
+  // Stop and shutdown the existing sink.
+  StopMediaSink();
+  mMediaSink->Shutdown();
+  // Create a new sink according to whether audio is captured.
+  mMediaSink = CreateMediaSink(false);
+  // Restore playback parameters.
+  mMediaSink->SetPlaybackParams(params);
+  // Start the new sink
+  if (wasPlaying) {
+    nsresult rv = StartMediaSink();
+    if (NS_FAILED(rv)) {
+      return GenericPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
+    }
+  }
+  return GenericPromise::CreateAndResolve(wasPlaying, __func__);
+}
+
 TimeUnit
 MediaDecoderStateMachine::AudioEndTime() const
 {
   MOZ_ASSERT(OnTaskQueue());
   if (mMediaSink->IsStarted()) {
     return mMediaSink->GetEndTime(TrackInfo::kAudioTrack);
   }
   return GetMediaTime();
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -291,16 +291,18 @@ public:
 
   size_t SizeOfVideoQueue() const;
 
   size_t SizeOfAudioQueue() const;
 
   // Sets the video decode mode. Used by the suspend-video-decoder feature.
   void SetVideoDecodeMode(VideoDecodeMode aMode);
 
+  RefPtr<GenericPromise> InvokeSetSink(RefPtr<AudioDeviceInfo> aSink);
+
 private:
   class StateObject;
   class DecodeMetadataState;
   class DormantState;
   class DecodingFirstFrameState;
   class DecodingState;
   class SeekingState;
   class AccurateSeekingState;
@@ -364,16 +366,26 @@ private:
 
   // Resets all states related to decoding and aborts all pending requests
   // to the decoders.
   void ResetDecode(TrackSet aTracks = TrackSet(TrackInfo::kAudioTrack,
                                                TrackInfo::kVideoTrack));
 
   void SetVideoDecodeModeInternal(VideoDecodeMode aMode);
 
+  // Set new sink device and restart MediaSink if playback is started.
+  // Returned promise will be resolved with true if the playback is
+  // started and false if playback is stopped after setting the new sink.
+  // Returned promise will be rejected with value NS_ERROR_ABORT
+  // if the action fails or it is not supported.
+  // If there are multiple pending requests only the last one will be
+  // executed, for all previous requests the promise will be resolved
+  // with true or false similar to above.
+  RefPtr<GenericPromise> SetSink(RefPtr<AudioDeviceInfo> aSink);
+
 protected:
   virtual ~MediaDecoderStateMachine();
 
   void BufferedRangeUpdated();
 
   void ReaderSuspendedChanged();
 
   // Inserts a sample into the Audio/Video queue.
@@ -442,17 +454,18 @@ protected:
   // Stops the media sink and shut it down.
   // The decoder monitor must be held with exactly one lock count.
   // Called on the state machine thread.
   void StopMediaSink();
 
   // Create and start the media sink.
   // The decoder monitor must be held with exactly one lock count.
   // Called on the state machine thread.
-  void StartMediaSink();
+  // If start fails an NS_ERROR_FAILURE is returned.
+  nsresult StartMediaSink();
 
   // Notification method invoked when mPlayState changes.
   void PlayStateChanged();
 
   // Notification method invoked when mIsVisible changes.
   void VisibilityChanged();
 
   // Sets internal state which causes playback of media to pause.
@@ -734,16 +747,19 @@ private:
   // The time of the current frame, corresponding to the "current
   // playback position" in HTML5. This is referenced from 0, which is the initial
   // playback position.
   Canonical<media::TimeUnit> mCurrentPosition;
 
   // Used to distinguish whether the audio is producing sound.
   Canonical<bool> mIsAudioDataAudible;
 
+  // Used to count the number of pending requests to set a new sink.
+  Atomic<int> mSetSinkRequestsCount;
+
 public:
   AbstractCanonical<media::TimeIntervals>* CanonicalBuffered() const;
 
   AbstractCanonical<media::NullableTimeUnit>* CanonicalDuration()
   {
     return &mDuration;
   }
   AbstractCanonical<media::TimeUnit>* CanonicalCurrentPosition()
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -911,55 +911,59 @@ private:
   RefPtr<MediaManager> mManager; // get ref to this when creating the runnable
 };
 
 /**
  * nsIMediaDevice implementation.
  */
 NS_IMPL_ISUPPORTS(MediaDevice, nsIMediaDevice)
 
-MediaDevice::MediaDevice(MediaEngineSource* aSource,
+MediaDevice::MediaDevice(const RefPtr<MediaEngineSource>& aSource,
                          const nsString& aName,
                          const nsString& aID,
                          const nsString& aRawID)
   : mSource(aSource)
+  , mSinkInfo(nullptr)
   , mKind((mSource && MediaEngineSource::IsVideo(mSource->GetMediaSource())) ?
           dom::MediaDeviceKind::Videoinput : dom::MediaDeviceKind::Audioinput)
   , mScary(mSource->GetScary())
   , mType(NS_ConvertUTF8toUTF16(dom::MediaDeviceKindValues::strings[uint32_t(mKind)].value))
   , mName(aName)
   , mID(aID)
   , mRawID(aRawID)
 {
   MOZ_ASSERT(mSource);
 }
 
-MediaDevice::MediaDevice(const nsString& aName,
-                         const dom::MediaDeviceKind aKind,
+MediaDevice::MediaDevice(const RefPtr<AudioDeviceInfo>& aAudioDeviceInfo,
                          const nsString& aID,
                          const nsString& aRawID)
   : mSource(nullptr)
-  , mKind(aKind)
+  , mSinkInfo(aAudioDeviceInfo)
+  , mKind(mSinkInfo->Type() == AudioDeviceInfo::TYPE_INPUT ? dom::MediaDeviceKind::Audioinput
+                                                           : dom::MediaDeviceKind::Audiooutput)
   , mScary(false)
   , mType(NS_ConvertUTF8toUTF16(dom::MediaDeviceKindValues::strings[uint32_t(mKind)].value))
-  , mName(aName)
+  , mName(mSinkInfo->Name())
   , mID(aID)
   , mRawID(aRawID)
 {
   // For now this ctor is used only for Audiooutput.
   // It could be used for Audioinput and Videoinput
   // when we do not instantiate a MediaEngineSource
   // during EnumerateDevices.
   MOZ_ASSERT(mKind == dom::MediaDeviceKind::Audiooutput);
+  MOZ_ASSERT(mSinkInfo);
 }
 
-MediaDevice::MediaDevice(const MediaDevice* aOther,
+MediaDevice::MediaDevice(const RefPtr<MediaDevice>& aOther,
                          const nsString& aID,
                          const nsString& aRawID)
   : mSource(aOther->mSource)
+  , mSinkInfo(aOther->mSinkInfo)
   , mKind(aOther->mKind)
   , mScary(aOther->mScary)
   , mType(aOther->mType)
   , mName(aOther->mName)
   , mID(aID)
   , mRawID(aRawID)
 {
   MOZ_ASSERT(aOther);
@@ -3413,16 +3417,71 @@ MediaManager::EnumerateDevices(nsPIDOMWi
     // have removed all previous active listeners. Attempt to clean it here,
     // just in case, but ignore the return value.
     windowListener->Remove(sourceListener);
     onFailure->OnError(reason);
   });
   return NS_OK;
 }
 
+RefPtr<SinkInfoPromise>
+MediaManager::GetSinkDevice(nsPIDOMWindowInner* aWindow,
+                            const nsString& aDeviceId)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aWindow);
+
+  // We have to add the window id here because enumerate methods
+  // check for that and abort silently if it does not exist.
+  uint64_t windowId = aWindow->WindowID();
+  nsIPrincipal* principal = aWindow->GetExtantDoc()->NodePrincipal();
+  RefPtr<GetUserMediaWindowListener> windowListener = GetWindowListener(windowId);
+  if (windowListener) {
+    PrincipalHandle existingPrincipalHandle =
+      windowListener->GetPrincipalHandle();
+    MOZ_ASSERT(PrincipalHandleMatches(existingPrincipalHandle, principal));
+  } else {
+    windowListener = new GetUserMediaWindowListener(mMediaThread, windowId,
+                                                    MakePrincipalHandle(principal));
+    AddWindowID(windowId, windowListener);
+  }
+  // Create an inactive SourceListener to act as a placeholder, so the
+  // window listener doesn't clean itself up until we're done.
+  RefPtr<SourceListener> sourceListener = new SourceListener();
+  windowListener->Register(sourceListener);
+
+  bool isSecure = aWindow->IsSecureContext();
+
+  return EnumerateDevicesImpl(aWindow->WindowID(),
+                              MediaSourceEnum::Other,
+                              MediaSourceEnum::Other,
+                              MediaSinkEnum::Speaker,
+                              DeviceEnumerationType::Normal,
+                              DeviceEnumerationType::Normal)
+  ->Then(GetCurrentThreadSerialEventTarget(), __func__,
+         [aDeviceId, isSecure](RefPtr<MediaDeviceSetRefCnt>&& aDevices) {
+    for (RefPtr<MediaDevice>& device : **aDevices) {
+      if (aDeviceId.IsEmpty() && device->mSinkInfo->Preferred()) {
+        return SinkInfoPromise::CreateAndResolve(device->mSinkInfo, __func__);
+      }
+      if (device->mID.Equals(aDeviceId)) {
+        // TODO: Check if the application is authorized to play audio
+        // through this device (Bug 1493982).
+        if (isSecure || device->mSinkInfo->Preferred()) {
+          return SinkInfoPromise::CreateAndResolve(device->mSinkInfo, __func__);
+        }
+        return SinkInfoPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR, __func__);
+      }
+    }
+    return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__);
+  }, [](RefPtr<MediaStreamError>&& reason) {
+    return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__);
+  });
+}
+
 /*
  * GetUserMediaDevices - called by the UI-part of getUserMedia from chrome JS.
  */
 
 nsresult
 MediaManager::GetUserMediaDevices(nsPIDOMWindowInner* aWindow,
                                   const MediaStreamConstraints& aConstraints,
                                   dom::MozGetUserMediaDevicesSuccessCallback& aOnSuccess,
--- a/dom/media/MediaManager.h
+++ b/dom/media/MediaManager.h
@@ -66,27 +66,26 @@ class SourceListener;
 LogModule* GetMediaManagerLog();
 
 class MediaDevice : public nsIMediaDevice
 {
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIMEDIADEVICE
 
-  explicit MediaDevice(MediaEngineSource* aSource,
+  explicit MediaDevice(const RefPtr<MediaEngineSource>& aSource,
                        const nsString& aName,
                        const nsString& aID,
+                       const nsString& aRawID);
+
+  explicit MediaDevice(const RefPtr<AudioDeviceInfo>& aAudioDeviceInfo,
+                       const nsString& aID,
                        const nsString& aRawID = NS_LITERAL_STRING(""));
 
-  explicit MediaDevice(const nsString& aName,
-                       const dom::MediaDeviceKind aKind,
-                       const nsString& aID,
-                       const nsString& aRawID = NS_LITERAL_STRING(""));
-
-  explicit MediaDevice(const MediaDevice* aOther,
+  explicit MediaDevice(const RefPtr<MediaDevice>& aOther,
                        const nsString& aID,
                        const nsString& aRawID);
 
   uint32_t GetBestFitnessDistance(
       const nsTArray<const NormalizedConstraintSet*>& aConstraintSets,
       bool aIsChrome);
 
   nsresult Allocate(const dom::MediaTrackConstraints& aConstraints,
@@ -124,25 +123,27 @@ private:
       const dom::ConstrainDOMStringParameters& aParams);
 
   // Assigned on allocation on media thread, then read on the media thread and
   // graph thread
   RefPtr<AllocationHandle> mAllocationHandle;
 
 public:
   const RefPtr<MediaEngineSource> mSource;
+  const RefPtr<AudioDeviceInfo> mSinkInfo;
   const dom::MediaDeviceKind mKind;
   const bool mScary;
   const nsString mType;
   const nsString mName;
   const nsString mID;
   const nsString mRawID;
 };
 
 typedef nsRefPtrHashtable<nsUint64HashKey, GetUserMediaWindowListener> WindowTable;
+typedef MozPromise<RefPtr<AudioDeviceInfo>, nsresult, true> SinkInfoPromise;
 
 class MediaManager final : public nsIMediaManagerService,
                            public nsIObserver
                           ,public DeviceChangeCallback
 {
   friend SourceListener;
 public:
   static already_AddRefed<MediaManager> GetInstance();
@@ -222,16 +223,36 @@ public:
                                const nsAString& aCallID = nsString());
 
   nsresult EnumerateDevices(nsPIDOMWindowInner* aWindow,
                             nsIGetUserMediaDevicesSuccessCallback* aOnSuccess,
                             nsIDOMGetUserMediaErrorCallback* aOnFailure,
                             dom::CallerType aCallerType);
 
   nsresult EnumerateDevices(nsPIDOMWindowInner* aWindow, dom::Promise& aPromise);
+
+  // Get the sink that corresponds to the given device id.
+  // It is resposible to check if an application is
+  // authorized to play audio through the requested device.
+  // The returned promise will be resolved with the device
+  // information if the device id matches one and operation is
+  // allowed. The default device is always allowed. Non default
+  // devices are allowed only in secure context. It is pending to
+  // implement an user authorization model. The promise will be
+  // rejected in the following cases:
+  // NS_ERROR_NOT_AVAILABLE: Device id does not exist.
+  // NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR:
+  //   The requested device exists but it is not allowed to be used.
+  //   Currently, this happens only on non-default default devices
+  //   and non https connections. TODO, authorization model to allow
+  //   an application to play audio through the device (Bug 1493982).
+  // NS_ERROR_ABORT: General error.
+  RefPtr<SinkInfoPromise> GetSinkDevice(nsPIDOMWindowInner* aWindow,
+                                        const nsString& aDeviceId);
+
   void OnNavigation(uint64_t aWindowID);
   bool IsActivelyCapturingOrHasAPermission(uint64_t aWindowId);
 
   MediaEnginePrefs mPrefs;
 
   typedef nsTArray<RefPtr<MediaDevice>> MediaDeviceSet;
   typedef media::Refcountable<UniquePtr<MediaDeviceSet>> MediaDeviceSetRefCnt;
 
--- a/dom/media/MediaStreamTrack.cpp
+++ b/dom/media/MediaStreamTrack.cpp
@@ -206,16 +206,22 @@ NS_INTERFACE_MAP_END_INHERITING(DOMEvent
 
 nsPIDOMWindowInner*
 MediaStreamTrack::GetParentObject() const
 {
   MOZ_RELEASE_ASSERT(mOwningStream);
   return mOwningStream->GetParentObject();
 }
 
+JSObject*
+MediaStreamTrack::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+  return MediaStreamTrack_Binding::Wrap(aCx, this, aGivenProto);
+}
+
 void
 MediaStreamTrack::GetId(nsAString& aID) const
 {
   aID = mID;
 }
 
 void
 MediaStreamTrack::SetEnabled(bool aEnabled)
--- a/dom/media/MediaStreamTrack.h
+++ b/dom/media/MediaStreamTrack.h
@@ -381,17 +381,17 @@ public:
       MediaStreamTrackSource* aSource,
       const MediaTrackConstraints& aConstraints = MediaTrackConstraints());
 
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaStreamTrack,
                                            DOMEventTargetHelper)
 
   nsPIDOMWindowInner* GetParentObject() const;
-  virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override = 0;
+  JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   virtual AudioStreamTrack* AsAudioStreamTrack() { return nullptr; }
   virtual VideoStreamTrack* AsVideoStreamTrack() { return nullptr; }
 
   virtual const AudioStreamTrack* AsAudioStreamTrack() const { return nullptr; }
   virtual const VideoStreamTrack* AsVideoStreamTrack() const { return nullptr; }
 
   // WebIDL
--- a/dom/media/VideoStreamTrack.cpp
+++ b/dom/media/VideoStreamTrack.cpp
@@ -4,27 +4,19 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "VideoStreamTrack.h"
 
 #include "MediaStreamVideoSink.h"
 #include "MediaStreamGraph.h"
 #include "nsContentUtils.h"
 
-#include "mozilla/dom/VideoStreamTrackBinding.h"
-
 namespace mozilla {
 namespace dom {
 
-JSObject*
-VideoStreamTrack::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
-{
-  return VideoStreamTrack_Binding::Wrap(aCx, this, aGivenProto);
-}
-
 void
 VideoStreamTrack::AddVideoOutput(MediaStreamVideoSink* aSink)
 {
   GetOwnedStream()->AddVideoOutput(aSink, mTrackID);
 }
 
 void
 VideoStreamTrack::RemoveVideoOutput(MediaStreamVideoSink* aSink)
--- a/dom/media/VideoStreamTrack.h
+++ b/dom/media/VideoStreamTrack.h
@@ -18,20 +18,17 @@ namespace dom {
 class VideoStreamTrack : public MediaStreamTrack {
 public:
   VideoStreamTrack(DOMMediaStream* aStream, TrackID aTrackID,
                    TrackID aInputTrackID,
                    MediaStreamTrackSource* aSource,
                    const MediaTrackConstraints& aConstraints = MediaTrackConstraints())
     : MediaStreamTrack(aStream, aTrackID, aInputTrackID, aSource, aConstraints) {}
 
-  JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
-
   VideoStreamTrack* AsVideoStreamTrack() override { return this; }
-
   const VideoStreamTrack* AsVideoStreamTrack() const override { return this; }
 
   void AddVideoOutput(MediaStreamVideoSink* aSink);
   void RemoveVideoOutput(MediaStreamVideoSink* aSink);
 
   // WebIDL
   void GetKind(nsAString& aKind) override { aKind.AssignLiteral("video"); }
 
--- a/dom/media/imagecapture/CaptureTask.cpp
+++ b/dom/media/imagecapture/CaptureTask.cpp
@@ -74,28 +74,28 @@ CaptureTask::TaskComplete(already_AddRef
   return rv;
 }
 
 void
 CaptureTask::AttachTrack()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  dom::VideoStreamTrack* track = mImageCapture->GetVideoStreamTrack();
+  dom::MediaStreamTrack* track = mImageCapture->GetVideoStreamTrack();
   track->AddPrincipalChangeObserver(this);
   track->AddListener(mEventListener.get());
   track->AddDirectListener(this);
 }
 
 void
 CaptureTask::DetachTrack()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  dom::VideoStreamTrack* track = mImageCapture->GetVideoStreamTrack();
+  dom::MediaStreamTrack* track = mImageCapture->GetVideoStreamTrack();
   track->RemovePrincipalChangeObserver(this);
   track->RemoveListener(mEventListener.get());
   track->RemoveDirectListener(this);
 }
 
 void
 CaptureTask::PrincipalChanged(dom::MediaStreamTrack* aMediaStreamTrack)
 {
--- a/dom/media/imagecapture/ImageCapture.cpp
+++ b/dom/media/imagecapture/ImageCapture.cpp
@@ -23,59 +23,63 @@ LogModule* GetICLog()
 {
   static LazyLogModule log("ImageCapture");
   return log;
 }
 
 namespace dom {
 
 NS_IMPL_CYCLE_COLLECTION_INHERITED(ImageCapture, DOMEventTargetHelper,
-                                   mVideoStreamTrack)
+                                   mTrack)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ImageCapture)
 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
 
 NS_IMPL_ADDREF_INHERITED(ImageCapture, DOMEventTargetHelper)
 NS_IMPL_RELEASE_INHERITED(ImageCapture, DOMEventTargetHelper)
 
-ImageCapture::ImageCapture(VideoStreamTrack* aVideoStreamTrack,
+ImageCapture::ImageCapture(VideoStreamTrack* aTrack,
                            nsPIDOMWindowInner* aOwnerWindow)
   : DOMEventTargetHelper(aOwnerWindow)
+  , mTrack(aTrack)
 {
   MOZ_ASSERT(aOwnerWindow);
-  MOZ_ASSERT(aVideoStreamTrack);
-
-  mVideoStreamTrack = aVideoStreamTrack;
+  MOZ_ASSERT(aTrack);
 }
 
 ImageCapture::~ImageCapture()
 {
   MOZ_ASSERT(NS_IsMainThread());
 }
 
 already_AddRefed<ImageCapture>
 ImageCapture::Constructor(const GlobalObject& aGlobal,
-                          VideoStreamTrack& aTrack,
+                          MediaStreamTrack& aTrack,
                           ErrorResult& aRv)
 {
   nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal.GetAsSupports());
   if (!win) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
-  RefPtr<ImageCapture> object = new ImageCapture(&aTrack, win);
+  if (!aTrack.AsVideoStreamTrack()) {
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return nullptr;
+  }
+
+  RefPtr<ImageCapture> object = new ImageCapture(aTrack.AsVideoStreamTrack(), win);
 
   return object.forget();
 }
 
-VideoStreamTrack*
+MediaStreamTrack*
 ImageCapture::GetVideoStreamTrack() const
 {
-  return mVideoStreamTrack;
+  return mTrack;
 }
 
 nsresult
 ImageCapture::TakePhotoByMediaEngine()
 {
   // Callback for TakPhoto(), it also monitor the principal. If principal
   // changes, it returns PHOTO_ERROR with security error.
   class TakePhotoCallback : public MediaEnginePhotoCallback,
@@ -113,35 +117,35 @@ ImageCapture::TakePhotoByMediaEngine()
 
   protected:
     ~TakePhotoCallback()
     {
       MOZ_ASSERT(NS_IsMainThread());
       mVideoTrack->RemovePrincipalChangeObserver(this);
     }
 
-    RefPtr<VideoStreamTrack> mVideoTrack;
-    RefPtr<ImageCapture> mImageCapture;
+    const RefPtr<VideoStreamTrack> mVideoTrack;
+    const RefPtr<ImageCapture> mImageCapture;
     bool mPrincipalChanged;
   };
 
   RefPtr<MediaEnginePhotoCallback> callback =
-    new TakePhotoCallback(mVideoStreamTrack, this);
-  return mVideoStreamTrack->GetSource().TakePhoto(callback);
+    new TakePhotoCallback(mTrack, this);
+  return mTrack->GetSource().TakePhoto(callback);
 }
 
 void
 ImageCapture::TakePhoto(ErrorResult& aResult)
 {
-  // According to spec, VideoStreamTrack.readyState must be "live"; however
+  // According to spec, MediaStreamTrack.readyState must be "live"; however
   // gecko doesn't implement it yet (bug 910249). Instead of readyState, we
-  // check VideoStreamTrack.enable before bug 910249 is fixed.
+  // check MediaStreamTrack.enable before bug 910249 is fixed.
   // The error code should be INVALID_TRACK, but spec doesn't define it in
   // ImageCaptureError. So it returns PHOTO_ERROR here before spec updates.
-  if (!mVideoStreamTrack->Enabled()) {
+  if (!mTrack->Enabled()) {
     PostErrorEvent(ImageCaptureError::PHOTO_ERROR, NS_ERROR_FAILURE);
     return;
   }
 
   // Try if MediaEngine supports taking photo.
   nsresult rv = TakePhotoByMediaEngine();
 
   // It falls back to MediaStreamGraph image capture if MediaEngine doesn't
@@ -206,17 +210,17 @@ ImageCapture::PostErrorEvent(uint16_t aE
   return DispatchTrustedEvent(event);
 }
 
 bool
 ImageCapture::CheckPrincipal()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
-  nsCOMPtr<nsIPrincipal> principal = mVideoStreamTrack->GetPrincipal();
+  nsCOMPtr<nsIPrincipal> principal = mTrack->GetPrincipal();
 
   if (!GetOwner()) {
     return false;
   }
   nsCOMPtr<nsIDocument> doc = GetOwner()->GetExtantDoc();
   if (!doc || !principal) {
     return false;
   }
--- a/dom/media/imagecapture/ImageCapture.h
+++ b/dom/media/imagecapture/ImageCapture.h
@@ -16,61 +16,62 @@ namespace mozilla {
 #ifndef IC_LOG
 LogModule* GetICLog();
 #define IC_LOG(...) MOZ_LOG(GetICLog(), mozilla::LogLevel::Debug, (__VA_ARGS__))
 #endif
 
 namespace dom {
 
 class Blob;
+class MediaStreamTrack;
 class VideoStreamTrack;
 
 /**
- *  Implementation of https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-
- *  capture/ImageCapture.html.
- *  The ImageCapture accepts a VideoStreamTrack as input source. The image will
- *  be sent back as a JPG format via Blob event.
+ * Implementation of https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-
+ * capture/ImageCapture.html.
+ * The ImageCapture accepts a video MediaStreamTrack as input source. The image
+ * will be sent back as a JPG format via Blob event.
  *
- *  All the functions in ImageCapture are run in main thread.
+ * All the functions in ImageCapture are run in main thread.
  *
- *  There are two ways to capture image, MediaEngineSource and MediaStreamGraph.
- *  When the implementation of MediaEngineSource supports TakePhoto(),
- *  it uses the platform camera to grab image. Otherwise, it falls back
- *  to the MediaStreamGraph way.
+ * There are two ways to capture image, MediaEngineSource and MediaStreamGraph.
+ * When the implementation of MediaEngineSource supports TakePhoto(),
+ * it uses the platform camera to grab image. Otherwise, it falls back
+ * to the MediaStreamGraph way.
  */
 
 class ImageCapture final : public DOMEventTargetHelper
 {
 public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ImageCapture, DOMEventTargetHelper)
 
   IMPL_EVENT_HANDLER(photo)
   IMPL_EVENT_HANDLER(error)
 
   // WebIDL members.
   void TakePhoto(ErrorResult& aResult);
 
-  // The MediaStream passed into the constructor.
-  VideoStreamTrack* GetVideoStreamTrack() const;
+  // The MediaStreamTrack passed into the constructor.
+  MediaStreamTrack* GetVideoStreamTrack() const;
 
   // nsWrapperCache member
   JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
   {
     return ImageCapture_Binding::Wrap(aCx, this, aGivenProto);
   }
 
   // ImageCapture class members
   nsPIDOMWindowInner* GetParentObject() { return GetOwner(); }
 
   static already_AddRefed<ImageCapture> Constructor(const GlobalObject& aGlobal,
-                                                    VideoStreamTrack& aTrack,
+                                                    MediaStreamTrack& aTrack,
                                                     ErrorResult& aRv);
 
-  ImageCapture(VideoStreamTrack* aVideoStreamTrack,
+  ImageCapture(VideoStreamTrack* aTrack,
                nsPIDOMWindowInner* aOwnerWindow);
 
   // Post a Blob event to script.
   nsresult PostBlobEvent(Blob* aBlob);
 
   // Post an error event to script.
   // aErrorCode should be one of error codes defined in ImageCaptureError.h.
   // aReason is the nsresult which maps to a error string in dom/base/domerr.msg.
@@ -80,15 +81,15 @@ public:
 
 protected:
   virtual ~ImageCapture();
 
   // Capture image by MediaEngine. If it's not support taking photo, this function
   // should return NS_ERROR_NOT_IMPLEMENTED.
   nsresult TakePhotoByMediaEngine();
 
-  RefPtr<VideoStreamTrack> mVideoStreamTrack;
+  RefPtr<VideoStreamTrack> mTrack;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // IMAGECAPTURE_H
--- a/dom/media/mediasink/AudioSink.cpp
+++ b/dom/media/mediasink/AudioSink.cpp
@@ -66,37 +66,37 @@ AudioSink::AudioSink(AbstractThread* aTh
 
   mOutputChannels = DecideAudioPlaybackChannels(mInfo);
 }
 
 AudioSink::~AudioSink()
 {
 }
 
-RefPtr<GenericPromise>
-AudioSink::Init(const PlaybackParams& aParams)
+nsresult
+AudioSink::Init(const PlaybackParams& aParams, RefPtr<GenericPromise>& aEndPromise)
 {
   MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
 
   mAudioQueueListener = mAudioQueue.PushEvent().Connect(
     mOwnerThread, this, &AudioSink::OnAudioPushed);
   mAudioQueueFinishListener = mAudioQueue.FinishEvent().Connect(
     mOwnerThread, this, &AudioSink::NotifyAudioNeeded);
   mProcessedQueueListener = mProcessedQueue.PopEvent().Connect(
     mOwnerThread, this, &AudioSink::OnAudioPopped);
 
   // To ensure at least one audio packet will be popped from AudioQueue and
   // ready to be played.
   NotifyAudioNeeded();
-  RefPtr<GenericPromise> p = mEndPromise.Ensure(__func__);
+  aEndPromise = mEndPromise.Ensure(__func__);
   nsresult rv = InitializeAudioStream(aParams);
   if (NS_FAILED(rv)) {
     mEndPromise.Reject(rv, __func__);
   }
-  return p;
+  return rv;
 }
 
 TimeUnit
 AudioSink::GetPosition()
 {
   int64_t tmp;
   if (mAudioStream &&
       (tmp = mAudioStream->GetPosition()) >= 0) {
@@ -192,31 +192,30 @@ AudioSink::InitializeAudioStream(const P
   // the coming audio data, so we use the predefined channel map instead.
   AudioConfig::ChannelLayout::ChannelMap channelMap =
     mConverter ? mConverter->OutputConfig().Layout().Map()
                : AudioConfig::ChannelLayout(mOutputChannels).Map();
   // The layout map used here is already processed by mConverter with
   // mOutputChannels into SMPTE format, so there is no need to worry if
   // StaticPrefs::accessibility_monoaudio_enable() or
   // StaticPrefs::MediaForcestereoEnabled() is applied.
-  nsresult rv = mAudioStream->Init(mOutputChannels, channelMap, mOutputRate);
+  nsresult rv = mAudioStream->Init(mOutputChannels, channelMap,
+                                   mOutputRate, aParams.mSink);
   if (NS_FAILED(rv)) {
     mAudioStream->Shutdown();
     mAudioStream = nullptr;
     return rv;
   }
 
   // Set playback params before calling Start() so they can take effect
   // as soon as the 1st DataCallback of the AudioStream fires.
   mAudioStream->SetVolume(aParams.mVolume);
   mAudioStream->SetPlaybackRate(aParams.mPlaybackRate);
   mAudioStream->SetPreservesPitch(aParams.mPreservesPitch);
-  mAudioStream->Start();
-
-  return NS_OK;
+  return mAudioStream->Start();
 }
 
 TimeUnit
 AudioSink::GetEndTime() const
 {
   int64_t written;
   {
     MonitorAutoLock mon(mMonitor);
--- a/dom/media/mediasink/AudioSink.h
+++ b/dom/media/mediasink/AudioSink.h
@@ -33,17 +33,17 @@ public:
             MediaQueue<AudioData>& aAudioQueue,
             const TimeUnit& aStartTime,
             const AudioInfo& aInfo);
 
   ~AudioSink();
 
   // Return a promise which will be resolved when AudioSink
   // finishes playing, or rejected if any error.
-  RefPtr<GenericPromise> Init(const PlaybackParams& aParams);
+  nsresult Init(const PlaybackParams& aParams, RefPtr<GenericPromise>& aEndPromise);
 
   /*
    * All public functions are not thread-safe.
    * Called on the task queue of MDSM only.
    */
   TimeUnit GetPosition();
   TimeUnit GetEndTime() const;
 
--- a/dom/media/mediasink/AudioSinkWrapper.cpp
+++ b/dom/media/mediasink/AudioSinkWrapper.cpp
@@ -173,39 +173,41 @@ AudioSinkWrapper::SetPlaying(bool aPlayi
     // Remember how long we've played.
     mPlayDuration = GetPosition();
     // mPlayStartTime must be updated later since GetPosition()
     // depends on the value of mPlayStartTime.
     mPlayStartTime = TimeStamp();
   }
 }
 
-void
+nsresult
 AudioSinkWrapper::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
 {
   AssertOwnerThread();
   MOZ_ASSERT(!mIsStarted, "playback already started.");
 
   mIsStarted = true;
   mPlayDuration = aStartTime;
   mPlayStartTime = TimeStamp::Now();
 
   // no audio is equivalent to audio ended before video starts.
   mAudioEnded = !aInfo.HasAudio();
 
+  nsresult rv = NS_OK;
   if (aInfo.HasAudio()) {
     mAudioSink.reset(mCreator->Create());
-    mEndPromise = mAudioSink->Init(mParams);
+    rv = mAudioSink->Init(mParams, mEndPromise);
 
     mEndPromise->Then(
       mOwnerThread.get(), __func__, this,
       &AudioSinkWrapper::OnAudioEnded,
       &AudioSinkWrapper::OnAudioEnded
     )->Track(mAudioSinkPromise);
   }
+  return rv;
 }
 
 void
 AudioSinkWrapper::Stop()
 {
   AssertOwnerThread();
   MOZ_ASSERT(mIsStarted, "playback not started.");
 
--- a/dom/media/mediasink/AudioSinkWrapper.h
+++ b/dom/media/mediasink/AudioSinkWrapper.h
@@ -62,17 +62,17 @@ public:
   TimeUnit GetPosition(TimeStamp* aTimeStamp = nullptr) const override;
   bool HasUnplayedFrames(TrackType aType) const override;
 
   void SetVolume(double aVolume) override;
   void SetPlaybackRate(double aPlaybackRate) override;
   void SetPreservesPitch(bool aPreservesPitch) override;
   void SetPlaying(bool aPlaying) override;
 
-  void Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) override;
+  nsresult Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) override;
   void Stop() override;
   bool IsStarted() const override;
   bool IsPlaying() const override;
 
   void Shutdown() override;
 
   nsCString GetDebugInfo() override;
 
--- a/dom/media/mediasink/DecodedStream.cpp
+++ b/dom/media/mediasink/DecodedStream.cpp
@@ -299,17 +299,17 @@ DecodedStream::OnEnded(TrackType aType)
     // finished.
     return mFinishPromise;
   } else if (aType == TrackInfo::kVideoTrack && mInfo.HasVideo()) {
     return mFinishPromise;
   }
   return nullptr;
 }
 
-void
+nsresult
 DecodedStream::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
 {
   AssertOwnerThread();
   MOZ_ASSERT(mStartTime.isNothing(), "playback already started.");
 
   mStartTime.emplace(aStartTime);
   mLastOutputTime = TimeUnit::Zero();
   mInfo = aInfo;
@@ -366,16 +366,17 @@ DecodedStream::Start(const TimeUnit& aSt
   mData = static_cast<R*>(r.get())->ReleaseData();
 
   if (mData) {
     mOutputListener = mData->OnOutput().Connect(
       mOwnerThread, this, &DecodedStream::NotifyOutput);
     mData->SetPlaying(mPlaying);
     SendData();
   }
+  return NS_OK;
 }
 
 void
 DecodedStream::Stop()
 {
   AssertOwnerThread();
   MOZ_ASSERT(mStartTime.isSome(), "playback not started.");
 
--- a/dom/media/mediasink/DecodedStream.h
+++ b/dom/media/mediasink/DecodedStream.h
@@ -56,17 +56,17 @@ public:
     return false;
   }
 
   void SetVolume(double aVolume) override;
   void SetPlaybackRate(double aPlaybackRate) override;
   void SetPreservesPitch(bool aPreservesPitch) override;
   void SetPlaying(bool aPlaying) override;
 
-  void Start(const media::TimeUnit& aStartTime, const MediaInfo& aInfo) override;
+  nsresult Start(const media::TimeUnit& aStartTime, const MediaInfo& aInfo) override;
   void Stop() override;
   bool IsStarted() const override;
   bool IsPlaying() const override;
 
   nsCString GetDebugInfo() override;
 
 protected:
   virtual ~DecodedStream();
--- a/dom/media/mediasink/MediaSink.h
+++ b/dom/media/mediasink/MediaSink.h
@@ -2,20 +2,21 @@
 /* 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/. */
 
 #ifndef MediaSink_h_
 #define MediaSink_h_
 
+#include "AudioDeviceInfo.h"
+#include "MediaInfo.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/MozPromise.h"
 #include "nsISupportsImpl.h"
-#include "MediaInfo.h"
 
 namespace mozilla {
 
 class TimeStamp;
 
 namespace media {
 
 /**
@@ -39,16 +40,17 @@ public:
   typedef mozilla::TrackInfo::TrackType TrackType;
 
   struct PlaybackParams {
     PlaybackParams()
       : mVolume(1.0) , mPlaybackRate(1.0) , mPreservesPitch(true) {}
     double mVolume;
     double mPlaybackRate;
     bool mPreservesPitch;
+    RefPtr<AudioDeviceInfo> mSink;
   };
 
   // Return the playback parameters of this sink.
   // Can be called in any state.
   virtual const PlaybackParams& GetPlaybackParams() const = 0;
 
   // Set the playback parameters of this sink.
   // Can be called in any state.
@@ -95,17 +97,17 @@ public:
 
   // Single frame rendering operation may need to be done before playback
   // started (1st frame) or right after seek completed or playback stopped.
   // Do nothing if this sink has no video track. Can be called in any state.
   virtual void Redraw(const VideoInfo& aInfo) {};
 
   // Begin a playback session with the provided start time and media info.
   // Must be called when playback is stopped.
-  virtual void Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) = 0;
+  virtual nsresult Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) = 0;
 
   // Finish a playback session.
   // Must be called after playback starts.
   virtual void Stop() = 0;
 
   // Return true if playback has started.
   // Can be called in any state.
   virtual bool IsStarted() const = 0;
--- a/dom/media/mediasink/VideoSink.cpp
+++ b/dom/media/mediasink/VideoSink.cpp
@@ -200,23 +200,23 @@ VideoSink::SetPlaying(bool aPlaying)
     // rendering while becoming playing status. because the VideoQueue may be
     // full already.
     TryUpdateRenderedVideoFrames();
   }
 
   EnsureHighResTimersOnOnlyIfPlaying();
 }
 
-void
+nsresult
 VideoSink::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo)
 {
   AssertOwnerThread();
   VSINK_LOG("[%s]", __func__);
 
-  mAudioSink->Start(aStartTime, aInfo);
+  nsresult rv = mAudioSink->Start(aStartTime, aInfo);
 
   mHasVideo = aInfo.HasVideo();
 
   if (mHasVideo) {
     mEndPromise = mEndPromiseHolder.Ensure(__func__);
 
     // If the underlying MediaSink has an end promise for the video track (which
     // happens when mAudioSink refers to a DecodedStream), we must wait for it
@@ -242,16 +242,17 @@ VideoSink::Start(const TimeUnit& aStartT
         ->Track(mVideoSinkEndRequest);
     }
 
     ConnectListener();
     // Run the render loop at least once so we can resolve the end promise
     // when video duration is 0.
     UpdateRenderedVideoFrames();
   }
+  return rv;
 }
 
 void
 VideoSink::Stop()
 {
   AssertOwnerThread();
   MOZ_ASSERT(mAudioSink->IsStarted(), "playback not started.");
   VSINK_LOG("[%s]", __func__);
--- a/dom/media/mediasink/VideoSink.h
+++ b/dom/media/mediasink/VideoSink.h
@@ -53,17 +53,17 @@ public:
   void SetVolume(double aVolume) override;
 
   void SetPreservesPitch(bool aPreservesPitch) override;
 
   void SetPlaying(bool aPlaying) override;
 
   void Redraw(const VideoInfo& aInfo) override;
 
-  void Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) override;
+  nsresult Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) override;
 
   void Stop() override;
 
   bool IsStarted() const override;
 
   bool IsPlaying() const override;
 
   void Shutdown() override;
--- a/dom/media/moz.build
+++ b/dom/media/moz.build
@@ -92,16 +92,17 @@ EXPORTS += [
     'ADTSDecoder.h',
     'ADTSDemuxer.h',
     'AsyncLogger.h',
     'AudioBufferUtils.h',
     'AudioChannelFormat.h',
     'AudioCompactor.h',
     'AudioConfig.h',
     'AudioConverter.h',
+    'AudioDeviceInfo.h',
     'AudioMixer.h',
     'AudioPacketizer.h',
     'AudioSampleFormat.h',
     'AudioSegment.h',
     'AudioStream.h',
     'AutoplayPolicy.h',
     'BackgroundVideoDecodingPermissionObserver.h',
     'Benchmark.h',
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -353,8 +353,11 @@ skip-if = (android_version == '18') # an
 skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_verifyDescriptions.html]
 skip-if = (android_version == '18')
 [test_fingerprinting_resistance.html]
 [test_getUserMedia_nonDefaultRate.html]
 [test_peerConnection_nonDefaultRate.html]
 skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_forceSampleRate.html]
+
+[test_setSinkId.html]
+skip-if = os != 'linux' # the only platform with real devices
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_setSinkId.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<audio id="audio"></audio>
+
+<script>
+  createHTML({
+    title: "SetSinkId in HTMLMediaElement",
+    bug: "934425",
+  });
+
+  /**
+   * Run a test to verify set sink id in audio element.
+   */
+  runTest(async () => {
+    await pushPrefs(["media.setsinkid.enabled", true]);
+
+    if (!SpecialPowers.getCharPref("media.audio_loopback_dev", "")) {
+      ok(false, "No loopback device set by framework. Try --use-test-media-devices");
+      return;
+    }
+
+    const allDevices = await navigator.mediaDevices.enumerateDevices();
+    const audioDevices = allDevices.filter(({kind}) => kind == 'audiooutput');
+    info(`Found  ${audioDevices.length} output devices`);
+    ok(audioDevices.length > 0, "More than one output device found");
+
+    is(audio.sinkId, "", "Initial value is empty string");
+
+    const p = audio.setSinkId(audioDevices[0].deviceId);
+    is(audio.sinkId, "", "Value is unchanged upon function return");
+    is(await p, undefined, "promise resolves with undefined");
+    is(audio.sinkId, audioDevices[0].deviceId, `Sink device is set, id: ${audio.sinkId}`);
+
+    await audio.setSinkId(audioDevices[0].deviceId);
+    ok(true, `Sink device is set for 2nd time for the same id: ${audio.sinkId}`);
+
+    try {
+      await audio.setSinkId("dummy sink id");
+      ok(false, "Never enter here, this must fail");
+    } catch (error) {
+      ok(true, `Set sink id expected to fail: ${error}`);
+      is(error.name, "NotFoundError", "Verify correct error");
+    }
+  });
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/media/webrtc/MediaEngineDefault.cpp
+++ b/dom/media/webrtc/MediaEngineDefault.cpp
@@ -575,39 +575,42 @@ MediaEngineDefault::EnumerateDevices(uin
 
       nsTArray<RefPtr<MediaEngineSource>>*
         devicesForThisWindow = mVSources.LookupOrAdd(aWindowId);
       auto newSource = MakeRefPtr<MediaEngineDefaultVideoSource>();
       devicesForThisWindow->AppendElement(newSource);
       aDevices->AppendElement(MakeRefPtr<MediaDevice>(
                                 newSource,
                                 newSource->GetName(),
-                                NS_ConvertUTF8toUTF16(newSource->GetUUID())));
+                                NS_ConvertUTF8toUTF16(newSource->GetUUID()),
+                                NS_LITERAL_STRING("")));
       return;
     }
     case dom::MediaSourceEnum::Microphone: {
       nsTArray<RefPtr<MediaEngineDefaultAudioSource>>*
         devicesForThisWindow = mASources.LookupOrAdd(aWindowId);
       for (const RefPtr<MediaEngineDefaultAudioSource>& source : *devicesForThisWindow) {
         if (source->IsAvailable()) {
           aDevices->AppendElement(MakeRefPtr<MediaDevice>(
                                     source,
                                     source->GetName(),
-                                    NS_ConvertUTF8toUTF16(source->GetUUID())));
+                                    NS_ConvertUTF8toUTF16(source->GetUUID()),
+                                    NS_LITERAL_STRING("")));
         }
       }
 
       if (aDevices->IsEmpty()) {
         // All streams are currently busy, just make a new one.
         auto newSource = MakeRefPtr<MediaEngineDefaultAudioSource>();
         devicesForThisWindow->AppendElement(newSource);
         aDevices->AppendElement(MakeRefPtr<MediaDevice>(
                                   newSource,
                                   newSource->GetName(),
-                                  NS_ConvertUTF8toUTF16(newSource->GetUUID())));
+                                  NS_ConvertUTF8toUTF16(newSource->GetUUID()),
+                                  NS_LITERAL_STRING("")));
       }
       return;
     }
     default:
       MOZ_ASSERT_UNREACHABLE("Unsupported source type");
       return;
   }
 
--- a/dom/media/webrtc/MediaEngineWebRTC.cpp
+++ b/dom/media/webrtc/MediaEngineWebRTC.cpp
@@ -161,25 +161,27 @@ MediaEngineWebRTC::EnumerateVideoDevices
     } else {
       vSource = new MediaEngineRemoteVideoSource(i, capEngine, aMediaSource,
                                                  scaryKind || scarySource);
       devicesForThisWindow->Put(uuid, vSource);
     }
     aDevices->AppendElement(MakeRefPtr<MediaDevice>(
                               vSource,
                               vSource->GetName(),
-                              NS_ConvertUTF8toUTF16(vSource->GetUUID())));
+                              NS_ConvertUTF8toUTF16(vSource->GetUUID()),
+                              NS_LITERAL_STRING("")));
   }
 
   if (mHasTabVideoSource || dom::MediaSourceEnum::Browser == aMediaSource) {
     RefPtr<MediaEngineSource> tabVideoSource = new MediaEngineTabVideoSource();
     aDevices->AppendElement(MakeRefPtr<MediaDevice>(
                               tabVideoSource,
                               tabVideoSource->GetName(),
-                              NS_ConvertUTF8toUTF16(tabVideoSource->GetUUID())));
+                              NS_ConvertUTF8toUTF16(tabVideoSource->GetUUID()),
+                              NS_LITERAL_STRING("")));
   }
 }
 
 void
 MediaEngineWebRTC::EnumerateMicrophoneDevices(uint64_t aWindowId,
                                               nsTArray<RefPtr<MediaDevice> >* aDevices)
 {
   mMutex.AssertCurrentThreadOwns();
@@ -213,17 +215,18 @@ MediaEngineWebRTC::EnumerateMicrophoneDe
             // 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()));
+                                     NS_ConvertUTF8toUTF16(source->GetUUID()),
+                                     NS_LITERAL_STRING(""));
       if (devices[i]->Preferred()) {
 #ifdef DEBUG
         if (!foundPreferredDevice) {
           foundPreferredDevice = true;
         } else {
           MOZ_ASSERT(!foundPreferredDevice,
               "Found more than one preferred audio input device"
               "while enumerating");
@@ -246,20 +249,17 @@ MediaEngineWebRTC::EnumerateSpeakerDevic
   for (auto& device : devices) {
     if (device->State() == CUBEB_DEVICE_STATE_ENABLED) {
       MOZ_ASSERT(device->Type() == CUBEB_DEVICE_TYPE_OUTPUT);
       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->Name(),
-                                dom::MediaDeviceKind::Audiooutput,
-                                uuid));
+      aDevices->AppendElement(MakeRefPtr<MediaDevice>(device, uuid));
     }
   }
 }
 
 
 void
 MediaEngineWebRTC::EnumerateDevices(uint64_t aWindowId,
                                     dom::MediaSourceEnum aMediaSource,
@@ -273,17 +273,18 @@ MediaEngineWebRTC::EnumerateDevices(uint
   if (MediaEngineSource::IsVideo(aMediaSource)) {
     EnumerateVideoDevices(aWindowId, aMediaSource, aDevices);
   } else if (aMediaSource == dom::MediaSourceEnum::AudioCapture) {
     RefPtr<MediaEngineWebRTCAudioCaptureSource> audioCaptureSource =
       new MediaEngineWebRTCAudioCaptureSource(nullptr);
     aDevices->AppendElement(MakeRefPtr<MediaDevice>(
                               audioCaptureSource,
                               audioCaptureSource->GetName(),
-                              NS_ConvertUTF8toUTF16(audioCaptureSource->GetUUID())));
+                              NS_ConvertUTF8toUTF16(audioCaptureSource->GetUUID()),
+                              NS_LITERAL_STRING("")));
   } else if (aMediaSource == dom::MediaSourceEnum::Microphone) {
     MOZ_ASSERT(aMediaSource == dom::MediaSourceEnum::Microphone);
     EnumerateMicrophoneDevices(aWindowId, aDevices);
   }
 
   if (aMediaSink == MediaSinkEnum::Speaker) {
     EnumerateSpeakerDevices(aWindowId, aDevices);
   }
--- a/dom/media/webrtc/MediaTrackConstraints.cpp
+++ b/dom/media/webrtc/MediaTrackConstraints.cpp
@@ -615,17 +615,18 @@ MediaConstraintsHelper::FindBadConstrain
 MediaConstraintsHelper::FindBadConstraint(
     const NormalizedConstraints& aConstraints,
     const RefPtr<MediaEngineSource>& aMediaEngineSource,
     const nsString& aDeviceId)
 {
   AutoTArray<RefPtr<MediaDevice>, 1> devices;
   devices.AppendElement(MakeRefPtr<MediaDevice>(aMediaEngineSource,
                                                 aMediaEngineSource->GetName(),
-                                                aDeviceId));
+                                                aDeviceId,
+                                                NS_LITERAL_STRING("")));
   return FindBadConstraint(aConstraints, devices);
 }
 
 /* static */ void
 MediaConstraintsHelper::ConvertOldWithWarning(
     const dom::OwningBooleanOrConstrainBooleanParameters& old,
     dom::OwningBooleanOrConstrainBooleanParameters& to,
     const char* aMessageName,
--- a/dom/tests/mochitest/general/test_interfaces.js
+++ b/dom/tests/mochitest/general/test_interfaces.js
@@ -148,18 +148,16 @@ var interfaceNamesInGlobalScope =
     {name: "AudioParam", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "AudioParamMap", insecureContext: true, disabled: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "AudioProcessingEvent", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "AudioScheduledSourceNode", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    {name: "AudioStreamTrack", insecureContext: true},
-// IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "AudioWorkletNode", insecureContext: false, disabled: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "AuthenticatorAssertionResponse"},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "AuthenticatorAttestationResponse"},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "AuthenticatorResponse"},
 // IMPORTANT: Do not change this list without review from a DOM peer!
@@ -1150,18 +1148,16 @@ var interfaceNamesInGlobalScope =
     {name: "URLSearchParams", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "UserProximityEvent", insecureContext: true, disabled: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "ValidityState", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "VideoPlaybackQuality", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    {name: "VideoStreamTrack", insecureContext: true},
-// IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "VisualViewport", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "VRDisplay", insecureContext: true, releaseNonWindows: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "VRDisplayCapabilities", insecureContext: true, releaseNonWindows: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "VRDisplayEvent", insecureContext: true, releaseNonWindows: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
deleted file mode 100644
--- a/dom/webidl/AudioStreamTrack.webidl
+++ /dev/null
@@ -1,16 +0,0 @@
-/* -*- Mode: IDL; 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/.
- *
- * The origin of this IDL file is
- * http://dev.w3.org/2011/webrtc/editor/getusermedia.html
- *
- * Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
- * liability, trademark and document use rules apply.
- */
-
-// [Constructor(optional MediaTrackConstraints audioConstraints)]
-interface AudioStreamTrack : MediaStreamTrack {
-//    static sequence<DOMString> getSourceIds ();
-};
--- a/dom/webidl/HTMLMediaElement.webidl
+++ b/dom/webidl/HTMLMediaElement.webidl
@@ -215,16 +215,24 @@ partial interface HTMLMediaElement {
 partial interface HTMLMediaElement {
   [Pref="media.test.video-suspend"]
   void setVisible(boolean aVisible);
 
   [Pref="media.test.video-suspend"]
   boolean hasSuspendTaint();
 };
 
+/* Audio Output Devices API */
+partial interface HTMLMediaElement {
+  [Pref="media.setsinkid.enabled"]
+  readonly attribute DOMString sinkId;
+  [Throws, Pref="media.setsinkid.enabled"]
+  Promise<void> setSinkId(DOMString sinkId);
+};
+
 /*
  * API that exposes whether a call to HTMLMediaElement.play() would be
  * blocked by autoplay policies; whether the promise returned by play()
  * would be rejected with NotAllowedError.
  */
 partial interface HTMLMediaElement {
   [Pref="media.allowed-to-play.enabled"]
   readonly attribute boolean allowedToPlay;
--- a/dom/webidl/ImageCapture.webidl
+++ b/dom/webidl/ImageCapture.webidl
@@ -5,20 +5,20 @@
  *
  * The origin of this IDL file is
  * https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/ImageCapture.html
  *
  * Copyright © 2012-2014 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved.
  * W3C liability, trademark and document use rules apply.
  */
 
-[Pref="dom.imagecapture.enabled", Constructor(VideoStreamTrack track)]
+[Pref="dom.imagecapture.enabled", Constructor(MediaStreamTrack track)]
 interface ImageCapture : EventTarget {
   // readonly attribute PhotoSettingsOptions photoSettingsOptions;
-  readonly attribute VideoStreamTrack videoStreamTrack;
+  readonly attribute MediaStreamTrack videoStreamTrack;
   attribute EventHandler onphoto;
   attribute EventHandler onerror;
   // attribute EventHandler onphotosettingschange;
   // attribute EventHandler onframegrab;
 
   // [Throws]
   // void setOptions (PhotoSettings? photoSettings);
   [Throws]
--- a/dom/webidl/MediaStream.webidl
+++ b/dom/webidl/MediaStream.webidl
@@ -24,18 +24,18 @@ dictionary MediaStreamConstraints {
 };
 
 [Exposed=Window,
  Constructor,
  Constructor (MediaStream stream),
  Constructor (sequence<MediaStreamTrack> tracks)]
 interface MediaStream : EventTarget {
     readonly    attribute DOMString    id;
-    sequence<AudioStreamTrack> getAudioTracks ();
-    sequence<VideoStreamTrack> getVideoTracks ();
+    sequence<MediaStreamTrack> getAudioTracks ();
+    sequence<MediaStreamTrack> getVideoTracks ();
     sequence<MediaStreamTrack> getTracks ();
     MediaStreamTrack?          getTrackById (DOMString trackId);
     void                       addTrack (MediaStreamTrack track);
     void                       removeTrack (MediaStreamTrack track);
     MediaStream                clone ();
     readonly    attribute boolean      active;
                 attribute EventHandler onaddtrack;
                 attribute EventHandler onremovetrack;
deleted file mode 100644
--- a/dom/webidl/VideoStreamTrack.webidl
+++ /dev/null
@@ -1,19 +0,0 @@
-/* -*- Mode: IDL; 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/.
- *
- * The origin of this IDL file is
- * http://dev.w3.org/2011/webrtc/editor/getusermedia.html
- *
- * Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
- * liability, trademark and document use rules apply.
- */
-
-// [Constructor(optional MediaTrackConstraints videoConstraints)]
-interface VideoStreamTrack : MediaStreamTrack {
-//    static sequence<DOMString> getSourceIds ();
-//    void                       takePhoto ();
-//                attribute EventHandler onphoto;
-//                attribute EventHandler onphotoerror;
-};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -382,17 +382,16 @@ WEBIDL_FILES = [
     'AudioContext.webidl',
     'AudioDestinationNode.webidl',
     'AudioListener.webidl',
     'AudioNode.webidl',
     'AudioParam.webidl',
     'AudioParamMap.webidl',
     'AudioProcessingEvent.webidl',
     'AudioScheduledSourceNode.webidl',
-    'AudioStreamTrack.webidl',
     'AudioTrack.webidl',
     'AudioTrackList.webidl',
     'AudioWorklet.webidl',
     'AudioWorkletGlobalScope.webidl',
     'AudioWorkletNode.webidl',
     'AudioWorkletProcessor.webidl',
     'AutocompleteInfo.webidl',
     'BarProp.webidl',
@@ -913,17 +912,16 @@ WEBIDL_FILES = [
     'U2F.webidl',
     'UDPMessageEvent.webidl',
     'UDPSocket.webidl',
     'UIEvent.webidl',
     'URL.webidl',
     'URLSearchParams.webidl',
     'ValidityState.webidl',
     'VideoPlaybackQuality.webidl',
-    'VideoStreamTrack.webidl',
     'VideoTrack.webidl',
     'VideoTrackList.webidl',
     'VisualViewport.webidl',
     'VRDisplay.webidl',
     'VRDisplayEvent.webidl',
     'VRServiceTest.webidl',
     'VTTCue.webidl',
     'VTTRegion.webidl',
--- a/gfx/thebes/gfxPlatform.cpp
+++ b/gfx/thebes/gfxPlatform.cpp
@@ -836,16 +836,24 @@ gfxPlatform::Init()
      * below. GfxInfo initialization annotates our
      * crash reports so we want to do it before
      * we try to load any drivers and do device detection
      * incase that code crashes. See bug #591561. */
     nsCOMPtr<nsIGfxInfo> gfxInfo;
     /* this currently will only succeed on Windows */
     gfxInfo = services::GetGfxInfo();
 
+    if (XRE_IsParentProcess()) {
+      // Some gfxVars must be initialized prior gPlatform for coherent results.
+      gfxVars::SetDXInterop2Blocked(IsDXInterop2Blocked());
+      gfxVars::SetDXNV12Blocked(IsDXNV12Blocked());
+      gfxVars::SetDXP010Blocked(IsDXP010Blocked());
+      gfxVars::SetDXP016Blocked(IsDXP016Blocked());
+    }
+
 #if defined(XP_WIN)
     gPlatform = new gfxWindowsPlatform;
 #elif defined(XP_MACOSX)
     gPlatform = new gfxPlatformMac;
 #elif defined(MOZ_WIDGET_GTK)
     gPlatform = new gfxPlatformGtk;
 #elif defined(ANDROID)
     gPlatform = new gfxAndroidPlatform;
@@ -956,20 +964,16 @@ gfxPlatform::Init()
       SkGraphics::SetFontCacheLimit(skiaCacheSize);
     }
 #endif
 
     InitNullMetadata();
     InitOpenGLConfig();
 
     if (XRE_IsParentProcess()) {
-      gfxVars::SetDXInterop2Blocked(IsDXInterop2Blocked());
-      gfxVars::SetDXNV12Blocked(IsDXNV12Blocked());
-      gfxVars::SetDXP010Blocked(IsDXP010Blocked());
-      gfxVars::SetDXP016Blocked(IsDXP016Blocked());
       Preferences::Unlock(FONT_VARIATIONS_PREF);
       if (!gPlatform->HasVariationFontSupport()) {
         // Ensure variation fonts are disabled and the pref is locked.
         Preferences::SetBool(FONT_VARIATIONS_PREF, false,
                              PrefValueKind::Default);
         Preferences::SetBool(FONT_VARIATIONS_PREF, false);
         Preferences::Lock(FONT_VARIATIONS_PREF);
       }
--- a/layout/style/nsDOMCSSAttrDeclaration.cpp
+++ b/layout/style/nsDOMCSSAttrDeclaration.cpp
@@ -73,22 +73,19 @@ nsDOMCSSAttributeDeclaration::SetCSSDecl
 {
   NS_ASSERTION(mElement, "Must have Element to set the declaration!");
 
   // Whenever changing element.style values, aClosureData must be non-null.
   // SMIL doesn't update Element's attribute values, so closure data isn't
   // needed.
   MOZ_ASSERT_IF(!mIsSMILOverride, aClosureData);
 
-  // If the closure hasn't been called because the declaration wasn't changed,
-  // we need to explicitly call it now to get InlineStyleDeclarationWillChange
-  // notification before SetInlineStyleDeclaration.
-  if (aClosureData && aClosureData->mClosure) {
-    aClosureData->mClosure(aClosureData);
-  }
+  // The closure needs to have been called by now, otherwise we shouldn't be
+  // getting here when the attribute hasn't changed.
+  MOZ_ASSERT_IF(aClosureData, !aClosureData->mClosure);
 
   aDecl->SetDirty();
   return mIsSMILOverride
     ? mElement->SetSMILOverrideStyleDeclaration(aDecl, true)
     : mElement->SetInlineStyleDeclaration(*aDecl, *aClosureData);
 }
 
 nsIDocument*
--- a/layout/style/nsDOMCSSDeclaration.cpp
+++ b/layout/style/nsDOMCSSDeclaration.cpp
@@ -122,22 +122,16 @@ nsDOMCSSDeclaration::SetCssText(const ns
   mozAutoDocUpdate autoUpdate(DocToUpdate(), true);
   DeclarationBlockMutationClosure closure = {};
   MutationClosureData closureData;
   GetPropertyChangeClosure(&closure, &closureData);
 
   ParsingEnvironment servoEnv =
     GetParsingEnvironment(aSubjectPrincipal);
   if (!servoEnv.mUrlExtraData) {
-    if (created) {
-      // In case we can't set a new declaration, but one was
-      // created for the old one, we need to set the old declaration to
-      // get right style attribute handling.
-      SetCSSDeclaration(olddecl, &closureData);
-    }
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return;
   }
 
   // Need to special case closure calling here, since parsing css text
   // doesn't modify any existing declaration and that is why the callback isn't
   // called implicitly.
   if (closureData.mClosure) {
@@ -282,32 +276,22 @@ nsDOMCSSDeclaration::ModifyDeclaration(n
   // rule (see stack in bug 209575).
   mozAutoDocUpdate autoUpdate(DocToUpdate(), true);
   RefPtr<DeclarationBlock> decl = olddecl->EnsureMutable();
 
   bool changed;
   ParsingEnvironment servoEnv =
     GetParsingEnvironment(aSubjectPrincipal);
   if (!servoEnv.mUrlExtraData) {
-    if (created) {
-      // In case we can't set a new declaration, but one was
-      // created for the old one, we need to set the old declaration to
-      // get right style attribute handling.
-      SetCSSDeclaration(olddecl, aClosureData);
-    }
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   changed = aFunc(decl, servoEnv);
 
   if (!changed) {
-    if (created) {
-      // See comment above about setting old declaration.
-      SetCSSDeclaration(olddecl, aClosureData);
-    }
     // Parsing failed -- but we don't throw an exception for that.
     return NS_OK;
   }
 
   return SetCSSDeclaration(decl, aClosureData);
 }
 
 nsresult
--- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java
+++ b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationClient.java
@@ -151,17 +151,17 @@ public final class NotificationClient im
                 .setAutoCancel(true)
                 .setDefaults(Notification.DEFAULT_SOUND)
                 .setStyle(new NotificationCompat.BigTextStyle()
                         .bigText(alertText)
                         .setSummaryText(host));
 
         if (!AppConstants.Versions.preO) {
             builder.setChannelId(NotificationHelper.getInstance(mContext)
-                    .getNotificationChannel(NotificationHelper.Channel.DEFAULT).getId());
+                    .getNotificationChannel(NotificationHelper.Channel.SITE_NOTIFICATIONS).getId());
         }
 
         // Fetch icon.
         if (!imageUrl.isEmpty()) {
             final Bitmap image = BitmapUtils.decodeUrl(imageUrl);
             builder.setLargeIcon(image);
         }
 
--- a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java
@@ -111,16 +111,20 @@ public final class NotificationHelper im
         /**
          * Synced tabs notification channel
          */
         SYNCED_TABS,
         /**
          * Leanplum notification channel - use only when <code>AppConstants.MOZ_ANDROID_MMA</code> is true.
          */
         LP_DEFAULT,
+        /**
+         * HTML5 web site notifications
+         */
+        SITE_NOTIFICATIONS,
     }
 
     // Holds the mapping between the Channel enum used by the rest of our codebase and the
     // channel ID used for communication with the system NotificationManager.
     // How to determine the initialCapacity: Count all channels (including the Updater, which is
     // only added further down in initNotificationChannels), multiply by 4/3 for a maximum load
     // factor of 75 % and round up to the next multiple of two.
     private final Map<Channel, String> mDefinedNotificationChannels = new HashMap<Channel, String>(16) {{
@@ -138,16 +142,19 @@ public final class NotificationHelper im
 
         if (AppConstants.MOZ_ANDROID_MMA) {
             final String LP_DEFAULT_CHANNEL_TAG = "lp-default-notification-channel";
             put(Channel.LP_DEFAULT, LP_DEFAULT_CHANNEL_TAG);
         }
 
         final String SYNCED_TABS_CHANNEL_TAG = "synced-tabs-notification-channel";
         put(Channel.SYNCED_TABS, SYNCED_TABS_CHANNEL_TAG);
+
+        final String SITE_NOTIFICATIONS_CHANNEL_TAG = "site-notifications";
+        put(Channel.SITE_NOTIFICATIONS, SITE_NOTIFICATIONS_CHANNEL_TAG);
     }};
 
     // These are channels we no longer require and want to retire from Android's settings UI.
     private final List<String> mDeprecatedNotificationChannels = new ArrayList<>(Arrays.asList(
             "default-notification-channel",
             null
     ));
 
@@ -256,16 +263,23 @@ public final class NotificationHelper im
 
                 case LP_DEFAULT: {
                     channel = new NotificationChannel(mDefinedNotificationChannels.get(definedChannel),
                             mContext.getString(R.string.leanplum_default_notifications_channel),
                             NotificationManager.IMPORTANCE_LOW);
                 }
                 break;
 
+                case SITE_NOTIFICATIONS: {
+                    channel = new NotificationChannel(mDefinedNotificationChannels.get(definedChannel),
+                            mContext.getString(R.string.site_notifications_channel),
+                            NotificationManager.IMPORTANCE_DEFAULT);
+                }
+                break;
+
                 case DEFAULT:
                 default: {
                     channel = new NotificationChannel(mDefinedNotificationChannels.get(definedChannel),
                             mContext.getString(R.string.default_notification_channel),
                             NotificationManager.IMPORTANCE_LOW);
                 }
                 break;
             }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -896,8 +896,11 @@ Picture-in-picture mini window -->
 <!ENTITY default_notification_channel "&brandShortName;">
 <!ENTITY mls_notification_channel "&vendorShortName; Location Service">
 <!ENTITY download_notification_channel "Downloads">
 <!ENTITY media_notification_channel "Media playback">
 <!-- These push notifications come without a specific channel and/or name from Leanplum -->
 <!ENTITY leanplum_default_notifications_channel "&brandShortName; Push notifications">
 <!ENTITY updater_notification_channel "App updates">
 <!ENTITY synced_tabs_notification_channel "Synced tabs">
+<!-- LOCALIZATION NOTE (site_notifications_channel): This is for system notifications displayed by
+web sites through the HTML Notifications API. -->
+<!ENTITY site_notifications_channel "Site notifications">
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -648,9 +648,10 @@
 
   <string name="default_notification_channel">&default_notification_channel;</string>
   <string name="mls_notification_channel">&mls_notification_channel;</string>
   <string name="media_notification_channel">&media_notification_channel;</string>
   <string name="download_notification_channel">&download_notification_channel;</string>
   <string name="leanplum_default_notifications_channel">&leanplum_default_notifications_channel;</string>
   <string name="updater_notification_channel">&updater_notification_channel;</string>
   <string name="synced_tabs_notification_channel">&synced_tabs_notification_channel;</string>
+  <string name="site_notifications_channel">&site_notifications_channel;</string>
 </resources>
--- a/testing/mozbase/mozleak/mozleak/lsan.py
+++ b/testing/mozbase/mozleak/mozleak/lsan.py
@@ -8,25 +8,25 @@ import re
 
 class LSANLeaks(object):
 
     """
     Parses the log when running an LSAN build, looking for interesting stack frames
     in allocation stacks
     """
 
-    def __init__(self, logger, scope=None, allowed=None):
+    def __init__(self, logger, scope=None, allowed=None, maxNumRecordedFrames=None):
         self.logger = logger
         self.inReport = False
         self.fatalError = False
         self.symbolizerError = False
         self.foundFrames = set()
         self.recordMoreFrames = None
         self.currStack = None
-        self.maxNumRecordedFrames = 4
+        self.maxNumRecordedFrames = maxNumRecordedFrames if maxNumRecordedFrames else 4
         self.summaryData = None
         self.scope = scope
         self.allowedMatch = None
         self.sawError = False
 
         # Don't various allocation-related stack frames, as they do not help much to
         # distinguish different leaks.
         unescapedSkipList = [
@@ -131,16 +131,17 @@ class LSANLeaks(object):
                               "This will cause leaks that "
                               "should be ignored to instead be reported as an error")
             failures += 1
 
         if self.foundFrames:
             self.logger.info("LeakSanitizer | To show the "
                              "addresses of leaked objects add report_objects=1 to LSAN_OPTIONS\n"
                              "This can be done in testing/mozbase/mozrunner/mozrunner/utils.py")
+            self.logger.info("Allowed depth was %d" % self.maxNumRecordedFrames)
 
             for frames, allowed in self.foundFrames:
                 self.logger.lsan_leak(frames, scope=self.scope, allowed_match=allowed)
                 if not allowed:
                     failures += 1
 
         if self.sawError and not (self.summaryData or
                                   self.foundFrames or
--- a/testing/profiles/common/user.js
+++ b/testing/profiles/common/user.js
@@ -29,16 +29,18 @@ user_pref("dom.send_after_paint_to_conte
 // Only load extensions from the application and user profile
 // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
 user_pref("extensions.enabledScopes", 5);
 user_pref("extensions.legacy.enabled", true);
 // Turn off extension updates so they don't bother tests
 user_pref("extensions.update.enabled", false);
 // Disable useragent updates.
 user_pref("general.useragent.updates.enabled", false);
+// Ensure WR doesn't get enabled in tests unless we do it explicitly with the MOZ_WEBRENDER envvar.
+user_pref("gfx.webrender.all.qualified", false);
 user_pref("hangmonitor.timeout", 0); // no hang monitor
 user_pref("media.gmp-manager.updateEnabled", false);
 // Make enablePrivilege continue to work for test code. :-(
 user_pref("security.turn_off_all_security_so_that_viruses_can_take_over_this_computer", true);
 user_pref("xpinstall.signatures.required", false);
 // Prevent Remote Settings to issue non local connections.
 user_pref("services.settings.server", "http://localhost/remote-settings-dummy/v1");
 // Ensure autoplay is enabled for all platforms.
--- a/testing/talos/talos/xtalos/xperf_whitelist.json
+++ b/testing/talos/talos/xtalos/xperf_whitelist.json
@@ -69,16 +69,22 @@
     "mincount": 2,
     "maxcount": 2,
     "minbytes": 120,
     "maxbytes": 120
   },
   "c:\\windows\\prefetch\\{prefetch}.pf": {
     "ignore": true
   },
+  "c:\\windows\\system32\\apphelp.dll": {
+    "mincount": 0,
+    "maxcount": 2,
+    "minbytes": 0,
+    "maxbytes": 32768
+  },
   "c:\\windows\\system32\\windows.storage.dll": {
     "mincount": 2,
     "maxcount": 2,
     "minbytes": 0,
     "maxbytes": 24576
   },
   "c:\\windows\\system32\\ole32.dll": {
     "mincount": 4,
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -327245,16 +327245,22 @@
     ]
    ],
    "audio-output/idlharness.window.js": [
     [
      "/audio-output/idlharness.window.html",
      {}
     ]
    ],
+   "audio-output/setSinkId.html": [
+    [
+     "/audio-output/setSinkId.html",
+     {}
+    ]
+   ],
    "audio-output/setSinkId.https.html": [
     [
      "/audio-output/setSinkId.https.html",
      {}
     ]
    ],
    "background-fetch/content-security-policy.https.window.js": [
     [
@@ -442340,16 +442346,20 @@
   "audio-output/idlharness.window.js": [
    "f10e523bcdc530ee1dbd04a52541ac0b343e9376",
    "testharness"
   ],
   "audio-output/setSinkId-manual.https.html": [
    "a083cdf09232110039d3bb825e207c678b336114",
    "manual"
   ],
+  "audio-output/setSinkId.html": [
+   "bd5d8e43b0fd9d0c9f1e078ed97a1bbd18b7b0be",
+   "testharness"
+  ],
   "audio-output/setSinkId.https.html": [
    "2ce0b482b7eea61c0c56c49ec14dc1630b7b9a9c",
    "testharness"
   ],
   "background-fetch/META.yml": [
    "8ce9f8faa2acdfe7a2ef8dfc6c1ad8cbdf01c72d",
    "support"
   ],
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/audio-output/setSinkId.html.ini
@@ -0,0 +1,2 @@
+[setSinkId.html]
+  prefs: [media.setsinkid.enabled:true]
--- a/testing/web-platform/meta/audio-output/setSinkId.https.html.ini
+++ b/testing/web-platform/meta/audio-output/setSinkId.https.html.ini
@@ -1,10 +1,2 @@
 [setSinkId.https.html]
-  [setSinkId on default audio output should always work]
-    expected: FAIL
-
-  [setSinkId fails with NotFoundError on made up deviceid]
-    expected: FAIL
-
-  [List media devices]
-    expected: FAIL
-
+  prefs: [media.setsinkid.enabled:true]
deleted file mode 100644
--- a/testing/web-platform/meta/css/cssom/cssstyledeclaration-mutationrecord-002.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[cssstyledeclaration-mutationrecord-002.html]
-  [CSSStyleDeclaration.setPropertyValue doesn't queue a mutation record when setting invalid values]
-    expected: FAIL
-
--- a/testing/web-platform/meta/mediacapture-streams/idlharness.https.window.js.ini
+++ b/testing/web-platform/meta/mediacapture-streams/idlharness.https.window.js.ini
@@ -1,25 +1,19 @@
 [idlharness.https.window.html]
   [MediaStreamTrack interface: operation getCapabilities()]
     expected: FAIL
 
   [MediaStreamTrack interface: attribute onoverconstrained]
     expected: FAIL
 
-  [MediaStreamTrack must be primary interface of [object AudioStreamTrack\]]
+  [MediaStreamTrack interface: [object MediaStreamTrack\] must inherit property "getCapabilities()" with the proper type]
     expected: FAIL
 
-  [Stringification of [object AudioStreamTrack\]]
-    expected: FAIL
-
-  [MediaStreamTrack interface: [object AudioStreamTrack\] must inherit property "getCapabilities()" with the proper type]
-    expected: FAIL
-
-  [MediaStreamTrack interface: [object AudioStreamTrack\] must inherit property "onoverconstrained" with the proper type]
+  [MediaStreamTrack interface: [object MediaStreamTrack\] must inherit property "onoverconstrained" with the proper type]
     expected: FAIL
 
   [OverconstrainedErrorEvent interface: existence and properties of interface object]
     expected: FAIL
 
   [OverconstrainedErrorEvent interface object length]
     expected: FAIL
 
--- a/testing/web-platform/meta/service-workers/service-worker/__dir__.ini
+++ b/testing/web-platform/meta/service-workers/service-worker/__dir__.ini
@@ -1,2 +1,2 @@
 prefs: [dom.serviceWorkers.enabled:true]
-lsan-allowed: [Alloc, Create, CreateInner, MakeUnique, Malloc, NewChannelFromURIWithProxyFlagsInternal, NewEmptyScopeData, NewPage, OrInsert, PLDHashTable::Add, Realloc, SharedMutex, __rdl_alloc, __rdl_realloc, js_new, js_pod_calloc, js_pod_malloc, js_pod_realloc, mozilla::BasePrincipal::CreateCodebasePrincipal, mozilla::ThrottledEventQueue::Create, mozilla::WeakPtr, mozilla::dom::ChromeUtils::GenerateQI, mozilla::dom::Performance::CreateForMainThread, mozilla::dom::PerformanceStorageWorker::Create, mozilla::dom::WorkerPrivate::WorkerPrivate, mozilla::net::HttpBaseChannel::HttpBaseChannel, mozilla::net::HttpChannelChild::HttpChannelChild, mozilla::net::nsHttpAuthIdentity::Set, mozilla::net::nsHttpHandler::NewProxiedChannel2, nsNodeSupportsWeakRefTearoff::GetWeakReference, nsPrefetchService::Preload, nsSegmentedBuffer::AppendNewSegment]
+lsan-allowed: [Alloc, Create, CreateInner, MakeUnique, Malloc, NewChannelFromURIWithProxyFlagsInternal, NewEmptyScopeData, NewPage, OrInsert, PLDHashTable::Add, Realloc, SharedMutex, __rdl_alloc, __rdl_realloc, js_new, js_pod_calloc, js_pod_malloc, js_pod_realloc, mozilla::BasePrincipal::CreateCodebasePrincipal, mozilla::ThrottledEventQueue::Create, mozilla::WeakPtr, mozilla::dom::ChromeUtils::GenerateQI, mozilla::dom::Performance::CreateForMainThread, mozilla::dom::PerformanceStorageWorker::Create, mozilla::dom::WorkerPrivate::WorkerPrivate, mozilla::net::HttpBaseChannel::HttpBaseChannel, mozilla::net::HttpChannelChild::HttpChannelChild, mozilla::net::nsHttpAuthIdentity::Set, mozilla::net::nsHttpHandler::NewProxiedChannel2, nsNodeSupportsWeakRefTearoff::GetWeakReference, nsPrefetchService::Preload, nsSegmentedBuffer::AppendNewSegment, nsDocShell::Create]
--- a/testing/web-platform/meta/websockets/__dir__.ini
+++ b/testing/web-platform/meta/websockets/__dir__.ini
@@ -1,1 +1,2 @@
-lsan-allowed: [Alloc, Create, Malloc, NewPage, PLDHashTable::Add, PLDHashTable::ChangeTable, Realloc, RecvOnAcknowledge, RecvOnStop, mozilla::BasePrincipal::CreateCodebasePrincipal, mozilla::SchedulerGroup::CreateEventTargetFor, mozilla::ThrottledEventQueue::Create, mozilla::WeakPtr, mozilla::dom::WebSocket::WebSocket, mozilla::dom::WorkerCSPEventListener::Create, mozilla::dom::nsIContentChild::GetConstructedEventTarget, mozilla::net::WebSocketChannelChild::RecvOnServerClose, nsAtomTable::Atomize, mozilla::net::nsStandardURL::TemplatedMutator]
+lsan-allowed: [Alloc, Create, Malloc, NewPage, PLDHashTable::Add, PLDHashTable::ChangeTable, Realloc, RecvOnAcknowledge, RecvOnStop, mozilla::BasePrincipal::CreateCodebasePrincipal, mozilla::SchedulerGroup::CreateEventTargetFor, mozilla::ThrottledEventQueue::Create, mozilla::WeakPtr, mozilla::dom::WebSocket::WebSocket, mozilla::dom::WorkerCSPEventListener::Create, mozilla::dom::nsIContentChild::GetConstructedEventTarget, mozilla::net::WebSocketChannelChild::RecvOnServerClose, nsAtomTable::Atomize, mozilla::net::nsStandardURL::TemplatedMutator, nsDocShell::Create]
+lsan-max-stack-depth: 7
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/audio-output/setSinkId.html
@@ -0,0 +1,44 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test setSinkId behavior </title>
+<div id='log'></div>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+"use strict";
+
+const audio = new Audio();
+
+promise_test(t => audio.setSinkId(""), "setSinkId on default audio output should always work");
+
+promise_test(t => promise_rejects(t, "NotFoundError", audio.setSinkId("nonexistent_device_id")),
+  "setSinkId fails with NotFoundError on made up deviceid");
+
+promise_test(async t => {
+  const list = await navigator.mediaDevices.enumerateDevices();
+  const outputDevicesList = list.filter(({kind}) => kind == "audiooutput");
+  assert_not_equals(outputDevicesList.length, 0,
+    "media device list includes at least one audio output device");
+
+  let acceptedDevices = 0;
+  for (const {deviceId} of outputDevicesList) {
+    const {deviceId} = outputDevicesList[0];
+    const p1 = audio.setSinkId(deviceId);
+    assert_equals(audio.sinkId, "", "before it resolves, setSinkId is unchanged");
+    try {
+      let r = await p1;
+      assert_equals(acceptedDevices, 0, "only the default sink device can be set");
+      acceptedDevices++;
+      assert_equals(r, undefined, "setSinkId resolves with undefined");
+      assert_equals(audio.sinkId, deviceId, "when it resolves, setSinkId updates sinkId to the requested deviceId");
+      r = await audio.setSinkId(deviceId);
+      assert_equals(r, undefined, "resetting sinkid on same current value should always work");
+      r = await audio.setSinkId("");
+      assert_equals(r, undefined, "resetting sinkid on default audio output should always work");
+    } catch (e) {
+      assert_equals(e.name, "NotAllowedError", "Non-default devices are failing with NotAllowed error");
+    }
+  }
+}, "List device, setSinkId should on the default, the rest of the devices will get a NotAlowedError");
+
+</script>
--- a/testing/web-platform/tests/audio-output/setSinkId.https.html
+++ b/testing/web-platform/tests/audio-output/setSinkId.https.html
@@ -1,45 +1,46 @@
 <!doctype html>
-<html>
 <head>
 <title>Test setSinkId behavior </title>
 <link rel="author" title="Dominique Hazael-Massieux" href="mailto:dom@w3.org"/>
 <link rel="help" href="https://www.w3.org/TR/audio-output/#dom-htmlmediaelement-setsinkid">
 </head>
-<body>
-<h1 class="instructions">Description</h1>
-<p class="instructions">This test checks that <code>setSinkId</code> follows the algorithm (but does not consider actual rendering of the audio which needs to be manual).</p>
-<div id='log'></div>
 <script src=/resources/testharness.js></script>
 <script src=/resources/testharnessreport.js></script>
 <script>
 "use strict";
 
-const is_output = d => d.kind === "audiooutput";
 const audio = new Audio();
 
 promise_test(t => audio.setSinkId(""), "setSinkId on default audio output should always work");
 
-promise_test(t => promise_rejects(t, "NotFoundError", audio.setSinkId("inexistent_device_id")), "setSinkId fails with NotFoundError on made up deviceid");
+promise_test(t => promise_rejects(t, "NotFoundError", audio.setSinkId("nonexistent_device_id")),
+  "setSinkId fails with NotFoundError on made up deviceid");
+
+promise_test(async t => {
+  const list = await navigator.mediaDevices.enumerateDevices();
+  const outputDevicesList = list.filter(({kind}) => kind == "audiooutput");
+  assert_not_equals(outputDevicesList.length, 0,
+    "media device list includes at least one audio output device");
 
-promise_test(t =>
-             navigator.mediaDevices.enumerateDevices().then(list => {
-                 assert_not_equals(list.find(is_output), undefined, "media device list includes at least one audio output device");
-                 // since we haven't gained any specific permission,
-                 // for all listed audio output devices, calling setSinkId with device id can
-                 // either create a security exception or work and thus reflect the deviceId
-                 let acceptedDevice = 0;
-                 list.filter(is_output).forEach((d,i) => promise_test(td => audio.setSinkId(d.deviceId).then(r => {
-                     assert_equals(r, undefined, "setSinkId resolves with undefined");
-                     assert_equals(audio.sinkId, d.deviceId, "when it resolves, setSinkId updates sinkId to the requested deviceId");
-                     assert_equals(acceptedDevice, 0, "only one output device can be set without permission");
-                     acceptedDevice++;
-                     promise_test(t => audio.setSinkId(d.deviceId), "resetting sinkid on same current value should always work");
-                     promise_test(t => audio.setSinkId(""), "resetting sinkid on default audio output should always work");
-                 }, e => {
-                     assert_equals(e.name, "SecurityError", "On known devices, the only possible failure of setSinkId is a securityerror"); // assuming AbortError can't happen in the test environment by default
-                 }), "Correctly reacts to setting known deviceid as sinkid "  + i));
-             }), "List media devices");
+  let acceptedDevices = 0;
+  for (const {deviceId} of outputDevicesList) {
+    const {deviceId} = outputDevicesList[0];
+    const p1 = audio.setSinkId(deviceId);
+    assert_equals(audio.sinkId, "", "before it resolves, setSinkId is unchanged");
+    try {
+      let r = await p1;
+      assert_equals(acceptedDevices, 0, "only the default sink device can be set");
+      acceptedDevices++;
+      assert_equals(r, undefined, "setSinkId resolves with undefined");
+      assert_equals(audio.sinkId, deviceId, "when it resolves, setSinkId updates sinkId to the requested deviceId");
+      r = await audio.setSinkId(deviceId);
+      assert_equals(r, undefined, "resetting sinkid on same current value should always work");
+      r = await audio.setSinkId("");
+      assert_equals(r, undefined, "resetting sinkid on default audio output should always work");
+    } catch (e) {
+      assert_equals(e.name, "NotAllowedError", "Non-default devices are failing with NotAllowed error");
+    }
+  }
+}, "List device, setSinkId should on the default, the rest of the devices will get a NotAlowedError");
 
 </script>
-</body>
-</html>
--- a/testing/web-platform/tests/css/cssom/cssstyledeclaration-mutationrecord-002.html
+++ b/testing/web-platform/tests/css/cssom/cssstyledeclaration-mutationrecord-002.html
@@ -1,12 +1,12 @@
 <!doctype html>
 <meta charset="utf-8">
 <title>CSSOM: CSSStyleDeclaration.setPropertyValue doesn't queue a mutation record for invalid values</title>
-<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setpropertyvalue">
+<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty">
 <link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
 <script src=/resources/testharness.js></script>
 <script src=/resources/testharnessreport.js></script>
 <script>
   let test = async_test("CSSStyleDeclaration.setPropertyValue doesn't queue a mutation record when setting invalid values");
   let m = new MutationObserver(test.unreached_func("shouldn't queue a mutation record"));
   m.observe(document.documentElement,  { attributes: true });
 
copy from testing/web-platform/tests/css/cssom/cssstyledeclaration-mutationrecord-002.html
copy to testing/web-platform/tests/css/cssom/cssstyledeclaration-mutationrecord-005.html
--- a/testing/web-platform/tests/css/cssom/cssstyledeclaration-mutationrecord-002.html
+++ b/testing/web-platform/tests/css/cssom/cssstyledeclaration-mutationrecord-005.html
@@ -1,12 +1,13 @@
 <!doctype html>
+<html style="color: inherit">
 <meta charset="utf-8">
 <title>CSSOM: CSSStyleDeclaration.setPropertyValue doesn't queue a mutation record for invalid values</title>
-<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setpropertyvalue">
+<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty">
 <link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
 <script src=/resources/testharness.js></script>
 <script src=/resources/testharnessreport.js></script>
 <script>
   let test = async_test("CSSStyleDeclaration.setPropertyValue doesn't queue a mutation record when setting invalid values");
   let m = new MutationObserver(test.unreached_func("shouldn't queue a mutation record"));
   m.observe(document.documentElement,  { attributes: true });
 
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css/cssom/cssstyledeclaration-setter-attr.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<title>CSSOM test: declaration block after setting via CSSOM</title>
+<link rel="help" href="https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<script>
+test(function() {
+  let element = document.createElement("div");
+  element.style.setProperty("doesntexist", "0");
+  assert_false(element.hasAttribute("style"));
+}, "Setting an invalid property via the declaration setter doesn't create a declaration");
+test(function() {
+  let element = document.createElement("div");
+  element.style.setProperty("width", "-100");
+  assert_false(element.hasAttribute("style"));
+}, "Setting an invalid value via the declaration setter doesn't create a declaration");
+</script>
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py
@@ -184,41 +184,44 @@ class FirefoxBrowser(Browser):
         else:
             self.stack_fixer = None
 
         if timeout_multiplier:
             self.init_timeout = self.init_timeout * timeout_multiplier
 
         self.asan = asan
         self.lsan_allowed = None
+        self.lsan_max_stack_depth = None
         self.leak_check = leak_check
         self.leak_report_file = None
         self.lsan_handler = None
         self.stylo_threads = stylo_threads
         self.chaos_mode_flags = chaos_mode_flags
         self.headless = headless
 
     def settings(self, test):
         self.lsan_allowed = test.lsan_allowed
+        self.lsan_max_stack_depth = test.lsan_max_stack_depth
         return {"check_leaks": self.leak_check and not test.leaks,
                 "lsan_allowed": test.lsan_allowed}
 
     def start(self, group_metadata=None, **kwargs):
         if group_metadata is None:
             group_metadata = {}
 
         if self.marionette_port is None:
             self.marionette_port = get_free_port(2828, exclude=self.used_ports)
             self.used_ports.add(self.marionette_port)
 
         if self.asan:
             print "Setting up LSAN"
             self.lsan_handler = mozleak.LSANLeaks(self.logger,
                                                   scope=group_metadata.get("scope", "/"),
-                                                  allowed=self.lsan_allowed)
+                                                  allowed=self.lsan_allowed,
+                                                  maxNumRecordedFrames=self.lsan_max_stack_depth)
 
         env = test_environment(xrePath=os.path.dirname(self.binary),
                                debugger=self.debug_info is not None,
                                log=self.logger,
                                lsanPath=self.prefs_root)
 
         env["STYLO_THREADS"] = str(self.stylo_threads)
         if self.chaos_mode_flags is not None:
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/manifestexpected.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/manifestexpected.py
@@ -151,16 +151,20 @@ class ExpectedManifest(ManifestItem):
     @property
     def prefs(self):
         return prefs(self)
 
     @property
     def lsan_allowed(self):
         return lsan_allowed(self)
 
+    @property
+    def lsan_max_stack_depth(self):
+        return int_prop("lsan-max-stack-depth", self)
+
 
 class DirectoryManifest(ManifestItem):
     @property
     def disabled(self):
         return bool_prop("disabled", self)
 
     @property
     def restart_after(self):
@@ -185,16 +189,19 @@ class DirectoryManifest(ManifestItem):
     @property
     def prefs(self):
         return prefs(self)
 
     @property
     def lsan_allowed(self):
         return lsan_allowed(self)
 
+    @property
+    def lsan_max_stack_depth(self):
+        return int_prop("lsan-max-stack-depth", self)
 
 class TestNode(ManifestItem):
     def __init__(self, name):
         """Tree node associated with a particular test in a manifest
 
         :param name: name of the test"""
         assert name is not None
         ManifestItem.__init__(self, name)
@@ -246,16 +253,20 @@ class TestNode(ManifestItem):
     @property
     def prefs(self):
         return prefs(self)
 
     @property
     def lsan_allowed(self):
         return lsan_allowed(self)
 
+    @property
+    def lsan_max_stack_depth(self):
+        return int_prop("lsan-max-stack-depth", self)
+
     def append(self, node):
         """Add a subtest to the current test
 
         :param node: AST Node associated with the subtest"""
         child = ManifestItem.append(self, node)
         self.subtests[child.name] = child
 
     def get_subtest(self, name):
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/tests/test_wpttest.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/tests/test_wpttest.py
@@ -13,31 +13,40 @@ prefs: [a:b]
 
 dir_ini_1 = """\
 prefs: [@Reset, b:c]
 max-asserts: 2
 min-asserts: 1
 tags: [b, c]
 """
 
+dir_ini_2 = """\
+lsan-max-stack-depth: 42
+"""
+
 test_0 = """\
 [0.html]
   prefs: [c:d]
   max-asserts: 3
   tags: [a, @Reset]
 """
 
 test_1 = """\
 [1.html]
   prefs:
     if os == 'win': [a:b, c:d]
   expected:
     if os == 'win': FAIL
 """
 
+test_2 = """\
+[2.html]
+  lsan-max-stack-depth: 42
+"""
+
 
 def test_metadata_inherit():
     tests = make_mock_manifest(("test", "a", 10), ("test", "a/b", 10),
                                ("test", "c", 10))
 
     inherit_metadata = [
         manifestexpected.static.compile(
             BytesIO(item),
@@ -67,8 +76,45 @@ def test_conditional():
                                                     data_cls_getter=manifestexpected.data_cls_getter,
                                                     test_path="a",
                                                     url_base="")
 
     test = tests[1][2].pop()
     test_obj = wpttest.from_manifest(test, [], test_metadata.get_test(test.id))
     assert test_obj.prefs == {"a": "b", "c": "d"}
     assert test_obj.expected() == "FAIL"
+
+def test_metadata_lsan_stack_depth():
+    tests = make_mock_manifest(("test", "a", 10), ("test", "a/b", 10))
+
+    test_metadata = manifestexpected.static.compile(BytesIO(test_2),
+                                                    {},
+                                                    data_cls_getter=manifestexpected.data_cls_getter,
+                                                    test_path="a",
+                                                    url_base="")
+
+    test = tests[2][2].pop()
+    test_obj = wpttest.from_manifest(test, [], test_metadata.get_test(test.id))
+
+    assert test_obj.lsan_max_stack_depth == 42
+
+    test = tests[1][2].pop()
+    test_obj = wpttest.from_manifest(test, [], test_metadata.get_test(test.id))
+
+    assert test_obj.lsan_max_stack_depth == None
+
+    test_metadata = manifestexpected.static.compile(BytesIO(test_0),
+                                                    {},
+                                                    data_cls_getter=manifestexpected.data_cls_getter,
+                                                    test_path="a",
+                                                    url_base="")
+
+    inherit_metadata = [
+        manifestexpected.static.compile(
+            BytesIO(dir_ini_2),
+            {},
+            data_cls_getter=lambda x,y: manifestexpected.DirectoryManifest)
+    ]
+
+    test = tests[0][2].pop()
+    test_obj = wpttest.from_manifest(test, inherit_metadata, test_metadata.get_test(test.id))
+
+    assert test_obj.lsan_max_stack_depth == 42
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/wpttest.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/wpttest.py
@@ -230,16 +230,24 @@ class Test(object):
         for meta in self.itermeta():
             lsan_allowed |= meta.lsan_allowed
             if atom_reset in lsan_allowed:
                 lsan_allowed.remove(atom_reset)
                 break
         return lsan_allowed
 
     @property
+    def lsan_max_stack_depth(self):
+        for meta in self.itermeta(None):
+            depth = meta.lsan_max_stack_depth
+            if depth is not None:
+                return depth
+        return None
+
+    @property
     def tags(self):
         tags = set()
         for meta in self.itermeta():
             meta_tags = meta.tags
             tags |= meta_tags
             if atom_reset in meta_tags:
                 tags.remove(atom_reset)
                 break
--- a/toolkit/components/places/PlacesSyncUtils.jsm
+++ b/toolkit/components/places/PlacesSyncUtils.jsm
@@ -1863,17 +1863,17 @@ async function placesBookmarkToSyncBookm
   }
 
   return item;
 }
 
 // Converts a Sync bookmark object to a Places bookmark or livemark object.
 // This function maps record IDs to Places GUIDs, and filters out extra Sync
 // properties like keywords, tags. Returns an object that can be passed to
-// `PlacesUtils.livemarks.addLivemark` or `PlacesUtils.bookmarks.{insert, update}`.
+// `PlacesUtils.bookmarks.{insert, update}`.
 function syncBookmarkToPlacesBookmark(info) {
   let bookmarkInfo = {
     source: SOURCE_SYNC,
   };
 
   for (let prop in info) {
     switch (prop) {
       case "kind":
deleted file mode 100644
--- a/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js
+++ /dev/null
@@ -1,516 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// Tests functionality of the mozIAsyncLivemarks interface.
-
-const FEED_URI = NetUtil.newURI("http://feed.rss/");
-const SITE_URI = NetUtil.newURI("http://site.org/");
-
-let unfiledFolderId;
-
-// This test must be the first one, since it's testing the cache.
-add_task(async function test_livemark_cache() {
-  unfiledFolderId =
-    await PlacesUtils.promiseItemId(PlacesUtils.bookmarks.unfiledGuid);
-
-  // Add a livemark through other APIs.
-  let folder = await PlacesUtils.bookmarks.insert({
-    type: PlacesUtils.bookmarks.TYPE_FOLDER,
-    title: "test",
-    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-  });
-  let id = await PlacesUtils.promiseItemId(folder.guid);
-  PlacesUtils.annotations
-             .setItemAnnotation(id, PlacesUtils.LMANNO_FEEDURI,
-                                "http://example.com/feed",
-                                0, PlacesUtils.annotations.EXPIRE_NEVER);
-  PlacesUtils.annotations
-             .setItemAnnotation(id, PlacesUtils.LMANNO_SITEURI,
-                                "http://example.com/site",
-                                0, PlacesUtils.annotations.EXPIRE_NEVER);
-
-  let livemark = await PlacesUtils.livemarks.getLivemark({ guid: folder.guid });
-  Assert.equal(folder.guid, livemark.guid);
-  Assert.equal(folder.dateAdded * 1000, livemark.dateAdded);
-  Assert.equal(folder.parentGuid, livemark.parentGuid);
-  Assert.equal(folder.index, livemark.index);
-  Assert.equal(folder.title, livemark.title);
-  Assert.equal(id, livemark.id);
-  Assert.equal(unfiledFolderId, livemark.parentId);
-  Assert.equal("http://example.com/feed", livemark.feedURI.spec);
-  Assert.equal("http://example.com/site", livemark.siteURI.spec);
-
-  await PlacesUtils.livemarks.removeLivemark(livemark);
-});
-
-add_task(async function test_addLivemark_noArguments_throws() {
-  try {
-    await PlacesUtils.livemarks.addLivemark();
-    do_throw("Invoking addLivemark with no arguments should throw");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_XPC_NOT_ENOUGH_ARGS);
-  }
-});
-
-add_task(async function test_addLivemark_emptyObject_throws() {
-  try {
-    await PlacesUtils.livemarks.addLivemark({});
-    do_throw("Invoking addLivemark with empty object should throw");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
-  }
-});
-
-add_task(async function test_addLivemark_badParentId_throws() {
-  try {
-    await PlacesUtils.livemarks.addLivemark({ parentId: "test" });
-    do_throw("Invoking addLivemark with a bad parent id should throw");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
-  }
-});
-
-add_task(async function test_addLivemark_invalidParentId_throws() {
-  try {
-    await PlacesUtils.livemarks.addLivemark({ parentId: -2 });
-    do_throw("Invoking addLivemark with an invalid parent id should throw");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
-  }
-});
-
-add_task(async function test_addLivemark_noIndex_throws() {
-  try {
-    await PlacesUtils.livemarks.addLivemark({
-      parentId: unfiledFolderId });
-    do_throw("Invoking addLivemark with no index should throw");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
-  }
-});
-
-add_task(async function test_addLivemark_badIndex_throws() {
-  try {
-    await PlacesUtils.livemarks.addLivemark(
-      { parentId: unfiledFolderId,
-        index: "test" });
-    do_throw("Invoking addLivemark with a bad index should throw");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
-  }
-});
-
-add_task(async function test_addLivemark_invalidIndex_throws() {
-  try {
-    await PlacesUtils.livemarks.addLivemark(
-      { parentId: unfiledFolderId,
-        index: -2,
-      });
-    do_throw("Invoking addLivemark with an invalid index should throw");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
-  }
-});
-
-add_task(async function test_addLivemark_noFeedURI_throws() {
-  try {
-    await PlacesUtils.livemarks.addLivemark(
-      { parentGuid: PlacesUtils.bookmarks.unfiledGuid });
-    do_throw("Invoking addLivemark with no feedURI should throw");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
-  }
-});
-
-add_task(async function test_addLivemark_badFeedURI_throws() {
-  try {
-    await PlacesUtils.livemarks.addLivemark(
-      { parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-        feedURI: "test" });
-    do_throw("Invoking addLivemark with a bad feedURI should throw");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
-  }
-});
-
-add_task(async function test_addLivemark_badSiteURI_throws() {
-  try {
-    await PlacesUtils.livemarks.addLivemark(
-      { parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-        feedURI: FEED_URI,
-        siteURI: "test" });
-    do_throw("Invoking addLivemark with a bad siteURI should throw");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
-  }
-});
-
-add_task(async function test_addLivemark_badGuid_throws() {
-  try {
-    await PlacesUtils.livemarks.addLivemark(
-      { parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-        feedURI: FEED_URI,
-        guid: "123456" });
-    do_throw("Invoking addLivemark with a bad guid should throw");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
-  }
-});
-
-add_task(async function test_addLivemark_parentId_succeeds() {
-  let onItemAddedCalled = false;
-  let listener = events => {
-    Assert.equal(events.length, 1);
-    let event = events[0];
-    onItemAddedCalled = true;
-    PlacesUtils.observers.removeListener(["bookmark-added"], listener);
-    Assert.equal(event.parentId, unfiledFolderId);
-    Assert.equal(event.index, 0);
-    Assert.equal(event.itemType, PlacesUtils.bookmarks.TYPE_FOLDER);
-    Assert.equal(event.title, "test");
-  };
-  PlacesUtils.observers.addListener(["bookmark-added"], listener);
-
-  await PlacesUtils.livemarks.addLivemark(
-    { title: "test",
-      parentId: unfiledFolderId,
-      feedURI: FEED_URI });
-  Assert.ok(onItemAddedCalled);
-});
-
-
-add_task(async function test_addLivemark_noSiteURI_succeeds() {
-  let livemark = await PlacesUtils.livemarks.addLivemark(
-    { title: "test",
-      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-      feedURI: FEED_URI,
-    });
-  Assert.ok(livemark.id > 0);
-  do_check_valid_places_guid(livemark.guid);
-  Assert.equal(livemark.title, "test");
-  Assert.equal(livemark.parentId, unfiledFolderId);
-  Assert.equal(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
-  Assert.ok(livemark.feedURI.equals(FEED_URI));
-  Assert.equal(livemark.siteURI, null);
-  Assert.ok(livemark.lastModified > 0);
-  Assert.equal(livemark.dateAdded, livemark.lastModified);
-
-  let bookmark = await PlacesUtils.bookmarks.fetch(livemark.guid);
-  Assert.equal(livemark.index, bookmark.index);
-  Assert.equal(livemark.dateAdded, bookmark.dateAdded * 1000);
-});
-
-add_task(async function test_addLivemark_succeeds() {
-  let livemark = await PlacesUtils.livemarks.addLivemark(
-    { title: "test",
-      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-      feedURI: FEED_URI,
-      siteURI: SITE_URI,
-    });
-
-  Assert.ok(livemark.id > 0);
-  do_check_valid_places_guid(livemark.guid);
-  Assert.equal(livemark.title, "test");
-  Assert.equal(livemark.parentId, unfiledFolderId);
-  Assert.equal(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
-  Assert.ok(livemark.feedURI.equals(FEED_URI));
-  Assert.ok(livemark.siteURI.equals(SITE_URI));
-  Assert.ok(PlacesUtils.annotations
-                       .itemHasAnnotation(livemark.id,
-                                          PlacesUtils.LMANNO_FEEDURI));
-  Assert.ok(PlacesUtils.annotations
-                       .itemHasAnnotation(livemark.id,
-                                          PlacesUtils.LMANNO_SITEURI));
-});
-
-add_task(async function test_addLivemark_bogusid_succeeds() {
-  let livemark = await PlacesUtils.livemarks.addLivemark(
-    { id: 100, // Should be ignored.
-      title: "test",
-      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-      feedURI: FEED_URI,
-      siteURI: SITE_URI,
-    });
-  Assert.ok(livemark.id > 0);
-  Assert.notEqual(livemark.id, 100);
-});
-
-add_task(async function test_addLivemark_bogusParentId_fails() {
-  try {
-    await PlacesUtils.livemarks.addLivemark(
-      { title: "test",
-        parentId: 187,
-        feedURI: FEED_URI,
-      });
-    do_throw("Adding a livemark with a bogus parent should fail");
-  } catch (ex) {}
-});
-
-add_task(async function test_addLivemark_bogusParentGuid_fails() {
-  try {
-    await PlacesUtils.livemarks.addLivemark(
-      { title: "test",
-        parentGuid: "123456789012",
-        feedURI: FEED_URI,
-      });
-    do_throw("Adding a livemark with a bogus parent should fail");
-  } catch (ex) {}
-});
-
-add_task(async function test_addLivemark_intoLivemark_fails() {
-  let livemark = await PlacesUtils.livemarks.addLivemark(
-    { title: "test",
-      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-      feedURI: FEED_URI,
-    });
-
-  try {
-    await PlacesUtils.livemarks.addLivemark(
-      { title: "test",
-        parentGuid: livemark.guid,
-        feedURI: FEED_URI,
-      });
-    do_throw("Adding a livemark into a livemark should fail");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
-  }
-});
-
-add_task(async function test_addLivemark_forceGuid_succeeds() {
-  let livemark = await PlacesUtils.livemarks.addLivemark(
-    { title: "test",
-      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-      feedURI: FEED_URI,
-      guid: "1234567890AB",
-    });
-    Assert.equal(livemark.guid, "1234567890AB");
-    do_check_guid_for_bookmark(livemark.id, "1234567890AB");
-});
-
-add_task(async function test_addLivemark_dateAdded_succeeds() {
-  let dateAdded = new Date("2013-03-01T01:10:00") * 1000;
-  let livemark = await PlacesUtils.livemarks.addLivemark(
-    { title: "test",
-      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-      feedURI: FEED_URI,
-      dateAdded,
-    });
-  Assert.equal(livemark.dateAdded, dateAdded);
-});
-
-add_task(async function test_addLivemark_lastModified_succeeds() {
-  let now = Date.now() * 1000;
-  let livemark = await PlacesUtils.livemarks.addLivemark(
-    { title: "test",
-      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-      feedURI: FEED_URI,
-      lastModified: now,
-    });
-  Assert.equal(livemark.dateAdded, now);
-  Assert.equal(livemark.lastModified, now);
-});
-
-add_task(async function test_removeLivemark_emptyObject_throws() {
-  try {
-    await PlacesUtils.livemarks.removeLivemark({});
-    do_throw("Invoking removeLivemark with empty object should throw");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
-  }
-});
-
-add_task(async function test_removeLivemark_noValidId_throws() {
-  try {
-    await PlacesUtils.livemarks.removeLivemark({ id: -10, guid: "test"});
-    do_throw("Invoking removeLivemark with no valid id should throw");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
-  }
-});
-
-add_task(async function test_removeLivemark_nonExistent_fails() {
-  try {
-    await PlacesUtils.livemarks.removeLivemark({ id: 1337 });
-    do_throw("Removing a non-existent livemark should fail");
-  } catch (ex) {
-  }
-});
-
-add_task(async function test_removeLivemark_guid_succeeds() {
-  let livemark = await PlacesUtils.livemarks.addLivemark(
-    { title: "test",
-      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-      feedURI: FEED_URI,
-      guid: "234567890ABC",
-  });
-
-  Assert.equal(livemark.guid, "234567890ABC");
-
-  await PlacesUtils.livemarks.removeLivemark({
-    id: 789, guid: "234567890ABC",
-  });
-
-  Assert.equal((await PlacesUtils.bookmarks.fetch("234567890ABC")), null);
-});
-
-add_task(async function test_removeLivemark_id_succeeds() {
-  let livemark = await PlacesUtils.livemarks.addLivemark(
-    { title: "test",
-      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-      feedURI: FEED_URI,
-  });
-
-  await PlacesUtils.livemarks.removeLivemark({ id: livemark.id });
-
-  Assert.equal((await PlacesUtils.bookmarks.fetch("234567890ABC")), null);
-});
-
-add_task(async function test_getLivemark_emptyObject_throws() {
-  try {
-    await PlacesUtils.livemarks.getLivemark({});
-    do_throw("Invoking getLivemark with empty object should throw");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
-  }
-});
-
-add_task(async function test_getLivemark_noValidId_throws() {
-  try {
-    await PlacesUtils.livemarks.getLivemark({ id: -10, guid: "test"});
-    do_throw("Invoking getLivemark with no valid id should throw");
-  } catch (ex) {
-    Assert.equal(ex.result, Cr.NS_ERROR_INVALID_ARG);
-  }
-});
-
-add_task(async function test_getLivemark_nonExistentId_fails() {
-  try {
-    await PlacesUtils.livemarks.getLivemark({ id: 1234 });
-    do_throw("getLivemark for a non existent id should fail");
-  } catch (ex) {}
-});
-
-add_task(async function test_getLivemark_nonExistentGUID_fails() {
-  try {
-    await PlacesUtils.livemarks.getLivemark({ guid: "34567890ABCD" });
-    do_throw("getLivemark for a non-existent guid should fail");
-  } catch (ex) {}
-});
-
-add_task(async function test_getLivemark_guid_succeeds() {
-  await PlacesUtils.livemarks.addLivemark(
-    { title: "test",
-      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-      feedURI: FEED_URI,
-      guid: "34567890ABCD" });
-
-  // invalid id to check the guid wins.
-  let livemark =
-    await PlacesUtils.livemarks.getLivemark({ id: 789, guid: "34567890ABCD" });
-
-  Assert.equal(livemark.title, "test");
-  Assert.equal(livemark.parentId, unfiledFolderId);
-  Assert.equal(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
-  Assert.ok(livemark.feedURI.equals(FEED_URI));
-  Assert.equal(livemark.siteURI, null);
-  Assert.equal(livemark.guid, "34567890ABCD");
-
-  let bookmark = await PlacesUtils.bookmarks.fetch("34567890ABCD");
-  Assert.equal(livemark.index, bookmark.index);
-});
-
-add_task(async function test_getLivemark_id_succeeds() {
-  let livemark = await PlacesUtils.livemarks.addLivemark(
-    { title: "test",
-      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-      feedURI: FEED_URI,
-    });
-
-  livemark = await PlacesUtils.livemarks.getLivemark({ id: livemark.id });
-
-  Assert.equal(livemark.title, "test");
-  Assert.equal(livemark.parentId, unfiledFolderId);
-  Assert.equal(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
-  Assert.ok(livemark.feedURI.equals(FEED_URI));
-  Assert.equal(livemark.siteURI, null);
-  do_check_guid_for_bookmark(livemark.id, livemark.guid);
-
-  let bookmark = await PlacesUtils.bookmarks.fetch(livemark.guid);
-  Assert.equal(livemark.index, bookmark.index);
-});
-
-add_task(async function test_getLivemark_removeItem_contention() {
-  // do not yield.
-  PlacesUtils.livemarks.addLivemark({ title: "test",
-                                      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-                                      feedURI: FEED_URI,
-                                  }).catch(() => { /* swallow errors*/ });
-  await PlacesUtils.bookmarks.eraseEverything();
-  let livemark = await PlacesUtils.livemarks.addLivemark(
-    { title: "test",
-      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-      feedURI: FEED_URI,
-    });
-
-  livemark = await PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
-
-  Assert.equal(livemark.title, "test");
-  Assert.equal(livemark.parentId, unfiledFolderId);
-  Assert.ok(livemark.feedURI.equals(FEED_URI));
-  Assert.equal(livemark.siteURI, null);
-  do_check_guid_for_bookmark(livemark.id, livemark.guid);
-});
-
-add_task(async function test_title_change() {
-  let livemark = await PlacesUtils.livemarks.addLivemark(
-    { title: "test",
-      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-      feedURI: FEED_URI,
-    });
-
-  await PlacesUtils.bookmarks.update({ guid: livemark.guid,
-                                       title: "test2" });
-  // Poll for the title change.
-  while (true) {
-    let lm = await PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
-    if (lm.title == "test2")
-      break;
-    await new Promise(resolve => do_timeout(resolve, 100));
-  }
-});
-
-add_task(async function test_livemark_move() {
-  let livemark = await PlacesUtils.livemarks.addLivemark(
-    { title: "test",
-      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-      feedURI: FEED_URI } );
-
-  await PlacesUtils.bookmarks.update({ guid: livemark.guid,
-                                       parentGuid: PlacesUtils.bookmarks.toolbarGuid,
-                                       index: PlacesUtils.bookmarks.DEFAULT_INDEX });
-  // Poll for the parent change.
-  while (true) {
-    let lm = await PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
-    if (lm.parentGuid == PlacesUtils.bookmarks.toolbarGuid)
-      break;
-    await new Promise(resolve => do_timeout(resolve, 100));
-  }
-});
-
-add_task(async function test_livemark_removed() {
-  let livemark = await PlacesUtils.livemarks.addLivemark(
-    { title: "test",
-      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-      feedURI: FEED_URI } );
-
-  await PlacesUtils.bookmarks.remove(livemark.guid);
-  // Poll for the livemark removal.
-  while (true) {
-    try {
-      await PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
-    } catch (ex) {
-      break;
-    }
-    await new Promise(resolve => do_timeout(resolve, 100));
-  }
-});