Merge mozilla-central to mozilla-inbound. CLOSED TREE
authorCsoregi Natalia <ncsoregi@mozilla.com>
Fri, 12 Oct 2018 20:00:55 +0300
changeset 496815 ba1ab0b667800182fbcb56a5ffc7b0933fe50433
parent 496814 8ce5fe12ae8d888639369cda659cb3c9130efc80 (current diff)
parent 496608 ede21c2f2f993c7bb00d37b1e52b62c9f0c4c671 (diff)
child 496816 2cd2f8f1bc806c45b5d797809abcec8688b1ab65
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound. CLOSED TREE
dom/media/MediaManager.cpp
dom/media/moz.build
dom/media/webrtc/MediaEngineWebRTC.cpp
dom/webidl/AudioStreamTrack.webidl
dom/webidl/VideoStreamTrack.webidl
testing/web-platform/meta/css/cssom/cssstyledeclaration-mutationrecord-002.html.ini
testing/web-platform/tests/css/cssom/cssstyledeclaration-mutationrecord-005.html
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/locales/en-US/chrome/dom/dom.properties
+++ b/dom/locales/en-US/chrome/dom/dom.properties
@@ -319,16 +319,20 @@ LargeAllocationNonGetRequest=A Large-All
 LargeAllocationNotOnlyToplevelInTabGroup=A Large-Allocation header was ignored due to the presence of windows which have a reference to this browsing context through the frame hierarchy or window.opener.
 # LOCALIZATION NOTE: Do not translate "Large-Allocation", as it is a literal header name
 LargeAllocationNonE10S=A Large-Allocation header was ignored due to the document not being loaded out of process.
 GeolocationInsecureRequestIsForbidden=A Geolocation request can only be fulfilled in a secure context.
 # LOCALIZATION NOTE: Do not translate "Large-Allocation", as it is a literal header name.
 LargeAllocationNonWin32=This page would be loaded in a new process due to a Large-Allocation header, however Large-Allocation process creation is disabled on non-Win32 platforms.
 # LOCALIZATION NOTE: Do not translate URL.createObjectURL(MediaStream).
 URLCreateObjectURL_MediaStreamWarning=URL.createObjectURL(MediaStream) is deprecated and will be removed soon.
+# LOCALIZATION NOTE: Do not translate MozAutoGainControl or autoGainControl.
+MozAutoGainControlWarning=mozAutoGainControl is deprecated. Use autoGainControl instead.
+# LOCALIZATION NOTE: Do not translate mozNoiseSuppression or noiseSuppression.
+MozNoiseSuppressionWarning=mozNoiseSuppression is deprecated. Use noiseSuppression instead.
 # LOCALIZATION NOTE: Do not translate xml:base.
 XMLBaseAttributeWarning=Use of xml:base attribute is deprecated and will be removed soon. Please remove any use of it.
 # LOCALIZATION NOTE: Do not translate "content", "Window", and "window.top"
 WindowContentUntrustedWarning=The ‘content’ attribute of Window objects is deprecated.  Please use ‘window.top’ instead.
 # LOCALIZATION NOTE: The first %S is the tag name of the element that starts the loop, the second %S is the element's ID.
 SVGRefLoopWarning=The SVG <%S> with ID “%S” has a reference loop.
 # LOCALIZATION NOTE: The first %S is the tag name of the element in the chain where the chain was broken, the second %S is the element's ID.
 SVGRefChainLengthExceededWarning=An SVG <%S> reference chain which is too long was abandoned at the element with ID “%S”.
