diff --git a/CLOBBER b/CLOBBER
index a439da1d2d8a94c4b0f481272848525a0dd03c7a..808a6cb10569127d52fe19f72defceb02e3da27e 100644
--- a/CLOBBER
+++ b/CLOBBER
@@ -22,4 +22,4 @@
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 1288460 requires another clobber due to bug 1298779.
+Bug 1302429 to fix also bustage.
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
index 070ddd023d5de47cd2da58afabc7e1b048c9d5e1..777e049000cf7edfcaada8a44ef3f9aa39e30192 100644
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1507,3 +1507,14 @@ pref("print.use_simplify_page", true);
 // Space separated list of URLS that are allowed to send objects (instead of
 // only strings) through webchannels. This list is duplicated in mobile/android/app/mobile.js
 pref("webchannel.allowObject.urlWhitelist", "https://accounts.firefox.com https://content.cdn.mozilla.net https://input.mozilla.org https://support.mozilla.org https://install.mozilla.org");
+
+// Whether or not the browser should scan for unsubmitted
+// crash reports, and then show a notification for submitting
+// those reports.
+#ifdef RELEASE_BUILD
+pref("browser.crashReports.unsubmittedCheck.enabled", false);
+#else
+pref("browser.crashReports.unsubmittedCheck.enabled", true);
+#endif
+
+pref("browser.crashReports.unsubmittedCheck.autoSubmit", false);
\ No newline at end of file
diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css
index 0eeece8c7cbb4ae5f4e8a4fbbf20cd0a43ac5c59..d1756f00cf5400371bdef6a50b664710c3b153b8 100644
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -777,13 +777,6 @@ html|*#fullscreen-exit-button {
   -moz-binding: url("chrome://browser/content/urlbarBindings.xml#click-to-play-plugins-notification");
 }
 
-#login-fill-notification {
-  -moz-binding: url("chrome://browser/content/urlbarBindings.xml#login-fill-notification");
-}
-
-.login-fill-item {
-  -moz-binding: url("chrome://passwordmgr/content/login.xml#login");
-}
 
 .plugin-popupnotification-centeritem {
   -moz-binding: url("chrome://browser/content/urlbarBindings.xml#plugin-popupnotification-center-item");
@@ -1185,10 +1178,6 @@ toolbarpaletteitem[place="palette"][hidden] {
   display: none;
 }
 
-#login-fill-doorhanger:not([inDetailView]) > #login-fill-clickcapturer {
-  pointer-events: none;
-}
-
 .popup-notification-invalid-input {
   box-shadow: 0 0 1.5px 1px red;
 }
diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul
index f1eae4fc0b34c067543713ca0ed0d8707e8d9505..4d607740b317c2dfebac9a798132531150ca93d0 100644
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -718,8 +718,6 @@
                          tooltiptext="&urlbar.addonsNotificationAnchor.tooltip;"/>
                   <image id="indexedDB-notification-icon" class="notification-anchor-icon indexedDB-icon" role="button"
                          tooltiptext="&urlbar.indexedDBNotificationAnchor.tooltip;"/>
-                  <image id="login-fill-notification-icon" class="notification-anchor-icon login-icon" role="button"
-                         tooltiptext="&urlbar.loginFillNotificationAnchor.tooltip;"/>
                   <image id="password-notification-icon" class="notification-anchor-icon login-icon" role="button"
                          tooltiptext="&urlbar.passwordNotificationAnchor.tooltip;"/>
                   <image id="plugins-notification-icon" class="notification-anchor-icon plugin-icon" role="button"
diff --git a/browser/base/content/popup-notifications.inc b/browser/base/content/popup-notifications.inc
index 622488001543d9c09fb3526e5ec4829df02c8ff2..62d28f1b6876ecc47f651578760bd973932c704e 100644
--- a/browser/base/content/popup-notifications.inc
+++ b/browser/base/content/popup-notifications.inc
@@ -52,22 +52,6 @@
       </popupnotificationcontent>
     </popupnotification>
 
-    <stack id="login-fill-doorhanger" hidden="true">
-      <vbox id="login-fill-mainview">
-        <description id="login-fill-testing"
-                     value="Thanks for testing the login fill doorhanger!"/>
-        <textbox id="login-fill-filter"/>
-        <richlistbox id="login-fill-list"/>
-      </vbox>
-      <vbox id="login-fill-clickcapturer"/>
-      <vbox id="login-fill-details">
-        <textbox id="login-fill-username" readonly="true"/>
-        <textbox id="login-fill-password" type="password" disabled="true"/>
-        <hbox>
-          <button id="login-fill-use" label="Use in form"/>
-        </hbox>
-      </vbox>
-    </stack>
 
     <popupnotification id="addon-progress-notification" hidden="true">
       <popupnotificationcontent orient="vertical">
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media.js b/browser/base/content/test/webrtc/browser_devices_get_user_media.js
index 4ac278c557436f77447b0411d30e566355da691c..5e10b843f86a030c22816a9b81138dd54d24cce2 100644
--- a/browser/base/content/test/webrtc/browser_devices_get_user_media.js
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media.js
@@ -543,6 +543,7 @@ function test() {
         yield expectNoObserverCalled();
       }
     }).then(finish, ex => {
+     Cu.reportError(ex);
      ok(false, "Unexpected Exception: " + ex);
      finish();
     });
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js
index 273453d44d8d15f2a6602a6bb68c9d162dddbfcb..b75629f6a979084cb7cd15c8a52e85632175d128 100644
--- a/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js
@@ -93,6 +93,7 @@ function test() {
         yield test.run();
       }
     }).then(finish, ex => {
+     Cu.reportError(ex);
      ok(false, "Unexpected Exception: " + ex);
      finish();
     });
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js
index 9be3d4be4f7a2f406e0660917493893d92c4d576..e9dd87b49b2243d549cbf6dca80d5fee32c28b16 100644
--- a/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_in_frame.js
@@ -262,6 +262,7 @@ function test() {
         yield expectNoObserverCalled();
       }
     }).then(finish, ex => {
+     Cu.reportError(ex);
      ok(false, "Unexpected Exception: " + ex);
      finish();
     });
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js
index d930ee1a69590ee914c5f459d1e97ea28c0aa1a8..9061c7a00fd4d0400105835e16af800beead00a9 100644
--- a/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js
+++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_tear_off_tab.js
@@ -95,6 +95,7 @@ function test() {
         yield expectNoObserverCalled();
       }
     }).then(finish, ex => {
+     Cu.reportError(ex);
      ok(false, "Unexpected Exception: " + ex);
      finish();
     });
diff --git a/browser/base/content/urlbarBindings.xml b/browser/base/content/urlbarBindings.xml
index c7f8694938e8df424afafb697f4458d92ef003ec..b9b300774081e994e93034cf3f3bc3420375a02a 100644
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -2478,16 +2478,6 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
     </handlers>
   </binding>
 
-  <!-- This is the XBL notification definition for the login fill doorhanger,
-       which is empty because the actual panel is not implemented inside an XBL
-       binding, but made of elements added to the notification panel. This
-       allows accessing the full structure while the panel is hidden. -->
-  <binding id="login-fill-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
-    <content>
-      <children/>
-    </content>
-  </binding>
-
   <binding id="splitmenu">
     <content>
       <xul:hbox anonid="menuitem" flex="1"
diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js
index a27decbce95c67acf1080035da927fd080d463c2..eebbef271ea0f00526708519ead6c663ce6d0cd4 100644
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -71,6 +71,8 @@ XPCOMUtils.defineLazyServiceGetter(this, "AlertsService", "@mozilla.org/alerts-s
 if (AppConstants.MOZ_CRASHREPORTER) {
   XPCOMUtils.defineLazyModuleGetter(this, "PluginCrashReporter",
                                     "resource:///modules/ContentCrashHandlers.jsm");
+  XPCOMUtils.defineLazyModuleGetter(this, "UnsubmittedCrashHandler",
+                                    "resource:///modules/ContentCrashHandlers.jsm");
   XPCOMUtils.defineLazyModuleGetter(this, "CrashSubmit",
                                     "resource://gre/modules/CrashSubmit.jsm");
 }
@@ -714,6 +716,7 @@ BrowserGlue.prototype = {
     TabCrashHandler.init();
     if (AppConstants.MOZ_CRASHREPORTER) {
       PluginCrashReporter.init();
+      UnsubmittedCrashHandler.init();
     }
 
     Services.obs.notifyObservers(null, "browser-ui-startup-complete", "");
@@ -744,64 +747,6 @@ BrowserGlue.prototype = {
     }
   },
 
-  checkForPendingCrashReports: function() {
-    // We don't process crash reports older than 28 days, so don't bother submitting them
-    const PENDING_CRASH_REPORT_DAYS = 28;
-    if (AppConstants.MOZ_CRASHREPORTER) {
-      let dateLimit = new Date();
-      dateLimit.setDate(dateLimit.getDate() - PENDING_CRASH_REPORT_DAYS);
-      CrashSubmit.pendingIDsAsync(dateLimit).then(
-        function onSuccess(ids) {
-          let count = ids.length;
-          if (count) {
-            let win = RecentWindow.getMostRecentBrowserWindow();
-            if (!win) {
-              return;
-            }
-            let nb =  win.document.getElementById("global-notificationbox");
-            let notification = nb.getNotificationWithValue("pending-crash-reports");
-            if (notification) {
-              return;
-            }
-            let buttons = [
-              {
-                label: win.gNavigatorBundle.getString("pendingCrashReports.submitAll"),
-                callback: function() {
-                  ids.forEach(function(id) {
-                    CrashSubmit.submit(id, {extraExtraKeyVals: {"SubmittedFromInfobar": true}});
-                  });
-                }
-              },
-              {
-                label: win.gNavigatorBundle.getString("pendingCrashReports.ignoreAll"),
-                callback: function() {
-                  ids.forEach(function(id) {
-                    CrashSubmit.ignore(id);
-                  });
-                }
-              },
-              {
-                label: win.gNavigatorBundle.getString("pendingCrashReports.viewAll"),
-                callback: function() {
-                  win.openUILinkIn("about:crashes", "tab");
-                  return true;
-                }
-              }
-            ];
-            nb.appendNotification(PluralForm.get(count,
-                                                 win.gNavigatorBundle.getString("pendingCrashReports.label")).replace("#1", count),
-                                  "pending-crash-reports",
-                                  "chrome://browser/skin/tab-crashed.svg",
-                                  nb.PRIORITY_INFO_HIGH, buttons);
-          }
-        },
-        function onError(err) {
-          Cu.reportError(err);
-        }
-      );
-    }
-  },
-
   _onSafeModeRestart: function BG_onSafeModeRestart() {
     // prompt the user to confirm
     let strings = gBrowserBundle;
@@ -1070,10 +1015,6 @@ BrowserGlue.prototype = {
 
     this._checkForOldBuildUpdates();
 
-    if (!AppConstants.RELEASE_BUILD) {
-      this.checkForPendingCrashReports();
-    }
-
     CaptivePortalWatcher.init();
 
     AutoCompletePopup.init();
diff --git a/browser/components/preferences/in-content/advanced.js b/browser/components/preferences/in-content/advanced.js
index f03e1250d733133612f15d5e08381a7dce68a07d..c54f52f426e92ff8af199f27a6a90e23da1c4164 100644
--- a/browser/components/preferences/in-content/advanced.js
+++ b/browser/components/preferences/in-content/advanced.js
@@ -60,10 +60,7 @@ var gAdvancedPane = {
       setEventListener("submitHealthReportBox", "command",
                        gAdvancedPane.updateSubmitHealthReport);
     }
-    if (AppConstants.MOZ_CRASHREPORTER) {
-      setEventListener("submitCrashesBox", "command",
-                       gAdvancedPane.updateSubmitCrashes);
-    }
+
     setEventListener("connectionSettings", "command",
                      gAdvancedPane.showConnections);
     setEventListener("clearCacheButton", "command",
@@ -243,28 +240,6 @@ var gAdvancedPane = {
   {
     this._setupLearnMoreLink("toolkit.crashreporter.infoURL",
                              "crashReporterLearnMore");
-
-    var checkbox = document.getElementById("submitCrashesBox");
-    try {
-      var cr = Components.classes["@mozilla.org/toolkit/crash-reporter;1"].
-               getService(Components.interfaces.nsICrashReporter);
-      checkbox.checked = cr.submitReports;
-    } catch (e) {
-      checkbox.style.display = "none";
-    }
-  },
-
-  /**
-   *
-   */
-  updateSubmitCrashes: function ()
-  {
-    var checkbox = document.getElementById("submitCrashesBox");
-    try {
-      var cr = Components.classes["@mozilla.org/toolkit/crash-reporter;1"].
-               getService(Components.interfaces.nsICrashReporter);
-      cr.submitReports = checkbox.checked;
-    } catch (e) { }
   },
 
   /**
@@ -762,9 +737,7 @@ var gAdvancedPane = {
    */
   showCertificates: function ()
   {
-    openDialog("chrome://pippki/content/certManager.xul",
-               "mozilla:certmanager",
-               "modal=yes", null);
+    gSubDialog.open("chrome://pippki/content/certManager.xul");
   },
 
   /**
diff --git a/browser/components/preferences/in-content/advanced.xul b/browser/components/preferences/in-content/advanced.xul
index a125e7ce651702dfdd2fed9af46352ef63d9dbb8..8a8536c107c0dffad9631b3b6eb3ba3846c8caab 100644
--- a/browser/components/preferences/in-content/advanced.xul
+++ b/browser/components/preferences/in-content/advanced.xul
@@ -54,6 +54,13 @@
               type="bool"/>
 #endif
 
+  <!-- Data Choices tab -->
+#ifdef MOZ_CRASHREPORTER
+  <preference id="browser.crashReports.unsubmittedCheck.autoSubmit"
+              name="browser.crashReports.unsubmittedCheck.autoSubmit"
+              type="bool"/>
+#endif
+
   <!-- Network tab -->
   <preference id="browser.cache.disk.capacity"
               name="browser.cache.disk.capacity"
@@ -229,11 +236,13 @@
 #ifdef MOZ_CRASHREPORTER
       <groupbox>
         <caption>
-          <checkbox id="submitCrashesBox" label="&enableCrashReporter.label;"
-                    accesskey="&enableCrashReporter.accesskey;"/>
+          <checkbox id="automaticallySubmitCrashesBox"
+                    preference="browser.crashReports.unsubmittedCheck.autoSubmit"
+                    label="&alwaysSubmitCrashReports.label;"
+                    accesskey="&alwaysSubmitCrashReports.accesskey;"/>
         </caption>
         <hbox class="indent">
-          <label flex="1">&crashReporterDesc.label;</label>
+          <label flex="1">&crashReporterDesc2.label;</label>
           <spacer flex="10"/>
           <label id="crashReporterLearnMore"
                  class="text-link">&crashReporterLearnMore.label;</label>
diff --git a/browser/config/tooltool-manifests/win32/releng.manifest b/browser/config/tooltool-manifests/win32/releng.manifest
index 43af53c507efaee41b2b68bf0bdc3fb5e1980fe6..7b6d77771331c03a97462fbf4e21a9508fdfdb5a 100644
--- a/browser/config/tooltool-manifests/win32/releng.manifest
+++ b/browser/config/tooltool-manifests/win32/releng.manifest
@@ -29,11 +29,11 @@
 "unpack": true
 },
 {
-"version": "Visual Studio 2015 Update 2 / SDK 10.0.10586.0/212",
-"size": 332442800,
-"digest": "995394a4a515c7cb0f8595f26f5395361a638870dd0bbfcc22193fe1d98a0c47126057d5999cc494f3f3eac5cb49160e79757c468f83ee5797298e286ef6252c",
+"version": "Visual Studio 2015 Update 3 14.0.25425.01 / SDK 10.0.14393.0",
+"size": 326656969,
+"digest": "babc414ffc0457d27f5a1ed24a8e4873afbe2f1c1a4075469a27c005e1babc3b2a788f643f825efedff95b79686664c67ec4340ed535487168a3482e68559bc7",
 "algorithm": "sha512",
-"filename": "vs2015u2.zip",
+"filename": "vs2015u3.zip",
 "unpack": true
 }
 ]
diff --git a/browser/config/tooltool-manifests/win64/releng.manifest b/browser/config/tooltool-manifests/win64/releng.manifest
index b6d3a6197bf5e3447b9beb6f6a1bb2a233e0d436..3bba2d91a96a8bf8942a5d9e6fad2d4d27d2e978 100644
--- a/browser/config/tooltool-manifests/win64/releng.manifest
+++ b/browser/config/tooltool-manifests/win64/releng.manifest
@@ -30,11 +30,11 @@
 "unpack": true
 },
 {
-"version": "Visual Studio 2015 Update 2 / SDK 10.0.10586.0/212",
-"size": 332442800,
-"digest": "995394a4a515c7cb0f8595f26f5395361a638870dd0bbfcc22193fe1d98a0c47126057d5999cc494f3f3eac5cb49160e79757c468f83ee5797298e286ef6252c",
+"version": "Visual Studio 2015 Update 3 14.0.25425.01 / SDK 10.0.14393.0",
+"size": 326656969,
+"digest": "babc414ffc0457d27f5a1ed24a8e4873afbe2f1c1a4075469a27c005e1babc3b2a788f643f825efedff95b79686664c67ec4340ed535487168a3482e68559bc7",
 "algorithm": "sha512",
-"filename": "vs2015u2.zip",
+"filename": "vs2015u3.zip",
 "unpack": true
 }
 ]
diff --git a/browser/locales/en-US/chrome/browser/browser.dtd b/browser/locales/en-US/chrome/browser/browser.dtd
index 8042f717b0a554fbe597598ebee4aadc7aa9a8bc..ba169e9a3542fb2785e12cf288f3eb9e694a717b 100644
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -211,7 +211,6 @@ These should match what Safari and other Apple applications use on OS X Lion. --
 <!ENTITY urlbar.geolocationNotificationAnchor.tooltip     "Open location request panel">
 <!ENTITY urlbar.addonsNotificationAnchor.tooltip          "Open add-on installation message panel">
 <!ENTITY urlbar.indexedDBNotificationAnchor.tooltip       "Open offline storage message panel">
-<!ENTITY urlbar.loginFillNotificationAnchor.tooltip       "Manage your login information">
 <!ENTITY urlbar.passwordNotificationAnchor.tooltip        "Open save password message panel">
 <!ENTITY urlbar.pluginsNotificationAnchor.tooltip         "Manage plug-in use">
 <!ENTITY urlbar.webNotificationAnchor.tooltip             "Change whether you can receive notifications from the site">
diff --git a/browser/locales/en-US/chrome/browser/browser.properties b/browser/locales/en-US/chrome/browser/browser.properties
index 04ee8665ee773f8c897d3dc79d9add982e21c6b2..59173e12f937e0386d7b319af399596378fc68d4 100644
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -730,10 +730,10 @@ tabgroups.migration.tabGroupBookmarkFolderName = Bookmarked Tab Groups
 # LOCALIZATION NOTE (pendingCrashReports.label): Semi-colon list of plural forms
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # #1 is the number of pending crash reports
-pendingCrashReports.label = You have an unsubmitted crash report;You have #1 unsubmitted crash reports
+pendingCrashReports2.label = You have an unsent crash report;You have #1 unsent crash reports
 pendingCrashReports.viewAll = View
-pendingCrashReports.submitAll = Submit
-pendingCrashReports.ignoreAll = Ignore
+pendingCrashReports.send = Send
+pendingCrashReports.alwaysSend = Always Send
 
 decoder.noCodecs.button = Learn how
 decoder.noCodecs.accesskey = L
diff --git a/browser/locales/en-US/chrome/browser/preferences/advanced.dtd b/browser/locales/en-US/chrome/browser/preferences/advanced.dtd
index c65132d53c169256565c41e637d28372276da57e..124c00d845f42d4b6208a956c99e77bd0e529398 100644
--- a/browser/locales/en-US/chrome/browser/preferences/advanced.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/advanced.dtd
@@ -40,10 +40,10 @@
 <!ENTITY enableTelemetryData.accesskey   "T">
 <!ENTITY telemetryLearnMore.label        "Learn More">
 
-<!ENTITY crashReporterDesc.label         "&brandShortName; submits crash reports to help &vendorShortName; make your browser more stable and secure">
-<!ENTITY enableCrashReporter.label       "Enable Crash Reporter">
-<!ENTITY enableCrashReporter.accesskey   "C">
-<!ENTITY crashReporterLearnMore.label    "Learn More">
+<!ENTITY crashReporterDesc2.label         "Crash reports help &vendorShortName; fix problems and make your browser more stable and secure">
+<!ENTITY alwaysSubmitCrashReports.label   "Allow &brandShortName; to send backlogged crash reports on your behalf">
+<!ENTITY alwaysSubmitCrashReports.accesskey "c">
+<!ENTITY crashReporterLearnMore.label     "Learn More">
 
 <!ENTITY networkTab.label                "Network">
 
diff --git a/browser/modules/ContentCrashHandlers.jsm b/browser/modules/ContentCrashHandlers.jsm
index dbd5972ea6118dd1f6c7c6514b94388188e99179..285f5277640b23dc5d8356335a04c33fb5f2fe0b 100644
--- a/browser/modules/ContentCrashHandlers.jsm
+++ b/browser/modules/ContentCrashHandlers.jsm
@@ -8,7 +8,9 @@ var Cc = Components.classes;
 var Ci = Components.interfaces;
 var Cu = Components.utils;
 
-this.EXPORTED_SYMBOLS = [ "TabCrashHandler", "PluginCrashReporter" ];
+this.EXPORTED_SYMBOLS = [ "TabCrashHandler",
+                          "PluginCrashReporter",
+                          "UnsubmittedCrashHandler" ];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
@@ -21,6 +23,21 @@ XPCOMUtils.defineLazyModuleGetter(this, "RemotePages",
   "resource://gre/modules/RemotePageManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
   "resource:///modules/sessionstore/SessionStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+  "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
+  "resource:///modules/RecentWindow.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+  "resource://gre/modules/PluralForm.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gNavigatorBundle", function() {
+  const url = "chrome://browser/locale/browser.properties";
+  return Services.strings.createBundle(url);
+});
+
+// We don't process crash reports older than 28 days, so don't bother
+// submitting them
+const PENDING_CRASH_REPORT_DAYS = 28;
 
 this.TabCrashHandler = {
   _crashedTabCount: 0,
@@ -319,6 +336,204 @@ this.TabCrashHandler = {
   },
 }
 
+/**
+ * This component is responsible for scanning the pending
+ * crash report directory for reports, and (if enabled), to
+ * prompt the user to submit those reports. It might also
+ * submit those reports automatically without prompting if
+ * the user has opted in.
+ */
+this.UnsubmittedCrashHandler = {
+  init() {
+    if (this.initialized) {
+      return;
+    }
+
+    this.initialized = true;
+
+    let pref = "browser.crashReports.unsubmittedCheck.enabled";
+    let shouldCheck = Services.prefs.getBoolPref(pref);
+
+    if (shouldCheck) {
+      Services.obs.addObserver(this, "browser-delayed-startup-finished",
+                               false);
+    }
+  },
+
+  observe(subject, topic, data) {
+    if (topic != "browser-delayed-startup-finished") {
+      return;
+    }
+
+    Services.obs.removeObserver(this, topic);
+    this.checkForUnsubmittedCrashReports();
+  },
+
+  /**
+   * Scans the profile directory for unsubmitted crash reports
+   * within the past PENDING_CRASH_REPORT_DAYS days. If it
+   * finds any, it will, if necessary, attempt to open a notification
+   * bar to prompt the user to submit them.
+   *
+   * @returns Promise
+   *          Resolves after it tries to append a notification on
+   *          the most recent browser window. If a notification
+   *          cannot be shown, will resolve anyways.
+   */
+  checkForUnsubmittedCrashReports: Task.async(function*() {
+    let dateLimit = new Date();
+    dateLimit.setDate(dateLimit.getDate() - PENDING_CRASH_REPORT_DAYS);
+
+    let reportIDs = [];
+    try {
+      reportIDs = yield CrashSubmit.pendingIDsAsync(dateLimit);
+    } catch (e) {
+      Cu.reportError(e);
+      return;
+    }
+
+    if (reportIDs.length) {
+      if (CrashNotificationBar.autoSubmit) {
+        CrashNotificationBar.submitReports(reportIDs);
+      } else {
+        this.showPendingSubmissionsNotification(reportIDs);
+      }
+    }
+  }),
+
+  /**
+   * Given an array of unsubmitted crash report IDs, try to open
+   * up a notification asking the user to submit them.
+   *
+   * @param reportIDs (Array<string>)
+   *        The Array of report IDs to offer the user to send.
+   */
+  showPendingSubmissionsNotification(reportIDs) {
+    let count = reportIDs.length;
+    if (!count) {
+      return;
+    }
+
+    let messageTemplate =
+      gNavigatorBundle.GetStringFromName("pendingCrashReports2.label");
+
+    let message = PluralForm.get(count, messageTemplate).replace("#1", count);
+
+    CrashNotificationBar.show({
+      notificationID: "pending-crash-reports",
+      message,
+      reportIDs,
+    });
+  },
+};
+
+this.CrashNotificationBar = {
+  /**
+   * Attempts to show a notification bar to the user in the most
+   * recent browser window asking them to submit some crash report
+   * IDs. If a notification cannot be shown (for example, there
+   * is no browser window), this method exits silently.
+   *
+   * The notification will allow the user to submit their crash
+   * reports. If the user dismissed the notification, the crash
+   * reports will be marked to be ignored (though they can
+   * still be manually submitted via about:crashes).
+   *
+   * @param JS Object
+   *        An Object with the following properties:
+   *
+   *        notificationID (string)
+   *          The ID for the notification to be opened.
+   *
+   *        message (string)
+   *          The message to be displayed in the notification.
+   *
+   *        reportIDs (Array<string>)
+   *          The array of report IDs to offer to the user.
+   */
+  show({ notificationID, message, reportIDs }) {
+    let chromeWin = RecentWindow.getMostRecentBrowserWindow();
+    if (!chromeWin) {
+      // Can't show a notification in this case. We'll hopefully
+      // get another opportunity to have the user submit their
+      // crash reports later.
+      return;
+    }
+
+    let nb =  chromeWin.document.getElementById("global-notificationbox");
+    let notification = nb.getNotificationWithValue(notificationID);
+    if (notification) {
+      return;
+    }
+
+    let buttons = [{
+      label: gNavigatorBundle.GetStringFromName("pendingCrashReports.send"),
+      callback: () => {
+        this.submitReports(reportIDs);
+      },
+    },
+    {
+      label: gNavigatorBundle.GetStringFromName("pendingCrashReports.alwaysSend"),
+      callback: () => {
+        this.autoSubmit = true;
+        this.submitReports(reportIDs);
+      },
+    },
+    {
+      label: gNavigatorBundle.GetStringFromName("pendingCrashReports.viewAll"),
+      callback: function() {
+        chromeWin.openUILinkIn("about:crashes", "tab");
+        return true;
+      },
+    }];
+
+    let eventCallback = (eventType) => {
+      if (eventType == "dismissed") {
+        // The user intentionally dismissed the notification,
+        // which we interpret as meaning that they don't care
+        // to submit the reports. We'll ignore these particular
+        // reports going forward.
+        reportIDs.forEach(function(reportID) {
+          CrashSubmit.ignore(reportID);
+        });
+      }
+    };
+
+    nb.appendNotification(message, notificationID,
+                          "chrome://browser/skin/tab-crashed.svg",
+                          nb.PRIORITY_INFO_HIGH, buttons,
+                          eventCallback);
+  },
+
+  get autoSubmit() {
+    return Services.prefs
+                   .getBoolPref("browser.crashReports.unsubmittedCheck.autoSubmit");
+  },
+
+  set autoSubmit(val) {
+    Services.prefs.setBoolPref("browser.crashReports.unsubmittedCheck.autoSubmit",
+                               val);
+  },
+
+  /**
+   * Attempt to submit reports to the crash report server. Each
+   * report will have the "SubmittedFromInfobar" extra key set
+   * to true.
+   *
+   * @param reportIDs (Array<string>)
+   *        The array of reportIDs to submit.
+   */
+  submitReports(reportIDs) {
+    for (let reportID of reportIDs) {
+      CrashSubmit.submit(reportID, {
+        extraExtraKeyVals: {
+          "SubmittedFromInfobar": true,
+        },
+      });
+    }
+  },
+};
+
 this.PluginCrashReporter = {
   /**
    * Makes the PluginCrashReporter ready to hear about and
diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css
index 8435867433e267961bb0dcb2522a60ba17bd3e7c..5d2a63de8fe8e59cff82be12db01eaafd47b75b9 100644
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1745,7 +1745,6 @@ notification.pluginVulnerable > .notification-inner > .messageCloseButton:not(:h
   background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 80, 16, 64);
 }
 
-%include ../shared/login-doorhanger.inc.css
 
 %include downloads/indicator.css
 
diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css
index cec8b2b5d87348a71649e403a1e3ba7fb1607725..e7d74e229551e73c00d7e8e0dafdb66a36285745 100644
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -3155,7 +3155,6 @@ menulist.translate-infobar-element > .menulist-dropmarker {
 %include ../../../devtools/client/themes/responsivedesign.inc.css
 %include ../../../devtools/client/themes/commandline.inc.css
 %include ../shared/plugin-doorhanger.inc.css
-%include ../shared/login-doorhanger.inc.css
 
 %include downloads/indicator.css
 
diff --git a/browser/themes/shared/login-doorhanger.inc.css b/browser/themes/shared/login-doorhanger.inc.css
deleted file mode 100644
index 7304eeea90206d643e1a61b500b76587f148fe0f..0000000000000000000000000000000000000000
--- a/browser/themes/shared/login-doorhanger.inc.css
+++ /dev/null
@@ -1,79 +0,0 @@
-#notification-popup[popupid="login-fill"] > .panel-arrowcontainer > .panel-arrowcontent {
-  /* Since we display a sliding subview that extends to the border, we cannot
-   * keep the default padding of arrow panels. We use the same padding in the
-   * individual content views instead. Since we removed the padding, we also
-   * have to ensure the contents are clipped to the border box. */
-  padding: 0;
-  overflow: hidden;
-}
-
-#login-fill-mainview,
-#login-fill-details {
-  padding: var(--panel-arrowcontent-padding);
-}
-
-#login-fill-doorhanger[inDetailView] > #login-fill-mainview {
-  transform: translateX(-14px);
-}
-
-#login-fill-mainview,
-#login-fill-details {
-  transition: transform 150ms;
-}
-
-#login-fill-doorhanger:not([inDetailView]) > #login-fill-details {
-  transform: translateX(105%);
-}
-
-#login-fill-doorhanger:not([inDetailView]) > #login-fill-details:-moz-locale-dir(rtl) {
-  transform: translateX(-105%);
-}
-
-#login-fill-doorhanger[inDetailView] > #login-fill-clickcapturer {
-  background-color: hsla(210,4%,10%,.1);
-}
-
-#login-fill-testing {
-  color: #b33;
-  font-weight: bold;
-}
-
-#login-fill-list {
-  border: 1px solid black;
-  max-height: 20em;
-}
-
-.login-fill-item[disabled] {
-  color: #888;
-  background-color: #fff;
-}
-
-.login-fill-item[disabled][selected] {
-  background-color: #eef;
-}
-
-.login-hostname {
-  margin: 4px;
-  font-weight: bold;
-}
-
-.login-fill-item.different-hostname > .login-hostname {
-  color: #888;
-  font-style: italic;
-}
-
-.login-username {
-  margin: 4px;
-  color: #888;
-}
-
-#login-fill-details {
-  padding: 4px;
-  background: var(--panel-arrowcontent-background);
-  color: var(--panel-arrowcontent-color);
-  background-clip: padding-box;
-  border-left: 1px solid hsla(210,4%,10%,.3);
-  box-shadow: 0 3px 5px hsla(210,4%,10%,.1),
-              0 0 7px hsla(210,4%,10%,.1);
-  margin-inline-start: 38px;
-}
diff --git a/browser/themes/shared/notification-icons.inc.css b/browser/themes/shared/notification-icons.inc.css
index 0766bf22d348b114b92a0a46a4bf121307e6032a..bc8b3256b52c8a17a250fb0a53c0886321910df5 100644
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -129,11 +129,6 @@
   list-style-image: url(chrome://browser/skin/notification-icons.svg#login-detailed);
 }
 
-#login-fill-notification-icon {
-  /* Temporary solution until the capture and fill doorhangers are unified. */
-  transform: scaleX(-1);
-}
-
 .camera-icon,
 .popup-notification-icon[popupid="webRTC-shareDevices"] {
   list-style-image: url(chrome://browser/skin/notification-icons.svg#camera);
diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css
index c46d6ac33c5ea1c1394803514c25fceabf641ea9..f30abc4154ba58159648f609c99f39ce00c7a48d 100644
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -2434,7 +2434,6 @@ notification.pluginVulnerable > .notification-inner > .messageCloseButton {
   }
 }
 
-%include ../shared/login-doorhanger.inc.css
 
 %include downloads/indicator.css
 
diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.jsm
index c97a2c21d231d6be7206918b0503c99a1d7f1cee..0c351d2198356c78f01ae7f70becf5aaf1ed3cc5 100644
--- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.jsm
+++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.jsm
@@ -10,21 +10,19 @@ const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://testing-common/TestUtils.jsm");
 Cu.import("resource://testing-common/ContentTask.jsm");
 
 this.Preferences = {
 
   init(libDir) {
-    Services.prefs.setBoolPref("browser.preferences.inContent", true);
-
     let panes = [
       ["paneGeneral", null],
       ["paneSearch", null],
       ["paneContent", null],
       ["paneApplications", null],
       ["panePrivacy", null],
+      ["panePrivacy", null, DNTDialog],
       ["paneSecurity", null],
       ["paneSync", null],
       ["paneAdvanced", "generalTab"],
@@ -32,40 +30,41 @@ this.Preferences = {
       ["paneAdvanced", "networkTab"],
       ["paneAdvanced", "updateTab"],
       ["paneAdvanced", "encryptionTab"],
+      ["paneAdvanced", "encryptionTab", certManager],
     ];
-    for (let [primary, advanced] of panes) {
+    for (let [primary, advanced, customFn] of panes) {
       let configName = primary.replace(/^pane/, "prefs") + (advanced ? "-" + advanced : "");
+      if (customFn) {
+        configName += "-" + customFn.name;
+      }
       this.configurations[configName] = {};
-      this.configurations[configName].applyConfig = prefHelper.bind(null, primary, advanced);
+      this.configurations[configName].applyConfig = prefHelper.bind(null, primary, advanced, customFn);
     }
   },
 
-  configurations: {
-    "panePrivacy-DNTDialog": {
-      applyConfig: Task.async(function*() {
-        let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
-        yield prefHelper("panePrivacy", null);
-
-        yield ContentTask.spawn(browserWindow.gBrowser.selectedBrowser, null, function* () {
-          content.document.getElementById("doNotTrackSettings").click();
-        });
-      }),
-    },
-  },
+  configurations: {},
 };
 
-let prefHelper = Task.async(function*(primary, advanced) {
+let prefHelper = Task.async(function*(primary, advanced = null, customFn = null) {
   let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
-  let selectedBrowser = browserWindow.gBrowser;
+  let selectedBrowser = browserWindow.gBrowser.selectedBrowser;
+
+  // close any dialog that might still be open
+  yield ContentTask.spawn(selectedBrowser, null, function*() {
+    if (!content.window.gSubDialog) {
+      return;
+    }
+    content.window.gSubDialog.close();
+  });
+
   let readyPromise = null;
   if (selectedBrowser.currentURI.specIgnoringRef == "about:preferences") {
-    readyPromise = new Promise((resolve) => {
-      browserWindow.addEventListener("MozAfterPaint", function paneSwitch() {
-        browserWindow.removeEventListener("MozAfterPaint", paneSwitch);
-        resolve();
-      });
-    });
-
+    if (selectedBrowser.currentURI.spec == "about:preferences#" + primary.replace(/^pane/, "")) {
+      // We're already on the correct pane.
+      readyPromise = Promise.resolve();
+    } else {
+      readyPromise = paintPromise(browserWindow);
+    }
   } else {
     readyPromise = TestUtils.topicObserved("advanced-pane-loaded");
   }
@@ -78,8 +77,30 @@ let prefHelper = Task.async(function*(primary, advanced) {
 
   yield readyPromise;
 
-  // close any dialog that might still be open
-  yield ContentTask.spawn(selectedBrowser.selectedBrowser, null, function*() {
-    content.window.gSubDialog.close();
-  });
+  if (customFn) {
+    let customPaintPromise = paintPromise(browserWindow);
+    yield* customFn(selectedBrowser);
+    yield customPaintPromise;
+  }
 });
+
+function paintPromise(browserWindow) {
+  return new Promise((resolve) => {
+    browserWindow.addEventListener("MozAfterPaint", function onPaint() {
+      browserWindow.removeEventListener("MozAfterPaint", onPaint);
+      resolve();
+    });
+  });
+}
+
+function* DNTDialog(aBrowser) {
+  yield ContentTask.spawn(aBrowser, null, function* () {
+    content.document.getElementById("doNotTrackSettings").click();
+  });
+}
+
+function* certManager(aBrowser) {
+  yield ContentTask.spawn(aBrowser, null, function* () {
+    content.document.getElementById("viewCertificatesButton").click();
+  });
+}
diff --git a/build/docs/toolchains.rst b/build/docs/toolchains.rst
index e6553a562169bf71e556bb8ad3f4e7f5847fa62e..eba640fa06092ba90421d79587dd5feaec84800e 100644
--- a/build/docs/toolchains.rst
+++ b/build/docs/toolchains.rst
@@ -51,7 +51,7 @@ Once Visual Studio 2015 Community has been installed, from a checkout
 of mozilla-central, run something like the following to produce a ZIP
 archive::
 
-   $ ./mach python build/windows_toolchain.py create-zip vs2015u2
+   $ ./mach python build/windows_toolchain.py create-zip vs2015u3
 
 The produced archive will be the argument to ``create-zip`` + ``.zip``.
 
diff --git a/build/moz.configure/toolchain.configure b/build/moz.configure/toolchain.configure
index bba62023c8365745dacd1594d5e24d273ff87e97..87bd50bef1c65d1c98e8e9025e11f62f0830ab78 100644
--- a/build/moz.configure/toolchain.configure
+++ b/build/moz.configure/toolchain.configure
@@ -274,9 +274,11 @@ def get_compiler_info(compiler, language):
         raise FatalCheckError(
             'Unknown compiler or compiler not supported.')
 
+    # Metadata emitted by preprocessors such as GCC with LANG=ja_JP.utf-8 may
+    # have non-ASCII characters. Treat the output as bytearray.
     data = {}
     for line in result.splitlines():
-        if line.startswith('%'):
+        if line.startswith(b'%'):
             k, _, v = line.partition(' ')
             k = k.lstrip('%')
             data[k] = v.replace(' ', '').lstrip('"').rstrip('"')
diff --git a/build/win32/mozconfig.vs2015-win64 b/build/win32/mozconfig.vs2015-win64
index c7c13c0c91e7e208fc7082198a51d706f0c01062..b81afa681dc3dca483a73cc2b5cedd716634ebe5 100644
--- a/build/win32/mozconfig.vs2015-win64
+++ b/build/win32/mozconfig.vs2015-win64
@@ -1,6 +1,6 @@
 if [ -z "${VSPATH}" ]; then
     TOOLTOOL_DIR=${TOOLTOOL_DIR:-$topsrcdir}
-    VSPATH="$(cd ${TOOLTOOL_DIR} && pwd)/vs2015u2"
+    VSPATH="$(cd ${TOOLTOOL_DIR} && pwd)/vs2015u3"
 fi
 
 VSWINPATH="$(cd ${VSPATH} && pwd -W)"
@@ -12,8 +12,8 @@ export WIN_UCRT_REDIST_DIR="${VSPATH}/SDK/Redist/ucrt/DLLs/x86"
 export PATH="${VSPATH}/VC/bin/amd64_x86:${VSPATH}/VC/bin/amd64:${VSPATH}/VC/bin:${VSPATH}/SDK/bin/x86:${VSPATH}/SDK/bin/x64:${VSPATH}/DIA SDK/bin:${PATH}"
 export PATH="${VSPATH}/VC/redist/x86/Microsoft.VC140.CRT:${VSPATH}/VC/redist/x64/Microsoft.VC140.CRT:${VSPATH}/SDK/Redist/ucrt/DLLs/x86:${VSPATH}/SDK/Redist/ucrt/DLLs/x64:${PATH}"
 
-export INCLUDE="${VSPATH}/VC/include:${VSPATH}/VC/atlmfc/include:${VSPATH}/SDK/Include/10.0.10586.0/ucrt:${VSPATH}/SDK/Include/10.0.10586.0/shared:${VSPATH}/SDK/Include/10.0.10586.0/um:${VSPATH}/SDK/Include/10.0.10586.0/winrt:${VSPATH}/DIA SDK/include"
-export LIB="${VSPATH}/VC/lib:${VSPATH}/VC/atlmfc/lib:${VSPATH}/SDK/lib/10.0.10586.0/ucrt/x86:${VSPATH}/SDK/lib/10.0.10586.0/um/x86:${VSPATH}/DIA SDK/lib"
+export INCLUDE="${VSPATH}/VC/include:${VSPATH}/VC/atlmfc/include:${VSPATH}/SDK/Include/10.0.14393.0/ucrt:${VSPATH}/SDK/Include/10.0.14393.0/shared:${VSPATH}/SDK/Include/10.0.14393.0/um:${VSPATH}/SDK/Include/10.0.14393.0/winrt:${VSPATH}/DIA SDK/include"
+export LIB="${VSPATH}/VC/lib:${VSPATH}/VC/atlmfc/lib:${VSPATH}/SDK/lib/10.0.14393.0/ucrt/x86:${VSPATH}/SDK/lib/10.0.14393.0/um/x86:${VSPATH}/DIA SDK/lib"
 
 . $topsrcdir/build/mozconfig.vs-common
 
diff --git a/build/win64/mozconfig.vs2015 b/build/win64/mozconfig.vs2015
index c38aae4b0e53cbf765ded35c63646794372e4d9c..e81a00064c21b56f2c463869bb135fdc1b6b71b5 100644
--- a/build/win64/mozconfig.vs2015
+++ b/build/win64/mozconfig.vs2015
@@ -1,6 +1,6 @@
 if [ -z "${VSPATH}" ]; then
     TOOLTOOL_DIR=${TOOLTOOL_DIR:-$topsrcdir}
-    VSPATH="$(cd ${TOOLTOOL_DIR} && pwd)/vs2015u2"
+    VSPATH="$(cd ${TOOLTOOL_DIR} && pwd)/vs2015u3"
 fi
 
 VSWINPATH="$(cd ${VSPATH} && pwd -W)"
@@ -11,8 +11,8 @@ export WIN_UCRT_REDIST_DIR="${VSPATH}/SDK/Redist/ucrt/DLLs/x64"
 
 export PATH="${VSPATH}/VC/bin/amd64:${VSPATH}/VC/bin:${VSPATH}/SDK/bin/x64:${VSPATH}/VC/redist/x64/Microsoft.VC140.CRT:${VSPATH}/SDK/Redist/ucrt/DLLs/x64:${VSPATH}/DIA SDK/bin/amd64:${PATH}"
 
-export INCLUDE="${VSPATH}/VC/include:${VSPATH}/VC/atlmfc/include:${VSPATH}/SDK/Include/10.0.10586.0/ucrt:${VSPATH}/SDK/Include/10.0.10586.0/shared:${VSPATH}/SDK/Include/10.0.10586.0/um:${VSPATH}/SDK/Include/10.0.10586.0/winrt:${VSPATH}/DIA SDK/include"
-export LIB="${VSPATH}/VC/lib/amd64:${VSPATH}/VC/atlmfc/lib/amd64:${VSPATH}/SDK/lib/10.0.10586.0/ucrt/x64:${VSPATH}/SDK/lib/10.0.10586.0/um/x64:${VSPATH}/DIA SDK/lib/amd64"
+export INCLUDE="${VSPATH}/VC/include:${VSPATH}/VC/atlmfc/include:${VSPATH}/SDK/Include/10.0.14393.0/ucrt:${VSPATH}/SDK/Include/10.0.14393.0/shared:${VSPATH}/SDK/Include/10.0.14393.0/um:${VSPATH}/SDK/Include/10.0.14393.0/winrt:${VSPATH}/DIA SDK/include"
+export LIB="${VSPATH}/VC/lib/amd64:${VSPATH}/VC/atlmfc/lib/amd64:${VSPATH}/SDK/lib/10.0.14393.0/ucrt/x64:${VSPATH}/SDK/lib/10.0.14393.0/um/x64:${VSPATH}/DIA SDK/lib/amd64"
 
 . $topsrcdir/build/mozconfig.vs-common
 
diff --git a/build/windows_toolchain.py b/build/windows_toolchain.py
index ff898375486f78f08f6b9d17b4001401ea299b8a..22e656eff3b015c720ab63e4b5c8432004b21cb8 100644
--- a/build/windows_toolchain.py
+++ b/build/windows_toolchain.py
@@ -84,7 +84,7 @@ VS_PATTERNS = [
     },
 ]
 
-SDK_RELEASE = '10.0.10586.0'
+SDK_RELEASE = '10.0.14393.0'
 
 # Files from the Windows 10 SDK to install.
 SDK_PATTERNS = [
diff --git a/config/msvc-stl-wrapper.template.h b/config/msvc-stl-wrapper.template.h
index b8f774e88236763a6290c1bd760bfba72d845755..ed9d98b0dd1b89e7970e138c7f2b8117e662d6ce 100644
--- a/config/msvc-stl-wrapper.template.h
+++ b/config/msvc-stl-wrapper.template.h
@@ -59,7 +59,11 @@
 #pragma warning( push )
 #pragma warning( disable : 4275 4530 )
 
+#ifdef __clang__
+#include_next <${HEADER}>
+#else
 #include <${HEADER_PATH}>
+#endif
 
 #pragma warning( pop )
 
diff --git a/devtools/client/aboutdebugging/test/head.js b/devtools/client/aboutdebugging/test/head.js
index 6db065d2d65b30fbdbf9ed4e3c7273c9fdb40249..a158826da29d7c92a0ca54c3db3e692db30be05a 100644
--- a/devtools/client/aboutdebugging/test/head.js
+++ b/devtools/client/aboutdebugging/test/head.js
@@ -82,11 +82,11 @@ function addTab(url, win, backgroundTab = false) {
     }
     let linkedBrowser = tab.linkedBrowser;
 
-    linkedBrowser.addEventListener("load", function onLoad() {
-      linkedBrowser.removeEventListener("load", onLoad, true);
-      info("Tab added and finished loading: " + url);
-      done(tab);
-    }, true);
+    BrowserTestUtils.browserLoaded(linkedBrowser)
+      .then(function () {
+        info("Tab added and finished loading: " + url);
+        done(tab);
+      });
   });
 }
 
diff --git a/devtools/client/canvasdebugger/test/head.js b/devtools/client/canvasdebugger/test/head.js
index 8f0da64ccd75785e98bdc7e65196930e77d4092c..a718551cee02b4d1af60bf83dec7dda169926ebb 100644
--- a/devtools/client/canvasdebugger/test/head.js
+++ b/devtools/client/canvasdebugger/test/head.js
@@ -81,11 +81,11 @@ function addTab(aUrl, aWindow) {
   let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
   let linkedBrowser = tab.linkedBrowser;
 
-  linkedBrowser.addEventListener("load", function onLoad() {
-    linkedBrowser.removeEventListener("load", onLoad, true);
-    info("Tab added and finished loading: " + aUrl);
-    deferred.resolve(tab);
-  }, true);
+  BrowserTestUtils.browserLoaded(linkedBrowser)
+    .then(function () {
+      info("Tab added and finished loading: " + aUrl);
+      deferred.resolve(tab);
+    });
 
   return deferred.promise;
 }
diff --git a/devtools/client/debugger/test/mochitest/head.js b/devtools/client/debugger/test/mochitest/head.js
index b7271007df74621024227624b9c2d5293531dc07..9ca0e0c8698c75fcc3159adafcfd32da3463ec52 100644
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -91,11 +91,11 @@ this.addTab = function addTab(aUrl, aWindow) {
   info("Loading frame script with url " + FRAME_SCRIPT_URL + ".");
   linkedBrowser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
 
-  linkedBrowser.addEventListener("load", function onLoad() {
-    linkedBrowser.removeEventListener("load", onLoad, true);
-    info("Tab added and finished loading: " + aUrl);
-    deferred.resolve(tab);
-  }, true);
+  BrowserTestUtils.browserLoaded(linkedBrowser)
+    .then(function () {
+      info("Tab added and finished loading: " + aUrl);
+      deferred.resolve(tab);
+    });
 
   return deferred.promise;
 };
diff --git a/devtools/client/framework/test/browser_keybindings_01.js b/devtools/client/framework/test/browser_keybindings_01.js
index 54b5ca6b8ac3586b67186f4b77446611acde2b1e..4e4effb070c53c2a8b37bd79da78ba9735eff466 100644
--- a/devtools/client/framework/test/browser_keybindings_01.js
+++ b/devtools/client/framework/test/browser_keybindings_01.js
@@ -5,7 +5,9 @@
 
 // Tests that the keybindings for opening and closing the inspector work as expected
 // Can probably make this a shared test that tests all of the tools global keybindings
-
+const TEST_URL = "data:text/html,<html><head><title>Test for the " +
+                 "highlighter keybindings</title></head><body>" +
+                 "<h1>Keybindings!</h1></body></html>"
 function test()
 {
   waitForExplicitFinish();
@@ -15,17 +17,11 @@ function test()
   let inspector;
   let keysetMap = { };
 
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function onload() {
-    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+  addTab(TEST_URL).then(function () {
     doc = content.document;
     node = doc.querySelector("h1");
     waitForFocus(setupKeyBindingsTest);
-  }, true);
-
-  content.location = "data:text/html,<html><head><title>Test for the " +
-                     "highlighter keybindings</title></head><body>" +
-                     "<h1>Keybindings!</h1></body></html>";
+  });
 
   function buildDevtoolsKeysetMap(keyset) {
     [].forEach.call(keyset.querySelectorAll("key"), function (key) {
diff --git a/devtools/client/framework/test/browser_toolbox_custom_host.js b/devtools/client/framework/test/browser_toolbox_custom_host.js
index 48700a5147c9e0c9538d64a4bed822aff2ebb0ea..8d5f2215d2b1544cfd21c940b9382cbcb5e44360 100644
--- a/devtools/client/framework/test/browser_toolbox_custom_host.js
+++ b/devtools/client/framework/test/browser_toolbox_custom_host.js
@@ -3,28 +3,25 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+const TEST_URL = "data:text/html,test custom host";
+
 function test() {
   let {Toolbox} = require("devtools/client/framework/toolbox");
 
-  let toolbox, iframe, target, tab;
-
-  gBrowser.selectedTab = gBrowser.addTab();
-  target = TargetFactory.forTab(gBrowser.selectedTab);
+  let toolbox, iframe, target;
 
   window.addEventListener("message", onMessage);
 
   iframe = document.createElement("iframe");
   document.documentElement.appendChild(iframe);
 
-  gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
-    gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+  addTab(TEST_URL).then(function (tab) {
+    target = TargetFactory.forTab(tab);
     let options = {customIframe: iframe};
     gDevTools.showToolbox(target, null, Toolbox.HostType.CUSTOM, options)
              .then(testCustomHost, console.error)
              .then(null, console.error);
-  }, true);
-
-  content.location = "data:text/html,test custom host";
+  });
 
   function onMessage(event) {
     info("onMessage: " + event.data);
@@ -50,7 +47,7 @@ function test() {
     // toolbox.destroy() returns a singleton promise that ensures
     // everything is cleaned up before proceeding.
     toolbox.destroy().then(() => {
-      toolbox = iframe = target = tab = null;
+      toolbox = iframe = target = null;
       finish();
     });
   }
diff --git a/devtools/client/framework/test/browser_toolbox_dynamic_registration.js b/devtools/client/framework/test/browser_toolbox_dynamic_registration.js
index 7c7337fe671d742ec41eae9c37dec9c5a6ef3bb7..2583ca68e9fa1f8890cfb4c5b3d914a468f59f08 100644
--- a/devtools/client/framework/test/browser_toolbox_dynamic_registration.js
+++ b/devtools/client/framework/test/browser_toolbox_dynamic_registration.js
@@ -3,19 +3,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+const TEST_URL = "data:text/html,test for dynamically registering and unregistering tools";
+
 var toolbox;
 
 function test()
 {
-  gBrowser.selectedTab = gBrowser.addTab();
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
-    gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+  addTab(TEST_URL).then(tab => {
+    let target = TargetFactory.forTab(tab);
     gDevTools.showToolbox(target).then(testRegister);
-  }, true);
-
-  content.location = "data:text/html,test for dynamically registering and unregistering tools";
+  });
 }
 
 function testRegister(aToolbox)
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js b/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js
index 5e2faae843c2269c5bdeda53bfa75ab0a5ca516a..a068f822e5dc69c48fd52b2058d92e2388a8f7ac 100644
--- a/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js
@@ -6,23 +6,19 @@
 /* import-globals-from shared-head.js */
 "use strict";
 
+const TEST_URL = "data:text/html;charset=utf8,test for dynamically " +
+                 "registering and unregistering tools";
 var doc = null, toolbox = null, panelWin = null, modifiedPrefs = [];
 
 function test() {
-  gBrowser.selectedTab = gBrowser.addTab();
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
-    gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+  addTab(TEST_URL).then(tab => {
+    let target = TargetFactory.forTab(tab);
     gDevTools.showToolbox(target)
       .then(testSelectTool)
       .then(testToggleToolboxButtons)
       .then(testPrefsAreRespectedWhenReopeningToolbox)
       .then(cleanup, errorHandler);
-  }, true);
-
-  content.location = "data:text/html;charset=utf8,test for dynamically " +
-                     "registering and unregistering tools";
+  });
 }
 
 function testPrefsAreRespectedWhenReopeningToolbox() {
diff --git a/devtools/client/framework/test/browser_toolbox_options_disable_js.js b/devtools/client/framework/test/browser_toolbox_options_disable_js.js
index 7db8ba2357a3da998854ad9a85377e5295e6b564..b0c14a805fd2da16184c71c0850f80b5fee8e8a3 100644
--- a/devtools/client/framework/test/browser_toolbox_options_disable_js.js
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_js.js
@@ -8,15 +8,10 @@
 const TEST_URI = URL_ROOT + "browser_toolbox_options_disable_js.html";
 
 function test() {
-  gBrowser.selectedTab = gBrowser.addTab();
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
-    gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+  addTab(TEST_URI).then(tab => {
+    let target = TargetFactory.forTab(tab);
     gDevTools.showToolbox(target).then(testSelectTool);
-  }, true);
-
-  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
+  });
 }
 
 function testSelectTool(toolbox) {
diff --git a/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js
index 95e2267c20ce929025e8bdf5343d4d3709492323..3273f439514e6595d2ed71bbb0c74558abfe0800 100644
--- a/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js
+++ b/devtools/client/framework/test/browser_toolbox_options_enable_serviceworkers_testing.js
@@ -31,19 +31,15 @@ function test() {
 }
 
 function init() {
-  let tab = gBrowser.selectedTab = gBrowser.addTab();
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-  let linkedBrowser = tab.linkedBrowser;
+  addTab(TEST_URI).then(tab => {
+    let target = TargetFactory.forTab(tab);
+    let linkedBrowser = tab.linkedBrowser;
 
-  linkedBrowser.messageManager.loadFrameScript(COMMON_FRAME_SCRIPT_URL, false);
-  linkedBrowser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
+    linkedBrowser.messageManager.loadFrameScript(COMMON_FRAME_SCRIPT_URL, false);
+    linkedBrowser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
 
-  gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
-    gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
     gDevTools.showToolbox(target).then(testSelectTool);
-  }, true);
-
-  content.location = TEST_URI;
+  });
 }
 
 function testSelectTool(aToolbox) {
diff --git a/devtools/client/framework/test/browser_toolbox_raise.js b/devtools/client/framework/test/browser_toolbox_raise.js
index fe4a163747e6fe701553fc0bf6319e9039d74244..c1f26659fec0307201b7ab8fdf26516c89c0ca78 100644
--- a/devtools/client/framework/test/browser_toolbox_raise.js
+++ b/devtools/client/framework/test/browser_toolbox_raise.js
@@ -3,23 +3,20 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+const TEST_URL = "data:text/html,test for opening toolbox in different hosts";
+
 var {Toolbox} = require("devtools/client/framework/toolbox");
 
-var toolbox, target, tab1, tab2;
+var toolbox, tab1, tab2;
 
 function test() {
-  gBrowser.selectedTab = tab1 = gBrowser.addTab();
-  tab2 = gBrowser.addTab();
-  target = TargetFactory.forTab(gBrowser.selectedTab);
-
-  gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
-    gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+  addTab(TEST_URL).then(tab => {
+    tab2 = gBrowser.addTab();
+    let target = TargetFactory.forTab(tab);
     gDevTools.showToolbox(target)
              .then(testBottomHost, console.error)
              .then(null, console.error);
-  }, true);
-
-  content.location = "data:text/html,test for opening toolbox in different hosts";
+  });
 }
 
 function testBottomHost(aToolbox) {
@@ -73,7 +70,7 @@ function cleanup() {
   Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM);
 
   toolbox.destroy().then(function () {
-    toolbox = target = null;
+    toolbox = null;
     gBrowser.removeCurrentTab();
     gBrowser.removeCurrentTab();
     finish();
diff --git a/devtools/client/framework/test/browser_toolbox_ready.js b/devtools/client/framework/test/browser_toolbox_ready.js
index 1d61474fc864a94f7c0d0cf4e76d9d7695159f3e..e1a59b3f0b642673290dbda284d2ac38ab4dbb19 100644
--- a/devtools/client/framework/test/browser_toolbox_ready.js
+++ b/devtools/client/framework/test/browser_toolbox_ready.js
@@ -3,25 +3,19 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-function test() {
-  gBrowser.selectedTab = gBrowser.addTab();
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
+const TEST_URL = "data:text/html,test for toolbox being ready";
 
-  const onLoad = Task.async(function* (evt) {
-    gBrowser.selectedBrowser.removeEventListener("load", onLoad);
+add_task(function* () {
+  let tab = yield addTab(TEST_URL);
+  let target = TargetFactory.forTab(tab);
 
-    const toolbox = yield gDevTools.showToolbox(target, "webconsole");
-    ok(toolbox.isReady, "toolbox isReady is set");
-    ok(toolbox.threadClient, "toolbox has a thread client");
+  const toolbox = yield gDevTools.showToolbox(target, "webconsole");
+  ok(toolbox.isReady, "toolbox isReady is set");
+  ok(toolbox.threadClient, "toolbox has a thread client");
 
-    const toolbox2 = yield gDevTools.showToolbox(toolbox.target, toolbox.toolId);
-    is(toolbox2, toolbox, "same toolbox");
+  const toolbox2 = yield gDevTools.showToolbox(toolbox.target, toolbox.toolId);
+  is(toolbox2, toolbox, "same toolbox");
 
-    yield toolbox.destroy();
-    gBrowser.removeCurrentTab();
-    finish();
-  });
-
-  gBrowser.selectedBrowser.addEventListener("load", onLoad, true);
-  content.location = "data:text/html,test for toolbox being ready";
-}
+  yield toolbox.destroy();
+  gBrowser.removeCurrentTab();
+});
diff --git a/devtools/client/framework/test/browser_two_tabs.js b/devtools/client/framework/test/browser_two_tabs.js
index 3f997ed48b267db04efdee1ab00c39fae4a26e28..08d5f2391b3cca837f80f26fa4a51dbcb6c97a7e 100644
--- a/devtools/client/framework/test/browser_two_tabs.js
+++ b/devtools/client/framework/test/browser_two_tabs.js
@@ -30,17 +30,14 @@ function test() {
 
 function openTabs() {
   // Open two tabs, select the second
-  gTab1 = gBrowser.addTab(TAB_URL_1);
-  gTab1.linkedBrowser.addEventListener("load", function onLoad1(evt) {
-    gTab1.linkedBrowser.removeEventListener("load", onLoad1);
-
-    gTab2 = gBrowser.selectedTab = gBrowser.addTab(TAB_URL_2);
-    gTab2.linkedBrowser.addEventListener("load", function onLoad2(evt) {
-      gTab2.linkedBrowser.removeEventListener("load", onLoad2);
+  addTab(TAB_URL_1).then(tab1 => {
+    gTab1 = tab1;
+    addTab(TAB_URL_2).then(tab2 => {
+      gTab2 = tab2;
 
       connect();
-    }, true);
-  }, true);
+    });
+  });
 }
 
 function connect() {
diff --git a/devtools/client/framework/test/helper_disable_cache.js b/devtools/client/framework/test/helper_disable_cache.js
index fbc1634e4a30a691e636882336e723e1b60e9541..5e2feef8f2ec0434a730234c2f2b46ff295083be 100644
--- a/devtools/client/framework/test/helper_disable_cache.js
+++ b/devtools/client/framework/test/helper_disable_cache.js
@@ -91,12 +91,10 @@ function reloadTab(tabX) {
   let def = defer();
   let browser = gBrowser.selectedBrowser;
 
-  // once() doesn't work here so we use a standard handler instead.
-  browser.addEventListener("load", function onLoad() {
-    browser.removeEventListener("load", onLoad, true);
+  BrowserTestUtils.browserLoaded(browser).then(function () {
     info("Reloaded tab " + tabX.title);
     def.resolve();
-  }, true);
+  });
 
   info("Reloading tab " + tabX.title);
   let mm = getFrameScript();
diff --git a/devtools/client/framework/test/shared-head.js b/devtools/client/framework/test/shared-head.js
index d51e487834bb40b869ff1b693682d96d3bb980a5..eef571b47923c708661394286a2d9b31c6adf159 100644
--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -113,7 +113,7 @@ var addTab = Task.async(function* (url) {
   info("Adding a new tab with URL: " + url);
 
   let tab = gBrowser.selectedTab = gBrowser.addTab(url);
-  yield once(gBrowser.selectedBrowser, "load", true);
+  yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
 
   info("Tab added and finished loading");
 
@@ -142,7 +142,7 @@ var removeTab = Task.async(function* (tab) {
  */
 var refreshTab = Task.async(function*(tab) {
   info("Refreshing tab.");
-  const finished = once(gBrowser.selectedBrowser, "load", true);
+  const finished = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
   gBrowser.reloadTab(gBrowser.selectedTab);
   yield finished;
   info("Tab finished refreshing.");
@@ -291,9 +291,7 @@ function waitForTick() {
  * @return A promise that resolves when the time is passed
  */
 function wait(ms) {
-  let def = defer();
-  content.setTimeout(def.resolve, ms);
-  return def.promise;
+  return new promise(resolve => setTimeout(resolve, ms));
 }
 
 /**
diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js
index dbe405c948568ba6c8450f3d7af6a61e1f5a8922..7d8626b7251c3a889d6082acde0ba458a859fe92 100644
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -551,8 +551,7 @@ Toolbox.prototype = {
       this.toggleSplitConsole();
       // If the debugger is paused, don't let the ESC key stop any pending
       // navigation.
-      let jsdebugger = this.getPanel("jsdebugger");
-      if (jsdebugger && jsdebugger.panelWin.gThreadClient.state == "paused") {
+      if (this._threadClient.state == "paused") {
         e.preventDefault();
       }
     }
diff --git a/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js b/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js
index 5120a3eb4a46be3d5aa67d622f03da29c86a6359..34d8fb2a2dbb53218e2e9af75ce8c6507df2fce0 100644
--- a/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js
+++ b/devtools/client/inspector/computed/test/browser_computed_search-filter_context-menu.js
@@ -52,7 +52,7 @@ add_task(function* () {
   EventUtils.synthesizeMouse(searchField, 2, 2,
     {type: "contextmenu", button: 2}, win);
   yield onContextMenuPopup;
-  yield waitForClipboard(() => cmdCopy.click(), TEST_INPUT);
+  yield waitForClipboardPromise(() => cmdCopy.click(), TEST_INPUT);
   searchContextMenu.hidePopup();
   yield onContextMenuHidden;
 
diff --git a/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles.js b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles.js
index 37b7f1a9e0c5e13e54a871459cdb3a1eda3cd867..ce8be59ad827839efc340d43db7135ef209e7648 100644
--- a/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles.js
+++ b/devtools/client/inspector/computed/test/browser_computed_select-and-copy-styles.js
@@ -62,7 +62,7 @@ function* checkCopySelection(view) {
                         "font-variant-caps: small-caps;[\\r\\n]*";
 
   try {
-    yield waitForClipboard(() => fireCopyEvent(props[0]),
+    yield waitForClipboardPromise(() => fireCopyEvent(props[0]),
                            () => checkClipboardData(expectedPattern));
   } catch (e) {
     failedClipboard(expectedPattern);
@@ -84,7 +84,7 @@ function* checkSelectAll(view) {
                         "font-variant-caps: small-caps;[\\r\\n]*";
 
   try {
-    yield waitForClipboard(() => fireCopyEvent(prop),
+    yield waitForClipboardPromise(() => fireCopyEvent(prop),
                            () => checkClipboardData(expectedPattern));
   } catch (e) {
     failedClipboard(expectedPattern);
diff --git a/devtools/client/inspector/markup/test/browser_markup_links_05.js b/devtools/client/inspector/markup/test/browser_markup_links_05.js
index 08ce2c2cd7719d0e20f77c13af13ac3d92dbfa91..feaf257a83516d16b418125a5b86cf2fb05e4fe6 100644
--- a/devtools/client/inspector/markup/test/browser_markup_links_05.js
+++ b/devtools/client/inspector/markup/test/browser_markup_links_05.js
@@ -25,7 +25,7 @@ add_task(function* () {
   let onTabOpened = once(gBrowser.tabContainer, "TabOpen");
   inspector.onFollowLink();
   let {target: tab} = yield onTabOpened;
-  yield waitForTabLoad(tab);
+  yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
 
   ok(true, "A new tab opened");
   is(tab.linkedBrowser.currentURI.spec, URL_ROOT + "doc_markup_tooltip.png",
@@ -67,16 +67,3 @@ add_task(function* () {
   is(inspector.selection.nodeFront.tagName.toLowerCase(), "output",
     "The <output> node is still selected");
 });
-
-function waitForTabLoad(tab) {
-  let def = defer();
-  tab.addEventListener("load", function onLoad(e) {
-    // Skip load event for about:blank
-    if (tab.linkedBrowser.currentURI.spec === "about:blank") {
-      return;
-    }
-    tab.removeEventListener("load", onLoad);
-    def.resolve();
-  });
-  return def.promise;
-}
diff --git a/devtools/client/inspector/markup/test/browser_markup_links_07.js b/devtools/client/inspector/markup/test/browser_markup_links_07.js
index b322285db18bbbbef3e9decd57ef86ab1fc76057..793c1ee90922151566f46e36f5df0023b31c6bc7 100644
--- a/devtools/client/inspector/markup/test/browser_markup_links_07.js
+++ b/devtools/client/inspector/markup/test/browser_markup_links_07.js
@@ -58,19 +58,6 @@ add_task(function* () {
   yield followLinkNoNewNode(linkEl, true, inspector);
 });
 
-function waitForTabLoad(tab) {
-  let def = defer();
-  tab.addEventListener("load", function onLoad() {
-    // Skip load event for about:blank
-    if (tab.linkedBrowser.currentURI.spec === "about:blank") {
-      return;
-    }
-    tab.removeEventListener("load", onLoad);
-    def.resolve();
-  });
-  return def.promise;
-}
-
 function performMouseDown(linkEl, metactrl) {
   let evt = linkEl.ownerDocument.createEvent("MouseEvents");
 
@@ -95,7 +82,7 @@ function* followLinkWaitForTab(linkEl, isMetaClick, expectedTabURI) {
   let onTabOpened = once(gBrowser.tabContainer, "TabOpen");
   performMouseDown(linkEl, isMetaClick);
   let {target} = yield onTabOpened;
-  yield waitForTabLoad(target);
+  yield BrowserTestUtils.browserLoaded(target.linkedBrowser);
   ok(true, "A new tab opened");
   is(target.linkedBrowser.currentURI.spec, expectedTabURI,
      "The URL for the new tab is correct");
diff --git a/devtools/client/inspector/markup/test/head.js b/devtools/client/inspector/markup/test/head.js
index e38bce947269bf3544af9f498849c792b9a64419..f7d55a27211df35d58ed4a98a56414fe3aff0c1c 100644
--- a/devtools/client/inspector/markup/test/head.js
+++ b/devtools/client/inspector/markup/test/head.js
@@ -271,18 +271,6 @@ function searchUsingSelectorSearch(selector, inspector) {
   EventUtils.sendKey("return", inspector.panelWin);
 }
 
-/**
- * This shouldn't be used in the tests, but is useful when writing new tests or
- * debugging existing tests in order to introduce delays in the test steps
- * @param {Number} ms The time to wait
- * @return A promise that resolves when the time is passed
- */
-function wait(ms) {
-  let def = defer();
-  setTimeout(def.resolve, ms);
-  return def.promise;
-}
-
 /**
  * Check to see if the inspector menu items for editing are disabled.
  * Things like Edit As HTML, Delete Node, etc.
diff --git a/devtools/client/inspector/rules/test/browser_rules_copy_styles.js b/devtools/client/inspector/rules/test/browser_rules_copy_styles.js
index e5e6f1ec92d939c76f9771791af3d22c2d8b6711..a6f991a60dbf2f1f989e578e49ef9945a360ab89 100644
--- a/devtools/client/inspector/rules/test/browser_rules_copy_styles.js
+++ b/devtools/client/inspector/rules/test/browser_rules_copy_styles.js
@@ -267,7 +267,7 @@ function* checkCopyStyle(view, node, menuItemLabel, expectedPattern, visible) {
      visible.copyRule);
 
   try {
-    yield waitForClipboard(() => menuItem.click(),
+    yield waitForClipboardPromise(() => menuItem.click(),
       () => checkClipboardData(expectedPattern));
   } catch (e) {
     failedClipboard(expectedPattern);
diff --git a/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js b/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js
index 2ce7bf4eb0406a9ee3cd5e4fdb89da96d582e8c5..2b4120116f7ac1accb25fc7696938731398c5bef 100644
--- a/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js
+++ b/devtools/client/inspector/rules/test/browser_rules_search-filter_context-menu.js
@@ -51,7 +51,7 @@ add_task(function* () {
   EventUtils.synthesizeMouse(searchField, 2, 2,
     {type: "contextmenu", button: 2}, win);
   yield onContextMenuPopup;
-  yield waitForClipboard(() => cmdCopy.click(), TEST_INPUT);
+  yield waitForClipboardPromise(() => cmdCopy.click(), TEST_INPUT);
   searchContextMenu.hidePopup();
   yield onContextMenuHidden;
 
diff --git a/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js b/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js
index 6a6946a84f5091cc2c089d6d6deb050a779e71b5..b3f4ef3648a6ef93cffdfb462f16c07397685ec4 100644
--- a/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js
+++ b/devtools/client/inspector/rules/test/browser_rules_select-and-copy-styles.js
@@ -75,7 +75,7 @@ function* checkCopySelection(view) {
     "Copy menu item is displayed as expected");
 
   try {
-    yield waitForClipboard(() => menuitemCopy.click(),
+    yield waitForClipboardPromise(() => menuitemCopy.click(),
       () => checkClipboardData(expectedPattern));
   } catch (e) {
     failedClipboard(expectedPattern);
@@ -109,7 +109,7 @@ function* checkSelectAll(view) {
     "Copy menu item is displayed as expected");
 
   try {
-    yield waitForClipboard(() => menuitemCopy.click(),
+    yield waitForClipboardPromise(() => menuitemCopy.click(),
       () => checkClipboardData(expectedPattern));
   } catch (e) {
     failedClipboard(expectedPattern);
@@ -137,7 +137,7 @@ function* checkCopyEditorValue(view) {
     "Copy menu item is displayed as expected");
 
   try {
-    yield waitForClipboard(() => menuitemCopy.click(),
+    yield waitForClipboardPromise(() => menuitemCopy.click(),
       () => checkClipboardData(expectedPattern));
   } catch (e) {
     failedClipboard(expectedPattern);
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js
index 5dee0e1ee9549042d1fb94cfa1da43d435eb311b..afae7a2b6f6f49e236672a5b2b14dec411e36ebe 100644
--- a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js
@@ -39,7 +39,7 @@ function* testCopyToClipboard(inspector, view) {
 
   ok(menuitemCopyColor.visible, "Copy color is visible");
 
-  yield waitForClipboard(() => menuitemCopyColor.click(),
+  yield waitForClipboardPromise(() => menuitemCopyColor.click(),
     "#123ABC");
 
   EventUtils.synthesizeKey("VK_ESCAPE", { });
diff --git a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js
index 45d0a94fa62869b16ed6ad7efbd9b56972fa294c..7e74591ad69c7eefd6c435ae46863840a254e5c6 100644
--- a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-urls.js
@@ -87,12 +87,12 @@ function* testCopyUrlToClipboard({view, inspector}, type, selector, expected) {
 
   if (type == "data-uri") {
     info("Click Copy Data URI and wait for clipboard");
-    yield waitForClipboard(() => {
+    yield waitForClipboardPromise(() => {
       return menuitemCopyImageDataUrl.click();
     }, expected);
   } else {
     info("Click Copy URL and wait for clipboard");
-    yield waitForClipboard(() => {
+    yield waitForClipboardPromise(() => {
       return menuitemCopyUrl.click();
     }, expected);
   }
diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js
index 467984124c66bfa48d91b5dad2924afff06d0ba8..c287f29137ced38f40431c4ad885037b628dbdb5 100644
--- a/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-eyedropper-clipboard.js
@@ -23,7 +23,7 @@ add_task(function* () {
   info("Make sure to wait until the eyedropper is done taking a screenshot of the page");
   yield waitForElementAttributeSet("root", "drawn", helper);
 
-  yield waitForClipboard(() => {
+  yield waitForClipboardPromise(() => {
     info("Activate the eyedropper so the background color is copied");
     EventUtils.synthesizeKey("VK_RETURN", {});
   }, "#FF0000");
diff --git a/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js b/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js
index 38ad0583e89f964b37642beaa501b6c8046c0886..46b0ce5f5afa96ecd8f90b58df995a829a7217f7 100644
--- a/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js
+++ b/devtools/client/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js
@@ -34,7 +34,7 @@ function* setSelectionNodeFront(node, inspector) {
 
 function* checkClipboard(expectedText, node) {
   try {
-    yield waitForClipboard(() => fireCopyEvent(node), expectedText);
+    yield waitForClipboardPromise(() => fireCopyEvent(node), expectedText);
     ok(true, "Clipboard successfully filled with : " + expectedText);
   } catch (e) {
     ok(false, "Clipboard could not be filled with the expected text : " +
diff --git a/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js b/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js
index 2f5199801b2a9c2dea96d9eeff0ece0660563bc4..0c96e9bbea5e88894c098f672bee198ff48691a8 100644
--- a/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js
+++ b/devtools/client/inspector/test/browser_inspector_menu-02-copy-items.js
@@ -44,6 +44,6 @@ add_task(function* () {
     let item = allMenuItems.find(i => i.id === id);
     ok(item, "The popup has a " + desc + " menu item.");
 
-    yield waitForClipboard(() => item.click(), text);
+    yield waitForClipboardPromise(() => item.click(), text);
   }
 });
diff --git a/devtools/client/inspector/test/browser_inspector_search-filter_context-menu.js b/devtools/client/inspector/test/browser_inspector_search-filter_context-menu.js
index 086d43f8c80a22c7d242d932dee8b341b15305c2..9784a6da4d8055b8929302a226a042127bd06cee 100644
--- a/devtools/client/inspector/test/browser_inspector_search-filter_context-menu.js
+++ b/devtools/client/inspector/test/browser_inspector_search-filter_context-menu.js
@@ -50,7 +50,7 @@ add_task(function* () {
   EventUtils.synthesizeMouse(searchBox, 2, 2,
     {type: "contextmenu", button: 2}, win);
   yield onContextMenuPopup;
-  yield waitForClipboard(() => cmdCopy.click(), TEST_INPUT);
+  yield waitForClipboardPromise(() => cmdCopy.click(), TEST_INPUT);
   searchContextMenu.hidePopup();
   yield onContextMenuHidden;
 
diff --git a/devtools/client/inspector/test/head.js b/devtools/client/inspector/test/head.js
index b890da1cfeefb497cf377db875e3736cbd0f6b6b..141cb5c20843ed35ebed8b08817bfc5be836bcb6 100644
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -618,23 +618,6 @@ function waitForStyleEditor(toolbox, href) {
   return def.promise;
 }
 
-/**
- * @see SimpleTest.waitForClipboard
- *
- * @param {Function} setup
- *        Function to execute before checking for the
- *        clipboard content
- * @param {String|Function} expected
- *        An expected string or validator function
- * @return a promise that resolves when the expected string has been found or
- * the validator function has returned true, rejects otherwise.
- */
-function waitForClipboard(setup, expected) {
-  let def = defer();
-  SimpleTest.waitForClipboard(expected, setup, def.resolve, def.reject);
-  return def.promise;
-}
-
 /**
  * Checks if document's active element is within the given element.
  * @param  {HTMLDocument}  doc document with active element in question
@@ -662,8 +645,7 @@ var waitForTab = Task.async(function* () {
   info("Waiting for a tab to open");
   yield once(gBrowser.tabContainer, "TabOpen");
   let tab = gBrowser.selectedTab;
-  let browser = tab.linkedBrowser;
-  yield once(browser, "load", true);
+  yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
   info("The tab load completed");
   return tab;
 });
diff --git a/devtools/client/projecteditor/test/head.js b/devtools/client/projecteditor/test/head.js
index bbda81262ad0b123ed3ad8fb8fddcc92c31c4efc..d5d9ce8497953dc1b824b94bd0dd279c9d743f79 100644
--- a/devtools/client/projecteditor/test/head.js
+++ b/devtools/client/projecteditor/test/head.js
@@ -57,15 +57,13 @@ function addTab(url) {
   info("Adding a new tab with URL: '" + url + "'");
   let def = promise.defer();
 
-  let tab = gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function onload() {
-    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+  let tab = gBrowser.selectedTab = gBrowser.addTab(url);
+  BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(function () {
     info("URL '" + url + "' loading complete");
     waitForFocus(() => {
       def.resolve(tab);
     }, content);
-  }, true);
-  content.location = url;
+  });
 
   return def.promise;
 }
diff --git a/devtools/client/responsive.html/actions/touch-simulation.js b/devtools/client/responsive.html/actions/touch-simulation.js
index 9950d45fbb4b680691599baf572020c7445f3d6c..dc27f9a94830327b23ca776933be45bff08ebd44 100644
--- a/devtools/client/responsive.html/actions/touch-simulation.js
+++ b/devtools/client/responsive.html/actions/touch-simulation.js
@@ -12,7 +12,7 @@ const {
 
 module.exports = {
 
-  updateTouchSimulationEnabled(enabled) {
+  updateTouchSimulationEnabled(enabled = false) {
     return {
       type: UPDATE_TOUCH_SIMULATION_ENABLED,
       enabled,
diff --git a/devtools/client/responsive.html/app.js b/devtools/client/responsive.html/app.js
index 3b5bdc29544b8683bbdc4c2f424b067f02bce390..37cd7caf7cdcbd4774ff5d55517aece5ecfb3717 100644
--- a/devtools/client/responsive.html/app.js
+++ b/devtools/client/responsive.html/app.js
@@ -48,6 +48,7 @@ let App = createClass({
       device,
     }, "*");
     this.props.dispatch(changeDevice(id, device.name));
+    this.props.dispatch(updateTouchSimulationEnabled(device.touch));
   },
 
   onContentResize({ width, height }) {
diff --git a/devtools/client/responsive.html/manager.js b/devtools/client/responsive.html/manager.js
index af040f9b8af7adf2cd825390ffca0ab20b92f432..158a9b856b70f23800d0d3a6a5dbe0a99f24d128 100644
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -424,9 +424,10 @@ ResponsiveUI.prototype = {
 
     switch (event.data.type) {
       case "change-viewport-device":
-        let { userAgent, pixelRatio } = event.data.device;
+        let { userAgent, pixelRatio, touch } = event.data.device;
         this.updateUserAgent(userAgent);
         this.updateDPPX(pixelRatio);
+        this.updateTouchSimulation(touch);
         break;
       case "content-resize":
         let { width, height } = event.data;
diff --git a/devtools/client/responsive.html/test/browser/browser_device_change.js b/devtools/client/responsive.html/test/browser/browser_device_change.js
index 29e34372857c4e6e4384f907225b635fbb71af64..9e1c3e38eccaaa36c5ba5f53ad5ea67410f9adcd 100644
--- a/devtools/client/responsive.html/test/browser/browser_device_change.js
+++ b/devtools/client/responsive.html/test/browser/browser_device_change.js
@@ -40,31 +40,31 @@ addRDMTask(TEST_URL, function* ({ ui, manager }) {
   // Test defaults
   testViewportDimensions(ui, 320, 480);
   yield testUserAgent(ui, DEFAULT_UA);
-  testDevicePixelRatio(yield getViewportDevicePixelRatio(ui), DEFAULT_DPPX);
+  yield testDevicePixelRatio(ui, DEFAULT_DPPX);
+  yield testTouchEventsOverride(ui, false);
   testViewportSelectLabel(ui, "no device selected");
 
-  let waitingPixelRatio = onceDevicePixelRatioChange(ui);
-
-  // Test device with custom UA
+  // Test device with custom properties
   yield switchDevice(ui, "Fake Phone RDM Test");
   yield waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
   yield testUserAgent(ui, testDevice.userAgent);
-
-  // Test device with custom pixelRatio
-  testDevicePixelRatio(yield waitingPixelRatio, testDevice.pixelRatio);
-  waitingPixelRatio = onceDevicePixelRatioChange(ui);
+  yield testDevicePixelRatio(ui, testDevice.pixelRatio);
+  yield testTouchEventsOverride(ui, true);
 
   // Test resetting device when resizing viewport
   yield testViewportResize(ui, ".viewport-vertical-resize-handle",
     [-10, -10], [testDevice.width, testDevice.height - 10], [0, -10], ui);
   yield testUserAgent(ui, DEFAULT_UA);
+  yield testDevicePixelRatio(ui, DEFAULT_DPPX);
+  yield testTouchEventsOverride(ui, false);
   testViewportSelectLabel(ui, "no device selected");
-  testDevicePixelRatio(yield waitingPixelRatio, DEFAULT_DPPX);
 
-  // Test device where UA field is blank
+  // Test device with generic properties
   yield switchDevice(ui, "Laptop (1366 x 768)");
   yield waitForViewportResizeTo(ui, 1366, 768);
   yield testUserAgent(ui, DEFAULT_UA);
+  yield testDevicePixelRatio(ui, 1);
+  yield testTouchEventsOverride(ui, false);
 
   ok(removeDevice(testDevice),
     "Test Device properly removed.");
@@ -79,39 +79,37 @@ function testViewportDimensions(ui, w, h) {
      `${h}px`, `Viewport should have height of ${h}px`);
 }
 
-function testViewportSelectLabel(ui, label) {
+function testViewportSelectLabel(ui, expected) {
   let select = ui.toolWindow.document.querySelector(".viewport-device-selector");
-  is(select.selectedOptions[0].textContent, label,
-     `Select label should be changed to ${label}`);
+  is(select.selectedOptions[0].textContent, expected,
+     `Select label should be changed to ${expected}`);
 }
 
-function* testUserAgent(ui, value) {
+function* testUserAgent(ui, expected) {
   let ua = yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
     return content.navigator.userAgent;
   });
-  is(ua, value, `UA should be set to ${value}`);
+  is(ua, expected, `UA should be set to ${expected}`);
 }
 
-function testDevicePixelRatio(dppx, expected) {
+function* testDevicePixelRatio(ui, expected) {
+  let dppx = yield getViewportDevicePixelRatio(ui);
   is(dppx, expected, `devicePixelRatio should be set to ${expected}`);
 }
 
+function* testTouchEventsOverride(ui, expected) {
+  let { document } = ui.toolWindow;
+  let touchButton = document.querySelector("#global-touch-simulation-button");
+
+  let flag = yield ui.emulationFront.getTouchEventsOverride();
+  is(flag === Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED, expected,
+    `Touch events override should be ${expected ? "enabled" : "disabled"}`);
+  is(touchButton.classList.contains("active"), expected,
+    `Touch simulation button should be ${expected ? "" : "not"} active.`);
+}
+
 function* getViewportDevicePixelRatio(ui) {
   return yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
     return content.devicePixelRatio;
   });
 }
-
-function onceDevicePixelRatioChange(ui) {
-  return ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
-    let pixelRatio = content.devicePixelRatio;
-    let mql = content.matchMedia(`(resolution: ${pixelRatio}dppx)`);
-
-    return new Promise(resolve => {
-      mql.addListener(function listener() {
-        mql.removeListener(listener);
-        resolve(content.devicePixelRatio);
-      });
-    });
-  });
-}
diff --git a/devtools/client/responsivedesign/test/head.js b/devtools/client/responsivedesign/test/head.js
index 6d557a342cbd2b4aab41c0370560380726fc979e..bcc297514feb9ad6f072e08394df172ebea13a90 100644
--- a/devtools/client/responsivedesign/test/head.js
+++ b/devtools/client/responsivedesign/test/head.js
@@ -195,18 +195,12 @@ var addTab = Task.async(function* (url) {
   let tab = gBrowser.selectedTab = gBrowser.addTab(url);
   let browser = tab.linkedBrowser;
 
-  yield once(browser, "load", true);
+  yield BrowserTestUtils.browserLoaded(browser);
   info("URL '" + url + "' loading complete");
 
   return tab;
 });
 
-function wait(ms) {
-  let def = promise.defer();
-  setTimeout(def.resolve, ms);
-  return def.promise;
-}
-
 /**
  * Waits for the next load to complete in the current browser.
  *
diff --git a/devtools/client/shadereditor/test/head.js b/devtools/client/shadereditor/test/head.js
index 9db351692348a91e30ec985baadf5b1842a06bbf..754a0605d9f1ae34c5894799f02416c2e2548ee0 100644
--- a/devtools/client/shadereditor/test/head.js
+++ b/devtools/client/shadereditor/test/head.js
@@ -76,11 +76,10 @@ function addTab(aUrl, aWindow) {
   let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
   let linkedBrowser = tab.linkedBrowser;
 
-  linkedBrowser.addEventListener("load", function onLoad() {
-    linkedBrowser.removeEventListener("load", onLoad, true);
+  BrowserTestUtils.browserLoaded(linkedBrowser).then(function () {
     info("Tab added and finished loading: " + aUrl);
     deferred.resolve(tab);
-  }, true);
+  });
 
   return deferred.promise;
 }
diff --git a/devtools/client/sourceeditor/test/browser_codemirror.js b/devtools/client/sourceeditor/test/browser_codemirror.js
index 7b9a4efbcd05b452ca9c96e62eb27d5e1d33c398..381a6530f9d21e1e388b86bc1eb25f0ef070a2e8 100644
--- a/devtools/client/sourceeditor/test/browser_codemirror.js
+++ b/devtools/client/sourceeditor/test/browser_codemirror.js
@@ -12,14 +12,7 @@ function test() {
   requestLongerTimeout(3);
   waitForExplicitFinish();
 
-  let tab = gBrowser.addTab();
-  gBrowser.selectedTab = tab;
-
-  let browser = gBrowser.getBrowserForTab(tab);
-  browser.addEventListener("load", function onLoad() {
-    browser.removeEventListener("load", onLoad, true);
-    runCodeMirrorTest(browser);
-  }, true);
-
-  browser.loadURI(URI);
+  addTab(URI).then(function (tab) {
+    runCodeMirrorTest(tab.linkedBrowser);
+  });
 }
diff --git a/devtools/client/sourceeditor/test/browser_css_autocompletion.js b/devtools/client/sourceeditor/test/browser_css_autocompletion.js
index 40158dda91d32fc5a91aa7475218cf3702eecdc0..f4a913060f8f3608868a9a8779afc477a32a3826 100644
--- a/devtools/client/sourceeditor/test/browser_css_autocompletion.js
+++ b/devtools/client/sourceeditor/test/browser_css_autocompletion.js
@@ -79,13 +79,10 @@ let inspector;
 
 function test() {
   waitForExplicitFinish();
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function onload() {
-    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+  addTab(TEST_URI).then(function () {
     doc = content.document;
     runTests();
-  }, true);
-  content.location = TEST_URI;
+  });
 }
 
 function runTests() {
diff --git a/devtools/client/sourceeditor/test/browser_vimemacs.js b/devtools/client/sourceeditor/test/browser_vimemacs.js
index 21ad5cf7547fc183ed5ae0ba6b3d690e77a13fa4..46ff02b5e0ea3dd0bb82f87846724e625638bbf8 100644
--- a/devtools/client/sourceeditor/test/browser_vimemacs.js
+++ b/devtools/client/sourceeditor/test/browser_vimemacs.js
@@ -11,14 +11,7 @@ function test() {
   requestLongerTimeout(4);
   waitForExplicitFinish();
 
-  let tab = gBrowser.addTab();
-  gBrowser.selectedTab = tab;
-
-  let browser = gBrowser.getBrowserForTab(tab);
-  browser.addEventListener("load", function onLoad() {
-    browser.removeEventListener("load", onLoad, true);
-    runCodeMirrorTest(browser);
-  }, true);
-
-  browser.loadURI(URI);
+  addTab(URI).then(function (tab) {
+    runCodeMirrorTest(tab.linkedBrowser);
+  });
 }
diff --git a/devtools/client/sourceeditor/test/head.js b/devtools/client/sourceeditor/test/head.js
index 6f283b637f2ab4c24ccf052f9e406bcebcd3cfe4..91f878f3d3576195d134e4812174e80d0dd462c8 100644
--- a/devtools/client/sourceeditor/test/head.js
+++ b/devtools/client/sourceeditor/test/head.js
@@ -22,18 +22,16 @@ SimpleTest.registerCleanupFunction(() => {
 function addTab(url, callback) {
   waitForExplicitFinish();
 
-  gBrowser.selectedTab = gBrowser.addTab();
-  content.location = url;
-
+  gBrowser.selectedTab = gBrowser.addTab(url);
   let tab = gBrowser.selectedTab;
   let browser = gBrowser.getBrowserForTab(tab);
 
-  function onTabLoad() {
-    browser.removeEventListener("load", onTabLoad, true);
-    callback(browser, tab, browser.contentDocument);
-  }
-
-  browser.addEventListener("load", onTabLoad, true);
+  return BrowserTestUtils.browserLoaded(browser).then(function () {
+    if (typeof(callback) == "function") {
+      callback(browser, tab, browser.contentDocument);
+    }
+    return tab;
+  });
 }
 
 function promiseTab(url) {
diff --git a/devtools/client/styleeditor/test/head.js b/devtools/client/styleeditor/test/head.js
index 3183c0d444a3cc377f583935846e7e315bca3d49..c7abaa43570c54c46e7fed1b248338ccde383c65 100644
--- a/devtools/client/styleeditor/test/head.js
+++ b/devtools/client/styleeditor/test/head.js
@@ -29,11 +29,11 @@ var addTab = function (url, win) {
   let targetBrowser = targetWindow.gBrowser;
 
   let tab = targetBrowser.selectedTab = targetBrowser.addTab(url);
-  targetBrowser.selectedBrowser.addEventListener("load", function onload() {
-    targetBrowser.selectedBrowser.removeEventListener("load", onload, true);
-    info("URL '" + url + "' loading complete");
-    def.resolve(tab);
-  }, true);
+  BrowserTestUtils.browserLoaded(targetBrowser.selectedBrowser)
+    .then(function () {
+      info("URL '" + url + "' loading complete");
+      def.resolve(tab);
+    });
 
   return def.promise;
 };
diff --git a/devtools/client/webaudioeditor/test/head.js b/devtools/client/webaudioeditor/test/head.js
index 822113e81eede42abcb9904caa9a18c4b46053d4..7b0b0f01ad2bcd4a0e4b6bbe6e6dad14ef1e77b2 100644
--- a/devtools/client/webaudioeditor/test/head.js
+++ b/devtools/client/webaudioeditor/test/head.js
@@ -73,11 +73,10 @@ function addTab(aUrl, aWindow) {
   let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
   let linkedBrowser = tab.linkedBrowser;
 
-  linkedBrowser.addEventListener("load", function onLoad() {
-    linkedBrowser.removeEventListener("load", onLoad, true);
+  BrowserTestUtils.browserLoaded(linkedBrowser).then(function () {
     info("Tab added and finished loading: " + aUrl);
     deferred.resolve(tab);
-  }, true);
+  });
 
   return deferred.promise;
 }
diff --git a/devtools/client/webconsole/test/browser_webconsole_output_06.js b/devtools/client/webconsole/test/browser_webconsole_output_06.js
index 9bcfce9082d17f54cdbf5e0dff47c23f9b766a7a..ad69b390861b3cd52ffc04b87f4e3bae4f6538c4 100644
--- a/devtools/client/webconsole/test/browser_webconsole_output_06.js
+++ b/devtools/client/webconsole/test/browser_webconsole_output_06.js
@@ -198,7 +198,7 @@ var inputTests = [
   // 21
   {
     input: '({0: "a", 1: "b", length: 1})',
-    output: 'Object { 1: "b", length: 1, 1 more\u2026 }',
+    output: 'Object { 0: "a", 1: "b", length: 1 }',
     printOutput: "[object Object]",
     inspectable: true,
     variablesViewLabel: "Object",
@@ -225,7 +225,7 @@ var inputTests = [
   // 24
   {
     input: '({0: "a", 2: "b", length: 2})',
-    output: 'Object { 2: "b", length: 2, 1 more\u2026 }',
+    output: 'Object { 0: "a", 2: "b", length: 2 }',
     printOutput: "[object Object]",
     inspectable: true,
     variablesViewLabel: "Object",
@@ -243,7 +243,7 @@ var inputTests = [
   // 26
   {
     input: '({0: "a", b: "b", length: 1})',
-    output: 'Object { b: "b", length: 1, 1 more\u2026 }',
+    output: 'Object { 0: "a", b: "b", length: 1 }',
     printOutput: "[object Object]",
     inspectable: true,
     variablesViewLabel: "Object",
@@ -252,7 +252,7 @@ var inputTests = [
   // 27
   {
     input: '({0: "a", b: "b", length: 2})',
-    output: 'Object { b: "b", length: 2, 1 more\u2026 }',
+    output: 'Object { 0: "a", b: "b", length: 2 }',
     printOutput: "[object Object]",
     inspectable: true,
     variablesViewLabel: "Object",
diff --git a/devtools/client/webide/test/head.js b/devtools/client/webide/test/head.js
index 5f9534530e91406d9adbe06556b3d5b30928e2bf..c0171c730b64732e422ef0fb647caf4a1f8d16b6 100644
--- a/devtools/client/webide/test/head.js
+++ b/devtools/client/webide/test/head.js
@@ -167,11 +167,10 @@ function addTab(aUrl, aWindow) {
   let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
   let linkedBrowser = tab.linkedBrowser;
 
-  linkedBrowser.addEventListener("load", function onLoad() {
-    linkedBrowser.removeEventListener("load", onLoad, true);
+  BrowserTestUtils.browserLoaded(linkedBrowser).then(function () {
     info("Tab added and finished loading: " + aUrl);
     deferred.resolve(tab);
-  }, true);
+  });
 
   return deferred.promise;
 }
diff --git a/devtools/server/actors/object.js b/devtools/server/actors/object.js
index 6816338c737e9b9199a66d73b2f1e6e62436292a..1f417b951593df525c088bffc9a8233fd10ae8b5 100644
--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -1090,7 +1090,7 @@ function enumWeakSetEntries(objectActor) {
  * having customized output. This object holds arrays mapped by
  * Debugger.Object.prototype.class.
  *
- * In each array you can add functions that take two
+ * In each array you can add functions that take three
  * arguments:
  *   - the ObjectActor instance and its hooks to make a preview for,
  *   - the grip object being prepared for the client,
@@ -1102,16 +1102,16 @@ function enumWeakSetEntries(objectActor) {
  * information for the debugger object, or true otherwise.
  */
 DebuggerServer.ObjectActorPreviewers = {
-  String: [function (objectActor, grip) {
-    return wrappedPrimitivePreviewer("String", String, objectActor, grip);
+  String: [function (objectActor, grip, rawObj) {
+    return wrappedPrimitivePreviewer("String", String, objectActor, grip, rawObj);
   }],
 
-  Boolean: [function (objectActor, grip) {
-    return wrappedPrimitivePreviewer("Boolean", Boolean, objectActor, grip);
+  Boolean: [function (objectActor, grip, rawObj) {
+    return wrappedPrimitivePreviewer("Boolean", Boolean, objectActor, grip, rawObj);
   }],
 
-  Number: [function (objectActor, grip) {
-    return wrappedPrimitivePreviewer("Number", Number, objectActor, grip);
+  Number: [function (objectActor, grip, rawObj) {
+    return wrappedPrimitivePreviewer("Number", Number, objectActor, grip, rawObj);
   }],
 
   Function: [function ({obj, hooks}, grip) {
@@ -1379,17 +1379,16 @@ DebuggerServer.ObjectActorPreviewers = {
  *        The result grip to fill in
  * @return Booolean true if the object was handled, false otherwise
  */
-function wrappedPrimitivePreviewer(className, classObj, objectActor, grip) {
+function wrappedPrimitivePreviewer(className, classObj, objectActor, grip, rawObj) {
   let {obj, hooks} = objectActor;
 
   if (!obj.proto || obj.proto.class != className) {
     return false;
   }
 
-  let raw = obj.unsafeDereference();
   let v = null;
   try {
-    v = classObj.prototype.valueOf.call(raw);
+    v = classObj.prototype.valueOf.call(rawObj);
   } catch (ex) {
     // valueOf() can throw if the raw JS object is "misbehaved".
     return false;
@@ -1399,7 +1398,7 @@ function wrappedPrimitivePreviewer(className, classObj, objectActor, grip) {
     return false;
   }
 
-  let canHandle = GenericObject(objectActor, grip, className === "String");
+  let canHandle = GenericObject(objectActor, grip, rawObj, className === "String");
   if (!canHandle) {
     return false;
   }
@@ -1409,7 +1408,7 @@ function wrappedPrimitivePreviewer(className, classObj, objectActor, grip) {
   return true;
 }
 
-function GenericObject(objectActor, grip, specialStringBehavior = false) {
+function GenericObject(objectActor, grip, rawObj, specialStringBehavior = false) {
   let {obj, hooks} = objectActor;
   if (grip.preview || grip.displayString || hooks.getGripDepth() > 1) {
     return false;
@@ -1859,7 +1858,9 @@ DebuggerServer.ObjectActorPreviewers.Object = [
     return true;
   },
 
-  GenericObject,
+  function Object(objectActor, grip, rawObj) {
+    return GenericObject(objectActor, grip, rawObj, /* specialStringBehavior = */ false);
+  },
 ];
 
 /**
diff --git a/devtools/server/tests/browser/browser_canvasframe_helper_04.js b/devtools/server/tests/browser/browser_canvasframe_helper_04.js
index b3c52c0723668c7ab5815b95bc0580858104d62b..d038f84a0f432ba51b19594b0f7cb79f0c7c3e83 100644
--- a/devtools/server/tests/browser/browser_canvasframe_helper_04.js
+++ b/devtools/server/tests/browser/browser_canvasframe_helper_04.js
@@ -65,7 +65,7 @@ add_task(function* () {
   is(mouseDownHandled, 1, "The mousedown event was handled once before navigation");
 
   info("Navigating to a new page");
-  let loaded = once(gBrowser.selectedBrowser, "load", true);
+  let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
   content.location = TEST_URL_2;
   yield loaded;
   doc = gBrowser.selectedBrowser.contentWindow.document;
diff --git a/devtools/server/tests/browser/head.js b/devtools/server/tests/browser/head.js
index 0a16876457b28ce5d84d9315ee0c8e24df1eae71..1e7f09d95764666bb2de8097d8dce00ff4fcef2f 100644
--- a/devtools/server/tests/browser/head.js
+++ b/devtools/server/tests/browser/head.js
@@ -33,7 +33,7 @@ waitForExplicitFinish();
 var addTab = Task.async(function* (url) {
   info(`Adding a new tab with URL: ${url}`);
   let tab = gBrowser.selectedTab = gBrowser.addTab(url);
-  yield once(gBrowser.selectedBrowser, "load", true);
+  yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
 
   info(`Tab added and URL ${url} loaded`);
 
diff --git a/dom/animation/test/css-animations/file_animation-id.html b/dom/animation/test/css-animations/file_animation-id.html
index 412616a7ad388faa7315c9a1429bf90fddadbf9c..dbd5ee0eef37e5232c53a5057c7d1dc5325a2a55 100644
--- a/dom/animation/test/css-animations/file_animation-id.html
+++ b/dom/animation/test/css-animations/file_animation-id.html
@@ -18,14 +18,6 @@ test(function(t) {
   assert_equals(animation.id, 'anim', 'animation.id reflects the value set');
 }, 'Animation.id for CSS Animations');
 
-test(function(t) {
-  var div = addDiv(t);
-  var animation = div.animate({}, 100 * MS_PER_SEC);
-  assert_equals(animation.id, '', 'id for CSS Animation is initially empty');
-  animation.id = 'anim'
-
-  assert_equals(animation.id, 'anim', 'animation.id reflects the value set');
-}, 'Animation.id for CSS Animations');
 done();
 </script>
 </body>
diff --git a/dom/animation/test/css-transitions/file_animation-cancel.html b/dom/animation/test/css-transitions/file_animation-cancel.html
index 7db609ad979c98da1b1714121beaa9d2e588c9fb..12417956e174b4d7520ff62a62e0c102930b4b14 100644
--- a/dom/animation/test/css-transitions/file_animation-cancel.html
+++ b/dom/animation/test/css-transitions/file_animation-cancel.html
@@ -5,7 +5,7 @@
 <script>
 'use strict';
 
-async_test(function(t) {
+promise_test(function(t) {
   var div = addDiv(t, { style: 'margin-left: 0px' });
   flushComputedStyle(div);
 
@@ -14,17 +14,16 @@ async_test(function(t) {
   flushComputedStyle(div);
 
   var animation = div.getAnimations()[0];
-  animation.ready.then(waitForFrame).then(t.step_func(function() {
+  return animation.ready.then(waitForFrame).then(function() {
     assert_not_equals(getComputedStyle(div).marginLeft, '1000px',
                       'transform style is animated before cancelling');
     animation.cancel();
     assert_equals(getComputedStyle(div).marginLeft, div.style.marginLeft,
                   'transform style is no longer animated after cancelling');
-    t.done();
-  }));
+  });
 }, 'Animated style is cleared after cancelling a running CSS transition');
 
-async_test(function(t) {
+promise_test(function(t) {
   var div = addDiv(t, { style: 'margin-left: 0px' });
   flushComputedStyle(div);
 
@@ -32,24 +31,22 @@ async_test(function(t) {
   div.style.marginLeft = '1000px';
   flushComputedStyle(div);
 
-  div.addEventListener('transitionend', t.step_func(function() {
+  div.addEventListener('transitionend', function() {
     assert_unreached('Got unexpected end event on cancelled transition');
-  }));
+  });
 
   var animation = div.getAnimations()[0];
-  animation.ready.then(t.step_func(function() {
+  return animation.ready.then(function() {
     // Seek to just before the end then cancel
     animation.currentTime = 99.9 * 1000;
     animation.cancel();
 
     // Then wait a couple of frames and check that no event was dispatched
     return waitForAnimationFrames(2);
-  })).then(t.step_func(function() {
-    t.done();
-  }));
+  });
 }, 'Cancelled CSS transitions do not dispatch events');
 
-async_test(function(t) {
+promise_test(function(t) {
   var div = addDiv(t, { style: 'margin-left: 0px' });
   flushComputedStyle(div);
 
@@ -58,7 +55,7 @@ async_test(function(t) {
   flushComputedStyle(div);
 
   var animation = div.getAnimations()[0];
-  animation.ready.then(t.step_func(function() {
+  return animation.ready.then(function() {
     animation.cancel();
     assert_equals(getComputedStyle(div).marginLeft, '1000px',
                   'margin-left style is not animated after cancelling');
@@ -66,14 +63,13 @@ async_test(function(t) {
     assert_equals(getComputedStyle(div).marginLeft, '0px',
                   'margin-left style is animated after re-starting transition');
     return animation.ready;
-  })).then(t.step_func(function() {
+  }).then(function() {
     assert_equals(animation.playState, 'running',
                   'Transition succeeds in running after being re-started');
-    t.done();
-  }));
+  });
 }, 'After cancelling a transition, it can still be re-used');
 
-async_test(function(t) {
+promise_test(function(t) {
   var div = addDiv(t, { style: 'margin-left: 0px' });
   flushComputedStyle(div);
 
@@ -82,7 +78,7 @@ async_test(function(t) {
   flushComputedStyle(div);
 
   var animation = div.getAnimations()[0];
-  animation.ready.then(t.step_func(function() {
+  return animation.ready.then(function() {
     animation.finish();
     animation.cancel();
     assert_equals(getComputedStyle(div).marginLeft, '1000px',
@@ -91,11 +87,10 @@ async_test(function(t) {
     assert_equals(getComputedStyle(div).marginLeft, '0px',
                   'margin-left style is animated after re-starting transition');
     return animation.ready;
-  })).then(t.step_func(function() {
+  }).then(function() {
     assert_equals(animation.playState, 'running',
                   'Transition succeeds in running after being re-started');
-    t.done();
-  }));
+  });
 }, 'After cancelling a finished transition, it can still be re-used');
 
 test(function(t) {
@@ -123,6 +118,20 @@ test(function(t) {
 }, 'After cancelling a transition, updating transition properties doesn\'t make'
    + ' it live again');
 
+test(function(t) {
+  var div = addDiv(t, { style: 'margin-left: 0px' });
+  flushComputedStyle(div);
+
+  div.style.transition = 'margin-left 100s';
+  div.style.marginLeft = '1000px';
+  flushComputedStyle(div);
+
+  var animation = div.getAnimations()[0];
+  div.style.display = 'none';
+  assert_equals(animation.playState, 'idle');
+  assert_equals(getComputedStyle(div).marginLeft, '1000px');
+}, 'Setting display:none on an element cancels its transitions');
+
 done();
 </script>
 </body>
diff --git a/dom/canvas/test/webgl-mochitest/mochitest.ini b/dom/canvas/test/webgl-mochitest/mochitest.ini
index 100dc6c6df3d0bf5eb9eba1ae70fe9ec278345c2..e29b2f12fce0f7d524e15f0ae72259f738a7828e 100644
--- a/dom/canvas/test/webgl-mochitest/mochitest.ini
+++ b/dom/canvas/test/webgl-mochitest/mochitest.ini
@@ -84,7 +84,7 @@ skip-if = android_version == '18' #Android 4.3 aws only; bug 1030942
 skip-if = toolkit == 'android' #bug 865443- seperate suite - the non_conf* tests pass except for one on armv6 tests
 [test_webgl_compressed_texture_es3.html]
 [test_webgl_disjoint_timer_query.html]
-fail-if = (os == 'win' && (os_version == '6.1' || os_version == '6.2'))
+fail-if = (os == 'win' && (os_version == '6.1' || os_version == '6.2' || os_version == '10.0'))
 [test_webgl_force_enable.html]
 [test_webgl_request_context.html]
 skip-if = toolkit == 'android' #bug 865443- seperate suite - the non_conf* tests pass except for one on armv6 tests
diff --git a/dom/html/HTMLMediaElement.cpp b/dom/html/HTMLMediaElement.cpp
index 50e23c037a4435ae743adf4cc4db5ee87bfbceea..b3abd46abcb2343616aaa0c9e9cc12423a7cc82f 100644
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -4344,7 +4344,7 @@ void HTMLMediaElement::MetadataLoaded(const MediaInfo* aInfo,
   }
   if (mIsEncrypted) {
     if (!mMediaSource && Preferences::GetBool("media.eme.mse-only", true)) {
-      DecodeError();
+      DecodeError(NS_ERROR_DOM_MEDIA_FATAL_ERR);
       return;
     }
 
@@ -4419,7 +4419,7 @@ void HTMLMediaElement::NetworkError()
   Error(nsIDOMMediaError::MEDIA_ERR_NETWORK);
 }
 
-void HTMLMediaElement::DecodeError()
+void HTMLMediaElement::DecodeError(const MediaResult& aError)
 {
   nsAutoString src;
   GetCurrentSrc(src);
@@ -4443,7 +4443,7 @@ void HTMLMediaElement::DecodeError()
       NS_WARNING("Should know the source we were loading from!");
     }
   } else {
-    Error(nsIDOMMediaError::MEDIA_ERR_DECODE);
+    Error(nsIDOMMediaError::MEDIA_ERR_DECODE, aError);
   }
 }
 
@@ -4457,7 +4457,8 @@ void HTMLMediaElement::LoadAborted()
   Error(nsIDOMMediaError::MEDIA_ERR_ABORTED);
 }
 
-void HTMLMediaElement::Error(uint16_t aErrorCode)
+void HTMLMediaElement::Error(uint16_t aErrorCode,
+                             const MediaResult& aErrorDetails)
 {
   NS_ASSERTION(aErrorCode == nsIDOMMediaError::MEDIA_ERR_DECODE ||
                aErrorCode == nsIDOMMediaError::MEDIA_ERR_NETWORK ||
@@ -4470,8 +4471,12 @@ void HTMLMediaElement::Error(uint16_t aErrorCode)
   if (mError) {
     return;
   }
+  nsCString message;
+  if (NS_FAILED(aErrorDetails)) {
+    message = aErrorDetails.Description();
+  }
+  mError = new MediaError(this, aErrorCode, message);
 
-  mError = new MediaError(this, aErrorCode);
   DispatchAsyncEvent(NS_LITERAL_STRING("error"));
   if (mReadyState == nsIDOMHTMLMediaElement::HAVE_NOTHING) {
     ChangeNetworkState(nsIDOMHTMLMediaElement::NETWORK_EMPTY);
diff --git a/dom/html/HTMLMediaElement.h b/dom/html/HTMLMediaElement.h
index 3484271be3fb34bfa177dceb4c324e64e5557fee..cd673782e081c7d4b4857b2fc321f7e7fa76a805 100644
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -179,7 +179,7 @@ public:
 
   // Called by the video decoder object, on the main thread, when the
   // resource has a decode error during metadata loading or decoding.
-  virtual void DecodeError() final override;
+  virtual void DecodeError(const MediaResult& aError) final override;
 
   // Return true if error attribute is not null.
   virtual bool HasError() const final override;
@@ -1127,7 +1127,7 @@ protected:
    * Resets the media element for an error condition as per aErrorCode.
    * aErrorCode must be one of nsIDOMHTMLMediaError codes.
    */
-  void Error(uint16_t aErrorCode);
+  void Error(uint16_t aErrorCode, const MediaResult& aErrorDetails = NS_OK);
 
   /**
    * Returns the URL spec of the currentSrc.
diff --git a/dom/html/MediaError.cpp b/dom/html/MediaError.cpp
index ee11a36abce07f47c4ad0fc2d1048fdcf42009f2..f4a913a9756759d0882a331c8947b6ea8452cf2f 100644
--- a/dom/html/MediaError.cpp
+++ b/dom/html/MediaError.cpp
@@ -21,9 +21,11 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaError)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMMediaError)
 NS_INTERFACE_MAP_END
 
-MediaError::MediaError(HTMLMediaElement* aParent, uint16_t aCode)
+MediaError::MediaError(HTMLMediaElement* aParent, uint16_t aCode,
+                       const nsACString& aMessage)
   : mParent(aParent)
   , mCode(aCode)
+  , mMessage(aMessage)
 {
 }
 
@@ -35,6 +37,12 @@ NS_IMETHODIMP MediaError::GetCode(uint16_t* aCode)
   return NS_OK;
 }
 
+NS_IMETHODIMP MediaError::GetMessage(nsAString& aResult)
+{
+  CopyUTF8toUTF16(mMessage, aResult);
+  return NS_OK;
+}
+
 JSObject*
 MediaError::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
diff --git a/dom/html/MediaError.h b/dom/html/MediaError.h
index 068abc8d7795fa52b313c3a2fb757d19d5ce20f7..cebe01efdeb222384ed0198ca10ee521062589d9 100644
--- a/dom/html/MediaError.h
+++ b/dom/html/MediaError.h
@@ -22,7 +22,8 @@ class MediaError final : public nsIDOMMediaError,
   ~MediaError() {}
 
 public:
-  MediaError(HTMLMediaElement* aParent, uint16_t aCode);
+  MediaError(HTMLMediaElement* aParent, uint16_t aCode,
+             const nsACString& aMessage = nsCString());
 
   // nsISupports
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
@@ -48,6 +49,8 @@ private:
 
   // Error code
   const uint16_t mCode;
+  // Error details;
+  const nsCString mMessage;
 };
 
 } // namespace dom
diff --git a/dom/interfaces/html/nsIDOMMediaError.idl b/dom/interfaces/html/nsIDOMMediaError.idl
index 75fc386518e0f131058accf527b1d89cee6f9f7a..39488dfd6f8ba9b003a99adc11781c9b93fd1cc0 100644
--- a/dom/interfaces/html/nsIDOMMediaError.idl
+++ b/dom/interfaces/html/nsIDOMMediaError.idl
@@ -12,11 +12,11 @@ interface nsIDOMMediaError : nsISupports
      the user agent at the user's requet */
   const unsigned short MEDIA_ERR_ABORTED = 1;
 
-  /* A network error of some description caused the 
+  /* A network error of some description caused the
      user agent to stop downloading the media resource */
   const unsigned short MEDIA_ERR_NETWORK = 2;
 
-  /* An error of some description occurred while decoding 
+  /* An error of some description occurred while decoding
      the media resource */
   const unsigned short MEDIA_ERR_DECODE  = 3;
 
@@ -24,4 +24,6 @@ interface nsIDOMMediaError : nsISupports
   const unsigned short MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
 
   readonly attribute unsigned short code;
+
+  readonly attribute DOMString message;
 };
diff --git a/dom/ipc/ContentChild.cpp b/dom/ipc/ContentChild.cpp
index a4bc0e0dd5483e20c70df569af866e05812aa6af..eef4387cbf11c751f6b0bf4a1acc87f9df03763b 100644
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -1312,6 +1312,16 @@ StartMacOSContentSandbox()
     MOZ_CRASH("Failed to get NS_OS_TEMP_DIR path");
   }
 
+  nsCOMPtr<nsIFile> profileDir;
+  ContentChild::GetSingleton()->GetProfileDir(getter_AddRefs(profileDir));
+  nsCString profileDirPath;
+  if (profileDir) {
+    rv = profileDir->GetNativePath(profileDirPath);
+    if (NS_FAILED(rv) || profileDirPath.IsEmpty()) {
+      MOZ_CRASH("Failed to get profile path");
+    }
+  }
+
   MacSandboxInfo info;
   info.type = MacSandboxType_Content;
   info.level = info.level = sandboxLevel;
@@ -1320,6 +1330,13 @@ StartMacOSContentSandbox()
   info.appDir.assign(appDir.get());
   info.appTempDir.assign(tempDirPath.get());
 
+  if (profileDir) {
+    info.hasSandboxedProfile = true;
+    info.profileDir.assign(profileDirPath.get());
+  } else {
+    info.hasSandboxedProfile = false;
+  }
+
   std::string err;
   if (!mozilla::StartMacSandbox(info, err)) {
     NS_WARNING(err.c_str());
diff --git a/dom/ipc/ContentChild.h b/dom/ipc/ContentChild.h
index b86e4e1d312d497a68067aa77e6ddfdbd9fbe127..d8cef55f0d98129248f0e060a4de4dd958d1b16a 100644
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -21,6 +21,9 @@
 #include "nsWeakPtr.h"
 #include "nsIWindowProvider.h"
 
+#if defined(XP_MACOSX) && defined(MOZ_CONTENT_SANDBOX)
+#include "nsIFile.h"
+#endif
 
 struct ChromePackage;
 class nsIObserver;
@@ -114,6 +117,19 @@ public:
 
   void GetProcessName(nsACString& aName) const;
 
+#if defined(XP_MACOSX) && defined(MOZ_CONTENT_SANDBOX)
+  void GetProfileDir(nsIFile** aProfileDir) const
+  {
+    *aProfileDir = mProfileDir;
+    NS_IF_ADDREF(*aProfileDir);
+  }
+
+  void SetProfileDir(nsIFile* aProfileDir)
+  {
+    mProfileDir = aProfileDir;
+  }
+#endif
+
   bool IsAlive() const;
 
   bool IsShuttingDown() const;
@@ -681,6 +697,10 @@ private:
   nsCOMPtr<nsIDomainPolicy> mPolicy;
   nsCOMPtr<nsITimer> mForceKillTimer;
 
+#if defined(XP_MACOSX) && defined(MOZ_CONTENT_SANDBOX)
+  nsCOMPtr<nsIFile> mProfileDir;
+#endif
+
   // Hashtable to keep track of the pending GetFilesHelper objects.
   // This GetFilesHelperChild objects are removed when RecvGetFilesResponse is
   // received.
diff --git a/dom/ipc/ContentProcess.cpp b/dom/ipc/ContentProcess.cpp
index 978534fc3cfa8cf12fe2927d9511fe1a686d7b3a..66125f3322aa9e60a7987a595eac4289bdadb16c 100644
--- a/dom/ipc/ContentProcess.cpp
+++ b/dom/ipc/ContentProcess.cpp
@@ -114,6 +114,21 @@ ContentProcess::SetAppDir(const nsACString& aPath)
   mXREEmbed.SetAppDir(aPath);
 }
 
+#if defined(XP_MACOSX) && defined(MOZ_CONTENT_SANDBOX)
+void
+ContentProcess::SetProfile(const nsACString& aProfile)
+{
+  bool flag;
+  nsresult rv =
+    XRE_GetFileFromPath(aProfile.BeginReading(), getter_AddRefs(mProfileDir));
+  if (NS_FAILED(rv) ||
+      NS_FAILED(mProfileDir->Exists(&flag)) || !flag) {
+    NS_WARNING("Invalid profile directory passed to content process.");
+    mProfileDir = nullptr;
+  }
+}
+#endif
+
 bool
 ContentProcess::Init()
 {
@@ -124,6 +139,10 @@ ContentProcess::Init()
     mContent.InitXPCOM();
     mContent.InitGraphicsDeviceData();
 
+#if (defined(XP_MACOSX)) && defined(MOZ_CONTENT_SANDBOX)
+    mContent.SetProfileDir(mProfileDir);
+#endif
+
 #if (defined(XP_WIN) || defined(XP_MACOSX)) && defined(MOZ_CONTENT_SANDBOX)
     SetUpSandboxEnvironment();
 #endif
diff --git a/dom/ipc/ContentProcess.h b/dom/ipc/ContentProcess.h
index 67ee74ec8f3d8eb4def3fe3d11643a39be550613..bf9968f8cad9cba0626bf3f259b2eca67d13cd6a 100644
--- a/dom/ipc/ContentProcess.h
+++ b/dom/ipc/ContentProcess.h
@@ -39,9 +39,18 @@ public:
 
   void SetAppDir(const nsACString& aPath);
 
+#if defined(XP_MACOSX) && defined(MOZ_CONTENT_SANDBOX)
+  void SetProfile(const nsACString& aProfile);
+#endif
+
 private:
   ContentChild mContent;
   mozilla::ipc::ScopedXREEmbed mXREEmbed;
+
+#if defined(XP_MACOSX) && defined(MOZ_CONTENT_SANDBOX)
+  nsCOMPtr<nsIFile> mProfileDir;
+#endif
+
 #if defined(XP_WIN)
   // This object initializes and configures COM.
   mozilla::mscom::MainThreadRuntime mCOMRuntime;
diff --git a/dom/media/ADTSDemuxer.cpp b/dom/media/ADTSDemuxer.cpp
index 42a78e55e86ca58fcddd7c7f8a75353c40f72c4d..82075b65be46b1305e7859db48d04b1482aa3088 100644
--- a/dom/media/ADTSDemuxer.cpp
+++ b/dom/media/ADTSDemuxer.cpp
@@ -318,7 +318,7 @@ ADTSDemuxer::Init()
     ADTSLOG("Init() failure: waiting for data");
 
     return InitPromise::CreateAndReject(
-      DemuxerFailureReason::DEMUXER_ERROR, __func__);
+      NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
   }
 
   ADTSLOG("Init() successful");
@@ -515,10 +515,7 @@ ADTSTrackDemuxer::GetSamples(int32_t aNumSamples)
           aNumSamples, mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
           mSamplesPerFrame, mSamplesPerSecond, mChannels);
 
-  if (!aNumSamples) {
-    return SamplesPromise::CreateAndReject(
-      DemuxerFailureReason::DEMUXER_ERROR, __func__);
-  }
+  MOZ_ASSERT(aNumSamples);
 
   RefPtr<SamplesHolder> frames = new SamplesHolder();
 
@@ -540,7 +537,7 @@ ADTSTrackDemuxer::GetSamples(int32_t aNumSamples)
 
   if (frames->mSamples.IsEmpty()) {
     return SamplesPromise::CreateAndReject(
-      DemuxerFailureReason::END_OF_STREAM, __func__);
+      NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
   }
 
   return SamplesPromise::CreateAndResolve(frames, __func__);
@@ -562,7 +559,7 @@ ADTSTrackDemuxer::SkipToNextRandomAccessPoint(media::TimeUnit aTimeThreshold)
 {
   // Will not be called for audio-only resources.
   return SkipAccessPointPromise::CreateAndReject(
-    SkipFailureHolder(DemuxerFailureReason::DEMUXER_ERROR, 0), __func__);
+    SkipFailureHolder(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, 0), __func__);
 }
 
 int64_t
diff --git a/dom/media/AccurateSeekTask.cpp b/dom/media/AccurateSeekTask.cpp
index 4703a430bbb2e243469115b3f5d71e7a9042ec52..95b4077f55c0bb4b14be31d0898b1977f2d5d6ef 100644
--- a/dom/media/AccurateSeekTask.cpp
+++ b/dom/media/AccurateSeekTask.cpp
@@ -61,7 +61,7 @@ AccurateSeekTask::Discard()
   AssertOwnerThread();
 
   // Disconnect MDSM.
-  RejectIfExist(__func__);
+  RejectIfExist(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
 
   // Disconnect MediaDecoderReaderWrapper.
   mSeekRequest.DisconnectIfExists();
@@ -120,7 +120,7 @@ AccurateSeekTask::DropAudioUpToSeekTarget(MediaData* aSample)
 
   CheckedInt64 sampleDuration = FramesToUsecs(audio->mFrames, mAudioRate);
   if (!sampleDuration.isValid()) {
-    return NS_ERROR_FAILURE;
+    return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
   }
 
   if (audio->mTime + sampleDuration.value() <= mTarget.GetTime().ToMicroseconds()) {
@@ -154,7 +154,7 @@ AccurateSeekTask::DropAudioUpToSeekTarget(MediaData* aSample)
   CheckedInt64 framesToPrune =
     UsecsToFrames(mTarget.GetTime().ToMicroseconds() - audio->mTime, mAudioRate);
   if (!framesToPrune.isValid()) {
-    return NS_ERROR_FAILURE;
+    return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
   }
   if (framesToPrune.value() > audio->mFrames) {
     // We've messed up somehow. Don't try to trim frames, the |frames|
@@ -174,7 +174,7 @@ AccurateSeekTask::DropAudioUpToSeekTarget(MediaData* aSample)
          frames * channels * sizeof(AudioDataValue));
   CheckedInt64 duration = FramesToUsecs(frames, mAudioRate);
   if (!duration.isValid()) {
-    return NS_ERROR_FAILURE;
+    return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
   }
   RefPtr<AudioData> data(new AudioData(audio->mOffset,
                                        mTarget.GetTime().ToMicroseconds(),
@@ -260,7 +260,7 @@ AccurateSeekTask::OnSeekRejected(nsresult aResult)
 
   mSeekRequest.Complete();
   MOZ_ASSERT(NS_FAILED(aResult), "Cancels should also disconnect mSeekRequest");
-  RejectIfExist(__func__);
+  RejectIfExist(aResult, __func__);
 }
 
 void
@@ -308,10 +308,13 @@ AccurateSeekTask::OnAudioDecoded(MediaData* aAudioSample)
     // Non-precise seek; we can stop the seek at the first sample.
     mSeekedAudioData = audio;
     mDoneAudioSeeking = true;
-  } else if (NS_FAILED(DropAudioUpToSeekTarget(audio))) {
-    CancelCallbacks();
-    RejectIfExist(__func__);
-    return;
+  } else {
+    nsresult rv = DropAudioUpToSeekTarget(audio);
+    if (NS_FAILED(rv)) {
+      CancelCallbacks();
+      RejectIfExist(rv, __func__);
+      return;
+    }
   }
 
   if (!mDoneAudioSeeking) {
@@ -323,33 +326,26 @@ AccurateSeekTask::OnAudioDecoded(MediaData* aAudioSample)
 
 void
 AccurateSeekTask::OnNotDecoded(MediaData::Type aType,
-                               MediaDecoderReader::NotDecodedReason aReason)
+                               const MediaResult& aError)
 {
   AssertOwnerThread();
   MOZ_ASSERT(!mSeekTaskPromise.IsEmpty(), "Seek shouldn't be finished");
 
-  SAMPLE_LOG("OnNotDecoded type=%d reason=%u", aType, aReason);
+  SAMPLE_LOG("OnNotDecoded type=%d reason=%u", aType, aError.Code());
 
   // Ignore pending requests from video-only seek.
   if (aType == MediaData::AUDIO_DATA && mTarget.IsVideoOnly()) {
     return;
   }
 
-  if (aReason == MediaDecoderReader::DECODE_ERROR) {
-    // If this is a decode error, delegate to the generic error path.
-    CancelCallbacks();
-    RejectIfExist(__func__);
-    return;
-  }
-
   // If the decoder is waiting for data, we tell it to call us back when the
   // data arrives.
-  if (aReason == MediaDecoderReader::WAITING_FOR_DATA) {
+  if (aError == NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA) {
     mReader->WaitForData(aType);
     return;
   }
 
-  if (aReason == MediaDecoderReader::CANCELED) {
+  if (aError == NS_ERROR_DOM_MEDIA_CANCELED) {
     if (aType == MediaData::AUDIO_DATA) {
       RequestAudioData();
     } else {
@@ -358,7 +354,7 @@ AccurateSeekTask::OnNotDecoded(MediaData::Type aType,
     return;
   }
 
-  if (aReason == MediaDecoderReader::END_OF_STREAM) {
+  if (aError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
     if (aType == MediaData::AUDIO_DATA) {
       mIsAudioQueueFinished = true;
       mDoneAudioSeeking = true;
@@ -372,7 +368,12 @@ AccurateSeekTask::OnNotDecoded(MediaData::Type aType,
       }
     }
     MaybeFinishSeek();
+    return;
   }
+
+  // This is a decode error, delegate to the generic error path.
+  CancelCallbacks();
+  RejectIfExist(aError, __func__);
 }
 
 void
@@ -395,10 +396,13 @@ AccurateSeekTask::OnVideoDecoded(MediaData* aVideoSample)
     // Non-precise seek. We can stop the seek at the first sample.
     mSeekedVideoData = video;
     mDoneVideoSeeking = true;
-  } else if (NS_FAILED(DropVideoUpToSeekTarget(video.get()))) {
-    CancelCallbacks();
-    RejectIfExist(__func__);
-    return;
+  } else {
+    nsresult rv = DropVideoUpToSeekTarget(video.get());
+    if (NS_FAILED(rv)) {
+      CancelCallbacks();
+      RejectIfExist(rv, __func__);
+      return;
+    }
   }
 
   if (!mDoneVideoSeeking) {
@@ -419,7 +423,7 @@ AccurateSeekTask::SetCallbacks()
       OnAudioDecoded(aData.as<MediaData*>());
     } else {
       OnNotDecoded(MediaData::AUDIO_DATA,
-        aData.as<MediaDecoderReader::NotDecodedReason>());
+        aData.as<MediaResult>());
     }
   });
 
@@ -430,7 +434,7 @@ AccurateSeekTask::SetCallbacks()
       OnVideoDecoded(Get<0>(aData.as<Type>()));
     } else {
       OnNotDecoded(MediaData::VIDEO_DATA,
-        aData.as<MediaDecoderReader::NotDecodedReason>());
+        aData.as<MediaResult>());
     }
   });
 
diff --git a/dom/media/AccurateSeekTask.h b/dom/media/AccurateSeekTask.h
index 8272820be75c1e3cc5e2ba2f74c33ea3daed3edf..e2e6771f1d9c00a77dc6fe18b70d9a3e72af3a7b 100644
--- a/dom/media/AccurateSeekTask.h
+++ b/dom/media/AccurateSeekTask.h
@@ -50,7 +50,7 @@ private:
 
   void OnVideoDecoded(MediaData* aVideoSample);
 
-  void OnNotDecoded(MediaData::Type, MediaDecoderReader::NotDecodedReason);
+  void OnNotDecoded(MediaData::Type, const MediaResult&);
 
   void SetCallbacks();
 
diff --git a/dom/media/Benchmark.cpp b/dom/media/Benchmark.cpp
index b1d2ab265715b66e52243418fa9dd1561b8505d8..e647057b492edb0fa3fb06f989c3edf7d7451667 100644
--- a/dom/media/Benchmark.cpp
+++ b/dom/media/Benchmark.cpp
@@ -169,7 +169,7 @@ BenchmarkPlayback::DemuxSamples()
       }
       DemuxNextSample();
     },
-    [this, ref](DemuxerFailureReason aReason) { MainThreadShutdown(); });
+    [this, ref](const MediaResult& aError) { MainThreadShutdown(); });
 }
 
 void
@@ -190,9 +190,9 @@ BenchmarkPlayback::DemuxNextSample()
         Dispatch(NS_NewRunnableFunction([this, ref]() { DemuxNextSample(); }));
       }
     },
-    [this, ref](DemuxerFailureReason aReason) {
-      switch (aReason) {
-        case DemuxerFailureReason::END_OF_STREAM:
+    [this, ref](const MediaResult& aError) {
+      switch (aError.Code()) {
+        case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
           InitDecoder(Move(*mTrackDemuxer->GetInfo()));
           break;
         default:
@@ -218,7 +218,7 @@ BenchmarkPlayback::InitDecoder(TrackInfo&& aInfo)
     [this, ref](TrackInfo::TrackType aTrackType) {
       InputExhausted();
     },
-    [this, ref](MediaDataDecoder::DecoderFailureReason aReason) {
+    [this, ref](MediaResult aError) {
       MainThreadShutdown();
     });
 }
@@ -281,7 +281,7 @@ BenchmarkPlayback::Output(MediaData* aData)
 }
 
 void
-BenchmarkPlayback::Error(MediaDataDecoderError aError)
+BenchmarkPlayback::Error(const MediaResult& aError)
 {
   RefPtr<Benchmark> ref(mMainThreadState);
   Dispatch(NS_NewRunnableFunction([this, ref]() {  MainThreadShutdown(); }));
diff --git a/dom/media/Benchmark.h b/dom/media/Benchmark.h
index 44dccf3cc33f49d9b28677f7e63d1ca08a59aca4..bb14751f45c48ee291a4b1d233527e694625f4f5 100644
--- a/dom/media/Benchmark.h
+++ b/dom/media/Benchmark.h
@@ -32,7 +32,7 @@ class BenchmarkPlayback : public QueueObject, private MediaDataDecoderCallback
   // MediaDataDecoderCallback
   // Those methods are called on the MediaDataDecoder's task queue.
   void Output(MediaData* aData) override;
-  void Error(MediaDataDecoderError aError) override;
+  void Error(const MediaResult& aError) override;
   void InputExhausted() override;
   void DrainComplete() override;
   bool OnReaderTaskQueue() override;
diff --git a/dom/media/MP3Demuxer.cpp b/dom/media/MP3Demuxer.cpp
index 57aa1391dff9dfaeec9bf2b38c75fdc6ef108e36..7d478a41b1a8c3179de7006a343114338d99eb24 100644
--- a/dom/media/MP3Demuxer.cpp
+++ b/dom/media/MP3Demuxer.cpp
@@ -55,7 +55,7 @@ MP3Demuxer::Init() {
     MP3LOG("MP3Demuxer::Init() failure: waiting for data");
 
     return InitPromise::CreateAndReject(
-      DemuxerFailureReason::DEMUXER_ERROR, __func__);
+      NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
   }
 
   MP3LOG("MP3Demuxer::Init() successful");
@@ -276,7 +276,7 @@ MP3TrackDemuxer::GetSamples(int32_t aNumSamples) {
 
   if (!aNumSamples) {
     return SamplesPromise::CreateAndReject(
-        DemuxerFailureReason::DEMUXER_ERROR, __func__);
+        NS_ERROR_DOM_MEDIA_DEMUXER_ERR, __func__);
   }
 
   RefPtr<SamplesHolder> frames = new SamplesHolder();
@@ -300,7 +300,7 @@ MP3TrackDemuxer::GetSamples(int32_t aNumSamples) {
 
   if (frames->mSamples.IsEmpty()) {
     return SamplesPromise::CreateAndReject(
-        DemuxerFailureReason::END_OF_STREAM, __func__);
+        NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
   }
   return SamplesPromise::CreateAndResolve(frames, __func__);
 }
@@ -317,7 +317,7 @@ RefPtr<MP3TrackDemuxer::SkipAccessPointPromise>
 MP3TrackDemuxer::SkipToNextRandomAccessPoint(TimeUnit aTimeThreshold) {
   // Will not be called for audio-only resources.
   return SkipAccessPointPromise::CreateAndReject(
-    SkipFailureHolder(DemuxerFailureReason::DEMUXER_ERROR, 0), __func__);
+    SkipFailureHolder(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, 0), __func__);
 }
 
 int64_t
diff --git a/dom/media/MediaDataDemuxer.h b/dom/media/MediaDataDemuxer.h
index 4d22ec2b96184fe37247529d42ae2b3d141a93b3..cf2b265d58ca6947a9343881cebf1f4977080a0a 100644
--- a/dom/media/MediaDataDemuxer.h
+++ b/dom/media/MediaDataDemuxer.h
@@ -12,6 +12,7 @@
 
 #include "MediaData.h"
 #include "MediaInfo.h"
+#include "MediaResult.h"
 #include "TimeUnits.h"
 #include "nsISupportsImpl.h"
 #include "mozilla/RefPtr.h"
@@ -22,16 +23,6 @@ namespace mozilla {
 class MediaTrackDemuxer;
 class TrackMetadataHolder;
 
-enum class DemuxerFailureReason : int8_t
-{
-  WAITING_FOR_DATA,
-  END_OF_STREAM,
-  DEMUXER_ERROR,
-  CANCELED,
-  SHUTDOWN,
-};
-
-
 // Allows reading the media data: to retrieve the metadata and demux samples.
 // MediaDataDemuxer isn't designed to be thread safe.
 // When used by the MediaFormatDecoder, care is taken to ensure that the demuxer
@@ -41,7 +32,7 @@ class MediaDataDemuxer
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDataDemuxer)
 
-  typedef MozPromise<nsresult, DemuxerFailureReason, /* IsExclusive = */ true> InitPromise;
+  typedef MozPromise<nsresult, MediaResult, /* IsExclusive = */ true> InitPromise;
 
   // Initializes the demuxer. Other methods cannot be called unless
   // initialization has completed and succeeded.
@@ -120,16 +111,16 @@ public:
 
   class SkipFailureHolder {
   public:
-    SkipFailureHolder(DemuxerFailureReason aFailure, uint32_t aSkipped)
+    SkipFailureHolder(const MediaResult& aFailure, uint32_t aSkipped)
       : mFailure(aFailure)
       , mSkipped(aSkipped)
     {}
-    DemuxerFailureReason mFailure;
+    MediaResult mFailure;
     uint32_t mSkipped;
   };
 
-  typedef MozPromise<media::TimeUnit, DemuxerFailureReason, /* IsExclusive = */ true> SeekPromise;
-  typedef MozPromise<RefPtr<SamplesHolder>, DemuxerFailureReason, /* IsExclusive = */ true> SamplesPromise;
+  typedef MozPromise<media::TimeUnit, MediaResult, /* IsExclusive = */ true> SeekPromise;
+  typedef MozPromise<RefPtr<SamplesHolder>, MediaResult, /* IsExclusive = */ true> SamplesPromise;
   typedef MozPromise<uint32_t, SkipFailureHolder, /* IsExclusive = */ true> SkipAccessPointPromise;
 
   // Returns the TrackInfo (a.k.a Track Description) for this track.
diff --git a/dom/media/MediaDecoder.cpp b/dom/media/MediaDecoder.cpp
index ab2f6bab1b174a37820eea587ebb7f8fd3209a42..726df20db9eb70fc4eef6ac3cbd7c6c28bcc8aba 100644
--- a/dom/media/MediaDecoder.cpp
+++ b/dom/media/MediaDecoder.cpp
@@ -198,7 +198,7 @@ MediaDecoder::ResourceCallback::NotifyDecodeError()
   RefPtr<ResourceCallback> self = this;
   nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction([=] () {
     if (self->mDecoder) {
-      self->mDecoder->DecodeError();
+      self->mDecoder->DecodeError(NS_ERROR_DOM_MEDIA_FATAL_ERR);
     }
   });
   AbstractThread::MainThread()->Dispatch(r.forget());
@@ -607,6 +607,7 @@ MediaDecoder::Shutdown()
     mMetadataLoadedListener.Disconnect();
     mFirstFrameLoadedListener.Disconnect();
     mOnPlaybackEvent.Disconnect();
+    mOnPlaybackErrorEvent.Disconnect();
     mOnMediaNotSeekable.Disconnect();
 
     mDecoderStateMachine->BeginShutdown()
@@ -661,9 +662,6 @@ MediaDecoder::OnPlaybackEvent(MediaEventType aEvent)
     case MediaEventType::SeekStarted:
       SeekingStarted();
       break;
-    case MediaEventType::DecodeError:
-      DecodeError();
-      break;
     case MediaEventType::Invalidate:
       Invalidate();
       break;
@@ -676,6 +674,12 @@ MediaDecoder::OnPlaybackEvent(MediaEventType aEvent)
   }
 }
 
+void
+MediaDecoder::OnPlaybackErrorEvent(const MediaResult& aError)
+{
+  DecodeError(aError);
+}
+
 void
 MediaDecoder::FinishShutdown()
 {
@@ -749,6 +753,8 @@ MediaDecoder::SetStateMachineParameters()
 
   mOnPlaybackEvent = mDecoderStateMachine->OnPlaybackEvent().Connect(
     AbstractThread::MainThread(), this, &MediaDecoder::OnPlaybackEvent);
+  mOnPlaybackErrorEvent = mDecoderStateMachine->OnPlaybackErrorEvent().Connect(
+    AbstractThread::MainThread(), this, &MediaDecoder::OnPlaybackErrorEvent);
   mOnMediaNotSeekable = mDecoderStateMachine->OnMediaNotSeekable().Connect(
     AbstractThread::MainThread(), this, &MediaDecoder::OnMediaNotSeekable);
 }
@@ -1006,11 +1012,11 @@ MediaDecoder::NetworkError()
 }
 
 void
-MediaDecoder::DecodeError()
+MediaDecoder::DecodeError(const MediaResult& aError)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!IsShutdown());
-  mOwner->DecodeError();
+  mOwner->DecodeError(aError);
   MOZ_ASSERT(IsShutdown());
 }
 
diff --git a/dom/media/MediaDecoder.h b/dom/media/MediaDecoder.h
index 26063b87890237b9ef0f65ba107ea99d66fc6390..4692eba41b65da590388b6d4f22ee39c1be2b176 100644
--- a/dom/media/MediaDecoder.h
+++ b/dom/media/MediaDecoder.h
@@ -436,7 +436,7 @@ private:
   int64_t GetDownloadPosition();
 
   // Notifies the element that decoding has failed.
-  void DecodeError();
+  void DecodeError(const MediaResult& aError);
 
   // Indicate whether the media is same-origin with the element.
   void UpdateSameOriginStatus(bool aSameOrigin);
@@ -592,6 +592,7 @@ private:
   DataArrivedEvent() override { return &mDataArrivedEvent; }
 
   void OnPlaybackEvent(MediaEventType aEvent);
+  void OnPlaybackErrorEvent(const MediaResult& aError);
 
   void OnMediaNotSeekable()
   {
@@ -731,6 +732,7 @@ protected:
   MediaEventListener mFirstFrameLoadedListener;
 
   MediaEventListener mOnPlaybackEvent;
+  MediaEventListener mOnPlaybackErrorEvent;
   MediaEventListener mOnMediaNotSeekable;
 
 protected:
diff --git a/dom/media/MediaDecoderOwner.h b/dom/media/MediaDecoderOwner.h
index bcc879da599099b59a42b1bcdfab31733a74ee47..22f634665cfb4a7970e3cea9e270b0bd1a7a35ef 100644
--- a/dom/media/MediaDecoderOwner.h
+++ b/dom/media/MediaDecoderOwner.h
@@ -11,6 +11,7 @@
 namespace mozilla {
 
 class VideoFrameContainer;
+class MediaResult;
 
 namespace dom {
 class HTMLMediaElement;
@@ -67,7 +68,7 @@ public:
   // resource has a decode error during metadata loading or decoding.
   // The decoder owner should call Shutdown() on the decoder and drop the
   // reference to the decoder to prevent further calls into the decoder.
-  virtual void DecodeError() = 0;
+  virtual void DecodeError(const MediaResult& aError) = 0;
 
   // Return true if media element error attribute is not null.
   virtual bool HasError() const = 0;
diff --git a/dom/media/MediaDecoderReader.cpp b/dom/media/MediaDecoderReader.cpp
index be9b3f2ce06b7863b2d8945489f4c7437a80528b..238b461363d3399222155fd25fd473b2891e5f28 100644
--- a/dom/media/MediaDecoderReader.cpp
+++ b/dom/media/MediaDecoderReader.cpp
@@ -289,12 +289,12 @@ nsresult MediaDecoderReader::ResetDecode(TrackSet aTracks)
 {
   if (aTracks.contains(TrackInfo::kVideoTrack)) {
     VideoQueue().Reset();
-    mBaseVideoPromise.RejectIfExists(CANCELED, __func__);
+    mBaseVideoPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   }
 
   if (aTracks.contains(TrackInfo::kAudioTrack)) {
     AudioQueue().Reset();
-    mBaseAudioPromise.RejectIfExists(CANCELED, __func__);
+    mBaseAudioPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   }
 
   return NS_OK;
@@ -323,7 +323,7 @@ MediaDecoderReader::DecodeToFirstVideoData()
     p->Resolve(self->VideoQueue().PeekFront(), __func__);
   }, [p] () {
     // We don't have a way to differentiate EOS, error, and shutdown here. :-(
-    p->Reject(END_OF_STREAM, __func__);
+    p->Reject(NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
   });
 
   return p.forget();
@@ -360,8 +360,6 @@ MediaDecoderReader::GetBuffered()
 RefPtr<MediaDecoderReader::MetadataPromise>
 MediaDecoderReader::AsyncReadMetadata()
 {
-  typedef ReadMetadataFailureReason Reason;
-
   MOZ_ASSERT(OnTaskQueue());
   DECODER_LOG("MediaDecoderReader::AsyncReadMetadata");
 
@@ -374,7 +372,7 @@ MediaDecoderReader::AsyncReadMetadata()
   // error.
   if (NS_FAILED(rv) || !metadata->mInfo.HasValidMedia()) {
     DECODER_WARN("ReadMetadata failed, rv=%x HasValidMedia=%d", rv, metadata->mInfo.HasValidMedia());
-    return MetadataPromise::CreateAndReject(Reason::METADATA_ERROR, __func__);
+    return MetadataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
   }
 
   // Success!
@@ -456,7 +454,7 @@ MediaDecoderReader::RequestVideoData(bool aSkipToNextKeyframe,
     RefPtr<VideoData> v = VideoQueue().PopFront();
     mBaseVideoPromise.Resolve(v, __func__);
   } else if (VideoQueue().IsFinished()) {
-    mBaseVideoPromise.Reject(END_OF_STREAM, __func__);
+    mBaseVideoPromise.Reject(NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
   } else {
     MOZ_ASSERT(false, "Dropping this promise on the floor");
   }
@@ -488,7 +486,9 @@ MediaDecoderReader::RequestAudioData()
     RefPtr<AudioData> a = AudioQueue().PopFront();
     mBaseAudioPromise.Resolve(a, __func__);
   } else if (AudioQueue().IsFinished()) {
-    mBaseAudioPromise.Reject(mHitAudioDecodeError ? DECODE_ERROR : END_OF_STREAM, __func__);
+    mBaseAudioPromise.Reject(mHitAudioDecodeError
+                             ? NS_ERROR_DOM_MEDIA_FATAL_ERR
+                             : NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
     mHitAudioDecodeError = false;
   } else {
     MOZ_ASSERT(false, "Dropping this promise on the floor");
@@ -503,8 +503,8 @@ MediaDecoderReader::Shutdown()
   MOZ_ASSERT(OnTaskQueue());
   mShutdown = true;
 
-  mBaseAudioPromise.RejectIfExists(END_OF_STREAM, __func__);
-  mBaseVideoPromise.RejectIfExists(END_OF_STREAM, __func__);
+  mBaseAudioPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
+  mBaseVideoPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
 
   mDataArrivedListener.DisconnectIfExists();
 
diff --git a/dom/media/MediaDecoderReader.h b/dom/media/MediaDecoderReader.h
index 6e3a02a805cc40958b6200a8ed4b3f4757a2c845..681c12fb27830b926f5adac65777e896cabdaa56 100644
--- a/dom/media/MediaDecoderReader.h
+++ b/dom/media/MediaDecoderReader.h
@@ -13,6 +13,7 @@
 #include "AbstractMediaDecoder.h"
 #include "MediaInfo.h"
 #include "MediaData.h"
+#include "MediaResult.h"
 #include "MediaMetadataManager.h"
 #include "MediaQueue.h"
 #include "MediaTimer.h"
@@ -50,11 +51,6 @@ private:
   virtual ~MetadataHolder() {}
 };
 
-enum class ReadMetadataFailureReason : int8_t
-{
-  METADATA_ERROR
-};
-
 // Encapsulates the decoding and reading of media data. Reading can either
 // synchronous and done on the calling "decode" thread, or asynchronous and
 // performed on a background thread, with the result being returned by
@@ -68,19 +64,12 @@ class MediaDecoderReader {
   static const bool IsExclusive = true;
 
 public:
-  enum NotDecodedReason {
-    END_OF_STREAM,
-    DECODE_ERROR,
-    WAITING_FOR_DATA,
-    CANCELED
-  };
-
   using TrackSet = EnumSet<TrackInfo::TrackType>;
 
   using MetadataPromise =
-    MozPromise<RefPtr<MetadataHolder>, ReadMetadataFailureReason, IsExclusive>;
+    MozPromise<RefPtr<MetadataHolder>, MediaResult, IsExclusive>;
   using MediaDataPromise =
-    MozPromise<RefPtr<MediaData>, NotDecodedReason, IsExclusive>;
+    MozPromise<RefPtr<MediaData>, MediaResult, IsExclusive>;
   using SeekPromise = MozPromise<media::TimeUnit, nsresult, IsExclusive>;
 
   // Note that, conceptually, WaitForData makes sense in a non-exclusive sense.
diff --git a/dom/media/MediaDecoderReaderWrapper.cpp b/dom/media/MediaDecoderReaderWrapper.cpp
index 63379ad28bab9e78bca0dc3ef53c8e8ce8f0a1ca..455e86f4329018e74139c7be66025b6c117a1c0e 100644
--- a/dom/media/MediaDecoderReaderWrapper.cpp
+++ b/dom/media/MediaDecoderReaderWrapper.cpp
@@ -76,22 +76,22 @@ public:
         p->Resolve(data, __func__);
       },
       [p] () {
-        p->Reject(MediaDecoderReader::CANCELED, __func__);
+        p->Reject(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
       });
 
     return p.forget();
   }
 
   template<MediaData::Type SampleType>
-  void FirstSampleRejected(MediaDecoderReader::NotDecodedReason aReason)
+  void FirstSampleRejected(const MediaResult& aError)
   {
     MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
-    if (aReason == MediaDecoderReader::DECODE_ERROR) {
-      mHaveStartTimePromise.RejectIfExists(false, __func__);
-    } else if (aReason == MediaDecoderReader::END_OF_STREAM) {
+    if (aError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
       LOG("StartTimeRendezvous=%p SampleType(%d) Has no samples.",
            this, SampleType);
       MaybeSetChannelStartTime<SampleType>(INT64_MAX);
+    } else if (aError != NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA) {
+      mHaveStartTimePromise.RejectIfExists(false, __func__);
     }
   }
 
@@ -200,9 +200,9 @@ MediaDecoderReaderWrapper::RequestAudioData()
       aAudioSample->AdjustForStartTime(self->StartTime().ToMicroseconds());
       self->mAudioCallback.Notify(AsVariant(aAudioSample));
     },
-    [self] (MediaDecoderReader::NotDecodedReason aReason) {
+    [self] (const MediaResult& aError) {
       self->mAudioDataRequest.Complete();
-      self->mAudioCallback.Notify(AsVariant(aReason));
+      self->mAudioCallback.Notify(AsVariant(aError));
     }));
 }
 
@@ -240,9 +240,9 @@ MediaDecoderReaderWrapper::RequestVideoData(bool aSkipToNextKeyframe,
       aVideoSample->AdjustForStartTime(self->StartTime().ToMicroseconds());
       self->mVideoCallback.Notify(AsVariant(MakeTuple(aVideoSample, videoDecodeStartTime)));
     },
-    [self] (MediaDecoderReader::NotDecodedReason aReason) {
+    [self] (const MediaResult& aError) {
       self->mVideoDataRequest.Complete();
-      self->mVideoCallback.Notify(AsVariant(aReason));
+      self->mVideoCallback.Notify(AsVariant(aError));
     }));
 }
 
diff --git a/dom/media/MediaDecoderReaderWrapper.h b/dom/media/MediaDecoderReaderWrapper.h
index 69d5969151a149dc1c64b2ae906c47f83eb2c4be..e712689d6c10861e1282b942c3bbe6a79d6a7a77 100644
--- a/dom/media/MediaDecoderReaderWrapper.h
+++ b/dom/media/MediaDecoderReaderWrapper.h
@@ -21,8 +21,8 @@ class StartTimeRendezvous;
 
 typedef MozPromise<bool, bool, /* isExclusive = */ false> HaveStartTimePromise;
 
-typedef Variant<MediaData*, MediaDecoderReader::NotDecodedReason> AudioCallbackData;
-typedef Variant<Tuple<MediaData*, TimeStamp>, MediaDecoderReader::NotDecodedReason> VideoCallbackData;
+typedef Variant<MediaData*, MediaResult> AudioCallbackData;
+typedef Variant<Tuple<MediaData*, TimeStamp>, MediaResult> VideoCallbackData;
 typedef Variant<MediaData::Type, WaitForDataRejectValue> WaitCallbackData;
 
 /**
diff --git a/dom/media/MediaDecoderStateMachine.cpp b/dom/media/MediaDecoderStateMachine.cpp
index 7cf2d402803ae462650c4043ed0280cc6fc39afd..34a348795e4fc2835070bf6d26767df1f88544e9 100644
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -263,8 +263,8 @@ public:
         [this] (MetadataHolder* aMetadata) {
           OnMetadataRead(aMetadata);
         },
-        [this] (ReadMetadataFailureReason aReason) {
-          OnMetadataNotRead(aReason);
+        [this] (const MediaResult& aError) {
+          OnMetadataNotRead(aError);
         }));
   }
 
@@ -355,11 +355,11 @@ private:
     SetState(DECODER_STATE_DECODING_FIRSTFRAME);
   }
 
-  void OnMetadataNotRead(ReadMetadataFailureReason aReason)
+  void OnMetadataNotRead(const MediaResult& aError)
   {
     mMetadataRequest.Complete();
     SWARN("Decode metadata failed, shutting down decoder");
-    mMaster->DecodeError();
+    mMaster->DecodeError(aError);
   }
 
   MozPromiseRequestHolder<MediaDecoderReader::MetadataPromise> mMetadataRequest;
@@ -979,12 +979,12 @@ MediaDecoderStateMachine::OnVideoPopped(const RefPtr<MediaData>& aSample)
 
 void
 MediaDecoderStateMachine::OnNotDecoded(MediaData::Type aType,
-                                       MediaDecoderReader::NotDecodedReason aReason)
+                                       const MediaResult& aError)
 {
   MOZ_ASSERT(OnTaskQueue());
   MOZ_ASSERT(mState != DECODER_STATE_SEEKING);
 
-  SAMPLE_LOG("OnNotDecoded (aType=%u, aReason=%u)", aType, aReason);
+  SAMPLE_LOG("OnNotDecoded (aType=%u, aError=%u)", aType, aError.Code());
   bool isAudio = aType == MediaData::AUDIO_DATA;
   MOZ_ASSERT_IF(!isAudio, aType == MediaData::VIDEO_DATA);
 
@@ -993,15 +993,9 @@ MediaDecoderStateMachine::OnNotDecoded(MediaData::Type aType,
     return;
   }
 
-  // If this is a decode error, delegate to the generic error path.
-  if (aReason == MediaDecoderReader::DECODE_ERROR) {
-    DecodeError();
-    return;
-  }
-
   // If the decoder is waiting for data, we tell it to call us back when the
   // data arrives.
-  if (aReason == MediaDecoderReader::WAITING_FOR_DATA) {
+  if (aError == NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA) {
     MOZ_ASSERT(mReader->IsWaitForDataSupported(),
                "Readers that send WAITING_FOR_DATA need to implement WaitForData");
     mReader->WaitForData(aType);
@@ -1017,7 +1011,7 @@ MediaDecoderStateMachine::OnNotDecoded(MediaData::Type aType,
     return;
   }
 
-  if (aReason == MediaDecoderReader::CANCELED) {
+  if (aError == NS_ERROR_DOM_MEDIA_CANCELED) {
     if (isAudio) {
       EnsureAudioDecodeTaskQueued();
     } else {
@@ -1026,9 +1020,14 @@ MediaDecoderStateMachine::OnNotDecoded(MediaData::Type aType,
     return;
   }
 
+  // If this is a decode error, delegate to the generic error path.
+  if (aError != NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
+    DecodeError(aError);
+    return;
+  }
+
   // This is an EOS. Finish off the queue, and then handle things based on our
   // state.
-  MOZ_ASSERT(aReason == MediaDecoderReader::END_OF_STREAM);
   if (isAudio) {
     AudioQueue().Finish();
     StopPrerollingAudio();
@@ -1219,7 +1218,7 @@ MediaDecoderStateMachine::SetMediaDecoderReaderWrapperCallback()
     if (aData.is<MediaData*>()) {
       OnAudioDecoded(aData.as<MediaData*>());
     } else {
-      OnNotDecoded(MediaData::AUDIO_DATA, aData.as<MediaDecoderReader::NotDecodedReason>());
+      OnNotDecoded(MediaData::AUDIO_DATA, aData.as<MediaResult>());
     }
   });
 
@@ -1230,7 +1229,7 @@ MediaDecoderStateMachine::SetMediaDecoderReaderWrapperCallback()
       auto&& v = aData.as<Type>();
       OnVideoDecoded(Get<0>(v), Get<1>(v));
     } else {
-      OnNotDecoded(MediaData::VIDEO_DATA, aData.as<MediaDecoderReader::NotDecodedReason>());
+      OnNotDecoded(MediaData::VIDEO_DATA, aData.as<MediaResult>());
     }
   });
 
@@ -2083,7 +2082,7 @@ MediaDecoderStateMachine::OnSeekTaskRejected(SeekTaskRejectValue aValue)
     StopPrerollingVideo();
   }
 
-  DecodeError();
+  DecodeError(aValue.mError);
 
   DiscardSeekTaskIfExist();
 }
@@ -2292,13 +2291,13 @@ bool MediaDecoderStateMachine::HasLowUndecodedData(int64_t aUsecs)
 }
 
 void
-MediaDecoderStateMachine::DecodeError()
+MediaDecoderStateMachine::DecodeError(const MediaResult& aError)
 {
   MOZ_ASSERT(OnTaskQueue());
   MOZ_ASSERT(!IsShutdown());
   DECODER_WARN("Decode error");
   // Notify the decode error and MediaDecoder will shut down MDSM.
-  mOnPlaybackEvent.Notify(MediaEventType::DecodeError);
+  mOnPlaybackErrorEvent.Notify(aError);
 }
 
 void
@@ -2919,7 +2918,7 @@ MediaDecoderStateMachine::OnMediaSinkVideoError()
   if (HasAudio()) {
     return;
   }
-  DecodeError();
+  DecodeError(MediaResult(NS_ERROR_DOM_MEDIA_MEDIASINK_ERR, __func__));
 }
 
 void MediaDecoderStateMachine::OnMediaSinkAudioComplete()
@@ -2950,7 +2949,7 @@ void MediaDecoderStateMachine::OnMediaSinkAudioError()
 
   // Otherwise notify media decoder/element about this error for it makes
   // no sense to play an audio-only file without sound output.
-  DecodeError();
+  DecodeError(MediaResult(NS_ERROR_DOM_MEDIA_MEDIASINK_ERR, __func__));
 }
 
 #ifdef MOZ_EME
diff --git a/dom/media/MediaDecoderStateMachine.h b/dom/media/MediaDecoderStateMachine.h
index 2d40ec6615280867bbaeaad6b51f278ec1c39e22..70760ad7614f05699d89319cb79a872d47512e39 100644
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -119,7 +119,6 @@ enum class MediaEventType : int8_t {
   PlaybackStopped,
   PlaybackEnded,
   SeekStarted,
-  DecodeError,
   Invalidate,
   EnterVideoSuspend,
   ExitVideoSuspend
@@ -245,6 +244,8 @@ public:
 
   MediaEventSource<MediaEventType>&
   OnPlaybackEvent() { return mOnPlaybackEvent; }
+  MediaEventSource<MediaResult>&
+  OnPlaybackErrorEvent() { return mOnPlaybackErrorEvent; }
 
   size_t SizeOfVideoQueue() const;
 
@@ -344,7 +345,7 @@ private:
   // Need to figure out a suitable API name for this case.
   void OnAudioDecoded(MediaData* aAudioSample);
   void OnVideoDecoded(MediaData* aVideoSample, TimeStamp aDecodeStartTime);
-  void OnNotDecoded(MediaData::Type aType, MediaDecoderReader::NotDecodedReason aReason);
+  void OnNotDecoded(MediaData::Type aType, const MediaResult& aError);
 
   // Resets all state related to decoding and playback, emptying all buffers
   // and aborting all pending operations on the decode task queue.
@@ -481,7 +482,7 @@ protected:
   // event to the media element. This begins shutting down the decoder.
   // The decoder monitor must be held. This is only called on the
   // decode thread.
-  void DecodeError();
+  void DecodeError(const MediaResult& aError);
 
   // Dispatches a LoadedMetadataEvent.
   // This is threadsafe and can be called on any thread.
@@ -892,6 +893,7 @@ private:
                         MediaDecoderEventVisibility> mFirstFrameLoadedEvent;
 
   MediaEventProducer<MediaEventType> mOnPlaybackEvent;
+  MediaEventProducer<MediaResult> mOnPlaybackErrorEvent;
 
   // True if audio is offloading.
   // Playback will not start when audio is offloading.
diff --git a/dom/media/MediaFormatReader.cpp b/dom/media/MediaFormatReader.cpp
index 2a88ff90852822e7b2c7a142c4d463902de6c4c5..dda2af89babb30ff3ce4dcbeaff5c8b7654acb22 100644
--- a/dom/media/MediaFormatReader.cpp
+++ b/dom/media/MediaFormatReader.cpp
@@ -93,14 +93,14 @@ MediaFormatReader::Shutdown()
   MOZ_ASSERT(OnTaskQueue());
 
   mDemuxerInitRequest.DisconnectIfExists();
-  mMetadataPromise.RejectIfExists(ReadMetadataFailureReason::METADATA_ERROR, __func__);
-  mSeekPromise.RejectIfExists(NS_ERROR_FAILURE, __func__);
+  mMetadataPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+  mSeekPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   mSkipRequest.DisconnectIfExists();
 
   if (mAudio.mDecoder) {
     Reset(TrackInfo::kAudioTrack);
     if (mAudio.HasPromise()) {
-      mAudio.RejectPromise(CANCELED, __func__);
+      mAudio.RejectPromise(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
     }
     mAudio.ShutdownDecoder();
   }
@@ -119,7 +119,7 @@ MediaFormatReader::Shutdown()
   if (mVideo.mDecoder) {
     Reset(TrackInfo::kVideoTrack);
     if (mVideo.HasPromise()) {
-      mVideo.RejectPromise(CANCELED, __func__);
+      mVideo.RejectPromise(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
     }
     mVideo.ShutdownDecoder();
   }
@@ -283,7 +283,7 @@ MediaFormatReader::OnDemuxerInitDone(nsresult)
     // We currently only handle the first video track.
     mVideo.mTrackDemuxer = mDemuxer->GetTrackDemuxer(TrackInfo::kVideoTrack, 0);
     if (!mVideo.mTrackDemuxer) {
-      mMetadataPromise.Reject(ReadMetadataFailureReason::METADATA_ERROR, __func__);
+      mMetadataPromise.Reject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
       return;
     }
 
@@ -292,7 +292,7 @@ MediaFormatReader::OnDemuxerInitDone(nsresult)
     if (videoActive) {
       if (platform && !platform->SupportsMimeType(videoInfo->mMimeType, nullptr)) {
         // We have no decoder for this track. Error.
-        mMetadataPromise.Reject(ReadMetadataFailureReason::METADATA_ERROR, __func__);
+        mMetadataPromise.Reject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
         return;
       }
       mInfo.mVideo = *videoInfo->GetAsVideoInfo();
@@ -312,7 +312,7 @@ MediaFormatReader::OnDemuxerInitDone(nsresult)
   if (audioActive) {
     mAudio.mTrackDemuxer = mDemuxer->GetTrackDemuxer(TrackInfo::kAudioTrack, 0);
     if (!mAudio.mTrackDemuxer) {
-      mMetadataPromise.Reject(ReadMetadataFailureReason::METADATA_ERROR, __func__);
+      mMetadataPromise.Reject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
       return;
     }
 
@@ -364,7 +364,7 @@ MediaFormatReader::OnDemuxerInitDone(nsresult)
     mDemuxer->IsSeekableOnlyInBufferedRanges();
 
   if (!videoActive && !audioActive) {
-    mMetadataPromise.Reject(ReadMetadataFailureReason::METADATA_ERROR, __func__);
+    mMetadataPromise.Reject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
     return;
   }
 
@@ -376,13 +376,13 @@ MediaFormatReader::OnDemuxerInitDone(nsresult)
 }
 
 void
-MediaFormatReader::OnDemuxerInitFailed(DemuxerFailureReason aFailure)
+MediaFormatReader::OnDemuxerInitFailed(const MediaResult& aError)
 {
   mDemuxerInitRequest.Complete();
-  mMetadataPromise.Reject(ReadMetadataFailureReason::METADATA_ERROR, __func__);
+  mMetadataPromise.Reject(aError, __func__);
 }
 
-bool
+MediaResult
 MediaFormatReader::EnsureDecoderCreated(TrackType aTrack)
 {
   MOZ_ASSERT(OnTaskQueue());
@@ -391,19 +391,17 @@ MediaFormatReader::EnsureDecoderCreated(TrackType aTrack)
   auto& decoder = GetDecoderData(aTrack);
 
   if (decoder.mDecoder) {
-    return true;
+    return NS_OK;
   }
 
   if (!mPlatform) {
     mPlatform = new PDMFactory();
-    NS_ENSURE_TRUE(mPlatform, false);
     if (IsEncrypted()) {
 #ifdef MOZ_EME
       MOZ_ASSERT(mCDMProxy);
       mPlatform->SetCDMProxy(mCDMProxy);
 #else
-      // EME not supported.
-      return false;
+      return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "EME not supported");
 #endif
     }
   }
@@ -443,10 +441,10 @@ MediaFormatReader::EnsureDecoderCreated(TrackType aTrack)
   }
   if (decoder.mDecoder ) {
     decoder.mDescription = decoder.mDecoder->GetDescriptionName();
-  } else {
-    decoder.mDescription = "error creating decoder";
+    return NS_OK;
   }
-  return decoder.mDecoder != nullptr;
+  decoder.mDescription = "error creating decoder";
+  return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "error creating decoder");
 }
 
 bool
@@ -479,11 +477,11 @@ MediaFormatReader::EnsureDecoderInitialized(TrackType aTrack)
                 self->SetVideoDecodeThreshold();
                 self->ScheduleUpdate(aTrack);
               },
-              [self, aTrack] (MediaDataDecoder::DecoderFailureReason aResult) {
+              [self, aTrack] (MediaResult aError) {
                 auto& decoder = self->GetDecoderData(aTrack);
                 decoder.mInitPromise.Complete();
                 decoder.ShutdownDecoder();
-                self->NotifyError(aTrack);
+                self->NotifyError(aTrack, aError);
               }));
   return false;
 }
@@ -534,21 +532,21 @@ MediaFormatReader::RequestVideoData(bool aSkipToNextKeyframe,
 
   if (!HasVideo()) {
     LOG("called with no video track");
-    return MediaDataPromise::CreateAndReject(DECODE_ERROR, __func__);
+    return MediaDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   if (IsSeeking()) {
     LOG("called mid-seek. Rejecting.");
-    return MediaDataPromise::CreateAndReject(CANCELED, __func__);
+    return MediaDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   }
 
   if (mShutdown) {
     NS_WARNING("RequestVideoData on shutdown MediaFormatReader!");
-    return MediaDataPromise::CreateAndReject(CANCELED, __func__);
+    return MediaDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   }
 
   if (IsSuspended()) {
-    return MediaDataPromise::CreateAndReject(CANCELED, __func__);
+    return MediaDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   }
 
   media::TimeUnit timeThreshold{media::TimeUnit::FromMicroseconds(aTimeThreshold)};
@@ -568,37 +566,33 @@ MediaFormatReader::RequestVideoData(bool aSkipToNextKeyframe,
 }
 
 void
-MediaFormatReader::OnDemuxFailed(TrackType aTrack, DemuxerFailureReason aFailure)
+MediaFormatReader::OnDemuxFailed(TrackType aTrack, const MediaResult& aError)
 {
   MOZ_ASSERT(OnTaskQueue());
-  LOG("Failed to demux %s, failure:%d",
-      aTrack == TrackType::kVideoTrack ? "video" : "audio", aFailure);
+  LOG("Failed to demux %s, failure:%u",
+      aTrack == TrackType::kVideoTrack ? "video" : "audio", aError.Code());
   auto& decoder = GetDecoderData(aTrack);
   decoder.mDemuxRequest.Complete();
-  switch (aFailure) {
-    case DemuxerFailureReason::END_OF_STREAM:
+  switch (aError.Code()) {
+    case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
       if (!decoder.mWaitingForData) {
         decoder.mNeedDraining = true;
       }
       NotifyEndOfStream(aTrack);
       break;
-    case DemuxerFailureReason::DEMUXER_ERROR:
-      NotifyError(aTrack);
-      break;
-    case DemuxerFailureReason::WAITING_FOR_DATA:
+    case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
       if (!decoder.mWaitingForData) {
         decoder.mNeedDraining = true;
       }
       NotifyWaitingForData(aTrack);
       break;
-    case DemuxerFailureReason::CANCELED: MOZ_FALLTHROUGH;
-    case DemuxerFailureReason::SHUTDOWN:
+    case NS_ERROR_DOM_MEDIA_CANCELED:
       if (decoder.HasPromise()) {
-        decoder.RejectPromise(CANCELED, __func__);
+        decoder.RejectPromise(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
       }
       break;
     default:
-      MOZ_ASSERT(false);
+      NotifyError(aTrack, aError);
       break;
   }
 }
@@ -638,21 +632,21 @@ MediaFormatReader::RequestAudioData()
 
   if (!HasAudio()) {
     LOG("called with no audio track");
-    return MediaDataPromise::CreateAndReject(DECODE_ERROR, __func__);
+    return MediaDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   if (IsSuspended()) {
-    return MediaDataPromise::CreateAndReject(CANCELED, __func__);
+    return MediaDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   }
 
   if (IsSeeking()) {
     LOG("called mid-seek. Rejecting.");
-    return MediaDataPromise::CreateAndReject(CANCELED, __func__);
+    return MediaDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   }
 
   if (mShutdown) {
     NS_WARNING("RequestAudioData on shutdown MediaFormatReader!");
-    return MediaDataPromise::CreateAndReject(CANCELED, __func__);
+    return MediaDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   }
 
   RefPtr<MediaDataPromise> p = mAudio.EnsurePromise(__func__);
@@ -723,7 +717,7 @@ MediaFormatReader::NotifyDrainComplete(TrackType aTrack)
 }
 
 void
-MediaFormatReader::NotifyError(TrackType aTrack, MediaDataDecoderError aError)
+MediaFormatReader::NotifyError(TrackType aTrack, const MediaResult& aError)
 {
   MOZ_ASSERT(OnTaskQueue());
   LOGV("%s Decoding error", TrackTypeToStr(aTrack));
@@ -938,9 +932,10 @@ MediaFormatReader::HandleDemuxedSamples(TrackType aTrack,
     return;
   }
 
-  if (!EnsureDecoderCreated(aTrack)) {
+  MediaResult rv = EnsureDecoderCreated(aTrack);
+  if (NS_FAILED(rv)) {
     NS_WARNING("Error constructing decoders");
-    NotifyError(aTrack);
+    NotifyError(aTrack, rv);
     return;
   }
 
@@ -1052,24 +1047,23 @@ MediaFormatReader::InternalSeek(TrackType aTrack, const InternalSeekTarget& aTar
                       self->SetVideoDecodeThreshold();
                       self->ScheduleUpdate(aTrack);
                     },
-                    [self, aTrack] (DemuxerFailureReason aResult) {
+                    [self, aTrack] (const MediaResult& aError) {
                       auto& decoder = self->GetDecoderData(aTrack);
                       decoder.mSeekRequest.Complete();
-                      switch (aResult) {
-                        case DemuxerFailureReason::WAITING_FOR_DATA:
+                      switch (aError.Code()) {
+                        case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
                           self->NotifyWaitingForData(aTrack);
                           break;
-                        case DemuxerFailureReason::END_OF_STREAM:
+                        case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
                           decoder.mTimeThreshold.reset();
                           self->NotifyEndOfStream(aTrack);
                           break;
-                        case DemuxerFailureReason::CANCELED: MOZ_FALLTHROUGH;
-                        case DemuxerFailureReason::SHUTDOWN:
+                        case NS_ERROR_DOM_MEDIA_CANCELED:
                           decoder.mTimeThreshold.reset();
                           break;
                         default:
                           decoder.mTimeThreshold.reset();
-                          self->NotifyError(aTrack);
+                          self->NotifyError(aTrack, aError);
                           break;
                       }
                     }));
@@ -1205,7 +1199,7 @@ MediaFormatReader::Update(TrackType aTrack)
       }
     } else if (decoder.HasFatalError()) {
       LOG("Rejecting %s promise: DECODE_ERROR", TrackTypeToStr(aTrack));
-      decoder.RejectPromise(DECODE_ERROR, __func__);
+      decoder.RejectPromise(decoder.mError.ref(), __func__);
       return;
     } else if (decoder.mDrainComplete) {
       bool wasDraining = decoder.mDraining;
@@ -1213,7 +1207,7 @@ MediaFormatReader::Update(TrackType aTrack)
       decoder.mDraining = false;
       if (decoder.mDemuxEOS) {
         LOG("Rejecting %s promise: EOS", TrackTypeToStr(aTrack));
-        decoder.RejectPromise(END_OF_STREAM, __func__);
+        decoder.RejectPromise(NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
       } else if (decoder.mWaitingForData) {
         if (wasDraining && decoder.mLastSampleTime &&
             !decoder.mNextStreamSourceID) {
@@ -1226,7 +1220,7 @@ MediaFormatReader::Update(TrackType aTrack)
         }
         if (!decoder.mReceivedNewData) {
           LOG("Rejecting %s promise: WAITING_FOR_DATA", TrackTypeToStr(aTrack));
-          decoder.RejectPromise(WAITING_FOR_DATA, __func__);
+          decoder.RejectPromise(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA, __func__);
         }
       }
       // Now that draining has completed, we check if we have received
@@ -1243,7 +1237,7 @@ MediaFormatReader::Update(TrackType aTrack)
       // There is no more samples left to be decoded and we are already in
       // EOS state. We can immediately reject the data promise.
       LOG("Rejecting %s promise: EOS", TrackTypeToStr(aTrack));
-      decoder.RejectPromise(END_OF_STREAM, __func__);
+      decoder.RejectPromise(NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
     }
   }
 
@@ -1252,14 +1246,13 @@ MediaFormatReader::Update(TrackType aTrack)
     return;
   }
 
-  if (decoder.mError &&
-      decoder.mError.ref() == MediaDataDecoderError::DECODE_ERROR) {
+  if (decoder.mError && !decoder.HasFatalError()) {
     decoder.mDecodePending = false;
-    decoder.mError.reset();
     if (++decoder.mNumOfConsecutiveError > decoder.mMaxConsecutiveError) {
-      NotifyError(aTrack);
+      NotifyError(aTrack, decoder.mError.ref());
       return;
     }
+    decoder.mError.reset();
     LOG("%s decoded error count %d", TrackTypeToStr(aTrack),
                                      decoder.mNumOfConsecutiveError);
     media::TimeUnit nextKeyframe;
@@ -1398,7 +1391,7 @@ MediaFormatReader::ResetDecode(TrackSet aTracks)
     mVideo.ResetDemuxer();
     Reset(TrackInfo::kVideoTrack);
     if (mVideo.HasPromise()) {
-      mVideo.RejectPromise(CANCELED, __func__);
+      mVideo.RejectPromise(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
     }
   }
 
@@ -1406,7 +1399,7 @@ MediaFormatReader::ResetDecode(TrackSet aTracks)
     mAudio.ResetDemuxer();
     Reset(TrackInfo::kAudioTrack);
     if (mAudio.HasPromise()) {
-      mAudio.RejectPromise(CANCELED, __func__);
+      mAudio.RejectPromise(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
     }
   }
 
@@ -1418,7 +1411,7 @@ MediaFormatReader::Output(TrackType aTrack, MediaData* aSample)
 {
   if (!aSample) {
     NS_WARNING("MediaFormatReader::Output() passed a null sample");
-    Error(aTrack);
+    Error(aTrack, MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR, __func__));
     return;
   }
 
@@ -1451,10 +1444,10 @@ MediaFormatReader::InputExhausted(TrackType aTrack)
 }
 
 void
-MediaFormatReader::Error(TrackType aTrack, MediaDataDecoderError aError)
+MediaFormatReader::Error(TrackType aTrack, const MediaResult& aError)
 {
   RefPtr<nsIRunnable> task =
-    NewRunnableMethod<TrackType, MediaDataDecoderError>(
+    NewRunnableMethod<TrackType, MediaResult>(
       this, &MediaFormatReader::NotifyError, aTrack, aError);
   OwnerThread()->Dispatch(task.forget());
 }
@@ -1567,9 +1560,9 @@ MediaFormatReader::OnVideoSkipFailed(MediaTrackDemuxer::SkipFailureHolder aFailu
   LOG("Skipping failed, skipped %u frames", aFailure.mSkipped);
   mSkipRequest.Complete();
 
-  switch (aFailure.mFailure) {
-    case DemuxerFailureReason::END_OF_STREAM: MOZ_FALLTHROUGH;
-    case DemuxerFailureReason::WAITING_FOR_DATA:
+  switch (aFailure.mFailure.Code()) {
+    case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
+    case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
       // Some frames may have been output by the decoder since we initiated the
       // videoskip process and we know they would be late.
       DropDecodedSamples(TrackInfo::kVideoTrack);
@@ -1577,14 +1570,13 @@ MediaFormatReader::OnVideoSkipFailed(MediaTrackDemuxer::SkipFailureHolder aFailu
       // normally.
       ScheduleUpdate(TrackInfo::kVideoTrack);
       break;
-    case DemuxerFailureReason::CANCELED: MOZ_FALLTHROUGH;
-    case DemuxerFailureReason::SHUTDOWN:
+    case NS_ERROR_DOM_MEDIA_CANCELED:
       if (mVideo.HasPromise()) {
-        mVideo.RejectPromise(CANCELED, __func__);
+        mVideo.RejectPromise(aFailure.mFailure, __func__);
       }
       break;
     default:
-      NotifyError(TrackType::kVideoTrack);
+      NotifyError(TrackType::kVideoTrack, aFailure.mFailure);
       break;
   }
 }
@@ -1696,17 +1688,17 @@ MediaFormatReader::AttemptSeek()
 }
 
 void
-MediaFormatReader::OnSeekFailed(TrackType aTrack, DemuxerFailureReason aResult)
+MediaFormatReader::OnSeekFailed(TrackType aTrack, const MediaResult& aError)
 {
   MOZ_ASSERT(OnTaskQueue());
-  LOGV("%s failure:%d", TrackTypeToStr(aTrack), aResult);
+  LOGV("%s failure:%u", TrackTypeToStr(aTrack), aError.Code());
   if (aTrack == TrackType::kVideoTrack) {
     mVideo.mSeekRequest.Complete();
   } else {
     mAudio.mSeekRequest.Complete();
   }
 
-  if (aResult == DemuxerFailureReason::WAITING_FOR_DATA) {
+  if (aError == NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA) {
     if (HasVideo() && aTrack == TrackType::kAudioTrack &&
         mFallbackSeekTime.isSome() &&
         mPendingSeekTime.ref() != mFallbackSeekTime.ref()) {
@@ -1740,7 +1732,7 @@ MediaFormatReader::OnSeekFailed(TrackType aTrack, DemuxerFailureReason aResult)
   }
   MOZ_ASSERT(!mVideo.mSeekRequest.Exists() && !mAudio.mSeekRequest.Exists());
   mPendingSeekTime.reset();
-  mSeekPromise.Reject(NS_ERROR_FAILURE, __func__);
+  mSeekPromise.Reject(aError, __func__);
 }
 
 void
@@ -1781,10 +1773,10 @@ MediaFormatReader::OnVideoSeekCompleted(media::TimeUnit aTime)
 }
 
 void
-MediaFormatReader::OnVideoSeekFailed(DemuxerFailureReason aFailure)
+MediaFormatReader::OnVideoSeekFailed(const MediaResult& aError)
 {
   mPreviousDecodedKeyframeTime_us = sNoPreviousDecodedKeyframe;
-  OnSeekFailed(TrackType::kVideoTrack, aFailure);
+  OnSeekFailed(TrackType::kVideoTrack, aError);
 }
 
 void
@@ -1848,9 +1840,9 @@ MediaFormatReader::OnAudioSeekCompleted(media::TimeUnit aTime)
 }
 
 void
-MediaFormatReader::OnAudioSeekFailed(DemuxerFailureReason aFailure)
+MediaFormatReader::OnAudioSeekFailed(const MediaResult& aError)
 {
-  OnSeekFailed(TrackType::kAudioTrack, aFailure);
+  OnSeekFailed(TrackType::kAudioTrack, aError);
 }
 
 media::TimeIntervals
diff --git a/dom/media/MediaFormatReader.h b/dom/media/MediaFormatReader.h
index b793f62efc2576d4894945dce073da2bedf605a8..64646ef28dc4b08380fbd279bd50cf3f99b77383 100644
--- a/dom/media/MediaFormatReader.h
+++ b/dom/media/MediaFormatReader.h
@@ -117,7 +117,7 @@ private:
   void NotifyDemuxer();
   void ReturnOutput(MediaData* aData, TrackType aTrack);
 
-  bool EnsureDecoderCreated(TrackType aTrack);
+  MediaResult EnsureDecoderCreated(TrackType aTrack);
   bool EnsureDecoderInitialized(TrackType aTrack);
 
   // Enqueues a task to call Update(aTrack) on the decoder task queue.
@@ -166,7 +166,7 @@ private:
   void NotifyNewOutput(TrackType aTrack, MediaData* aSample);
   void NotifyInputExhausted(TrackType aTrack);
   void NotifyDrainComplete(TrackType aTrack);
-  void NotifyError(TrackType aTrack, MediaDataDecoderError aError = MediaDataDecoderError::FATAL_ERROR);
+  void NotifyError(TrackType aTrack, const MediaResult& aError);
   void NotifyWaitingForData(TrackType aTrack);
   void NotifyEndOfStream(TrackType aTrack);
 
@@ -179,7 +179,7 @@ private:
   // functions.
   void Output(TrackType aType, MediaData* aSample);
   void InputExhausted(TrackType aTrack);
-  void Error(TrackType aTrack, MediaDataDecoderError aError = MediaDataDecoderError::FATAL_ERROR);
+  void Error(TrackType aTrack, const MediaResult& aError);
   void Reset(TrackType aTrack);
   void DrainComplete(TrackType aTrack);
   void DropDecodedSamples(TrackType aTrack);
@@ -206,7 +206,7 @@ private:
     void InputExhausted() override {
       mReader->InputExhausted(mType);
     }
-    void Error(MediaDataDecoderError aError) override {
+    void Error(const MediaResult& aError) override {
       mReader->Error(mType, aError);
     }
     void DrainComplete() override {
@@ -327,10 +327,10 @@ private:
     uint32_t mNumOfConsecutiveError;
     uint32_t mMaxConsecutiveError;
 
-    Maybe<MediaDataDecoderError> mError;
+    Maybe<MediaResult> mError;
     bool HasFatalError() const
     {
-      return mError.isSome() && mError.ref() == MediaDataDecoderError::FATAL_ERROR;
+      return mError.isSome() && mError.ref() != NS_ERROR_DOM_MEDIA_DECODE_ERR;
     }
 
     // If set, all decoded samples prior mTimeThreshold will be dropped.
@@ -354,7 +354,7 @@ private:
     virtual bool HasPromise() const = 0;
     virtual RefPtr<MediaDataPromise> EnsurePromise(const char* aMethodName) = 0;
     virtual void ResolvePromise(MediaData* aData, const char* aMethodName) = 0;
-    virtual void RejectPromise(MediaDecoderReader::NotDecodedReason aReason,
+    virtual void RejectPromise(const MediaResult& aError,
                                const char* aMethodName) = 0;
 
     // Clear track demuxer related data.
@@ -462,11 +462,11 @@ private:
       mHasPromise = false;
     }
 
-    void RejectPromise(MediaDecoderReader::NotDecodedReason aReason,
+    void RejectPromise(const MediaResult& aError,
                        const char* aMethodName) override
     {
       MOZ_ASSERT(mOwner->OnTaskQueue());
-      mPromise.Reject(aReason, aMethodName);
+      mPromise.Reject(aError, aMethodName);
       mHasPromise = false;
     }
 
@@ -487,22 +487,22 @@ private:
   RefPtr<MediaDataDemuxer> mDemuxer;
   bool mDemuxerInitDone;
   void OnDemuxerInitDone(nsresult);
-  void OnDemuxerInitFailed(DemuxerFailureReason aFailure);
+  void OnDemuxerInitFailed(const MediaResult& aError);
   MozPromiseRequestHolder<MediaDataDemuxer::InitPromise> mDemuxerInitRequest;
-  void OnDemuxFailed(TrackType aTrack, DemuxerFailureReason aFailure);
+  void OnDemuxFailed(TrackType aTrack, const MediaResult& aError);
 
   void DoDemuxVideo();
   void OnVideoDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples);
-  void OnVideoDemuxFailed(DemuxerFailureReason aFailure)
+  void OnVideoDemuxFailed(const MediaResult& aError)
   {
-    OnDemuxFailed(TrackType::kVideoTrack, aFailure);
+    OnDemuxFailed(TrackType::kVideoTrack, aError);
   }
 
   void DoDemuxAudio();
   void OnAudioDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples);
-  void OnAudioDemuxFailed(DemuxerFailureReason aFailure)
+  void OnAudioDemuxFailed(const MediaResult& aError)
   {
-    OnDemuxFailed(TrackType::kAudioTrack, aFailure);
+    OnDemuxFailed(TrackType::kAudioTrack, aError);
   }
 
   void SkipVideoDemuxToNextKeyFrame(media::TimeUnit aTimeThreshold);
@@ -550,15 +550,15 @@ private:
   }
   void ScheduleSeek();
   void AttemptSeek();
-  void OnSeekFailed(TrackType aTrack, DemuxerFailureReason aFailure);
+  void OnSeekFailed(TrackType aTrack, const MediaResult& aError);
   void DoVideoSeek();
   void OnVideoSeekCompleted(media::TimeUnit aTime);
-  void OnVideoSeekFailed(DemuxerFailureReason aFailure);
+  void OnVideoSeekFailed(const MediaResult& aError);
   bool mSeekScheduled;
 
   void DoAudioSeek();
   void OnAudioSeekCompleted(media::TimeUnit aTime);
-  void OnAudioSeekFailed(DemuxerFailureReason aFailure);
+  void OnAudioSeekFailed(const MediaResult& aError);
   // The SeekTarget that was last given to Seek()
   SeekTarget mOriginalSeekTarget;
   // Temporary seek information while we wait for the data
diff --git a/dom/media/MediaResult.h b/dom/media/MediaResult.h
new file mode 100644
index 0000000000000000000000000000000000000000..593f0a8c553cdb9d267d578ce4b190ef4e1c216e
--- /dev/null
+++ b/dom/media/MediaResult.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 MediaResult_h_
+#define MediaResult_h_
+
+#include "nsError.h"
+#include "nsPrintfCString.h"
+
+// MediaResult can be used interchangeably with nsresult.
+// It allows to store extra information such as where the error occurred.
+// While nsresult is typically passed by value; due to its potential size, using
+// MediaResult const references is recommended.
+namespace mozilla {
+
+class MediaResult
+{
+public:
+  MOZ_IMPLICIT MediaResult(nsresult aResult)
+    : mCode(aResult)
+  {
+  }
+  MediaResult(nsresult aResult, const nsACString& aMessage)
+    : mCode(aResult)
+    , mMessage(aMessage)
+  {
+  }
+  MediaResult(nsresult aResult, const char* aMessage)
+    : mCode(aResult)
+    , mMessage(aMessage)
+  {
+  }
+  MediaResult(const MediaResult& aOther) = default;
+  MediaResult(MediaResult&& aOther) = default;
+  MediaResult& operator=(const MediaResult& aOther) = default;
+  MediaResult& operator=(MediaResult&& aOther) = default;
+
+  nsresult Code() const { return mCode; }
+  const nsCString& Message() const { return mMessage; }
+
+  // Interoperations with nsresult.
+  bool operator==(nsresult aResult) const { return aResult == mCode; }
+  bool operator!=(nsresult aResult) const { return aResult != mCode; }
+  operator nsresult () const { return mCode; }
+
+  nsCString Description() const
+  {
+    return nsPrintfCString("0x%08x: %s", mCode, mMessage.get());
+  }
+
+private:
+  nsresult mCode;
+  nsCString mMessage;
+};
+
+} // namespace mozilla
+#endif // MediaResult_h_
\ No newline at end of file
diff --git a/dom/media/NextFrameSeekTask.cpp b/dom/media/NextFrameSeekTask.cpp
index 3906493fe2bb7bed8c539cb18115d968c75602b5..d1257da727894f1c2a666051ad552eb5bec3a57f 100644
--- a/dom/media/NextFrameSeekTask.cpp
+++ b/dom/media/NextFrameSeekTask.cpp
@@ -53,7 +53,7 @@ NextFrameSeekTask::Discard()
   AssertOwnerThread();
 
   // Disconnect MDSM.
-  RejectIfExist(__func__);
+  RejectIfExist(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
 
   // Disconnect MediaDecoderReader.
   CancelCallbacks();
@@ -183,12 +183,12 @@ NextFrameSeekTask::OnAudioDecoded(MediaData* aAudioSample)
 }
 
 void
-NextFrameSeekTask::OnAudioNotDecoded(MediaDecoderReader::NotDecodedReason aReason)
+NextFrameSeekTask::OnAudioNotDecoded(const MediaResult& aError)
 {
   AssertOwnerThread();
   MOZ_ASSERT(!mSeekTaskPromise.IsEmpty(), "Seek shouldn't be finished");
 
-  SAMPLE_LOG("OnAudioNotDecoded (aReason=%u)", aReason);
+  SAMPLE_LOG("OnAudioNotDecoded (aError=%u)", aError.Code());
 
   // We don't really handle audio deocde error here. Let MDSM to trigger further
   // audio decoding tasks if it needs to play audio, and MDSM will then receive
@@ -224,37 +224,37 @@ NextFrameSeekTask::OnVideoDecoded(MediaData* aVideoSample)
 }
 
 void
-NextFrameSeekTask::OnVideoNotDecoded(MediaDecoderReader::NotDecodedReason aReason)
+NextFrameSeekTask::OnVideoNotDecoded(const MediaResult& aError)
 {
   AssertOwnerThread();
   MOZ_ASSERT(!mSeekTaskPromise.IsEmpty(), "Seek shouldn't be finished");
 
-  SAMPLE_LOG("OnVideoNotDecoded (aReason=%u)", aReason);
+  SAMPLE_LOG("OnVideoNotDecoded (aError=%u)", aError.Code());
 
-  if (aReason == MediaDecoderReader::END_OF_STREAM) {
+  if (aError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
     mIsVideoQueueFinished = true;
   }
 
   // Video seek not finished.
   if (NeedMoreVideo()) {
-    switch (aReason) {
-      case MediaDecoderReader::DECODE_ERROR:
-        // We might lose the audio sample after canceling the callbacks.
-        // However it doesn't really matter because MDSM is gonna shut down
-        // when seek fails.
-        CancelCallbacks();
-        // Reject the promise since we can't finish video seek anyway.
-        RejectIfExist(__func__);
-        break;
-      case MediaDecoderReader::WAITING_FOR_DATA:
+    switch (aError.Code()) {
+      case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
         mReader->WaitForData(MediaData::VIDEO_DATA);
         break;
-      case MediaDecoderReader::CANCELED:
+      case NS_ERROR_DOM_MEDIA_CANCELED:
         RequestVideoData();
         break;
-      case MediaDecoderReader::END_OF_STREAM:
+      case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
         MOZ_ASSERT(false, "Shouldn't want more data for ended video.");
         break;
+      default:
+        // We might lose the audio sample after canceling the callbacks.
+        // However it doesn't really matter because MDSM is gonna shut down
+        // when seek fails.
+        CancelCallbacks();
+        // Reject the promise since we can't finish video seek anyway.
+        RejectIfExist(aError, __func__);
+        break;
     }
     return;
   }
@@ -274,7 +274,7 @@ NextFrameSeekTask::SetCallbacks()
     if (aData.is<MediaData*>()) {
       OnAudioDecoded(aData.as<MediaData*>());
     } else {
-      OnAudioNotDecoded(aData.as<MediaDecoderReader::NotDecodedReason>());
+      OnAudioNotDecoded(aData.as<MediaResult>());
     }
   });
 
@@ -284,7 +284,7 @@ NextFrameSeekTask::SetCallbacks()
     if (aData.is<Type>()) {
       OnVideoDecoded(Get<0>(aData.as<Type>()));
     } else {
-      OnVideoNotDecoded(aData.as<MediaDecoderReader::NotDecodedReason>());
+      OnVideoNotDecoded(aData.as<MediaResult>());
     }
   });
 
@@ -303,7 +303,7 @@ NextFrameSeekTask::SetCallbacks()
       } else {
         // Reject if we can't finish video seeking.
         CancelCallbacks();
-        RejectIfExist(__func__);
+        RejectIfExist(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
       }
       return;
     }
diff --git a/dom/media/NextFrameSeekTask.h b/dom/media/NextFrameSeekTask.h
index 2c07d980d0535c551109d34257b143e3d1651732..debd58c6668a2eed7384fa37932d362984eb5efc 100644
--- a/dom/media/NextFrameSeekTask.h
+++ b/dom/media/NextFrameSeekTask.h
@@ -57,11 +57,11 @@ private:
 
   void OnAudioDecoded(MediaData* aAudioSample);
 
-  void OnAudioNotDecoded(MediaDecoderReader::NotDecodedReason aReason);
+  void OnAudioNotDecoded(const MediaResult& aError);
 
   void OnVideoDecoded(MediaData* aVideoSample);
 
-  void OnVideoNotDecoded(MediaDecoderReader::NotDecodedReason aReason);
+  void OnVideoNotDecoded(const MediaResult& aError);
 
   void SetCallbacks();
 
diff --git a/dom/media/SeekTask.cpp b/dom/media/SeekTask.cpp
index cafcc38f4dc936690fd21a6185a00b01d505ecc0..ee93033da1de0d75f24f5320bc59a1be8a2285cb 100644
--- a/dom/media/SeekTask.cpp
+++ b/dom/media/SeekTask.cpp
@@ -46,13 +46,14 @@ SeekTask::Resolve(const char* aCallSite)
 }
 
 void
-SeekTask::RejectIfExist(const char* aCallSite)
+SeekTask::RejectIfExist(const MediaResult& aError, const char* aCallSite)
 {
   AssertOwnerThread();
 
   SeekTaskRejectValue val;
   val.mIsAudioQueueFinished = mIsAudioQueueFinished;
   val.mIsVideoQueueFinished = mIsVideoQueueFinished;
+  val.mError = aError;
 
   mSeekTaskPromise.RejectIfExists(val, aCallSite);
 }
diff --git a/dom/media/SeekTask.h b/dom/media/SeekTask.h
index 8e9500b357f51482233077244c1866de21b35358..e93e4bae37c112833e55262cafb43d60578c7d5f 100644
--- a/dom/media/SeekTask.h
+++ b/dom/media/SeekTask.h
@@ -8,6 +8,7 @@
 #define SEEK_TASK_H
 
 #include "mozilla/MozPromise.h"
+#include "MediaResult.h"
 #include "SeekTarget.h"
 
 namespace mozilla {
@@ -30,8 +31,15 @@ struct SeekTaskResolveValue
 
 struct SeekTaskRejectValue
 {
+  SeekTaskRejectValue()
+    : mIsAudioQueueFinished(false)
+    , mIsVideoQueueFinished(false)
+    , mError(NS_ERROR_DOM_MEDIA_FATAL_ERR)
+  {
+  }
   bool mIsAudioQueueFinished;
   bool mIsVideoQueueFinished;
+  MediaResult mError;
 };
 
 class SeekTask {
@@ -62,7 +70,7 @@ protected:
 
   void Resolve(const char* aCallSite);
 
-  void RejectIfExist(const char* aCallSite);
+  void RejectIfExist(const MediaResult& aError, const char* aCallSite);
 
   void AssertOwnerThread() const;
 
diff --git a/dom/media/eme/MediaKeys.cpp b/dom/media/eme/MediaKeys.cpp
index 37bf89a10fed72c8d91bc488402d26a0179ca61e..e136850907da97af35f763b1f0ddc952f5e65a66 100644
--- a/dom/media/eme/MediaKeys.cpp
+++ b/dom/media/eme/MediaKeys.cpp
@@ -88,7 +88,7 @@ MediaKeys::Terminated()
 
   // Notify the element about that CDM has terminated.
   if (mElement) {
-    mElement->DecodeError();
+    mElement->DecodeError(NS_ERROR_DOM_MEDIA_CDM_ERR);
   }
 
   Shutdown();
diff --git a/dom/media/flac/FlacDemuxer.cpp b/dom/media/flac/FlacDemuxer.cpp
index 6f84ad39da26883985142689ba889f864f10e8b4..63b1f6c0553da53dd39a7972f8073da409dd9689 100644
--- a/dom/media/flac/FlacDemuxer.cpp
+++ b/dom/media/flac/FlacDemuxer.cpp
@@ -584,7 +584,7 @@ FlacDemuxer::Init()
     LOG("Init() failure: waiting for data");
 
     return InitPromise::CreateAndReject(
-      DemuxerFailureReason::DEMUXER_ERROR, __func__);
+      NS_ERROR_DOM_MEDIA_DEMUXER_ERR, __func__);
   }
 
   LOG("Init() successful");
@@ -855,7 +855,7 @@ FlacTrackDemuxer::GetSamples(int32_t aNumSamples)
 
   if (!aNumSamples) {
     return SamplesPromise::CreateAndReject(
-      DemuxerFailureReason::DEMUXER_ERROR, __func__);
+      NS_ERROR_DOM_MEDIA_DEMUXER_ERR, __func__);
   }
 
   RefPtr<SamplesHolder> frames = new SamplesHolder();
@@ -875,7 +875,7 @@ FlacTrackDemuxer::GetSamples(int32_t aNumSamples)
 
   if (frames->mSamples.IsEmpty()) {
     return SamplesPromise::CreateAndReject(
-      DemuxerFailureReason::END_OF_STREAM, __func__);
+      NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
   }
 
   return SamplesPromise::CreateAndResolve(frames, __func__);
@@ -899,7 +899,7 @@ FlacTrackDemuxer::SkipToNextRandomAccessPoint(TimeUnit aTimeThreshold)
 {
   // Will not be called for audio-only resources.
   return SkipAccessPointPromise::CreateAndReject(
-    SkipFailureHolder(DemuxerFailureReason::DEMUXER_ERROR, 0), __func__);
+    SkipFailureHolder(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, 0), __func__);
 }
 
 int64_t
diff --git a/dom/media/fmp4/MP4Decoder.cpp b/dom/media/fmp4/MP4Decoder.cpp
index adcaabdc1b21a4f34b847044d53a908681689f87..c617ccfec91baeed604fe1edc77c886be8d669e4 100644
--- a/dom/media/fmp4/MP4Decoder.cpp
+++ b/dom/media/fmp4/MP4Decoder.cpp
@@ -280,7 +280,7 @@ MP4Decoder::IsVideoAccelerated(layers::LayersBackend aBackend, nsIGlobalObject*
              taskQueue->AwaitShutdownAndIdle();
              promise->MaybeResolve(result);
            },
-           [promise, decoder, taskQueue] (MediaDataDecoder::DecoderFailureReason aResult) {
+           [promise, decoder, taskQueue] (MediaResult aError) {
              decoder->Shutdown();
              taskQueue->BeginShutdown();
              taskQueue->AwaitShutdownAndIdle();
diff --git a/dom/media/fmp4/MP4Demuxer.cpp b/dom/media/fmp4/MP4Demuxer.cpp
index 48a9968aa294f268c8a8a2d430da45f9e09e2b7b..4548258de03151b2192a5c96c6540e98a806671c 100644
--- a/dom/media/fmp4/MP4Demuxer.cpp
+++ b/dom/media/fmp4/MP4Demuxer.cpp
@@ -125,13 +125,13 @@ MP4Demuxer::Init()
 
   // Check that we have enough data to read the metadata.
   if (!mp4_demuxer::MP4Metadata::HasCompleteMetadata(stream)) {
-    return InitPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, __func__);
   }
 
   mInitData = mp4_demuxer::MP4Metadata::Metadata(stream);
   if (!mInitData) {
     // OOM
-    return InitPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, __func__);
   }
 
   RefPtr<mp4_demuxer::BufferStream> bufferstream =
@@ -141,7 +141,7 @@ MP4Demuxer::Init()
 
   if (!mMetadata->GetNumberTracks(mozilla::TrackInfo::kAudioTrack) &&
       !mMetadata->GetNumberTracks(mozilla::TrackInfo::kVideoTrack)) {
-    return InitPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, __func__);
   }
 
   return InitPromise::CreateAndResolve(NS_OK, __func__);
@@ -298,7 +298,7 @@ MP4TrackDemuxer::Seek(media::TimeUnit aTime)
   do {
     sample = GetNextSample();
     if (!sample) {
-      return SeekPromise::CreateAndReject(DemuxerFailureReason::END_OF_STREAM, __func__);
+      return SeekPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
     }
     if (!sample->Size()) {
       // This sample can't be decoded, continue searching.
@@ -370,7 +370,7 @@ MP4TrackDemuxer::GetSamples(int32_t aNumSamples)
   EnsureUpToDateIndex();
   RefPtr<SamplesHolder> samples = new SamplesHolder;
   if (!aNumSamples) {
-    return SamplesPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__);
+    return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, __func__);
   }
 
   if (mQueuedSample) {
@@ -390,7 +390,7 @@ MP4TrackDemuxer::GetSamples(int32_t aNumSamples)
   }
 
   if (samples->mSamples.IsEmpty()) {
-    return SamplesPromise::CreateAndReject(DemuxerFailureReason::END_OF_STREAM, __func__);
+    return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
   } else {
     for (const auto& sample : samples->mSamples) {
       // Collect telemetry from h264 Annex B SPS.
@@ -461,7 +461,7 @@ MP4TrackDemuxer::SkipToNextRandomAccessPoint(media::TimeUnit aTimeThreshold)
   if (found) {
     return SkipAccessPointPromise::CreateAndResolve(parsed, __func__);
   } else {
-    SkipFailureHolder failure(DemuxerFailureReason::END_OF_STREAM, parsed);
+    SkipFailureHolder failure(NS_ERROR_DOM_MEDIA_END_OF_STREAM, parsed);
     return SkipAccessPointPromise::CreateAndReject(Move(failure), __func__);
   }
 }
diff --git a/dom/media/gtest/MockMediaDecoderOwner.h b/dom/media/gtest/MockMediaDecoderOwner.h
index 9990963de566ba956736468a7c95920509d0cf2e..81acc20a26bf87e36d853a1c117de45c3ece2215 100644
--- a/dom/media/gtest/MockMediaDecoderOwner.h
+++ b/dom/media/gtest/MockMediaDecoderOwner.h
@@ -25,7 +25,7 @@ public:
   {
   }
   void NetworkError() override {}
-  void DecodeError() override {}
+  void DecodeError(const MediaResult& aError) override {}
   bool HasError() const override { return false; }
   void LoadAborted() override {}
   void PlaybackEnded() override {}
diff --git a/dom/media/gtest/TestMP4Demuxer.cpp b/dom/media/gtest/TestMP4Demuxer.cpp
index 7fbcf940391f88c51633656eeb4dedfefac22ab8..fcfb4eacfd4342c98c49202470afa19f9cbd285e 100644
--- a/dom/media/gtest/TestMP4Demuxer.cpp
+++ b/dom/media/gtest/TestMP4Demuxer.cpp
@@ -119,11 +119,8 @@ public:
               binding->CheckTrackSamples(track);
             }
           },
-          [binding] (DemuxerFailureReason aReason) {
-            if (aReason == DemuxerFailureReason::DEMUXER_ERROR) {
-              EXPECT_TRUE(false);
-              binding->mCheckTrackSamples.Reject(NS_ERROR_FAILURE, __func__);
-            } else if (aReason == DemuxerFailureReason::END_OF_STREAM) {
+          [binding] (const MediaResult& aError) {
+            if (aError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
               EXPECT_TRUE(binding->mSamples.Length() > 1);
               for (uint32_t i = 0; i < (binding->mSamples.Length() - 1); i++) {
                 EXPECT_LT(binding->mSamples[i]->mTimecode, binding->mSamples[i + 1]->mTimecode);
@@ -132,6 +129,9 @@ public:
                 }
               }
               binding->mCheckTrackSamples.Resolve(true, __func__);
+            } else {
+              EXPECT_TRUE(false);
+              binding->mCheckTrackSamples.Reject(aError, __func__);
             }
           }
         );
diff --git a/dom/media/gtest/TestMediaFormatReader.cpp b/dom/media/gtest/TestMediaFormatReader.cpp
index b4325fa45b1d22487a738ff38cdaadb7dff73ca7..ad222e8b807405b7f6fa09edffc345e40ccd74e4 100644
--- a/dom/media/gtest/TestMediaFormatReader.cpp
+++ b/dom/media/gtest/TestMediaFormatReader.cpp
@@ -88,7 +88,7 @@ public:
              &MediaFormatReaderBinding::OnNotDemuxed);
   }
 
-  void OnMetadataNotRead(ReadMetadataFailureReason aReason) {
+  void OnMetadataNotRead(const MediaResult& aError) {
     EXPECT_TRUE(false);
     ReaderShutdown();
   }
@@ -107,7 +107,7 @@ public:
     ReaderShutdown();
   }
 
-  void OnNotDemuxed(MediaDecoderReader::NotDecodedReason aReason)
+  void OnNotDemuxed(const MediaResult& aReason)
   {
     EXPECT_TRUE(false);
     ReaderShutdown();
diff --git a/dom/media/mediasource/MediaSource.cpp b/dom/media/mediasource/MediaSource.cpp
index 7fd66b43d7abdeb80aef5d9e5fd9195bc22d0be3..85a0850fe94d1c74f83f9db1cb7df3d8e23c3d92 100644
--- a/dom/media/mediasource/MediaSource.cpp
+++ b/dom/media/mediasource/MediaSource.cpp
@@ -10,6 +10,7 @@
 #include "DecoderTraits.h"
 #include "Benchmark.h"
 #include "DecoderDoctorDiagnostics.h"
+#include "MediaResult.h"
 #include "MediaSourceUtils.h"
 #include "SourceBuffer.h"
 #include "SourceBufferList.h"
@@ -330,13 +331,24 @@ MediaSource::EndOfStream(const Optional<MediaSourceEndOfStreamError>& aError, Er
     mDecoder->NetworkError();
     break;
   case MediaSourceEndOfStreamError::Decode:
-    mDecoder->DecodeError();
+    mDecoder->DecodeError(NS_ERROR_DOM_MEDIA_FATAL_ERR);
     break;
   default:
     aRv.Throw(NS_ERROR_DOM_TYPE_ERR);
   }
 }
 
+void
+MediaSource::EndOfStream(const MediaResult& aError)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MSE_API("EndOfStream(aError=%d)", aError.Code());
+
+  SetReadyState(MediaSourceReadyState::Ended);
+  mSourceBuffers->Ended();
+  mDecoder->DecodeError(aError);
+}
+
 /* static */ bool
 MediaSource::IsTypeSupported(const GlobalObject& aOwner, const nsAString& aType)
 {
diff --git a/dom/media/mediasource/MediaSource.h b/dom/media/mediasource/MediaSource.h
index 867e955f90eaeffd3111735703263b11823ccafa..0d2dc0588866159393fffec33f939cdeb9a3a6a6 100644
--- a/dom/media/mediasource/MediaSource.h
+++ b/dom/media/mediasource/MediaSource.h
@@ -29,6 +29,7 @@ namespace mozilla {
 
 class ErrorResult;
 template <typename T> class AsyncEventRunner;
+class MediaResult;
 
 namespace dom {
 
@@ -60,6 +61,7 @@ public:
   void RemoveSourceBuffer(SourceBuffer& aSourceBuffer, ErrorResult& aRv);
 
   void EndOfStream(const Optional<MediaSourceEndOfStreamError>& aError, ErrorResult& aRv);
+  void EndOfStream(const MediaResult& aError);
 
   void SetLiveSeekableRange(double aStart, double aEnd, ErrorResult& aRv);
   void ClearLiveSeekableRange(ErrorResult& aRv);
diff --git a/dom/media/mediasource/MediaSourceDemuxer.cpp b/dom/media/mediasource/MediaSourceDemuxer.cpp
index 241a9aaa854bc023f822d738f9417cbfdf1de2a7..385f1bc2a36b6743a62a6623a4912b2e53c891d5 100644
--- a/dom/media/mediasource/MediaSourceDemuxer.cpp
+++ b/dom/media/mediasource/MediaSourceDemuxer.cpp
@@ -247,7 +247,7 @@ MediaSourceDemuxer::GetManager(TrackType aTrack)
 
 MediaSourceDemuxer::~MediaSourceDemuxer()
 {
-  mInitPromise.RejectIfExists(DemuxerFailureReason::SHUTDOWN, __func__);
+  mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
 }
 
 void
@@ -379,8 +379,6 @@ MediaSourceTrackDemuxer::BreakCycles()
 RefPtr<MediaSourceTrackDemuxer::SeekPromise>
 MediaSourceTrackDemuxer::DoSeek(media::TimeUnit aTime)
 {
-  typedef TrackBuffersManager::GetSampleResult Result;
-
   TimeIntervals buffered = mManager->Buffered(mType);
   // Fuzz factor represents a +/- threshold. So when seeking it allows the gap
   // to be twice as big as the fuzz value. We only want to allow EOS_FUZZ gap.
@@ -397,7 +395,7 @@ MediaSourceTrackDemuxer::DoSeek(media::TimeUnit aTime)
   if (!buffered.ContainsWithStrictEnd(seekTime)) {
     if (!buffered.ContainsWithStrictEnd(aTime)) {
       // We don't have the data to seek to.
-      return SeekPromise::CreateAndReject(DemuxerFailureReason::WAITING_FOR_DATA,
+      return SeekPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA,
                                           __func__);
     }
     // Theoretically we should reject the promise with WAITING_FOR_DATA,
@@ -410,12 +408,12 @@ MediaSourceTrackDemuxer::DoSeek(media::TimeUnit aTime)
     seekTime = buffered[index].mStart;
   }
   seekTime = mManager->Seek(mType, seekTime, MediaSourceDemuxer::EOS_FUZZ);
-  Result result;
+  MediaResult result = NS_OK;
   RefPtr<MediaRawData> sample =
     mManager->GetSample(mType,
                         media::TimeUnit(),
                         result);
-  MOZ_ASSERT(result != Result::ERROR && sample);
+  MOZ_ASSERT(NS_SUCCEEDED(result) && sample);
   mNextSample = Some(sample);
   mReset = false;
   {
@@ -429,8 +427,6 @@ MediaSourceTrackDemuxer::DoSeek(media::TimeUnit aTime)
 RefPtr<MediaSourceTrackDemuxer::SamplesPromise>
 MediaSourceTrackDemuxer::DoGetSamples(int32_t aNumSamples)
 {
-  typedef TrackBuffersManager::GetSampleResult Result;
-
   if (mReset) {
     // If a seek (or reset) was recently performed, we ensure that the data
     // we are about to retrieve is still available.
@@ -438,11 +434,11 @@ MediaSourceTrackDemuxer::DoGetSamples(int32_t aNumSamples)
     buffered.SetFuzz(MediaSourceDemuxer::EOS_FUZZ / 2);
 
     if (!buffered.Length() && mManager->IsEnded()) {
-      return SamplesPromise::CreateAndReject(DemuxerFailureReason::END_OF_STREAM,
+      return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
                                              __func__);
     }
     if (!buffered.ContainsWithStrictEnd(TimeUnit::FromMicroseconds(0))) {
-      return SamplesPromise::CreateAndReject(DemuxerFailureReason::WAITING_FOR_DATA,
+      return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA,
                                              __func__);
     }
     mReset = false;
@@ -452,16 +448,17 @@ MediaSourceTrackDemuxer::DoGetSamples(int32_t aNumSamples)
     sample = mNextSample.ref();
     mNextSample.reset();
   } else {
-    Result result;
+    MediaResult result = NS_OK;
     sample = mManager->GetSample(mType, MediaSourceDemuxer::EOS_FUZZ, result);
     if (!sample) {
-      if (result == Result::ERROR) {
-        return SamplesPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__);
+      if (result == NS_ERROR_DOM_MEDIA_END_OF_STREAM ||
+          result == NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA) {
+        return SamplesPromise::CreateAndReject(
+          (result == NS_ERROR_DOM_MEDIA_END_OF_STREAM && mManager->IsEnded())
+          ? NS_ERROR_DOM_MEDIA_END_OF_STREAM
+          : NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA, __func__);
       }
-      return SamplesPromise::CreateAndReject(
-        (result == Result::EOS && mManager->IsEnded())
-        ? DemuxerFailureReason::END_OF_STREAM
-        : DemuxerFailureReason::WAITING_FOR_DATA, __func__);
+      return SamplesPromise::CreateAndReject(result, __func__);
     }
   }
   RefPtr<SamplesHolder> samples = new SamplesHolder;
@@ -492,8 +489,8 @@ MediaSourceTrackDemuxer::DoSkipToNextRandomAccessPoint(media::TimeUnit aTimeThre
     }
   }
   SkipFailureHolder holder(
-    mManager->IsEnded() ? DemuxerFailureReason::END_OF_STREAM :
-                          DemuxerFailureReason::WAITING_FOR_DATA, parsed);
+    mManager->IsEnded() ? NS_ERROR_DOM_MEDIA_END_OF_STREAM :
+                          NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA, parsed);
   return SkipAccessPointPromise::CreateAndReject(holder, __func__);
 }
 
diff --git a/dom/media/mediasource/MediaSourceDemuxer.h b/dom/media/mediasource/MediaSourceDemuxer.h
index 529a64c8e62a9604af28d4404ade1eb129fb8971..02c91e3bc4fb1f675ef080e57dc1311a39205284 100644
--- a/dom/media/mediasource/MediaSourceDemuxer.h
+++ b/dom/media/mediasource/MediaSourceDemuxer.h
@@ -20,6 +20,7 @@
 
 namespace mozilla {
 
+class MediaResult;
 class MediaSourceTrackDemuxer;
 
 class MediaSourceDemuxer : public MediaDataDemuxer
@@ -119,7 +120,7 @@ private:
   RefPtr<SeekPromise> DoSeek(media::TimeUnit aTime);
   RefPtr<SamplesPromise> DoGetSamples(int32_t aNumSamples);
   RefPtr<SkipAccessPointPromise> DoSkipToNextRandomAccessPoint(media::TimeUnit aTimeThreadshold);
-  already_AddRefed<MediaRawData> GetSample(DemuxerFailureReason& aFailure);
+  already_AddRefed<MediaRawData> GetSample(MediaResult& aError);
   // Return the timestamp of the next keyframe after mLastSampleIndex.
   media::TimeUnit GetNextRandomAccessPoint();
 
diff --git a/dom/media/mediasource/SourceBuffer.cpp b/dom/media/mediasource/SourceBuffer.cpp
index 8ed787fe40643389b3cc3801ca9b206011561424..6412f478892e2152deae03fac2e08a6cafde7410 100644
--- a/dom/media/mediasource/SourceBuffer.cpp
+++ b/dom/media/mediasource/SourceBuffer.cpp
@@ -445,24 +445,24 @@ SourceBuffer::AppendDataCompletedWithSuccess(SourceBufferTask::AppendBufferResul
 }
 
 void
-SourceBuffer::AppendDataErrored(nsresult aError)
+SourceBuffer::AppendDataErrored(const MediaResult& aError)
 {
   MOZ_ASSERT(mUpdating);
   mPendingAppend.Complete();
 
-  switch (aError) {
-    case NS_ERROR_ABORT:
+  switch (aError.Code()) {
+    case NS_ERROR_DOM_MEDIA_CANCELED:
       // Nothing further to do as the trackbuffer has been shutdown.
       // or append was aborted and abort() has handled all the events.
       break;
     default:
-      AppendError(true);
+      AppendError(aError);
       break;
   }
 }
 
 void
-SourceBuffer::AppendError(bool aDecoderError)
+SourceBuffer::AppendError(const MediaResult& aDecodeError)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
@@ -473,12 +473,9 @@ SourceBuffer::AppendError(bool aDecoderError)
   QueueAsyncSimpleEvent("error");
   QueueAsyncSimpleEvent("updateend");
 
-  if (aDecoderError) {
-    Optional<MediaSourceEndOfStreamError> decodeError(
-      MediaSourceEndOfStreamError::Decode);
-    ErrorResult dummy;
-    mMediaSource->EndOfStream(decodeError, dummy);
-  }
+  MOZ_ASSERT(NS_FAILED(aDecodeError));
+
+  mMediaSource->EndOfStream(aDecodeError);
 }
 
 already_AddRefed<MediaByteBuffer>
diff --git a/dom/media/mediasource/SourceBuffer.h b/dom/media/mediasource/SourceBuffer.h
index 2567e53942ed23301282d8749e3ee463f12edf41..440e8f60e5e6c943c9ba5decab47ebc7f6ad9f80 100644
--- a/dom/media/mediasource/SourceBuffer.h
+++ b/dom/media/mediasource/SourceBuffer.h
@@ -155,7 +155,7 @@ private:
   // Will call endOfStream() with "decode" error if aDecodeError is true.
   // 3.5.3 Append Error Algorithm
   // http://w3c.github.io/media-source/#sourcebuffer-append-error
-  void AppendError(bool aDecoderError);
+  void AppendError(const MediaResult& aDecodeError);
 
   // Implements the "Prepare Append Algorithm". Returns MediaByteBuffer object
   // on success or nullptr (with aRv set) on error.
@@ -164,7 +164,7 @@ private:
                                                   ErrorResult& aRv);
 
   void AppendDataCompletedWithSuccess(SourceBufferTask::AppendBufferResult aResult);
-  void AppendDataErrored(nsresult aError);
+  void AppendDataErrored(const MediaResult& aError);
 
   RefPtr<MediaSource> mMediaSource;
 
diff --git a/dom/media/mediasource/SourceBufferTask.h b/dom/media/mediasource/SourceBufferTask.h
index 2ba8ea0ef627693a36211371705735ebe2b1a750..868b608590f5327247fa86fb7df536fb72493f59 100644
--- a/dom/media/mediasource/SourceBufferTask.h
+++ b/dom/media/mediasource/SourceBufferTask.h
@@ -12,6 +12,7 @@
 #include "mozilla/RefPtr.h"
 #include "SourceBufferAttributes.h"
 #include "TimeUnits.h"
+#include "MediaResult.h"
 
 namespace mozilla {
 
@@ -28,7 +29,7 @@ public:
   };
 
   typedef Pair<bool, SourceBufferAttributes> AppendBufferResult;
-  typedef MozPromise<AppendBufferResult, nsresult, /* IsExclusive = */ true> AppendPromise;
+  typedef MozPromise<AppendBufferResult, MediaResult, /* IsExclusive = */ true> AppendPromise;
   typedef MozPromise<bool, nsresult, /* IsExclusive = */ true> RangeRemovalPromise;
 
   virtual Type GetType() const = 0;
diff --git a/dom/media/mediasource/TrackBuffersManager.cpp b/dom/media/mediasource/TrackBuffersManager.cpp
index 4ab95e3e2625f46502f22743abf33f5015f2ad03..219a279a13e5a36bbf710551f163e8d10783356f 100644
--- a/dom/media/mediasource/TrackBuffersManager.cpp
+++ b/dom/media/mediasource/TrackBuffersManager.cpp
@@ -287,17 +287,21 @@ TrackBuffersManager::EvictData(const TimeUnit& aPlaybackTime, int64_t aSize)
     return EvictDataResult::CANT_EVICT;
   }
 
+  EvictDataResult result;
+
   if (mBufferFull && mEvictionState == EvictionState::EVICTION_COMPLETED) {
-    return EvictDataResult::BUFFER_FULL;
+    // Our buffer is currently full. We will make another eviction attempt.
+    // However, the current appendBuffer will fail as we can't know ahead of
+    // time if the eviction will later succeed.
+    result = EvictDataResult::BUFFER_FULL;
+  } else {
+    mEvictionState = EvictionState::EVICTION_NEEDED;
+    result = EvictDataResult::NO_DATA_EVICTED;
   }
-
-  MSE_DEBUG("Reaching our size limit, schedule eviction of %lld bytes", toEvict);
-
-  mEvictionState = EvictionState::EVICTION_NEEDED;
-
+  MSE_DEBUG("Reached our size limit, schedule eviction of %lld bytes", toEvict);
   QueueTask(new EvictDataTask(aPlaybackTime, toEvict));
 
-  return EvictDataResult::NO_DATA_EVICTED;
+  return result;
 }
 
 TimeIntervals
@@ -717,7 +721,7 @@ TrackBuffersManager::SegmentParserLoop()
                      self->ScheduleSegmentParserLoop();
                    }
                  },
-                 [self] (nsresult aRejectValue) {
+                 [self] (const MediaResult& aRejectValue) {
                    self->mProcessingRequest.Complete();
                    self->RejectAppend(aRejectValue, __func__);
                  }));
@@ -743,9 +747,9 @@ TrackBuffersManager::NeedMoreData()
 }
 
 void
-TrackBuffersManager::RejectAppend(nsresult aRejectValue, const char* aName)
+TrackBuffersManager::RejectAppend(const MediaResult& aRejectValue, const char* aName)
 {
-  MSE_DEBUG("rv=%d", aRejectValue);
+  MSE_DEBUG("rv=%u", aRejectValue.Code());
   MOZ_DIAGNOSTIC_ASSERT(mCurrentTask && mCurrentTask->GetType() == SourceBufferTask::Type::AppendBuffer);
 
   mCurrentTask->As<AppendBufferTask>()->mPromise.Reject(aRejectValue, __func__);
@@ -1095,12 +1099,12 @@ TrackBuffersManager::OnDemuxerInitDone(nsresult)
 }
 
 void
-TrackBuffersManager::OnDemuxerInitFailed(DemuxerFailureReason aFailure)
+TrackBuffersManager::OnDemuxerInitFailed(const MediaResult& aError)
 {
-  MOZ_ASSERT(aFailure != DemuxerFailureReason::WAITING_FOR_DATA);
+  MOZ_ASSERT(aError != NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA);
   mDemuxerInitRequest.Complete();
 
-  RejectAppend(NS_ERROR_FAILURE, __func__);
+  RejectAppend(aError, __func__);
 }
 
 RefPtr<TrackBuffersManager::CodedFrameProcessingPromise>
@@ -1149,29 +1153,22 @@ TrackBuffersManager::CodedFrameProcessing()
 
 void
 TrackBuffersManager::OnDemuxFailed(TrackType aTrack,
-                                   DemuxerFailureReason aFailure)
+                                   const MediaResult& aError)
 {
   MOZ_ASSERT(OnTaskQueue());
-  MSE_DEBUG("Failed to demux %s, failure:%d",
-            aTrack == TrackType::kVideoTrack ? "video" : "audio", aFailure);
-  switch (aFailure) {
-    case DemuxerFailureReason::END_OF_STREAM:
-    case DemuxerFailureReason::WAITING_FOR_DATA:
+  MSE_DEBUG("Failed to demux %s, failure:%u",
+            aTrack == TrackType::kVideoTrack ? "video" : "audio", aError.Code());
+  switch (aError.Code()) {
+    case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
+    case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
       if (aTrack == TrackType::kVideoTrack) {
         DoDemuxAudio();
       } else {
         CompleteCodedFrameProcessing();
       }
       break;
-    case DemuxerFailureReason::DEMUXER_ERROR:
-      RejectProcessing(NS_ERROR_FAILURE, __func__);
-      break;
-    case DemuxerFailureReason::CANCELED:
-    case DemuxerFailureReason::SHUTDOWN:
-      RejectProcessing(NS_ERROR_ABORT, __func__);
-      break;
     default:
-      MOZ_ASSERT(false);
+      RejectProcessing(aError, __func__);
       break;
   }
 }
@@ -1323,7 +1320,7 @@ TrackBuffersManager::CompleteCodedFrameProcessing()
 }
 
 void
-TrackBuffersManager::RejectProcessing(nsresult aRejectValue, const char* aName)
+TrackBuffersManager::RejectProcessing(const MediaResult& aRejectValue, const char* aName)
 {
   mProcessingPromise.RejectIfExists(aRejectValue, __func__);
 }
@@ -2157,16 +2154,16 @@ TrackBuffersManager::GetSample(TrackInfo::TrackType aTrack,
 already_AddRefed<MediaRawData>
 TrackBuffersManager::GetSample(TrackInfo::TrackType aTrack,
                                const TimeUnit& aFuzz,
-                               GetSampleResult& aResult)
+                               MediaResult& aResult)
 {
   MOZ_ASSERT(OnTaskQueue());
   auto& trackData = GetTracksData(aTrack);
   const TrackBuffer& track = GetTrackBuffer(aTrack);
 
-  aResult = GetSampleResult::WAITING_FOR_DATA;
+  aResult = NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA;
 
   if (!track.Length()) {
-    aResult = GetSampleResult::EOS;
+    aResult = NS_ERROR_DOM_MEDIA_END_OF_STREAM;
     return nullptr;
   }
 
@@ -2178,7 +2175,7 @@ TrackBuffersManager::GetSample(TrackInfo::TrackType aTrack,
 
   if (trackData.mNextGetSampleIndex.isSome()) {
     if (trackData.mNextGetSampleIndex.ref() >= track.Length()) {
-      aResult = GetSampleResult::EOS;
+      aResult = NS_ERROR_DOM_MEDIA_END_OF_STREAM;
       return nullptr;
     }
     const MediaRawData* sample =
@@ -2193,7 +2190,7 @@ TrackBuffersManager::GetSample(TrackInfo::TrackType aTrack,
 
     RefPtr<MediaRawData> p = sample->Clone();
     if (!p) {
-      aResult = GetSampleResult::ERROR;
+      aResult = MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
       return nullptr;
     }
     trackData.mNextGetSampleIndex.ref()++;
@@ -2219,7 +2216,7 @@ TrackBuffersManager::GetSample(TrackInfo::TrackType aTrack,
       trackData.mNextSampleTimecode = nextSampleTimecode;
       trackData.mNextSampleTime = nextSampleTime;
     }
-    aResult = GetSampleResult::NO_ERROR;
+    aResult = NS_OK;
     return p.forget();
   }
 
@@ -2227,7 +2224,7 @@ TrackBuffersManager::GetSample(TrackInfo::TrackType aTrack,
       track.LastElement()->mTimecode + track.LastElement()->mDuration) {
     // The next element is past our last sample. We're done.
     trackData.mNextGetSampleIndex = Some(uint32_t(track.Length()));
-    aResult = GetSampleResult::EOS;
+    aResult = NS_ERROR_DOM_MEDIA_END_OF_STREAM;
     return nullptr;
   }
 
@@ -2244,7 +2241,7 @@ TrackBuffersManager::GetSample(TrackInfo::TrackType aTrack,
   RefPtr<MediaRawData> p = sample->Clone();
   if (!p) {
     // OOM
-    aResult = GetSampleResult::ERROR;
+    aResult = MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
     return nullptr;
   }
   trackData.mNextGetSampleIndex = Some(uint32_t(pos)+1);
@@ -2252,7 +2249,7 @@ TrackBuffersManager::GetSample(TrackInfo::TrackType aTrack,
     TimeUnit::FromMicroseconds(sample->mTimecode + sample->mDuration);
   trackData.mNextSampleTime =
     TimeUnit::FromMicroseconds(sample->GetEndTime());
-  aResult = GetSampleResult::NO_ERROR;
+  aResult = NS_OK;
   return p.forget();
 }
 
diff --git a/dom/media/mediasource/TrackBuffersManager.h b/dom/media/mediasource/TrackBuffersManager.h
index ffea285a6289ab2182de0446e0284a442fa2ab2c..0bec46a47052f54d46e8e9b65937600e78f51ecc 100644
--- a/dom/media/mediasource/TrackBuffersManager.h
+++ b/dom/media/mediasource/TrackBuffersManager.h
@@ -15,6 +15,7 @@
 
 #include "MediaData.h"
 #include "MediaDataDemuxer.h"
+#include "MediaResult.h"
 #include "MediaSourceDecoder.h"
 #include "SourceBufferTask.h"
 #include "TimeUnits.h"
@@ -154,17 +155,9 @@ public:
                                        const media::TimeUnit& aFuzz,
                                        bool& aFound);
 
-  enum class GetSampleResult
-  {
-    NO_ERROR,
-    ERROR,
-    WAITING_FOR_DATA,
-    EOS
-  };
-
   already_AddRefed<MediaRawData> GetSample(TrackInfo::TrackType aTrack,
                                            const media::TimeUnit& aFuzz,
-                                           GetSampleResult& aResult);
+                                           MediaResult& aResult);
   int32_t FindCurrentPosition(TrackInfo::TrackType aTrack,
                               const media::TimeUnit& aFuzz);
   media::TimeUnit GetNextRandomAccessPoint(TrackInfo::TrackType aTrack,
@@ -173,7 +166,7 @@ public:
   void AddSizeOfResources(MediaSourceDecoder::ResourceSizes* aSizes);
 
 private:
-  typedef MozPromise<bool, nsresult, /* IsExclusive = */ true> CodedFrameProcessingPromise;
+  typedef MozPromise<bool, MediaResult, /* IsExclusive = */ true> CodedFrameProcessingPromise;
 
   // for MediaSourceDemuxer::GetMozDebugReaderData
   friend class MediaSourceDemuxer;
@@ -188,7 +181,7 @@ private:
   void CreateDemuxerforMIMEType();
   void ResetDemuxingState();
   void NeedMoreData();
-  void RejectAppend(nsresult aRejectValue, const char* aName);
+  void RejectAppend(const MediaResult& aRejectValue, const char* aName);
   // Will return a promise that will be resolved once all frames of the current
   // media segment have been processed.
   RefPtr<CodedFrameProcessingPromise> CodedFrameProcessing();
@@ -243,25 +236,25 @@ private:
   Maybe<media::TimeUnit> mLastParsedEndTime;
 
   void OnDemuxerInitDone(nsresult);
-  void OnDemuxerInitFailed(DemuxerFailureReason aFailure);
+  void OnDemuxerInitFailed(const MediaResult& aFailure);
   void OnDemuxerResetDone(nsresult);
   MozPromiseRequestHolder<MediaDataDemuxer::InitPromise> mDemuxerInitRequest;
   bool mEncrypted;
 
-  void OnDemuxFailed(TrackType aTrack, DemuxerFailureReason aFailure);
+  void OnDemuxFailed(TrackType aTrack, const MediaResult& aError);
   void DoDemuxVideo();
   void OnVideoDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples);
-  void OnVideoDemuxFailed(DemuxerFailureReason aFailure)
+  void OnVideoDemuxFailed(const MediaResult& aError)
   {
     mVideoTracks.mDemuxRequest.Complete();
-    OnDemuxFailed(TrackType::kVideoTrack, aFailure);
+    OnDemuxFailed(TrackType::kVideoTrack, aError);
   }
   void DoDemuxAudio();
   void OnAudioDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples);
-  void OnAudioDemuxFailed(DemuxerFailureReason aFailure)
+  void OnAudioDemuxFailed(const MediaResult& aError)
   {
     mAudioTracks.mDemuxRequest.Complete();
-    OnDemuxFailed(TrackType::kAudioTrack, aFailure);
+    OnDemuxFailed(TrackType::kAudioTrack, aError);
   }
 
   void DoEvictData(const media::TimeUnit& aPlaybackTime, int64_t aSizeToEvict);
@@ -379,7 +372,7 @@ private:
                                 const media::TimeUnit& aExpectedPts,
                                 const media::TimeUnit& aFuzz);
   void UpdateBufferedRanges();
-  void RejectProcessing(nsresult aRejectValue, const char* aName);
+  void RejectProcessing(const MediaResult& aRejectValue, const char* aName);
   void ResolveProcessing(bool aResolveValue, const char* aName);
   MozPromiseRequestHolder<CodedFrameProcessingPromise> mProcessingRequest;
   MozPromiseHolder<CodedFrameProcessingPromise> mProcessingPromise;
diff --git a/dom/media/moz.build b/dom/media/moz.build
index 52eee2e8d0dabd8731d6322e163f71c58f49db0b..626ed72f7362a1a9071122932d4c9c3691754a10 100644
--- a/dom/media/moz.build
+++ b/dom/media/moz.build
@@ -121,6 +121,7 @@ EXPORTS += [
     'MediaRecorder.h',
     'MediaResource.h',
     'MediaResourceCallback.h',
+    'MediaResult.h',
     'MediaSegment.h',
     'MediaStatistics.h',
     'MediaStreamGraph.h',
diff --git a/dom/media/ogg/OggDemuxer.cpp b/dom/media/ogg/OggDemuxer.cpp
index 0afcb8ec432e6c86e76db37e42454254e3ecc486..a1608360c2ceab8a31e6da433a7922068b4c8d92 100644
--- a/dom/media/ogg/OggDemuxer.cpp
+++ b/dom/media/ogg/OggDemuxer.cpp
@@ -215,24 +215,19 @@ OggDemuxer::Init()
 {
   int ret = ogg_sync_init(OggSyncState(TrackInfo::kAudioTrack));
   if (ret != 0) {
-    return InitPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
   }
   ret = ogg_sync_init(OggSyncState(TrackInfo::kVideoTrack));
   if (ret != 0) {
-    return InitPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
   }
-  /*
-  if (InitBufferedState() != NS_OK) {
-    return InitPromise::CreateAndReject(DemuxerFailureReason::WAITING_FOR_DATA, __func__);
-  }
-  */
   if (ReadMetadata() != NS_OK) {
-    return InitPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
   }
 
   if (!GetNumberTracks(TrackInfo::kAudioTrack) &&
       !GetNumberTracks(TrackInfo::kVideoTrack)) {
-    return InitPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
   }
 
   return InitPromise::CreateAndResolve(NS_OK, __func__);
@@ -1483,7 +1478,7 @@ OggTrackDemuxer::Seek(TimeUnit aTime)
 
     return SeekPromise::CreateAndResolve(seekTime, __func__);
   } else {
-    return SeekPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__);
+    return SeekPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, __func__);
   }
 }
 
@@ -1516,7 +1511,7 @@ OggTrackDemuxer::GetSamples(int32_t aNumSamples)
 {
   RefPtr<SamplesHolder> samples = new SamplesHolder;
   if (!aNumSamples) {
-    return SamplesPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__);
+    return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, __func__);
   }
 
   while (aNumSamples) {
@@ -1529,7 +1524,7 @@ OggTrackDemuxer::GetSamples(int32_t aNumSamples)
   }
 
   if (samples->mSamples.IsEmpty()) {
-    return SamplesPromise::CreateAndReject(DemuxerFailureReason::END_OF_STREAM, __func__);
+    return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
   } else {
     return SamplesPromise::CreateAndResolve(samples, __func__);
   }
@@ -1563,7 +1558,7 @@ OggTrackDemuxer::SkipToNextRandomAccessPoint(TimeUnit aTimeThreshold)
                parsed);
     return SkipAccessPointPromise::CreateAndResolve(parsed, __func__);
   } else {
-    SkipFailureHolder failure(DemuxerFailureReason::END_OF_STREAM, parsed);
+    SkipFailureHolder failure(NS_ERROR_DOM_MEDIA_END_OF_STREAM, parsed);
     return SkipAccessPointPromise::CreateAndReject(Move(failure), __func__);
   }
 }
diff --git a/dom/media/omx/MediaOmxReader.cpp b/dom/media/omx/MediaOmxReader.cpp
index 411801770a2c1c4b9044dc8abea32f5214d346ee..3d57ea6f7a943dc4d05e676021df8c94b240cdab 100644
--- a/dom/media/omx/MediaOmxReader.cpp
+++ b/dom/media/omx/MediaOmxReader.cpp
@@ -175,7 +175,7 @@ MediaOmxReader::Shutdown()
 void MediaOmxReader::ReleaseResources()
 {
   mMediaResourceRequest.DisconnectIfExists();
-  mMetadataPromise.RejectIfExists(ReadMetadataFailureReason::METADATA_ERROR, __func__);
+  mMetadataPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
 
   ResetDecode();
   // Before freeing a video codec, all video buffers needed to be released
@@ -221,7 +221,7 @@ MediaOmxReader::AsyncReadMetadata()
   nsresult rv = InitOmxDecoder();
   if (NS_FAILED(rv)) {
     return MediaDecoderReader::MetadataPromise::CreateAndReject(
-             ReadMetadataFailureReason::METADATA_ERROR, __func__);
+             NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
   }
 
   bool isMP3 = mDecoder->GetResource()->GetContentType().EqualsASCII(AUDIO_MP3);
@@ -243,7 +243,7 @@ MediaOmxReader::AsyncReadMetadata()
         self->HandleResourceAllocated();
       }, [self] (bool) -> void {
         self->mMediaResourceRequest.Complete();
-        self->mMetadataPromise.Reject(ReadMetadataFailureReason::METADATA_ERROR, __func__);
+        self->mMetadataPromise.Reject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
       }));
 
   return p;
@@ -255,7 +255,7 @@ void MediaOmxReader::HandleResourceAllocated()
 
   // After resources are available, set the metadata.
   if (!mOmxDecoder->EnsureMetadata()) {
-    mMetadataPromise.Reject(ReadMetadataFailureReason::METADATA_ERROR, __func__);
+    mMetadataPromise.Reject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
     return;
   }
 
@@ -289,7 +289,7 @@ void MediaOmxReader::HandleResourceAllocated()
     nsIntSize displaySize(displayWidth, displayHeight);
     nsIntSize frameSize(width, height);
     if (!IsValidVideoRegion(frameSize, pictureRect, displaySize)) {
-      mMetadataPromise.Reject(ReadMetadataFailureReason::METADATA_ERROR, __func__);
+      mMetadataPromise.Reject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
       return;
     }
 
diff --git a/dom/media/platforms/PDMFactory.cpp b/dom/media/platforms/PDMFactory.cpp
index 14f29f2eaf1b3869af8af32022e1a343f31b106c..5bd783cb74af1fcd2843f53ee0941da719da2523 100644
--- a/dom/media/platforms/PDMFactory.cpp
+++ b/dom/media/platforms/PDMFactory.cpp
@@ -267,7 +267,7 @@ PDMFactory::CreateDecoderWithPDM(PlatformDecoderModule* aPDM,
   CreateDecoderParams params = aParams;
   params.mCallback = callback;
 
-  if (MP4Decoder::IsH264(config.mMimeType)) {
+  if (MP4Decoder::IsH264(config.mMimeType) && !aParams.mUseBlankDecoder) {
     RefPtr<H264Converter> h = new H264Converter(aPDM, params);
     const nsresult rv = h->GetLastError();
     if (NS_SUCCEEDED(rv) || rv == NS_ERROR_NOT_INITIALIZED) {
diff --git a/dom/media/platforms/PlatformDecoderModule.h b/dom/media/platforms/PlatformDecoderModule.h
index fd79d980ba0d8d42eb10f87f03c63b79bd5f3aee..7d94f94e10d44390f348cc37d19deb5adda361bb 100644
--- a/dom/media/platforms/PlatformDecoderModule.h
+++ b/dom/media/platforms/PlatformDecoderModule.h
@@ -14,6 +14,7 @@
 #include "mozilla/RefPtr.h"
 #include "GMPService.h"
 #include <queue>
+#include "MediaResult.h"
 
 namespace mozilla {
 class TrackInfo;
@@ -154,11 +155,6 @@ protected:
   CreateAudioDecoder(const CreateDecoderParams& aParams) = 0;
 };
 
-enum class MediaDataDecoderError : uint8_t{
-  FATAL_ERROR,
-  DECODE_ERROR
-};
-
 // A callback used by MediaDataDecoder to return output/errors to the
 // MediaFormatReader.
 // Implementation is threadsafe, and can be called on any thread.
@@ -171,7 +167,7 @@ public:
 
   // Denotes an error in the decoding process. The reader will stop calling
   // the decoder.
-  virtual void Error(MediaDataDecoderError aError) = 0;
+  virtual void Error(const MediaResult& aError) = 0;
 
   // Denotes that the last input sample has been inserted into the decoder,
   // and no more output can be produced unless more input is sent.
@@ -219,13 +215,8 @@ protected:
   virtual ~MediaDataDecoder() {};
 
 public:
-  enum class DecoderFailureReason : uint8_t {
-    INIT_ERROR,
-    CANCELED
-  };
-
   typedef TrackInfo::TrackType TrackType;
-  typedef MozPromise<TrackType, DecoderFailureReason, /* IsExclusive = */ true> InitPromise;
+  typedef MozPromise<TrackType, MediaResult, /* IsExclusive = */ true> InitPromise;
 
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDataDecoder)
 
diff --git a/dom/media/platforms/agnostic/BlankDecoderModule.cpp b/dom/media/platforms/agnostic/BlankDecoderModule.cpp
index 049c35f8511bd6bd4416ff6084bda39257a4d129..7609e377de90472d861391c78b573572a42b98c2 100644
--- a/dom/media/platforms/agnostic/BlankDecoderModule.cpp
+++ b/dom/media/platforms/agnostic/BlankDecoderModule.cpp
@@ -11,6 +11,7 @@
 #include "mozilla/mozalloc.h" // for operator new, and new (fallible)
 #include "mozilla/RefPtr.h"
 #include "mozilla/TaskQueue.h"
+#include "mp4_demuxer/AnnexB.h"
 #include "mp4_demuxer/H264.h"
 #include "MP4Decoder.h"
 #include "nsAutoPtr.h"
@@ -34,7 +35,9 @@ public:
     , mCallback(aParams.mCallback)
     , mMaxRefFrames(aParams.mConfig.GetType() == TrackInfo::kVideoTrack &&
                     MP4Decoder::IsH264(aParams.mConfig.mMimeType)
-                    ? mp4_demuxer::H264::ComputeMaxRefFrames(aParams.VideoConfig().mExtraData)
+                    ? mp4_demuxer::AnnexB::HasSPS(aParams.VideoConfig().mExtraData)
+                      ? mp4_demuxer::H264::ComputeMaxRefFrames(aParams.VideoConfig().mExtraData)
+                      : 16
                     : 0)
     , mType(aParams.mConfig.GetType())
   {
@@ -79,7 +82,7 @@ private:
   void OutputFrame(MediaData* aData)
   {
     if (!aData) {
-      mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+      mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__));
       return;
     }
 
diff --git a/dom/media/platforms/agnostic/OpusDecoder.cpp b/dom/media/platforms/agnostic/OpusDecoder.cpp
index 59ad296c8ab94e1a8485b11bd8c1b33346d7e8b8..190ed787446be9e66a00a8a5f435f0b002212be7 100644
--- a/dom/media/platforms/agnostic/OpusDecoder.cpp
+++ b/dom/media/platforms/agnostic/OpusDecoder.cpp
@@ -61,14 +61,14 @@ OpusDataDecoder::Init()
   uint8_t *p = mInfo.mCodecSpecificConfig->Elements();
   if (length < sizeof(uint64_t)) {
     OPUS_DEBUG("CodecSpecificConfig too short to read codecDelay!");
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
   int64_t codecDelay = BigEndian::readUint64(p);
   length -= sizeof(uint64_t);
   p += sizeof(uint64_t);
   if (NS_FAILED(DecodeHeader(p, length))) {
     OPUS_DEBUG("Error decoding header!");
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   int r;
@@ -84,7 +84,7 @@ OpusDataDecoder::Init()
   if (codecDelay != FramesToUsecs(mOpusParser->mPreSkip,
                                   mOpusParser->mRate).value()) {
     NS_WARNING("Invalid Opus header: CodecDelay and pre-skip do not match!");
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   if (mInfo.mRate != (uint32_t)mOpusParser->mRate) {
@@ -95,7 +95,7 @@ OpusDataDecoder::Init()
   }
 
   return r == OPUS_OK ? InitPromise::CreateAndResolve(TrackInfo::kAudioTrack, __func__)
-                      : InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+                      : InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
 }
 
 nsresult
@@ -151,21 +151,15 @@ OpusDataDecoder::ProcessDecode(MediaRawData* aSample)
     return;
   }
 
-  DecodeError err = DoDecode(aSample);
-  switch (err) {
-    case DecodeError::FATAL_ERROR:
-      mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
-      return;
-    case DecodeError::DECODE_ERROR:
-      mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
-      break;
-    case DecodeError::DECODE_SUCCESS:
-      mCallback->InputExhausted();
-      break;
+  MediaResult rv = DoDecode(aSample);
+  if (NS_FAILED(rv)) {
+    mCallback->Error(rv);
+    return;
   }
+  mCallback->InputExhausted();
 }
 
-OpusDataDecoder::DecodeError
+MediaResult
 OpusDataDecoder::DoDecode(MediaRawData* aSample)
 {
   int64_t aDiscardPadding = 0;
@@ -178,7 +172,7 @@ OpusDataDecoder::DoDecode(MediaRawData* aSample)
     // Discard padding should be used only on the final packet, so
     // decoding after a padding discard is invalid.
     OPUS_DEBUG("Opus error, discard padding on interstitial packet");
-    return FATAL_ERROR;
+    return NS_ERROR_DOM_MEDIA_FATAL_ERR;
   }
 
   if (!mLastFrameTime || mLastFrameTime.ref() != aSample->mTime) {
@@ -193,7 +187,7 @@ OpusDataDecoder::DoDecode(MediaRawData* aSample)
   if (frames_number <= 0) {
     OPUS_DEBUG("Invalid packet header: r=%ld length=%ld",
                frames_number, aSample->Size());
-    return FATAL_ERROR;
+    return NS_ERROR_DOM_MEDIA_DECODE_ERR;
   }
 
   int32_t samples = opus_packet_get_samples_per_frame(aSample->Data(),
@@ -204,12 +198,12 @@ OpusDataDecoder::DoDecode(MediaRawData* aSample)
   int32_t frames = frames_number*samples;
   if (frames < 120 || frames > 5760) {
     OPUS_DEBUG("Invalid packet frames: %ld", frames);
-    return FATAL_ERROR;
+    return NS_ERROR_DOM_MEDIA_DECODE_ERR;
   }
 
   AlignedAudioBuffer buffer(frames * channels);
   if (!buffer) {
-    return FATAL_ERROR;
+    return MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
   }
 
   // Decode to the appropriate sample type.
@@ -223,7 +217,7 @@ OpusDataDecoder::DoDecode(MediaRawData* aSample)
                                     buffer.get(), frames, false);
 #endif
   if (ret < 0) {
-    return DECODE_ERROR;
+    return NS_ERROR_DOM_MEDIA_DECODE_ERR;
   }
   NS_ASSERTION(ret == frames, "Opus decoded too few audio samples");
   CheckedInt64 startTime = aSample->mTime;
@@ -244,7 +238,7 @@ OpusDataDecoder::DoDecode(MediaRawData* aSample)
   if (aDiscardPadding < 0) {
     // Negative discard padding is invalid.
     OPUS_DEBUG("Opus error, negative discard padding");
-    return FATAL_ERROR;
+    return NS_ERROR_DOM_MEDIA_FATAL_ERR;
   }
   if (aDiscardPadding > 0) {
     OPUS_DEBUG("OpusDecoder discardpadding %" PRId64 "", aDiscardPadding);
@@ -253,12 +247,12 @@ OpusDataDecoder::DoDecode(MediaRawData* aSample)
                        mOpusParser->mRate);
     if (!discardFrames.isValid()) {
       NS_WARNING("Int overflow in DiscardPadding");
-      return FATAL_ERROR;
+      return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
     }
     if (discardFrames.value() > frames) {
       // Discarding more than the entire packet is invalid.
       OPUS_DEBUG("Opus error, discard padding larger than packet");
-      return FATAL_ERROR;
+      return NS_ERROR_DOM_MEDIA_FATAL_ERR;
     }
     OPUS_DEBUG("Opus decoder discarding %d of %d frames",
         int32_t(discardFrames.value()), frames);
@@ -293,14 +287,14 @@ OpusDataDecoder::DoDecode(MediaRawData* aSample)
   CheckedInt64 duration = FramesToUsecs(frames, mOpusParser->mRate);
   if (!duration.isValid()) {
     NS_WARNING("OpusDataDecoder: Int overflow converting WebM audio duration");
-    return FATAL_ERROR;
+    return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
   }
   CheckedInt64 time =
     startTime - FramesToUsecs(mOpusParser->mPreSkip, mOpusParser->mRate) +
     FramesToUsecs(mFrames, mOpusParser->mRate);
   if (!time.isValid()) {
     NS_WARNING("OpusDataDecoder: Int overflow shifting tstamp by codec delay");
-    return FATAL_ERROR;
+    return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
   };
 
   mCallback->Output(new AudioData(aSample->mOffset,
@@ -311,7 +305,7 @@ OpusDataDecoder::DoDecode(MediaRawData* aSample)
                                   mOpusParser->mChannels,
                                   mOpusParser->mRate));
   mFrames += frames;
-  return DECODE_SUCCESS;
+  return NS_OK;
 }
 
 void
diff --git a/dom/media/platforms/agnostic/OpusDecoder.h b/dom/media/platforms/agnostic/OpusDecoder.h
index 4e8acb6e10773f65cfb2016c0cbdeae90f97b39e..04f9860afac23a02adfe5594427e224cd8767125 100644
--- a/dom/media/platforms/agnostic/OpusDecoder.h
+++ b/dom/media/platforms/agnostic/OpusDecoder.h
@@ -41,16 +41,10 @@ public:
   static void AppendCodecDelay(MediaByteBuffer* config, uint64_t codecDelayUS);
 
 private:
-  enum DecodeError {
-    DECODE_SUCCESS,
-    DECODE_ERROR,
-    FATAL_ERROR
-  };
-
   nsresult DecodeHeader(const unsigned char* aData, size_t aLength);
 
   void ProcessDecode(MediaRawData* aSample);
-  DecodeError DoDecode(MediaRawData* aSample);
+  MediaResult DoDecode(MediaRawData* aSample);
   void ProcessDrain();
 
   const AudioInfo& mInfo;
diff --git a/dom/media/platforms/agnostic/TheoraDecoder.cpp b/dom/media/platforms/agnostic/TheoraDecoder.cpp
index 375db566e4fa13df793373c2fcffac65ea18f251..76644ba66ef7af55c189b040a959b11580efc765 100644
--- a/dom/media/platforms/agnostic/TheoraDecoder.cpp
+++ b/dom/media/platforms/agnostic/TheoraDecoder.cpp
@@ -78,22 +78,22 @@ TheoraDecoder::Init()
   if (!XiphExtradataToHeaders(headers, headerLens,
       mInfo.mCodecSpecificConfig->Elements(),
       mInfo.mCodecSpecificConfig->Length())) {
-      return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+      return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
   for (size_t i = 0; i < headers.Length(); i++) {
     if (NS_FAILED(DoDecodeHeader(headers[i], headerLens[i]))) {
-      return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+      return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
     }
   }
   if (mPacketCount != 3) {
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   mTheoraDecoderContext = th_decode_alloc(&mTheoraInfo, mTheoraSetupInfo);
   if (mTheoraDecoderContext) {
     return InitPromise::CreateAndResolve(TrackInfo::kVideoTrack, __func__);
   } else {
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
 }
@@ -123,7 +123,7 @@ TheoraDecoder::DoDecodeHeader(const unsigned char* aData, size_t aLength)
   return r > 0 ? NS_OK : NS_ERROR_FAILURE;
 }
 
-int
+MediaResult
 TheoraDecoder::DoDecode(MediaRawData* aSample)
 {
   MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
@@ -181,13 +181,13 @@ TheoraDecoder::DoDecode(MediaRawData* aSample)
       LOG("Image allocation error source %ldx%ld display %ldx%ld picture %ldx%ld",
           mTheoraInfo.frame_width, mTheoraInfo.frame_height, mInfo.mDisplay.width, mInfo.mDisplay.height,
           mInfo.mImage.width, mInfo.mImage.height);
-      return -1;
+      return MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
     }
     mCallback->Output(v);
-    return 0;
+    return NS_OK;
   } else {
     LOG("Theora Decode error: %d", ret);
-    return -1;
+    return NS_ERROR_DOM_MEDIA_DECODE_ERR;
   }
 }
 
@@ -198,8 +198,9 @@ TheoraDecoder::ProcessDecode(MediaRawData* aSample)
   if (mIsFlushing) {
     return;
   }
-  if (DoDecode(aSample) == -1) {
-    mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
+  MediaResult rv = DoDecode(aSample);
+  if (NS_FAILED(rv)) {
+    mCallback->Error(rv);
   } else {
     mCallback->InputExhausted();
   }
diff --git a/dom/media/platforms/agnostic/TheoraDecoder.h b/dom/media/platforms/agnostic/TheoraDecoder.h
index 33fc0e73b69b7391294b47f662dc6bb7ca7ddc20..4a5daebb9149113fdfa5e08a24b6ebfcf666ea55 100644
--- a/dom/media/platforms/agnostic/TheoraDecoder.h
+++ b/dom/media/platforms/agnostic/TheoraDecoder.h
@@ -41,7 +41,7 @@ private:
   nsresult DoDecodeHeader(const unsigned char* aData, size_t aLength);
 
   void ProcessDecode(MediaRawData* aSample);
-  int DoDecode(MediaRawData* aSample);
+  MediaResult DoDecode(MediaRawData* aSample);
   void ProcessDrain();
 
   RefPtr<ImageContainer> mImageContainer;
diff --git a/dom/media/platforms/agnostic/VPXDecoder.cpp b/dom/media/platforms/agnostic/VPXDecoder.cpp
index 093030f09d0159cc1864a0a115e92bae47d5423b..458fcc6ca6d9ba39db4859321685623c8979eed6 100644
--- a/dom/media/platforms/agnostic/VPXDecoder.cpp
+++ b/dom/media/platforms/agnostic/VPXDecoder.cpp
@@ -80,7 +80,7 @@ VPXDecoder::Init()
   config.w = config.h = 0; // set after decode
 
   if (!dx || vpx_codec_dec_init(&mVPX, dx, &config, 0)) {
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
   return InitPromise::CreateAndResolve(TrackInfo::kVideoTrack, __func__);
 }
@@ -97,7 +97,7 @@ VPXDecoder::Flush()
   mIsFlushing = false;
 }
 
-int
+MediaResult
 VPXDecoder::DoDecode(MediaRawData* aSample)
 {
   MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
@@ -116,7 +116,7 @@ VPXDecoder::DoDecode(MediaRawData* aSample)
 
   if (vpx_codec_err_t r = vpx_codec_decode(&mVPX, aSample->Data(), aSample->Size(), nullptr, 0)) {
     LOG("VPX Decode error: %s", vpx_codec_err_to_string(r));
-    return -1;
+    return NS_ERROR_DOM_MEDIA_DECODE_ERR;
   }
 
   vpx_codec_iter_t  iter = nullptr;
@@ -157,7 +157,7 @@ VPXDecoder::DoDecode(MediaRawData* aSample)
       b.mPlanes[2].mWidth = img->d_w;
     } else {
       LOG("VPX Unknown image format");
-      return -1;
+      return NS_ERROR_DOM_MEDIA_DECODE_ERR;
     }
 
     RefPtr<VideoData> v =
@@ -176,11 +176,11 @@ VPXDecoder::DoDecode(MediaRawData* aSample)
       LOG("Image allocation error source %ldx%ld display %ldx%ld picture %ldx%ld",
           img->d_w, img->d_h, mInfo.mDisplay.width, mInfo.mDisplay.height,
           mInfo.mImage.width, mInfo.mImage.height);
-      return -1;
+      return MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
     }
     mCallback->Output(v);
   }
-  return 0;
+  return NS_OK;
 }
 
 void
@@ -190,8 +190,9 @@ VPXDecoder::ProcessDecode(MediaRawData* aSample)
   if (mIsFlushing) {
     return;
   }
-  if (DoDecode(aSample) == -1) {
-    mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
+  MediaResult rv = DoDecode(aSample);
+  if (NS_FAILED(rv)) {
+    mCallback->Error(rv);
   } else {
     mCallback->InputExhausted();
   }
diff --git a/dom/media/platforms/agnostic/VPXDecoder.h b/dom/media/platforms/agnostic/VPXDecoder.h
index e8a771b869107010cf11b25427d7ac99efd18474..d420ec069b7764f6dcbc027aba1373f1c8c556f8 100644
--- a/dom/media/platforms/agnostic/VPXDecoder.h
+++ b/dom/media/platforms/agnostic/VPXDecoder.h
@@ -48,7 +48,7 @@ public:
 
 private:
   void ProcessDecode(MediaRawData* aSample);
-  int DoDecode(MediaRawData* aSample);
+  MediaResult DoDecode(MediaRawData* aSample);
   void ProcessDrain();
 
   const RefPtr<ImageContainer> mImageContainer;
diff --git a/dom/media/platforms/agnostic/VorbisDecoder.cpp b/dom/media/platforms/agnostic/VorbisDecoder.cpp
index 995f2624b4f6df8d93cb613779a12c7be052d084..9613924d06b5df40161eff3c9a5bb919d7a74db4 100644
--- a/dom/media/platforms/agnostic/VorbisDecoder.cpp
+++ b/dom/media/platforms/agnostic/VorbisDecoder.cpp
@@ -72,11 +72,11 @@ VorbisDataDecoder::Init()
   if (!XiphExtradataToHeaders(headers, headerLens,
                               mInfo.mCodecSpecificConfig->Elements(),
                               mInfo.mCodecSpecificConfig->Length())) {
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
   for (size_t i = 0; i < headers.Length(); i++) {
     if (NS_FAILED(DecodeHeader(headers[i], headerLens[i]))) {
-      return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+      return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
     }
   }
 
@@ -84,12 +84,12 @@ VorbisDataDecoder::Init()
 
   int r = vorbis_synthesis_init(&mVorbisDsp, &mVorbisInfo);
   if (r) {
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   r = vorbis_block_init(&mVorbisDsp, &mVorbisBlock);
   if (r) {
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   if (mInfo.mRate != (uint32_t)mVorbisDsp.vi->rate) {
@@ -103,7 +103,7 @@ VorbisDataDecoder::Init()
 
   AudioConfig::ChannelLayout layout(mVorbisDsp.vi->channels);
   if (!layout.IsValid()) {
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   return InitPromise::CreateAndResolve(TrackInfo::kAudioTrack, __func__);
@@ -137,14 +137,16 @@ VorbisDataDecoder::ProcessDecode(MediaRawData* aSample)
   if (mIsFlushing) {
     return;
   }
-  if (DoDecode(aSample) == -1) {
-    mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
+
+  MediaResult rv = DoDecode(aSample);
+  if (NS_FAILED(rv)) {
+    mCallback->Error(rv);
   } else {
     mCallback->InputExhausted();
   }
 }
 
-int
+MediaResult
 VorbisDataDecoder::DoDecode(MediaRawData* aSample)
 {
   MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
@@ -167,26 +169,25 @@ VorbisDataDecoder::DoDecode(MediaRawData* aSample)
                                     aSample->mTimecode, mPacketCount++);
 
   if (vorbis_synthesis(&mVorbisBlock, &pkt) != 0) {
-    return -1;
+    return NS_ERROR_DOM_MEDIA_DECODE_ERR;
   }
 
   if (vorbis_synthesis_blockin(&mVorbisDsp,
                                &mVorbisBlock) != 0) {
-    return -1;
+    return NS_ERROR_DOM_MEDIA_DECODE_ERR;
   }
 
   VorbisPCMValue** pcm = 0;
   int32_t frames = vorbis_synthesis_pcmout(&mVorbisDsp, &pcm);
   if (frames == 0) {
-    mCallback->InputExhausted();
-    return 0;
+    return NS_OK;
   }
   while (frames > 0) {
     uint32_t channels = mVorbisDsp.vi->channels;
     uint32_t rate = mVorbisDsp.vi->rate;
     AlignedAudioBuffer buffer(frames*channels);
     if (!buffer) {
-      return -1;
+      return MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
     }
     for (uint32_t j = 0; j < channels; ++j) {
       VorbisPCMValue* channel = pcm[j];
@@ -198,18 +199,18 @@ VorbisDataDecoder::DoDecode(MediaRawData* aSample)
     CheckedInt64 duration = FramesToUsecs(frames, rate);
     if (!duration.isValid()) {
       NS_WARNING("Int overflow converting WebM audio duration");
-      return -1;
+      return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
     }
     CheckedInt64 total_duration = FramesToUsecs(mFrames, rate);
     if (!total_duration.isValid()) {
       NS_WARNING("Int overflow converting WebM audio total_duration");
-      return -1;
+      return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
     }
 
     CheckedInt64 time = total_duration + aTstampUsecs;
     if (!time.isValid()) {
       NS_WARNING("Int overflow adding total_duration and aTstampUsecs");
-      return -1;
+      return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
     };
 
     if (!mAudioConverter) {
@@ -217,7 +218,7 @@ VorbisDataDecoder::DoDecode(MediaRawData* aSample)
                      rate);
       AudioConfig out(channels, rate);
       if (!in.IsValid() || !out.IsValid()) {
-       return -1;
+       return NS_ERROR_DOM_MEDIA_FATAL_ERR;
       }
       mAudioConverter = MakeUnique<AudioConverter>(in, out);
     }
@@ -235,13 +236,13 @@ VorbisDataDecoder::DoDecode(MediaRawData* aSample)
                                     rate));
     mFrames += frames;
     if (vorbis_synthesis_read(&mVorbisDsp, frames) != 0) {
-      return -1;
+      return NS_ERROR_DOM_MEDIA_DECODE_ERR;
     }
 
     frames = vorbis_synthesis_pcmout(&mVorbisDsp, &pcm);
   }
 
-  return aTotalFrames > 0 ? 1 : 0;
+  return NS_OK;
 }
 
 void
diff --git a/dom/media/platforms/agnostic/VorbisDecoder.h b/dom/media/platforms/agnostic/VorbisDecoder.h
index 0054fd51695d5353b623b26153363ee5d495e75c..0ed7bb645377862b533423dfd9604768d77bc639 100644
--- a/dom/media/platforms/agnostic/VorbisDecoder.h
+++ b/dom/media/platforms/agnostic/VorbisDecoder.h
@@ -42,7 +42,7 @@ private:
   nsresult DecodeHeader(const unsigned char* aData, size_t aLength);
 
   void ProcessDecode(MediaRawData* aSample);
-  int DoDecode(MediaRawData* aSample);
+  MediaResult DoDecode(MediaRawData* aSample);
   void ProcessDrain();
 
   const AudioInfo& mInfo;
diff --git a/dom/media/platforms/agnostic/WAVDecoder.cpp b/dom/media/platforms/agnostic/WAVDecoder.cpp
index 0824beb128edbc01e671241194e35d7a71e9d9d6..67a045287d49a0e99703d2ceaf7b731b7d54340b 100644
--- a/dom/media/platforms/agnostic/WAVDecoder.cpp
+++ b/dom/media/platforms/agnostic/WAVDecoder.cpp
@@ -65,14 +65,15 @@ WaveDataDecoder::Init()
 void
 WaveDataDecoder::Input(MediaRawData* aSample)
 {
-  if (!DoDecode(aSample)) {
-    mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
+  MediaResult rv = DoDecode(aSample);
+  if (NS_FAILED(rv)) {
+    mCallback->Error(rv);
   } else {
     mCallback->InputExhausted();
   }
 }
 
-bool
+MediaResult
 WaveDataDecoder::DoDecode(MediaRawData* aSample)
 {
   size_t aLength = aSample->Size();
@@ -84,7 +85,7 @@ WaveDataDecoder::DoDecode(MediaRawData* aSample)
 
   AlignedAudioBuffer buffer(frames * mInfo.mChannels);
   if (!buffer) {
-    return false;
+    return MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
   }
   for (int i = 0; i < frames; ++i) {
     for (unsigned int j = 0; j < mInfo.mChannels; ++j) {
@@ -126,7 +127,7 @@ WaveDataDecoder::DoDecode(MediaRawData* aSample)
                                   mInfo.mChannels,
                                   mInfo.mRate));
 
-  return true;
+  return NS_OK;
 }
 
 void
diff --git a/dom/media/platforms/agnostic/WAVDecoder.h b/dom/media/platforms/agnostic/WAVDecoder.h
index 680c63db55bfcc828eceb0b82c8ee7a14d803bfc..dea980168118d9c754f732bb270dea2de943117d 100644
--- a/dom/media/platforms/agnostic/WAVDecoder.h
+++ b/dom/media/platforms/agnostic/WAVDecoder.h
@@ -31,7 +31,7 @@ public:
   }
 
 private:
-  bool DoDecode(MediaRawData* aSample);
+  MediaResult DoDecode(MediaRawData* aSample);
 
   const AudioInfo& mInfo;
   MediaDataDecoderCallback* mCallback;
diff --git a/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp b/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp
index b3903430168a56c832e3389f5058d25a33615f54..923970fd7db9cd0d1f433a8c1d5c9bd9f24434ad 100644
--- a/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp
+++ b/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp
@@ -93,7 +93,8 @@ public:
       Input(aDecrypted.mSample);
     } else if (aDecrypted.mStatus != Ok) {
       if (mCallback) {
-        mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+        mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+                                          __func__));
       }
     } else {
       MOZ_ASSERT(!mIsShutdown);
diff --git a/dom/media/platforms/agnostic/gmp/GMPAudioDecoder.cpp b/dom/media/platforms/agnostic/gmp/GMPAudioDecoder.cpp
index c9d8f4fe4a391275bd35e6c9f5cc2bc4c0c6030c..796efbdfd3cb7729541b595fba3533f9ffd72332 100644
--- a/dom/media/platforms/agnostic/gmp/GMPAudioDecoder.cpp
+++ b/dom/media/platforms/agnostic/gmp/GMPAudioDecoder.cpp
@@ -8,6 +8,7 @@
 #include "nsServiceManagerUtils.h"
 #include "MediaInfo.h"
 #include "GMPDecoderModule.h"
+#include "nsPrintfCString.h"
 
 namespace mozilla {
 
@@ -31,7 +32,7 @@ AudioCallbackAdapter::Decoded(const nsTArray<int16_t>& aPCM, uint64_t aTimeStamp
 
   if (aRate == 0 || aChannels == 0) {
     NS_WARNING("Invalid rate or num channels returned on GMP audio samples");
-    mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+    mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__));
     return;
   }
 
@@ -39,7 +40,7 @@ AudioCallbackAdapter::Decoded(const nsTArray<int16_t>& aPCM, uint64_t aTimeStamp
   MOZ_ASSERT((aPCM.Length() % aChannels) == 0);
   AlignedAudioBuffer audioData(aPCM.Length());
   if (!audioData) {
-    mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+    mCallback->Error(MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__));
     return;
   }
 
@@ -52,7 +53,8 @@ AudioCallbackAdapter::Decoded(const nsTArray<int16_t>& aPCM, uint64_t aTimeStamp
     auto timestamp = UsecsToFrames(aTimeStamp, aRate);
     if (!timestamp.isValid()) {
       NS_WARNING("Invalid timestamp");
-      mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+      mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,
+                                        __func__));
       return;
     }
     mAudioFrameOffset = timestamp.value();
@@ -62,7 +64,8 @@ AudioCallbackAdapter::Decoded(const nsTArray<int16_t>& aPCM, uint64_t aTimeStamp
   auto timestamp = FramesToUsecs(mAudioFrameOffset + mAudioFrameSum, aRate);
   if (!timestamp.isValid()) {
     NS_WARNING("Invalid timestamp on audio samples");
-    mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+      mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,
+                                        __func__));
     return;
   }
   mAudioFrameSum += numFrames;
@@ -70,7 +73,8 @@ AudioCallbackAdapter::Decoded(const nsTArray<int16_t>& aPCM, uint64_t aTimeStamp
   auto duration = FramesToUsecs(numFrames, aRate);
   if (!duration.isValid()) {
     NS_WARNING("Invalid duration on audio samples");
-    mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+      mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,
+                                        __func__));
     return;
   }
 
@@ -116,14 +120,16 @@ void
 AudioCallbackAdapter::Error(GMPErr aErr)
 {
   MOZ_ASSERT(IsOnGMPThread());
-  mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+  mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+                                    nsPrintfCString("%s: %d", __func__, aErr)));
 }
 
 void
 AudioCallbackAdapter::Terminated()
 {
   NS_WARNING("AAC GMP decoder terminated.");
-  mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+  mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+                                    __func__));
 }
 
 GMPAudioDecoderParams::GMPAudioDecoderParams(const CreateDecoderParams& aParams)
@@ -190,7 +196,7 @@ GMPAudioDecoder::GMPInitDone(GMPAudioDecoderProxy* aGMP)
   MOZ_ASSERT(IsOnGMPThread());
 
   if (!aGMP) {
-    mInitPromise.RejectIfExists(MediaDataDecoder::DecoderFailureReason::INIT_ERROR, __func__);
+    mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
     return;
   }
   if (mInitPromise.IsEmpty()) {
@@ -211,7 +217,7 @@ GMPAudioDecoder::GMPInitDone(GMPAudioDecoderProxy* aGMP)
                                  mAdapter);
   if (NS_FAILED(rv)) {
     aGMP->Close();
-    mInitPromise.Reject(MediaDataDecoder::DecoderFailureReason::INIT_ERROR, __func__);
+    mInitPromise.Reject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
     return;
   }
 
@@ -233,7 +239,7 @@ GMPAudioDecoder::Init()
   InitTags(tags);
   UniquePtr<GetGMPAudioDecoderCallback> callback(new GMPInitDoneCallback(this));
   if (NS_FAILED(mMPS->GetGMPAudioDecoder(mCrashHelper, &tags, GetNodeId(), Move(callback)))) {
-    mInitPromise.Reject(MediaDataDecoder::DecoderFailureReason::INIT_ERROR, __func__);
+    mInitPromise.Reject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   return promise;
@@ -246,7 +252,7 @@ GMPAudioDecoder::Input(MediaRawData* aSample)
 
   RefPtr<MediaRawData> sample(aSample);
   if (!mGMP) {
-    mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+    mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__));
     return;
   }
 
@@ -255,7 +261,9 @@ GMPAudioDecoder::Input(MediaRawData* aSample)
   gmp::GMPAudioSamplesImpl samples(sample, mConfig.mChannels, mConfig.mRate);
   nsresult rv = mGMP->Decode(samples);
   if (NS_FAILED(rv)) {
-    mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
+    mCallback->Error(
+      MediaResult(rv, nsPrintfCString("%s: decode error (%d)",
+                                           __func__, rv)));
   }
 }
 
@@ -283,7 +291,7 @@ GMPAudioDecoder::Drain()
 void
 GMPAudioDecoder::Shutdown()
 {
-  mInitPromise.RejectIfExists(MediaDataDecoder::DecoderFailureReason::CANCELED, __func__);
+  mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   if (!mGMP) {
     return;
   }
diff --git a/dom/media/platforms/agnostic/gmp/GMPVideoDecoder.cpp b/dom/media/platforms/agnostic/gmp/GMPVideoDecoder.cpp
index 4e36783193bc3a5a855a2ef4b81a79d242b01232..dad4466fd6e38ced88d579c873f28f8b4a2dafdb 100644
--- a/dom/media/platforms/agnostic/gmp/GMPVideoDecoder.cpp
+++ b/dom/media/platforms/agnostic/gmp/GMPVideoDecoder.cpp
@@ -54,7 +54,7 @@ VideoCallbackAdapter::Decoded(GMPVideoi420Frame* aDecodedFrame)
   if (v) {
     mCallback->Output(v);
   } else {
-    mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+    mCallback->Error(MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__));
   }
 }
 
@@ -95,7 +95,8 @@ void
 VideoCallbackAdapter::Error(GMPErr aErr)
 {
   MOZ_ASSERT(IsOnGMPThread());
-  mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+  mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+                               nsPrintfCString("%s: %d", __func__, aErr)));
 }
 
 void
@@ -103,7 +104,7 @@ VideoCallbackAdapter::Terminated()
 {
   // Note that this *may* be called from the proxy thread also.
   NS_WARNING("GMP decoder terminated.");
-  mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+  mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__));
 }
 
 GMPVideoDecoderParams::GMPVideoDecoderParams(const CreateDecoderParams& aParams)
@@ -183,14 +184,14 @@ GMPVideoDecoder::CreateFrame(MediaRawData* aSample)
   GMPVideoFrame* ftmp = nullptr;
   GMPErr err = mHost->CreateFrame(kGMPEncodedVideoFrame, &ftmp);
   if (GMP_FAILED(err)) {
-    mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+    mCallback->Error(MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__));
     return nullptr;
   }
 
   GMPUniquePtr<GMPVideoEncodedFrame> frame(static_cast<GMPVideoEncodedFrame*>(ftmp));
   err = frame->CreateEmptyFrame(aSample->Size());
   if (GMP_FAILED(err)) {
-    mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+    mCallback->Error(MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__));
     return nullptr;
   }
 
@@ -232,7 +233,7 @@ GMPVideoDecoder::GMPInitDone(GMPVideoDecoderProxy* aGMP, GMPVideoHost* aHost)
   MOZ_ASSERT(IsOnGMPThread());
 
   if (!aGMP) {
-    mInitPromise.RejectIfExists(MediaDataDecoder::DecoderFailureReason::INIT_ERROR, __func__);
+    mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
     return;
   }
   MOZ_ASSERT(aHost);
@@ -261,7 +262,7 @@ GMPVideoDecoder::GMPInitDone(GMPVideoDecoderProxy* aGMP, GMPVideoHost* aHost)
   } else {
     // Unrecognized mime type
     aGMP->Close();
-    mInitPromise.Reject(MediaDataDecoder::DecoderFailureReason::INIT_ERROR, __func__);
+    mInitPromise.Reject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
     return;
   }
   codec.mWidth = mConfig.mImage.width;
@@ -273,7 +274,7 @@ GMPVideoDecoder::GMPInitDone(GMPVideoDecoderProxy* aGMP, GMPVideoHost* aHost)
                                  PR_GetNumberOfProcessors());
   if (NS_FAILED(rv)) {
     aGMP->Close();
-    mInitPromise.Reject(MediaDataDecoder::DecoderFailureReason::INIT_ERROR, __func__);
+    mInitPromise.Reject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
     return;
   }
 
@@ -307,7 +308,7 @@ GMPVideoDecoder::Init()
   InitTags(tags);
   UniquePtr<GetGMPVideoDecoderCallback> callback(new GMPInitDoneCallback(this));
   if (NS_FAILED(mMPS->GetGMPVideoDecoder(mCrashHelper, &tags, GetNodeId(), Move(callback)))) {
-    mInitPromise.Reject(MediaDataDecoder::DecoderFailureReason::INIT_ERROR, __func__);
+    mInitPromise.Reject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   return promise;
@@ -320,7 +321,8 @@ GMPVideoDecoder::Input(MediaRawData* aSample)
 
   RefPtr<MediaRawData> sample(aSample);
   if (!mGMP) {
-    mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+    mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+                                      __func__));
     return;
   }
 
@@ -328,13 +330,13 @@ GMPVideoDecoder::Input(MediaRawData* aSample)
 
   GMPUniquePtr<GMPVideoEncodedFrame> frame = CreateFrame(sample);
   if (!frame) {
-    mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+    mCallback->Error(MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__));
     return;
   }
   nsTArray<uint8_t> info; // No codec specific per-frame info to pass.
   nsresult rv = mGMP->Decode(Move(frame), false, info, 0);
   if (NS_FAILED(rv)) {
-    mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
+    mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR, __func__));
   }
 }
 
@@ -362,7 +364,7 @@ GMPVideoDecoder::Drain()
 void
 GMPVideoDecoder::Shutdown()
 {
-  mInitPromise.RejectIfExists(MediaDataDecoder::DecoderFailureReason::CANCELED, __func__);
+  mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
   // Note that this *may* be called from the proxy thread also.
   if (!mGMP) {
     return;
diff --git a/dom/media/platforms/agnostic/gmp/MediaDataDecoderProxy.cpp b/dom/media/platforms/agnostic/gmp/MediaDataDecoderProxy.cpp
index 63cb8ecfce20bebac9fd06eeeb051c45f587d84b..717c0f9cf9a71fa1652a92bf51ec68afcc808aca 100644
--- a/dom/media/platforms/agnostic/gmp/MediaDataDecoderProxy.cpp
+++ b/dom/media/platforms/agnostic/gmp/MediaDataDecoderProxy.cpp
@@ -10,7 +10,7 @@
 namespace mozilla {
 
 void
-MediaDataDecoderCallbackProxy::Error(MediaDataDecoderError aError)
+MediaDataDecoderCallbackProxy::Error(const MediaResult& aError)
 {
   mProxyCallback->Error(aError);
 }
diff --git a/dom/media/platforms/agnostic/gmp/MediaDataDecoderProxy.h b/dom/media/platforms/agnostic/gmp/MediaDataDecoderProxy.h
index 261b5acde9bbc018ee0f5f301ee6f9c73a0482c3..735b6126ee67f2b226e7ebc70dfad55693d14fc7 100644
--- a/dom/media/platforms/agnostic/gmp/MediaDataDecoderProxy.h
+++ b/dom/media/platforms/agnostic/gmp/MediaDataDecoderProxy.h
@@ -74,7 +74,7 @@ public:
     mProxyCallback->Output(aData);
   }
 
-  void Error(MediaDataDecoderError aError) override;
+  void Error(const MediaResult& aError) override;
 
   void InputExhausted() override {
     mProxyCallback->InputExhausted();
diff --git a/dom/media/platforms/android/MediaCodecDataDecoder.cpp b/dom/media/platforms/android/MediaCodecDataDecoder.cpp
index 93ead17cb667c4cb9eea68e20322760c8a9f0f7f..20ffeb384aeba22386cc3ec037c487fc85978155 100644
--- a/dom/media/platforms/android/MediaCodecDataDecoder.cpp
+++ b/dom/media/platforms/android/MediaCodecDataDecoder.cpp
@@ -75,11 +75,11 @@ public:
     mSurfaceTexture = AndroidSurfaceTexture::Create();
     if (!mSurfaceTexture) {
       NS_WARNING("Failed to create SurfaceTexture for video decode\n");
-      return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+      return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
     }
 
     if (NS_FAILED(InitDecoder(mSurfaceTexture->JavaSurface()))) {
-      return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+      return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
     }
 
     return InitPromise::CreateAndResolve(TrackInfo::kVideoTrack, __func__);
@@ -260,7 +260,7 @@ MediaCodecDataDecoder::Init()
   return NS_SUCCEEDED(rv) ?
            InitPromise::CreateAndResolve(type, __func__) :
            InitPromise::CreateAndReject(
-               MediaDataDecoder::DecoderFailureReason::INIT_ERROR, __func__);
+               NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
 }
 
 nsresult
@@ -268,7 +268,8 @@ MediaCodecDataDecoder::InitDecoder(Surface::Param aSurface)
 {
   mDecoder = CreateDecoder(mMimeType);
   if (!mDecoder) {
-    INVOKE_CALLBACK(Error, MediaDataDecoderError::FATAL_ERROR);
+    INVOKE_CALLBACK(Error,
+                    MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__));
     return NS_ERROR_FAILURE;
   }
 
@@ -295,7 +296,7 @@ static const int64_t kDecoderTimeout = 10000;
       INVOKE_CALLBACK(DrainComplete); \
       SetState(ModuleState::kDecoding); \
     } \
-    INVOKE_CALLBACK(Error, MediaDataDecoderError::FATAL_ERROR); \
+    INVOKE_CALLBACK(Error, MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__)); \
     break; \
   }
 
@@ -535,7 +536,9 @@ MediaCodecDataDecoder::DecoderLoop()
       BREAK_ON_DECODER_ERROR();
     } else if (outputStatus < 0) {
       NS_WARNING("Unknown error from decoder!");
-      INVOKE_CALLBACK(Error, MediaDataDecoderError::DECODE_ERROR);
+      INVOKE_CALLBACK(Error,
+                      MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+                                       __func__));
       // Don't break here just in case it's recoverable. If it's not, other
       // stuff will fail later and we'll bail out.
     } else {
diff --git a/dom/media/platforms/android/RemoteDataDecoder.cpp b/dom/media/platforms/android/RemoteDataDecoder.cpp
index cdc1d36cb6232b1222ca4d6ca7f44755dc6bc76d..56c79ac4c0a02743231deae86a19da8e2ff607b2 100644
--- a/dom/media/platforms/android/RemoteDataDecoder.cpp
+++ b/dom/media/platforms/android/RemoteDataDecoder.cpp
@@ -80,8 +80,8 @@ public:
   {
     if (mDecoderCallback) {
       mDecoderCallback->Error(aIsFatal ?
-        MediaDataDecoderError::FATAL_ERROR :
-        MediaDataDecoderError::DECODE_ERROR);
+        MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__) :
+        MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR, __func__));
     }
   }
 
@@ -196,7 +196,7 @@ public:
     mSurfaceTexture = AndroidSurfaceTexture::Create();
     if (!mSurfaceTexture) {
       NS_WARNING("Failed to create SurfaceTexture for video decode\n");
-      return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+      return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
     }
 
     // Register native methods.
@@ -208,7 +208,7 @@ public:
 
     mJavaDecoder = CodecProxy::Create(mFormat, mSurfaceTexture->JavaSurface(), mJavaCallbacks);
     if (mJavaDecoder == nullptr) {
-      return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+      return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
     }
 
     mInputDurations.Clear();
@@ -306,7 +306,7 @@ public:
 
     mJavaDecoder = CodecProxy::Create(mFormat, nullptr, mJavaCallbacks);
     if (mJavaDecoder == nullptr) {
-      return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+      return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
     }
 
     return InitPromise::CreateAndResolve(TrackInfo::kAudioTrack, __func__);
@@ -382,7 +382,8 @@ private:
       aFormat->GetInteger(NS_LITERAL_STRING("channel-count"), &mOutputChannels);
       AudioConfig::ChannelLayout layout(mOutputChannels);
       if (!layout.IsValid()) {
-        mDecoderCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+        mDecoderCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+                                                 __func__));
         return;
       }
       aFormat->GetInteger(NS_LITERAL_STRING("sample-rate"), &mOutputSampleRate);
@@ -476,7 +477,7 @@ RemoteDataDecoder::Input(MediaRawData* aSample)
   BufferInfo::LocalRef bufferInfo;
   nsresult rv = BufferInfo::New(&bufferInfo);
   if (NS_FAILED(rv)) {
-    mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+    mCallback->Error(MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__));
     return;
   }
   bufferInfo->Set(0, aSample->Size(), aSample->mTime, 0);
diff --git a/dom/media/platforms/apple/AppleATDecoder.cpp b/dom/media/platforms/apple/AppleATDecoder.cpp
index 0f486787b509a268c53ff4f6fd41f270a44dbd3a..28b4711e082c1ea25f4698bc38cc3c35b89dad35 100644
--- a/dom/media/platforms/apple/AppleATDecoder.cpp
+++ b/dom/media/platforms/apple/AppleATDecoder.cpp
@@ -57,7 +57,7 @@ AppleATDecoder::Init()
 {
   if (!mFormatID) {
     NS_ERROR("Non recognised format");
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   return InitPromise::CreateAndResolve(TrackType::kAudioTrack, __func__);
@@ -194,7 +194,8 @@ AppleATDecoder::SubmitSample(MediaRawData* aSample)
   if (!mConverter) {
     rv = SetupDecoder(aSample);
     if (rv != NS_OK && rv != NS_ERROR_NOT_INITIALIZED) {
-      mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+      mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+                                        __func__));
       return;
     }
   }
@@ -203,9 +204,10 @@ AppleATDecoder::SubmitSample(MediaRawData* aSample)
 
   if (rv == NS_OK) {
     for (size_t i = 0; i < mQueuedSamples.Length(); i++) {
-      if (NS_FAILED(DecodeSample(mQueuedSamples[i]))) {
+      rv = DecodeSample(mQueuedSamples[i]);
+      if (NS_FAILED(rv)) {
         mQueuedSamples.Clear();
-        mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
+        mCallback->Error(MediaResult(rv, __func__));
         return;
       }
     }
@@ -262,7 +264,7 @@ AppleATDecoder::DecodeSample(MediaRawData* aSample)
 
     if (rv && rv != kNoMoreDataErr) {
       LOG("Error decoding audio stream: %d\n", rv);
-      return NS_ERROR_FAILURE;
+      return NS_ERROR_DOM_MEDIA_DECODE_ERR;
     }
 
     if (numFrames) {
@@ -283,7 +285,7 @@ AppleATDecoder::DecodeSample(MediaRawData* aSample)
   media::TimeUnit duration = FramesToTimeUnit(numFrames, rate);
   if (!duration.IsValid()) {
     NS_WARNING("Invalid count of accumulated audio samples");
-    return NS_ERROR_FAILURE;
+    return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
   }
 
 #ifdef LOG_SAMPLE_DECODE
@@ -300,7 +302,7 @@ AppleATDecoder::DecodeSample(MediaRawData* aSample)
     AudioConfig in(*mChannelLayout.get(), rate);
     AudioConfig out(channels, rate);
     if (!in.IsValid() || !out.IsValid()) {
-      return NS_ERROR_FAILURE;
+      return NS_ERROR_DOM_MEDIA_DECODE_ERR;
     }
     mAudioConverter = MakeUnique<AudioConverter>(in, out);
   }
diff --git a/dom/media/platforms/apple/AppleVTDecoder.cpp b/dom/media/platforms/apple/AppleVTDecoder.cpp
index 457ce3ef53c0f055e161705630cb5d98233eb6ac..c58e77cab8d6cc0ab3a7bb8f14c0000b0bb04e4c 100644
--- a/dom/media/platforms/apple/AppleVTDecoder.cpp
+++ b/dom/media/platforms/apple/AppleVTDecoder.cpp
@@ -71,7 +71,7 @@ AppleVTDecoder::Init()
     return InitPromise::CreateAndResolve(TrackType::kVideoTrack, __func__);
   }
 
-  return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+  return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
 }
 
 void
@@ -310,8 +310,8 @@ AppleVTDecoder::OutputFrame(CVPixelBufferRef aImage,
     CVReturn rv = CVPixelBufferLockBaseAddress(aImage, kCVPixelBufferLock_ReadOnly);
     if (rv != kCVReturnSuccess) {
       NS_ERROR("error locking pixel data");
-      mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
-      return NS_ERROR_FAILURE;
+      mCallback->Error(MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__));
+      return NS_ERROR_OUT_OF_MEMORY;
     }
     // Y plane.
     buffer.mPlanes[0].mData =
@@ -376,8 +376,8 @@ AppleVTDecoder::OutputFrame(CVPixelBufferRef aImage,
 
   if (!data) {
     NS_ERROR("Couldn't create VideoData for frame");
-    mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
-    return NS_ERROR_FAILURE;
+    mCallback->Error(MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__));
+    return NS_ERROR_OUT_OF_MEMORY;
   }
 
   // Frames come out in DTS order but we need to output them
@@ -420,7 +420,7 @@ TimingInfoFromSample(MediaRawData* aSample)
   return timestamp;
 }
 
-nsresult
+MediaResult
 AppleVTDecoder::DoDecode(MediaRawData* aSample)
 {
   AssertOnTaskQueueThread();
@@ -446,13 +446,15 @@ AppleVTDecoder::DoDecode(MediaRawData* aSample)
                                           block.receive());
   if (rv != noErr) {
     NS_ERROR("Couldn't create CMBlockBuffer");
-    return NS_ERROR_FAILURE;
+    mCallback->Error(MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__));
+    return MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
   }
   CMSampleTimingInfo timestamp = TimingInfoFromSample(aSample);
   rv = CMSampleBufferCreate(kCFAllocatorDefault, block, true, 0, 0, mFormat, 1, 1, &timestamp, 0, NULL, sample.receive());
   if (rv != noErr) {
     NS_ERROR("Couldn't create CMSampleBuffer");
-    return NS_ERROR_FAILURE;
+    mCallback->Error(MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__));
+    return MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
   }
 
   VTDecodeFrameFlags decodeFlags =
@@ -465,8 +467,8 @@ AppleVTDecoder::DoDecode(MediaRawData* aSample)
   if (rv != noErr && !(infoFlags & kVTDecodeInfo_FrameDropped)) {
     LOG("AppleVTDecoder: Error %d VTDecompressionSessionDecodeFrame", rv);
     NS_WARNING("Couldn't pass frame to decoder");
-    mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
-    return NS_ERROR_FAILURE;
+    mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR, __func__));
+    return NS_ERROR_DOM_MEDIA_DECODE_ERR;
   }
 
   return NS_OK;
diff --git a/dom/media/platforms/apple/AppleVTDecoder.h b/dom/media/platforms/apple/AppleVTDecoder.h
index 8305d4d5238659583e4aa6d6995b2fa410c4e5b3..05d08c7c79a8d1cbfbb393822cc16ef40b11b81d 100644
--- a/dom/media/platforms/apple/AppleVTDecoder.h
+++ b/dom/media/platforms/apple/AppleVTDecoder.h
@@ -96,7 +96,7 @@ private:
   CFDictionaryRef CreateDecoderSpecification();
   CFDictionaryRef CreateDecoderExtensions();
   // Method to pass a frame to VideoToolbox for decoding.
-  nsresult DoDecode(MediaRawData* aSample);
+  MediaResult DoDecode(MediaRawData* aSample);
 
   const RefPtr<TaskQueue> mTaskQueue;
   const uint32_t mMaxRefFrames;
diff --git a/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.cpp b/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.cpp
index 508f25ffc91e3e1df280271e70c2157bdbf3596a..6addd8b75b1d2c1d674fbd30d9ed2fe41c0fb74c 100644
--- a/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.cpp
+++ b/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.cpp
@@ -33,7 +33,7 @@ FFmpegAudioDecoder<LIBAV_VER>::Init()
   nsresult rv = InitDecoder();
 
   return rv == NS_OK ? InitPromise::CreateAndResolve(TrackInfo::kAudioTrack, __func__)
-                     : InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+                     : InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
 }
 
 void
@@ -117,7 +117,7 @@ CopyAndPackAudio(AVFrame* aFrame, uint32_t aNumChannels, uint32_t aNumAFrames)
   return audio;
 }
 
-FFmpegAudioDecoder<LIBAV_VER>::DecodeResult
+MediaResult
 FFmpegAudioDecoder<LIBAV_VER>::DoDecode(MediaRawData* aSample)
 {
   AVPacket packet;
@@ -128,12 +128,11 @@ FFmpegAudioDecoder<LIBAV_VER>::DoDecode(MediaRawData* aSample)
 
   if (!PrepareFrame()) {
     NS_WARNING("FFmpeg audio decoder failed to allocate frame.");
-    return DecodeResult::FATAL_ERROR;
+    return MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
   }
 
   int64_t samplePosition = aSample->mOffset;
   media::TimeUnit pts = media::TimeUnit::FromMicroseconds(aSample->mTime);
-  bool didOutput = false;
 
   while (packet.size > 0) {
     int decoded;
@@ -142,7 +141,7 @@ FFmpegAudioDecoder<LIBAV_VER>::DoDecode(MediaRawData* aSample)
 
     if (bytesConsumed < 0) {
       NS_WARNING("FFmpeg audio decoder error.");
-      return DecodeResult::DECODE_ERROR;
+      return NS_ERROR_DOM_MEDIA_DECODE_ERR;
     }
 
     if (mFrame->format != AV_SAMPLE_FMT_FLT &&
@@ -152,14 +151,14 @@ FFmpegAudioDecoder<LIBAV_VER>::DoDecode(MediaRawData* aSample)
         mFrame->format != AV_SAMPLE_FMT_S32 &&
         mFrame->format != AV_SAMPLE_FMT_S32P) {
       NS_WARNING("FFmpeg audio decoder outputs unsupported audio format.");
-      return DecodeResult::DECODE_ERROR;
+      return NS_ERROR_DOM_MEDIA_DECODE_ERR;
     }
 
     if (decoded) {
       uint32_t numChannels = mCodecContext->channels;
       AudioConfig::ChannelLayout layout(numChannels);
       if (!layout.IsValid()) {
-        return DecodeResult::FATAL_ERROR;
+        return NS_ERROR_DOM_MEDIA_FATAL_ERR;
       }
 
       uint32_t samplingRate = mCodecContext->sample_rate;
@@ -171,7 +170,7 @@ FFmpegAudioDecoder<LIBAV_VER>::DoDecode(MediaRawData* aSample)
         FramesToTimeUnit(mFrame->nb_samples, samplingRate);
       if (!audio || !duration.IsValid()) {
         NS_WARNING("Invalid count of accumulated audio samples");
-        return DecodeResult::DECODE_ERROR;
+        return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
       }
 
       RefPtr<AudioData> data = new AudioData(samplePosition,
@@ -182,19 +181,17 @@ FFmpegAudioDecoder<LIBAV_VER>::DoDecode(MediaRawData* aSample)
                                              numChannels,
                                              samplingRate);
       mCallback->Output(data);
-      didOutput = true;
       pts += duration;
       if (!pts.IsValid()) {
         NS_WARNING("Invalid count of accumulated audio samples");
-        return DecodeResult::DECODE_ERROR;
+        return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
       }
     }
     packet.data += bytesConsumed;
     packet.size -= bytesConsumed;
     samplePosition += bytesConsumed;
   }
-
-  return didOutput ? DecodeResult::DECODE_FRAME : DecodeResult::DECODE_NO_FRAME;
+  return NS_OK;
 }
 
 void
diff --git a/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.h b/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.h
index 2bf4647eacb414ba7271fa46967d645a9f3e7d25..6adaeee14024d54a9ce58ac7898416826d7380c4 100644
--- a/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.h
+++ b/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.h
@@ -35,7 +35,7 @@ public:
   }
 
 private:
-  DecodeResult DoDecode(MediaRawData* aSample) override;
+  MediaResult DoDecode(MediaRawData* aSample) override;
   void ProcessDrain() override;
 };
 
diff --git a/dom/media/platforms/ffmpeg/FFmpegDataDecoder.cpp b/dom/media/platforms/ffmpeg/FFmpegDataDecoder.cpp
index f566c3c1472c50d09391266f07e9a62a915546b6..0b31fb0f94f2a9e52a93059373a47519b8bb415e 100644
--- a/dom/media/platforms/ffmpeg/FFmpegDataDecoder.cpp
+++ b/dom/media/platforms/ffmpeg/FFmpegDataDecoder.cpp
@@ -109,19 +109,11 @@ FFmpegDataDecoder<LIBAV_VER>::ProcessDecode(MediaRawData* aSample)
   if (mIsFlushing) {
     return;
   }
-  switch (DoDecode(aSample)) {
-    case DecodeResult::DECODE_ERROR:
-      mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
-      break;
-    case DecodeResult::FATAL_ERROR:
-      mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
-      break;
-    case DecodeResult::DECODE_NO_FRAME:
-    case DecodeResult::DECODE_FRAME:
-      mCallback->InputExhausted();
-      break;
-    default:
-      break;
+  MediaResult rv = DoDecode(aSample);
+  if (NS_FAILED(rv)) {
+    mCallback->Error(rv);
+  } else {
+    mCallback->InputExhausted();
   }
 }
 
diff --git a/dom/media/platforms/ffmpeg/FFmpegDataDecoder.h b/dom/media/platforms/ffmpeg/FFmpegDataDecoder.h
index ecd579cefeb9ad9f5656948540e583258a5060bf..f9ff9d3c41eafc40db03eef2e5005d73dd9f9854 100644
--- a/dom/media/platforms/ffmpeg/FFmpegDataDecoder.h
+++ b/dom/media/platforms/ffmpeg/FFmpegDataDecoder.h
@@ -40,13 +40,6 @@ public:
   static AVCodec* FindAVCodec(FFmpegLibWrapper* aLib, AVCodecID aCodec);
 
 protected:
-  enum DecodeResult {
-    DECODE_FRAME,
-    DECODE_NO_FRAME,
-    DECODE_ERROR,
-    FATAL_ERROR
-  };
-
   // Flush and Drain operation, always run
   virtual void ProcessFlush();
   virtual void ProcessShutdown();
@@ -64,7 +57,7 @@ protected:
 
 private:
   void ProcessDecode(MediaRawData* aSample);
-  virtual DecodeResult DoDecode(MediaRawData* aSample) = 0;
+  virtual MediaResult DoDecode(MediaRawData* aSample) = 0;
   virtual void ProcessDrain() = 0;
 
   static StaticMutex sMonitor;
diff --git a/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.cpp b/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.cpp
index 87f01a6cbf0656e509984ad11bff82d5960d8997..3f516ebe9fcf61d2817bdf32a2092cf298dd8cb4 100644
--- a/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.cpp
+++ b/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.cpp
@@ -121,7 +121,7 @@ RefPtr<MediaDataDecoder::InitPromise>
 FFmpegVideoDecoder<LIBAV_VER>::Init()
 {
   if (NS_FAILED(InitDecoder())) {
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   return InitPromise::CreateAndResolve(TrackInfo::kVideoTrack, __func__);
@@ -161,8 +161,15 @@ FFmpegVideoDecoder<LIBAV_VER>::InitCodecContext()
   }
 }
 
-FFmpegVideoDecoder<LIBAV_VER>::DecodeResult
+MediaResult
 FFmpegVideoDecoder<LIBAV_VER>::DoDecode(MediaRawData* aSample)
+{
+  bool gotFrame = false;
+  return DoDecode(aSample, &gotFrame);
+}
+
+MediaResult
+FFmpegVideoDecoder<LIBAV_VER>::DoDecode(MediaRawData* aSample, bool* aGotFrame)
 {
   uint8_t* inputData = const_cast<uint8_t*>(aSample->Data());
   size_t inputSize = aSample->Size();
@@ -173,7 +180,6 @@ FFmpegVideoDecoder<LIBAV_VER>::DoDecode(MediaRawData* aSample)
       || mCodecID == AV_CODEC_ID_VP9
 #endif
       )) {
-    bool gotFrame = false;
     while (inputSize) {
       uint8_t* data;
       int size;
@@ -182,31 +188,31 @@ FFmpegVideoDecoder<LIBAV_VER>::DoDecode(MediaRawData* aSample)
                                        aSample->mTime, aSample->mTimecode,
                                        aSample->mOffset);
       if (size_t(len) > inputSize) {
-        return DecodeResult::DECODE_ERROR;
+        return NS_ERROR_DOM_MEDIA_DECODE_ERR;
       }
       inputData += len;
       inputSize -= len;
       if (size) {
-        switch (DoDecode(aSample, data, size)) {
-          case DecodeResult::DECODE_ERROR:
-            return DecodeResult::DECODE_ERROR;
-          case DecodeResult::DECODE_FRAME:
-            gotFrame = true;
-            break;
-          default:
-            break;
+        bool gotFrame = false;
+        MediaResult rv = DoDecode(aSample, data, size, &gotFrame);
+        if (NS_FAILED(rv)) {
+          return rv;
+        }
+        if (gotFrame && aGotFrame) {
+          *aGotFrame = true;
         }
       }
     }
-    return gotFrame ? DecodeResult::DECODE_FRAME : DecodeResult::DECODE_NO_FRAME;
+    return NS_OK;
   }
 #endif
-  return DoDecode(aSample, inputData, inputSize);
+  return DoDecode(aSample, inputData, inputSize, aGotFrame);
 }
 
-FFmpegVideoDecoder<LIBAV_VER>::DecodeResult
+MediaResult
 FFmpegVideoDecoder<LIBAV_VER>::DoDecode(MediaRawData* aSample,
-                                        uint8_t* aData, int aSize)
+                                        uint8_t* aData, int aSize,
+                                        bool* aGotFrame)
 {
   AVPacket packet;
   mLib->av_init_packet(&packet);
@@ -227,7 +233,7 @@ FFmpegVideoDecoder<LIBAV_VER>::DoDecode(MediaRawData* aSample,
 
   if (!PrepareFrame()) {
     NS_WARNING("FFmpeg h264 decoder failed to allocate frame.");
-    return DecodeResult::FATAL_ERROR;
+    return MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
   }
 
   // Required with old version of FFmpeg/LibAV
@@ -245,71 +251,78 @@ FFmpegVideoDecoder<LIBAV_VER>::DoDecode(MediaRawData* aSample,
 
   if (bytesConsumed < 0) {
     NS_WARNING("FFmpeg video decoder error.");
-    return DecodeResult::DECODE_ERROR;
+    return NS_ERROR_DOM_MEDIA_DECODE_ERR;
   }
 
-  // If we've decoded a frame then we need to output it
-  if (decoded) {
-    int64_t pts = mPtsContext.GuessCorrectPts(mFrame->pkt_pts, mFrame->pkt_dts);
-    // Retrieve duration from dts.
-    // We use the first entry found matching this dts (this is done to
-    // handle damaged file with multiple frames with the same dts)
-
-    int64_t duration;
-    if (!mDurationMap.Find(mFrame->pkt_dts, duration)) {
-      NS_WARNING("Unable to retrieve duration from map");
-      duration = aSample->mDuration;
-      // dts are probably incorrectly reported ; so clear the map as we're
-      // unlikely to find them in the future anyway. This also guards
-      // against the map becoming extremely big.
-      mDurationMap.Clear();
-    }
-    FFMPEG_LOG("Got one frame output with pts=%lld dts=%lld duration=%lld opaque=%lld",
-               pts, mFrame->pkt_dts, duration, mCodecContext->reordered_opaque);
-
-    VideoData::YCbCrBuffer b;
-    b.mPlanes[0].mData = mFrame->data[0];
-    b.mPlanes[1].mData = mFrame->data[1];
-    b.mPlanes[2].mData = mFrame->data[2];
-
-    b.mPlanes[0].mStride = mFrame->linesize[0];
-    b.mPlanes[1].mStride = mFrame->linesize[1];
-    b.mPlanes[2].mStride = mFrame->linesize[2];
-
-    b.mPlanes[0].mOffset = b.mPlanes[0].mSkip = 0;
-    b.mPlanes[1].mOffset = b.mPlanes[1].mSkip = 0;
-    b.mPlanes[2].mOffset = b.mPlanes[2].mSkip = 0;
-
-    b.mPlanes[0].mWidth = mFrame->width;
-    b.mPlanes[0].mHeight = mFrame->height;
-    if (mCodecContext->pix_fmt == AV_PIX_FMT_YUV444P) {
-      b.mPlanes[1].mWidth = b.mPlanes[2].mWidth = mFrame->width;
-      b.mPlanes[1].mHeight = b.mPlanes[2].mHeight = mFrame->height;
-    } else {
-      b.mPlanes[1].mWidth = b.mPlanes[2].mWidth = (mFrame->width + 1) >> 1;
-      b.mPlanes[1].mHeight = b.mPlanes[2].mHeight = (mFrame->height + 1) >> 1;
+  if (!decoded) {
+    if (aGotFrame) {
+      *aGotFrame = false;
     }
+    return NS_OK;
+  }
 
-    RefPtr<VideoData> v =
-      VideoData::CreateAndCopyData(mInfo,
-                                   mImageContainer,
-                                   aSample->mOffset,
-                                   pts,
-                                   duration,
-                                   b,
-                                   !!mFrame->key_frame,
-                                   -1,
-                                   mInfo.ScaledImageRect(mFrame->width,
-                                                         mFrame->height));
-
-    if (!v) {
-      NS_WARNING("image allocation error.");
-      return DecodeResult::FATAL_ERROR;
-    }
-    mCallback->Output(v);
-    return DecodeResult::DECODE_FRAME;
+  // If we've decoded a frame then we need to output it
+  int64_t pts = mPtsContext.GuessCorrectPts(mFrame->pkt_pts, mFrame->pkt_dts);
+  // Retrieve duration from dts.
+  // We use the first entry found matching this dts (this is done to
+  // handle damaged file with multiple frames with the same dts)
+
+  int64_t duration;
+  if (!mDurationMap.Find(mFrame->pkt_dts, duration)) {
+    NS_WARNING("Unable to retrieve duration from map");
+    duration = aSample->mDuration;
+    // dts are probably incorrectly reported ; so clear the map as we're
+    // unlikely to find them in the future anyway. This also guards
+    // against the map becoming extremely big.
+    mDurationMap.Clear();
+  }
+  FFMPEG_LOG("Got one frame output with pts=%lld dts=%lld duration=%lld opaque=%lld",
+              pts, mFrame->pkt_dts, duration, mCodecContext->reordered_opaque);
+
+  VideoData::YCbCrBuffer b;
+  b.mPlanes[0].mData = mFrame->data[0];
+  b.mPlanes[1].mData = mFrame->data[1];
+  b.mPlanes[2].mData = mFrame->data[2];
+
+  b.mPlanes[0].mStride = mFrame->linesize[0];
+  b.mPlanes[1].mStride = mFrame->linesize[1];
+  b.mPlanes[2].mStride = mFrame->linesize[2];
+
+  b.mPlanes[0].mOffset = b.mPlanes[0].mSkip = 0;
+  b.mPlanes[1].mOffset = b.mPlanes[1].mSkip = 0;
+  b.mPlanes[2].mOffset = b.mPlanes[2].mSkip = 0;
+
+  b.mPlanes[0].mWidth = mFrame->width;
+  b.mPlanes[0].mHeight = mFrame->height;
+  if (mCodecContext->pix_fmt == AV_PIX_FMT_YUV444P) {
+    b.mPlanes[1].mWidth = b.mPlanes[2].mWidth = mFrame->width;
+    b.mPlanes[1].mHeight = b.mPlanes[2].mHeight = mFrame->height;
+  } else {
+    b.mPlanes[1].mWidth = b.mPlanes[2].mWidth = (mFrame->width + 1) >> 1;
+    b.mPlanes[1].mHeight = b.mPlanes[2].mHeight = (mFrame->height + 1) >> 1;
   }
-  return DecodeResult::DECODE_NO_FRAME;
+
+  RefPtr<VideoData> v =
+    VideoData::CreateAndCopyData(mInfo,
+                                  mImageContainer,
+                                  aSample->mOffset,
+                                  pts,
+                                  duration,
+                                  b,
+                                  !!mFrame->key_frame,
+                                  -1,
+                                  mInfo.ScaledImageRect(mFrame->width,
+                                                        mFrame->height));
+
+  if (!v) {
+    NS_WARNING("image allocation error.");
+    return MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
+  }
+  mCallback->Output(v);
+  if (aGotFrame) {
+    *aGotFrame = true;
+  }
+  return NS_OK;
 }
 
 void
@@ -317,8 +330,8 @@ FFmpegVideoDecoder<LIBAV_VER>::ProcessDrain()
 {
   RefPtr<MediaRawData> empty(new MediaRawData());
   empty->mTimecode = mLastInputDts;
-  while (DoDecode(empty) == DecodeResult::DECODE_FRAME) {
-  }
+  bool gotFrame = false;
+  while (NS_SUCCEEDED(DoDecode(empty, &gotFrame)) && gotFrame);
   mCallback->DrainComplete();
 }
 
diff --git a/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.h b/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.h
index a814a763af178bc20340a3eb1dc750810dc5fb9c..786df0da1afa2ee094854ffa92ece5f8fcd9daab 100644
--- a/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.h
+++ b/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.h
@@ -46,8 +46,9 @@ public:
   static AVCodecID GetCodecId(const nsACString& aMimeType);
 
 private:
-  DecodeResult DoDecode(MediaRawData* aSample) override;
-  DecodeResult DoDecode(MediaRawData* aSample, uint8_t* aData, int aSize);
+  MediaResult DoDecode(MediaRawData* aSample) override;
+  MediaResult DoDecode(MediaRawData* aSample, bool* aGotFrame);
+  MediaResult DoDecode(MediaRawData* aSample, uint8_t* aData, int aSize, bool* aGotFrame);
   void ProcessDrain() override;
   void ProcessFlush() override;
   void OutputDelayedFrames();
diff --git a/dom/media/platforms/gonk/GonkAudioDecoderManager.cpp b/dom/media/platforms/gonk/GonkAudioDecoderManager.cpp
index 97334b61be3255538694817ff949d8af63e40eeb..0bc3fbea9af0077f0f55648efc4ac28d5b0bf3bd 100644
--- a/dom/media/platforms/gonk/GonkAudioDecoderManager.cpp
+++ b/dom/media/platforms/gonk/GonkAudioDecoderManager.cpp
@@ -57,7 +57,7 @@ GonkAudioDecoderManager::Init()
   if (InitMediaCodecProxy()) {
     return InitPromise::CreateAndResolve(TrackType::kAudioTrack, __func__);
   } else {
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 }
 
diff --git a/dom/media/platforms/gonk/GonkMediaDataDecoder.cpp b/dom/media/platforms/gonk/GonkMediaDataDecoder.cpp
index 9cc05ed7c2a8432e9e605278233b9df71402ce0d..6d59d72e115a5b06c0d6c32d2cd0185b88a118b6 100644
--- a/dom/media/platforms/gonk/GonkMediaDataDecoder.cpp
+++ b/dom/media/platforms/gonk/GonkMediaDataDecoder.cpp
@@ -141,7 +141,7 @@ GonkDecoderManager::Shutdown()
     mDecoder = nullptr;
   }
 
-  mInitPromise.RejectIfExists(DecoderFailureReason::CANCELED, __func__);
+  mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
 
   return NS_OK;
 }
@@ -175,7 +175,8 @@ GonkDecoderManager::ProcessInput(bool aEndOfStream)
     }
   } else {
     GMDD_LOG("input processed: error#%d", rv);
-    mDecodeCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+    mDecodeCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+                                            __func__));
   }
 }
 
@@ -189,7 +190,8 @@ GonkDecoderManager::ProcessFlush()
   mWaitOutput.Clear();
   if (mDecoder->flush() != OK) {
     GMDD_LOG("flush error");
-    mDecodeCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+    mDecodeCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+                                            __func__));
   }
   mIsFlushing = false;
   lock.NotifyAll();
@@ -225,7 +227,8 @@ GonkDecoderManager::ProcessToDo(bool aEndOfStream)
   mToDo.clear();
 
   if (NumQueuedSamples() > 0 && ProcessQueuedSamples() < 0) {
-    mDecodeCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+    mDecodeCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+                                            __func__));
     return;
   }
 
@@ -252,7 +255,8 @@ GonkDecoderManager::ProcessToDo(bool aEndOfStream)
     } else if (rv == NS_ERROR_NOT_AVAILABLE) {
       break;
     } else {
-      mDecodeCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+      mDecodeCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+                                              __func__));
       return;
     }
   }
@@ -280,7 +284,8 @@ GonkDecoderManager::ResetEOS()
   mWaitOutput.Clear();
   if (mDecoder->flush() != OK) {
     GMDD_LOG("flush error");
-    mDecodeCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+    mDecodeCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+                                            __func__));
   }
 }
 
diff --git a/dom/media/platforms/gonk/GonkMediaDataDecoder.h b/dom/media/platforms/gonk/GonkMediaDataDecoder.h
index a5283e0adf185d13444185a112c18065c12ef137..bba2a8645d7a4286757fe35eb7aacbe0ed525c57 100644
--- a/dom/media/platforms/gonk/GonkMediaDataDecoder.h
+++ b/dom/media/platforms/gonk/GonkMediaDataDecoder.h
@@ -23,7 +23,6 @@ class GonkDecoderManager : public android::AHandler {
 public:
   typedef TrackInfo::TrackType TrackType;
   typedef MediaDataDecoder::InitPromise InitPromise;
-  typedef MediaDataDecoder::DecoderFailureReason DecoderFailureReason;
 
   virtual ~GonkDecoderManager() {}
 
diff --git a/dom/media/platforms/gonk/GonkVideoDecoderManager.cpp b/dom/media/platforms/gonk/GonkVideoDecoderManager.cpp
index d827390f947c78bb31c45f803f211454483a5174..23ac2d5c3aced99316f4123fe3dd36bbdbcde507 100644
--- a/dom/media/platforms/gonk/GonkVideoDecoderManager.cpp
+++ b/dom/media/platforms/gonk/GonkVideoDecoderManager.cpp
@@ -130,25 +130,25 @@ GonkVideoDecoderManager::Init()
 
   if (uint32_t(mConfig.mImage.width * mConfig.mImage.height) > maxWidth * maxHeight) {
     GVDM_LOG("Video resolution exceeds hw codec capability");
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   // Validate the container-reported frame and pictureRect sizes. This ensures
   // that our video frame creation code doesn't overflow.
   if (!IsValidVideoRegion(mConfig.mImage, mConfig.ImageRect(), mConfig.mDisplay)) {
     GVDM_LOG("It is not a valid region");
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   mReaderTaskQueue = AbstractThread::GetCurrent()->AsTaskQueue();
   MOZ_ASSERT(mReaderTaskQueue);
 
   if (mDecodeLooper.get() != nullptr) {
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   if (!InitLoopers(MediaData::VIDEO_DATA)) {
-    return InitPromise::CreateAndReject(DecoderFailureReason::INIT_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
   }
 
   RefPtr<InitPromise> p = mInitPromise.Ensure(__func__);
@@ -672,7 +672,7 @@ GonkVideoDecoderManager::codecReserved()
 
   if (rv != OK) {
     GVDM_LOG("Failed to configure codec!!!!");
-    mInitPromise.Reject(DecoderFailureReason::INIT_ERROR, __func__);
+    mInitPromise.Reject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
     return;
   }
 
@@ -683,7 +683,7 @@ void
 GonkVideoDecoderManager::codecCanceled()
 {
   GVDM_LOG("codecCanceled");
-  mInitPromise.RejectIfExists(DecoderFailureReason::CANCELED, __func__);
+  mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
 }
 
 // Called on GonkDecoderManager::mTaskLooper thread.
diff --git a/dom/media/platforms/omx/OmxDataDecoder.cpp b/dom/media/platforms/omx/OmxDataDecoder.cpp
index 1e12c13d6c93ea0a1eb93b43ee03df3053625b0c..33f9dad303f5cd3f39a45cd83ebc18d39b43796d 100644
--- a/dom/media/platforms/omx/OmxDataDecoder.cpp
+++ b/dom/media/platforms/omx/OmxDataDecoder.cpp
@@ -169,7 +169,7 @@ OmxDataDecoder::Init()
         MOZ_ASSERT(self->mOmxState != OMX_StateIdle);
       },
       [self] () {
-        self->RejectInitPromise(DecoderFailureReason::INIT_ERROR, __func__);
+        self->RejectInitPromise(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
       });
 
   return p;
@@ -430,9 +430,9 @@ OmxDataDecoder::EmptyBufferFailure(OmxBufferFailureHolder aFailureHolder)
 }
 
 void
-OmxDataDecoder::NotifyError(OMX_ERRORTYPE aOmxError, const char* aLine, MediaDataDecoderError aError)
+OmxDataDecoder::NotifyError(OMX_ERRORTYPE aOmxError, const char* aLine, const MediaResult& aError)
 {
-  LOG("NotifyError %d (%d) at %s", aOmxError, aError, aLine);
+  LOG("NotifyError %d (%d) at %s", aOmxError, aError.Code(), aLine);
   mCallback->Error(aError);
 }
 
@@ -551,13 +551,13 @@ OmxDataDecoder::ResolveInitPromise(const char* aMethodName)
 }
 
 void
-OmxDataDecoder::RejectInitPromise(DecoderFailureReason aReason, const char* aMethodName)
+OmxDataDecoder::RejectInitPromise(MediaResult aError, const char* aMethodName)
 {
   RefPtr<OmxDataDecoder> self = this;
   nsCOMPtr<nsIRunnable> r =
-    NS_NewRunnableFunction([self, aReason, aMethodName] () {
+    NS_NewRunnableFunction([self, aError, aMethodName] () {
       MOZ_ASSERT(self->mReaderTaskQueue->IsCurrentThreadIn());
-      self->mInitPromise.RejectIfExists(aReason, aMethodName);
+      self->mInitPromise.RejectIfExists(aError, aMethodName);
     });
   mReaderTaskQueue->Dispatch(r.forget());
 }
@@ -583,7 +583,7 @@ OmxDataDecoder::OmxStateRunner()
                MOZ_ASSERT(self->mOmxState == OMX_StateIdle);
              },
              [self] () {
-               self->RejectInitPromise(DecoderFailureReason::INIT_ERROR, __func__);
+               self->RejectInitPromise(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
              });
 
     // Allocate input and output buffers.
@@ -591,7 +591,7 @@ OmxDataDecoder::OmxStateRunner()
     for(const auto id : types) {
       if (NS_FAILED(AllocateBuffers(id))) {
         LOG("Failed to allocate buffer on port %d", id);
-        RejectInitPromise(DecoderFailureReason::INIT_ERROR, __func__);
+        RejectInitPromise(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
         break;
       }
     }
@@ -606,7 +606,7 @@ OmxDataDecoder::OmxStateRunner()
                self->ResolveInitPromise(__func__);
              },
              [self] () {
-               self->RejectInitPromise(DecoderFailureReason::INIT_ERROR, __func__);
+               self->RejectInitPromise(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
              });
   } else if (mOmxState == OMX_StateExecuting) {
     // Configure codec once it gets OMX_StateExecuting state.
@@ -690,7 +690,8 @@ OmxDataDecoder::Event(OMX_EVENTTYPE aEvent, OMX_U32 aData1, OMX_U32 aData2)
     {
       // Got error during decoding, send msg to MFR skipping to next key frame.
       if (aEvent == OMX_EventError && mOmxState == OMX_StateExecuting) {
-        NotifyError((OMX_ERRORTYPE)aData1, __func__, MediaDataDecoderError::DECODE_ERROR);
+        NotifyError((OMX_ERRORTYPE)aData1, __func__,
+                    MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR, __func__));
         return true;
       }
       LOG("WARNING: got none handle event: %d, aData1: %d, aData2: %d",
diff --git a/dom/media/platforms/omx/OmxDataDecoder.h b/dom/media/platforms/omx/OmxDataDecoder.h
index 53700544aa0d48d6520db83eef52c4c9c669fb9d..ea75b2a2ac75d372dcd05c45a1ccc8e9fe951c26 100644
--- a/dom/media/platforms/omx/OmxDataDecoder.h
+++ b/dom/media/platforms/omx/OmxDataDecoder.h
@@ -87,7 +87,7 @@ protected:
 
   void ResolveInitPromise(const char* aMethodName);
 
-  void RejectInitPromise(DecoderFailureReason aReason, const char* aMethodName);
+  void RejectInitPromise(MediaResult aError, const char* aMethodName);
 
   void OmxStateRunner();
 
@@ -103,7 +103,7 @@ protected:
 
   void NotifyError(OMX_ERRORTYPE aOmxError,
                    const char* aLine,
-                   MediaDataDecoderError aError = MediaDataDecoderError::FATAL_ERROR);
+                   const MediaResult& aError = MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR));
 
   // Configure audio/video codec.
   // Some codec may just ignore this and rely on codec specific data in
diff --git a/dom/media/platforms/wmf/WMFMediaDataDecoder.cpp b/dom/media/platforms/wmf/WMFMediaDataDecoder.cpp
index 8b9423c4ae5f60b677a13cfd6294e0503ce93408..72087cb53d9f28687cdc9160648695d81117f8e4 100644
--- a/dom/media/platforms/wmf/WMFMediaDataDecoder.cpp
+++ b/dom/media/platforms/wmf/WMFMediaDataDecoder.cpp
@@ -123,7 +123,7 @@ WMFMediaDataDecoder::ProcessDecode(MediaRawData* aSample)
   HRESULT hr = mMFTManager->Input(aSample);
   if (FAILED(hr)) {
     NS_WARNING("MFTManager rejected sample");
-    mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
+    mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR, __func__));
     if (!mRecordedError) {
       SendTelemetry(hr);
       mRecordedError = true;
@@ -150,7 +150,7 @@ WMFMediaDataDecoder::ProcessOutput()
     mCallback->InputExhausted();
   } else if (FAILED(hr)) {
     NS_WARNING("WMFMediaDataDecoder failed to output data");
-    mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
+    mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR, __func__));
     if (!mRecordedError) {
       SendTelemetry(hr);
       mRecordedError = true;
diff --git a/dom/media/platforms/wrappers/FuzzingWrapper.cpp b/dom/media/platforms/wrappers/FuzzingWrapper.cpp
index 0cbaf2741cad1310be53a52b7fb1a847d58de188..bf5a5f68b7af732d99c1ecfc417883e5ab5c7903 100644
--- a/dom/media/platforms/wrappers/FuzzingWrapper.cpp
+++ b/dom/media/platforms/wrappers/FuzzingWrapper.cpp
@@ -171,19 +171,17 @@ DecoderCallbackFuzzingWrapper::Output(MediaData* aData)
 }
 
 void
-DecoderCallbackFuzzingWrapper::Error(MediaDataDecoderError aError)
+DecoderCallbackFuzzingWrapper::Error(const MediaResult& aError)
 {
   if (!mTaskQueue->IsCurrentThreadIn()) {
-    mTaskQueue->Dispatch(
-      NewRunnableMethod<MediaDataDecoderError>(this,
-                                               &DecoderCallbackFuzzingWrapper::Error,
-                                               aError));
+    mTaskQueue->Dispatch(NewRunnableMethod<MediaResult>(
+      this, &DecoderCallbackFuzzingWrapper::Error, aError));
     return;
   }
   CFW_LOGV("");
   MOZ_ASSERT(mCallback);
   ClearDelayedOutput();
-  mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+  mCallback->Error(aError);
 }
 
 void
diff --git a/dom/media/platforms/wrappers/FuzzingWrapper.h b/dom/media/platforms/wrappers/FuzzingWrapper.h
index 9a012d515c23f1b24efa326b3cce481d91d196c5..d2898a50f51c307b454d9b332bdba60c3dce4a8c 100644
--- a/dom/media/platforms/wrappers/FuzzingWrapper.h
+++ b/dom/media/platforms/wrappers/FuzzingWrapper.h
@@ -60,7 +60,7 @@ private:
 
   // MediaDataDecoderCallback implementation.
   void Output(MediaData* aData) override;
-  void Error(MediaDataDecoderError aError) override;
+  void Error(const MediaResult& aError) override;
   void InputExhausted() override;
   void DrainComplete() override;
   void ReleaseMediaResources() override;
diff --git a/dom/media/platforms/wrappers/H264Converter.cpp b/dom/media/platforms/wrappers/H264Converter.cpp
index b00c50b74cfe87bb8edccd732a1b3da1f2718110..25e6e8804d7e9683f5c3beccac43ce984400560d 100644
--- a/dom/media/platforms/wrappers/H264Converter.cpp
+++ b/dom/media/platforms/wrappers/H264Converter.cpp
@@ -55,7 +55,7 @@ H264Converter::Input(MediaRawData* aSample)
   if (!mp4_demuxer::AnnexB::ConvertSampleToAVCC(aSample)) {
     // We need AVCC content to be able to later parse the SPS.
     // This is a no-op if the data is already AVCC.
-    mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
+    mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__));
     return;
   }
 
@@ -88,7 +88,7 @@ H264Converter::Input(MediaRawData* aSample)
     rv = CheckForSPSChange(aSample);
   }
   if (NS_FAILED(rv)) {
-    mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
+    mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__));
     return;
   }
 
@@ -99,7 +99,7 @@ H264Converter::Input(MediaRawData* aSample)
 
   if (!mNeedAVCC &&
       !mp4_demuxer::AnnexB::ConvertSampleToAnnexB(aSample)) {
-    mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+    mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__));
     return;
   }
 
@@ -249,10 +249,11 @@ H264Converter::OnDecoderInitDone(const TrackType aTrackType)
 }
 
 void
-H264Converter::OnDecoderInitFailed(MediaDataDecoder::DecoderFailureReason aReason)
+H264Converter::OnDecoderInitFailed(MediaResult aError)
 {
   mInitPromiseRequest.Complete();
-  mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
+  mCallback->Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+                                    __func__));
 }
 
 nsresult
diff --git a/dom/media/platforms/wrappers/H264Converter.h b/dom/media/platforms/wrappers/H264Converter.h
index 1db44ce90eaf9054d0cd41e7c426f055e0e0665e..ab60d8dea678bb32a02ad001297f2246ee35c214 100644
--- a/dom/media/platforms/wrappers/H264Converter.h
+++ b/dom/media/platforms/wrappers/H264Converter.h
@@ -51,7 +51,7 @@ private:
   void UpdateConfigFromExtraData(MediaByteBuffer* aExtraData);
 
   void OnDecoderInitDone(const TrackType aTrackType);
-  void OnDecoderInitFailed(MediaDataDecoder::DecoderFailureReason aReason);
+  void OnDecoderInitFailed(MediaResult aError);
 
   RefPtr<PlatformDecoderModule> mPDM;
   VideoInfo mOriginalConfig;
diff --git a/dom/media/test/test_decode_error.html b/dom/media/test/test_decode_error.html
index 58d4c429997e81c3ec07ff9297586ccf6dbe0424..2a5d9998c3b582ac5dbb65d560a49f16696de738 100644
--- a/dom/media/test/test_decode_error.html
+++ b/dom/media/test/test_decode_error.html
@@ -27,6 +27,8 @@ function startTest(test, token) {
     ok(el.error, "Element 'error' attr expected to have a value");
     ok(el.error instanceof MediaError, "Element 'error' attr expected to be MediaError");
     is(el.error.code, MediaError.MEDIA_ERR_DECODE, "Expected a decode error");
+    ok(typeof el.error.message === 'string' || el.error.essage instanceof String, "Element 'message' attr expected to be a string");
+    ok(el.error.message.length > 0, "Element 'message' attr has content");
     el._sawError = true;
     manager.finished(token);
   }, false);
@@ -46,7 +48,12 @@ function startTest(test, token) {
 }
 
 SimpleTest.waitForExplicitFinish();
-SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, beginTest);
+SpecialPowers.pushPrefEnv({
+  "set": [
+    ["media.cache_size", 40000],
+    ["dom.MediaError.message.enabled", true]
+  ]
+}, beginTest);
 function beginTest() {
   manager.runTests(gDecodeErrorTests, startTest);
 }
diff --git a/dom/media/wave/WaveDemuxer.cpp b/dom/media/wave/WaveDemuxer.cpp
index 2b38d23e7048ba42b8eb7276d91bc5c168fdb440..23aba9a04ca6c6dc8c797d5f5114464d5f0e8bf3 100644
--- a/dom/media/wave/WaveDemuxer.cpp
+++ b/dom/media/wave/WaveDemuxer.cpp
@@ -43,7 +43,7 @@ WAVDemuxer::Init()
 {
   if (!InitInternal()) {
     return InitPromise::CreateAndReject(
-      DemuxerFailureReason::DEMUXER_ERROR, __func__);
+      NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
   }
   return InitPromise::CreateAndResolve(NS_OK, __func__);
 }
@@ -340,10 +340,7 @@ WAVTrackDemuxer::ScanUntil(const TimeUnit& aTime)
 RefPtr<WAVTrackDemuxer::SamplesPromise>
 WAVTrackDemuxer::GetSamples(int32_t aNumSamples)
 {
-  if (!aNumSamples) {
-    return SamplesPromise::CreateAndReject(
-        DemuxerFailureReason::DEMUXER_ERROR, __func__);
-  }
+  MOZ_ASSERT(aNumSamples);
 
   RefPtr<SamplesHolder> datachunks = new SamplesHolder();
 
@@ -357,7 +354,7 @@ WAVTrackDemuxer::GetSamples(int32_t aNumSamples)
 
   if (datachunks->mSamples.IsEmpty()) {
     return SamplesPromise::CreateAndReject(
-        DemuxerFailureReason::END_OF_STREAM, __func__);
+        NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
   }
 
   return SamplesPromise::CreateAndResolve(datachunks, __func__);
@@ -377,7 +374,7 @@ RefPtr<WAVTrackDemuxer::SkipAccessPointPromise>
 WAVTrackDemuxer::SkipToNextRandomAccessPoint(TimeUnit aTimeThreshold)
 {
   return SkipAccessPointPromise::CreateAndReject(
-    SkipFailureHolder(DemuxerFailureReason::DEMUXER_ERROR, 0), __func__);
+    SkipFailureHolder(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, 0), __func__);
 }
 
 int64_t
diff --git a/dom/media/webaudio/MediaBufferDecoder.cpp b/dom/media/webaudio/MediaBufferDecoder.cpp
index a3b5eb72165a44ed6034550dcd81844c4c1bb6c2..e9f1d5a47c721a32f30c17b8f8309a980c10aada 100644
--- a/dom/media/webaudio/MediaBufferDecoder.cpp
+++ b/dom/media/webaudio/MediaBufferDecoder.cpp
@@ -132,10 +132,10 @@ private:
 
   void Decode();
   void OnMetadataRead(MetadataHolder* aMetadata);
-  void OnMetadataNotRead(ReadMetadataFailureReason aReason);
+  void OnMetadataNotRead(const MediaResult& aError);
   void RequestSample();
   void SampleDecoded(MediaData* aData);
-  void SampleNotDecoded(MediaDecoderReader::NotDecodedReason aReason);
+  void SampleNotDecoded(const MediaResult& aError);
   void FinishDecode();
   void AllocateBuffer();
   void CallbackTheResult();
@@ -310,7 +310,7 @@ MediaDecodeTask::OnMetadataRead(MetadataHolder* aMetadata)
 }
 
 void
-MediaDecodeTask::OnMetadataNotRead(ReadMetadataFailureReason aReason)
+MediaDecodeTask::OnMetadataNotRead(const MediaResult& aReason)
 {
   mDecoderReader->Shutdown();
   ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent);
@@ -337,15 +337,14 @@ MediaDecodeTask::SampleDecoded(MediaData* aData)
 }
 
 void
-MediaDecodeTask::SampleNotDecoded(MediaDecoderReader::NotDecodedReason aReason)
+MediaDecodeTask::SampleNotDecoded(const MediaResult& aError)
 {
   MOZ_ASSERT(!NS_IsMainThread());
-  if (aReason == MediaDecoderReader::DECODE_ERROR) {
+  if (aError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
+    FinishDecode();
+  } else {
     mDecoderReader->Shutdown();
     ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent);
-  } else {
-    MOZ_ASSERT(aReason == MediaDecoderReader::END_OF_STREAM);
-    FinishDecode();
   }
 }
 
diff --git a/dom/media/webm/WebMDemuxer.cpp b/dom/media/webm/WebMDemuxer.cpp
index ae729ff541eb40f17edab6a3847cb70bf95095cc..2f39daab7558949851542b78c3c7d51fa020ae22 100644
--- a/dom/media/webm/WebMDemuxer.cpp
+++ b/dom/media/webm/WebMDemuxer.cpp
@@ -188,12 +188,12 @@ WebMDemuxer::Init()
   InitBufferedState();
 
   if (NS_FAILED(ReadMetadata())) {
-    return InitPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
   }
 
   if (!GetNumberTracks(TrackInfo::kAudioTrack) &&
       !GetNumberTracks(TrackInfo::kVideoTrack)) {
-    return InitPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__);
+    return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
   }
 
   return InitPromise::CreateAndResolve(NS_OK, __func__);
@@ -955,9 +955,7 @@ RefPtr<WebMTrackDemuxer::SamplesPromise>
 WebMTrackDemuxer::GetSamples(int32_t aNumSamples)
 {
   RefPtr<SamplesHolder> samples = new SamplesHolder;
-  if (!aNumSamples) {
-    return SamplesPromise::CreateAndReject(DemuxerFailureReason::DEMUXER_ERROR, __func__);
-  }
+  MOZ_ASSERT(aNumSamples);
 
   while (aNumSamples) {
     RefPtr<MediaRawData> sample(NextSample());
@@ -973,7 +971,7 @@ WebMTrackDemuxer::GetSamples(int32_t aNumSamples)
   }
 
   if (samples->mSamples.IsEmpty()) {
-    return SamplesPromise::CreateAndReject(DemuxerFailureReason::END_OF_STREAM, __func__);
+    return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
   } else {
     UpdateSamples(samples->mSamples);
     return SamplesPromise::CreateAndResolve(samples, __func__);
@@ -1109,7 +1107,7 @@ WebMTrackDemuxer::SkipToNextRandomAccessPoint(media::TimeUnit aTimeThreshold)
                parsed);
     return SkipAccessPointPromise::CreateAndResolve(parsed, __func__);
   } else {
-    SkipFailureHolder failure(DemuxerFailureReason::END_OF_STREAM, parsed);
+    SkipFailureHolder failure(NS_ERROR_DOM_MEDIA_END_OF_STREAM, parsed);
     return SkipAccessPointPromise::CreateAndReject(Move(failure), __func__);
   }
 }
diff --git a/dom/webidl/MediaError.webidl b/dom/webidl/MediaError.webidl
index 6ee572067d79964cb86e0ccc25a22a61282f2603..3b8a0bbe948ff0a31f30557fbdc30edc92dbcd81 100644
--- a/dom/webidl/MediaError.webidl
+++ b/dom/webidl/MediaError.webidl
@@ -19,4 +19,6 @@ interface MediaError {
 
   [Constant]
   readonly attribute unsigned short code;
+  [Pref="dom.MediaError.message.enabled"]
+  readonly attribute DOMString message;
 };
diff --git a/ipc/glue/GeckoChildProcessHost.cpp b/ipc/glue/GeckoChildProcessHost.cpp
index d4f323dcbac2df990b97d675507b520c71fdcdac..3431f628e3077fde8d2096b6ec59600097d19c9e 100644
--- a/ipc/glue/GeckoChildProcessHost.cpp
+++ b/ipc/glue/GeckoChildProcessHost.cpp
@@ -23,6 +23,10 @@
 #include "prenv.h"
 #include "nsXPCOMPrivate.h"
 
+#if defined(XP_MACOSX) && defined(MOZ_CONTENT_SANDBOX)
+#include "nsAppDirectoryServiceDefs.h"
+#endif
+
 #include "nsExceptionHandler.h"
 
 #include "nsDirectoryServiceDefs.h"
@@ -608,6 +612,20 @@ AddAppDirToCommandLine(std::vector<std::string>& aCmdLine)
         aCmdLine.push_back(path.get());
 #endif
       }
+
+#if defined(XP_MACOSX) && defined(MOZ_CONTENT_SANDBOX)
+      // Full path to the profile dir
+      nsCOMPtr<nsIFile> profileDir;
+      rv = directoryService->Get(NS_APP_USER_PROFILE_50_DIR,
+                                 NS_GET_IID(nsIFile),
+                                 getter_AddRefs(profileDir));
+      if (NS_SUCCEEDED(rv)) {
+        nsAutoCString path;
+        MOZ_ALWAYS_SUCCEEDS(profileDir->GetNativePath(path));
+        aCmdLine.push_back("-profile");
+        aCmdLine.push_back(path.get());
+      }
+#endif
     }
   }
 }
diff --git a/js/src/devtools/automation/winbuildenv.sh b/js/src/devtools/automation/winbuildenv.sh
index 3467e4ce96f1252b37fb44364cfbbdcd90176e02..f9d862ac45d926979234dfd08439445bd8073f64 100644
--- a/js/src/devtools/automation/winbuildenv.sh
+++ b/js/src/devtools/automation/winbuildenv.sh
@@ -10,7 +10,7 @@ topsrcdir="$SOURCE"
 # Tooltool installs in parent of topsrcdir for spidermonkey builds.
 # Resolve that path since the mozconfigs assume tooltool installs in
 # topsrcdir.
-VSPATH="$(cd ${topsrcdir}/.. && pwd)/vs2015u2"
+VSPATH="$(cd ${topsrcdir}/.. && pwd)/vs2015u3"
 
 # When running on a developer machine, several variables will already
 # have the right settings and we will need to keep them since the
diff --git a/js/src/old-configure.in b/js/src/old-configure.in
index 1cb4204aeeb3ff3e87d537fe43d78a90652d5935..9ff3a842ef7d7330dc2d10508935d2d8e9ce8fda 100644
--- a/js/src/old-configure.in
+++ b/js/src/old-configure.in
@@ -791,6 +791,10 @@ case "$target" in
             # use such pragmas, so just ignore them.
             CFLAGS="$CFLAGS -Wno-unknown-pragmas"
             CXXFLAGS="$CXXFLAGS -Wno-unknown-pragmas"
+            # We get errors about various #pragma intrinsic directives from
+            # clang-cl, and we don't need to hear about those.
+            CFLAGS="$CFLAGS -Wno-ignored-pragmas"
+            CXXFLAGS="$CXXFLAGS -Wno-ignored-pragmas"
             # clang-cl's Intrin.h marks things like _ReadWriteBarrier
             # as __attribute((__deprecated__)).  This is nice to know,
             # but since we don't get the equivalent warning from MSVC,
diff --git a/layout/generic/nsSubDocumentFrame.cpp b/layout/generic/nsSubDocumentFrame.cpp
index 6439347a0a21d9c04ea30851fdd86e0e778d119a..d87d8d70b28174b21c505725343bb2ccdc615bcb 100644
--- a/layout/generic/nsSubDocumentFrame.cpp
+++ b/layout/generic/nsSubDocumentFrame.cpp
@@ -41,10 +41,13 @@
 #include "nsIPermissionManager.h"
 #include "nsServiceManagerUtils.h"
 #include "nsIDOMMutationEvent.h"
+#include "mozilla/Preferences.h"
 
 using namespace mozilla;
 using mozilla::layout::RenderFrameParent;
 
+static bool sShowPreviousPage = true;
+
 static nsIDocument*
 GetDocumentFromView(nsView* aView)
 {
@@ -107,6 +110,12 @@ nsSubDocumentFrame::Init(nsIContent*       aContent,
   nsCOMPtr<nsIDOMHTMLFrameElement> frameElem = do_QueryInterface(aContent);
   mIsInline = frameElem ? false : true;
 
+  static bool addedShowPreviousPage = false;
+  if (!addedShowPreviousPage) {
+    Preferences::AddBoolVarCache(&sShowPreviousPage, "layout.show_previous_page", true);
+    addedShowPreviousPage = true;
+  }
+
   nsAtomicContainerFrame::Init(aContent, aParent, aPrevInFlow);
 
   // We are going to create an inner view.  If we need a view for the
@@ -227,7 +236,7 @@ nsSubDocumentFrame::GetSubdocumentPresShellForPainting(uint32_t aFlags)
     }
     if (frame) {
       nsIPresShell* ps = frame->PresContext()->PresShell();
-      if (!presShell || (ps && !ps->IsPaintingSuppressed())) {
+      if (!presShell || (ps && !ps->IsPaintingSuppressed() && sShowPreviousPage)) {
         subdocView = nextView;
         subdocRootFrame = frame;
         presShell = ps;
diff --git a/media/mtransport/test/ice_unittest.cpp b/media/mtransport/test/ice_unittest.cpp
index 48fab1225a4702a0eea3b1608c8c8d2c57ee13fa..62d54c4637d70f9e2be8cce504d83a966d56d8df 100644
--- a/media/mtransport/test/ice_unittest.cpp
+++ b/media/mtransport/test/ice_unittest.cpp
@@ -2880,6 +2880,24 @@ TEST_F(WebRtcIceConnectTest, TestConnectSymmetricNat) {
   Connect();
 }
 
+TEST_F(WebRtcIceConnectTest, TestConnectSymmetricNatAndNoNat) {
+  p1_ = MakeUnique<IceTestPeer>("P1", test_utils_, true, false, false);
+  p1_->UseNat();
+  p1_->SetFilteringType(TestNat::PORT_DEPENDENT);
+  p1_->SetMappingType(TestNat::PORT_DEPENDENT);
+
+  p2_ = MakeUnique<IceTestPeer>("P2", test_utils_, false, false, false);
+  initted_ = true;
+
+  AddStream(1);
+  p1_->SetExpectedTypes(NrIceCandidate::Type::ICE_PEER_REFLEXIVE,
+                        NrIceCandidate::Type::ICE_HOST);
+  p2_->SetExpectedTypes(NrIceCandidate::Type::ICE_HOST,
+                        NrIceCandidate::Type::ICE_PEER_REFLEXIVE);
+  ASSERT_TRUE(Gather());
+  Connect();
+}
+
 TEST_F(WebRtcIceConnectTest, TestGatherNatBlocksUDP) {
   if (turn_server_.empty())
     return;
diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
index 5370869d1dd5c76f9165acb87da4999f31e11f2e..eec54d78f7eff01184e9834fb653e7c0d0367005 100644
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -2521,6 +2521,11 @@ public abstract class GeckoApp
 
                         if (tab.isExternal()) {
                             moveTaskToBack(true);
+                            Tab nextSelectedTab = Tabs.getInstance().getNextTab(tab);
+                            if (nextSelectedTab != null) {
+                                int nextSelectedTabId = nextSelectedTab.getId();
+                                GeckoAppShell.notifyObservers("Tab:KeepZombified", Integer.toString(nextSelectedTabId));
+                            }
                             tabs.closeTab(tab);
                             return;
                         }
diff --git a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabReceivedService.java b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabReceivedService.java
index e56ac5f322d7d4dc020b1ad0cf5087a6b3b0491c..4f5baacdbdf56a96094d3b4aa38ee8b878225a11 100644
--- a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabReceivedService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabReceivedService.java
@@ -16,6 +16,8 @@ import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Resources;
 import android.database.Cursor;
+import android.media.RingtoneManager;
+import android.net.Uri;
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
 import android.support.v4.app.NotificationCompat;
@@ -67,6 +69,13 @@ public class TabReceivedService extends IntentService {
         builder.setContentText(uri);
         builder.setContentIntent(contentIntent);
 
+        // Trigger "heads-up" notification mode on supported Android versions.
+        builder.setPriority(NotificationCompat.PRIORITY_HIGH);
+        final Uri notificationSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
+        if (notificationSoundUri != null) {
+            builder.setSound(notificationSoundUri);
+        }
+
         final SharedPreferences prefs = GeckoSharedPrefs.forApp(this);
         final int notificationId = getNextNotificationId(prefs.getInt(PREF_NOTIFICATION_ID, 0));
         final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
diff --git a/mobile/android/components/LoginManagerPrompter.js b/mobile/android/components/LoginManagerPrompter.js
index de4c7616f44b3d431adcc2dc4d9eeeb3a75111c4..e70afbe147c4f232a32ce2e7b7f8bf3cbd1948f1 100644
--- a/mobile/android/components/LoginManagerPrompter.js
+++ b/mobile/android/components/LoginManagerPrompter.js
@@ -129,6 +129,7 @@ LoginManagerPrompter.prototype = {
   promptToSavePassword : function (aLogin) {
     this._showSaveLoginNotification(aLogin);
       Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION").add(PROMPT_DISPLAYED);
+    Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save", null);
   },
 
   /*
@@ -228,6 +229,8 @@ LoginManagerPrompter.prototype = {
   promptToChangePassword : function (aOldLogin, aNewLogin) {
     this._showChangeLoginNotification(aOldLogin, aNewLogin.password);
     Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION").add(PROMPT_DISPLAYED);
+    let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
+    Services.obs.notifyObservers(aNewLogin, "passwordmgr-prompt-change", oldGUID);
   },
 
   /*
diff --git a/mobile/android/components/SessionStore.js b/mobile/android/components/SessionStore.js
index f56b22bdc6a586d33240ad17186b54f975be54c5..5bfd05410894082d104a6cf4d04172f4818ffdf2 100644
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -86,6 +86,11 @@ SessionStore.prototype = {
   // Whether or not to send notifications for changes to the closed tabs.
   _notifyClosedTabs: false,
 
+  // If we're simultaneously closing both a tab and Firefox, we don't want
+  // to bother reloading the newly selected tab if it is zombified.
+  // The Java UI will tell us which tab to watch out for.
+  _keepAsZombieTabId: -1,
+
   init: function ss_init() {
     loggingEnabled = Services.prefs.getBoolPref("browser.sessionstore.debug_logging");
 
@@ -141,6 +146,7 @@ SessionStore.prototype = {
         observerService.addObserver(this, "quit-application", true);
         observerService.addObserver(this, "Session:Restore", true);
         observerService.addObserver(this, "Session:NotifyLocationChange", true);
+        observerService.addObserver(this, "Tab:KeepZombified", true);
         observerService.addObserver(this, "application-background", true);
         observerService.addObserver(this, "application-foreground", true);
         observerService.addObserver(this, "ClosedTabs:StartNotifications", true);
@@ -284,6 +290,13 @@ SessionStore.prototype = {
         }
         break;
       }
+      case "Tab:KeepZombified": {
+        if (aData >= 0) {
+          this._keepAsZombieTabId = aData;
+          log("Tab:KeepZombified " + aData);
+        }
+        break;
+      }
       case "application-background":
         // We receive this notification when Android's onPause callback is
         // executed. After onPause, the application may be terminated at any
@@ -301,6 +314,15 @@ SessionStore.prototype = {
         log("application-foreground");
         this._interval = Services.prefs.getIntPref("browser.sessionstore.interval");
         this._minSaveDelay = MINIMUM_SAVE_DELAY;
+
+        // If we skipped restoring a zombified tab before backgrounding,
+        // we might have to do it now instead.
+        let window = Services.wm.getMostRecentWindow("navigator:browser");
+        let tab = window.BrowserApp.selectedTab;
+
+        if (tab.browser.__SS_restore) {
+          this._restoreZombieTab(tab.browser, tab.id);
+        }
         break;
       case "ClosedTabs:StartNotifications":
         this._notifyClosedTabs = true;
@@ -388,7 +410,8 @@ SessionStore.prototype = {
         }
         break;
       }
-      case "pageshow": {
+      case "pageshow":
+      case "AboutReaderContentReady": {
         let browser = aEvent.currentTarget;
 
         // Skip subframe pageshows.
@@ -396,12 +419,19 @@ SessionStore.prototype = {
           return;
         }
 
+        if (browser.currentURI.spec.startsWith("about:reader") &&
+            !browser.contentDocument.body.classList.contains("loaded")) {
+          // Don't restore the scroll position of an about:reader page at this point;
+          // wait for the custom event dispatched from AboutReader.jsm instead.
+          return;
+        }
+
         // Restoring the scroll position needs to happen after the zoom level has been
         // restored, which is done by the MobileViewportManager either on first paint
         // or on load, whichever comes first.
         // In the latter case, our load handler runs before the MVM's one, which is the
         // wrong way around, so we have to use a later event instead.
-        log("pageshow for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+        log(aEvent.type + " for tab " + window.BrowserApp.getTabForBrowser(browser).id);
         if (browser.__SS_restoreDataOnPageshow) {
           delete browser.__SS_restoreDataOnPageshow;
           this._restoreScrollPosition(browser.__SS_data.scrolldata, browser);
@@ -514,6 +544,7 @@ SessionStore.prototype = {
     // Gecko might set the initial zoom level after the JS "load" event,
     // so we have to restore zoom and scroll position after that.
     aBrowser.addEventListener("pageshow", this, true);
+    aBrowser.addEventListener("AboutReaderContentReady", this, true);
 
     // Use a combination of events to watch for text data changes
     aBrowser.addEventListener("change", this, true);
@@ -537,6 +568,7 @@ SessionStore.prototype = {
     aBrowser.removeEventListener("DOMTitleChanged", this, true);
     aBrowser.removeEventListener("load", this, true);
     aBrowser.removeEventListener("pageshow", this, true);
+    aBrowser.removeListener("AboutReaderContentReady", this, true);
     aBrowser.removeEventListener("change", this, true);
     aBrowser.removeEventListener("input", this, true);
     aBrowser.removeEventListener("DOMAutoComplete", this, true);
@@ -657,13 +689,14 @@ SessionStore.prototype = {
 
     // Restore the resurrected browser
     if (aBrowser.__SS_restore) {
-      let data = aBrowser.__SS_data;
-      this._restoreTab(data, aBrowser);
-
-      delete aBrowser.__SS_restore;
-      aBrowser.removeAttribute("pending");
-      log("onTabSelect() restored zombie tab " + tabId);
+      if (tabId != this._keepAsZombieTabId) {
+        this._restoreZombieTab(aBrowser, tabId);
+      } else {
+        log("keeping as zombie tab " + tabId);
+      }
     }
+    // The tab id passed through Tab:KeepZombified is valid for one TabSelect only.
+    this._keepAsZombieTabId = -1;
 
     log("onTabSelect() ran for tab " + tabId);
     this.saveStateDelayed();
@@ -677,6 +710,15 @@ SessionStore.prototype = {
     }
   },
 
+  _restoreZombieTab: function ss_restoreZombieTab(aBrowser, aTabId) {
+    let data = aBrowser.__SS_data;
+    this._restoreTab(data, aBrowser);
+
+    delete aBrowser.__SS_restore;
+    aBrowser.removeAttribute("pending");
+    log("restoring zombie tab " + aTabId);
+  },
+
   onTabInput: function ss_onTabInput(aWindow, aBrowser) {
     // If this browser belongs to a zombie tab or the initial restore hasn't yet finished,
     // skip any session save activity.
diff --git a/mobile/android/tests/browser/chrome/test_session_scroll_position.html b/mobile/android/tests/browser/chrome/test_session_scroll_position.html
index fb0b4c8aac452ccbcd728a67880a4bbac3ab7d12..cfbeb5164b7eaea4b650a00ff2bde049eb649221 100644
--- a/mobile/android/tests/browser/chrome/test_session_scroll_position.html
+++ b/mobile/android/tests/browser/chrome/test_session_scroll_position.html
@@ -34,6 +34,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=810981
   const URL = "http://example.org/chrome/mobile/android/tests/browser/chrome/basic_article_mobile.html";
   // Something to test the zoom level scaling on rotation with.
   const URL_desktop = "http://example.org/chrome/mobile/android/tests/browser/chrome/basic_article.html";
+  // Reader mode URL
+  const URL_reader = "about:reader?url=http%3A%2F%2Fexample.org%2Fchrome%2Fmobile%2Fandroid%2Ftests%2Fbrowser%2Fchrome%2Fbasic_article_mobile.html";
 
   function dispatchUIEvent(browser, type) {
     let event = browser.contentDocument.createEvent("UIEvents");
@@ -109,6 +111,61 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=810981
     BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
   });
 
+  add_task(function* test_sessionStoreScrollPositionReaderMode() {
+    const SCROLL_X = 0;
+    const SCROLL_Y = 44;
+
+    chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+    let BrowserApp = chromeWin.BrowserApp;
+
+    // Creates a tab, sets a scroll position and closes the tab.
+    function createAndRemoveReaderTab() {
+      return Task.spawn(function () {
+        // Create a new tab.
+        tabScroll = BrowserApp.addTab(URL_reader);
+        let browser = tabScroll.browser;
+        yield promiseBrowserEvent(browser, "AboutReaderContentReady");
+
+        // Modify scroll position.
+        setScrollPosition(browser, SCROLL_X, SCROLL_Y);
+        yield promiseTabEvent(browser, "SSTabScrollCaptured");
+
+        // Check that we've actually scrolled.
+        let ifreq = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
+        let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
+        let scrollX = {}, scrollY = {};
+        utils.getScrollXY(false, scrollX, scrollY);
+        is(scrollX.value, SCROLL_X, "scrollX set correctly");
+        is(scrollY.value, SCROLL_Y, "scrollY set correctly");
+
+        // Remove the tab.
+        BrowserApp.closeTab(tabScroll);
+        yield promiseTabEvent(browser, "SSTabCloseProcessed");
+      });
+    }
+
+    yield createAndRemoveReaderTab();
+    let state = ss.getClosedTabs(chromeWin);
+    let [{scrolldata}] = state;
+    is(scrolldata.scroll, SCROLL_X + "," + SCROLL_Y, "stored scroll position is correct");
+
+    // Restore the closed tab.
+    let closedTabData = ss.getClosedTabs(chromeWin)[0];
+    let browser = ss.undoCloseTab(chromeWin, closedTabData);
+    yield promiseBrowserEvent(browser, "AboutReaderContentReady");
+
+    // Check the scroll position.
+    let ifreq = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
+    let utils = ifreq.getInterface(Ci.nsIDOMWindowUtils);
+    let scrollX = {}, scrollY = {};
+    utils.getScrollXY(false, scrollX, scrollY);
+    is(scrollX.value, SCROLL_X, "scrollX restored correctly");
+    is(scrollY.value, SCROLL_Y, "scrollY restored correctly");
+
+    // Remove the tab.
+    BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
+  });
+
   add_task(function* test_sessionStoreZoomLevel() {
     const ZOOM = 4.2;
     const SCROLL_X = 42;
diff --git a/mobile/android/tests/browser/chrome/test_session_zombification.html b/mobile/android/tests/browser/chrome/test_session_zombification.html
index baafed5b734f6e968c7a14df280b33df8672ca58..eba255ff62ec35ff4dda36d9c6282a9ec15eb923 100644
--- a/mobile/android/tests/browser/chrome/test_session_zombification.html
+++ b/mobile/android/tests/browser/chrome/test_session_zombification.html
@@ -96,6 +96,81 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1044556
     is(tabTest.browser.currentURI.spec, url2, "Test tab is showing the second URL.");
   });
 
+  add_task(function* test_sessionStoreKeepAsZombie() {
+    chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+    let BrowserApp = chromeWin.BrowserApp;
+    let observerService = Services.obs;
+
+    SimpleTest.registerCleanupFunction(function() {
+      BrowserApp.closeTab(tabBlank);
+      BrowserApp.closeTab(tabTest);
+    });
+
+    // Add a new tab with some content
+    tabTest = BrowserApp.addTab(url1 , { selected: true, parentId: BrowserApp.selectedTab.id });
+    yield promiseBrowserEvent(tabTest.browser, "DOMContentLoaded");
+
+    // Add a new tab with a blank page
+    tabBlank = BrowserApp.addTab("about:blank", { selected: true, parentId: BrowserApp.selectedTab.id });
+    yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+    is(BrowserApp.selectedTab, tabBlank, "Test tab is in background.");
+
+    // Zombify the backgrounded test tab
+    MemoryObserver.zombify(tabTest);
+
+    // Check that the test tab is actually zombified
+    ok(tabTest.browser.__SS_restore, "Test tab is set for delay loading.");
+    is(tabTest.browser.currentURI.spec, "about:blank", "Test tab is zombified.");
+
+    // Tell the session store that it shouldn't restore that tab on selecting
+    observerService.notifyObservers(null, "Tab:KeepZombified", tabTest.id);
+
+    // Switch back to the test tab and check that it remains zombified
+    BrowserApp.selectTab(tabTest);
+    yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+    is(BrowserApp.selectedTab, tabTest, "Test tab is selected.");
+    ok(tabTest.browser.__SS_restore, "Test tab is still set for delay loading.");
+
+    // Switch to the other tab and back again
+    BrowserApp.selectTab(tabBlank);
+    yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+    is(BrowserApp.selectedTab, tabBlank, "Test tab is in background.");
+    BrowserApp.selectTab(tabTest);
+
+    // "Tab:KeepZombified should be good for one TabSelect only
+    yield promiseBrowserEvent(tabTest.browser, "DOMContentLoaded");
+    is(BrowserApp.selectedTab, tabTest, "Test tab is selected.");
+
+    // Check that the test tab is no longer a zombie and has loaded the correct url
+    ok(!tabTest.browser.__SS_restore, "Test tab is no longer set for delay loading.");
+    is(tabTest.browser.currentURI.spec, url1, "Test tab is showing the test URL.");
+
+    // Zombify the test tab again
+    BrowserApp.selectTab(tabBlank);
+    yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+    is(BrowserApp.selectedTab, tabBlank, "Test tab is in background.");
+    MemoryObserver.zombify(tabTest);
+    ok(tabTest.browser.__SS_restore, "Test tab is set for delay loading.");
+    is(tabTest.browser.currentURI.spec, "about:blank", "Test tab is zombified.");
+
+    // Tell the session store that it shouldn't restore that tab on selecting
+    observerService.notifyObservers(null, "Tab:KeepZombified", tabTest.id);
+
+    // Switch back to the test tab and check that it remains zombified
+    BrowserApp.selectTab(tabTest);
+    yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+    is(BrowserApp.selectedTab, tabTest, "Test tab is selected.");
+    ok(tabTest.browser.__SS_restore, "Test tab is still set for delay loading.");
+
+    // Fake an "application-foreground" notification
+    observerService.notifyObservers(null, "application-foreground", null);
+
+    // The test tab should now start reloading
+    yield promiseBrowserEvent(tabTest.browser, "DOMContentLoaded");
+    ok(!tabTest.browser.__SS_restore, "Test tab is no longer set for delay loading.");
+    is(tabTest.browser.currentURI.spec, url1, "Test tab is showing the test URL.");
+  });
+
   </script>
 </head>
 <body>
diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js
index 45dcf351614a21c70c560e3e50a27faf36860a6e..bd33703315f33d0ddff353e1ba1971985660fcc3 100644
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -297,6 +297,13 @@ pref("mathml.disabled",    false);
 // Enable scale transform for stretchy MathML operators. See bug 414277.
 pref("mathml.scale_stretchy_operators.enabled", true);
 
+// Disable MediaError.message.
+#ifdef RELEASE_BUILD
+pref("dom.MediaError.message.enabled", false);
+#else
+pref("dom.MediaError.message.enabled", true);
+#endif
+
 // Media cache size in kilobytes
 pref("media.cache_size", 512000);
 // When a network connection is suspended, don't resume it until the
@@ -4313,7 +4320,6 @@ pref("signon.autofillForms",                true);
 pref("signon.autologin.proxy",              false);
 pref("signon.formlessCapture.enabled",      true);
 pref("signon.storeWhenAutocompleteOff",     true);
-pref("signon.ui.experimental",              false);
 pref("signon.debug",                        false);
 pref("signon.recipes.path",                 "chrome://passwordmgr/content/recipes.json");
 pref("signon.schemeUpgrades",               false);
diff --git a/old-configure.in b/old-configure.in
index 545e82f83d56ed8d2e57fb943967b721196e3d97..f9526eab531c776ad0009b7279d49c24cd356183 100644
--- a/old-configure.in
+++ b/old-configure.in
@@ -1143,6 +1143,10 @@ case "$target" in
             # use such pragmas, so just ignore them.
             CFLAGS="$CFLAGS -Wno-unknown-pragmas"
             CXXFLAGS="$CXXFLAGS -Wno-unknown-pragmas"
+            # We get errors about various #pragma intrinsic directives from
+            # clang-cl, and we don't need to hear about those.
+            CFLAGS="$CFLAGS -Wno-ignored-pragmas"
+            CXXFLAGS="$CXXFLAGS -Wno-ignored-pragmas"
             # clang-cl's Intrin.h marks things like _ReadWriteBarrier
             # as __attribute((__deprecated__)).  This is nice to know,
             # but since we don't get the equivalent warning from MSVC,
diff --git a/security/manager/pki/resources/content/certManager.xul b/security/manager/pki/resources/content/certManager.xul
index 3522d60f22f387ce1fb6ab5c36802e6c10aea945..3ea5862e49c8c59094784ba2e9807c0a267e8f55 100644
--- a/security/manager/pki/resources/content/certManager.xul
+++ b/security/manager/pki/resources/content/certManager.xul
@@ -13,14 +13,14 @@
 
 <!DOCTYPE dialog SYSTEM "chrome://pippki/locale/certManager.dtd">
 
-<dialog id="certmanager" 
+<dialog id="certmanager"
 	windowtype="mozilla:certmanager"
-	xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" 
+	xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         title="&certmgr.title;"
         onload="LoadCerts();"
         onunload="DeregisterSmartCardObservers();"
         buttons="accept"
-        style="width: 48em; height: 32em;"
+        style="width: 63em; height: 32em;"
         persist="screenX screenY width height">
 
   <stringbundle id="pippki_bundle" src="chrome://pippki/locale/pippki.properties"/>
diff --git a/security/nss/automation/taskcluster/windows/releng.manifest b/security/nss/automation/taskcluster/windows/releng.manifest
index b3f4498540b8afde95b7372edf534718be85533d..403be2b04627c4e9a4cce2062c14a0bc4b6b935b 100644
--- a/security/nss/automation/taskcluster/windows/releng.manifest
+++ b/security/nss/automation/taskcluster/windows/releng.manifest
@@ -1,10 +1,10 @@
 [
   {
-    "version": "Visual Studio 2015 Update 2 / SDK 10.0.10586.0/212",
-    "size": 332442800,
-    "digest": "995394a4a515c7cb0f8595f26f5395361a638870dd0bbfcc22193fe1d98a0c47126057d5999cc494f3f3eac5cb49160e79757c468f83ee5797298e286ef6252c",
+    "version": "Visual Studio 2015 Update 3 14.0.25425.01 / SDK 10.0.14393.0",
+    "size": 326656969,
+    "digest": "babc414ffc0457d27f5a1ed24a8e4873afbe2f1c1a4075469a27c005e1babc3b2a788f643f825efedff95b79686664c67ec4340ed535487168a3482e68559bc7",
     "algorithm": "sha512",
-    "filename": "vs2015u2.zip",
+    "filename": "vs2015u3.zip",
     "unpack": true
   }
 ]
diff --git a/security/nss/automation/taskcluster/windows/setup.sh b/security/nss/automation/taskcluster/windows/setup.sh
index 80cee2850e155c16651023b4ffb593ac2871ebdd..32732774a4888990746c2558ebebeefb97c67c86 100644
--- a/security/nss/automation/taskcluster/windows/setup.sh
+++ b/security/nss/automation/taskcluster/windows/setup.sh
@@ -18,7 +18,7 @@ hg_clone() {
 hg_clone https://hg.mozilla.org/build/tools tools default
 
 tools/scripts/tooltool/tooltool_wrapper.sh $(dirname $0)/releng.manifest https://api.pub.build.mozilla.org/tooltool/ non-existant-file.sh /c/mozilla-build/python/python.exe /c/builds/tooltool.py --authentication-file /c/builds/relengapi.tok -c /c/builds/tooltool_cache
-VSPATH="$(pwd)/vs2015u2"
+VSPATH="$(pwd)/vs2015u3"
 
 export WINDOWSSDKDIR="${VSPATH}/SDK"
 export WIN32_REDIST_DIR="${VSPATH}/VC/redist/x64/Microsoft.VC140.CRT"
@@ -26,5 +26,5 @@ export WIN_UCRT_REDIST_DIR="${VSPATH}/SDK/Redist/ucrt/DLLs/x64"
 
 export PATH="${VSPATH}/VC/bin/amd64:${VSPATH}/VC/bin:${VSPATH}/SDK/bin/x64:${VSPATH}/VC/redist/x64/Microsoft.VC140.CRT:${VSPATH}/SDK/Redist/ucrt/DLLs/x64:${PATH}"
 
-export INCLUDE="${VSPATH}/VC/include:${VSPATH}/SDK/Include/10.0.10586.0/ucrt:${VSPATH}/SDK/Include/10.0.10586.0/shared:${VSPATH}/SDK/Include/10.0.10586.0/um"
-export LIB="${VSPATH}/VC/lib/amd64:${VSPATH}/SDK/lib/10.0.10586.0/ucrt/x64:${VSPATH}/SDK/lib/10.0.10586.0/um/x64"
+export INCLUDE="${VSPATH}/VC/include:${VSPATH}/SDK/Include/10.0.14393.0/ucrt:${VSPATH}/SDK/Include/10.0.14393.0/shared:${VSPATH}/SDK/Include/10.0.14393.0/um"
+export LIB="${VSPATH}/VC/lib/amd64:${VSPATH}/SDK/lib/10.0.14393.0/ucrt/x64:${VSPATH}/SDK/lib/10.0.14393.0/um/x64"
diff --git a/security/sandbox/mac/Sandbox.h b/security/sandbox/mac/Sandbox.h
index 525544e8f3191a503d1789062a27609f87a047e4..b2e1a7ec5430d1c0831cdab61b97d314db1692d2 100644
--- a/security/sandbox/mac/Sandbox.h
+++ b/security/sandbox/mac/Sandbox.h
@@ -39,16 +39,21 @@ typedef struct _MacSandboxInfo {
   _MacSandboxInfo()
     : type(MacSandboxType_Default), level(0) {}
   _MacSandboxInfo(const struct _MacSandboxInfo& other)
-    : type(other.type), level(other.level), pluginInfo(other.pluginInfo),
+    : type(other.type), level(other.level),
+      hasSandboxedProfile(other.hasSandboxedProfile),
+      pluginInfo(other.pluginInfo),
       appPath(other.appPath), appBinaryPath(other.appBinaryPath),
-      appDir(other.appDir), appTempDir(other.appTempDir) {}
+      appDir(other.appDir), appTempDir(other.appTempDir),
+      profileDir(other.profileDir) {}
   MacSandboxType type;
   int32_t level;
+  bool hasSandboxedProfile;
   MacSandboxPluginInfo pluginInfo;
   std::string appPath;
   std::string appBinaryPath;
   std::string appDir;
   std::string appTempDir;
+  std::string profileDir;
 } MacSandboxInfo;
 
 namespace mozilla {
diff --git a/security/sandbox/mac/Sandbox.mm b/security/sandbox/mac/Sandbox.mm
index 09af10280a4e66d78b6af47f54a03b1cb0c345c1..dcceb9e1c8f663283d45129b1e35c9f2c9400f9b 100644
--- a/security/sandbox/mac/Sandbox.mm
+++ b/security/sandbox/mac/Sandbox.mm
@@ -157,6 +157,8 @@ static const char contentSandboxRules[] =
   "(define appBinaryPath \"%s\")\n"
   "(define appDir \"%s\")\n"
   "(define appTempDir \"%s\")\n"
+  "(define hasProfileDir %d)\n"
+  "(define profileDir \"%s\")\n"
   "(define home-path \"%s\")\n"
   "\n"
   "; Allow read access to standard system paths.\n"
@@ -232,6 +234,9 @@ static const char contentSandboxRules[] =
   "  (define (home-literal home-relative-literal)\n"
   "    (resolving-literal (string-append home-path home-relative-literal)))\n"
   "\n"
+  "  (define (profile-subpath profile-relative-subpath)\n"
+  "    (resolving-subpath (string-append profileDir profile-relative-subpath)))\n"
+  "\n"
   "  (define (container-regex container-relative-regex)\n"
   "    (resolving-regex (string-append \"^\" (regex-quote container-path) container-relative-regex)))\n"
   "  (define (container-subpath container-relative-subpath)\n"
@@ -368,19 +373,27 @@ static const char contentSandboxRules[] =
   "      (var-folders2-regex \"/[^/]+\\.mozrunner/extensions/[^/]+/chrome/[^/]+/content/[^/]+\\.j(s|ar)$\"))\n"
   "\n"
   "  (allow file-write* (var-folders2-regex \"/org\\.chromium\\.[a-zA-Z0-9]*$\"))\n"
+  "\n"
+  "; Per-user and system-wide Extensions dir\n"
   "  (allow file-read*\n"
   "      (home-regex \"/Library/Application Support/[^/]+/Extensions/[^/]/\")\n"
-  "      (resolving-regex \"/Library/Application Support/[^/]+/Extensions/[^/]/\")\n"
-  "      (home-regex \"/Library/Application Support/Firefox/Profiles/[^/]+/extensions/\")\n"
-  "      (home-regex \"/Library/Application Support/Firefox/Profiles/[^/]+/weave/\"))\n"
+  "      (resolving-regex \"/Library/Application Support/[^/]+/Extensions/[^/]/\"))\n"
   "\n"
-  "; the following rules should be removed when printing and \n"
+  "; Profile subdirectories\n"
+  "  (if (not (zero? hasProfileDir)) (allow file-read*\n"
+  "      (profile-subpath \"/extensions\")\n"
+  "      (profile-subpath \"/weave\")))\n"
+  "\n"
+  "; the following rules should be removed when printing and\n"
   "; opening a file from disk are brokered through the main process\n"
-  "  (if\n"
-  "    (< sandbox-level 2)\n"
-  "    (allow file*\n"
-  "        (require-not\n"
-  "            (home-subpath \"/Library\")))\n"
+  "  (if (< sandbox-level 2)\n"
+  "    (if (not (zero? hasProfileDir))\n"
+  "      (allow file*\n"
+  "          (require-all\n"
+  "              (require-not (home-subpath \"/Library\"))\n"
+  "              (require-not (subpath profileDir))))\n"
+  "      (allow file*\n"
+  "          (require-not (home-subpath \"/Library\"))))\n"
   "    (allow file*\n"
   "        (require-all\n"
   "            (subpath home-path)\n"
@@ -497,6 +510,8 @@ bool StartMacSandbox(MacSandboxInfo aInfo, std::string &aErrorMessage)
                aInfo.appBinaryPath.c_str(),
                aInfo.appDir.c_str(),
                aInfo.appTempDir.c_str(),
+               aInfo.hasSandboxedProfile ? 1 : 0,
+               aInfo.profileDir.c_str(),
                getenv("HOME"));
     } else {
       fprintf(stderr,
diff --git a/security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp b/security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
index 2c8940c6eb04f98872372f86513db9ce40482177..f9c1e45a5644e93b5099a5edc6df35062c07580f 100644
--- a/security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
+++ b/security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
@@ -8,6 +8,7 @@
 
 #include "base/win/windows_version.h"
 #include "mozilla/Assertions.h"
+#include "mozilla/Logging.h"
 #include "sandbox/win/src/sandbox.h"
 #include "sandbox/win/src/security_level.h"
 
@@ -16,6 +17,10 @@ namespace mozilla
 
 sandbox::BrokerServices *SandboxBroker::sBrokerService = nullptr;
 
+static LazyLogModule sSandboxBrokerLog("SandboxBroker");
+
+#define LOG_E(...) MOZ_LOG(sSandboxBrokerLog, LogLevel::Error, (__VA_ARGS__))
+
 /* static */
 void
 SandboxBroker::Initialize(sandbox::BrokerServices* aBrokerServices)
@@ -122,7 +127,7 @@ SandboxBroker::SetSecurityLevelForContentProcess(int32_t aSandboxLevel)
     delayedIntegrityLevel = sandbox::INTEGRITY_LEVEL_LOW;
   }
 
-  sandbox::ResultCode result = mPolicy->SetJobLevel(jobLevel,
+  sandbox::ResultCode result = mPolicy->SetJobLevel(jobLevel,
                                                     0 /* ui_exceptions */);
   MOZ_RELEASE_ASSERT(sandbox::SBOX_ALL_OK == result,
                      "Setting job level failed, have you set memory limit when jobLevel == JOB_NONE?");
@@ -458,7 +463,12 @@ SandboxBroker::AllowReadFile(wchar_t const *file)
     mPolicy->AddRule(sandbox::TargetPolicy::SUBSYS_FILES,
                      sandbox::TargetPolicy::FILES_ALLOW_READONLY,
                      file);
-  return (sandbox::SBOX_ALL_OK == result);
+  if (sandbox::SBOX_ALL_OK != result) {
+    LOG_E("Failed (ResultCode %d) to add read access to: %S", result, file);
+    return false;
+  }
+
+  return true;
 }
 
 bool
@@ -472,7 +482,13 @@ SandboxBroker::AllowReadWriteFile(wchar_t const *file)
     mPolicy->AddRule(sandbox::TargetPolicy::SUBSYS_FILES,
                      sandbox::TargetPolicy::FILES_ALLOW_ANY,
                      file);
-  return (sandbox::SBOX_ALL_OK == result);
+  if (sandbox::SBOX_ALL_OK != result) {
+    LOG_E("Failed (ResultCode %d) to add read/write access to: %S",
+          result, file);
+    return false;
+  }
+
+  return true;
 }
 
 bool
@@ -486,7 +502,12 @@ SandboxBroker::AllowDirectory(wchar_t const *dir)
     mPolicy->AddRule(sandbox::TargetPolicy::SUBSYS_FILES,
                      sandbox::TargetPolicy::FILES_ALLOW_DIR_ANY,
                      dir);
-  return (sandbox::SBOX_ALL_OK == result);
+  if (sandbox::SBOX_ALL_OK != result) {
+    LOG_E("Failed (ResultCode %d) to add directory access to: %S", result, dir);
+    return false;
+  }
+
+  return true;
 }
 
 bool
diff --git a/services/crypto/modules/utils.js b/services/crypto/modules/utils.js
index 0c727c2c507403a8d382b3393387ed65e158f85c..c17f5dfa185a4220bee362aec55836171b3d5e37 100644
--- a/services/crypto/modules/utils.js
+++ b/services/crypto/modules/utils.js
@@ -106,6 +106,13 @@ this.CryptoUtils = {
     return CommonUtils.encodeBase32(CryptoUtils.UTF8AndSHA1(message));
   },
 
+  sha256(message) {
+    let hasher = Cc["@mozilla.org/security/hash;1"]
+                 .createInstance(Ci.nsICryptoHash);
+    hasher.init(hasher.SHA256);
+    return CommonUtils.bytesAsHex(CryptoUtils.digestUTF8(message, hasher));
+  },
+
   /**
    * Produce an HMAC key object from a key string.
    */
diff --git a/services/sync/modules/browserid_identity.js b/services/sync/modules/browserid_identity.js
index c2edab785dc5da52a7337d33bfe01975f0792844..db382151842fc57f0d90b14cedf4bd10b3826176 100644
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -121,6 +121,10 @@ this.BrowserIDManager.prototype = {
     return this._token.hashed_fxa_uid
   },
 
+  deviceID() {
+    return this._signedInUser && this._signedInUser.deviceId;
+  },
+
   initialize: function() {
     for (let topic of OBSERVER_TOPICS) {
       Services.obs.addObserver(this, topic, false);
diff --git a/services/sync/modules/engines.js b/services/sync/modules/engines.js
index fb371692a34551918fdd5e03917c86bf60eff3c8..83b3d63f463a23a3b898c3b3dc5b4d8d15516237 100644
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -7,7 +7,8 @@ this.EXPORTED_SYMBOLS = [
   "Engine",
   "SyncEngine",
   "Tracker",
-  "Store"
+  "Store",
+  "Changeset"
 ];
 
 var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
@@ -131,26 +132,30 @@ Tracker.prototype = {
       this._ignored.splice(index, 1);
   },
 
+  _saveChangedID(id, when) {
+    this._log.trace(`Adding changed ID: ${id}, ${JSON.stringify(when)}`);
+    this.changedIDs[id] = when;
+    this.saveChangedIDs(this.onSavedChangedIDs);
+  },
+
   addChangedID: function (id, when) {
     if (!id) {
       this._log.warn("Attempted to add undefined ID to tracker");
       return false;
     }
 
-    if (this.ignoreAll || (id in this._ignored)) {
+    if (this.ignoreAll || this._ignored.includes(id)) {
       return false;
     }
 
     // Default to the current time in seconds if no time is provided.
     if (when == null) {
-      when = Math.floor(Date.now() / 1000);
+      when = Date.now() / 1000;
     }
 
     // Add/update the entry if we have a newer time.
     if ((this.changedIDs[id] || -Infinity) < when) {
-      this._log.trace("Adding changed ID: " + id + ", " + when);
-      this.changedIDs[id] = when;
-      this.saveChangedIDs(this.onSavedChangedIDs);
+      this._saveChangedID(id, when);
     }
 
     return true;
@@ -161,8 +166,9 @@ Tracker.prototype = {
       this._log.warn("Attempted to remove undefined ID to tracker");
       return false;
     }
-    if (this.ignoreAll || (id in this._ignored))
+    if (this.ignoreAll || this._ignored.includes(id)) {
       return false;
+    }
     if (this.changedIDs[id] != null) {
       this._log.trace("Removing changed ID " + id);
       delete this.changedIDs[id];
@@ -862,9 +868,8 @@ SyncEngine.prototype = {
   },
 
   /*
-   * Returns a mapping of IDs -> changed timestamp. Engine implementations
-   * can override this method to bypass the tracker for certain or all
-   * changed items.
+   * Returns a changeset for this sync. Engine implementations can override this
+   * method to bypass the tracker for certain or all changed items.
    */
   getChangedIDs: function () {
     return this._tracker.changedIDs;
@@ -932,20 +937,16 @@ SyncEngine.prototype = {
     // this._modified to the tracker.
     this.lastSyncLocal = Date.now();
     if (this.lastSync) {
-      this._modified = this.getChangedIDs();
+      this._modified = this.pullNewChanges();
     } else {
-      // Mark all items to be uploaded, but treat them as changed from long ago
       this._log.debug("First sync, uploading all items");
-      this._modified = {};
-      for (let id in this._store.getAllIDs()) {
-        this._modified[id] = 0;
-      }
+      this._modified = this.pullAllChanges();
     }
     // Clear the tracker now. If the sync fails we'll add the ones we failed
     // to upload back.
     this._tracker.clearChangedIDs();
 
-    this._log.info(Object.keys(this._modified).length +
+    this._log.info(this._modified.count() +
                    " outgoing items pre-reconciliation");
 
     // Keep track of what to delete at the end of sync
@@ -1293,12 +1294,12 @@ SyncEngine.prototype = {
     // because some state may change during the course of this function and we
     // need to operate on the original values.
     let existsLocally   = this._store.itemExists(item.id);
-    let locallyModified = item.id in this._modified;
+    let locallyModified = this._modified.has(item.id);
 
     // TODO Handle clock drift better. Tracked in bug 721181.
     let remoteAge = AsyncResource.serverTime - item.modified;
     let localAge  = locallyModified ?
-      (Date.now() / 1000 - this._modified[item.id]) : null;
+      (Date.now() / 1000 - this._modified.getModifiedTimestamp(item.id)) : null;
     let remoteIsNewer = remoteAge < localAge;
 
     this._log.trace("Reconciling " + item.id + ". exists=" +
@@ -1369,13 +1370,13 @@ SyncEngine.prototype = {
 
         // If the local item was modified, we carry its metadata forward so
         // appropriate reconciling can be performed.
-        if (dupeID in this._modified) {
+        if (this._modified.has(dupeID)) {
           locallyModified = true;
-          localAge = Date.now() / 1000 - this._modified[dupeID];
+          localAge = Date.now() / 1000 -
+            this._modified.getModifiedTimestamp(dupeID);
           remoteIsNewer = remoteAge < localAge;
 
-          this._modified[item.id] = this._modified[dupeID];
-          delete this._modified[dupeID];
+          this._modified.swap(dupeID, item.id);
         } else {
           locallyModified = false;
           localAge = null;
@@ -1409,7 +1410,7 @@ SyncEngine.prototype = {
       if (remoteIsNewer) {
         this._log.trace("Applying incoming because local item was deleted " +
                         "before the incoming item was changed.");
-        delete this._modified[item.id];
+        this._modified.delete(item.id);
         return true;
       }
 
@@ -1435,7 +1436,7 @@ SyncEngine.prototype = {
       this._log.trace("Ignoring incoming item because the local item is " +
                       "identical.");
 
-      delete this._modified[item.id];
+      this._modified.delete(item.id);
       return false;
     }
 
@@ -1460,7 +1461,7 @@ SyncEngine.prototype = {
   _uploadOutgoing: function () {
     this._log.trace("Uploading local changes to server.");
 
-    let modifiedIDs = Object.keys(this._modified);
+    let modifiedIDs = this._modified.ids();
     if (modifiedIDs.length) {
       this._log.trace("Preparing " + modifiedIDs.length +
                       " outgoing records");
@@ -1504,7 +1505,7 @@ SyncEngine.prototype = {
         counts.failed += failed.length;
 
         for (let id of successful) {
-          delete this._modified[id];
+          this._modified.delete(id);
         }
 
         this._onRecordsWritten(successful, failed);
@@ -1588,10 +1589,8 @@ SyncEngine.prototype = {
     }
 
     // Mark failed WBOs as changed again so they are reuploaded next time.
-    for (let [id, when] of Object.entries(this._modified)) {
-      this._tracker.addChangedID(id, when);
-    }
-    this._modified = {};
+    this.trackRemainingChanges();
+    this._modified.clear();
   },
 
   _sync: function () {
@@ -1677,5 +1676,108 @@ SyncEngine.prototype = {
     return (this.service.handleHMACEvent() && mayRetry) ?
            SyncEngine.kRecoveryStrategy.retry :
            SyncEngine.kRecoveryStrategy.error;
-  }
+  },
+
+  /**
+   * Returns a changeset containing all items in the store. The default
+   * implementation returns a changeset with timestamps from long ago, to
+   * ensure we always use the remote version if one exists.
+   *
+   * This function is only called for the first sync. Subsequent syncs call
+   * `pullNewChanges`.
+   *
+   * @return A `Changeset` object.
+   */
+  pullAllChanges() {
+    let changeset = new Changeset();
+    for (let id in this._store.getAllIDs()) {
+      changeset.set(id, 0);
+    }
+    return changeset;
+  },
+
+  /*
+   * Returns a changeset containing entries for all currently tracked items.
+   * The default implementation returns a changeset with timestamps indicating
+   * when the item was added to the tracker.
+   *
+   * @return A `Changeset` object.
+   */
+  pullNewChanges() {
+    return new Changeset(this.getChangedIDs());
+  },
+
+  /**
+   * Adds all remaining changeset entries back to the tracker, typically for
+   * items that failed to upload. This method is called at the end of each sync.
+   *
+   */
+  trackRemainingChanges() {
+    for (let [id, change] of this._modified.entries()) {
+      this._tracker.addChangedID(id, change);
+    }
+  },
 };
+
+/**
+ * A changeset is created for each sync in `Engine::get{Changed, All}IDs`,
+ * and stores opaque change data for tracked IDs. The default implementation
+ * only records timestamps, though engines can extend this to store additional
+ * data for each entry.
+ */
+class Changeset {
+  // Creates a changeset with an initial set of tracked entries.
+  constructor(changes = {}) {
+    this.changes = changes;
+  }
+
+  // Returns the last modified time, in seconds, for an entry in the changeset.
+  // `id` is guaranteed to be in the set.
+  getModifiedTimestamp(id) {
+    return this.changes[id];
+  }
+
+  // Adds a change for a tracked ID to the changeset.
+  set(id, change) {
+    this.changes[id] = change;
+  }
+
+  // Indicates whether an entry is in the changeset.
+  has(id) {
+    return id in this.changes;
+  }
+
+  // Deletes an entry from the changeset. Used to clean up entries for
+  // reconciled and successfully uploaded records.
+  delete(id) {
+    delete this.changes[id];
+  }
+
+  // Swaps two entries in the changeset. Used when reconciling duplicates that
+  // have local changes.
+  swap(oldID, newID) {
+    this.changes[newID] = this.changes[oldID];
+    delete this.changes[oldID];
+  }
+
+  // Returns an array of all tracked IDs in this changeset.
+  ids() {
+    return Object.keys(this.changes);
+  }
+
+  // Returns an array of `[id, change]` tuples. Used to repopulate the tracker
+  // with entries for failed uploads at the end of a sync.
+  entries() {
+    return Object.entries(this.changes);
+  }
+
+  // Returns the number of entries in this changeset.
+  count() {
+    return this.ids().length;
+  }
+
+  // Clears the changeset.
+  clear() {
+    this.changes = {};
+  }
+}
diff --git a/services/sync/modules/engines/bookmarks.js b/services/sync/modules/engines/bookmarks.js
index 6366afd992d3ce3ce596ce3937d6a979077e322d..f27fa0bc3d6ea2a4dd2003e0dec4f4ea4fa3f0b0 100644
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -33,6 +33,8 @@ const {
   SOURCE_IMPORT_REPLACE,
 } = Ci.nsINavBookmarksService;
 
+const SQLITE_MAX_VARIABLE_NUMBER = 999;
+
 // Maps Sync record property names to `PlacesSyncUtils` bookmark properties.
 const RECORD_PROPS_TO_BOOKMARK_PROPS = {
   title: "title",
@@ -426,7 +428,87 @@ BookmarksEngine.prototype = {
     // We must return a string, not an object, and the entries in the GUIDMap
     // are created via "new String()" making them an object.
     return mapped ? mapped.toString() : mapped;
-  }
+  },
+
+  pullAllChanges() {
+    return new BookmarksChangeset(this._store.getAllIDs());
+  },
+
+  pullNewChanges() {
+    let modifiedGUIDs = this._getModifiedGUIDs();
+    if (!modifiedGUIDs.length) {
+      return new BookmarksChangeset(this._tracker.changedIDs);
+    }
+
+    // We don't use `PlacesUtils.promiseDBConnection` here because
+    // `getChangedIDs` might be called while we're in a batch, meaning we
+    // won't see any changes until the batch finishes and the transaction
+    // commits.
+    let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+                        .DBConnection;
+
+    // Filter out tags, organizer queries, and other descendants that we're
+    // not tracking. We chunk `modifiedGUIDs` because SQLite limits the number
+    // of bound parameters per query.
+    for (let startIndex = 0;
+         startIndex < modifiedGUIDs.length;
+         startIndex += SQLITE_MAX_VARIABLE_NUMBER) {
+
+      let chunkLength = Math.min(startIndex + SQLITE_MAX_VARIABLE_NUMBER,
+                                 modifiedGUIDs.length);
+
+      let query = `
+        WITH RECURSIVE
+        modifiedGuids(guid) AS (
+          VALUES ${new Array(chunkLength).fill("(?)").join(", ")}
+        ),
+        syncedItems(id) AS (
+          VALUES ${getChangeRootIds().map(id => `(${id})`).join(", ")}
+          UNION ALL
+          SELECT b.id
+          FROM moz_bookmarks b
+          JOIN syncedItems s ON b.parent = s.id
+        )
+        SELECT b.guid, b.id
+        FROM modifiedGuids m
+        JOIN moz_bookmarks b ON b.guid = m.guid
+        LEFT JOIN syncedItems s ON b.id = s.id
+        WHERE s.id IS NULL
+      `;
+
+      let statement = db.createAsyncStatement(query);
+      try {
+        for (let i = 0; i < chunkLength; i++) {
+          statement.bindByIndex(i, modifiedGUIDs[startIndex + i]);
+        }
+        let results = Async.querySpinningly(statement, ["id", "guid"]);
+        for (let { id, guid } of results) {
+          let syncID = BookmarkSpecialIds.specialGUIDForId(id) || guid;
+          this._tracker.removeChangedID(syncID);
+        }
+      } finally {
+        statement.finalize();
+      }
+    }
+
+    return new BookmarksChangeset(this._tracker.changedIDs);
+  },
+
+  // Returns an array of Places GUIDs for all changed items. Ignores deletions,
+  // which won't exist in the DB and shouldn't be removed from the tracker.
+  _getModifiedGUIDs() {
+    let guids = [];
+    for (let syncID in this._tracker.changedIDs) {
+      if (this._tracker.changedIDs[syncID].deleted === true) {
+        // The `===` check also filters out old persisted timestamps,
+        // which won't have a `deleted` property.
+        continue;
+      }
+      let guid = BookmarkSpecialIds.syncIDToPlacesGUID(syncID);
+      guids.push(guid);
+    }
+    return guids;
+  },
 };
 
 function BookmarksStore(name, engine) {
@@ -639,13 +721,6 @@ BookmarksStore.prototype = {
     ));
   },
 
-  _getNode: function BStore__getNode(folder) {
-    let query = PlacesUtils.history.getNewQuery();
-    query.setFolders([folder], 1);
-    return PlacesUtils.history.executeQuery(
-      query, PlacesUtils.history.getNewQueryOptions()).root;
-  },
-
   _getTags: function BStore__getTags(uri) {
     try {
       if (typeof(uri) == "string")
@@ -828,51 +903,30 @@ BookmarksStore.prototype = {
     return index;
   },
 
-  _getChildren: function BStore_getChildren(guid, items) {
-    let node = guid; // the recursion case
-    if (typeof(node) == "string") { // callers will give us the guid as the first arg
-      let nodeID = this.idForGUID(guid, true);
-      if (!nodeID) {
-        this._log.debug("No node for GUID " + guid + "; returning no children.");
-        return items;
-      }
-      node = this._getNode(nodeID);
-    }
-
-    if (node.type == node.RESULT_TYPE_FOLDER) {
-      node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
-      node.containerOpen = true;
-      try {
-        // Remember all the children GUIDs and recursively get more
-        for (let i = 0; i < node.childCount; i++) {
-          let child = node.getChild(i);
-          items[this.GUIDForId(child.itemId)] = true;
-          this._getChildren(child, items);
-        }
-      }
-      finally {
-        node.containerOpen = false;
-      }
-    }
-
-    return items;
-  },
-
   getAllIDs: function BStore_getAllIDs() {
-    let items = {"menu": true,
-                 "toolbar": true,
-                 "unfiled": true,
-                };
-    // We also want "mobile" but only if a local mobile folder already exists
-    // (otherwise we'll later end up creating it, which we want to avoid until
-    // we actually need it.)
-    if (BookmarkSpecialIds.findMobileRoot(false)) {
-      items["mobile"] = true;
-    }
-    for (let guid of BookmarkSpecialIds.guids) {
-      if (guid != "places" && guid != "tags")
-        this._getChildren(guid, items);
+    let items = {};
+
+    let query = `
+      WITH RECURSIVE
+      changeRootContents(id) AS (
+        VALUES ${getChangeRootIds().map(id => `(${id})`).join(", ")}
+        UNION ALL
+        SELECT b.id
+        FROM moz_bookmarks b
+        JOIN changeRootContents c ON b.parent = c.id
+      )
+      SELECT id, guid
+      FROM changeRootContents
+      JOIN moz_bookmarks USING (id)
+    `;
+
+    let statement = this._getStmt(query);
+    let results = Async.querySpinningly(statement, ["id", "guid"]);
+    for (let { id, guid } of results) {
+      let syncID = BookmarkSpecialIds.specialGUIDForId(id) || guid;
+      items[syncID] = { modified: 0, deleted: false };
     }
+
     return items;
   },
 
@@ -964,16 +1018,50 @@ BookmarksTracker.prototype = {
     Ci.nsISupportsWeakReference
   ]),
 
+  addChangedID(id, change) {
+    if (!id) {
+      this._log.warn("Attempted to add undefined ID to tracker");
+      return false;
+    }
+    if (this._ignored.includes(id)) {
+      return false;
+    }
+    let shouldSaveChange = false;
+    let currentChange = this.changedIDs[id];
+    if (currentChange) {
+      if (typeof currentChange == "number") {
+        // Allow raw timestamps for backward-compatibility with persisted
+        // changed IDs. The new format uses tuples to track deleted items.
+        shouldSaveChange = currentChange < change.modified;
+      } else {
+        shouldSaveChange = currentChange.modified < change.modified ||
+                           currentChange.deleted != change.deleted;
+      }
+    } else {
+      shouldSaveChange = true;
+    }
+    if (shouldSaveChange) {
+      this._saveChangedID(id, change);
+    }
+    return true;
+  },
+
   /**
    * Add a bookmark GUID to be uploaded and bump up the sync score.
    *
-   * @param itemGuid
-   *        GUID of the bookmark to upload.
+   * @param itemId
+   *        The Places item ID of the bookmark to upload.
+   * @param guid
+   *        The Places GUID of the bookmark to upload.
+   * @param isTombstone
+   *        Whether we're uploading a tombstone for a removed bookmark.
    */
-  _add: function BMT__add(itemId, guid) {
+  _add: function BMT__add(itemId, guid, isTombstone = false) {
     guid = BookmarkSpecialIds.specialGUIDForId(itemId) || guid;
-    if (this.addChangedID(guid))
+    let info = { modified: Date.now() / 1000, deleted: isTombstone };
+    if (this.addChangedID(guid, info)) {
       this._upScore();
+    }
   },
 
   /* Every add/remove/change will trigger a sync for MULTI_DEVICE (except in
@@ -986,59 +1074,10 @@ BookmarksTracker.prototype = {
     }
   },
 
-  /**
-   * Determine if a change should be ignored.
-   *
-   * @param itemId
-   *        Item under consideration to ignore
-   * @param folder (optional)
-   *        Folder of the item being changed
-   * @param guid
-   *        Places GUID of the item being changed
-   * @param source
-   *        A change source constant from `nsINavBookmarksService::SOURCE_*`.
-   */
-  _ignore: function BMT__ignore(itemId, folder, guid, source) {
-    if (IGNORED_SOURCES.includes(source)) {
-      return true;
-    }
-
-    // Get the folder id if we weren't given one.
-    if (folder == null) {
-      try {
-        folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
-      } catch (ex) {
-        this._log.debug("getFolderIdForItem(" + itemId +
-                        ") threw; calling _ensureMobileQuery.");
-        // I'm guessing that gFIFI can throw, and perhaps that's why
-        // _ensureMobileQuery is here at all. Try not to call it.
-        this._ensureMobileQuery();
-        folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
-      }
-    }
-
-    // Ignore changes to tags (folders under the tags folder).
-    let tags = BookmarkSpecialIds.tags;
-    if (folder == tags)
-      return true;
-
-    // Ignore tag items (the actual instance of a tag for a bookmark).
-    if (PlacesUtils.bookmarks.getFolderIdForItem(folder) == tags)
-      return true;
-
-    // Make sure to remove items that have the exclude annotation.
-    if (PlacesUtils.annotations.itemHasAnnotation(itemId, BookmarkAnnos.EXCLUDEBACKUP_ANNO)) {
-      this.removeChangedID(guid);
-      return true;
-    }
-
-    return false;
-  },
-
   onItemAdded: function BMT_onItemAdded(itemId, folder, index,
                                         itemType, uri, title, dateAdded,
                                         guid, parentGuid, source) {
-    if (this._ignore(itemId, folder, guid, source)) {
+    if (IGNORED_SOURCES.includes(source)) {
       return;
     }
 
@@ -1049,12 +1088,50 @@ BookmarksTracker.prototype = {
 
   onItemRemoved: function (itemId, parentId, index, type, uri,
                            guid, parentGuid, source) {
-    if (this._ignore(itemId, parentId, guid, source)) {
+    if (IGNORED_SOURCES.includes(source)) {
       return;
     }
 
+    // Ignore changes to tags (folders under the tags folder).
+    if (parentId == PlacesUtils.tagsFolderId) {
+      return;
+    }
+
+    let grandParentId = -1;
+    try {
+      grandParentId = PlacesUtils.bookmarks.getFolderIdForItem(parentId);
+    } catch (ex) {
+      // `getFolderIdForItem` can throw if the item no longer exists, such as
+      // when we've removed a subtree using `removeFolderChildren`.
+      return;
+    }
+
+    // Ignore tag items (the actual instance of a tag for a bookmark).
+    if (grandParentId == PlacesUtils.tagsFolderId) {
+      return;
+    }
+
+    /**
+     * The above checks are incomplete: we can still write tombstones for
+     * items that we don't track, and upload extraneous roots.
+     *
+     * Consider the left pane root: it's a child of the Places root, and has
+     * children and grandchildren. `PlacesUIUtils` can create, delete, and
+     * recreate it as needed. We can't determine ancestors when the root or its
+     * children are deleted, because they've already been removed from the
+     * database when `onItemRemoved` is called. Likewise, we can't check their
+     * "exclude from backup" annos, because they've *also* been removed.
+     *
+     * So, we end up writing tombstones for the left pane queries and left
+     * pane root. For good measure, we'll also upload the Places root, because
+     * it's the parent of the left pane root.
+     *
+     * As a workaround, we can track the parent GUID and reconstruct the item's
+     * ancestry at sync time. This is complicated, and the previous behavior was
+     * already wrong, so we'll wait for bug 1258127 to fix this generally.
+     */
     this._log.trace("onItemRemoved: " + itemId);
-    this._add(itemId, guid);
+    this._add(itemId, guid, /* isTombstone */ true);
     this._add(parentId, parentGuid);
   },
 
@@ -1098,6 +1175,10 @@ BookmarksTracker.prototype = {
   onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value,
                                             lastModified, itemType, parentId,
                                             guid, parentGuid, source) {
+    if (IGNORED_SOURCES.includes(source)) {
+      return;
+    }
+
     if (isAnno && (ANNOS_TO_TRACK.indexOf(property) == -1))
       // Ignore annotations except for the ones that we sync.
       return;
@@ -1106,10 +1187,6 @@ BookmarksTracker.prototype = {
     if (property == "favicon")
       return;
 
-    if (this._ignore(itemId, parentId, guid, source)) {
-      return;
-    }
-
     this._log.trace("onItemChanged: " + itemId +
                     (", " + property + (isAnno? " (anno)" : "")) +
                     (value ? (" = \"" + value + "\"") : ""));
@@ -1120,7 +1197,7 @@ BookmarksTracker.prototype = {
                                         newParent, newIndex, itemType,
                                         guid, oldParentGuid, newParentGuid,
                                         source) {
-    if (this._ignore(itemId, newParent, guid, source)) {
+    if (IGNORED_SOURCES.includes(source)) {
       return;
     }
 
@@ -1146,3 +1223,26 @@ BookmarksTracker.prototype = {
   },
   onItemVisited: function () {}
 };
+
+// Returns an array of root IDs to recursively query for synced bookmarks.
+// Items in other roots, including tags and organizer queries, will be
+// ignored.
+function getChangeRootIds() {
+  let rootIds = [
+    PlacesUtils.bookmarksMenuFolderId,
+    PlacesUtils.toolbarFolderId,
+    PlacesUtils.unfiledBookmarksFolderId,
+  ];
+  let mobileRootId = BookmarkSpecialIds.findMobileRoot(false);
+  if (mobileRootId) {
+    rootIds.push(mobileRootId);
+  }
+  return rootIds;
+}
+
+class BookmarksChangeset extends Changeset {
+  getModifiedTimestamp(id) {
+    let change = this.changes[id];
+    return change ? change.modified : Number.NaN;
+  }
+}
diff --git a/services/sync/modules/engines/clients.js b/services/sync/modules/engines/clients.js
index 8e28f13c4c1b6936d6a274080ee1d7bcac27e5d1..18e27e2f2144d0baf50f5c9b0923093e5963a907 100644
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -311,7 +311,7 @@ ClientEngine.prototype = {
     const clientWithPendingCommands = Object.keys(this._currentlySyncingCommands);
     for (let clientId of clientWithPendingCommands) {
       if (this._store._remoteClients[clientId] || this.localID == clientId) {
-        this._modified[clientId] = 0;
+        this._modified.set(clientId, 0);
       }
     }
     SyncEngine.prototype._uploadOutgoing.call(this);
diff --git a/services/sync/modules/telemetry.js b/services/sync/modules/telemetry.js
index 2b2c3211b91b17ed98a7f5b9c30833ae10a3fa1e..a470e5a90048d6e69706bbc06c1c3d01f2348484 100644
--- a/services/sync/modules/telemetry.js
+++ b/services/sync/modules/telemetry.js
@@ -204,6 +204,7 @@ class TelemetryRecord {
       took: this.took,
       failureReason: this.failureReason,
       status: this.status,
+      deviceID: this.deviceID,
     };
     let engines = [];
     for (let engine of this.engines) {
@@ -228,8 +229,16 @@ class TelemetryRecord {
 
     try {
       this.uid = Weave.Service.identity.hashedUID();
+      let deviceID = Weave.Service.identity.deviceID();
+      if (deviceID) {
+        // Combine the raw device id with the metrics uid to create a stable
+        // unique identifier that can't be mapped back to the user's FxA
+        // identity without knowing the metrics HMAC key.
+        this.deviceID = Utils.sha256(deviceID + this.uid);
+      }
     } catch (e) {
       this.uid = "0".repeat(32);
+      this.deviceID = undefined;
     }
 
     // Check for engine statuses. -- We do this now, and not in engine.finished
diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js
index c4664ec42612d6736d504e83363819eb02967a8b..ddc9fc44060b05b437e47dd06514fc3f9938f8d4 100644
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -52,6 +52,7 @@ this.Utils = {
   digestBytes: CryptoUtils.digestBytes,
   sha1: CryptoUtils.sha1,
   sha1Base32: CryptoUtils.sha1Base32,
+  sha256: CryptoUtils.sha256,
   makeHMACKey: CryptoUtils.makeHMACKey,
   makeHMACHasher: CryptoUtils.makeHMACHasher,
   hkdfExpand: CryptoUtils.hkdfExpand,
diff --git a/services/sync/tests/unit/sync_ping_schema.json b/services/sync/tests/unit/sync_ping_schema.json
index 1590c202ca8741dba97be5f55319082b530e7da0..827be58fb07cb4396edc1b5c48d777d71b7f0800 100644
--- a/services/sync/tests/unit/sync_ping_schema.json
+++ b/services/sync/tests/unit/sync_ping_schema.json
@@ -26,6 +26,10 @@
           "type": "string",
           "pattern": "^[0-9a-f]{32}$"
         },
+        "deviceID": {
+          "type": "string",
+          "pattern": "^[0-9a-f]{64}$"
+        },
         "status": {
           "type": "object",
           "anyOf": [
diff --git a/services/sync/tests/unit/test_bookmark_engine.js b/services/sync/tests/unit/test_bookmark_engine.js
index e9dd2eb68fd7216ea9750bd8e49d65a10f1278de..fe443c75876798d33cfb1b7d262afbc7e6480e2f 100644
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -132,9 +132,6 @@ add_task(function* bad_record_allIDs() {
   _("Record is " + badRecordID);
   _("Type: " + PlacesUtils.bookmarks.getItemType(badRecordID));
 
-  _("Fetching children.");
-  store._getChildren("toolbar", {});
-
   _("Fetching all IDs.");
   let all = store.getAllIDs();
 
diff --git a/services/sync/tests/unit/test_bookmark_places_query_rewriting.js b/services/sync/tests/unit/test_bookmark_places_query_rewriting.js
index bcc25c1ec528247ace5e19c11c75835248f97ebb..0ddf81583eb2744c49789791c23053420dc09d08 100644
--- a/services/sync/tests/unit/test_bookmark_places_query_rewriting.js
+++ b/services/sync/tests/unit/test_bookmark_places_query_rewriting.js
@@ -33,15 +33,18 @@ function run_test() {
   _("Folder name: " + tagRecord.folderName);
   store.applyIncoming(tagRecord);
 
-  let tags = store._getNode(PlacesUtils.tagsFolderId);
-  tags.containerOpen = true;
+  let tags = PlacesUtils.getFolderContents(PlacesUtils.tagsFolderId).root;
   let tagID;
-  for (let i = 0; i < tags.childCount; ++i) {
-    let child = tags.getChild(i);
-    if (child.title == "bar")
-      tagID = child.itemId;
+  try {
+    for (let i = 0; i < tags.childCount; ++i) {
+      let child = tags.getChild(i);
+      if (child.title == "bar") {
+        tagID = child.itemId;
+      }
+    }
+  } finally {
+    tags.containerOpen = false;
   }
-  tags.containerOpen = false;
 
   _("Tag ID: " + tagID);
   let insertedRecord = store.createRecord("abcdefabcdef", "bookmarks");
diff --git a/services/sync/tests/unit/test_bookmark_tracker.js b/services/sync/tests/unit/test_bookmark_tracker.js
index 55a98d481737960c7a24f26a2934f08148ca08a6..7773faae89c398c51905cd3b05b7944ebb4eb899 100644
--- a/services/sync/tests/unit/test_bookmark_tracker.js
+++ b/services/sync/tests/unit/test_bookmark_tracker.js
@@ -22,8 +22,9 @@ const DAY_IN_MS = 24 * 60 * 60 * 1000;
 
 // Test helpers.
 function* verifyTrackerEmpty() {
-  do_check_empty(tracker.changedIDs);
-  do_check_eq(tracker.score, 0);
+  let changes = engine.pullNewChanges();
+  equal(changes.count(), 0);
+  equal(tracker.score, 0);
 }
 
 function* resetTracker() {
@@ -48,9 +49,12 @@ function* stopTracking() {
 }
 
 function* verifyTrackedItems(tracked) {
-  let trackedIDs = new Set(Object.keys(tracker.changedIDs));
+  let changes = engine.pullNewChanges();
+  let trackedIDs = new Set(changes.ids());
   for (let guid of tracked) {
-    ok(tracker.changedIDs[guid] > 0, `${guid} should be tracked`);
+    ok(changes.has(guid), `${guid} should be tracked`);
+    ok(changes.getModifiedTimestamp(guid) > 0,
+      `${guid} should have a modified time`);
     trackedIDs.delete(guid);
   }
   equal(trackedIDs.size, 0, `Unhandled tracked IDs: ${
@@ -58,7 +62,8 @@ function* verifyTrackedItems(tracked) {
 }
 
 function* verifyTrackedCount(expected) {
-  do_check_attribute_count(tracker.changedIDs, expected);
+  let changes = engine.pullNewChanges();
+  equal(changes.count(), expected);
 }
 
 add_task(function* test_tracking() {
@@ -389,7 +394,7 @@ add_task(function* test_onItemTagged() {
 
     // bookmark should be tracked, folder should not be.
     yield verifyTrackedItems([bGUID]);
-    do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
+    do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 5);
   } finally {
     _("Clean up.");
     yield cleanup();
@@ -522,7 +527,7 @@ add_task(function* test_async_onItemTagged() {
     });
 
     yield verifyTrackedItems([fxBmk1.guid, fxBmk2.guid]);
-    do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+    do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 6);
   } finally {
     _("Clean up.");
     yield cleanup();
@@ -700,31 +705,73 @@ add_task(function* test_onItemAnnoChanged() {
   }
 });
 
-add_task(function* test_onItemExcluded() {
-  _("Items excluded from backups should not be tracked");
+add_task(function* test_onItemAdded_filtered_root() {
+  _("Items outside the change roots should not be tracked");
+
+  try {
+    yield startTracking();
+
+    _("Create a new root");
+    let rootID = PlacesUtils.bookmarks.createFolder(
+      PlacesUtils.bookmarks.placesRoot,
+      "New root",
+      PlacesUtils.bookmarks.DEFAULT_INDEX);
+    let rootGUID = engine._store.GUIDForId(rootID);
+    _(`New root GUID: ${rootGUID}`);
+
+    _("Insert a bookmark underneath the new root");
+    let untrackedBmkID = PlacesUtils.bookmarks.insertBookmark(
+      rootID,
+      Utils.makeURI("http://getthunderbird.com"),
+      PlacesUtils.bookmarks.DEFAULT_INDEX,
+      "Get Thunderbird!");
+    let untrackedBmkGUID = engine._store.GUIDForId(untrackedBmkID);
+    _(`New untracked bookmark GUID: ${untrackedBmkGUID}`);
+
+    _("Insert a bookmark underneath the Places root");
+    let rootBmkID = PlacesUtils.bookmarks.insertBookmark(
+      PlacesUtils.bookmarks.placesRoot,
+      Utils.makeURI("http://getfirefox.com"),
+      PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
+    let rootBmkGUID = engine._store.GUIDForId(rootBmkID);
+    _(`New Places root bookmark GUID: ${rootBmkGUID}`);
+
+    _("New root and bookmark should be ignored");
+    yield verifyTrackedItems([]);
+    // ...But we'll still increment the score and filter out the changes at
+    // sync time.
+    do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 6);
+  } finally {
+    _("Clean up.");
+    yield cleanup();
+  }
+});
+
+add_task(function* test_onItemDeleted_filtered_root() {
+  _("Deleted items outside the change roots should be tracked");
 
   try {
     yield stopTracking();
 
-    _("Create a bookmark");
-    let b = PlacesUtils.bookmarks.insertBookmark(
-      PlacesUtils.bookmarks.bookmarksMenuFolder,
+    _("Insert a bookmark underneath the Places root");
+    let rootBmkID = PlacesUtils.bookmarks.insertBookmark(
+      PlacesUtils.bookmarks.placesRoot,
       Utils.makeURI("http://getfirefox.com"),
       PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
-    let bGUID = engine._store.GUIDForId(b);
+    let rootBmkGUID = engine._store.GUIDForId(rootBmkID);
+    _(`New Places root bookmark GUID: ${rootBmkGUID}`);
 
     yield startTracking();
 
-    _("Exclude the bookmark from backups");
-    PlacesUtils.annotations.setItemAnnotation(
-      b, BookmarkAnnos.EXCLUDEBACKUP_ANNO, "Don't back this up", 0,
-      PlacesUtils.annotations.EXPIRE_NEVER);
-
-    _("Modify the bookmark");
-    PlacesUtils.bookmarks.setItemTitle(b, "Download Firefox");
+    PlacesUtils.bookmarks.removeItem(rootBmkID);
 
-    _("Excluded items should be ignored");
-    yield verifyTrackerEmpty();
+    // We shouldn't upload tombstones for items in filtered roots, but the
+    // `onItemRemoved` observer doesn't have enough context to determine
+    // the root, so we'll end up uploading it.
+    yield verifyTrackedItems([rootBmkGUID]);
+    // We'll increment the counter twice (once for the removed item, and once
+    // for the Places root), then filter out the root.
+    do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
   } finally {
     _("Clean up.");
     yield cleanup();
@@ -1254,22 +1301,43 @@ add_task(function* test_async_onItemDeleted_eraseEverything() {
       url: "https://developer.mozilla.org",
       title: "MDN",
     });
+    _(`MDN GUID: ${mdnBmk.guid}`);
     let bugsFolder = yield PlacesUtils.bookmarks.insert({
       type: PlacesUtils.bookmarks.TYPE_FOLDER,
       parentGuid: PlacesUtils.bookmarks.toolbarGuid,
       title: "Bugs",
     });
+    _(`Bugs folder GUID: ${bugsFolder.guid}`);
     let bzBmk = yield PlacesUtils.bookmarks.insert({
       type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
       parentGuid: bugsFolder.guid,
       url: "https://bugzilla.mozilla.org",
       title: "Bugzilla",
     });
+    _(`Bugzilla GUID: ${bzBmk.guid}`);
+    let bugsChildFolder = yield PlacesUtils.bookmarks.insert({
+      type: PlacesUtils.bookmarks.TYPE_FOLDER,
+      parentGuid: bugsFolder.guid,
+      title: "Bugs child",
+    });
+    _(`Bugs child GUID: ${bugsChildFolder.guid}`);
+    let bugsGrandChildBmk = yield PlacesUtils.bookmarks.insert({
+      type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+      parentGuid: bugsChildFolder.guid,
+      url: "https://example.com",
+      title: "Bugs grandchild",
+    });
+    _(`Bugs grandchild GUID: ${bugsGrandChildBmk.guid}`);
 
     yield startTracking();
 
     yield PlacesUtils.bookmarks.eraseEverything();
 
+    // `eraseEverything` removes all items from the database before notifying
+    // observers. Because of this, grandchild lookup in the tracker's
+    // `onItemRemoved` observer will fail. That means we won't track
+    // (bzBmk.guid, bugsGrandChildBmk.guid, bugsChildFolder.guid), even
+    // though we should.
     yield verifyTrackedItems(["menu", mozBmk.guid, mdnBmk.guid, "toolbar",
                               bugsFolder.guid]);
     do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 6);
diff --git a/taskcluster/taskgraph/task/nightly_fennec.py b/taskcluster/taskgraph/task/nightly_fennec.py
index b9f08f6ed01dfb64e1e6f4d95e7c37690c23cbdb..37ad2b2bf3f00dec859b0e9b2fb4303a10c74b74 100644
--- a/taskcluster/taskgraph/task/nightly_fennec.py
+++ b/taskcluster/taskgraph/task/nightly_fennec.py
@@ -110,7 +110,10 @@ def query_vcs_info(repository, revision):
 class NightlyFennecTask(base.Task):
 
     def __init__(self, *args, **kwargs):
-        self.task_dict = kwargs.pop('task_dict')
+        try:
+            self.task_dict = kwargs.pop('task_dict')
+        except KeyError:
+            pass
         super(NightlyFennecTask, self).__init__(*args, **kwargs)
 
     @classmethod
diff --git a/testing/marionette/driver.js b/testing/marionette/driver.js
index c9de0e24ea62e0bfa76332720275a3f9e0c8464b..193aeaed4a40188f10ab1c7ca00a90a65332a6b0 100644
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -2576,7 +2576,7 @@ GeckoDriver.prototype.quitApplication = function(cmd, resp) {
   }
 
   let flags = Ci.nsIAppStartup.eAttemptQuit;
-  for (let k of cmd.parameters.flags) {
+  for (let k of cmd.parameters.flags || []) {
     flags |= Ci.nsIAppStartup[k];
   }
 
diff --git a/testing/mozharness/mozharness/base/script.py b/testing/mozharness/mozharness/base/script.py
index ffbb460a3e8d81246e53b71d27a44b481c2345be..fa24b16d70ec6b420edfd6228ad6a09b430b966b 100755
--- a/testing/mozharness/mozharness/base/script.py
+++ b/testing/mozharness/mozharness/base/script.py
@@ -50,7 +50,7 @@ try:
 except ImportError:
     import json
 
-from cStringIO import StringIO
+from io import BytesIO
 
 from mozprocess import ProcessHandler
 from mozharness.base.config import BaseConfig
@@ -58,6 +58,10 @@ from mozharness.base.log import SimpleFileLogger, MultiFileLogger, \
     LogMixin, OutputParser, DEBUG, INFO, ERROR, FATAL
 
 
+class FetchedIncorrectFilesize(Exception):
+    pass
+
+
 def platform_name():
     pm = PlatformMixin()
 
@@ -341,6 +345,71 @@ class ScriptMixin(PlatformMixin):
         url_quoted = urllib2.quote(url, safe='%/:=&?~#+!$,;\'@()*[]|')
         return urllib2.urlopen(url_quoted, **kwargs)
 
+
+
+    def fetch_url_into_memory(self, url):
+        ''' Downloads a file from a url into memory instead of disk.
+
+        Args:
+            url (str): URL path where the file to be downloaded is located.
+
+        Raises:
+            IOError: When the url points to a file on disk and cannot be found
+            FetchedIncorrectFilesize: When the size of the fetched file does not match the
+                                      expected file size.
+            ValueError: When the scheme of a url is not what is expected.
+
+        Returns:
+            BytesIO: contents of url
+        '''
+        self.info('Fetch {} into memory'.format(url))
+        parsed_url = urlparse.urlparse(url)
+
+        if parsed_url.scheme in ('', 'file'):
+            if not os.path.isfile(url):
+                raise IOError('Could not find file to extract: {}'.format(url))
+
+            expected_file_size = os.stat(url.replace('file://', '')).st_size
+
+            # In case we're referrencing a file without file://
+            if parsed_url.scheme == '':
+                url = 'file://%s' % os.path.abspath(url)
+                parsed_url = urlparse.urlparse(url)
+
+        request = urllib2.Request(url)
+        # Exceptions to be retried:
+        # Bug 1300663 - HTTPError: HTTP Error 404: Not Found
+        # Bug 1300413 - HTTPError: HTTP Error 500: Internal Server Error
+        # Bug 1300943 - HTTPError: HTTP Error 503: Service Unavailable
+        # Bug 1300953 - URLError: <urlopen error [Errno -2] Name or service not known>
+        # Bug 1301594 - URLError: <urlopen error [Errno 10054] An existing connection was ...
+        # Bug 1301597 - URLError: <urlopen error [Errno 8] _ssl.c:504: EOF occurred in ...
+        # Bug 1301855 - URLError: <urlopen error [Errno 60] Operation timed out>
+        # Bug 1302237 - URLError: <urlopen error [Errno 104] Connection reset by peer>
+        # Bug 1301807 - BadStatusLine: ''
+        response = urllib2.urlopen(request)
+
+        if parsed_url.scheme in ('http', 'https'):
+            expected_file_size = int(response.headers.get('Content-Length'))
+
+        self.info('Expected file size: {}'.format(expected_file_size))
+        self.debug('Url: {}'.format(url))
+        self.debug('Content-Encoding {}'.format(response.headers.get('Content-Encoding')))
+
+        file_contents = response.read()
+        obtained_file_size = len(file_contents)
+
+        if obtained_file_size != expected_file_size:
+            raise FetchedIncorrectFilesize(
+                'The expected file size is {} while we got instead {}'.format(
+                    expected_file_size, obtained_file_size)
+            )
+
+        # Use BytesIO instead of StringIO
+        # http://stackoverflow.com/questions/34162017/unzip-buffer-with-python/34162395#34162395
+        return BytesIO(file_contents)
+
+
     def _download_file(self, url, file_name):
         """ Helper script for download_file()
         Additionaly this function logs all exceptions as warnings before
@@ -470,50 +539,56 @@ class ScriptMixin(PlatformMixin):
             yield entry
 
 
-    def unzip(self, file_object, extract_to='.', extract_dirs='*', verbose=False):
+    def unzip(self, compressed_file, extract_to, extract_dirs='*', verbose=False):
         """This method allows to extract a zip file without writing to disk first.
 
         Args:
-            file_object (object): Any file like object that is seekable.
-            extract_to (str, optional): where to extract the compressed file.
+            compressed_file (object): File-like object with the contents of a compressed zip file.
+            extract_to (str): where to extract the compressed file.
             extract_dirs (list, optional): directories inside the archive file to extract.
                                            Defaults to '*'.
-        """
-        compressed_file = StringIO(file_object.read())
-        try:
-            with zipfile.ZipFile(compressed_file) as bundle:
-                entries = self._filter_entries(bundle.namelist(), extract_dirs)
-
-                for entry in entries:
-                    if verbose:
-                        self.info(' {}'.format(entry))
-                    bundle.extract(entry, path=extract_to)
+            verbose (bool, optional): whether or not extracted content should be displayed.
+                                      Defaults to False.
 
-                    # ZipFile doesn't preserve permissions during extraction:
-                    # http://bugs.python.org/issue15795
-                    fname = os.path.realpath(os.path.join(extract_to, entry))
+        Raises:
+            zipfile.BadZipFile: on contents of zipfile being invalid
+        """
+        with zipfile.ZipFile(compressed_file) as bundle:
+            entries = self._filter_entries(bundle.namelist(), extract_dirs)
+
+            for entry in entries:
+                if verbose:
+                    self.info(' {}'.format(entry))
+
+                # Exception to be retried:
+                # Bug 1301645 - BadZipfile: Bad CRC-32 for file ...
+                #    http://stackoverflow.com/questions/5624669/strange-badzipfile-bad-crc-32-problem/5626098#5626098
+                # Bug 1301802 - error: Error -3 while decompressing: invalid stored block lengths
+                bundle.extract(entry, path=extract_to)
+
+                # ZipFile doesn't preserve permissions during extraction:
+                # http://bugs.python.org/issue15795
+                fname = os.path.realpath(os.path.join(extract_to, entry))
+                try:
+                    # getinfo() can raise KeyError
                     mode = bundle.getinfo(entry).external_attr >> 16 & 0x1FF
                     # Only set permissions if attributes are available. Otherwise all
                     # permissions will be removed eg. on Windows.
                     if mode:
                         os.chmod(fname, mode)
 
-        except zipfile.BadZipfile as e:
-            self.exception('{}'.format(e.message))
+                except KeyError:
+                    self.warning('{} was not found in the zip file'.format(entry))
 
 
-    def deflate(self, file_object, mode, extract_to='.', extract_dirs='*', verbose=False):
-        """This method allows to extract a tar, tar.bz2 and tar.gz file without writing to disk first.
+    def deflate(self, compressed_file, mode, extract_to='.', *args, **kwargs):
+        """This method allows to extract a compressed file from a tar, tar.bz2 and tar.gz files.
 
         Args:
-            file_object (object): Any file like object that is seekable.
+            compressed_file (object): File-like object with the contents of a compressed file.
+            mode (str): string of the form 'filemode[:compression]' (e.g. 'r:gz' or 'r:bz2')
             extract_to (str, optional): where to extract the compressed file.
-            extract_dirs (list, optional): directories inside the archive file to extract.
-                                           Defaults to `*`.
-            verbose (bool, optional): whether or not extracted content should be displayed.
-                                      Defaults to False.
         """
-        compressed_file = StringIO(file_object.read())
         t = tarfile.open(fileobj=compressed_file, mode=mode)
         t.extractall(path=extract_to)
 
@@ -527,90 +602,84 @@ class ScriptMixin(PlatformMixin):
                                         be extracted to.
             extract_dirs (list, optional): directories inside the archive to extract.
                                            Defaults to `*`. It currently only applies to zip files.
-
-        Raises:
-            IOError: on `filename` file not found.
+            verbose (bool, optional): whether or not extracted content should be displayed.
+                                      Defaults to False.
 
         """
-        # Many scripts overwrite this method and set extract_dirs to None
-        extract_dirs = '*' if extract_dirs is None else extract_dirs
-        EXTENSION_TO_MIMETYPE = {
-            'bz2': 'application/x-bzip2',
-            'gz':  'application/x-gzip',
-            'tar': 'application/x-tar',
-            'zip': 'application/zip',
-        }
-        MIMETYPES = {
-            'application/x-bzip2': {
-                'function': self.deflate,
-                'kwargs': {'mode': 'r:bz2'},
-            },
-            'application/x-gzip': {
-                'function': self.deflate,
-                'kwargs': {'mode': 'r:gz'},
-            },
-            'application/x-tar': {
-                'function': self.deflate,
-                'kwargs': {'mode': 'r'},
-            },
-            'application/zip': {
-                'function': self.unzip,
-            },
-        }
-
-        parsed_url = urlparse.urlparse(url)
-
-        # In case we're referrencing a file without file://
-        if parsed_url.scheme == '':
-            if not os.path.isfile(url):
-                raise IOError('Could not find file to extract: {}'.format(url))
-
-            url = 'file://%s' % os.path.abspath(url)
-            parsed_fd = urlparse.urlparse(url)
-
-        request = urllib2.Request(url)
-        response = urllib2.urlopen(request)
+        def _determine_extraction_method_and_kwargs(url):
+            EXTENSION_TO_MIMETYPE = {
+                'bz2': 'application/x-bzip2',
+                'gz':  'application/x-gzip',
+                'tar': 'application/x-tar',
+                'zip': 'application/zip',
+            }
+            MIMETYPES = {
+                'application/x-bzip2': {
+                    'function': self.deflate,
+                    'kwargs': {'mode': 'r:bz2'},
+                },
+                'application/x-gzip': {
+                    'function': self.deflate,
+                    'kwargs': {'mode': 'r:gz'},
+                },
+                'application/x-tar': {
+                    'function': self.deflate,
+                    'kwargs': {'mode': 'r'},
+                },
+                'application/zip': {
+                    'function': self.unzip,
+                },
+            }
 
-        if parsed_url.scheme == 'file':
             filename = url.split('/')[-1]
             # XXX: bz2/gz instead of tar.{bz2/gz}
             extension = filename[filename.rfind('.')+1:]
             mimetype = EXTENSION_TO_MIMETYPE[extension]
-        else:
-            mimetype = response.headers.type
+            self.debug('Mimetype: {}'.format(mimetype))
 
-        self.debug('Url: {}'.format(url))
-        self.debug('Mimetype: {}'.format(mimetype))
-        self.debug('Content-Encoding {}'.format(response.headers.get('Content-Encoding')))
+            function = MIMETYPES[mimetype]['function']
+            kwargs = {
+                'compressed_file': compressed_file,
+                'extract_to': extract_to,
+                'extract_dirs': extract_dirs,
+                'verbose': verbose,
+            }
+            kwargs.update(MIMETYPES[mimetype].get('kwargs', {}))
 
-        function = MIMETYPES[mimetype]['function']
-        kwargs = {
-            'file_object': response,
-            'extract_to': extract_to,
-            'extract_dirs': extract_dirs,
-            'verbose': verbose,
-        }
-        kwargs.update(MIMETYPES[mimetype].get('kwargs', {}))
+            return function, kwargs
 
+        # Many scripts overwrite this method and set extract_dirs to None
+        extract_dirs = '*' if extract_dirs is None else extract_dirs
         self.info('Downloading and extracting to {} these dirs {} from {}'.format(
             extract_to,
             ', '.join(extract_dirs),
             url,
         ))
+
+        # 1) Let's fetch the file
         retry_args = dict(
-            failure_status=None,
-            retry_exceptions=(urllib2.HTTPError, urllib2.URLError,
-                              httplib.BadStatusLine,
-                              socket.timeout, socket.error),
+            retry_exceptions=(
+                urllib2.HTTPError,
+                urllib2.URLError,
+                httplib.BadStatusLine,
+                socket.timeout,
+                socket.error,
+                FetchedIncorrectFilesize,
+            ),
             error_message="Can't download from {}".format(url),
             error_level=FATAL,
         )
-        self.retry(
-            function,
-            kwargs=kwargs,
+        compressed_file = self.retry(
+            self.fetch_url_into_memory,
+            kwargs={'url': url},
             **retry_args
         )
 
+        # 2) We're guaranteed to have download the file with error_level=FATAL
+        #    Let's unpack the file
+        function, kwargs = _determine_extraction_method_and_kwargs(url)
+        function(**kwargs)
+
 
     def load_json_url(self, url, error_level=None, *args, **kwargs):
         """ Returns a json object from a url (it retries). """
diff --git a/testing/web-platform/README.md b/testing/web-platform/README.md
index ec22f1f3c26e9b01b4d640937e86e1a512ed0f47..f51e473bb2c2c6e75c8ab27004ecff74291e9664 100644
--- a/testing/web-platform/README.md
+++ b/testing/web-platform/README.md
@@ -40,7 +40,8 @@ FAQ
 
   It is important to note that in order for the tests to run the
   manifest file must be updated; this should not be done by hand, but
-  by running `mach web-platform-tests --manifest-update`.
+  by running `mach wpt-manifest-update` (or `mach web-platform-tests
+  --manifest-update`, if you also wish to run some tests).
 
   `mach web-platform-tests-create <path>` is a helper script designed
   to help create new web-platform-tests. It opens a locally configured
diff --git a/testing/web-platform/mach_commands.py b/testing/web-platform/mach_commands.py
index 4f8b2aa37568e80ae063dfdd2cf4340fc9ab91dd..4c41283e761c426538877661f9cef261f6677644 100644
--- a/testing/web-platform/mach_commands.py
+++ b/testing/web-platform/mach_commands.py
@@ -236,6 +236,27 @@ testing/web-platform/tests for tests that may be shared
             proc.wait()
 
 
+class WPTManifestUpdater(MozbuildObject):
+    def run_update(self):
+        import imp
+        from wptrunner import wptlogging
+        from wptrunner.wptcommandline import get_test_paths, set_from_config
+        from wptrunner.testloader import ManifestLoader
+
+        wpt_dir = os.path.abspath(os.path.join(self.topsrcdir, 'testing', 'web-platform'))
+
+        localpaths = imp.load_source("localpaths",
+                                     os.path.join(wpt_dir, "tests", "tools", "localpaths.py"))
+        kwargs = {"config": os.path.join(wpt_dir, "wptrunner.ini"),
+                  "tests_root": None,
+                  "metadata_root": None}
+
+        wptlogging.setup({}, {"mach": sys.stdout})
+        set_from_config(kwargs)
+        test_paths = get_test_paths(kwargs["config"])
+        ManifestLoader(test_paths, force_manifest_update=True).load()
+
+
 def create_parser_wpt():
     from wptrunner import wptcommandline
     return wptcommandline.create_parser(["firefox"])
@@ -292,6 +313,13 @@ class MachCommands(MachCommandBase):
         else:
             return wpt_runner.run_tests(**params)
 
+    @Command("wpt",
+             category="testing",
+             conditions=[conditions.is_firefox],
+             parser=create_parser_wpt)
+    def run_wpt(self, **params):
+        return self.run_web_platform_tests(**params)
+
     @Command("web-platform-tests-update",
              category="testing",
              parser=create_parser_update)
@@ -302,6 +330,12 @@ class MachCommands(MachCommandBase):
         wpt_updater = self._spawn(WebPlatformTestsUpdater)
         return wpt_updater.run_update(**params)
 
+    @Command("wpt-update",
+             category="testing",
+             parser=create_parser_update)
+    def update_wpt(self, **params):
+        return self.update_web_platform_tests(**params)
+
     def setup(self):
         self._activate_virtualenv()
 
@@ -314,6 +348,13 @@ class MachCommands(MachCommandBase):
         wpt_reduce = self._spawn(WebPlatformTestsReduce)
         return wpt_reduce.run_reduce(**params)
 
+    @Command("wpt-reduce",
+             category="testing",
+             conditions=[conditions.is_firefox],
+             parser=create_parser_reduce)
+    def unstable_wpt(self, **params):
+        return self.unstable_web_platform_tests(**params)
+
     @Command("web-platform-tests-create",
              category="testing",
              conditions=[conditions.is_firefox],
@@ -322,3 +363,17 @@ class MachCommands(MachCommandBase):
         self.setup()
         wpt_creator = self._spawn(WebPlatformTestsCreator)
         wpt_creator.run_create(self._mach_context, **params)
+
+    @Command("wpt-create",
+             category="testing",
+             conditions=[conditions.is_firefox],
+             parser=create_parser_create)
+    def create_wpt(self, **params):
+        return self.create_web_platform_test(**params)
+
+    @Command("wpt-manifest-update",
+             category="testing")
+    def wpt_manifest_update(self, **parms):
+        self.setup()
+        wpt_manifest_updater = self._spawn(WPTManifestUpdater)
+        wpt_manifest_updater.run_update()
diff --git a/testing/web-platform/tests/web-animations/interfaces/Animation/id.html b/testing/web-platform/tests/web-animations/interfaces/Animation/id.html
index 9d293733756350262046ff5d42b98c024bdce88b..2fadd562369951e9305e563846d1898e14f9af2a 100644
--- a/testing/web-platform/tests/web-animations/interfaces/Animation/id.html
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/id.html
@@ -13,11 +13,16 @@
 test(function(t) {
   var div = createDiv(t);
   var animation = div.animate({}, 100 * MS_PER_SEC);
-  assert_equals(animation.id, '', 'id for CSS Animation is initially empty');
-  animation.id = 'anim'
+  assert_equals(animation.id, '', 'id for Animation is initially empty');
+}, 'Animation.id initial value');
+
+test(function(t) {
+  var div = createDiv(t);
+  var animation = div.animate({}, 100 * MS_PER_SEC);
+  animation.id = 'anim';
 
   assert_equals(animation.id, 'anim', 'animation.id reflects the value set');
-}, 'Animation.id for CSS Animations');
+}, 'Animation.id setter');
 
 </script>
 </body>
diff --git a/toolkit/components/passwordmgr/LoginDoorhangers.jsm b/toolkit/components/passwordmgr/LoginDoorhangers.jsm
deleted file mode 100644
index beec28078414ae791bdd4a354746663998abcba7..0000000000000000000000000000000000000000
--- a/toolkit/components/passwordmgr/LoginDoorhangers.jsm
+++ /dev/null
@@ -1,391 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = [
-  "LoginDoorhangers",
-];
-
-const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/LoginManagerParent.jsm");
-
-const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-
-// Helper function needed because the "disabled" property may not be available
-// if the XBL binding of the UI control has not been constructed yet.
-function setDisabled(element, disabled) {
-  if (disabled) {
-    element.setAttribute("disabled", "true");
-  } else {
-    element.removeAttribute("disabled");
-  }
-}
-
-this.LoginDoorhangers = {};
-
-/**
- * Doorhanger for selecting and filling logins.
- *
- * @param {Object} properties
- *        Properties from this object will be applied to the new instance.
- */
-this.LoginDoorhangers.FillDoorhanger = function (properties) {
-  // Set up infrastructure to access our elements and listen to events.
-  this.el = new Proxy({}, {
-    get: (target, name) => {
-      return this.chromeDocument.getElementById("login-fill-" + name);
-    },
-  });
-  this.eventHandlers = [];
-  for (let elementName of Object.keys(this.events)) {
-    let handlers = this.events[elementName];
-    for (let eventName of Object.keys(handlers)) {
-      let handler = handlers[eventName];
-      this.eventHandlers.push([elementName, eventName, handler.bind(this)]);
-    }
-  }
-  for (let name of Object.getOwnPropertyNames(properties)) {
-    this[name] = properties[name];
-  }
-};
-
-this.LoginDoorhangers.FillDoorhanger.prototype = {
-  /**
-   * Whether the elements for this doorhanger are currently in the document.
-   */
-  bound: false,
-
-  /**
-   * Associates the doorhanger with its browser. When the tab associated to this
-   * browser is selected, the anchor icon for the doorhanger will appear.
-   *
-   * The browser may change during the lifetime of the doorhanger, in case the
-   * web page is moved to a different chrome window by the swapDocShells method.
-   */
-  set browser(browser) {
-    const MAX_DATE_VALUE = new Date(8640000000000000);
-
-    this._browser = browser;
-
-    let doorhanger = this;
-    let PopupNotifications = this.chromeDocument.defaultView.PopupNotifications;
-    let notification = PopupNotifications.show(
-      browser,
-      "login-fill",
-      "",
-      "login-fill-notification-icon",
-      null,
-      null,
-      {
-        dismissed: true,
-        // This will make the anchor persist forever even if the popup is not
-        // visible. We'll remove the notification manually when the page
-        // changes, after we had time to check its final state asynchronously.
-        timeout: MAX_DATE_VALUE,
-        eventCallback: function (topic, otherBrowser) {
-          switch (topic) {
-            case "shown":
-              // Since we specified the "dismissed" option, this event will only
-              // be called after the "show" method returns, so the reference to
-              // "this.notification" will be available in "bind" at this point.
-              doorhanger.promiseHidden =
-                         new Promise(resolve => doorhanger.onUnbind = resolve);
-              doorhanger.bind();
-              break;
-
-            case "dismissed":
-            case "removed":
-              if (doorhanger.bound) {
-                doorhanger.unbind();
-                doorhanger.onUnbind();
-              }
-              break;
-
-            case "swapping":
-              doorhanger._browser = otherBrowser;
-              return true;
-          }
-          return false;
-        },
-      }
-    );
-
-    this.notification = notification;
-    notification.doorhanger = this;
-  },
-  get browser() {
-    return this._browser;
-  },
-  _browser: null,
-
-  /**
-   * DOM document to which the doorhanger is currently associated.
-   *
-   * This may change during the lifetime of the doorhanger, in case the web page
-   * is moved to a different chrome window by the swapDocShells method.
-   */
-  get chromeDocument() {
-    return this.browser.ownerDocument;
-  },
-
-  /**
-   * Hides this notification, if the notification panel is currently open.
-   */
-  hide() {
-    let PopupNotifications = this.chromeDocument.defaultView.PopupNotifications;
-    if (PopupNotifications.isPanelOpen) {
-      PopupNotifications.panel.hidePopup();
-    }
-  },
-
-  /**
-   * Promise resolved as soon as the notification is hidden.
-   */
-  promiseHidden: Promise.resolve(),
-
-  /**
-   * Removes the doorhanger from the browser.
-   */
-  remove() {
-    this.notification.remove();
-  },
-
-  /**
-   * Binds this doorhanger to its UI controls.
-   */
-  bind() {
-    // Since this may ask for the master password, we must do it at bind time.
-    if (this.autoDetailLogin) {
-      let formLogins = Services.logins.findLogins({}, this.loginFormOrigin, "",
-                                                  null);
-      if (formLogins.length == 1) {
-        this.detailLogin = formLogins[0];
-      }
-      this.autoDetailLogin = false;
-    }
-
-    this.el.filter.setAttribute("value", this.filterString);
-    this.refreshList();
-    this.refreshDetailView();
-
-    this.eventHandlers.forEach(([elementName, eventName, handler]) => {
-      this.el[elementName].addEventListener(eventName, handler, true);
-    });
-
-    // Move the main element to the notification panel for displaying.
-    this.notification.owner.panel.firstElementChild.appendChild(this.el.doorhanger);
-    this.el.doorhanger.hidden = false;
-
-    this.bound = true;
-  },
-
-  /**
-   * Unbinds this doorhanger from its UI controls.
-   */
-  unbind() {
-    this.bound = false;
-
-    this.eventHandlers.forEach(([elementName, eventName, handler]) => {
-      this.el[elementName].removeEventListener(eventName, handler, true);
-    });
-
-    this.clearList();
-
-    // Place the element back in the document for the next time we need it.
-    this.el.doorhanger.hidden = true;
-    this.chromeDocument.getElementById("mainPopupSet")
-                       .appendChild(this.el.doorhanger);
-  },
-
-  /**
-   * Origin for which the manual fill UI should be displayed, for example
-   * "http://www.example.com".
-   */
-  loginFormOrigin: "",
-
-  /**
-   * When no login form is present on the page, we may still display a list of
-   * logins, but we cannot offer manual filling.
-   */
-  loginFormPresent: false,
-
-  /**
-   * User-editable string used to filter the list of all logins.
-   */
-  filterString: "",
-
-  /**
-   * Show login details automatically when the panel is first opened.
-   */
-  autoDetailLogin: false,
-
-  /**
-   * Indicates which particular login to show in the detail view.
-   */
-  set detailLogin(detailLogin) {
-    this._detailLogin = detailLogin;
-    if (this.bound) {
-      this.refreshDetailView();
-    }
-  },
-  get detailLogin() {
-    return this._detailLogin;
-  },
-  _detailLogin: null,
-
-  /**
-   * Prototype functions for event handling.
-   */
-  events: {
-    mainview: {
-      focus(event) {
-        // If keyboard focus returns to any control in the the main view (for
-        // example using SHIFT+TAB) close the details view.
-        this.detailLogin = null;
-      },
-    },
-    filter: {
-      input(event) {
-        this.filterString = this.el.filter.value;
-        this.refreshList();
-      },
-    },
-    list: {
-      click(event) {
-        if (event.button == 0 && this.el.list.selectedItem) {
-          this.displaySelectedLoginDetails();
-        }
-      },
-      keypress(event) {
-        if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
-            this.el.list.selectedItem) {
-          this.displaySelectedLoginDetails();
-        }
-      },
-    },
-    clickcapturer: {
-      click(event) {
-        this.detailLogin = null;
-      },
-    },
-    details: {
-      transitionend(event) {
-        // We must set focus to the detail controls only when the transition has
-        // ended, otherwise focus will interfere with the animation. We do this
-        // only when we're showing the detail view, not when leaving.
-        if (event.target == this.el.details && this.detailLogin) {
-          if (this.loginFormPresent) {
-            this.el.use.focus();
-          } else {
-            this.el.username.focus();
-          }
-        }
-      },
-    },
-    use: {
-      command(event) {
-        this.fillLogin();
-      },
-    },
-  },
-
-  /**
-   * Rebuilds the list of logins.
-   */
-  refreshList() {
-    this.clearList();
-
-    let formLogins = Services.logins.findLogins({}, "", "", null);
-    let filterToUse = this.filterString.trim().toLowerCase();
-    if (filterToUse) {
-      formLogins = formLogins.filter(login => {
-        return login.hostname.toLowerCase().indexOf(filterToUse) != -1 ||
-               login.username.toLowerCase().indexOf(filterToUse) != -1 ;
-      });
-    }
-
-    for (let { hostname, username } of formLogins) {
-      let item = this.chromeDocument.createElementNS(XUL_NS, "richlistitem");
-      item.classList.add("login-fill-item");
-      item.setAttribute("hostname", hostname);
-      item.setAttribute("username", username);
-      if (hostname != this.loginFormOrigin) {
-        item.classList.add("different-hostname");
-      }
-      this.el.list.appendChild(item);
-    }
-  },
-
-  /**
-   * Clears the list of logins.
-   */
-  clearList() {
-    let list = this.el.list;
-    while (list.firstChild) {
-      list.firstChild.remove();
-    }
-  },
-
-  /**
-   * Updates all the controls of the detail view based on the chosen login.
-   */
-  refreshDetailView() {
-    if (this.detailLogin) {
-      this.el.username.setAttribute("value", this.detailLogin.username);
-      this.el.password.setAttribute("value", this.detailLogin.password);
-      this.el.doorhanger.setAttribute("inDetailView", "true");
-      setDisabled(this.el.username, false);
-      setDisabled(this.el.use, !this.loginFormPresent);
-    } else {
-      this.el.doorhanger.removeAttribute("inDetailView");
-      // We must disable all the detail controls to ensure they cannot be
-      // selected with the keyboard while they are outside the visible area.
-      setDisabled(this.el.username, true);
-      setDisabled(this.el.use, true);
-    }
-  },
-
-  displaySelectedLoginDetails() {
-    let selectedItem = this.el.list.selectedItem;
-    let hostLogins = Services.logins.findLogins({},
-                               selectedItem.getAttribute("hostname"), "", null);
-    let login = hostLogins.find(login => {
-      return login.username == selectedItem.getAttribute("username");
-    });
-    if (!login) {
-      Cu.reportError("The selected login has been removed in the meantime.");
-      return;
-    }
-    this.detailLogin = login;
-  },
-
-  fillLogin() {
-    LoginManagerParent.fillForm({
-      browser: this.browser,
-      loginFormOrigin: this.loginFormOrigin,
-      login: this.detailLogin,
-    }).catch(Cu.reportError);
-    this.hide();
-  },
-};
-
-/**
- * Retrieves an existing FillDoorhanger associated with a browser, or null if an
- * associated doorhanger of that type cannot be found.
- *
- * @param An object with the following properties:
- *        {
- *          browser:
- *            The <browser> element to which the doorhanger is associated.
- *        }
- */
-this.LoginDoorhangers.FillDoorhanger.find = function ({ browser }) {
-  let PopupNotifications = browser.ownerDocument.defaultView.PopupNotifications;
-  let notification = PopupNotifications.getNotification("login-fill",
-                                                        browser);
-  return notification && notification.doorhanger;
-};
diff --git a/toolkit/components/passwordmgr/LoginManagerContent.jsm b/toolkit/components/passwordmgr/LoginManagerContent.jsm
index 8d0c8d83b9b9935e53ed051641b46d9ea0e2ecea..177082be1da5defff12f10b18d71c38e04c20a73 100644
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -408,7 +408,7 @@ var LoginManagerContent = {
    * @param {Window} window
    */
   _fetchLoginsFromParentAndFillForm(form, window) {
-    this._updateLoginFormPresence(window);
+    this._detectInsecureFormLikes(window);
 
     let messageManager = messageManagerFromWindow(window);
     messageManager.sendAsyncMessage("LoginStats:LoginEncountered");
@@ -423,7 +423,7 @@ var LoginManagerContent = {
   },
 
   onPageShow(event, window) {
-    this._updateLoginFormPresence(window);
+    this._detectInsecureFormLikes(window);
   },
 
   /**
@@ -448,38 +448,11 @@ var LoginManagerContent = {
   },
 
   /**
-   * Compute whether there is a login form on any frame of the current page, and
-   * notify the parent process. This is one of the factors used to control the
-   * visibility of the password fill doorhanger anchor.
+   * Compute whether there is an insecure login form on any frame of the current page, and
+   * notify the parent process. This is used to control whether insecure password UI appears.
    */
-  _updateLoginFormPresence(topWindow) {
-    log("_updateLoginFormPresence", topWindow.location.href);
-    // For the login form presence notification, we currently support only one
-    // origin for each browser, so the form origin will always match the origin
-    // of the top level document.
-    let loginFormOrigin =
-        LoginUtils._getPasswordOrigin(topWindow.document.documentURI);
-
-    // Returns the first known loginForm present in this window or in any
-    // same-origin subframes. Returns null if no loginForm is currently present.
-    let getFirstLoginForm = thisWindow => {
-      let loginForms = this.stateForDocument(thisWindow.document).loginFormRootElements;
-      if (loginForms.size) {
-        return [...loginForms][0];
-      }
-      for (let i = 0; i < thisWindow.frames.length; i++) {
-        let frame = thisWindow.frames[i];
-        if (LoginUtils._getPasswordOrigin(frame.document.documentURI) !=
-            loginFormOrigin) {
-          continue;
-        }
-        let loginForm = getFirstLoginForm(frame);
-        if (loginForm) {
-          return loginForm;
-        }
-      }
-      return null;
-    };
+  _detectInsecureFormLikes(topWindow) {
+    log("_detectInsecureFormLikes", topWindow.location.href);
 
     // Returns true if this window or any subframes have insecure login forms.
     let hasInsecureLoginForms = (thisWindow, parentIsInsecure) => {
@@ -491,16 +464,8 @@ var LoginManagerContent = {
                         frame => hasInsecureLoginForms(frame, isInsecure));
     };
 
-    // Store the actual form to use on the state for the top-level document.
-    let topState = this.stateForDocument(topWindow.document);
-    topState.loginFormForFill = getFirstLoginForm(topWindow);
-    log("_updateLoginFormPresence: topState.loginFormForFill", topState.loginFormForFill);
-
-    // Determine whether to show the anchor icon for the current tab.
     let messageManager = messageManagerFromWindow(topWindow);
-    messageManager.sendAsyncMessage("RemoteLogins:updateLoginFormPresence", {
-      loginFormOrigin,
-      loginFormPresent: !!topState.loginFormForFill,
+    messageManager.sendAsyncMessage("RemoteLogins:insecureLoginFormPresent", {
       hasInsecureLoginForms: hasInsecureLoginForms(topWindow, false),
     });
   },
@@ -526,14 +491,12 @@ var LoginManagerContent = {
    *          recipes:
    *            Fill recipes transmitted together with the original message.
    *          inputElement:
-   *            Optional input password element from the form we want to fill.
+   *            Username or password input element from the form we want to fill.
    *        }
    */
   fillForm({ topDocument, loginFormOrigin, loginsFound, recipes, inputElement }) {
-    let topState = this.stateForDocument(topDocument);
-    if (!inputElement && !topState.loginFormForFill) {
-      log("fillForm: There is no login form anymore. The form may have been",
-          "removed or the document may have changed.");
+    if (!inputElement) {
+      log("fillForm: No input element specified");
       return;
     }
     if (LoginUtils._getPasswordOrigin(topDocument.documentURI) != loginFormOrigin) {
@@ -546,18 +509,15 @@ var LoginManagerContent = {
         return;
       }
     }
-    let form = topState.loginFormForFill;
+
     let clobberUsername = true;
     let options = {
       inputElement,
     };
 
-    // If we have a target input, fills it's form.
-    if (inputElement) {
-      form = FormLikeFactory.createFromField(inputElement);
-      if (inputElement.type == "password") {
-        clobberUsername = false;
-      }
+    let form = FormLikeFactory.createFromField(inputElement);
+    if (inputElement.type == "password") {
+      clobberUsername = false;
     }
     this._fillForm(form, true, clobberUsername, true, true, loginsFound, recipes, options);
   },
diff --git a/toolkit/components/passwordmgr/LoginManagerParent.jsm b/toolkit/components/passwordmgr/LoginManagerParent.jsm
index 7a222c47efeb5d94b9f87e04fe4a57e5577134e0..7cf6381ba4ca53fb8f72f37304bc8b062463c5b6 100644
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -17,8 +17,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "AutoCompletePopup",
                                   "resource://gre/modules/AutoCompletePopup.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
                                   "resource://gre/modules/DeferredTask.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "LoginDoorhangers",
-                                  "resource://gre/modules/LoginDoorhangers.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
                                   "resource://gre/modules/LoginHelper.jsm");
 
@@ -48,7 +46,7 @@ var LoginManagerParent = {
     mm.addMessageListener("RemoteLogins:onFormSubmit", this);
     mm.addMessageListener("RemoteLogins:autoCompleteLogins", this);
     mm.addMessageListener("RemoteLogins:removeLogin", this);
-    mm.addMessageListener("RemoteLogins:updateLoginFormPresence", this);
+    mm.addMessageListener("RemoteLogins:insecureLoginFormPresent", this);
 
     XPCOMUtils.defineLazyGetter(this, "recipeParentPromise", () => {
       const { LoginRecipesParent } = Cu.import("resource://gre/modules/LoginRecipes.jsm", {});
@@ -89,8 +87,8 @@ var LoginManagerParent = {
         break;
       }
 
-      case "RemoteLogins:updateLoginFormPresence": {
-        this.updateLoginFormPresence(msg.target, data);
+      case "RemoteLogins:insecureLoginFormPresent": {
+        this.setHasInsecureLoginForms(msg.target, data.hasInsecureLoginForms);
         break;
       }
 
@@ -470,83 +468,18 @@ var LoginManagerParent = {
   },
 
   /**
-   * Called to indicate whether a login form on the currently loaded page is
-   * present or not. This is one of the factors used to control the visibility
-   * of the password fill doorhanger.
+   * Called to indicate whether an insecure password field is present so
+   * insecure password UI can know when to show.
    */
-  updateLoginFormPresence(browser, { loginFormOrigin, loginFormPresent,
-                                     hasInsecureLoginForms }) {
-    const ANCHOR_DELAY_MS = 200;
-
+  setHasInsecureLoginForms(browser, hasInsecureLoginForms) {
     let state = this.stateForBrowser(browser);
 
     // Update the data to use to the latest known values. Since messages are
     // processed in order, this will always be the latest version to use.
-    state.loginFormOrigin = loginFormOrigin;
-    state.loginFormPresent = loginFormPresent;
     state.hasInsecureLoginForms = hasInsecureLoginForms;
 
     // Report the insecure login form state immediately.
     browser.dispatchEvent(new browser.ownerDocument.defaultView
                                  .CustomEvent("InsecureLoginFormsStateChange"));
-
-    // Apply the data to the currently displayed login fill icon later.
-    if (!state.anchorDeferredTask) {
-      state.anchorDeferredTask = new DeferredTask(
-        () => this.updateLoginAnchor(browser),
-        ANCHOR_DELAY_MS
-      );
-    }
-    state.anchorDeferredTask.arm();
   },
-
-  updateLoginAnchor: Task.async(function* (browser) {
-    // Once this preference is removed, this version of the fill doorhanger
-    // should be enabled for Desktop only, and not for Android or B2G.
-    if (!Services.prefs.getBoolPref("signon.ui.experimental")) {
-      return;
-    }
-
-    // Copy the state to use for this execution of the task. These will not
-    // change during this execution of the asynchronous function, but in case a
-    // change happens in the state, the function will be retriggered.
-    let { loginFormOrigin, loginFormPresent } = this.stateForBrowser(browser);
-
-    yield Services.logins.initializationPromise;
-
-    // Check if there are form logins for the site, ignoring formSubmitURL.
-    let hasLogins = loginFormOrigin &&
-                    LoginHelper.searchLoginsWithObject({
-                      httpRealm: null,
-                      hostname: loginFormOrigin,
-                      schemeUpgrades: LoginHelper.schemeUpgrades,
-                    }).length > 0;
-
-    let showLoginAnchor = loginFormPresent || hasLogins;
-
-    let fillDoorhanger = LoginDoorhangers.FillDoorhanger.find({ browser });
-    if (fillDoorhanger) {
-      if (!showLoginAnchor) {
-        fillDoorhanger.remove();
-        return;
-      }
-      // We should only update the state of the doorhanger while it is hidden.
-      yield fillDoorhanger.promiseHidden;
-      fillDoorhanger.loginFormPresent = loginFormPresent;
-      fillDoorhanger.loginFormOrigin = loginFormOrigin;
-      fillDoorhanger.filterString = hasLogins ? loginFormOrigin : "";
-      fillDoorhanger.detailLogin = null;
-      fillDoorhanger.autoDetailLogin = true;
-      return;
-    }
-    if (showLoginAnchor) {
-      fillDoorhanger = new LoginDoorhangers.FillDoorhanger({
-        browser,
-        loginFormPresent,
-        loginFormOrigin,
-        filterString: hasLogins ? loginFormOrigin : "",
-        autoDetailLogin: true,
-      });
-    }
-  }),
 };
diff --git a/toolkit/components/passwordmgr/content/login.xml b/toolkit/components/passwordmgr/content/login.xml
deleted file mode 100644
index 42cf4afecb7a070bfad431ef38131225726bdbfc..0000000000000000000000000000000000000000
--- a/toolkit/components/passwordmgr/content/login.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-<!DOCTYPE bindings SYSTEM "chrome://passwordmgr/locale/passwordManager.dtd">
-
-<bindings id="login-bindings"
-          xmlns="http://www.mozilla.org/xbl"
-          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-          xmlns:xbl="http://www.mozilla.org/xbl">
-
-  <binding id="login"
-           extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
-    <content orient="vertical">
-      <xul:label class="login-hostname" crop="end"
-                 xbl:inherits="value=hostname"/>
-      <xul:label class="login-username" crop="end"
-                 xbl:inherits="value=username"/>
-    </content>
-  </binding>
-</bindings>
diff --git a/toolkit/components/passwordmgr/jar.mn b/toolkit/components/passwordmgr/jar.mn
index 5af9f7535c75e91810a4ef762a5d96fb8a5bfa3a..9fa574e4931fbb21c6d049b39076d40ebdb3bee3 100644
--- a/toolkit/components/passwordmgr/jar.mn
+++ b/toolkit/components/passwordmgr/jar.mn
@@ -4,7 +4,6 @@
 
 toolkit.jar:
 %   content passwordmgr %content/passwordmgr/
-    content/passwordmgr/login.xml                      (content/login.xml)
 *   content/passwordmgr/passwordManager.xul            (content/passwordManager.xul)
     content/passwordmgr/passwordManager.js             (content/passwordManager.js)
     content/passwordmgr/recipes.json                   (content/recipes.json)
diff --git a/toolkit/components/passwordmgr/moz.build b/toolkit/components/passwordmgr/moz.build
index ae27753598f3d8151530faf40d01941e3129e434..72c8c70a470d0de88d41cb7c02f28a84b70f2f12 100644
--- a/toolkit/components/passwordmgr/moz.build
+++ b/toolkit/components/passwordmgr/moz.build
@@ -69,7 +69,6 @@ if CONFIG['OS_TARGET'] == 'WINNT':
 
 if CONFIG['MOZ_BUILD_APP'] == 'browser':
     EXTRA_JS_MODULES += [
-        'LoginDoorhangers.jsm',
         'LoginManagerContextMenu.jsm',
     ]
 
diff --git a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
index 352a0470afc5c004e482576edbf64d16742faaaa..3c393f026eb7d13146cb5a05d34ccb3e6c332314 100644
--- a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
+++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
@@ -1065,6 +1065,8 @@ LoginManagerPrompter.prototype = {
       this._showLoginNotification(aNotifyObj, "password-save",
                                   notificationText, buttons);
     }
+
+    Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save", null);
   },
 
   _removeLoginNotifications : function () {
@@ -1144,6 +1146,8 @@ LoginManagerPrompter.prototype = {
       // userChoice == 1 --> just ignore the login.
       this.log("Ignoring login.");
     }
+
+    Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save", null);
   },
 
 
@@ -1235,6 +1239,9 @@ LoginManagerPrompter.prototype = {
       this._showLoginNotification(aNotifyObj, "password-change",
                                   notificationText, buttons);
     }
+
+    let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
+    Services.obs.notifyObservers(aNewLogin, "passwordmgr-prompt-change", oldGUID);
   },
 
 
@@ -1265,6 +1272,9 @@ LoginManagerPrompter.prototype = {
       this.log("Updating password for user " + aOldLogin.username);
       this._updateLogin(aOldLogin, aNewLogin);
     }
+
+    let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
+    Services.obs.notifyObservers(aNewLogin, "passwordmgr-prompt-change", oldGUID);
   },
 
 
diff --git a/toolkit/components/passwordmgr/test/browser/browser.ini b/toolkit/components/passwordmgr/test/browser/browser.ini
index 9eedd58e0701630365ee76ad06d9526562401260..b99b0899c235d93e2188279a21334a04895f0999 100644
--- a/toolkit/components/passwordmgr/test/browser/browser.ini
+++ b/toolkit/components/passwordmgr/test/browser/browser.ini
@@ -15,7 +15,6 @@ support-files =
   streamConverter_content.sjs
 
 [browser_capture_doorhanger.js]
-skip-if = e10s # Bug 1277105
 support-files =
   subtst_notifications_1.html
   subtst_notifications_2.html
@@ -39,7 +38,6 @@ support-files =
 [browser_DOMFormHasPassword.js]
 [browser_DOMInputPasswordAdded.js]
 [browser_exceptions_dialog.js]
-[browser_filldoorhanger.js]
 [browser_formless_submit_chrome.js]
 [browser_hasInsecureLoginForms.js]
 [browser_hasInsecureLoginForms_streamConverter.js]
diff --git a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js
index 18fb807664a6fa20ea22ff28113ca3ad8e7aa9a3..dc8b33c97bc5e3fa53e2cff2181f0761986bef19 100644
--- a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger.js
@@ -50,6 +50,8 @@ add_task(function* test_clickNever() {
     ok(notif, "got notification popup");
     is(true, Services.logins.getLoginSavingEnabled("http://example.com"),
        "Checking for login saving enabled");
+
+    yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
     clickDoorhangerButton(notif, NEVER_BUTTON);
   });
 
@@ -77,6 +79,8 @@ add_task(function* test_clickRemember() {
     ok(notif, "got notification popup");
 
     is(Services.logins.getAllLogins().length, 0, "Should not have any logins yet");
+
+    yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
     clickDoorhangerButton(notif, REMEMBER_BUTTON);
   });
 
@@ -271,6 +275,8 @@ add_task(function* test_changeUPLoginOnUPForm_dont() {
     is(fieldValues.password, "pass2", "Checking submitted password");
     let notif = getCaptureDoorhanger("password-change");
     ok(notif, "got notification popup");
+
+    yield* checkDoorhangerUsernamePassword("notifyu1", "pass2");
     clickDoorhangerButton(notif, DONT_CHANGE_BUTTON);
   });
 
@@ -293,7 +299,10 @@ add_task(function* test_changeUPLoginOnUPForm_change() {
     is(fieldValues.password, "pass2", "Checking submitted password");
     let notif = getCaptureDoorhanger("password-change");
     ok(notif, "got notification popup");
+
+    yield* checkDoorhangerUsernamePassword("notifyu1", "pass2");
     clickDoorhangerButton(notif, CHANGE_BUTTON);
+
     ok(!getCaptureDoorhanger("password-change"), "popup should be gone");
   });
 
@@ -321,7 +330,10 @@ add_task(function* test_changePLoginOnUPForm() {
     is(fieldValues.password, "pass2", "Checking submitted password");
     let notif = getCaptureDoorhanger("password-change");
     ok(notif, "got notification popup");
+
+    yield* checkDoorhangerUsernamePassword("", "pass2");
     clickDoorhangerButton(notif, CHANGE_BUTTON);
+
     ok(!getCaptureDoorhanger("password-change"), "popup should be gone");
   });
 
@@ -343,7 +355,10 @@ add_task(function* test_changePLoginOnPForm() {
     is(fieldValues.password, "notifyp1", "Checking submitted password");
     let notif = getCaptureDoorhanger("password-change");
     ok(notif, "got notification popup");
+
+    yield* checkDoorhangerUsernamePassword("", "notifyp1");
     clickDoorhangerButton(notif, CHANGE_BUTTON);
+
     ok(!getCaptureDoorhanger("password-change"), "popup should be gone");
   });
 
@@ -490,7 +505,10 @@ add_task(function* test_changeUPLoginOnPUpdateForm() {
     is(fieldValues.password, "pass2", "Checking submitted password");
     let notif = getCaptureDoorhanger("password-change");
     ok(notif, "got notification popup");
+
+    yield* checkDoorhangerUsernamePassword("notifyu1", "pass2");
     clickDoorhangerButton(notif, CHANGE_BUTTON);
+
     ok(!getCaptureDoorhanger("password-change"), "popup should be gone");
   });
 
@@ -522,7 +540,9 @@ add_task(function* test_recipeCaptureFields_NewLogin() {
     let logins = Services.logins.getAllLogins();
     is(logins.length, 0, "Should not have any logins yet");
 
+    yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
     clickDoorhangerButton(notif, REMEMBER_BUTTON);
+
   }, "http://example.org"); // The recipe is for example.org
 
   let logins = Services.logins.getAllLogins();
@@ -615,7 +635,10 @@ add_task(function* test_httpsUpgradeCaptureFields_changePW() {
     is(fieldValues.password, "pass2", "Checking submitted password");
     let notif = getCaptureDoorhanger("password-change");
     ok(notif, "checking for a change popup");
+
+    yield* checkDoorhangerUsernamePassword("notifyu1", "pass2");
     clickDoorhangerButton(notif, CHANGE_BUTTON);
+
     ok(!getCaptureDoorhanger("password-change"), "popup should be gone");
   }, "https://example.com"); // This is HTTPS whereas the saved login is HTTP
 
@@ -643,6 +666,8 @@ add_task(function* test_httpsUpgradeCaptureFields_captureMatchingHTTP() {
     ok(notif, "got notification popup");
 
     is(Services.logins.getAllLogins().length, 1, "Should only have the HTTPS login");
+
+    yield* checkDoorhangerUsernamePassword("notifyu1", "notifyp1");
     clickDoorhangerButton(notif, REMEMBER_BUTTON);
   });
 
diff --git a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js
index 40861c48219064bf38466b60e054b50106809977..1bcfec5eb169633673c804f285c9c028417ff194 100644
--- a/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_capture_doorhanger_window_open.js
@@ -33,17 +33,6 @@ function withTestTabUntilStorageChange(aPageFile, aTaskFn) {
   });
 }
 
-function* checkDoorhangerUsernamePassword(username, password) {
-  yield BrowserTestUtils.waitForCondition(() => {
-    return document.getElementById("password-notification-username").value == username;
-  }, "Wait for nsLoginManagerPrompter writeDataToUI()");
-  is(document.getElementById("password-notification-username").value, username,
-     "Check doorhanger username");
-  is(document.getElementById("password-notification-password").value, password,
-     "Check doorhanger password");
-}
-
-
 add_task(function* setup() {
   yield SimpleTest.promiseFocus(window);
 });
diff --git a/toolkit/components/passwordmgr/test/browser/browser_filldoorhanger.js b/toolkit/components/passwordmgr/test/browser/browser_filldoorhanger.js
deleted file mode 100644
index 37421180141c45977d26b2b6f98b2ebc306becf1..0000000000000000000000000000000000000000
--- a/toolkit/components/passwordmgr/test/browser/browser_filldoorhanger.js
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * All these tests require the experimental login fill UI to be enabled. We also
- * disable autofill for login forms for easier testing of manual fill.
- */
-add_task(function* test_initialize() {
-  Services.prefs.setBoolPref("signon.ui.experimental", true);
-  Services.prefs.setBoolPref("signon.autofillForms", false);
-  registerCleanupFunction(function () {
-    Services.prefs.clearUserPref("signon.ui.experimental");
-    Services.prefs.clearUserPref("signon.autofillForms");
-  });
-});
-
-/**
- * Tests manual fill when the page has a login form.
- */
-add_task(function* test_fill() {
-  Services.logins.addLogin(LoginTestUtils.testData.formLogin({
-    hostname: "https://example.com",
-    formSubmitURL: "https://example.com",
-    username: "username",
-    password: "password",
-  }));
-
-  // The anchor icon may be shown during the initial page load in the new tab,
-  // so we have to set up the observers first. When we receive the notification
-  // from PopupNotifications.jsm, we check it is the one for the right anchor.
-  let anchor = document.getElementById("login-fill-notification-icon");
-  let promiseAnchorShown =
-      TestUtils.topicObserved("PopupNotifications-updateNotShowing",
-                              () => anchor.hasAttribute("showing"));
-
-  yield BrowserTestUtils.withNewTab({
-    gBrowser,
-    url: "https://example.com/browser/toolkit/components/" +
-         "passwordmgr/test/browser/form_basic.html",
-  }, function* (browser) {
-    yield promiseAnchorShown;
-
-    let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
-                                                     "Shown");
-    anchor.click();
-    yield promiseShown;
-
-    let list = document.getElementById("login-fill-list");
-    Assert.equal(list.childNodes.length, 1,
-                 "list.childNodes.length === 1");
-
-    // The button will be focused after the "transitionend" event.
-    list.focus();
-    yield new Promise(resolve => executeSoon(resolve));
-    let details = document.getElementById("login-fill-details");
-    let promiseSubview = BrowserTestUtils.waitForEvent(details,
-                                                       "transitionend", true,
-                                                       e => e.target == details);
-    EventUtils.sendMouseEvent({ type: "click" }, list.childNodes[0]);
-    yield promiseSubview;
-
-    // Clicking the button will dismiss the panel.
-    let promiseHidden = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
-                                                      "popuphidden");
-    document.getElementById("login-fill-use").click();
-    yield promiseHidden;
-
-    yield ContentTask.spawn(browser, null, function* () {
-      let doc = content.document;
-      Assert.equal(doc.getElementById("form-basic-username").value, "username",
-        "result.username === \"username\"");
-      Assert.equal(doc.getElementById("form-basic-password").value, "password",
-        "result.password === \"password\"");
-    });
-  });
-
-  Services.logins.removeAllLogins();
-});
diff --git a/toolkit/components/passwordmgr/test/browser/head.js b/toolkit/components/passwordmgr/test/browser/head.js
index 781b723c74d5153f75ceb48945f27c3ea0e87fe5..786e7697b619c878dccb9a9c02010db57ad8a40e 100644
--- a/toolkit/components/passwordmgr/test/browser/head.js
+++ b/toolkit/components/passwordmgr/test/browser/head.js
@@ -118,4 +118,20 @@ function clickDoorhangerButton(aPopup, aButtonIndex) {
   }
 }
 
+/**
+ * Checks the doorhanger's username and password.
+ *
+ * @param {String} username The username.
+ * @param {String} password The password.
+ */
+function* checkDoorhangerUsernamePassword(username, password) {
+  yield BrowserTestUtils.waitForCondition(() => {
+    return document.getElementById("password-notification-username").value == username;
+  }, "Wait for nsLoginManagerPrompter writeDataToUI()");
+  is(document.getElementById("password-notification-username").value, username,
+     "Check doorhanger username");
+  is(document.getElementById("password-notification-password").value, password,
+     "Check doorhanger password");
+}
+
 // End popup notification (doorhanger) functions //
diff --git a/toolkit/components/passwordmgr/test/chrome/chrome.ini b/toolkit/components/passwordmgr/test/chrome/chrome.ini
index b5ada17cd80311cc9e5341551a6545ccac6d8ee4..c3e96c23fbbdf6dda140f796f8f722167c5017a1 100644
--- a/toolkit/components/passwordmgr/test/chrome/chrome.ini
+++ b/toolkit/components/passwordmgr/test/chrome/chrome.ini
@@ -5,7 +5,7 @@ skip-if = buildapp == 'b2g' || os == 'android'
 skip-if = true # Bug 1173337
 support-files =
   ../formsubmit.sjs
-  ../notification_common.js
+  notification_common.js
   privbrowsing_perwindowpb_iframe.html
   subtst_privbrowsing_1.html
   subtst_privbrowsing_2.html
diff --git a/toolkit/components/passwordmgr/test/notification_common.js b/toolkit/components/passwordmgr/test/chrome/notification_common.js
similarity index 100%
rename from toolkit/components/passwordmgr/test/notification_common.js
rename to toolkit/components/passwordmgr/test/chrome/notification_common.js
diff --git a/toolkit/components/passwordmgr/test/mochitest.ini b/toolkit/components/passwordmgr/test/mochitest.ini
index bb5cb948191224b90dd09285025dda9117cd2b92..41bd9b657688eedc8820995c2ef7abde70075c72 100644
--- a/toolkit/components/passwordmgr/test/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest.ini
@@ -4,7 +4,6 @@ support-files =
   authenticate.sjs
   blank.html
   formsubmit.sjs
-  notification_common.js
   prompt_common.js
   pwmgr_common.js
   subtst_master_pass.html
diff --git a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
index c3baaa86ecac6abe798adbc1aaf15aaf499e4b1e..a4b46a4760aa667b34eefe90fc507bb06b9d18d8 100644
--- a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
@@ -9,7 +9,6 @@ support-files =
   ../blank.html
   ../browser/form_basic.html
   ../browser/form_cross_origin_secure_action.html
-  ../notification_common.js
   ../pwmgr_common.js
   auth2/authenticate.sjs
 
@@ -49,7 +48,7 @@ skip-if = toolkit == 'android' # autocomplete
 [test_prompt.html]
 skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts
 [test_prompt_http.html]
-skip-if = e10s || os == "linux" || toolkit == 'android' # Tests desktop prompts
+skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts
 [test_prompt_promptAuth.html]
 skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts
 [test_prompt_promptAuth_proxy.html]
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html
index 632bbc7f40aa7835913d34def5b9aa033270b480..026847b9a6818934f726d7706ccb08c820613d42 100644
--- a/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html
@@ -7,7 +7,6 @@
   <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <script type="text/javascript" src="prompt_common.js"></script>
-  <script type="text/javascript" src="notification_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
@@ -146,40 +145,13 @@ add_task(function* test_iframe() {
   };
   promptDone = handlePrompt(state, action);
   iframeLoaded = onloadPromiseFor("iframe");
+  let promptShownPromise = promisePromptShown("passwordmgr-prompt-change");
   iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1-new";
   yield promptDone;
   yield iframeLoaded;
   checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1-new"},
                       iframe.contentDocument);
-
-
-  var pwchanged = promiseStorageChanged(["modifyLogin"]);
-
-  // Check for the popup notification, and change the password.
-  var popupNotifications = getPopupNotifications(window.top);
-  popup = getPopup(popupNotifications, "password-change");
-  ok(popup, "got popup notification");
-  clickPopupButton(popup, kChangeButton);
-  popup.remove();
-
-  yield pwchanged;
-
-  // Housekeeping: change it back
-  runInParent(() => {
-    const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-    Cu.import("resource://gre/modules/Services.jsm");
-
-    var tmpLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
-                   createInstance(Ci.nsILoginInfo);
-    tmpLogin.init("http://mochi.test:8888", null, "mochitest",
-                  "mochiuser1", "mochipass1-new", "", "");
-    var login3A = Cc["@mozilla.org/login-manager/loginInfo;1"].
-                  createInstance(Ci.nsILoginInfo);
-    login3A.init("http://mochi.test:8888", null, "mochitest",
-                 "mochiuser1", "mochipass1", "", "");
-
-    Services.logins.modifyLogin(tmpLogin, login3A);
-  });
+  yield promptShownPromise;
 
   // Same as last test, but for a realm we haven't already authenticated
   // to (but have an existing saved login for, so that we'll trigger
@@ -205,24 +177,15 @@ add_task(function* test_iframe() {
   };
   promptDone = handlePrompt(state, action);
   iframeLoaded = onloadPromiseFor("iframe");
+  promptShownPromise = promisePromptShown("passwordmgr-prompt-change");
   iframe.src = "authenticate.sjs?user=mochiuser3&pass=mochipass3-new&realm=mochitest3";
   yield promptDone;
   yield iframeLoaded;
   checkEchoedAuthInfo({user: "mochiuser3", pass: "mochipass3-new"},
                       iframe.contentDocument);
+  yield promptShownPromise;
 
-  pwchanged = promiseStorageChanged(["modifyLogin"]);
-
-  // Check for the popup notification, and change the password.
-  popup = getPopup(popupNotifications, "password-change");
-  ok(popup, "got popup notification");
-  clickPopupButton(popup, kChangeButton);
-  popup.remove();
-
-  yield pwchanged;
-
-  // Housekeeping: change it back to the original login4. Actually,
-  // just delete it and we'll re-add it as the next test.
+  // Housekeeping: Delete login4 to test the save prompt in the next test.
   runInParent(() => {
     const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
     Cu.import("resource://gre/modules/Services.jsm");
@@ -230,7 +193,7 @@ add_task(function* test_iframe() {
     var tmpLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
                    createInstance(Ci.nsILoginInfo);
     tmpLogin.init("http://mochi.test:8888", null, "mochitest3",
-                  "mochiuser3", "mochipass3-new", "", "");
+                  "mochiuser3", "mochipass3-old", "", "");
     Services.logins.removeLogin(tmpLogin);
 
     // Clear cached auth from this subtest, and avoid leaking due to bug 459620.
@@ -263,20 +226,13 @@ add_task(function* test_iframe() {
   promptDone = handlePrompt(state, action);
 
   iframeLoaded = onloadPromiseFor("iframe");
+  promptShownPromise = promisePromptShown("passwordmgr-prompt-save");
   iframe.src = "authenticate.sjs?user=mochiuser3&pass=mochipass3-old&realm=mochitest3";
   yield promptDone;
   yield iframeLoaded;
   checkEchoedAuthInfo({user: "mochiuser3", pass: "mochipass3-old"},
                       iframe.contentDocument);
-
-  var pwsaved = promiseStorageChanged(["addLogin"]);
-
-  // Check for the popup notification, and change the password.
-  popup = getPopup(popupNotifications, "password-save");
-  ok(popup, "got popup notification");
-  clickPopupButton(popup, kRememberButton);
-  popup.remove();
-  yield pwsaved;
+  yield promptShownPromise;
 });
 </script>
 </pre>
diff --git a/toolkit/components/passwordmgr/test/pwmgr_common.js b/toolkit/components/passwordmgr/test/pwmgr_common.js
index 0eabac1b2b34cb7003b3960e7075bdade20aa0bf..fc06e4aec8cf6681ed1e16b9a1798a470d783a4c 100644
--- a/toolkit/components/passwordmgr/test/pwmgr_common.js
+++ b/toolkit/components/passwordmgr/test/pwmgr_common.js
@@ -320,6 +320,17 @@ function promiseStorageChanged(expectedChangeTypes) {
   });
 }
 
+function promisePromptShown(expectedTopic) {
+  return new Promise((resolve, reject) => {
+    function onPromptShown({ topic, data }) {
+      is(topic, expectedTopic, "Check expected prompt topic");
+      chromeScript.removeMessageListener("promptShown", onPromptShown);
+      resolve();
+    }
+    chromeScript.addMessageListener("promptShown", onPromptShown);
+  });
+}
+
 /**
  * Run a function synchronously in the parent process and destroy it in the test cleanup function.
  * @param {Function|String} aFunctionOrURL - either a function that will be stringified and run
@@ -372,6 +383,15 @@ if (this.addMessageListener) {
   }
   Services.obs.addObserver(onStorageChanged, "passwordmgr-storage-changed", false);
 
+  function onPrompt(subject, topic, data) {
+    sendAsyncMessage("promptShown", {
+      topic,
+      data,
+    });
+  }
+  Services.obs.addObserver(onPrompt, "passwordmgr-prompt-change", false);
+  Services.obs.addObserver(onPrompt, "passwordmgr-prompt-save", false);
+
   addMessageListener("setupParent", ({selfFilling = false} = {selfFilling: false}) => {
     // Force LoginManagerParent to init for the tests since it's normally delayed
     // by apps such as on Android.
@@ -432,6 +452,18 @@ if (this.addMessageListener) {
       if (LoginManagerParent._recipeManager) {
         LoginManagerParent._recipeManager.reset();
       }
+
+      // Cleanup PopupNotifications (if on a relevant platform)
+      let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+      if (chromeWin && chromeWin.PopupNotifications) {
+        let notes = chromeWin.PopupNotifications._currentNotifications;
+        if (notes.length > 0) {
+          dump("Removing " + notes.length + " popup notifications.\n");
+        }
+        for (let note of notes) {
+	  note.remove();
+        }
+      }
     });
   });
 
diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json
index 6fd6048c6934f1c115c5dbcc3d285c262a620443..9dc410c046332f9ca1eeb5760959436280d91198 100644
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -60,38 +60,43 @@
     "alert_emails": ["benjamin@smedbergs.us"]
   },
   "APPLICATION_REPUTATION_SHOULD_BLOCK": {
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "never",
     "kind": "boolean",
-    "description": "Application reputation verdict (shouldBlock=false is OK)"
+    "description": "Overall (local or remote) application reputation verdict (shouldBlock=false is OK)."
   },
   "APPLICATION_REPUTATION_LOCAL": {
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values": 3,
     "description": "Application reputation local results (0=ALLOW, 1=BLOCK, 2=NONE)"
   },
   "APPLICATION_REPUTATION_SERVER": {
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values": 3,
-    "description": "Application reputation remote status (0=OK, 1=FAIL, 2=INVALID)"
+    "description": "Status of the application reputation remote lookup (0=OK, 1=failed, 2=invalid protobuf response)"
   },
   "APPLICATION_REPUTATION_SERVER_VERDICT": {
-    "expires_in_version": "52",
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+    "expires_in_version": "56",
     "releaseChannelCollection": "opt-out",
     "bug_numbers": [1272788],
     "kind": "enumerated",
     "n_values": 8,
-    "description": "Application reputation remote response (0=SAFE, 1=DANGEROUS, 2=UNCOMMON, 3=POTENTIALLY_UNWANTED, 4=DANGEROUS_HOST)"
+    "description": "Application reputation remote verdict (0=SAFE, 1=DANGEROUS, 2=UNCOMMON, 3=POTENTIALLY_UNWANTED, 4=DANGEROUS_HOST, 5=UNKNOWN)"
   },
   "APPLICATION_REPUTATION_COUNT": {
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Application reputation query count (both local and remote)"
   },
   "APPLICATION_REPUTATION_REMOTE_LOOKUP_TIMEOUT": {
-    "alert_emails": ["gcp@mozilla.com", "francois@mozilla.com"],
-    "expires_in_version": "55",
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+    "expires_in_version": "56",
     "kind": "boolean",
     "bug_numbers": [1172689],
     "description": "Recorded when application reputation remote lookup is performed, `true` is recorded if the lookup times out."
@@ -3621,6 +3626,7 @@
     "description": "Time spent checking for and notifying listeners that the user is idle (ms)"
   },
   "URLCLASSIFIER_LOOKUP_TIME": {
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 500,
@@ -3628,6 +3634,7 @@
     "description": "Time spent per dbservice lookup (ms)"
   },
   "URLCLASSIFIER_CL_CHECK_TIME": {
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 500,
@@ -3635,6 +3642,7 @@
     "description": "Time spent per classifier lookup (ms)"
   },
   "URLCLASSIFIER_CL_UPDATE_TIME": {
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "never",
     "kind": "exponential",
     "low": 20,
@@ -3643,6 +3651,7 @@
     "description": "Time spent per classifier update (ms)"
   },
   "URLCLASSIFIER_PS_FILELOAD_TIME": {
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 1000,
@@ -3650,13 +3659,15 @@
     "description": "Time spent loading PrefixSet from file (ms)"
   },
   "URLCLASSIFIER_PS_FALLOCATE_TIME": {
-    "expires_in_version": "default",
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+    "expires_in_version": "never",
     "kind": "exponential",
     "high": 1000,
     "n_buckets": 10,
     "description": "Time spent fallocating PrefixSet (ms)"
   },
   "URLCLASSIFIER_PS_CONSTRUCT_TIME": {
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 5000,
@@ -3664,6 +3675,7 @@
     "description": "Time spent constructing PrefixSet from DB (ms)"
   },
   "URLCLASSIFIER_LC_PREFIXES": {
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "never",
     "kind": "linear",
     "high": 1500000,
@@ -3671,19 +3683,15 @@
     "description": "Size of the prefix cache in entries"
   },
   "URLCLASSIFIER_LC_COMPLETIONS": {
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 200,
     "n_buckets": 10,
     "description": "Size of the completion cache in entries"
   },
-  "URLCLASSIFIER_PS_FAILURE": {
-    "expires_in_version": "default",
-    "kind": "boolean",
-    "description": "Did UrlClassifier fail to construct the PrefixSet?"
-  },
   "URLCLASSIFIER_UPDATE_REMOTE_STATUS": {
-    "alert_emails": ["gcp@mozilla.com", "francois@mozilla.com"],
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values": 16,
@@ -3691,7 +3699,7 @@
     "description": "Server HTTP status code from SafeBrowsing database updates. (0=1xx, 1=200, 2=2xx, 3=204, 4=3xx, 5=400, 6=4xx, 7=403, 8=404, 9=408, 10=413, 11=5xx, 12=502|504|511, 13=503, 14=505, 15=Other)"
   },
   "URLCLASSIFIER_COMPLETE_REMOTE_STATUS": {
-    "alert_emails": ["gcp@mozilla.com", "francois@mozilla.com"],
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values": 16,
@@ -3699,8 +3707,8 @@
     "description": "Server HTTP status code from remote SafeBrowsing gethash lookups. (0=1xx, 1=200, 2=2xx, 3=204, 4=3xx, 5=400, 6=4xx, 7=403, 8=404, 9=408, 10=413, 11=5xx, 12=502|504|511, 13=503, 14=505, 15=Other)"
   },
   "URLCLASSIFIER_COMPLETE_TIMEOUT": {
-    "alert_emails": ["gcp@mozilla.com", "francois@mozilla.com"],
-    "expires_in_version": "52",
+    "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+    "expires_in_version": "56",
     "kind": "boolean",
     "bug_numbers": [1172688],
     "description": "This metric is recorded every time a gethash lookup is performed, `true` is recorded if the lookup times out."
@@ -5536,6 +5544,14 @@
     "kind": "count",
     "keyed": true,
     "description": "a testing histogram; not meant to be touched"
+  },
+    "TELEMETRY_TEST_KEYED_BOOLEAN": {
+    "alert_emails": ["telemetry-client-dev@mozilla.com"],
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "keyed": true,
+    "bug_numbers": [1299144],
+    "description": "a testing histogram; not meant to be touched"
   },
   "TELEMETRY_TEST_RELEASE_OPTOUT": {
     "expires_in_version": "never",
diff --git a/toolkit/components/telemetry/Telemetry.cpp b/toolkit/components/telemetry/Telemetry.cpp
index f9854c70cd21020e6d0e24e20696d0a88179e03b..931574f6135d74bd05b102faf61a68d65b9e5d85 100644
--- a/toolkit/components/telemetry/Telemetry.cpp
+++ b/toolkit/components/telemetry/Telemetry.cpp
@@ -1030,17 +1030,6 @@ TelemetryImpl::InitMemoryReporter() {
   RegisterWeakMemoryReporter(this);
 }
 
-NS_IMETHODIMP
-TelemetryImpl::NewKeyedHistogram(const nsACString &name, const nsACString &expiration, uint32_t histogramType,
-                            uint32_t min, uint32_t max, uint32_t bucketCount, JSContext *cx,
-                            uint8_t optArgCount, JS::MutableHandle<JS::Value> ret)
-{
-  return TelemetryHistogram::NewKeyedHistogram(name, expiration, histogramType,
-                                               min, max, bucketCount,
-                                               cx, optArgCount, ret);
-}
-
-
 bool
 TelemetryImpl::ReflectSQL(const SlowSQLEntryType *entry,
                           const Stat *stat,
diff --git a/toolkit/components/telemetry/TelemetryHistogram.cpp b/toolkit/components/telemetry/TelemetryHistogram.cpp
index c8711bee124357e3a0dfba0ca7eab8eefb726ef7..1aae6bb539f7b285cd95354fe6ebc6533903543d 100644
--- a/toolkit/components/telemetry/TelemetryHistogram.cpp
+++ b/toolkit/components/telemetry/TelemetryHistogram.cpp
@@ -231,12 +231,6 @@ internal_IsHistogramEnumId(mozilla::Telemetry::ID aID)
   return aID < mozilla::Telemetry::HistogramCount;
 }
 
-bool
-internal_IsValidHistogramName(const nsACString& name)
-{
-  return !FindInReadable(NS_LITERAL_CSTRING(KEYED_HISTOGRAM_NAME_SEPARATOR), name);
-}
-
 // Note: this is completely unrelated to mozilla::IsEmpty.
 bool
 internal_IsEmpty(const Histogram *h)
@@ -2012,42 +2006,6 @@ TelemetryHistogram::GetHistogramName(mozilla::Telemetry::ID id)
   return h.id();
 }
 
-nsresult
-TelemetryHistogram::NewKeyedHistogram(const nsACString &name,
-                                      const nsACString &expiration,
-                                      uint32_t histogramType,
-                                      uint32_t min, uint32_t max,
-                                      uint32_t bucketCount, JSContext *cx,
-                                      uint8_t optArgCount,
-                                      JS::MutableHandle<JS::Value> ret)
-{
-  KeyedHistogram* keyed = nullptr;
-  {
-    StaticMutexAutoLock locker(gTelemetryHistogramMutex);
-    if (!internal_IsValidHistogramName(name)) {
-      return NS_ERROR_INVALID_ARG;
-    }
-
-    nsresult rv
-      = internal_CheckHistogramArguments(histogramType, min, max,
-                                         bucketCount, optArgCount == 3);
-    if (NS_FAILED(rv)) {
-      return rv;
-    }
-
-    keyed = new KeyedHistogram(name, expiration, histogramType,
-                               min, max, bucketCount,
-                               nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN);
-    if (MOZ_UNLIKELY(!gKeyedHistograms.Put(name, keyed, mozilla::fallible))) {
-      delete keyed;
-      return NS_ERROR_OUT_OF_MEMORY;
-    }
-  }
-
-  // Runs without protection from |gTelemetryHistogramMutex|
-  return internal_WrapAndReturnKeyedHistogram(keyed, cx, ret);
-}
-
 nsresult
 TelemetryHistogram::HistogramFrom(const nsACString &name,
                                   const nsACString &existing_name,
diff --git a/toolkit/components/telemetry/TelemetryHistogram.h b/toolkit/components/telemetry/TelemetryHistogram.h
index 4bb83dbf5f309519fa66812c752f8547786d659d..2995ae4c0208d4c8fe9b02e20ea28184be677d7e 100644
--- a/toolkit/components/telemetry/TelemetryHistogram.h
+++ b/toolkit/components/telemetry/TelemetryHistogram.h
@@ -56,12 +56,6 @@ GetKeyedHistogramById(const nsACString &name, JSContext *cx,
 const char*
 GetHistogramName(mozilla::Telemetry::ID id);
 
-nsresult
-NewKeyedHistogram(const nsACString &name, const nsACString &expiration,
-                  uint32_t histogramType, uint32_t min, uint32_t max,
-                  uint32_t bucketCount, JSContext *cx,
-                  uint8_t optArgCount, JS::MutableHandle<JS::Value> ret);
-
 nsresult
 HistogramFrom(const nsACString &name, const nsACString &existing_name,
               JSContext *cx, JS::MutableHandle<JS::Value> ret);
diff --git a/toolkit/components/telemetry/docs/data/sync-ping.rst b/toolkit/components/telemetry/docs/data/sync-ping.rst
index 77af96f6dd4304ac4946ed2c4389a76e543eb521..a2066c0269b76dd410e5a21b3d6c8290b3eefcb3 100644
--- a/toolkit/components/telemetry/docs/data/sync-ping.rst
+++ b/toolkit/components/telemetry/docs/data/sync-ping.rst
@@ -25,6 +25,7 @@ Structure:
           when: <integer milliseconds since epoch>,
           took: <integer duration in milliseconds>,
           uid: <string>, // Hashed FxA unique ID, or string of 32 zeros.
+          deviceID: <string>, // Hashed FxA Device ID, hex string of 64 characters, not included if the user is not logged in.
           didLogin: <bool>, // Optional, is this the first sync after login? Excluded if we don't know.
           why: <string>, // Optional, why the sync occured, excluded if we don't know.
 
diff --git a/toolkit/components/telemetry/histogram-whitelists.json b/toolkit/components/telemetry/histogram-whitelists.json
index ea916e400af31ea371865e187ee02eb275dd21c8..2ca545dfb0ead99de3391d0b2221d3eca7264827 100644
--- a/toolkit/components/telemetry/histogram-whitelists.json
+++ b/toolkit/components/telemetry/histogram-whitelists.json
@@ -6,11 +6,6 @@
     "A11Y_ISIMPLEDOM_USAGE_FLAG",
     "A11Y_UPDATE_TIME",
     "ADDON_SHIM_USAGE",
-    "APPLICATION_REPUTATION_COUNT",
-    "APPLICATION_REPUTATION_LOCAL",
-    "APPLICATION_REPUTATION_SERVER",
-    "APPLICATION_REPUTATION_SERVER_VERDICT",
-    "APPLICATION_REPUTATION_SHOULD_BLOCK",
     "AUDIOSTREAM_FIRST_OPEN_MS",
     "AUDIOSTREAM_LATER_OPEN_MS",
     "AUTO_REJECTED_TRANSLATION_OFFERS",
@@ -749,15 +744,6 @@
     "TRANSLATED_PAGES_BY_LANGUAGE",
     "TRANSLATION_OPPORTUNITIES",
     "TRANSLATION_OPPORTUNITIES_BY_LANGUAGE",
-    "URLCLASSIFIER_CL_CHECK_TIME",
-    "URLCLASSIFIER_CL_UPDATE_TIME",
-    "URLCLASSIFIER_LC_COMPLETIONS",
-    "URLCLASSIFIER_LC_PREFIXES",
-    "URLCLASSIFIER_LOOKUP_TIME",
-    "URLCLASSIFIER_PS_CONSTRUCT_TIME",
-    "URLCLASSIFIER_PS_FAILURE",
-    "URLCLASSIFIER_PS_FALLOCATE_TIME",
-    "URLCLASSIFIER_PS_FILELOAD_TIME",
     "VIDEO_CANPLAYTYPE_H264_CONSTRAINT_SET_FLAG",
     "VIDEO_CANPLAYTYPE_H264_LEVEL",
     "VIDEO_CANPLAYTYPE_H264_PROFILE",
@@ -1812,7 +1798,6 @@
     "URLCLASSIFIER_LC_PREFIXES",
     "URLCLASSIFIER_LOOKUP_TIME",
     "URLCLASSIFIER_PS_CONSTRUCT_TIME",
-    "URLCLASSIFIER_PS_FAILURE",
     "URLCLASSIFIER_PS_FALLOCATE_TIME",
     "URLCLASSIFIER_PS_FILELOAD_TIME",
     "VIDEO_CANPLAYTYPE_H264_CONSTRAINT_SET_FLAG",
diff --git a/toolkit/components/telemetry/nsITelemetry.idl b/toolkit/components/telemetry/nsITelemetry.idl
index 23e5da6abfd167d91c1a028e65e010bcece5ad08..c65e99796b41bd33ca62e49ef2acd9e79041131e 100644
--- a/toolkit/components/telemetry/nsITelemetry.idl
+++ b/toolkit/components/telemetry/nsITelemetry.idl
@@ -214,30 +214,6 @@ interface nsITelemetry : nsISupports
   [implicit_jscontext]
   readonly attribute jsval keyedHistogramSnapshots;
 
-  /**
-   * Create and return a keyed histogram.  Parameters:
-   *
-   * @param name Unique histogram name
-   * @param expiration Expiration version
-   * @param type - HISTOGRAM_EXPONENTIAL, HISTOGRAM_LINEAR, HISTOGRAM_BOOLEAN, HISTOGRAM_FLAG or HISTOGRAM_COUNT
-   * @param min - Minimal bucket size
-   * @param max - Maximum bucket size
-   * @param bucket_count - number of buckets in the histogram.
-   * The returned object has the following functions:
-   *   add(string key, [optional] int) - Add an int value to the histogram for that key. If no histogram for that key exists yet, it is created.
-   *   snapshot([optional] string key) - If key is provided, returns a snapshot for the histogram with that key or null. If key is not provided, returns the snapshots of all the registered keys in the form {key1: snapshot1, key2: snapshot2, ...}.
-   *   keys() - Returns an array with the string keys of the currently registered histograms
-   *   clear() - Clears the registered histograms from this.
-   *   dataset() - identifies what dataset this is in: DATASET_RELEASE_CHANNEL_OPTOUT or ...OPTIN
-   */
-  [implicit_jscontext, optional_argc]
-  jsval newKeyedHistogram(in ACString name,
-                          in ACString expiration,
-                          in unsigned long histogram_type,
-                          [optional] in uint32_t min,
-                          [optional] in uint32_t max,
-                          [optional] in uint32_t bucket_count);
-
   /**
    * Returns an array whose values are the names of histograms defined
    * in Histograms.json.
@@ -248,10 +224,15 @@ interface nsITelemetry : nsISupports
                                  [retval, array, size_is(count)] out string histograms);
 
   /**
-   * Same as newKeyedHistogram above, but for histograms registered in TelemetryHistograms.h.
+   * Create and return a histogram registered in TelemetryHistograms.h.
    *
    * @param id - unique identifier from TelemetryHistograms.h
-   * The returned object has the same functions as a histogram returned from newKeyedHistogram.
+   * The returned object has the following functions:
+   *   add(string key, [optional] int) - Add an int value to the histogram for that key. If no histogram for that key exists yet, it is created.
+   *   snapshot([optional] string key) - If key is provided, returns a snapshot for the histogram with that key or null. If key is not provided, returns the snapshots of all the registered keys in the form {key1: snapshot1, key2: snapshot2, ...}.
+   *   keys() - Returns an array with the string keys of the currently registered histograms
+   *   clear() - Clears the registered histograms from this.
+   *   dataset() - identifies what dataset this is in: DATASET_RELEASE_CHANNEL_OPTOUT or ...OPTIN
    */
   [implicit_jscontext]
   jsval getKeyedHistogramById(in ACString id);
diff --git a/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js b/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js
index 0525517863867afcc62ee2e1b587af59065b717b..d93f0b470af9bdb662ce37f9ce408c66b1a4805e 100644
--- a/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js
+++ b/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js
@@ -461,15 +461,6 @@ add_task(function* test_keyed_histogram() {
   // Check that invalid names get rejected.
 
   let threw = false;
-  try {
-    Telemetry.newKeyedHistogram("test::invalid # histogram", "never", Telemetry.HISTOGRAM_BOOLEAN);
-  } catch (e) {
-    // This should throw as we reject names with the # separator
-    threw = true;
-  }
-  Assert.ok(threw, "newKeyedHistogram should have thrown");
-
-  threw = false;
   try {
     Telemetry.getKeyedHistogramById("test::unknown histogram", "never", Telemetry.HISTOGRAM_BOOLEAN);
   } catch (e) {
@@ -480,7 +471,7 @@ add_task(function* test_keyed_histogram() {
 });
 
 add_task(function* test_keyed_boolean_histogram() {
-  const KEYED_ID = "test::keyed::boolean";
+  const KEYED_ID = "TELEMETRY_TEST_KEYED_BOOLEAN";
   let KEYS = numberRange(0, 2).map(i => "key" + (i + 1));
   KEYS.push("漢語");
   let histogramBase = {
@@ -495,7 +486,7 @@ add_task(function* test_keyed_boolean_histogram() {
   let testKeys = [];
   let testSnapShot = {};
 
-  let h = Telemetry.newKeyedHistogram(KEYED_ID, "never", Telemetry.HISTOGRAM_BOOLEAN);
+  let h = Telemetry.getKeyedHistogramById(KEYED_ID);
   for (let i=0; i<2; ++i) {
     let key = KEYS[i];
     h.add(key, true);
@@ -528,7 +519,7 @@ add_task(function* test_keyed_boolean_histogram() {
 });
 
 add_task(function* test_keyed_count_histogram() {
-  const KEYED_ID = "test::keyed::count";
+  const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
   const KEYS = numberRange(0, 5).map(i => "key" + (i + 1));
   let histogramBase = {
     "min": 1,
@@ -542,7 +533,7 @@ add_task(function* test_keyed_count_histogram() {
   let testKeys = [];
   let testSnapShot = {};
 
-  let h = Telemetry.newKeyedHistogram(KEYED_ID, "never", Telemetry.HISTOGRAM_COUNT);
+  let h = Telemetry.getKeyedHistogramById(KEYED_ID);
   for (let i=0; i<4; ++i) {
     let key = KEYS[i];
     let value = i*2 + 1;
@@ -583,8 +574,8 @@ add_task(function* test_keyed_count_histogram() {
 });
 
 add_task(function* test_keyed_flag_histogram() {
-  const KEYED_ID = "test::keyed::flag";
-  let h = Telemetry.newKeyedHistogram(KEYED_ID, "never", Telemetry.HISTOGRAM_FLAG);
+  const KEYED_ID = "TELEMETRY_TEST_KEYED_FLAG";
+  let h = Telemetry.getKeyedHistogramById(KEYED_ID);
 
   const KEY = "default";
   h.add(KEY, true);
@@ -634,12 +625,6 @@ add_task(function* test_keyed_histogram_recording() {
   Assert.equal(h.snapshot(TEST_KEY).sum, 0,
                "The keyed histograms should not record any data.");
 
-  // Runtime created histograms should not be recorded.
-  h = Telemetry.newKeyedHistogram("test::runtime_keyed_boolean", "never", Telemetry.HISTOGRAM_BOOLEAN);
-  h.add(TEST_KEY, 1);
-  Assert.equal(h.snapshot(TEST_KEY).sum, 0,
-               "The keyed histogram should not record any data.");
-
   // Check that extended histograms are recorded when required.
   Telemetry.canRecordExtended = true;
 
diff --git a/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_cookies_stored.js b/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_cookies_stored.js
index be3ee5ab4dc30157f49fa25385ee7d3a07e2aec5..16aa382d7140eebdca447a6574906f61f7411ed2 100644
--- a/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_cookies_stored.js
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_bg_no_cookies_stored.js
@@ -15,14 +15,15 @@ function* runTests() {
   removeThumbnail(url);
   // now load it up in a browser - it should *not* be red, otherwise the
   // cookie above was saved.
-  let tab = gBrowser.loadOneTab(url, { inBackground: false });
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url);
   let browser = tab.linkedBrowser;
-  yield whenLoaded(browser);
 
   // The root element of the page shouldn't be red.
-  let redStr = "rgb(255, 0, 0)";
-  isnot(browser.contentDocument.documentElement.style.backgroundColor,
-        redStr,
-        "The page shouldn't be red.");
+  yield ContentTask.spawn(browser, null, function() {
+    Assert.notEqual(content.document.documentElement.style.backgroundColor,
+                    "rgb(255, 0, 0)",
+                    "The page shouldn't be red.");
+  });
+
   gBrowser.removeTab(tab);
 }
diff --git a/toolkit/components/url-classifier/LookupCache.cpp b/toolkit/components/url-classifier/LookupCache.cpp
index d03f1ce052d5dd87d97b417cb647b5265c554328..98e334db1e7dfe8a47a8f2e0fb9fd41acaf4727b 100644
--- a/toolkit/components/url-classifier/LookupCache.cpp
+++ b/toolkit/components/url-classifier/LookupCache.cpp
@@ -499,9 +499,7 @@ LookupCache::ConstructPrefixSet(AddPrefixArray& aAddPrefixes)
 
   // construct new one, replace old entries
   nsresult rv = mPrefixSet->SetPrefixes(array.Elements(), array.Length());
-  if (NS_FAILED(rv)) {
-    goto error_bailout;
-  }
+  NS_ENSURE_SUCCESS(rv, rv);
 
 #ifdef DEBUG
   uint32_t size;
@@ -512,10 +510,6 @@ LookupCache::ConstructPrefixSet(AddPrefixArray& aAddPrefixes)
   mPrimed = true;
 
   return NS_OK;
-
- error_bailout:
-  Telemetry::Accumulate(Telemetry::URLCLASSIFIER_PS_FAILURE, 1);
-  return rv;
 }
 
 nsresult
diff --git a/toolkit/components/url-classifier/content/listmanager.js b/toolkit/components/url-classifier/content/listmanager.js
index 60b37442cdfd9dd5daa63bab0f03caeea13da76f..aeca28ae5b9b38785fe021154aadf16efc1262ad 100644
--- a/toolkit/components/url-classifier/content/listmanager.js
+++ b/toolkit/components/url-classifier/content/listmanager.js
@@ -388,7 +388,12 @@ PROT_ListManager.prototype.makeUpdateRequest_ = function(updateUrl, tableData) {
   }
 
   if (useProtobuf) {
-    let tableArray = streamerMap.tableList.split(',');
+    let tableArray = [];
+    Object.keys(streamerMap.tableNames).forEach(aTableName => {
+      if (streamerMap.tableNames[aTableName]) {
+        tableArray.push(aTableName);
+      }
+    });
 
     // The state is a byte stream which server told us from the
     // last table update. The state would be used to do the partial
diff --git a/toolkit/components/url-classifier/nsUrlClassifierUtils.cpp b/toolkit/components/url-classifier/nsUrlClassifierUtils.cpp
index 4677b27497391cd74c0e1192fa5aef3c1c7f04ac..fa2f52523a0239776368763da378bd194ed426fd 100644
--- a/toolkit/components/url-classifier/nsUrlClassifierUtils.cpp
+++ b/toolkit/components/url-classifier/nsUrlClassifierUtils.cpp
@@ -218,6 +218,7 @@ static const struct {
 
   // For testing purpose.
   { "test-phish-proto",    SOCIAL_ENGINEERING_PUBLIC}, // 2
+  { "test-unwanted-proto", UNWANTED_SOFTWARE}, // 3
 };
 
 NS_IMETHODIMP
diff --git a/toolkit/components/url-classifier/tests/unit/test_listmanager.js b/toolkit/components/url-classifier/tests/unit/test_listmanager.js
index e7d4353aa778f398fdc1ec685949f89ab944fe4f..d6d8a56ddffed060327b4b2faa1250a4f8849ee1 100644
--- a/toolkit/components/url-classifier/tests/unit/test_listmanager.js
+++ b/toolkit/components/url-classifier/tests/unit/test_listmanager.js
@@ -31,13 +31,19 @@ const TEST_TABLE_DATA_LIST = [
   }
 ];
 
-// This table has a different update URL (for v4).
+// These tables have a different update URL (for v4).
 const TEST_TABLE_DATA_V4 = {
   tableName: "test-phish-proto",
   providerName: "google4",
   updateUrl: "http://localhost:5555/safebrowsing/update?",
   gethashUrl: "http://localhost:5555/safebrowsing/gethash-v4",
 };
+const TEST_TABLE_DATA_V4_DISABLED = {
+  tableName: "test-unwanted-proto",
+  providerName: "google4",
+  updateUrl: "http://localhost:5555/safebrowsing/update?",
+  gethashUrl: "http://localhost:5555/safebrowsing/gethash-v4",
+};
 
 const PREF_NEXTUPDATETIME = "browser.safebrowsing.provider.google.nextupdatetime";
 const PREF_NEXTUPDATETIME_V4 = "browser.safebrowsing.provider.google4.nextupdatetime";
@@ -79,6 +85,12 @@ gListManager.registerTable(TEST_TABLE_DATA_V4.tableName,
                            TEST_TABLE_DATA_V4.updateUrl,
                            TEST_TABLE_DATA_V4.gethashUrl);
 
+// To test Bug 1302044.
+gListManager.registerTable(TEST_TABLE_DATA_V4_DISABLED.tableName,
+                           TEST_TABLE_DATA_V4_DISABLED.providerName,
+                           TEST_TABLE_DATA_V4_DISABLED.updateUrl,
+                           TEST_TABLE_DATA_V4_DISABLED.gethashUrl);
+
 const SERVER_INVOLVED_TEST_CASE_LIST = [
   // - Do table0 update.
   // - Server would respond "a:5:32:32\n[DATA]".
@@ -122,7 +134,12 @@ const SERVER_INVOLVED_TEST_CASE_LIST = [
     TEST_TABLE_DATA_LIST.forEach(function(t) {
       gListManager.enableUpdate(t.tableName);
     });
+
+    // We register two v4 tables but only enable one of them
+    // to verify that the disabled tables are not updated.
+    // See Bug 1302044.
     gListManager.enableUpdate(TEST_TABLE_DATA_V4.tableName);
+    gListManager.disableUpdate(TEST_TABLE_DATA_V4_DISABLED.tableName);
 
     // Expected results for v2.
     gExpectedUpdateRequest = TEST_TABLE_DATA_LIST[0].tableName + ";a:5:s:2-12\n" +
diff --git a/toolkit/components/url-classifier/tests/unit/test_threat_type_conversion.js b/toolkit/components/url-classifier/tests/unit/test_threat_type_conversion.js
index c20d3563ee08b43aa54a3a5b81f5f4408204e857..f7c51b9562a77d63a0b7090d1c7fb4e419c04fb5 100644
--- a/toolkit/components/url-classifier/tests/unit/test_threat_type_conversion.js
+++ b/toolkit/components/url-classifier/tests/unit/test_threat_type_conversion.js
@@ -22,7 +22,7 @@ function run_test() {
   // Test threat type to list name conversion.
   equal(urlUtils.convertThreatTypeToListNames(1), "goog-malware-proto");
   equal(urlUtils.convertThreatTypeToListNames(2), "googpub-phish-proto,test-phish-proto");
-  equal(urlUtils.convertThreatTypeToListNames(3), "goog-unwanted-proto");
+  equal(urlUtils.convertThreatTypeToListNames(3), "goog-unwanted-proto,test-unwanted-proto");
   equal(urlUtils.convertThreatTypeToListNames(5), "goog-phish-proto");
 
   try {
diff --git a/toolkit/content/tests/chrome/test_notificationbox.xul b/toolkit/content/tests/chrome/test_notificationbox.xul
index b1b47be16b159f3bc88ab05d0e582aca9e131910..a99d0824e8c49b0e2356b271b46c54868f836dff 100644
--- a/toolkit/content/tests/chrome/test_notificationbox.xul
+++ b/toolkit/content/tests/chrome/test_notificationbox.xul
@@ -48,22 +48,35 @@ function testtag_notificationbox(nb)
   runTimedTests(tests, -1, nb, null);
 }
 
-var notification_last_event, notification_last_event_item;
+var notification_last_events = [];
 function notification_eventCallback(event)
 {
-  notification_last_event = event;
-  notification_last_event_item = this;
+  notification_last_events.push({ actualEvent: event , item: this });
 }
 
-function testtag_notification_eventCallback(expectedEvent, ntf, testName)
+/**
+ * For any notifications that have the notification_eventCallback on
+ * them, we will have recorded instances of those callbacks firing
+ * and stored them. This checks to see that the expected event types
+ * are being fired in order, and targeting the right item.
+ *
+ * @param {Array<string>} expectedEvents
+ *        The list of event types, in order, that we expect to have been
+ *        fired on the item.
+ * @param {<xul:notification>} ntf
+ *        The notification we expect the callback to have been fired from.
+ * @param {string} testName
+ *        The name of the current test, for logging.
+ */
+function testtag_notification_eventCallback(expectedEvents, ntf, testName)
 {
-  SimpleTest.is(notification_last_event, expectedEvent,
-                testName + ": event name");
-  SimpleTest.is(notification_last_event_item, ntf,
-                testName + ": event item");
-
-  notification_last_event = null;
-  notification_last_event_item = null;
+  for (let i = 0; i < expectedEvents; ++i) {
+    let expected = expectedEvents[i];
+    let { actualEvent, item } = notification_last_events[i];
+    SimpleTest.is(actualEvent, expected, testName + ": event name");
+    SimpleTest.is(item, ntf, testName + ": event item");
+  }
+  notification_last_events = [];
 }
 
 var tests =
@@ -133,7 +146,36 @@ var tests =
       testtag_notificationbox_State(nb, "removeNotification with callback",
                                     null, 0);
 
-      testtag_notification_eventCallback("removed", ntf, "removeNotification()");
+      testtag_notification_eventCallback(["removed"], ntf, "removeNotification()");
+      return ntf;
+    }
+  },
+  {
+    test: function(nb, ntf) {
+      var ntf = nb.appendNotification("Notification", "note", "happy.png",
+                                      nb.PRIORITY_INFO_LOW,
+                                      testtag_notificationbox_buttons,
+                                      notification_eventCallback);
+      SimpleTest.is(ntf && ntf.localName == "notification", true, "append notification with callback");
+      return ntf;
+    },
+    result: function(nb, ntf) {
+      testtag_notificationbox_State(nb, "append with callback", ntf, 1);
+      return ntf;
+    }
+  },
+  {
+    test: function(rb, ntf) {
+      // Dismissing the notification instead of removing it should
+      // fire a dismissed "event" on the callback, followed by
+      // a removed "event".
+      ntf.dismiss();
+      return ntf;
+    },
+    result: function(nb, ntf) {
+      testtag_notificationbox_State(nb, "called dismiss()", null, 0);
+      testtag_notification_eventCallback(["dismissed", "removed"], ntf,
+                                         "dismiss()");
       return ntf;
     }
   },
diff --git a/toolkit/content/widgets/notification.xml b/toolkit/content/widgets/notification.xml
index c3e20f9474eac60fc8cfd12968e22324f77135c4..db8c8cd6fa910907cbdba422004e72eaad043051 100644
--- a/toolkit/content/widgets/notification.xml
+++ b/toolkit/content/widgets/notification.xml
@@ -395,7 +395,7 @@
                            class="messageCloseButton close-icon tabbable"
                            xbl:inherits="hidden=hideclose"
                            tooltiptext="&closeNotification.tooltip;"
-                           oncommand="document.getBindingParent(this).close();"/>
+                           oncommand="document.getBindingParent(this).dismiss();"/>
       </xul:hbox>
     </content>
     <resources>
@@ -430,6 +430,21 @@
         </getter>
       </property>
 
+      <!-- This method should only be called when the user has
+           manually closed the notification. If you want to
+           programmatically close the notification, you should
+           call close() instead. -->
+      <method name="dismiss">
+        <body>
+          <![CDATA[
+            if (this.eventCallback) {
+              this.eventCallback("dismissed");
+            }
+            this.close();
+          ]]>
+        </body>
+      </method>
+
       <method name="close">
         <body>
           <![CDATA[
diff --git a/toolkit/modules/FinderHighlighter.jsm b/toolkit/modules/FinderHighlighter.jsm
index 894b13cb5585598a11f639700551ffa01da49d26..6b40701ce4fc2f69401cbfcf241a8de3be81670d 100644
--- a/toolkit/modules/FinderHighlighter.jsm
+++ b/toolkit/modules/FinderHighlighter.jsm
@@ -78,7 +78,7 @@ const kModalStyles = {
     ["padding", "0 1px 2px 1px !important"],
     ["position", "absolute"]
   ],
-  maskRectBrightText: [ "background", "#000" ]
+  maskRectBrightText: [ ["background", "#000"] ]
 };
 const kModalOutlineAnim = {
   "keyframes": [
@@ -444,7 +444,7 @@ FinderHighlighter.prototype = {
   },
 
   /**
-   * Invalidates the list by clearing the map of highglighted ranges that we
+   * Invalidates the list by clearing the map of highlighted ranges that we
    * keep to build the mask for.
    */
   clear(window = null) {
@@ -460,7 +460,6 @@ FinderHighlighter.prototype = {
     let dict = this.getForWindow(window.top);
     if (dict.animation)
       dict.animation.finish();
-    dict.currentFoundRange = null;
     dict.dynamicRangesSet.clear();
     dict.frames.clear();
     dict.modalHighlightRectsMap.clear();
@@ -476,6 +475,7 @@ FinderHighlighter.prototype = {
     let window = this.finder._getWindow();
     let dict = this.getForWindow(window);
     this.clear(window);
+    dict.currentFoundRange = null;
 
     if (!dict.modalHighlightOutline)
       return;
@@ -747,7 +747,8 @@ FinderHighlighter.prototype = {
 
   /**
    * Read and store the rectangles that encompass the entire region of a range
-   * for use by the drawing function of the highlighter.
+   * for use by the drawing function of the highlighter and store them in the
+   * cache.
    *
    * @param  {nsIDOMRange} range            Range to fetch the rectangles from
    * @param  {Boolean}     [checkIfDynamic] Whether we should check if the range
@@ -785,6 +786,7 @@ FinderHighlighter.prototype = {
         rects.add(rect);
     }
 
+    // Only fetch the rect at this point, if not passed in as argument.
     dict = dict || this.getForWindow(window.top);
     dict.modalHighlightRectsMap.set(range, rects);
     if (checkIfDynamic && this._isInDynamicContainer(range))
@@ -960,6 +962,8 @@ FinderHighlighter.prototype = {
       const rectStyle = this._getStyleString(kModalStyles.maskRect,
         dict.brightText ? kModalStyles.maskRectBrightText : []);
       for (let [range, rects] of dict.modalHighlightRectsMap) {
+        if (this._checkOverlap(dict.currentFoundRange, range))
+          continue;
         if (dict.updateAllRanges)
           rects = this._updateRangeRects(range);
         for (let rect of rects) {
@@ -1202,6 +1206,8 @@ FinderHighlighter.prototype = {
    * @returns true if they intersect, false otherwise
    */
   _checkOverlap(selectionRange, findRange) {
+    if (!selectionRange || !findRange)
+      return false;
     // The ranges overlap if one of the following is true:
     // 1) At least one of the endpoints of the deleted selection
     //    is in the find selection
diff --git a/toolkit/modules/tests/browser/browser_FinderHighlighter.js b/toolkit/modules/tests/browser/browser_FinderHighlighter.js
index e51603cc5f7a494a18d38c7e65ae69cdf88319c5..1164f9d6b65c81dc1d35d1733482ce64116bd5ef 100644
--- a/toolkit/modules/tests/browser/browser_FinderHighlighter.js
+++ b/toolkit/modules/tests/browser/browser_FinderHighlighter.js
@@ -142,22 +142,22 @@ add_task(function* setup() {
 add_task(function* testModalResults() {
   let tests = new Map([
     ["Roland", {
-      rectCount: 2,
+      rectCount: 1,
       insertCalls: [2, 4],
       removeCalls: [1, 2]
     }],
     ["ro", {
-      rectCount: 41,
+      rectCount: 40,
       insertCalls: [1, 4],
       removeCalls: [1, 3]
     }],
     ["new", {
-      rectCount: 2,
+      rectCount: 1,
       insertCalls: [1, 4],
       removeCalls: [1, 3]
     }],
     ["o", {
-      rectCount: 492,
+      rectCount: 491,
       insertCalls: [1, 5],
       removeCalls: [1, 4]
     }]
@@ -192,7 +192,7 @@ add_task(function* testModalSwitching() {
 
     let word = "Roland";
     let expectedResult = {
-      rectCount: 2,
+      rectCount: 1,
       insertCalls: [2, 4],
       removeCalls: [1, 2]
     };
@@ -228,7 +228,7 @@ add_task(function* testDarkPageDetection() {
 
     let word = "Roland";
     let expectedResult = {
-      rectCount: 2,
+      rectCount: 1,
       insertCalls: [2, 4],
       removeCalls: [1, 2]
     };
@@ -248,7 +248,7 @@ add_task(function* testDarkPageDetection() {
 
     let word = "Roland";
     let expectedResult = {
-      rectCount: 2,
+      rectCount: 1,
       insertCalls: [2, 4],
       removeCalls: [1, 2]
     };
@@ -285,7 +285,7 @@ add_task(function* testHighlightAllToggle() {
 
     let word = "Roland";
     let expectedResult = {
-      rectCount: 2,
+      rectCount: 1,
       insertCalls: [2, 4],
       removeCalls: [1, 2]
     };
diff --git a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
index f2b6980f0842c0dfceb283eb4699dcd7dce21ae5..bc5d362ba9a21889819f8f2fcac5be599d6c384d 100644
--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -641,7 +641,8 @@ var AddonTestUtils = {
 
     let props = ["id", "version", "type", "internalName", "updateURL", "updateKey",
                  "optionsURL", "optionsType", "aboutURL", "iconURL", "icon64URL",
-                 "skinnable", "bootstrap", "unpack", "strictCompatibility", "multiprocessCompatible"];
+                 "skinnable", "bootstrap", "unpack", "strictCompatibility",
+                 "multiprocessCompatible", "hasEmbeddedWebExtension"];
     rdf += this._writeProps(data, props);
 
     rdf += this._writeLocaleStrings(data);
diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
index ffe5a2222c25a96f10a67de84cac9ce96f2a382f..6034751e7943ed8b4f13242fc9eb73890f2ac02a 100644
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -59,6 +59,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "isAddonPartOfE10SRollout",
                                   "resource://gre/modules/addons/E10SAddonsRollout.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LegacyExtensionsUtils",
+                                  "resource://gre/modules/LegacyExtensionsUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "Blocklist",
                                    "@mozilla.org/extensions/blocklist;1",
@@ -177,7 +179,7 @@ const TOOLKIT_ID                      = "toolkit@mozilla.org";
 
 const XPI_SIGNATURE_CHECK_PERIOD      = 24 * 60 * 60;
 
-XPCOMUtils.defineConstant(this, "DB_SCHEMA", 17);
+XPCOMUtils.defineConstant(this, "DB_SCHEMA", 18);
 
 const NOTIFICATION_TOOLBOXPROCESS_LOADED      = "ToolboxProcessLoaded";
 
@@ -803,6 +805,7 @@ function createAddonDetails(id, aAddon) {
     multiprocessCompatible: aAddon.multiprocessCompatible,
     runInSafeMode: aAddon.runInSafeMode,
     dependencies: aAddon.dependencies,
+    hasEmbeddedWebExtension: aAddon.hasEmbeddedWebExtension,
   };
 }
 
@@ -1152,6 +1155,7 @@ function loadManifestFromRDF(aUri, aStream) {
   if (addon.type == "extension") {
     addon.bootstrap = getRDFProperty(ds, root, "bootstrap") == "true";
     addon.multiprocessCompatible = getRDFProperty(ds, root, "multiprocessCompatible") == "true";
+    addon.hasEmbeddedWebExtension = getRDFProperty(ds, root, "hasEmbeddedWebExtension") == "true";
     if (addon.optionsType &&
         addon.optionsType != AddonManager.OPTIONS_TYPE_DIALOG &&
         addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE &&
@@ -1300,6 +1304,21 @@ function defineSyncGUID(aAddon) {
   });
 }
 
+// Generate a unique ID based on the path to this temporary add-on location.
+function generateTemporaryInstallID(aFile) {
+  const hasher = Cc["@mozilla.org/security/hash;1"]
+        .createInstance(Ci.nsICryptoHash);
+  hasher.init(hasher.SHA1);
+  const data = new TextEncoder().encode(aFile.path);
+  // Make it so this ID cannot be guessed.
+  const sess = TEMP_INSTALL_ID_GEN_SESSION;
+  hasher.update(sess, sess.length);
+  hasher.update(data, data.length);
+  let id = `${getHashStringForCrypto(hasher)}@temporary-addon`;
+  logger.info(`Generated temp id ${id} (${sess.join("")}) for ${aFile.path}`);
+  return id;
+}
+
 /**
  * Loads an AddonInternal object from an add-on extracted in a directory.
  *
@@ -1375,19 +1394,7 @@ var loadManifestFromDir = Task.async(function*(aDir, aInstallLocation) {
     addon = yield loadManifestFromWebManifest(uri);
     if (!addon.id) {
       if (aInstallLocation == TemporaryInstallLocation) {
-        // Generate a unique ID based on the directory path of
-        // this temporary add-on location.
-        const hasher = Cc["@mozilla.org/security/hash;1"]
-          .createInstance(Ci.nsICryptoHash);
-        hasher.init(hasher.SHA1);
-        const data = new TextEncoder().encode(aDir.path);
-        // Make it so this ID cannot be guessed.
-        const sess = TEMP_INSTALL_ID_GEN_SESSION;
-        hasher.update(sess, sess.length);
-        hasher.update(data, data.length);
-        addon.id = `${getHashStringForCrypto(hasher)}@temporary-addon`;
-        logger.info(
-          `Generated temp id ${addon.id} (${sess.join("")}) for ${aDir.path}`);
+        addon.id = generateTemporaryInstallID(aDir);
       } else {
         addon.id = aDir.leafName;
       }
@@ -1475,10 +1482,15 @@ var loadManifestFromZipReader = Task.async(function*(aZipReader, aInstallLocatio
 
   let {signedState, cert} = yield verifyZipSignedState(aZipReader.file, addon);
   addon.signedState = signedState;
-  if (isWebExtension && !addon.id && cert) {
-    addon.id = cert.commonName;
-    if (!gIDTest.test(addon.id)) {
-      throw new Error(`Webextension is signed with an invalid id (${addon.id})`);
+  if (isWebExtension && !addon.id) {
+    if (cert) {
+      addon.id = cert.commonName;
+      if (!gIDTest.test(addon.id)) {
+        throw new Error(`Webextension is signed with an invalid id (${addon.id})`);
+      }
+    }
+    if (!addon.id && aInstallLocation == TemporaryInstallLocation) {
+      addon.id = generateTemporaryInstallID(aZipReader.file);
     }
   }
   addon.appDisabled = !isUsableAddon(addon);
@@ -4819,8 +4831,7 @@ this.XPIProvider = {
    */
   callBootstrapMethod: function(aAddon, aFile, aMethod, aReason, aExtraParams) {
     if (!aAddon.id || !aAddon.version || !aAddon.type) {
-      logger.error(new Error("aAddon must include an id, version, and type"));
-      return;
+      throw new Error("aAddon must include an id, version, and type");
     }
 
     // Only run in safe mode if allowed to
@@ -4892,6 +4903,17 @@ this.XPIProvider = {
         }
       }
 
+      if (aAddon.hasEmbeddedWebExtension) {
+        if (aMethod == "startup") {
+          const webExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor(params);
+          params.webExtension = {
+            startup: () => webExtension.startup(),
+          };
+        } else if (aMethod == "shutdown") {
+          LegacyExtensionsUtils.getEmbeddedExtensionFor(params).shutdown();
+        }
+      }
+
       logger.debug("Calling bootstrap method " + aMethod + " on " + aAddon.id + " version " +
                    aAddon.version);
       try {
@@ -6937,6 +6959,7 @@ AddonInternal.prototype = {
    *   add-ons is not installed and enabled.
    */
   dependencies: Object.freeze([]),
+  hasEmbeddedWebExtension: false,
 
   get selectedLocale() {
     if (this._selectedLocale)
@@ -7254,6 +7277,10 @@ AddonWrapper.prototype = {
     return addonFor(this).seen;
   },
 
+  get hasEmbeddedWebExtension() {
+    return addonFor(this).hasEmbeddedWebExtension;
+  },
+
   markAsSeen: function() {
     addonFor(this).seen = true;
     XPIDatabase.saveChanges();
diff --git a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
index 7ffde0c87a561d5ccc862457bfd9cb33e87465d5..fb307835acb62c6dd0be12fd11043a615db35239 100644
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -87,7 +87,7 @@ const PROP_JSON_FIELDS = ["id", "syncGUID", "location", "version", "type",
                           "softDisabled", "foreignInstall", "hasBinaryComponents",
                           "strictCompatibility", "locales", "targetApplications",
                           "targetPlatforms", "multiprocessCompatible", "signedState",
-                          "seen", "dependencies"];
+                          "seen", "dependencies", "hasEmbeddedWebExtension"];
 
 // Properties that should be migrated where possible from an old database. These
 // shouldn't include properties that can be read directly from install.rdf files
@@ -2156,6 +2156,7 @@ this.XPIDatabaseReconcile = {
           multiprocessCompatible: currentAddon.multiprocessCompatible,
           runInSafeMode: canRunInSafeMode(currentAddon),
           dependencies: currentAddon.dependencies,
+          hasEmbeddedWebExtension: currentAddon.hasEmbeddedWebExtension,
         };
       }
 
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/BootstrapMonitor.jsm b/toolkit/mozapps/extensions/test/xpcshell/data/BootstrapMonitor.jsm
index dfc0eaf3207a19332429eba8a3b2bf680aee8f31..7c1e4aa9d6cc70aa478c2782803b01aecab7bc8a 100644
--- a/toolkit/mozapps/extensions/test/xpcshell/data/BootstrapMonitor.jsm
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/BootstrapMonitor.jsm
@@ -12,7 +12,9 @@ function notify(event, originalMethod, data, reason) {
     reason
   };
 
-  Services.obs.notifyObservers(null, "bootstrapmonitor-event", JSON.stringify(info));
+  let subject = {wrappedJSObject: {data}};
+
+  Services.obs.notifyObservers(subject, "bootstrapmonitor-event", JSON.stringify(info));
 
   // If the bootstrap scope already declares a method call it
   if (originalMethod)
diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
index 8e8b066e1a9b6af37d207b1282f1966c9ad7eb92..e1a2f3f6ca3f5e7fae2604bccd71e4b1ecf2788c 100644
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -236,6 +236,17 @@ this.BootstrapMonitor = {
     let id = info.data.id;
     let installPath = new FileUtils.File(info.data.installPath);
 
+    if (subject && subject.wrappedJSObject) {
+      // NOTE: in some of the new tests, we need to received the real objects instead of
+      // their JSON representations, but most of the current tests expect intallPath
+      // and resourceURI to have been converted to strings.
+      const {installPath, resourceURI} = info.data;
+      info.data = Object.assign({}, subject.wrappedJSObject.data, {
+        installPath,
+        resourceURI,
+      });
+    }
+
     // If this is the install event the add-ons shouldn't already be installed
     if (info.event == "install") {
       this.checkAddonNotInstalled(id);
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_embedded.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_embedded.js
new file mode 100644
index 0000000000000000000000000000000000000000..3b7198d4751e00b23c22dd449560c56aff54e5ba
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_embedded.js
@@ -0,0 +1,235 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+BootstrapMonitor.init();
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49");
+startupManager();
+
+// NOTE: the following import needs to be called after the `createAppInfo`
+// or it will fail Extension.jsm internally imports AddonManager.jsm and
+// AddonManager will raise a ReferenceError exception because it tried to
+// access an undefined `Services.appinfo` object.
+const { Management } = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+
+const {
+  EmbeddedExtensionManager,
+  LegacyExtensionsUtils,
+} = Components.utils.import("resource://gre/modules/LegacyExtensionsUtils.jsm");
+
+// Wait the startup of the embedded webextension.
+function promiseWebExtensionStartup() {
+  return new Promise(resolve => {
+    let listener = (event, extension) => {
+      Management.off("startup", listener);
+      resolve(extension);
+    };
+
+    Management.on("startup", listener);
+  });
+}
+
+function promiseWebExtensionShutdown() {
+  return new Promise(resolve => {
+    let listener = (event, extension) => {
+      Management.off("shutdown", listener);
+      resolve(extension);
+    };
+
+    Management.on("shutdown", listener);
+  });
+}
+
+const BOOTSTRAP = String.raw`
+  Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
+`;
+
+const EMBEDDED_WEBEXT_MANIFEST = JSON.stringify({
+  name: "embedded webextension addon",
+  manifest_version: 2,
+  version: "1.0",
+});
+
+/**
+ *  This test case checks that an addon with hasEmbeddedWebExtension set to true
+ *  in its install.rdf gets the expected `embeddedWebExtension` object in the
+ *  parameters of its bootstrap methods.
+ */
+add_task(function* run_embedded_webext_bootstrap() {
+  const ID = "embedded-webextension-addon2@tests.mozilla.org";
+
+  const xpiFile = createTempXPIFile({
+    id: ID,
+    name: "Test Add-on",
+    version: "1.0",
+    bootstrap: true,
+    hasEmbeddedWebExtension: true,
+    targetApplications: [{
+      id: "xpcshell@tests.mozilla.org",
+      minVersion: "1",
+      maxVersion: "1.9.2"
+    }]
+  }, {
+    "bootstrap.js": BOOTSTRAP,
+    "webextension/manifest.json": EMBEDDED_WEBEXT_MANIFEST,
+  });
+
+  yield AddonManager.installTemporaryAddon(xpiFile);
+
+  let addon = yield promiseAddonByID(ID);
+
+  notEqual(addon, null, "Got an addon object as expected");
+  equal(addon.version, "1.0", "Got the expected version");
+  equal(addon.hasEmbeddedWebExtension, true,
+        "Got the expected hasEmbeddedWebExtension value");
+
+  // Check that the addon has been installed and started.
+  BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+
+  let installInfo = BootstrapMonitor.installed.get(ID);
+  ok(!("webExtension" in installInfo.data),
+     "No webExtension property is expected in the install bootstrap method params");
+
+  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+  let startupInfo = BootstrapMonitor.started.get(ID);
+
+  ok(("webExtension" in startupInfo.data),
+     "Got an webExtension property in the startup bootstrap method params");
+
+  ok(("startup" in startupInfo.data.webExtension),
+     "Got the expected 'startup' property in the webExtension object");
+
+  const waitForWebExtensionStartup = promiseWebExtensionStartup();
+
+  const embeddedAPI = yield startupInfo.data.webExtension.startup();
+
+  // WebExtension startup should have been fully resolved.
+  yield waitForWebExtensionStartup;
+
+  Assert.deepEqual(
+    Object.keys(embeddedAPI.browser.runtime).sort(),
+    ["onConnect", "onMessage"],
+    `Got the expected 'runtime' in the 'browser' API object`
+  );
+
+  // Uninstall the addon and wait that the embedded webextension has been stopped and
+  // test the params of the shutdown and uninstall bootstrap method.
+  let waitForWebExtensionShutdown = promiseWebExtensionShutdown();
+  let waitUninstall = promiseAddonEvent("onUninstalled");
+  addon.uninstall();
+  yield waitForWebExtensionShutdown;
+  yield waitUninstall;
+
+  BootstrapMonitor.checkAddonNotStarted(ID, "1.0");
+
+  let shutdownInfo = BootstrapMonitor.stopped.get(ID);
+  ok(!("webExtension" in shutdownInfo.data),
+     "No webExtension property is expected in the shutdown bootstrap method params");
+
+  let uninstallInfo = BootstrapMonitor.uninstalled.get(ID);
+  ok(!("webExtension" in uninstallInfo.data),
+     "No webExtension property is expected in the uninstall bootstrap method params");
+});
+
+/**
+ *  This test case checks that an addon with hasEmbeddedWebExtension can be reloaded
+ *  without raising unexpected exceptions due to race conditions.
+ */
+add_task(function* reload_embedded_webext_bootstrap() {
+  const ID = "embedded-webextension-addon2@tests.mozilla.org";
+
+  // No embedded webextension should be currently around.
+  equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
+        "No embedded extension instance should be tracked here");
+
+  const xpiFile = createTempXPIFile({
+    id: ID,
+    name: "Test Add-on",
+    version: "1.0",
+    bootstrap: true,
+    hasEmbeddedWebExtension: true,
+    targetApplications: [{
+      id: "xpcshell@tests.mozilla.org",
+      minVersion: "1",
+      maxVersion: "1.9.2"
+    }]
+  }, {
+    "bootstrap.js": BOOTSTRAP,
+    "webextension/manifest.json": EMBEDDED_WEBEXT_MANIFEST,
+  });
+
+  yield AddonManager.installTemporaryAddon(xpiFile);
+
+  let addon = yield promiseAddonByID(ID);
+
+  notEqual(addon, null, "Got an addon object as expected");
+  equal(addon.version, "1.0", "Got the expected version");
+  equal(addon.isActive, true, "The Addon is active");
+  equal(addon.appDisabled, false, "The addon is not app disabled");
+  equal(addon.userDisabled, false, "The addon is not user disabled");
+
+  // Check that the addon has been installed and started.
+  BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+  // Only one embedded extension.
+  equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1,
+        "Got the expected number of tracked extension instances");
+
+  const embeddedWebExtension = EmbeddedExtensionManager.embeddedExtensionsByAddonId.get(ID);
+
+  let startupInfo = BootstrapMonitor.started.get(ID);
+  yield startupInfo.data.webExtension.startup();
+
+  const waitForAddonDisabled = promiseAddonEvent("onDisabled");
+  addon.userDisabled = true;
+  yield waitForAddonDisabled;
+
+  // No embedded webextension should be currently around.
+  equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
+        "No embedded extension instance should be tracked here");
+
+  const waitForAddonEnabled = promiseAddonEvent("onEnabled");
+  addon.userDisabled = false;
+  yield waitForAddonEnabled;
+
+  // Only one embedded extension.
+  equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1,
+        "Got the expected number of tracked extension instances");
+
+  const embeddedWebExtensionAfterEnabled = EmbeddedExtensionManager.embeddedExtensionsByAddonId.get(ID);
+  notEqual(embeddedWebExtensionAfterEnabled, embeddedWebExtension,
+           "Got a new EmbeddedExtension instance after the addon has been disabled and then enabled");
+
+  startupInfo = BootstrapMonitor.started.get(ID);
+  yield startupInfo.data.webExtension.startup();
+
+  const waitForReinstalled = promiseAddonEvent("onInstalled");
+  addon.reload();
+  yield waitForReinstalled;
+
+  // No leaked embedded extension after the previous reloads.
+  equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1,
+        "Got the expected number of tracked extension instances");
+
+  const embeddedWebExtensionAfterReload = EmbeddedExtensionManager.embeddedExtensionsByAddonId.get(ID);
+  notEqual(embeddedWebExtensionAfterReload, embeddedWebExtensionAfterEnabled,
+           "Got a new EmbeddedExtension instance after the addon has been reloaded");
+
+  startupInfo = BootstrapMonitor.started.get(ID);
+  yield startupInfo.data.webExtension.startup();
+
+  // Uninstall the test addon
+  let waitUninstalled = promiseAddonEvent("onUninstalled");
+  addon.uninstall();
+  yield waitUninstalled;
+
+  // No leaked embedded extension after uninstalling.
+  equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
+        "No embedded extension instance should be tracked after the addon uninstall");
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
index 96a103e0574fafd1b7afac422767fecc5cde988c..839c64f410baf851ea123b4ce5607febf4894f6c 100644
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
@@ -69,10 +69,7 @@ add_task(function* test_implicit_id_temp() {
 // We should be able to temporarily install an unsigned web extension
 // that does not have an ID in its manifest.
 add_task(function* test_unsigned_no_id_temp_install() {
-  if (!TEST_UNPACKED) {
-    do_print("This test does not apply when using packed extensions");
-    return;
-  }
+  AddonTestUtils.useRealCertChecks = true;
   const manifest = {
     name: "no ID",
     description: "extension without an ID",
@@ -97,16 +94,15 @@ add_task(function* test_unsigned_no_id_temp_install() {
   equal(secondAddon.id, addon.id, "Reinstalled add-on has the expected ID");
 
   secondAddon.uninstall();
+  Services.obs.notifyObservers(addonDir, "flush-cache-entry", null);
   addonDir.remove(true);
+  AddonTestUtils.useRealCertChecks = false;
 });
 
 // We should be able to install two extensions from manifests without IDs
 // at different locations and get two unique extensions.
 add_task(function* test_multiple_no_id_extensions() {
-  if (!TEST_UNPACKED) {
-    do_print("This test does not apply when using packed extensions");
-    return;
-  }
+  AddonTestUtils.useRealCertChecks = true;
   const manifest = {
     name: "no ID",
     description: "extension without an ID",
@@ -132,9 +128,12 @@ add_task(function* test_multiple_no_id_extensions() {
   equal(filtered.length, 2, "Two add-ons are installed with the same name");
 
   firstAddon.uninstall();
+  Services.obs.notifyObservers(firstAddonDir, "flush-cache-entry", null);
   firstAddonDir.remove(true);
   secondAddon.uninstall();
+  Services.obs.notifyObservers(secondAddonDir, "flush-cache-entry", null);
   secondAddonDir.remove(true);
+  AddonTestUtils.useRealCertChecks = false;
 });
 
 // Test that we can get the ID from browser_specific_settings
diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
index 02cd906b0b18949c693c5183f0d6028ed164b15d..29265a0a52f021ca4d6c4ff446612b1b5ec9c2ce 100644
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
@@ -315,6 +315,9 @@ tags = webextensions
 [test_webextension_install.js]
 skip-if = appname == "thunderbird"
 tags = webextensions
+[test_webextension_embedded.js]
+skip-if = appname == "thunderbird"
+tags = webextensions
 [test_bootstrap_globals.js]
 [test_bug1180901_2.js]
 skip-if = os != "win"
diff --git a/toolkit/xre/nsEmbedFunctions.cpp b/toolkit/xre/nsEmbedFunctions.cpp
index fdb6bbd07a90312ef4a85066ac280966e10242cc..19fb12860cf50c991eebee1049f1e9b31a459936 100644
--- a/toolkit/xre/nsEmbedFunctions.cpp
+++ b/toolkit/xre/nsEmbedFunctions.cpp
@@ -606,13 +606,44 @@ XRE_InitChildProcess(int aArgc,
       case GeckoProcessType_Content: {
           process = new ContentProcess(parentPID);
           // If passed in grab the application path for xpcom init
-          nsCString appDir;
+          bool foundAppdir = false;
+
+#if defined(XP_MACOSX) && defined(MOZ_CONTENT_SANDBOX)
+          // If passed in grab the profile path for sandboxing
+          bool foundProfile = false;
+#endif
+
           for (int idx = aArgc; idx > 0; idx--) {
             if (aArgv[idx] && !strcmp(aArgv[idx], "-appdir")) {
+              MOZ_ASSERT(!foundAppdir);
+              if (foundAppdir) {
+                  continue;
+              }
+              nsCString appDir;
               appDir.Assign(nsDependentCString(aArgv[idx+1]));
               static_cast<ContentProcess*>(process.get())->SetAppDir(appDir);
+              foundAppdir = true;
+            }
+
+#if defined(XP_MACOSX) && defined(MOZ_CONTENT_SANDBOX)
+            if (aArgv[idx] && !strcmp(aArgv[idx], "-profile")) {
+              MOZ_ASSERT(!foundProfile);
+              if (foundProfile) {
+                continue;
+              }
+              nsCString profile;
+              profile.Assign(nsDependentCString(aArgv[idx+1]));
+              static_cast<ContentProcess*>(process.get())->SetProfile(profile);
+              foundProfile = true;
+            }
+            if (foundProfile && foundAppdir) {
+              break;
+            }
+#else
+            if (foundAppdir) {
               break;
             }
+#endif /* XP_MACOSX && MOZ_CONTENT_SANDBOX */
           }
         }
         break;
diff --git a/xpcom/base/ErrorList.h b/xpcom/base/ErrorList.h
index f1f0a07103346005f19ec06176617c29aa03ead1..711de7ca7058a76edc5c5a1f8f3ee48465edd2dd 100644
--- a/xpcom/base/ErrorList.h
+++ b/xpcom/base/ErrorList.h
@@ -976,6 +976,18 @@
   ERROR(NS_ERROR_DOM_MEDIA_ABORT_ERR,           FAILURE(1)),
   ERROR(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR,     FAILURE(2)),
   ERROR(NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR,   FAILURE(3)),
+
+  /* HTMLMediaElement internal decoding error */
+  ERROR(NS_ERROR_DOM_MEDIA_DECODE_ERR,          FAILURE(4)),
+  ERROR(NS_ERROR_DOM_MEDIA_FATAL_ERR,           FAILURE(5)),
+  ERROR(NS_ERROR_DOM_MEDIA_METADATA_ERR,        FAILURE(6)),
+  ERROR(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,        FAILURE(7)),
+  ERROR(NS_ERROR_DOM_MEDIA_END_OF_STREAM,       FAILURE(8)),
+  ERROR(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA,    FAILURE(9)),
+  ERROR(NS_ERROR_DOM_MEDIA_CANCELED,            FAILURE(10)),
+  ERROR(NS_ERROR_DOM_MEDIA_MEDIASINK_ERR,       FAILURE(11)),
+  ERROR(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,         FAILURE(12)),
+  ERROR(NS_ERROR_DOM_MEDIA_CDM_ERR,             FAILURE(13)),
 #undef MODULE
 
   /* ======================================================================= */
diff --git a/xpcom/base/nsCycleCollector.cpp b/xpcom/base/nsCycleCollector.cpp
index 61a339ee2f3da6aa2d9a29bd003db743aafadb2b..4d3e6c6ec58a728d1b28d02e5e9e1b9e765ba977 100644
--- a/xpcom/base/nsCycleCollector.cpp
+++ b/xpcom/base/nsCycleCollector.cpp
@@ -1330,7 +1330,7 @@ public:
                SliceBudget& aBudget,
                nsICycleCollectorListener* aManualListener,
                bool aPreferShorterSlices = false);
-  void Shutdown();
+  void Shutdown(bool aDoCollect);
 
   bool IsIdle() const { return mIncrementalPhase == IdlePhase; }
 
@@ -3435,14 +3435,14 @@ nsCycleCollector::RegisterJSRuntime(CycleCollectedJSRuntime* aJSRuntime)
   MOZ_RELEASE_ASSERT(!mJSRuntime, "Multiple registrations of JS runtime in cycle collector");
   mJSRuntime = aJSRuntime;
 
+  if (!NS_IsMainThread()) {
+    return;
+  }
+
   // We can't register as a reporter in nsCycleCollector() because that runs
   // before the memory reporter manager is initialized.  So we do it here
   // instead.
-  static bool registered = false;
-  if (!registered) {
-    RegisterWeakMemoryReporter(this);
-    registered = true;
-  }
+  RegisterWeakMemoryReporter(this);
 }
 
 void
@@ -3876,17 +3876,14 @@ nsCycleCollector::SuspectedCount()
 }
 
 void
-nsCycleCollector::Shutdown()
+nsCycleCollector::Shutdown(bool aDoCollect)
 {
   CheckThreadSafety();
 
   // Always delete snow white objects.
   FreeSnowWhite(true);
 
-#ifndef NS_FREE_PERMANENT_DATA
-  if (PR_GetEnv("MOZ_CC_RUN_DURING_SHUTDOWN"))
-#endif
-  {
+  if (aDoCollect) {
     ShutdownCollect();
   }
 }
@@ -4196,7 +4193,7 @@ nsCycleCollector_finishAnyCurrentCollection()
 }
 
 void
-nsCycleCollector_shutdown()
+nsCycleCollector_shutdown(bool aDoCollect)
 {
   CollectorData* data = sCollectorData.get();
 
@@ -4205,7 +4202,7 @@ nsCycleCollector_shutdown()
     PROFILER_LABEL("nsCycleCollector", "shutdown",
                    js::ProfileEntry::Category::CC);
 
-    data->mCollector->Shutdown();
+    data->mCollector->Shutdown(aDoCollect);
     data->mCollector = nullptr;
     if (data->mRuntime) {
       // Run any remaining tasks that may have been enqueued via
diff --git a/xpcom/base/nsCycleCollector.h b/xpcom/base/nsCycleCollector.h
index 3988702e48d6ed6a7d401dbc52482143634209b3..75b65704b9315e550b1262adea159d0b2c635bfd 100644
--- a/xpcom/base/nsCycleCollector.h
+++ b/xpcom/base/nsCycleCollector.h
@@ -51,7 +51,10 @@ void nsCycleCollector_collectSlice(js::SliceBudget& budget,
                                    bool aPreferShorterSlices = false);
 
 uint32_t nsCycleCollector_suspectedCount();
-void nsCycleCollector_shutdown();
+
+// If aDoCollect is true, then run the GC and CC a few times before
+// shutting down the CC completely.
+void nsCycleCollector_shutdown(bool aDoCollect = true);
 
 // Helpers for interacting with JS
 void nsCycleCollector_registerJSRuntime(mozilla::CycleCollectedJSRuntime* aRt);
diff --git a/xpcom/build/XPCOMInit.cpp b/xpcom/build/XPCOMInit.cpp
index 1a71d0fcdbde24d15d3983aef72381b89ae47f8a..3988ce7e8241fc3bd3642ba1a9feb4dc950718f0 100644
--- a/xpcom/build/XPCOMInit.cpp
+++ b/xpcom/build/XPCOMInit.cpp
@@ -984,7 +984,13 @@ ShutdownXPCOM(nsIServiceManager* aServMgr)
     moduleLoaders = nullptr;
   }
 
-  nsCycleCollector_shutdown();
+  bool shutdownCollect;
+#ifdef NS_FREE_PERMANENT_DATA
+  shutdownCollect = true;
+#else
+  shutdownCollect = !!PR_GetEnv("MOZ_CC_RUN_DURING_SHUTDOWN");
+#endif
+  nsCycleCollector_shutdown(shutdownCollect);
 
   PROFILER_MARKER("Shutdown xpcom");
   // If we are doing any shutdown checks, poison writes.