--- 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
@@ -910,55 +910,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);
@@ -2789,16 +2793,24 @@ MediaManager::GetUserMedia(nsPIDOMWindow
   } else if (IsOn(c.mVideo)) {
     videoType = MediaSourceEnum::Camera;
     Telemetry::Accumulate(Telemetry::WEBRTC_GET_USER_MEDIA_TYPE,
                           (uint32_t) videoType);
   }
 
   if (c.mAudio.IsMediaTrackConstraints()) {
     auto& ac = c.mAudio.GetAsMediaTrackConstraints();
+    MediaConstraintsHelper::ConvertOldWithWarning(ac.mMozAutoGainControl,
+                                                  ac.mAutoGainControl,
+                                                  "MozAutoGainControlWarning",
+                                                  aWindow);
+    MediaConstraintsHelper::ConvertOldWithWarning(ac.mMozNoiseSuppression,
+                                                  ac.mNoiseSuppression,
+                                                  "MozNoiseSuppressionWarning",
+                                                  aWindow);
     audioType = StringToEnum(dom::MediaSourceEnumValues::strings,
                              ac.mMediaSource,
                              MediaSourceEnum::Other);
     // Work around WebIDL default since spec uses same dictionary w/audio & video.
     if (audioType == MediaSourceEnum::Camera) {
       audioType = MediaSourceEnum::Microphone;
       ac.mMediaSource.AssignASCII(EnumToASCII(dom::MediaSourceEnumValues::strings,
                                               audioType));
@@ -3404,16 +3416,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,
@@ -4757,49 +4824,59 @@ SourceListener::CapturingSource(MediaSou
 
   return CaptureState::Disabled;
 }
 
 RefPtr<SourceListener::ApplyConstraintsPromise>
 SourceListener::ApplyConstraintsToTrack(
     nsPIDOMWindowInner* aWindow,
     TrackID aTrackID,
-    const MediaTrackConstraints& aConstraints,
+    const MediaTrackConstraints& aConstraintsPassedIn,
     dom::CallerType aCallerType)
 {
   MOZ_ASSERT(NS_IsMainThread());
   DeviceState& state = GetDeviceStateFor(aTrackID);
 
   if (mStopped || state.mStopped) {
     LOG(("gUM %s track %d applyConstraints, but source is stopped",
          aTrackID == kAudioTrack ? "audio" : "video", aTrackID));
     return ApplyConstraintsPromise::CreateAndResolve(false, __func__);
   }
 
+  MediaTrackConstraints c(aConstraintsPassedIn); // use a modifiable copy
+  MediaConstraintsHelper::ConvertOldWithWarning(c.mMozAutoGainControl,
+                                                c.mAutoGainControl,
+                                                "MozAutoGainControlWarning",
+                                                aWindow);
+  MediaConstraintsHelper::ConvertOldWithWarning(c.mMozNoiseSuppression,
+                                                c.mNoiseSuppression,
+                                                "MozNoiseSuppressionWarning",
+                                                aWindow);
+
   MediaManager* mgr = MediaManager::GetIfExists();
   if (!mgr) {
     return ApplyConstraintsPromise::CreateAndResolve(false, __func__);
   }
 
   return MediaManager::PostTask<ApplyConstraintsPromise>(__func__,
-      [device = state.mDevice, aConstraints,
+      [device = state.mDevice, c,
        isChrome = aCallerType == dom::CallerType::System]
       (MozPromiseHolder<ApplyConstraintsPromise>& aHolder) mutable {
     MOZ_ASSERT(MediaManager::IsInMediaThread());
     MediaManager* mgr = MediaManager::GetIfExists();
     MOZ_RELEASE_ASSERT(mgr); // Must exist while media thread is alive
     const char* badConstraint = nullptr;
-    nsresult rv = device->Reconfigure(aConstraints, mgr->mPrefs, &badConstraint);
+    nsresult rv = device->Reconfigure(c, mgr->mPrefs, &badConstraint);
     if (rv == NS_ERROR_INVALID_ARG) {
       // Reconfigure failed due to constraints
       if (!badConstraint) {
         nsTArray<RefPtr<MediaDevice>> devices;
         devices.AppendElement(device);
         badConstraint = MediaConstraintsHelper::SelectSettings(
-            NormalizedConstraints(aConstraints), devices, isChrome);
+            NormalizedConstraints(c), devices, isChrome);
       }
 
       aHolder.Reject(Some(NS_ConvertASCIItoUTF16(badConstraint)), __func__);
       return;
     }
 
     if (NS_FAILED(rv)) {
       // Reconfigure failed unexpectedly
--- 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
@@ -162,25 +162,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();
@@ -214,17 +216,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");
@@ -247,20 +250,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,
@@ -274,17 +274,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,20 +615,48 @@ 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,
+    nsPIDOMWindowInner* aWindow) {
+  if ((old.IsBoolean() ||
+       old.GetAsConstrainBooleanParameters().mExact.WasPassed() ||
+       old.GetAsConstrainBooleanParameters().mIdeal.WasPassed()) &&
+      !(to.IsBoolean() ||
+        to.GetAsConstrainBooleanParameters().mExact.WasPassed() ||
+        to.GetAsConstrainBooleanParameters().mIdeal.WasPassed())) {
+    nsCOMPtr<nsIDocument> doc = aWindow->GetDoc();
+    if (doc) {
+      nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+                                      NS_LITERAL_CSTRING("DOM"), doc,
+                                      nsContentUtils::eDOM_PROPERTIES,
+                                      aMessageName);
+    }
+    if (old.IsBoolean()) {
+      to.SetAsBoolean() = old.GetAsBoolean();
+    } else {
+      to.SetAsConstrainBooleanParameters() = old.GetAsConstrainBooleanParameters();
+    }
+  }
+}
+
 static void
 LogConstraintStringRange(const NormalizedConstraintSet::StringRange& aRange)
 {
   if (aRange.mExact.size() <= 1 && aRange.mIdeal.size() <= 1) {
     LOG("  %s: { exact: [%s], ideal: [%s] }",
         aRange.mName,
         (aRange.mExact.size()? NS_ConvertUTF16toUTF8(*aRange.mExact.begin()).get() : ""),
         (aRange.mIdeal.size()? NS_ConvertUTF16toUTF8(*aRange.mIdeal.begin()).get() : ""));
--- a/dom/media/webrtc/MediaTrackConstraints.h
+++ b/dom/media/webrtc/MediaTrackConstraints.h
@@ -333,14 +333,22 @@ public:
   FindBadConstraint(const NormalizedConstraints& aConstraints,
                     const nsTArray<RefPtr<MediaDevice>>& aDevices);
 
   static const char*
   FindBadConstraint(const NormalizedConstraints& aConstraints,
                     const RefPtr<MediaEngineSource>& aMediaEngineSource,
                     const nsString& aDeviceId);
 
+  // Warn on and convert use of deprecated constraints to new ones
+  static void
+  ConvertOldWithWarning(
+      const dom::OwningBooleanOrConstrainBooleanParameters& old,
+      dom::OwningBooleanOrConstrainBooleanParameters& to,
+      const char* aMessageName,
+      nsPIDOMWindowInner* aWindow);
+
   static void LogConstraints(const NormalizedConstraintSet& aConstraints);
 };
 
 } // namespace mozilla
 
 #endif /* MEDIATRACKCONSTRAINTS_H_ */
--- 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;
--- a/dom/webidl/MediaStreamTrack.webidl
+++ b/dom/webidl/MediaStreamTrack.webidl
@@ -53,16 +53,19 @@ dictionary MediaTrackConstraintSet {
     ConstrainLong viewportOffsetX;
     ConstrainLong viewportOffsetY;
     ConstrainLong viewportWidth;
     ConstrainLong viewportHeight;
     ConstrainBoolean echoCancellation;
     ConstrainBoolean noiseSuppression;
     ConstrainBoolean autoGainControl;
     ConstrainLong channelCount;
+
+    ConstrainBoolean mozNoiseSuppression;
+    ConstrainBoolean mozAutoGainControl;
 };
 
 dictionary MediaTrackConstraints : MediaTrackConstraintSet {
     sequence<MediaTrackConstraintSet> advanced;
 };
 
 enum MediaStreamTrackState {
     "live",
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));
-  }
-});