Merge autoland to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 01 Sep 2016 12:11:51 -0400
changeset 312263 4dd14de3fa420bf5d750ca7c969ace2a9a9931ac
parent 312262 fc4a79cf9ac9681bfbeafb60f0056153b780e99e (current diff)
parent 312259 0d714827d06cd931283ce1863994ee3d0ae2763e (diff)
child 312264 a1c000291e340ca1ab6ef14b3d4385b77516fffd
child 312268 3ba5426a03b495b6417fffb872d42874edb80855
child 312279 e6a877db45f0d686adcd8923f3b18e9ec4d344bf
child 312297 292a3b62983fd33329bca5a27534db2299dcbc4c
push id20438
push userryanvm@gmail.com
push dateThu, 01 Sep 2016 16:13:32 +0000
treeherderfx-team@4dd14de3fa42 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone51.0a1
Merge autoland to m-c. a=merge
dom/base/nsContentUtils.cpp
dom/base/nsContentUtils.h
dom/ipc/TabParent.cpp
dom/media/test/external/external_media_tests/urls/youtube/long2-720.ini
dom/media/test/external/external_media_tests/urls/youtube/long3-crashes-720.ini
dom/media/test/external/external_media_tests/urls/youtube/long4-crashes-900.ini
dom/media/test/external/external_media_tests/urls/youtube/massive-6000.ini
dom/media/test/external/external_media_tests/urls/youtube/medium2-60.ini
dom/media/test/external/external_media_tests/urls/youtube/medium3-120.ini
dom/media/test/external/external_media_tests/urls/youtube/short0-10.ini
dom/media/test/external/external_media_tests/urls/youtube/short1-15.ini
dom/media/test/external/external_media_tests/urls/youtube/short2-15.ini
dom/media/test/external/external_media_tests/urls/youtube/short3-crashes-15.ini
mobile/android/components/AndroidActivitiesGlue.js
mobile/android/components/MobileComponents.manifest
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WebActivityMapper.java
mobile/android/installer/package-manifest.in
old-configure.in
toolkit/components/telemetry/Histograms.json
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -690,18 +690,21 @@ nsContextMenu.prototype = {
       if (this.target instanceof Ci.nsIImageLoadingContent &&
           this.target.currentURI) {
         this.onImage = true;
 
         var request =
           this.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
         if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE))
           this.onLoadedImage = true;
-        if (request && (request.imageStatus & request.STATUS_LOAD_COMPLETE))
+        if (request &&
+            (request.imageStatus & request.STATUS_LOAD_COMPLETE) &&
+            !(request.imageStatus & request.STATUS_ERROR)) {
           this.onCompletedImage = true;
+        }
 
         this.mediaURL = this.target.currentURI.spec;
 
         var descURL = this.target.getAttribute("longdesc");
         if (descURL) {
           this.imageDescURL = makeURLAbsolute(ownerDoc.body.baseURI, descURL);
         }
       }
--- a/build/autoconf/android.m4
+++ b/build/autoconf/android.m4
@@ -216,18 +216,18 @@ if test -n "$MOZ_INSTALL_TRACKING"; then
     AC_SUBST(MOZ_INSTALL_TRACKING)
     MOZ_ANDROID_AAR(play-services-ads, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
     MOZ_ANDROID_AAR(play-services-basement, $ANDROID_GOOGLE_PLAY_SERVICES_VERSION, google, com/google/android/gms)
 fi
 
 ])
 
 dnl Configure an Android SDK.
-dnl Arg 1: target SDK version, like 22.
-dnl Arg 2: build tools version, like 22.0.1.
+dnl Arg 1: target SDK version, like 23.
+dnl Arg 2: list of build-tools versions, like "23.0.3 23.0.1".
 AC_DEFUN([MOZ_ANDROID_SDK],
 [
 
 MOZ_ARG_WITH_STRING(android-sdk,
 [  --with-android-sdk=DIR
                           location where the Android SDK can be found (like ~/.mozbuild/android-sdk-linux)],
     android_sdk_root=$withval)
 
@@ -249,22 +249,31 @@ case "$target" in
     android_target_sdk=$1
     AC_MSG_CHECKING([for Android SDK platform version $android_target_sdk])
     android_sdk=$android_sdk_root/platforms/android-$android_target_sdk
     if ! test -e "$android_sdk/source.properties" ; then
         AC_MSG_ERROR([You must download Android SDK platform version $android_target_sdk.  Try |mach bootstrap|.  (Looked for $android_sdk)])
     fi
     AC_MSG_RESULT([$android_sdk])
 
-    android_build_tools="$android_sdk_root"/build-tools/$2
-    AC_MSG_CHECKING([for Android build-tools version $2])
-    if test -d "$android_build_tools" -a -f "$android_build_tools/aapt"; then
-        AC_MSG_RESULT([$android_build_tools])
-    else
-        AC_MSG_ERROR([You must install the Android build-tools version $2.  Try |mach bootstrap|.  (Looked for $android_build_tools)])
+    AC_MSG_CHECKING([for Android build-tools])
+    android_build_tools_base="$android_sdk_root"/build-tools
+    android_build_tools_version=""
+    versions=($2)
+    for version in $versions; do
+        android_build_tools="$android_build_tools_base"/$version
+        if test -d "$android_build_tools" -a -f "$android_build_tools/aapt"; then
+            android_build_tools_version=$version
+            AC_MSG_RESULT([$android_build_tools])
+            break
+        fi
+    done
+    if test "$android_build_tools_version" == ""; then
+        version=$(echo $versions | cut -d" " -f1)
+        AC_MSG_ERROR([You must install the Android build-tools version $version.  Try |mach bootstrap|.  (Looked for "$android_build_tools_base"/$version)])
     fi
 
     MOZ_PATH_PROG(ZIPALIGN, zipalign, :, [$android_build_tools])
     MOZ_PATH_PROG(DX, dx, :, [$android_build_tools])
     MOZ_PATH_PROG(AAPT, aapt, :, [$android_build_tools])
     MOZ_PATH_PROG(AIDL, aidl, :, [$android_build_tools])
     if test -z "$ZIPALIGN" -o "$ZIPALIGN" = ":"; then
       AC_MSG_ERROR([The program zipalign was not found.  Try |mach bootstrap|.])
@@ -304,17 +313,17 @@ case "$target" in
     if test -z "$EMULATOR" -o "$EMULATOR" = ":"; then
       AC_MSG_ERROR([The program emulator was not found.  Try |mach bootstrap|.])
     fi
 
     ANDROID_TARGET_SDK="${android_target_sdk}"
     ANDROID_SDK="${android_sdk}"
     ANDROID_SDK_ROOT="${android_sdk_root}"
     ANDROID_TOOLS="${android_tools}"
-    ANDROID_BUILD_TOOLS_VERSION="$2"
+    ANDROID_BUILD_TOOLS_VERSION="$android_build_tools_version"
     AC_DEFINE_UNQUOTED(ANDROID_TARGET_SDK,$ANDROID_TARGET_SDK)
     AC_SUBST(ANDROID_TARGET_SDK)
     AC_SUBST(ANDROID_SDK_ROOT)
     AC_SUBST(ANDROID_SDK)
     AC_SUBST(ANDROID_TOOLS)
     AC_SUBST(ANDROID_BUILD_TOOLS_VERSION)
 
     MOZ_ANDROID_AAR(customtabs, $ANDROID_SUPPORT_LIBRARY_VERSION, android, com/android/support)
--- a/devtools/client/shared/components/frame.js
+++ b/devtools/client/shared/components/frame.js
@@ -171,17 +171,18 @@ module.exports = createClass({
       let functionDisplayName = frame.functionDisplayName;
       if (!functionDisplayName && showAnonymousFunctionName) {
         functionDisplayName = webl10n.getStr("stacktrace.anonymousFunction");
       }
 
       if (functionDisplayName) {
         elements.push(
           dom.span({ className: "frame-link-function-display-name" },
-            functionDisplayName)
+            functionDisplayName),
+          " "
         );
       }
     }
 
     let displaySource = showFullSourceUrl ? long : short;
     if (isSourceMapped) {
       displaySource = getSourceMappedFile(displaySource);
     } else if (showEmptyPathAsHost && (displaySource === "" || displaySource === "/")) {
@@ -231,14 +232,14 @@ module.exports = createClass({
     } else {
       sourceEl = dom.span({
         className: "frame-link-source",
       }, sourceInnerEl);
     }
     elements.push(sourceEl);
 
     if (showHost && host) {
-      elements.push(dom.span({ className: "frame-link-host" }, host));
+      elements.push(" ", dom.span({ className: "frame-link-host" }, host));
     }
 
     return dom.span(attributes, ...elements);
   }
 });
--- a/devtools/client/shared/components/stack-trace.js
+++ b/devtools/client/shared/components/stack-trace.js
@@ -37,32 +37,32 @@ const StackTrace = createClass({
   },
 
   render() {
     let { stacktrace, onViewSourceInDebugger } = this.props;
 
     let frames = [];
     stacktrace.forEach(s => {
       if (s.asyncCause) {
-        frames.push(AsyncFrame({
+        frames.push("\t", AsyncFrame({
           asyncCause: s.asyncCause
-        }));
+        }), "\n");
       }
 
-      frames.push(Frame({
+      frames.push("\t", Frame({
         frame: {
           functionDisplayName: s.functionName,
           source: s.filename.split(" -> ").pop(),
           line: s.lineNumber,
           column: s.columnNumber,
         },
         showFunctionName: true,
         showAnonymousFunctionName: true,
         showFullSourceUrl: true,
         onClick: onViewSourceInDebugger
-      }));
+      }), "\n");
     });
 
     return dom.div({ className: "stack-trace" }, frames);
   }
 });
 
 module.exports = StackTrace;
--- a/devtools/client/shared/components/test/mochitest/test_stack-trace.html
+++ b/devtools/client/shared/components/test/mochitest/test_stack-trace.html
@@ -40,17 +40,19 @@ window.onload = function() {
     };
 
     let trace = ReactDOM.render(StackTrace(props), window.document.body);
     yield forceRender(trace);
 
     let traceEl = trace.getDOMNode();
     ok(traceEl, "Rendered StackTrace has an element");
 
-    let frameEls = traceEl.childNodes;
+    // Get the child nodes and filter out the text-only whitespace ones
+    let frameEls = Array.from(traceEl.childNodes)
+      .filter(n => n.className.includes("frame"));
     ok(frameEls, "Rendered StackTrace has frames");
     is(frameEls.length, 3, "StackTrace has 3 frames");
 
     // Check the top frame, function name should be anonymous
     checkFrameString({
       el: frameEls[0],
       functionName: "<anonymous>",
       source: "http://myfile.com/mahscripts.js",
@@ -71,13 +73,21 @@ window.onload = function() {
       functionName: "loadFunc",
       source: "http://myfile.com/loadee.js",
       file: "http://myfile.com/loadee.js",
       line: 10,
       column: null,
       shouldLink: true,
       tooltip: "View source in Debugger → http://myfile.com/loadee.js:10",
     });
+
+    // Check the tabs and newlines in the stack trace textContent
+    let traceText = traceEl.textContent;
+    let traceLines = traceText.split("\n");
+    ok(traceLines.length > 0, "There are newlines in the stack trace text");
+    is(traceLines.pop(), "", "There is a newline at the end of the stack trace text");
+    is(traceLines.length, 3, "The stack trace text has 3 lines");
+    ok(traceLines.every(l => l[0] == "\t"), "Every stack trace line starts with tab");
   });
 }
 </script>
 </body>
 </html>
--- a/devtools/client/webconsole/console-output.js
+++ b/devtools/client/webconsole/console-output.js
@@ -930,18 +930,16 @@ Messages.Simple.prototype = extend(Messa
       twisty.addEventListener("click", this._onClickCollapsible);
       this.element.appendChild(twisty);
       this.collapsible = true;
       this.element.setAttribute("collapsible", true);
     }
 
     this.element.appendChild(body);
 
-    this.element.appendChild(this.document.createTextNode("\n"));
-
     this.element.clipboardText = this.element.textContent;
 
     if (this.private) {
       this.element.setAttribute("private", true);
     }
 
     // TODO: handle object releasing in a more elegant way once all console
     // messages use the new API - bug 778766.
@@ -988,22 +986,26 @@ Messages.Simple.prototype = extend(Messa
 
     // do this before repeatNode is rendered - it has no effect afterwards
     this._repeatID.textContent += "|" + container.textContent;
 
     let repeatNode = this._renderRepeatNode();
     let location = this._renderLocation();
 
     if (repeatNode) {
+      bodyFlex.appendChild(this.document.createTextNode(" "));
       bodyFlex.appendChild(repeatNode);
     }
     if (location) {
+      bodyFlex.appendChild(this.document.createTextNode(" "));
       bodyFlex.appendChild(location);
     }
 
+    bodyFlex.appendChild(this.document.createTextNode("\n"));
+
     if (this.stack) {
       this._attachment = new Widgets.Stacktrace(this, this.stack).render().element;
     }
 
     if (this._attachment) {
       bodyWrapper.appendChild(this._attachment);
     }
 
--- a/devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js
+++ b/devtools/client/webconsole/test/browser_console_copy_entire_message_context_menu.js
@@ -1,69 +1,97 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+/* globals goDoCommand */
+
 "use strict";
 
 // Test copying of the entire console message when right-clicked
 // with no other text selected. See Bug 1100562.
 
-function test() {
+add_task(function* () {
   let hud;
   let outputNode;
   let contextMenu;
 
-  const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
-                   "test/test-console.html";
-
-  Task.spawn(runner).then(finishTest);
+  const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test/test-console.html";
 
-  function* runner() {
-    const {tab} = yield loadTab(TEST_URI);
-    hud = yield openConsole(tab);
-    outputNode = hud.outputNode;
-    contextMenu = hud.iframeWindow.document.getElementById("output-contextmenu");
+  const { tab, browser } = yield loadTab(TEST_URI);
+  hud = yield openConsole(tab);
+  outputNode = hud.outputNode;
+  contextMenu = hud.iframeWindow.document.getElementById("output-contextmenu");
+
+  registerCleanupFunction(() => {
+    hud = outputNode = contextMenu = null;
+  });
 
-    registerCleanupFunction(() => {
-      hud = outputNode = contextMenu = null;
-    });
+  hud.jsterm.clearOutput();
 
-    hud.jsterm.clearOutput();
-    content.console.log("bug 1100562");
+  yield ContentTask.spawn(browser, {}, function* () {
+    let button = content.document.getElementById("testTrace");
+    button.click();
+  });
 
-    let [results] = yield waitForMessages({
-      webconsole: hud,
-      messages: [{
+  let results = yield waitForMessages({
+    webconsole: hud,
+    messages: [
+      {
         text: "bug 1100562",
         category: CATEGORY_WEBDEV,
         severity: SEVERITY_LOG,
-      }]
-    });
+        lines: 1,
+      },
+      {
+        name: "console.trace output",
+        consoleTrace: true,
+        lines: 3,
+      },
+    ]
+  });
 
-    outputNode.focus();
-    let message = [...results.matched][0];
+  outputNode.focus();
 
-    yield waitForContextMenu(contextMenu, message, copyFromPopup,
-                             testContextMenuCopy);
+  for (let result of results) {
+    let message = [...result.matched][0];
 
-    function copyFromPopup() {
+    yield waitForContextMenu(contextMenu, message, () => {
       let copyItem = contextMenu.querySelector("#cMenu_copy");
       copyItem.doCommand();
 
       let controller = top.document.commandDispatcher
                                    .getControllerForCommand("cmd_copy");
       is(controller.isCommandEnabled("cmd_copy"), true, "cmd_copy is enabled");
-    }
+    });
+
+    let clipboardText;
+
+    yield waitForClipboardPromise(
+      () => goDoCommand("cmd_copy"),
+      (str) => {
+        clipboardText = str;
+        return message.textContent == clipboardText;
+      }
+    );
+
+    ok(clipboardText, "Clipboard text was found and saved");
 
-    function testContextMenuCopy() {
-      waitForClipboard((str) => {
-        return message.textContent.trim() == str.trim();
-      }, () => {
-        goDoCommand("cmd_copy");
-      }, () => {}, () => {}
-      );
+    let lines = clipboardText.split("\n");
+    ok(lines.length > 0, "There is at least one newline in the message");
+    is(lines.pop(), "", "There is a newline at the end");
+    is(lines.length, result.lines, `There are ${result.lines} lines in the message`);
+
+    // Test the first line for "timestamp message repeat file:line"
+    let firstLine = lines.shift();
+    ok(/^[\d:.]+ .+ \d+ .+:\d+$/.test(firstLine),
+      "The message's first line has the right format");
+
+    // Test the remaining lines (stack trace) for "TABfunctionName sourceURL:line:col"
+    for (let line of lines) {
+      ok(/^\t.+ .+:\d+:\d+$/.test(line), "The stack trace line has the right format");
     }
+  }
 
-    yield closeConsole(tab);
-  }
-}
+  yield closeConsole(tab);
+  yield finishTest();
+});
--- a/devtools/client/webconsole/test/test-console.html
+++ b/devtools/client/webconsole/test/test-console.html
@@ -8,20 +8,27 @@
       };
 
       function test() {
         var str = "Dolske Digs Bacon, Now and Forevermore."
         for (var i=0; i < 5; i++) {
           console.log(str);
         }
       }
+
+      function testTrace() {
+        console.log("bug 1100562");
+        console.trace();
+      }
+
       console.info("INLINE SCRIPT:");
       test();
       console.warn("I'm warning you, he will eat up all yr bacon.");
       console.error("Error Message");
     </script>
   </head>
   <body>
     <h1 id="header">Heads Up Display Demo</h1>
     <button onclick="test();">Log stuff about Dolske</button>
+    <button id="testTrace" onclick="testTrace();">Log stuff with stacktrace</button>
     <div id="myDiv"></div>
   </body>
 </html>
--- a/dom/animation/AnimationEffectReadOnly.cpp
+++ b/dom/animation/AnimationEffectReadOnly.cpp
@@ -138,39 +138,45 @@ AnimationEffectReadOnly::GetComputedTimi
   }
   const TimeDuration& localTime = aLocalTime.Value();
 
   // Calculate the time within the active interval.
   // https://w3c.github.io/web-animations/#active-time
   StickyTimeDuration activeTime;
 
   StickyTimeDuration beforeActiveBoundary =
-    std::min(StickyTimeDuration(aTiming.mDelay), result.mEndTime);
+    std::max(std::min(StickyTimeDuration(aTiming.mDelay), result.mEndTime),
+             zeroDuration);
+
   StickyTimeDuration activeAfterBoundary =
-    std::min(StickyTimeDuration(aTiming.mDelay + result.mActiveDuration),
-             result.mEndTime);
+    std::max(std::min(StickyTimeDuration(aTiming.mDelay +
+                                         result.mActiveDuration),
+                      result.mEndTime),
+             zeroDuration);
 
   if (localTime > activeAfterBoundary ||
       (aPlaybackRate >= 0 && localTime == activeAfterBoundary)) {
     result.mPhase = ComputedTiming::AnimationPhase::After;
     if (!result.FillsForwards()) {
       // The animation isn't active or filling at this time.
       return result;
     }
-    activeTime = std::max(std::min(result.mActiveDuration,
-                                   result.mActiveDuration + aTiming.mEndDelay),
-                          zeroDuration);
+    activeTime =
+      std::max(std::min(StickyTimeDuration(localTime - aTiming.mDelay),
+                        result.mActiveDuration),
+               zeroDuration);
   } else if (localTime < beforeActiveBoundary ||
              (aPlaybackRate < 0 && localTime == beforeActiveBoundary)) {
     result.mPhase = ComputedTiming::AnimationPhase::Before;
     if (!result.FillsBackwards()) {
       // The animation isn't active or filling at this time.
       return result;
     }
-    // activeTime is zero
+    activeTime = std::max(StickyTimeDuration(localTime - aTiming.mDelay),
+                          zeroDuration);
   } else {
     MOZ_ASSERT(result.mActiveDuration != zeroDuration,
                "How can we be in the middle of a zero-duration interval?");
     result.mPhase = ComputedTiming::AnimationPhase::Active;
     activeTime = localTime - aTiming.mDelay;
   }
 
   // Convert active time to a multiple of iterations.
--- a/dom/animation/TimingParams.h
+++ b/dom/animation/TimingParams.h
@@ -109,17 +109,18 @@ struct TimingParams
       return zeroDuration;
     }
 
     return mDuration->MultDouble(mIterations);
   }
 
   StickyTimeDuration EndTime() const
   {
-    return mDelay + ActiveDuration() + mEndDelay;
+    return std::max(mDelay + ActiveDuration() + mEndDelay,
+                    StickyTimeDuration());
   }
 
   bool operator==(const TimingParams& aOther) const;
   bool operator!=(const TimingParams& aOther) const
   {
     return !(*this == aOther);
   }
 };
--- a/dom/animation/test/css-animations/file_animation-computed-timing.html
+++ b/dom/animation/test/css-animations/file_animation-computed-timing.html
@@ -187,17 +187,17 @@ test(function(t) {
                 'Initial value of endTime');
 }, 'endTime of an infinitely repeating zero-duration animation');
 
 test(function(t) {
   // Fill forwards so div.getAnimations()[0] won't return an
   // undefined value.
   var div = addDiv(t, {style: 'animation: moveAnimation 10s -100s forwards'});
   var effect = div.getAnimations()[0].effect;
-  assert_equals(effect.getComputedTiming().endTime, -90 * MS_PER_SEC,
+  assert_equals(effect.getComputedTiming().endTime, 0,
                 'Initial value of endTime');
 }, 'endTime of an animation that finishes before its startTime');
 
 
 // --------------------
 // activeDuration
 // = iteration duration * iteration count
 // --------------------
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -274,61 +274,76 @@ public:
                                                 NS_LITERAL_STRING("error"),
                                                 false,
                                                 false);
   }
 };
 
 /**
  * This listener observes the first video frame to arrive with a non-empty size,
- * and calls HTMLMediaElement::ReceivedMediaStreamInitialSize() with that size.
+ * and calls HTMLMediaElement::UpdateInitialMediaSize() with that size.
  */
 class HTMLMediaElement::StreamSizeListener : public DirectMediaStreamTrackListener {
 public:
   explicit StreamSizeListener(HTMLMediaElement* aElement) :
     mElement(aElement),
     mInitialSizeFound(false)
   {}
+
   void Forget() { mElement = nullptr; }
 
   void ReceivedSize(gfx::IntSize aSize)
   {
+    MOZ_ASSERT(NS_IsMainThread());
+
     if (!mElement) {
       return;
     }
+
     RefPtr<HTMLMediaElement> deathGrip = mElement;
     mElement->UpdateInitialMediaSize(aSize);
   }
 
   void NotifyRealtimeTrackData(MediaStreamGraph* aGraph,
                                StreamTime aTrackOffset,
                                const MediaSegment& aMedia) override
   {
-    if (mInitialSizeFound || aMedia.GetType() != MediaSegment::VIDEO) {
+    if (mInitialSizeFound) {
       return;
     }
+
+    if (aMedia.GetType() != MediaSegment::VIDEO) {
+      MOZ_ASSERT(false, "Should only lock on to a video track");
+      return;
+    }
+
     const VideoSegment& video = static_cast<const VideoSegment&>(aMedia);
     for (VideoSegment::ConstChunkIterator c(video); !c.IsEnded(); c.Next()) {
       if (c->mFrame.GetIntrinsicSize() != gfx::IntSize(0,0)) {
         mInitialSizeFound = true;
         nsCOMPtr<nsIRunnable> event =
-          NewRunnableMethod<gfx::IntSize>(
-              this, &StreamSizeListener::ReceivedSize,
-              c->mFrame.GetIntrinsicSize());
-        aGraph->DispatchToMainThreadAfterStreamStateUpdate(event.forget());
+          NewRunnableMethod<gfx::IntSize>(this, &StreamSizeListener::ReceivedSize,
+                                          c->mFrame.GetIntrinsicSize());
+        // This is fine to dispatch straight to main thread (instead of via
+        // ...AfterStreamUpdate()) since it reflects state of the element,
+        // not the stream. Events reflecting stream or track state should be
+        // dispatched so their order is preserved.
+        NS_DispatchToMainThread(event.forget());
         return;
       }
     }
   }
 
 private:
   // These fields may only be accessed on the main thread
   HTMLMediaElement* mElement;
 
-  // These fields may only be accessed on the MSG thread
+  // These fields may only be accessed on the MSG's appending thread.
+  // (this is a direct listener so we get called by whoever is producing
+  // this track's data)
   bool mInitialSizeFound;
 };
 
 /**
  * There is a reference cycle involving this class: MediaLoadListener
  * holds a reference to the HTMLMediaElement, which holds a reference
  * to an nsIChannel, which holds a reference to this listener.
  * We break the reference cycle in OnStartRequest by clearing mElement.
@@ -2570,26 +2585,31 @@ HTMLMediaElement::CaptureStreamInternal(
     }
 
     // mAudioCaptured tells the user that the audio played by this media element
     // is being routed to the captureStreams *instead* of being played to
     // speakers.
     mAudioCaptured = true;
   }
 
+  if (mDecoder) {
+    out->mCapturingDecoder = true;
+    mDecoder->AddOutputStream(out->mStream->GetInputStream()->AsProcessedStream(),
+                              aFinishWhenEnded);
+  } else if (mSrcStream) {
+    out->mCapturingMediaStream = true;
+  }
+
   if (mReadyState == HAVE_NOTHING) {
-    // Do not expose the tracks directly before we have metadata.
+    // Do not expose the tracks until we have metadata.
     RefPtr<DOMMediaStream> result = out->mStream;
     return result.forget();
   }
 
   if (mDecoder) {
-    out->mCapturingDecoder = true;
-    mDecoder->AddOutputStream(out->mStream->GetInputStream()->AsProcessedStream(),
-                              aFinishWhenEnded);
     if (HasAudio()) {
       TrackID audioTrackId = mMediaInfo.mAudio.mTrackId;
       RefPtr<MediaStreamTrackSource> trackSource =
         getter->GetMediaStreamTrackSource(audioTrackId);
       RefPtr<MediaStreamTrack> track =
         out->mStream->CreateDOMTrack(audioTrackId, MediaSegment::AUDIO,
                                      trackSource);
       out->mStream->AddTrackInternal(track);
@@ -2605,32 +2625,16 @@ HTMLMediaElement::CaptureStreamInternal(
                                      trackSource);
       out->mStream->AddTrackInternal(track);
       LOG(LogLevel::Debug,
           ("Created video track %d for captured decoder", videoTrackId));
     }
   }
 
   if (mSrcStream) {
-    out->mCapturingMediaStream = true;
-    MediaStream* inputStream = out->mStream->GetInputStream();
-    if (!inputStream) {
-      NS_ERROR("No input stream");
-      RefPtr<DOMMediaStream> result = out->mStream;
-      return result.forget();
-    }
-
-    ProcessedMediaStream* processedInputStream =
-      inputStream->AsProcessedStream();
-    if (!processedInputStream) {
-      NS_ERROR("Input stream not a ProcessedMediaStream");
-      RefPtr<DOMMediaStream> result = out->mStream;
-      return result.forget();
-    }
-
     for (size_t i = 0; i < AudioTracks()->Length(); ++i) {
       AudioTrack* t = (*AudioTracks())[i];
       if (t->Enabled()) {
         AddCaptureMediaTrackToOutputStream(t, *out, false);
       }
     }
     if (IsVideo() && !out->mCapturingAudioOnly) {
       // Only add video tracks if we're a video element and the output stream
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -237,17 +237,16 @@ MediaDecoderStateMachine::MediaDecoderSt
   mIsVideoPrerolling(false),
   mAudioCaptured(false),
   INIT_WATCHABLE(mAudioCompleted, false),
   INIT_WATCHABLE(mVideoCompleted, false),
   mNotifyMetadataBeforeFirstFrame(false),
   mQuickBuffering(false),
   mMinimizePreroll(false),
   mDecodeThreadWaiting(false),
-  mDecodingFirstFrame(true),
   mSentLoadedMetadataEvent(false),
   mSentFirstFrameLoadedEvent(false),
   mSentPlaybackEndedEvent(false),
   mVideoDecodeSuspended(false),
   mVideoDecodeSuspendTimer(mTaskQueue),
   mOutputStreamManager(new OutputStreamManager()),
   mResource(aDecoder->GetResource()),
   mAudioOffloading(false),
@@ -483,25 +482,26 @@ bool MediaDecoderStateMachine::HaveEnoug
 bool
 MediaDecoderStateMachine::NeedToDecodeVideo()
 {
   MOZ_ASSERT(OnTaskQueue());
   SAMPLE_LOG("NeedToDecodeVideo() isDec=%d minPrl=%d enufVid=%d",
              IsVideoDecoding(), mMinimizePreroll, HaveEnoughDecodedVideo());
   return IsVideoDecoding() &&
          mState != DECODER_STATE_SEEKING &&
-         ((IsDecodingFirstFrame() && VideoQueue().GetSize() == 0) ||
+         ((!mSentFirstFrameLoadedEvent && VideoQueue().GetSize() == 0) ||
           (!mMinimizePreroll && !HaveEnoughDecodedVideo()));
 }
 
 bool
 MediaDecoderStateMachine::NeedToSkipToNextKeyframe()
 {
   MOZ_ASSERT(OnTaskQueue());
-  if (IsDecodingFirstFrame()) {
+  // Don't skip when we're still decoding first frames.
+  if (!mSentFirstFrameLoadedEvent) {
     return false;
   }
   MOZ_ASSERT(mState == DECODER_STATE_DECODING ||
              mState == DECODER_STATE_BUFFERING ||
              mState == DECODER_STATE_SEEKING);
 
   // Since GetClock() can only be called after starting MediaSink, we return
   // false quickly if it is not started because we won't fall behind playback
@@ -552,17 +552,17 @@ bool
 MediaDecoderStateMachine::NeedToDecodeAudio()
 {
   MOZ_ASSERT(OnTaskQueue());
   SAMPLE_LOG("NeedToDecodeAudio() isDec=%d minPrl=%d enufAud=%d",
              IsAudioDecoding(), mMinimizePreroll, HaveEnoughDecodedAudio());
 
   return IsAudioDecoding() &&
          mState != DECODER_STATE_SEEKING &&
-         ((IsDecodingFirstFrame() && AudioQueue().GetSize() == 0) ||
+         ((!mSentFirstFrameLoadedEvent && AudioQueue().GetSize() == 0) ||
           (!mMinimizePreroll && !HaveEnoughDecodedAudio()));
 }
 
 void
 MediaDecoderStateMachine::OnAudioDecoded(MediaData* aAudioSample)
 {
   MOZ_ASSERT(OnTaskQueue());
   MOZ_ASSERT(mState != DECODER_STATE_SEEKING);
@@ -728,17 +728,17 @@ MediaDecoderStateMachine::OnNotDecoded(M
     }
   }
 }
 
 bool
 MediaDecoderStateMachine::MaybeFinishDecodeFirstFrame()
 {
   MOZ_ASSERT(OnTaskQueue());
-  if (!IsDecodingFirstFrame() ||
+  if (mSentFirstFrameLoadedEvent ||
       (IsAudioDecoding() && AudioQueue().GetSize() == 0) ||
       (IsVideoDecoding() && VideoQueue().GetSize() == 0)) {
     return false;
   }
   FinishDecodeFirstFrame();
   if (!mQueuedSeek.Exists()) {
     return false;
   }
@@ -785,17 +785,17 @@ MediaDecoderStateMachine::OnVideoDecoded
       // arrive, increase the amount of audio we buffer to ensure that we
       // don't run out of audio. This is unnecessary for async readers,
       // since they decode audio and video on different threads so they
       // are unlikely to run out of decoded audio.
       if (mReader->IsAsync()) {
         return;
       }
       TimeDuration decodeTime = TimeStamp::Now() - aDecodeStartTime;
-      if (!IsDecodingFirstFrame() &&
+      if (mSentFirstFrameLoadedEvent &&
           THRESHOLD_FACTOR * DurationToUsecs(decodeTime) > mLowAudioThresholdUsecs &&
           !HasLowUndecodedData())
       {
         mLowAudioThresholdUsecs =
           std::min(THRESHOLD_FACTOR * DurationToUsecs(decodeTime), mAmpleAudioThresholdUsecs);
         mAmpleAudioThresholdUsecs = std::max(THRESHOLD_FACTOR * mLowAudioThresholdUsecs,
                                               mAmpleAudioThresholdUsecs);
         DECODER_LOG("Slow video decode, set mLowAudioThresholdUsecs=%lld mAmpleAudioThresholdUsecs=%lld",
@@ -1072,17 +1072,16 @@ MediaDecoderStateMachine::ExitState(Stat
 }
 
 void
 MediaDecoderStateMachine::EnterState(State aState)
 {
   MOZ_ASSERT(OnTaskQueue());
   switch (aState) {
     case DECODER_STATE_DECODING_METADATA:
-      mDecodingFirstFrame = true;
       ReadMetadata();
       break;
     case DECODER_STATE_DORMANT:
       DiscardSeekTaskIfExist();
       if (IsPlaying()) {
         StopPlayback();
       }
       Reset();
@@ -1255,29 +1254,21 @@ MediaDecoderStateMachine::Shutdown()
 }
 
 void
 MediaDecoderStateMachine::StartDecoding()
 {
   MOZ_ASSERT(OnTaskQueue());
   MOZ_ASSERT(mState == DECODER_STATE_DECODING);
 
-  if (mDecodingFirstFrame && mSentFirstFrameLoadedEvent) {
-    // We're resuming from dormant state, so we don't need to request
-    // the first samples in order to determine the media start time,
-    // we have the start time from last time we loaded.
-    // FinishDecodeFirstFrame will be launched upon completion of the seek when
-    // we have data ready to play.
-    MOZ_ASSERT(mQueuedSeek.Exists() && mSentFirstFrameLoadedEvent,
-               "Return from dormant must have queued seek");
-
-    if (mQueuedSeek.Exists()) {
-      InitiateSeek(Move(mQueuedSeek));
-      return;
-    }
+  // Handle the pending seek now if we've decoded first frames. Otherwise it
+  // will be handled after decoding first frames.
+  if (mSentFirstFrameLoadedEvent && mQueuedSeek.Exists()) {
+    InitiateSeek(Move(mQueuedSeek));
+    return;
   }
 
   if (CheckIfDecodeComplete()) {
     SetState(DECODER_STATE_COMPLETED);
     return;
   }
 
   mDecodeStartTime = TimeStamp::Now();
@@ -1506,18 +1497,22 @@ MediaDecoderStateMachine::Seek(SeekTarge
   if (aTarget.IsNextFrame() && !HasVideo()) {
     DECODER_WARN("Ignore a NextFrameSeekTask on a media file without video track.");
     return MediaDecoder::SeekPromise::CreateAndReject(/* aIgnored = */ true, __func__);
   }
 
   MOZ_ASSERT(mState > DECODER_STATE_DECODING_METADATA,
                "We should have got duration already");
 
-  if (mState < DECODER_STATE_DECODING ||
-      (IsDecodingFirstFrame() && !mReader->ForceZeroStartTime())) {
+  // Can't seek until the start time is known.
+  bool hasStartTime = mSentFirstFrameLoadedEvent || mReader->ForceZeroStartTime();
+  // Can't seek when state is WAIT_FOR_CDM or DORMANT.
+  bool stateAllowed = mState >= DECODER_STATE_DECODING;
+
+  if (!stateAllowed || !hasStartTime) {
     DECODER_LOG("Seek() Not Enough Data to continue at this stage, queuing seek");
     mQueuedSeek.RejectIfExists(__func__);
     mQueuedSeek.mTarget = aTarget;
     return mQueuedSeek.mPromise.Ensure(__func__);
   }
   mQueuedSeek.RejectIfExists(__func__);
 
   DECODER_LOG("Changed state to SEEKING (to %lld)", aTarget.GetTime().ToMicroseconds());
@@ -1901,18 +1896,18 @@ bool MediaDecoderStateMachine::HasLowUnd
 {
   MOZ_ASSERT(OnTaskQueue());
   return HasLowUndecodedData(mLowDataThresholdUsecs);
 }
 
 bool MediaDecoderStateMachine::HasLowUndecodedData(int64_t aUsecs)
 {
   MOZ_ASSERT(OnTaskQueue());
-  NS_ASSERTION(mState >= DECODER_STATE_DECODING && !IsDecodingFirstFrame(),
-               "Must have loaded first frame for mBuffered to be valid");
+  MOZ_ASSERT(mState >= DECODER_STATE_DECODING && mSentFirstFrameLoadedEvent,
+             "Must have loaded first frame for mBuffered to be valid");
 
   // If we don't have a duration, mBuffered is probably not going to have
   // a useful buffered range. Return false here so that we don't get stuck in
   // buffering mode for live streams.
   if (Duration().IsInfinite()) {
     return false;
   }
 
@@ -2069,22 +2064,16 @@ MediaDecoderStateMachine::EnqueueFirstFr
                              : MediaDecoderEventVisibility::Observable;
       self->mFirstFrameLoadedEvent.Notify(
         nsAutoPtr<MediaInfo>(new MediaInfo(self->mInfo)), visibility);
     },
     // Reject
     []() { MOZ_CRASH("Should not reach"); }));
 }
 
-bool
-MediaDecoderStateMachine::IsDecodingFirstFrame()
-{
-  return mState == DECODER_STATE_DECODING && mDecodingFirstFrame;
-}
-
 void
 MediaDecoderStateMachine::FinishDecodeFirstFrame()
 {
   MOZ_ASSERT(OnTaskQueue());
   DECODER_LOG("FinishDecodeFirstFrame");
 
   if (!mSentFirstFrameLoadedEvent) {
     mMediaSink->Redraw(mInfo.mVideo);
@@ -2101,19 +2090,18 @@ MediaDecoderStateMachine::FinishDecodeFi
 
   // Get potentially updated metadata
   mReader->ReadUpdatedMetadata(&mInfo);
 
   if (!mNotifyMetadataBeforeFirstFrame) {
     // If we didn't have duration and/or start time before, we should now.
     EnqueueLoadedMetadataEvent();
   }
+
   EnqueueFirstFrameLoadedEvent();
-
-  mDecodingFirstFrame = false;
 }
 
 void
 MediaDecoderStateMachine::SeekCompleted()
 {
   MOZ_ASSERT(OnTaskQueue());
   MOZ_ASSERT(mState == DECODER_STATE_SEEKING);
 
@@ -2168,19 +2156,21 @@ MediaDecoderStateMachine::SeekCompleted(
   // Reset the MediaDecoderReaderWrapper's callbask.
   DiscardSeekTaskIfExist();
 
   // NOTE: Discarding the mSeekTask must be done before here. The following code
   // might ask the MediaDecoderReaderWrapper to request media data, however, the
   // SeekTask::Discard() will ask MediaDecoderReaderWrapper to discard media
   // data requests.
 
-  if (mDecodingFirstFrame) {
-    // We were resuming from dormant, or initiated a seek early.
-    // We can fire loadeddata now.
+  // Notify FirstFrameLoaded now if we haven't since we've decoded some data
+  // for readyState to transition to HAVE_CURRENT_DATA and fire 'loadeddata'.
+  if (!mSentFirstFrameLoadedEvent) {
+    // Only MSE can start seeking before finishing decoding first frames.
+    MOZ_ASSERT(mReader->ForceZeroStartTime());
     FinishDecodeFirstFrame();
   }
 
   // Ensure timestamps are up to date.
   UpdatePlaybackPositionInternal(newCurrentTime);
 
   // Try to decode another frame to detect if we're at the end...
   DECODER_LOG("Seek completed, mCurrentPosition=%lld", mCurrentPosition.Ref());
@@ -2268,19 +2258,18 @@ nsresult MediaDecoderStateMachine::RunSt
   switch (mState) {
     case DECODER_STATE_SHUTDOWN:
     case DECODER_STATE_DORMANT:
     case DECODER_STATE_WAIT_FOR_CDM:
     case DECODER_STATE_DECODING_METADATA:
       return NS_OK;
 
     case DECODER_STATE_DECODING: {
-      if (IsDecodingFirstFrame()) {
-        // We haven't completed decoding our first frames, we can't start
-        // playback yet.
+      // Can't start playback until having decoded first frames.
+      if (!mSentFirstFrameLoadedEvent) {
         return NS_OK;
       }
       if (mPlayState != MediaDecoder::PLAY_STATE_PLAYING && IsPlaying())
       {
         // We're playing, but the element/decoder is in paused state. Stop
         // playing!
         StopPlayback();
       }
@@ -2487,17 +2476,17 @@ MediaDecoderStateMachine::UpdatePlayback
 }
 
 void MediaDecoderStateMachine::UpdateNextFrameStatus()
 {
   MOZ_ASSERT(OnTaskQueue());
 
   MediaDecoderOwner::NextFrameStatus status;
   const char* statusString;
-  if (mState <= DECODER_STATE_WAIT_FOR_CDM || IsDecodingFirstFrame()) {
+  if (mState < DECODER_STATE_DECODING || !mSentFirstFrameLoadedEvent) {
     status = MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE;
     statusString = "NEXT_FRAME_UNAVAILABLE";
   } else if (IsBuffering()) {
     status = MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_BUFFERING;
     statusString = "NEXT_FRAME_UNAVAILABLE_BUFFERING";
   } else if (IsSeeking()) {
     status = MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_SEEKING;
     statusString = "NEXT_FRAME_UNAVAILABLE_SEEKING";
@@ -2825,22 +2814,22 @@ MediaDecoderStateMachine::DumpDebugInfo(
   MOZ_ASSERT(NS_IsMainThread());
 
   // It is fine to capture a raw pointer here because MediaDecoder only call
   // this function before shutdown begins.
   nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction([this] () {
     mMediaSink->DumpDebugInfo();
     DUMP_LOG(
       "GetMediaTime=%lld GetClock=%lld mMediaSink=%p "
-      "mState=%s mPlayState=%d mDecodingFirstFrame=%d IsPlaying=%d "
+      "mState=%s mPlayState=%d mSentFirstFrameLoadedEvent=%d IsPlaying=%d "
       "mAudioStatus=%s mVideoStatus=%s mDecodedAudioEndTime=%lld mDecodedVideoEndTime=%lld "
       "mIsAudioPrerolling=%d mIsVideoPrerolling=%d "
       "mAudioCompleted=%d mVideoCompleted=%d",
       GetMediaTime(), mMediaSink->IsStarted() ? GetClock() : -1, mMediaSink.get(),
-      ToStateStr(), mPlayState.Ref(), mDecodingFirstFrame, IsPlaying(),
+      ToStateStr(), mPlayState.Ref(), mSentFirstFrameLoadedEvent, IsPlaying(),
       AudioRequestStatus(), VideoRequestStatus(), mDecodedAudioEndTime, mDecodedVideoEndTime,
       mIsAudioPrerolling, mIsVideoPrerolling, mAudioCompleted.Ref(), mVideoCompleted.Ref());
   });
 
   OwnerThread()->DispatchStateChange(r.forget());
 }
 
 void MediaDecoderStateMachine::AddOutputStream(ProcessedMediaStream* aStream,
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -562,18 +562,17 @@ protected:
   void OnMetadataRead(MetadataHolder* aMetadata);
   void OnMetadataNotRead(ReadMetadataFailureReason aReason);
 
   // Checks whether we're finished decoding first audio and/or video packets.
   // If so will trigger firing loadeddata event.
   // If there are any queued seek, will change state to DECODER_STATE_SEEKING
   // and return true.
   bool MaybeFinishDecodeFirstFrame();
-  // Return true if we are currently decoding the first frames.
-  bool IsDecodingFirstFrame();
+
   void FinishDecodeFirstFrame();
 
   // Completes the seek operation, moves onto the next appropriate state.
   void SeekCompleted();
 
   // Queries our state to see whether the decode has finished for all streams.
   bool CheckIfDecodeComplete();
 
@@ -880,28 +879,24 @@ private:
 
   nsAutoPtr<MetadataTags> mMetadataTags;
 
   mozilla::MediaMetadataManager mMetadataManager;
 
   // Track our request to update the buffered ranges
   MozPromiseRequestHolder<MediaDecoderReader::BufferedUpdatePromise> mBufferedUpdateRequest;
 
-  // True if we need to call FinishDecodeFirstFrame() upon frame decoding
-  // succeeding.
-  bool mDecodingFirstFrame;
-
   // True if we are back from DECODER_STATE_DORMANT state and
   // LoadedMetadataEvent was already sent.
   bool mSentLoadedMetadataEvent;
-  // True if we are back from DECODER_STATE_DORMANT state and
-  // FirstFrameLoadedEvent was already sent, then we can skip
-  // SetStartTime because the mStartTime already set before. Also we don't need
-  // to decode any audio/video since the MediaDecoder will trigger a seek
-  // operation soon.
+
+  // True if we've decoded first frames (thus having the start time) and
+  // notified the FirstFrameLoaded event. Note we can't initiate seek until the
+  // start time is known which happens when the first frames are decoded or we
+  // are playing an MSE stream (the start time is always assumed 0).
   bool mSentFirstFrameLoadedEvent;
 
   bool mSentPlaybackEndedEvent;
 
   // True if video decoding is suspended.
   bool mVideoDecodeSuspended;
 
   // Track enabling video decode suspension via timer
--- a/dom/media/MediaFormatReader.cpp
+++ b/dom/media/MediaFormatReader.cpp
@@ -57,19 +57,19 @@ TrackTypeToStr(TrackInfo::TrackType aTra
   }
 }
 
 MediaFormatReader::MediaFormatReader(AbstractMediaDecoder* aDecoder,
                                      MediaDataDemuxer* aDemuxer,
                                      VideoFrameContainer* aVideoFrameContainer,
                                      layers::LayersBackend aLayersBackend)
   : MediaDecoderReader(aDecoder)
-  , mAudio(this, MediaData::AUDIO_DATA, Preferences::GetUint("media.audio-decode-ahead", 2),
+  , mAudio(this, MediaData::AUDIO_DATA,
            Preferences::GetUint("media.audio-max-decode-error", 3))
-  , mVideo(this, MediaData::VIDEO_DATA, Preferences::GetUint("media.video-decode-ahead", 2),
+  , mVideo(this, MediaData::VIDEO_DATA,
            Preferences::GetUint("media.video-max-decode-error", 2))
   , mDemuxer(aDemuxer)
   , mDemuxerInitDone(false)
   , mLastReportedNumDecodedFrames(0)
   , mPreviousDecodedKeyframeTime_us(sNoPreviousDecodedKeyframe)
   , mLayersBackendType(aLayersBackend)
   , mInitDone(false)
   , mIsEncrypted(false)
@@ -557,17 +557,17 @@ MediaFormatReader::RequestVideoData(bool
   if (!mVideo.HasInternalSeekPending() &&
       ShouldSkip(aSkipToNextKeyframe, timeThreshold)) {
     RefPtr<MediaDataPromise> p = mVideo.EnsurePromise(__func__);
     SkipVideoDemuxToNextKeyFrame(timeThreshold);
     return p;
   }
 
   RefPtr<MediaDataPromise> p = mVideo.EnsurePromise(__func__);
-  NotifyDecodingRequested(TrackInfo::kVideoTrack);
+  ScheduleUpdate(TrackInfo::kVideoTrack);
 
   return p;
 }
 
 void
 MediaFormatReader::OnDemuxFailed(TrackType aTrack, DemuxerFailureReason aFailure)
 {
   MOZ_ASSERT(OnTaskQueue());
@@ -601,17 +601,16 @@ MediaFormatReader::OnDemuxFailed(TrackTy
       MOZ_ASSERT(false);
       break;
   }
 }
 
 void
 MediaFormatReader::DoDemuxVideo()
 {
-  // TODO Use DecodeAhead value rather than 1.
   mVideo.mDemuxRequest.Begin(mVideo.mTrackDemuxer->GetSamples(1)
                       ->Then(OwnerThread(), __func__, this,
                              &MediaFormatReader::OnVideoDemuxCompleted,
                              &MediaFormatReader::OnVideoDemuxFailed));
 }
 
 void
 MediaFormatReader::OnVideoDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples)
@@ -652,25 +651,24 @@ MediaFormatReader::RequestAudioData()
   }
 
   if (mShutdown) {
     NS_WARNING("RequestAudioData on shutdown MediaFormatReader!");
     return MediaDataPromise::CreateAndReject(CANCELED, __func__);
   }
 
   RefPtr<MediaDataPromise> p = mAudio.EnsurePromise(__func__);
-  NotifyDecodingRequested(TrackInfo::kAudioTrack);
+  ScheduleUpdate(TrackInfo::kAudioTrack);
 
   return p;
 }
 
 void
 MediaFormatReader::DoDemuxAudio()
 {
-  // TODO Use DecodeAhead value rather than 1.
   mAudio.mDemuxRequest.Begin(mAudio.mTrackDemuxer->GetSamples(1)
                       ->Then(OwnerThread(), __func__, this,
                              &MediaFormatReader::OnAudioDemuxCompleted,
                              &MediaFormatReader::OnAudioDemuxFailed));
 }
 
 void
 MediaFormatReader::OnAudioDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples)
@@ -701,17 +699,17 @@ MediaFormatReader::NotifyNewOutput(Track
 }
 
 void
 MediaFormatReader::NotifyInputExhausted(TrackType aTrack)
 {
   MOZ_ASSERT(OnTaskQueue());
   LOGV("Decoder has requested more %s data", TrackTypeToStr(aTrack));
   auto& decoder = GetDecoderData(aTrack);
-  decoder.mInputExhausted = true;
+  decoder.mDecodePending = false;
   ScheduleUpdate(aTrack);
 }
 
 void
 MediaFormatReader::NotifyDrainComplete(TrackType aTrack)
 {
   MOZ_ASSERT(OnTaskQueue());
   auto& decoder = GetDecoderData(aTrack);
@@ -750,43 +748,31 @@ void
 MediaFormatReader::NotifyEndOfStream(TrackType aTrack)
 {
   MOZ_ASSERT(OnTaskQueue());
   auto& decoder = GetDecoderData(aTrack);
   decoder.mDemuxEOS = true;
   ScheduleUpdate(aTrack);
 }
 
-void
-MediaFormatReader::NotifyDecodingRequested(TrackType aTrack)
-{
-  MOZ_ASSERT(OnTaskQueue());
-  auto& decoder = GetDecoderData(aTrack);
-  decoder.mDecodingRequested = true;
-  ScheduleUpdate(aTrack);
-}
-
 bool
 MediaFormatReader::NeedInput(DecoderData& aDecoder)
 {
-  // We try to keep a few more compressed samples input than decoded samples
-  // have been output, provided the state machine has requested we send it a
-  // decoded sample. To account for H.264 streams which may require a longer
-  // run of input than we input, decoders fire an "input exhausted" callback,
-  // which overrides our "few more samples" threshold.
+  // To account for H.264 streams which may require a longer
+  // run of input than we input, decoders fire an "input exhausted" callback.
+  // The decoder will not be fed a new raw sample until InputExhausted
+  // has been called.
   return
+    (aDecoder.HasPromise() || aDecoder.mTimeThreshold.isSome()) &&
     !aDecoder.HasPendingDrain() &&
     !aDecoder.HasFatalError() &&
-    aDecoder.mDecodingRequested &&
     !aDecoder.mDemuxRequest.Exists() &&
+    !aDecoder.mOutput.Length() &&
     !aDecoder.HasInternalSeekPending() &&
-    aDecoder.mOutput.Length() <= aDecoder.mDecodeAhead &&
-    (aDecoder.mInputExhausted || !aDecoder.mQueuedSamples.IsEmpty() ||
-     aDecoder.mTimeThreshold.isSome() ||
-     aDecoder.mNumSamplesInput - aDecoder.mNumSamplesOutput <= aDecoder.mDecodeAhead);
+    !aDecoder.mDecodePending;
 }
 
 void
 MediaFormatReader::ScheduleUpdate(TrackType aTrack)
 {
   MOZ_ASSERT(OnTaskQueue());
   if (mShutdown) {
     return;
@@ -927,16 +913,17 @@ MediaFormatReader::DecodeDemuxedSamples(
                                         MediaRawData* aSample)
 {
   MOZ_ASSERT(OnTaskQueue());
   auto& decoder = GetDecoderData(aTrack);
   if (NS_FAILED(decoder.mDecoder->Input(aSample))) {
       LOG("Unable to pass frame to decoder");
       return false;
   }
+  decoder.mDecodePending = true;
   return true;
 }
 
 void
 MediaFormatReader::HandleDemuxedSamples(TrackType aTrack,
                                         AbstractMediaDecoder::AutoNotifyDecoded& aA)
 {
   MOZ_ASSERT(OnTaskQueue());
@@ -1002,17 +989,17 @@ MediaFormatReader::HandleDemuxedSamples(
       decoder.mLastStreamSourceID = info->GetID();
       decoder.mNextStreamSourceID.reset();
       // Reset will clear our array of queued samples. So make a copy now.
       nsTArray<RefPtr<MediaRawData>> samples{decoder.mQueuedSamples};
       Reset(aTrack);
       decoder.ShutdownDecoder();
       if (sample->mKeyframe) {
         decoder.mQueuedSamples.AppendElements(Move(samples));
-        NotifyDecodingRequested(aTrack);
+        ScheduleUpdate(aTrack);
       } else {
         TimeInterval time =
           TimeInterval(TimeUnit::FromMicroseconds(sample->mTime),
                        TimeUnit::FromMicroseconds(sample->GetEndTime()));
         InternalSeekTarget seekTarget =
           decoder.mTimeThreshold.refOr(InternalSeekTarget(time, false));
         LOG("Stream change occurred on a non-keyframe. Seeking to:%lld",
             sample->mTime);
@@ -1040,19 +1027,16 @@ MediaFormatReader::HandleDemuxedSamples(
     decoder.mQueuedSamples.RemoveElementAt(0);
     if (mDemuxOnly) {
       // If demuxed-only case, ReturnOutput will resolve with one demuxed data.
       // Then we should stop doing the iteration.
       return;
     }
     samplesPending = true;
   }
-
-  // We have serviced the decoder's request for more data.
-  decoder.mInputExhausted = false;
 }
 
 void
 MediaFormatReader::InternalSeek(TrackType aTrack, const InternalSeekTarget& aTarget)
 {
   MOZ_ASSERT(OnTaskQueue());
   LOG("%s internal seek to %f",
       TrackTypeToStr(aTrack), aTarget.Time().ToSeconds());
@@ -1066,17 +1050,17 @@ MediaFormatReader::InternalSeek(TrackTyp
              ->Then(OwnerThread(), __func__,
                     [self, aTrack] (media::TimeUnit aTime) {
                       auto& decoder = self->GetDecoderData(aTrack);
                       decoder.mSeekRequest.Complete();
                       MOZ_ASSERT(decoder.mTimeThreshold,
                                  "Seek promise must be disconnected when timethreshold is reset");
                       decoder.mTimeThreshold.ref().mHasSeeked = true;
                       self->SetVideoDecodeThreshold();
-                      self->NotifyDecodingRequested(aTrack);
+                      self->ScheduleUpdate(aTrack);
                     },
                     [self, aTrack] (DemuxerFailureReason aResult) {
                       auto& decoder = self->GetDecoderData(aTrack);
                       decoder.mSeekRequest.Complete();
                       switch (aResult) {
                         case DemuxerFailureReason::WAITING_FOR_DATA:
                           self->NotifyWaitingForData(aTrack);
                           break;
@@ -1270,16 +1254,17 @@ MediaFormatReader::Update(TrackType aTra
 
   if (decoder.mNeedDraining) {
     DrainDecoder(aTrack);
     return;
   }
 
   if (decoder.mError &&
       decoder.mError.ref() == MediaDataDecoderError::DECODE_ERROR) {
+    decoder.mDecodePending = false;
     decoder.mError.reset();
     if (++decoder.mNumOfConsecutiveError > decoder.mMaxConsecutiveError) {
       NotifyError(aTrack);
       return;
     }
     LOG("%s decoded error count %d", TrackTypeToStr(aTrack),
                                      decoder.mNumOfConsecutiveError);
     media::TimeUnit nextKeyframe;
@@ -1287,21 +1272,21 @@ MediaFormatReader::Update(TrackType aTra
         NS_SUCCEEDED(decoder.mTrackDemuxer->GetNextRandomAccessPoint(&nextKeyframe))) {
       SkipVideoDemuxToNextKeyFrame(decoder.mLastSampleTime.refOr(TimeInterval()).Length());
       return;
     }
   }
 
   bool needInput = NeedInput(decoder);
 
-  LOGV("Update(%s) ni=%d no=%d ie=%d, in:%llu out:%llu qs=%u pending:%u waiting:%d ahead:%d sid:%u",
-       TrackTypeToStr(aTrack), needInput, needOutput, decoder.mInputExhausted,
+  LOGV("Update(%s) ni=%d no=%d ie=%d, in:%llu out:%llu qs=%u pending:%u waiting:%d promise:%d sid:%u",
+       TrackTypeToStr(aTrack), needInput, needOutput, decoder.mDecodePending,
        decoder.mNumSamplesInput, decoder.mNumSamplesOutput,
        uint32_t(size_t(decoder.mSizeOfQueue)), uint32_t(decoder.mOutput.Length()),
-       decoder.mWaitingForData, !decoder.HasPromise(), decoder.mLastStreamSourceID);
+       decoder.mWaitingForData, decoder.HasPromise(), decoder.mLastStreamSourceID);
 
   if (decoder.mWaitingForData &&
       (!decoder.mTimeThreshold || decoder.mTimeThreshold.ref().mWaiting)) {
     // Nothing more we can do at present.
     LOGV("Still waiting for data.");
     return;
   }
 
@@ -1572,17 +1557,17 @@ void
 MediaFormatReader::OnVideoSkipCompleted(uint32_t aSkipped)
 {
   MOZ_ASSERT(OnTaskQueue());
   LOG("Skipping succeeded, skipped %u frames", aSkipped);
   mSkipRequest.Complete();
 
   VideoSkipReset(aSkipped);
 
-  NotifyDecodingRequested(TrackInfo::kVideoTrack);
+  ScheduleUpdate(TrackInfo::kVideoTrack);
 }
 
 void
 MediaFormatReader::OnVideoSkipFailed(MediaTrackDemuxer::SkipFailureHolder aFailure)
 {
   MOZ_ASSERT(OnTaskQueue());
   LOG("Skipping failed, skipped %u frames", aFailure.mSkipped);
   mSkipRequest.Complete();
@@ -1590,17 +1575,17 @@ MediaFormatReader::OnVideoSkipFailed(Med
   switch (aFailure.mFailure) {
     case DemuxerFailureReason::END_OF_STREAM: MOZ_FALLTHROUGH;
     case DemuxerFailureReason::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);
       // We can't complete the skip operation, will just service a video frame
       // normally.
-      NotifyDecodingRequested(TrackInfo::kVideoTrack);
+      ScheduleUpdate(TrackInfo::kVideoTrack);
       break;
     case DemuxerFailureReason::CANCELED: MOZ_FALLTHROUGH;
     case DemuxerFailureReason::SHUTDOWN:
       if (mVideo.HasPromise()) {
         mVideo.RejectPromise(CANCELED, __func__);
       }
       break;
     default:
@@ -2008,22 +1993,21 @@ MediaFormatReader::GetMozDebugReaderData
     MonitorAutoLock mon(mVideo.mMonitor);
     videoName = mVideo.mDescription;
   }
 
   result += nsPrintfCString("audio decoder: %s\n", audioName);
   result += nsPrintfCString("audio frames decoded: %lld\n",
                             mAudio.mNumSamplesOutputTotal);
   if (HasAudio()) {
-    result += nsPrintfCString("audio state: ni=%d no=%d ie=%d demuxr:%d demuxq:%d decoder:%d tt:%f tths:%d in:%llu out:%llu qs=%u pending:%u waiting:%d sid:%u\n",
+    result += nsPrintfCString("audio state: ni=%d no=%d ie=%d demuxr:%d demuxq:%d tt:%f tths:%d in:%llu out:%llu qs=%u pending:%u waiting:%d sid:%u\n",
                               NeedInput(mAudio), mAudio.HasPromise(),
-                              mAudio.mInputExhausted,
+                              mAudio.mDecodePending,
                               mAudio.mDemuxRequest.Exists(),
                               int(mAudio.mQueuedSamples.Length()),
-                              mAudio.mDecodingRequested,
                               mAudio.mTimeThreshold
                               ? mAudio.mTimeThreshold.ref().Time().ToSeconds()
                               : -1.0,
                               mAudio.mTimeThreshold
                               ? mAudio.mTimeThreshold.ref().mHasSeeked
                               : -1,
                               mAudio.mNumSamplesInput, mAudio.mNumSamplesOutput,
                               unsigned(size_t(mAudio.mSizeOfQueue)),
@@ -2032,22 +2016,21 @@ MediaFormatReader::GetMozDebugReaderData
   }
   result += nsPrintfCString("video decoder: %s\n", videoName);
   result += nsPrintfCString("hardware video decoding: %s\n",
                             VideoIsHardwareAccelerated() ? "enabled" : "disabled");
   result += nsPrintfCString("video frames decoded: %lld (skipped:%lld)\n",
                             mVideo.mNumSamplesOutputTotal,
                             mVideo.mNumSamplesSkippedTotal);
   if (HasVideo()) {
-    result += nsPrintfCString("video state: ni=%d no=%d ie=%d demuxr:%d demuxq:%d decoder:%d tt:%f tths:%d in:%llu out:%llu qs=%u pending:%u waiting:%d sid:%u\n",
+    result += nsPrintfCString("video state: ni=%d no=%d ie=%d demuxr:%d demuxq:%d tt:%f tths:%d in:%llu out:%llu qs=%u pending:%u waiting:%d sid:%u\n",
                               NeedInput(mVideo), mVideo.HasPromise(),
-                              mVideo.mInputExhausted,
+                              mVideo.mDecodePending,
                               mVideo.mDemuxRequest.Exists(),
                               int(mVideo.mQueuedSamples.Length()),
-                              mVideo.mDecodingRequested,
                               mVideo.mTimeThreshold
                               ? mVideo.mTimeThreshold.ref().Time().ToSeconds()
                               : -1.0,
                               mVideo.mTimeThreshold
                               ? mVideo.mTimeThreshold.ref().mHasSeeked
                               : -1,
                               mVideo.mNumSamplesInput, mVideo.mNumSamplesOutput,
                               unsigned(size_t(mVideo.mSizeOfQueue)),
@@ -2075,14 +2058,14 @@ MediaFormatReader::SetBlankDecode(TrackT
 
   if (decoder.mIsBlankDecode == aIsBlankDecode) {
     return;
   }
 
   decoder.mIsBlankDecode = aIsBlankDecode;
   decoder.Flush();
   decoder.ShutdownDecoder();
-  NotifyDecodingRequested(TrackInfo::kVideoTrack); // Calls ScheduleUpdate().
+  ScheduleUpdate(TrackInfo::kVideoTrack);
 
   return;
 }
 
 } // namespace mozilla
--- a/dom/media/MediaFormatReader.h
+++ b/dom/media/MediaFormatReader.h
@@ -164,17 +164,16 @@ private:
   // Drain the current decoder.
   void DrainDecoder(TrackType aTrack);
   void NotifyNewOutput(TrackType aTrack, MediaData* aSample);
   void NotifyInputExhausted(TrackType aTrack);
   void NotifyDrainComplete(TrackType aTrack);
   void NotifyError(TrackType aTrack, MediaDataDecoderError aError = MediaDataDecoderError::FATAL_ERROR);
   void NotifyWaitingForData(TrackType aTrack);
   void NotifyEndOfStream(TrackType aTrack);
-  void NotifyDecodingRequested(TrackType aTrack);
 
   void ExtractCryptoInitData(nsTArray<uint8_t>& aInitData);
 
   // Initializes mLayersBackendType if possible.
   void InitLayersBackendType();
 
   // DecoderCallback proxies the MediaDataDecoderCallback calls to these
   // functions.
@@ -226,31 +225,28 @@ private:
   private:
     MediaFormatReader* mReader;
     TrackType mType;
   };
 
   struct DecoderData {
     DecoderData(MediaFormatReader* aOwner,
                 MediaData::Type aType,
-                uint32_t aDecodeAhead,
                 uint32_t aNumOfMaxError)
       : mOwner(aOwner)
       , mType(aType)
       , mMonitor("DecoderData")
       , mDescription("shutdown")
-      , mDecodeAhead(aDecodeAhead)
       , mUpdateScheduled(false)
       , mDemuxEOS(false)
       , mWaitingForData(false)
       , mReceivedNewData(false)
       , mDecoderInitialized(false)
-      , mDecodingRequested(false)
       , mOutputRequested(false)
-      , mInputExhausted(false)
+      , mDecodePending(false)
       , mNeedDraining(false)
       , mDraining(false)
       , mDrainComplete(false)
       , mNumOfConsecutiveError(0)
       , mMaxConsecutiveError(aNumOfMaxError)
       , mNumSamplesInput(0)
       , mNumSamplesOutput(0)
       , mNumSamplesOutputTotal(0)
@@ -283,17 +279,16 @@ private:
       if (mDecoder) {
         mDecoder->Shutdown();
       }
       mDescription = "shutdown";
       mDecoder = nullptr;
     }
 
     // Only accessed from reader's task queue.
-    uint32_t mDecodeAhead;
     bool mUpdateScheduled;
     bool mDemuxEOS;
     bool mWaitingForData;
     bool mReceivedNewData;
 
     // Pending seek.
     MozPromiseRequestHolder<MediaTrackDemuxer::SeekPromise> mSeekRequest;
 
@@ -307,21 +302,24 @@ private:
       return !mWaitingPromise.IsEmpty();
     }
 
     // MediaDataDecoder handler's variables.
     // Decoder initialization promise holder.
     MozPromiseRequestHolder<MediaDataDecoder::InitPromise> mInitPromise;
     // False when decoder is created. True when decoder Init() promise is resolved.
     bool mDecoderInitialized;
-    // Set when decoding can proceed. It is reset when a decoding promise is
-    // rejected or prior a seek operation.
-    bool mDecodingRequested;
     bool mOutputRequested;
-    bool mInputExhausted;
+    // Set to true once the MediaDataDecoder has been fed a compressed sample.
+    // No more sample will be passed to the decoder while true.
+    // mDecodePending is reset when:
+    // 1- The decoder returns a sample
+    // 2- The decoder calls InputExhausted
+    // 3- The decoder is Flushed or Reset.
+    bool mDecodePending;
     bool mNeedDraining;
     bool mDraining;
     bool mDrainComplete;
 
     bool HasPendingDrain() const
     {
       return mDraining || mDrainComplete;
     }
@@ -371,19 +369,18 @@ private:
     // Flush the decoder if present and reset decoding related data.
     // Decoding will be suspended until mInputRequested is set again.
     // Following a flush, the decoder is ready to accept any new data.
     void Flush()
     {
       if (mDecoder) {
         mDecoder->Flush();
       }
-      mDecodingRequested = false;
       mOutputRequested = false;
-      mInputExhausted = false;
+      mDecodePending = false;
       mOutput.Clear();
       mNumSamplesInput = 0;
       mNumSamplesOutput = 0;
       mSizeOfQueue = 0;
       mDraining = false;
       mDrainComplete = false;
     }
 
@@ -392,20 +389,19 @@ private:
     // Decoding will be suspended until mInputRequested is set again.
     // The track demuxer is *not* reset.
     void ResetState()
     {
       MOZ_ASSERT(mOwner->OnTaskQueue());
       mDemuxEOS = false;
       mWaitingForData = false;
       mQueuedSamples.Clear();
-      mDecodingRequested = false;
       mOutputRequested = false;
-      mInputExhausted = false;
       mNeedDraining = false;
+      mDecodePending = false;
       mDraining = false;
       mDrainComplete = false;
       mTimeThreshold.reset();
       mLastSampleTime.reset();
       mOutput.Clear();
       mNumSamplesInput = 0;
       mNumSamplesOutput = 0;
       mSizeOfQueue = 0;
@@ -436,19 +432,18 @@ private:
     bool mIsBlankDecode;
 
   };
 
   class DecoderDataWithPromise : public DecoderData {
   public:
     DecoderDataWithPromise(MediaFormatReader* aOwner,
                            MediaData::Type aType,
-                           uint32_t aDecodeAhead,
                            uint32_t aNumOfMaxError)
-      : DecoderData(aOwner, aType, aDecodeAhead, aNumOfMaxError)
+      : DecoderData(aOwner, aType, aNumOfMaxError)
       , mHasPromise(false)
 
     {}
 
     bool HasPromise() const override
     {
       return mHasPromise;
     }
@@ -467,17 +462,16 @@ private:
       mHasPromise = false;
     }
 
     void RejectPromise(MediaDecoderReader::NotDecodedReason aReason,
                        const char* aMethodName) override
     {
       MOZ_ASSERT(mOwner->OnTaskQueue());
       mPromise.Reject(aReason, aMethodName);
-      mDecodingRequested = false;
       mHasPromise = false;
     }
 
   private:
     MozPromiseHolder<MediaDataPromise> mPromise;
     Atomic<bool> mHasPromise;
   };
 
--- a/dom/media/MediaStreamGraph.cpp
+++ b/dom/media/MediaStreamGraph.cpp
@@ -1072,16 +1072,35 @@ void
 MediaStreamGraph::NotifyOutputData(AudioDataValue* aBuffer, size_t aFrames,
                                    TrackRate aRate, uint32_t aChannels)
 {
   for (auto& listener : mAudioInputs) {
     listener->NotifyOutputData(this, aBuffer, aFrames, aRate, aChannels);
   }
 }
 
+void
+MediaStreamGraph::AssertOnGraphThreadOrNotRunning() const
+{
+  // either we're on the right thread (and calling CurrentDriver() is safe),
+  // or we're going to assert anyways, so don't cross-check CurrentDriver
+#ifdef DEBUG
+  MediaStreamGraphImpl const * graph =
+    static_cast<MediaStreamGraphImpl const *>(this);
+  // if all the safety checks fail, assert we own the monitor
+  if (!graph->mDriver->OnThread()) {
+    if (!(graph->mDetectedNotRunning &&
+          graph->mLifecycleState > MediaStreamGraphImpl::LIFECYCLE_RUNNING &&
+          NS_IsMainThread())) {
+      graph->mMonitor.AssertCurrentThreadOwns();
+    }
+  }
+#endif
+}
+
 bool
 MediaStreamGraphImpl::ShouldUpdateMainThread()
 {
   if (mRealtime) {
     return true;
   }
 
   TimeStamp now = TimeStamp::Now();
--- a/dom/media/MediaStreamGraph.h
+++ b/dom/media/MediaStreamGraph.h
@@ -1348,16 +1348,17 @@ public:
    * Media graph thread only.
    * Dispatches a runnable that will run on the main thread after all
    * main-thread stream state has been next updated.
    * Should only be called during MediaStreamListener callbacks or during
    * ProcessedMediaStream::ProcessInput().
    */
   virtual void DispatchToMainThreadAfterStreamStateUpdate(already_AddRefed<nsIRunnable> aRunnable)
   {
+    AssertOnGraphThreadOrNotRunning();
     *mPendingUpdateRunnables.AppendElement() = aRunnable;
   }
 
   /**
    * Returns graph sample rate in Hz.
    */
   TrackRate GraphRate() const { return mSampleRate; }
 
@@ -1369,16 +1370,18 @@ public:
 
   /**
    * Data going to the speakers from the GraphDriver's DataCallback
    * to notify any listeners (for echo cancellation).
    */
   void NotifyOutputData(AudioDataValue* aBuffer, size_t aFrames,
                         TrackRate aRate, uint32_t aChannels);
 
+  void AssertOnGraphThreadOrNotRunning() const;
+
 protected:
   explicit MediaStreamGraph(TrackRate aSampleRate)
     : mSampleRate(aSampleRate)
   {
     MOZ_COUNT_CTOR(MediaStreamGraph);
   }
   virtual ~MediaStreamGraph()
   {
--- a/dom/media/MediaStreamGraphImpl.h
+++ b/dom/media/MediaStreamGraphImpl.h
@@ -197,34 +197,18 @@ public:
   /**
    * Respond to CollectReports with sizes collected on the graph thread.
    */
   static void
   FinishCollectReports(nsIHandleReportCallback* aHandleReport,
                        nsISupports* aData,
                        const nsTArray<AudioNodeSizes>& aAudioStreamSizes);
 
-  // The following methods run on the graph thread (or possibly the main thread if
-  // mLifecycleState > LIFECYCLE_RUNNING)
-  void AssertOnGraphThreadOrNotRunning() const
-  {
-    // either we're on the right thread (and calling CurrentDriver() is safe),
-    // or we're going to assert anyways, so don't cross-check CurrentDriver
-#ifdef DEBUG
-    // if all the safety checks fail, assert we own the monitor
-    if (!mDriver->OnThread()) {
-      if (!(mDetectedNotRunning &&
-            mLifecycleState > LIFECYCLE_RUNNING &&
-            NS_IsMainThread())) {
-        mMonitor.AssertCurrentThreadOwns();
-      }
-    }
-#endif
-  }
-
+  // The following methods run on the graph thread (or possibly the main thread
+  // if mLifecycleState > LIFECYCLE_RUNNING)
   void CollectSizesForMemoryReport(
          already_AddRefed<nsIHandleReportCallback> aHandleReport,
          already_AddRefed<nsISupports> aHandlerData);
 
   /**
    * Returns true if this MediaStreamGraph should keep running
    */
   bool UpdateMainThreadState();
--- a/dom/media/flac/FlacFrameParser.cpp
+++ b/dom/media/flac/FlacFrameParser.cpp
@@ -127,17 +127,17 @@ FlacFrameParser::DecodeHeaderBlock(const
       uint32_t sampleRate = (blob >> 44) & BITMASK(20);
       if (!sampleRate) {
         return false;
       }
       uint32_t numChannels = ((blob >> 41) & BITMASK(3)) + 1;
       if (numChannels > FLAC_MAX_CHANNELS) {
         return false;
       }
-      uint32_t bps = ((blob >> 38) & BITMASK(5)) + 1;
+      uint32_t bps = ((blob >> 36) & BITMASK(5)) + 1;
       if (bps > 24) {
         return false;
       }
       mNumFrames = blob & BITMASK(36);
 
       mInfo.mMimeType = "audio/flac";
       mInfo.mRate = sampleRate;
       mInfo.mChannels = numChannels;
--- a/dom/media/platforms/PlatformDecoderModule.h
+++ b/dom/media/platforms/PlatformDecoderModule.h
@@ -170,16 +170,19 @@ public:
   virtual void Output(MediaData* aData) = 0;
 
   // Denotes an error in the decoding process. The reader will stop calling
   // the decoder.
   virtual void Error(MediaDataDecoderError 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.
+  // A frame decoding session is completed once InputExhausted has been called.
+  // MediaDataDecoder::Input will not be called again until InputExhausted has
+  // been called.
   virtual void InputExhausted() = 0;
 
   virtual void DrainComplete() = 0;
 
   virtual void ReleaseMediaResources() {}
 
   virtual bool OnReaderTaskQueue() = 0;
 
--- a/dom/media/platforms/agnostic/BlankDecoderModule.cpp
+++ b/dom/media/platforms/agnostic/BlankDecoderModule.cpp
@@ -91,21 +91,17 @@ private:
     }
 
     // Frames come out in DTS order but we need to output them in PTS order.
     mReorderQueue.Push(aData);
 
     while (mReorderQueue.Length() > mMaxRefFrames) {
       mCallback->Output(mReorderQueue.Pop().get());
     }
-
-    if (mReorderQueue.Length() <= mMaxRefFrames) {
-      mCallback->InputExhausted();
-    }
-
+    mCallback->InputExhausted();
   }
 
 private:
   nsAutoPtr<BlankMediaDataCreator> mCreator;
   MediaDataDecoderCallback* mCallback;
   const uint32_t mMaxRefFrames;
   ReorderQueue mReorderQueue;
   TrackInfo::TrackType mType;
--- a/dom/media/platforms/agnostic/OpusDecoder.cpp
+++ b/dom/media/platforms/agnostic/OpusDecoder.cpp
@@ -158,22 +158,19 @@ OpusDataDecoder::ProcessDecode(MediaRawD
   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;
   }
-
-  if (mTaskQueue->IsEmpty()) {
-    mCallback->InputExhausted();
-  }
 }
 
 OpusDataDecoder::DecodeError
 OpusDataDecoder::DoDecode(MediaRawData* aSample)
 {
   int64_t aDiscardPadding = 0;
   if (aSample->mExtraData) {
     aDiscardPadding = BigEndian::readInt64(aSample->mExtraData->Elements());
--- a/dom/media/platforms/agnostic/TheoraDecoder.cpp
+++ b/dom/media/platforms/agnostic/TheoraDecoder.cpp
@@ -197,17 +197,17 @@ void
 TheoraDecoder::ProcessDecode(MediaRawData* aSample)
 {
   MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
   if (mIsFlushing) {
     return;
   }
   if (DoDecode(aSample) == -1) {
     mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
-  } else if (mTaskQueue->IsEmpty()) {
+  } else {
     mCallback->InputExhausted();
   }
 }
 
 nsresult
 TheoraDecoder::Input(MediaRawData* aSample)
 {
   MOZ_ASSERT(mCallback->OnReaderTaskQueue());
--- a/dom/media/platforms/agnostic/VPXDecoder.cpp
+++ b/dom/media/platforms/agnostic/VPXDecoder.cpp
@@ -187,17 +187,17 @@ void
 VPXDecoder::ProcessDecode(MediaRawData* aSample)
 {
   MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
   if (mIsFlushing) {
     return;
   }
   if (DoDecode(aSample) == -1) {
     mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
-  } else if (mTaskQueue->IsEmpty()) {
+  } else {
     mCallback->InputExhausted();
   }
 }
 
 nsresult
 VPXDecoder::Input(MediaRawData* aSample)
 {
   MOZ_ASSERT(mCallback->OnReaderTaskQueue());
--- a/dom/media/platforms/agnostic/VorbisDecoder.cpp
+++ b/dom/media/platforms/agnostic/VorbisDecoder.cpp
@@ -138,17 +138,17 @@ void
 VorbisDataDecoder::ProcessDecode(MediaRawData* aSample)
 {
   MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
   if (mIsFlushing) {
     return;
   }
   if (DoDecode(aSample) == -1) {
     mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
-  } else if (mTaskQueue->IsEmpty()) {
+  } else {
     mCallback->InputExhausted();
   }
 }
 
 int
 VorbisDataDecoder::DoDecode(MediaRawData* aSample)
 {
   MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
--- a/dom/media/platforms/agnostic/WAVDecoder.cpp
+++ b/dom/media/platforms/agnostic/WAVDecoder.cpp
@@ -63,16 +63,18 @@ WaveDataDecoder::Init()
   return InitPromise::CreateAndResolve(TrackInfo::kAudioTrack, __func__);
 }
 
 nsresult
 WaveDataDecoder::Input(MediaRawData* aSample)
 {
   if (!DoDecode(aSample)) {
     mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
+  } else {
+    mCallback->InputExhausted();
   }
   return NS_OK;
 }
 
 bool
 WaveDataDecoder::DoDecode(MediaRawData* aSample)
 {
   size_t aLength = aSample->Size();
--- a/dom/media/platforms/apple/AppleATDecoder.cpp
+++ b/dom/media/platforms/apple/AppleATDecoder.cpp
@@ -210,20 +210,17 @@ AppleATDecoder::SubmitSample(MediaRawDat
       if (NS_FAILED(DecodeSample(mQueuedSamples[i]))) {
         mQueuedSamples.Clear();
         mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
         return;
       }
     }
     mQueuedSamples.Clear();
   }
-
-  if (mTaskQueue->IsEmpty()) {
-    mCallback->InputExhausted();
-  }
+  mCallback->InputExhausted();
 }
 
 nsresult
 AppleATDecoder::DecodeSample(MediaRawData* aSample)
 {
   MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
 
   // Array containing the queued decoded audio frames, about to be output.
--- a/dom/media/platforms/apple/AppleVTDecoder.cpp
+++ b/dom/media/platforms/apple/AppleVTDecoder.cpp
@@ -29,21 +29,19 @@ AppleVTDecoder::AppleVTDecoder(const Vid
                                MediaDataDecoderCallback* aCallback,
                                layers::ImageContainer* aImageContainer)
   : mExtraData(aConfig.mExtraData)
   , mCallback(aCallback)
   , mPictureWidth(aConfig.mImage.width)
   , mPictureHeight(aConfig.mImage.height)
   , mDisplayWidth(aConfig.mDisplay.width)
   , mDisplayHeight(aConfig.mDisplay.height)
-  , mQueuedSamples(0)
   , mTaskQueue(aTaskQueue)
   , mMaxRefFrames(mp4_demuxer::H264::ComputeMaxRefFrames(aConfig.mExtraData))
   , mImageContainer(aImageContainer)
-  , mInputIncoming(0)
   , mIsShutDown(false)
 #ifdef MOZ_WIDGET_UIKIT
   , mUseSoftwareImages(true)
 #else
   , mUseSoftwareImages(false)
 #endif
   , mIsFlushing(false)
   , mMonitor("AppleVideoDecoder")
@@ -83,34 +81,30 @@ AppleVTDecoder::Input(MediaRawData* aSam
 
   LOG("mp4 input sample %p pts %lld duration %lld us%s %d bytes",
       aSample,
       aSample->mTime,
       aSample->mDuration,
       aSample->mKeyframe ? " keyframe" : "",
       aSample->Size());
 
-  mInputIncoming++;
-
   mTaskQueue->Dispatch(NewRunnableMethod<RefPtr<MediaRawData>>(
     this, &AppleVTDecoder::ProcessDecode, aSample));
   return NS_OK;
 }
 
 nsresult
 AppleVTDecoder::Flush()
 {
   MOZ_ASSERT(mCallback->OnReaderTaskQueue());
   mIsFlushing = true;
   nsCOMPtr<nsIRunnable> runnable =
     NewRunnableMethod(this, &AppleVTDecoder::ProcessFlush);
   SyncRunnable::DispatchToThread(mTaskQueue, runnable);
   mIsFlushing = false;
-  // All ProcessDecode() tasks should be done.
-  MOZ_ASSERT(mInputIncoming == 0);
 
   mSeekTargetThreshold.reset();
 
   return NS_OK;
 }
 
 nsresult
 AppleVTDecoder::Drain()
@@ -137,28 +131,21 @@ AppleVTDecoder::Shutdown()
   return NS_OK;
 }
 
 nsresult
 AppleVTDecoder::ProcessDecode(MediaRawData* aSample)
 {
   AssertOnTaskQueueThread();
 
-  mInputIncoming--;
-
   if (mIsFlushing) {
     return NS_OK;
   }
 
   auto rv = DoDecode(aSample);
-  // Ask for more data.
-  if (NS_SUCCEEDED(rv) && !mInputIncoming && mQueuedSamples <= mMaxRefFrames) {
-    LOG("%s task queue empty; requesting more data", GetDescriptionName());
-    mCallback->InputExhausted();
-  }
 
   return rv;
 }
 
 void
 AppleVTDecoder::ProcessShutdown()
 {
   if (mSession) {
@@ -208,27 +195,25 @@ AppleVTDecoder::CreateAppleFrameRef(cons
 
 void
 AppleVTDecoder::DrainReorderedFrames()
 {
   MonitorAutoLock mon(mMonitor);
   while (!mReorderQueue.IsEmpty()) {
     mCallback->Output(mReorderQueue.Pop().get());
   }
-  mQueuedSamples = 0;
 }
 
 void
 AppleVTDecoder::ClearReorderedFrames()
 {
   MonitorAutoLock mon(mMonitor);
   while (!mReorderQueue.IsEmpty()) {
     mReorderQueue.Pop();
   }
-  mQueuedSamples = 0;
 }
 
 void
 AppleVTDecoder::SetSeekThreshold(const media::TimeUnit& aTime)
 {
   LOG("SetSeekThreshold %lld", aTime.ToMicroseconds());
   mSeekTargetThreshold = Some(aTime);
 }
@@ -283,26 +268,20 @@ AppleVTDecoder::OutputFrame(CVPixelBuffe
   LOG("mp4 output frame %lld dts %lld pts %lld duration %lld us%s",
       aFrameRef.byte_offset,
       aFrameRef.decode_timestamp.ToMicroseconds(),
       aFrameRef.composition_timestamp.ToMicroseconds(),
       aFrameRef.duration.ToMicroseconds(),
       aFrameRef.is_sync_point ? " keyframe" : ""
   );
 
-  if (mQueuedSamples > mMaxRefFrames) {
-    // We had stopped requesting more input because we had received too much at
-    // the time. We can ask for more once again.
+  if (!aImage) {
+    // Image was dropped by decoder or none return yet.
+    // We need more input to continue.
     mCallback->InputExhausted();
-  }
-  MOZ_ASSERT(mQueuedSamples);
-  mQueuedSamples--;
-
-  if (!aImage) {
-    // Image was dropped by decoder.
     return NS_OK;
   }
 
   bool useNullSample = false;
   if (mSeekTargetThreshold.isSome()) {
     if ((aFrameRef.composition_timestamp + aFrameRef.duration) < mSeekTargetThreshold.ref()) {
       useNullSample = true;
     } else {
@@ -405,19 +384,20 @@ AppleVTDecoder::OutputFrame(CVPixelBuffe
     mCallback->Error(MediaDataDecoderError::FATAL_ERROR);
     return NS_ERROR_FAILURE;
   }
 
   // Frames come out in DTS order but we need to output them
   // in composition order.
   MonitorAutoLock mon(mMonitor);
   mReorderQueue.Push(data);
-  while (mReorderQueue.Length() > mMaxRefFrames) {
+  if (mReorderQueue.Length() > mMaxRefFrames) {
     mCallback->Output(mReorderQueue.Pop().get());
   }
+  mCallback->InputExhausted();
   LOG("%llu decoded frames queued",
       static_cast<unsigned long long>(mReorderQueue.Length()));
 
   return NS_OK;
 }
 
 nsresult
 AppleVTDecoder::WaitForAsynchronousFrames()
@@ -475,18 +455,16 @@ AppleVTDecoder::DoDecode(MediaRawData* a
   }
   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;
   }
 
-  mQueuedSamples++;
-
   VTDecodeFrameFlags decodeFlags =
     kVTDecodeFrame_EnableAsynchronousDecompression;
   rv = VTDecompressionSessionDecodeFrame(mSession,
                                          sample,
                                          decodeFlags,
                                          CreateAppleFrameRef(aSample),
                                          &infoFlags);
   if (rv != noErr && !(infoFlags & kVTDecodeInfo_FrameDropped)) {
--- a/dom/media/platforms/apple/AppleVTDecoder.h
+++ b/dom/media/platforms/apple/AppleVTDecoder.h
@@ -85,35 +85,27 @@ private:
 
   const RefPtr<MediaByteBuffer> mExtraData;
   MediaDataDecoderCallback* mCallback;
   const uint32_t mPictureWidth;
   const uint32_t mPictureHeight;
   const uint32_t mDisplayWidth;
   const uint32_t mDisplayHeight;
 
-  // Number of times a sample was queued via Input(). Will be decreased upon
-  // the decoder's callback being invoked.
-  // This is used to calculate how many frames has been buffered by the decoder.
-  Atomic<uint32_t> mQueuedSamples;
-
   // Method to set up the decompression session.
   nsresult InitializeSession();
   nsresult WaitForAsynchronousFrames();
   CFDictionaryRef CreateDecoderSpecification();
   CFDictionaryRef CreateDecoderExtensions();
   // Method to pass a frame to VideoToolbox for decoding.
   nsresult DoDecode(MediaRawData* aSample);
 
   const RefPtr<TaskQueue> mTaskQueue;
   const uint32_t mMaxRefFrames;
   const RefPtr<layers::ImageContainer> mImageContainer;
-  // Increased when Input is called, and decreased when ProcessFrame runs.
-  // Reaching 0 indicates that there's no pending Input.
-  Atomic<uint32_t> mInputIncoming;
   Atomic<bool> mIsShutDown;
   const bool mUseSoftwareImages;
 
   // Set on reader/decode thread calling Flush() to indicate that output is
   // not required and so input samples on mTaskQueue need not be processed.
   // Cleared on mTaskQueue in ProcessDrain().
   Atomic<bool> mIsFlushing;
   // Protects mReorderQueue.
--- a/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.cpp
+++ b/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.cpp
@@ -128,16 +128,17 @@ FFmpegAudioDecoder<LIBAV_VER>::DoDecode(
 
   if (!PrepareFrame()) {
     NS_WARNING("FFmpeg audio decoder failed to allocate frame.");
     return DecodeResult::FATAL_ERROR;
   }
 
   int64_t samplePosition = aSample->mOffset;
   media::TimeUnit pts = media::TimeUnit::FromMicroseconds(aSample->mTime);
+  bool didOutput = false;
 
   while (packet.size > 0) {
     int decoded;
     int bytesConsumed =
       mLib->avcodec_decode_audio4(mCodecContext, mFrame, &decoded, &packet);
 
     if (bytesConsumed < 0) {
       NS_WARNING("FFmpeg audio decoder error.");
@@ -176,28 +177,29 @@ FFmpegAudioDecoder<LIBAV_VER>::DoDecode(
       RefPtr<AudioData> data = new AudioData(samplePosition,
                                              pts.ToMicroseconds(),
                                              duration.ToMicroseconds(),
                                              mFrame->nb_samples,
                                              Move(audio),
                                              numChannels,
                                              samplingRate);
       mCallback->Output(data);
+      didOutput = true;
       pts += duration;
       if (!pts.IsValid()) {
         NS_WARNING("Invalid count of accumulated audio samples");
         return DecodeResult::DECODE_ERROR;
       }
     }
     packet.data += bytesConsumed;
     packet.size -= bytesConsumed;
     samplePosition += bytesConsumed;
   }
 
-  return DecodeResult::DECODE_FRAME;
+  return didOutput ? DecodeResult::DECODE_FRAME : DecodeResult::DECODE_NO_FRAME;
 }
 
 void
 FFmpegAudioDecoder<LIBAV_VER>::ProcessDrain()
 {
   ProcessFlush();
   mCallback->DrainComplete();
 }
--- a/dom/media/platforms/ffmpeg/FFmpegDataDecoder.cpp
+++ b/dom/media/platforms/ffmpeg/FFmpegDataDecoder.cpp
@@ -112,20 +112,22 @@ FFmpegDataDecoder<LIBAV_VER>::ProcessDec
   }
   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:
-      if (mTaskQueue->IsEmpty()) {
-        mCallback->InputExhausted();
-      }
+      break;
   }
 }
 
 nsresult
 FFmpegDataDecoder<LIBAV_VER>::Input(MediaRawData* aSample)
 {
   mTaskQueue->Dispatch(NewRunnableMethod<RefPtr<MediaRawData>>(
     this, &FFmpegDataDecoder::ProcessDecode, aSample));
--- a/dom/media/platforms/wmf/WMFMediaDataDecoder.cpp
+++ b/dom/media/platforms/wmf/WMFMediaDataDecoder.cpp
@@ -144,19 +144,17 @@ WMFMediaDataDecoder::ProcessOutput()
   RefPtr<MediaData> output;
   HRESULT hr = S_OK;
   while (SUCCEEDED(hr = mMFTManager->Output(mLastStreamOffset, output)) &&
          output) {
     mHasSuccessfulOutput = true;
     mCallback->Output(output);
   }
   if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) {
-    if (mTaskQueue->IsEmpty()) {
-      mCallback->InputExhausted();
-    }
+    mCallback->InputExhausted();
   } else if (FAILED(hr)) {
     NS_WARNING("WMFMediaDataDecoder failed to output data");
     mCallback->Error(MediaDataDecoderError::DECODE_ERROR);
     if (!mRecordedError) {
       SendTelemetry(hr);
       mRecordedError = true;
     }
   }
--- a/dom/media/test/external/external_media_tests/media_utils/youtube_puppeteer.py
+++ b/dom/media/test/external/external_media_tests/media_utils/youtube_puppeteer.py
@@ -10,19 +10,26 @@ from marionette import Marionette
 from marionette_driver import By, expected, Wait
 from marionette_driver.errors import TimeoutException, NoSuchElementException
 from video_puppeteer import VideoPuppeteer, VideoException
 from external_media_tests.utils import verbose_until
 
 
 class YouTubePuppeteer(VideoPuppeteer):
     """
-    Wrapper around a YouTube #movie_player element.
+    Wrapper around a YouTube .html5-video-player element.
 
-    Partial reference: https://developers.google.com/youtube/js_api_reference.
+    Can be used with youtube videos or youtube videos at embedded URLS. E.g.
+    both https://www.youtube.com/watch?v=AbAACm1IQE0 and
+    https://www.youtube.com/embed/AbAACm1IQE0 should work.
+
+    Using an embedded video has the advantage of not auto-playing more videos
+    while a test is running.
+
+    Partial reference: https://developers.google.com/youtube/iframe_api_reference.
     This reference is useful for site-specific features such as interacting
     with ads, or accessing YouTube's debug data.
     """
 
     _yt_player_state = {
         'UNSTARTED': -1,
         'ENDED': 0,
         'PLAYING': 1,
@@ -32,24 +39,26 @@ class YouTubePuppeteer(VideoPuppeteer):
     }
     _yt_player_state_name = {v: k for k, v in _yt_player_state.items()}
     _time_pattern = re.compile('(?P<minute>\d+):(?P<second>\d+)')
 
     def __init__(self, marionette, url, **kwargs):
         self.player = None
         super(YouTubePuppeteer,
               self).__init__(marionette, url,
-                             video_selector='#movie_player video',
+                             video_selector='.html5-video-player video',
                              **kwargs)
         wait = Wait(self.marionette, timeout=30)
         with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
             verbose_until(wait, self,
-                          expected.element_present(By.ID, 'movie_player'))
-            self.player = self.marionette.find_element(By.ID, 'movie_player')
-            self.marionette.execute_script("log('#movie_player "
+                          expected.element_present(By.CLASS_NAME,
+                                                   'html5-video-player'))
+            self.player = self.marionette.find_element(By.CLASS_NAME,
+                                                       'html5-video-player')
+            self.marionette.execute_script("log('.html5-video-player "
                                            "element obtained');")
         # When an ad is playing, self.player_duration indicates the duration
         # of the spliced-in ad stream, not the duration of the main video, so
         # we attempt to skip the ad first.
         for attempt in range(5):
             sleep(1)
             self.process_ad()
             if (self.ad_inactive and self.duration and not
@@ -112,31 +121,31 @@ class YouTubePuppeteer(VideoPuppeteer):
                 return loads(text)
             except ValueError:
                 self.marionette.log('Error loading json: DebugText',
                                     level='DEBUG')
 
     def execute_yt_script(self, script):
         """
         Execute JS script in content context with access to video element and
-        YouTube #movie_player element.
+        YouTube .html5-video-player element.
 
         :param script: script to be executed.
 
         :return: value returned by script
         """
         with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
             return self.marionette.execute_script(script,
                                                   script_args=[self.video,
                                                                self.player])
 
     @property
     def playback_quality(self):
         """
-        Please see https://developers.google.com/youtube/js_api_reference#Playback_quality
+        Please see https://developers.google.com/youtube/iframe_api_reference#Playback_quality
         for valid values.
 
         :return: A string with a valid value returned via YouTube.
         """
         return self.execute_yt_script('return arguments[1].'
                                       'wrappedJSObject.getPlaybackQuality();')
 
     @property
@@ -171,29 +180,29 @@ class YouTubePuppeteer(VideoPuppeteer):
         return self.execute_yt_script('return arguments[1].'
                                       'wrappedJSObject.getVideoUrl();')
 
     @property
     def player_state(self):
         """
 
         :return: The YouTube state of the video. See
-         https://developers.google.com/youtube/js_api_reference#getPlayerState
+         https://developers.google.com/youtube/iframe_api_reference#getPlayerState
          for valid values.
         """
         state = self.execute_yt_script('return arguments[1].'
                                        'wrappedJSObject.getPlayerState();')
         return state
 
     @property
     def player_unstarted(self):
         """
         This and the following properties are based on the
         player.getPlayerState() call
-        (https://developers.google.com/youtube/js_api_reference#Playback_status)
+        (https://developers.google.com/youtube/iframe_api_reference#Playback_status)
 
         :return: True if the video has not yet started.
         """
         return self.player_state == self._yt_player_state['UNSTARTED']
 
     @property
     def player_ended(self):
         """
@@ -235,17 +244,17 @@ class YouTubePuppeteer(VideoPuppeteer):
         return self.player_state == self._yt_player_state['CUED']
 
     @property
     def ad_state(self):
         """
         Get state of current ad.
 
         :return: Returns one of the constants listed in
-         https://developers.google.com/youtube/js_api_reference#Playback_status
+         https://developers.google.com/youtube/iframe_api_reference#Playback_status
          for an ad.
 
         """
         # Note: ad_state is sometimes not accurate, due to some sort of lag?
         return self.execute_yt_script('return arguments[1].'
                                       'wrappedJSObject.getAdState();')
 
     @property
@@ -345,17 +354,17 @@ class YouTubePuppeteer(VideoPuppeteer):
         """
         if self.ad_playing:
             self.marionette.log('Waiting while ad plays')
             sleep(10)
         else:
             # no ad playing
             return False
         if self.ad_skippable:
-            selector = '#movie_player .videoAdUiSkipContainer'
+            selector = '.html5-video-player .videoAdUiSkipContainer'
             wait = Wait(self.marionette, timeout=30)
             try:
                 with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
                     wait.until(expected.element_displayed(By.CSS_SELECTOR,
                                                           selector))
                     ad_button = self.marionette.find_element(By.CSS_SELECTOR,
                                                              selector)
                     ad_button.click()
@@ -373,17 +382,17 @@ class YouTubePuppeteer(VideoPuppeteer):
         :return: ad duration in seconds, if currently displayed in player
         """
         if not (self.ad_playing or self.player_measure_progress() == 0):
             return None
         # If the ad is not Flash...
         if (self.ad_playing and self.video_src.startswith('mediasource') and
                 self.duration):
             return self.duration
-        selector = '#movie_player .videoAdUiAttribution'
+        selector = '.html5-media-player .videoAdUiAttribution'
         wait = Wait(self.marionette, timeout=5)
         try:
             with self.marionette.using_context(Marionette.CONTEXT_CONTENT):
                 wait.until(expected.element_present(By.CSS_SELECTOR,
                                                     selector))
                 countdown = self.marionette.find_element(By.CSS_SELECTOR,
                                                          selector)
                 ad_time = self._time_pattern.search(countdown.text)
@@ -459,28 +468,28 @@ class YouTubePuppeteer(VideoPuppeteer):
             return False
 
     def __str__(self):
         messages = [super(YouTubePuppeteer, self).__str__()]
         if self.player:
             player_state = self._yt_player_state_name[self.player_state]
             ad_state = self._yt_player_state_name[self.ad_state]
             messages += [
-                '#movie_player: {',
+                '.html5-media-player: {',
                 '\tvideo id: {0},'.format(self.movie_id),
                 '\tvideo_title: {0}'.format(self.movie_title),
                 '\tcurrent_state: {0},'.format(player_state),
                 '\tad_state: {0},'.format(ad_state),
                 '\tplayback_quality: {0},'.format(self.playback_quality),
                 '\tcurrent_time: {0},'.format(self.player_current_time),
                 '\tduration: {0},'.format(self.player_duration),
                 '}'
             ]
         else:
-            messages += ['\t#movie_player: None']
+            messages += ['\t.html5-media-player: None']
         return '\n'.join(messages)
 
 
 def playback_started(yt):
     """
     Check whether playback has started.
 
     :param yt: YouTubePuppeteer
--- a/dom/media/test/external/external_media_tests/urls/default.ini
+++ b/dom/media/test/external/external_media_tests/urls/default.ini
@@ -1,9 +1,9 @@
-# short videos; no ads; max 5 minutes
+# short videos; no ads; embedded; max 5 minutes
 # 0:12
-[https://youtu.be/AbAACm1IQE0]
+[https://youtube.com/embed/AbAACm1IQE0?autoplay=1]
 # 2:18
-[https://www.youtube.com/watch?v=yOQQCoxs8-k]
+[https://youtube.com/embed/yOQQCoxs8-k?autoplay=1]
 # 0:08
-[https://www.youtube.com/watch?v=1visYpIREUM]
+[https://youtube.com/embed/1visYpIREUM?autoplay=1]
 # 2:09
-[https://www.youtube.com/watch?v=rjmuKV9BTkE]
+[https://youtube.com/embed/rjmuKV9BTkE?autoplay=1]
--- a/dom/media/test/external/external_media_tests/urls/youtube/long1-720.ini
+++ b/dom/media/test/external/external_media_tests/urls/youtube/long1-720.ini
@@ -1,14 +1,5 @@
-# all long videos; < 12 hours total
-# 2:18:00
-[http://youtu.be/FLX64H5FYa8]
-# 1:00:00
-[https://www.youtube.com/watch?v=AYYDshv8C4g]
-# 1:10:00
-[https://www.youtube.com/watch?v=V0Vy4kYAPDk]
-# 1:47:00
-[https://www.youtube.com/watch?v=bFtGE2C7Pxs]
-
-
-# shutdownhang | WaitForSingleObjectEx | WaitForSingleObject | PR_Wait | nsThread::ProcessNextEvent(bool, bool*) | NS_ProcessNextEvent(nsIThread*, bool) | mozilla::MediaShutdownManager::Shutdown()
-# 1:43:00
-[https://www.youtube.com/watch?v=BXMtXpmpXPU]
+# a couple of very long videos, < 12 hours total
+# 6:00:00 - can't embed due to copyright
+[https://www.youtube.com/watch?v=5N8sUccRiTA]
+# 2:09:00
+[https://www.youtube.com/embed/b6q5N16dje4?autoplay=1]
deleted file mode 100644
--- a/dom/media/test/external/external_media_tests/urls/youtube/long2-720.ini
+++ /dev/null
@@ -1,9 +0,0 @@
-# a couple of very long videos, < 12 hours total
-# 6:00:00
-[https://www.youtube.com/watch?v=5N8sUccRiTA]
-# 2:27:00
-[https://www.youtube.com/watch?v=NAVrm3wjzq8]
-# 58:50
-[https://www.youtube.com/watch?v=uP1BBw3IYco]
-# 2:09:00
-[https://www.youtube.com/watch?v=b6q5N16dje4]
rename from dom/media/test/external/external_media_tests/urls/youtube/long3-crashes-720.ini
rename to dom/media/test/external/external_media_tests/urls/youtube/long2-crashes-720.ini
--- a/dom/media/test/external/external_media_tests/urls/youtube/long3-crashes-720.ini
+++ b/dom/media/test/external/external_media_tests/urls/youtube/long2-crashes-720.ini
@@ -1,8 +1,11 @@
+# It appears these are not currently used by tests. They are left here as they
+# reference failure scenarios. If tese are fixed that can be removed.
+
 # videos from crashes, < 12 hours
 
 # hang | NtUserMessageCall | SendMessageW
 # 1:10:00
 [https://www.youtube.com/watch?v=Ztie4DqeOak]
 
 # nsPluginInstanceOwner::GetDocument(nsIDocument**)
 # 22:40
rename from dom/media/test/external/external_media_tests/urls/youtube/long4-crashes-900.ini
rename to dom/media/test/external/external_media_tests/urls/youtube/long3-crashes-900.ini
--- a/dom/media/test/external/external_media_tests/urls/youtube/long4-crashes-900.ini
+++ b/dom/media/test/external/external_media_tests/urls/youtube/long3-crashes-900.ini
@@ -1,8 +1,11 @@
+# It appears these are not currently used by tests. They are left here as they
+# reference failure scenarios. If tese are fixed that can be removed.
+
 # Total time: about 12-13 hours + unskippable ads
 #Request url:  https://crash-stats.mozilla.com/api/SuperSearchUnredacted/?product=Firefox&url=%24https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D&url=%21~list&url=%21~index&_results_number=50&platform=Windows&version=37.0&date=%3E2015-03-26
 
 #Request url:    https://crash-stats.mozilla.com/api/SuperSearchUnredacted/?product=Firefox&url=%24https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D&url=%21~list&url=%21~index&_results_number=5&platform=Windows&version=37.0&signature=%3Dhang+%7C+NtUserMessageCall+%7C+SendMessageW&date=%3E2015-03-26
 
 #Request url:    https://crash-stats.mozilla.com/api/SuperSearchUnredacted/?product=Firefox&url=%24https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D&url=%21~list&url=%21~index&_results_number=5&platform=Windows&version=37.0&signature=%3DOOM+%7C+small&date=%3E2015-03-26
 
 #Request url:    https://crash-stats.mozilla.com/api/SuperSearchUnredacted/?product=Firefox&url=%24https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D&url=%21~list&url=%21~index&_results_number=5&platform=Windows&version=37.0&signature=%3Dmozilla%3A%3Alayers%3A%3ACompositorD3D11%3A%3AHandleError%28long%2C+mozilla%3A%3Alayers%3A%3ACompositorD3D11%3A%3ASeverity%29+%7C+mozilla%3A%3Alayers%3A%3ACompositorD3D11%3A%3AFailed%28long%2C+mozilla%3A%3Alayers%3A%3ACompositorD3D11%3A%3ASeverity%29+%7C+mozilla%3A%3Alayers%3A%3ACompositorD3D11%3A%3AUpdateRenderTarget%28%29&date=%3E2015-03-26
deleted file mode 100644
--- a/dom/media/test/external/external_media_tests/urls/youtube/massive-6000.ini
+++ /dev/null
@@ -1,15 +0,0 @@
-# very long test; 96-100 hours?
-# 00:3:26
-[https://www.youtube.com/watch?v=7RMQksXpQSk]
-# nyan cat 10 hours
-[http://youtu.be/9bZkp7q19f0]
-# 4:54:00
-[https://www.youtube.com/watch?v=jWlKjw3LBDk]
-# 3:00:01
-[https://www.youtube.com/watch?v=ub9JUDS_6i8]
-# 10 hours rick roll
-[https://www.youtube.com/watch?v=BROWqjuTM0g]
-# 24 hours
-[https://www.youtube.com/watch?v=FvHiLLkPhQE]
-# 2 hours
-[https://www.youtube.com/watch?v=VmOuW5zTt9w
--- a/dom/media/test/external/external_media_tests/urls/youtube/medium1-60.ini
+++ b/dom/media/test/external/external_media_tests/urls/youtube/medium1-60.ini
@@ -1,18 +1,18 @@
 # mix of shorter/longer videos with/without ads, < 60 min
-# 4:59
-[http://youtu.be/pWI8RB2dmfU]
+# 4:59 - can't embed
+[https://www.youtube.com/watch?v=pWI8RB2dmfU]
 # 0:46 ad at start
-[http://youtu.be/6SFp1z7uA6g]
+[https://www.youtube.com/embed/6SFp1z7uA6g?autoplay=1]
 # 0:58 ad at start
-[http://youtu.be/Aebs62bX0dA]
+[https://www.youtube.com/embed/Aebs62bX0dA?autoplay=1]
 # 1:43 ad
-[https://www.youtube.com/watch?v=l5ODwR6FPRQ]
-# 8:00 ad
+[https://www.youtube.com/embed/l5ODwR6FPRQ?autoplay=1]
+# 8:00 ad - can't embed
 [https://www.youtube.com/watch?v=KlyXNRrsk4A]
 # video with ad in beginning and in the middle 20:00
 # https://bugzilla.mozilla.org/show_bug.cgi?id=1176815
-[https://www.youtube.com/watch?v=cht9Xq9suGg]
+[https://www.youtube.com/embed/cht9Xq9suGg?autoplay=1]
 # 1:35 ad
-[https://www.youtube.com/watch?v=orybDrUj4vA]
-# 3:02 - ad
-[https://youtu.be/tDDVAErOI5U]
+[https://www.youtube.com/embed/orybDrUj4vA?autoplay=1]
+# 3:02 ad
+[https://www.youtube.com/embed/tDDVAErOI5U?autoplay=1]
deleted file mode 100644
--- a/dom/media/test/external/external_media_tests/urls/youtube/medium2-60.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-# a few longer videos, < 60 min total
-# 0:30:00 no ad
-[https://www.youtube.com/watch?v=-qXxNPvqHtQ]
-# 0:20:00
-[http://youtu.be/Fu2DcHzokew]
-
deleted file mode 100644
--- a/dom/media/test/external/external_media_tests/urls/youtube/medium3-120.ini
+++ /dev/null
@@ -1,11 +0,0 @@
-# a few longer videos, < 120 min total
-# video with ad in the middle
-# 21:00
-[https://www.youtube.com/watch?v=cht9Xq9suGg]
-# 16:00
-[https://www.youtube.com/watch?v=6Lm9EHhbJAY]
-# 20:00
-[https://www.youtube.com/watch?v=8XQ1onjXJK0]
-# 59:06
-[https://www.youtube.com/watch?v=kmpiY5kssU4]
-
rename from dom/media/test/external/external_media_tests/urls/youtube/short0-10.ini
rename to dom/media/test/external/external_media_tests/urls/youtube/short1-10.ini
deleted file mode 100644
--- a/dom/media/test/external/external_media_tests/urls/youtube/short1-15.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-# 00:12
-[https://youtu.be/AbAACm1IQE0]
-# longer video with ads; < 15 min total
-# 13:40
-[https://www.youtube.com/watch?v=87uo2TPrsl8]
deleted file mode 100644
--- a/dom/media/test/external/external_media_tests/urls/youtube/short2-15.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-# 1-2 longer videos with ads; < 15 minutes total
-[https://www.youtube.com/watch?v=v678Em6qyzk]
-[https://www.youtube.com/watch?v=l8XOZJkozfI]
-
-
rename from dom/media/test/external/external_media_tests/urls/youtube/short3-crashes-15.ini
rename to dom/media/test/external/external_media_tests/urls/youtube/short2-crashes-15.ini
--- a/dom/media/test/external/external_media_tests/urls/youtube/short3-crashes-15.ini
+++ b/dom/media/test/external/external_media_tests/urls/youtube/short2-crashes-15.ini
@@ -1,8 +1,11 @@
+# It appears these are not currently used by tests. They are left here as they
+# reference failure scenarios. If tese are fixed that can be removed.
+
 # crash-data videos, < 15 minutes total
 
 # hang | NtUserMessageCall | SendMessageW
 # 5:40
 [https://www.youtube.com/watch?v=UIobdRNLNek]
 
 # F1398665248_____________________________
 # 3:59
--- a/dom/plugins/base/npapi.h
+++ b/dom/plugins/base/npapi.h
@@ -41,16 +41,17 @@
 #endif
 #endif
 
 #if defined(XP_UNIX)
 #include <stdio.h>
 #if defined(MOZ_X11)
 #include <X11/Xlib.h>
 #include <X11/Xutil.h>
+#include "X11UndefineNone.h"
 #endif
 #endif
 
 #if defined(XP_SYMBIAN)
 #include <QEvent>
 #include <QRegion>
 #endif
 
--- a/dom/plugins/base/nsPluginInstanceOwner.cpp
+++ b/dom/plugins/base/nsPluginInstanceOwner.cpp
@@ -2605,17 +2605,17 @@ nsEventStatus nsPluginInstanceOwner::Pro
         // Get reference point relative to screen:
         LayoutDeviceIntPoint rootPoint(-1, -1);
         if (widget) {
           rootPoint = anEvent.mRefPoint + widget->WidgetToScreenOffset();
         }
 #ifdef MOZ_WIDGET_GTK
         Window root = GDK_ROOT_WINDOW();
 #else
-        Window root = None; // Could XQueryTree, but this is not important.
+        Window root = X11None; // Could XQueryTree, but this is not important.
 #endif
 
         switch (anEvent.mMessage) {
           case eMouseOver:
           case eMouseOut:
             {
               XCrossingEvent& event = pluginEvent.xcrossing;
               event.type = anEvent.mMessage == eMouseOver ?
@@ -2623,17 +2623,17 @@ nsEventStatus nsPluginInstanceOwner::Pro
               event.root = root;
               event.time = anEvent.mTime;
               event.x = pluginPoint.x;
               event.y = pluginPoint.y;
               event.x_root = rootPoint.x;
               event.y_root = rootPoint.y;
               event.state = XInputEventState(mouseEvent);
               // information lost
-              event.subwindow = None;
+              event.subwindow = X11None;
               event.mode = -1;
               event.detail = NotifyDetailNone;
               event.same_screen = True;
               event.focus = mContentFocused;
             }
             break;
           case eMouseMove:
             {
@@ -2642,17 +2642,17 @@ nsEventStatus nsPluginInstanceOwner::Pro
               event.root = root;
               event.time = anEvent.mTime;
               event.x = pluginPoint.x;
               event.y = pluginPoint.y;
               event.x_root = rootPoint.x;
               event.y_root = rootPoint.y;
               event.state = XInputEventState(mouseEvent);
               // information lost
-              event.subwindow = None;
+              event.subwindow = X11None;
               event.is_hint = NotifyNormal;
               event.same_screen = True;
             }
             break;
           case eMouseDown:
           case eMouseUp:
             {
               XButtonEvent& event = pluginEvent.xbutton;
@@ -2673,17 +2673,17 @@ nsEventStatus nsPluginInstanceOwner::Pro
                 case WidgetMouseEvent::eRightButton:
                   event.button = 3;
                   break;
                 default: // WidgetMouseEvent::eLeftButton;
                   event.button = 1;
                   break;
                 }
               // information lost:
-              event.subwindow = None;
+              event.subwindow = X11None;
               event.same_screen = True;
             }
             break;
           default:
             break;
           }
       }
       break;
@@ -2717,17 +2717,17 @@ nsEventStatus nsPluginInstanceOwner::Pro
               break;
             default:
               break;
             }
 #endif
 
           // Information that could be obtained from pluginEvent but we may not
           // want to promise to provide:
-          event.subwindow = None;
+          event.subwindow = X11None;
           event.x = 0;
           event.y = 0;
           event.x_root = -1;
           event.y_root = -1;
           event.same_screen = False;
         }
       else
         {
@@ -2759,17 +2759,17 @@ nsEventStatus nsPluginInstanceOwner::Pro
   if (!pluginEvent.type) {
     return rv;
   }
 
   // Fill in (useless) generic event information.
   XAnyEvent& event = pluginEvent.xany;
   event.display = widget ?
     static_cast<Display*>(widget->GetNativeData(NS_NATIVE_DISPLAY)) : nullptr;
-  event.window = None; // not a real window
+  event.window = X11None; // not a real window
   // information lost:
   event.serial = 0;
   event.send_event = False;
 
   int16_t response = kNPEventNotHandled;
   mInstance->HandleEvent(&pluginEvent, &response, NS_PLUGIN_CALL_SAFE_TO_REENTER_GECKO);
   if (response == kNPEventHandled)
     rv = nsEventStatus_eConsumeNoDefault;
--- a/dom/plugins/base/nsPluginNativeWindowGtk.cpp
+++ b/dom/plugins/base/nsPluginNativeWindowGtk.cpp
@@ -212,17 +212,17 @@ nsresult nsPluginNativeWindowGtk::Create
     return NS_ERROR_FAILURE;
 
   mWsInfo.display = GDK_WINDOW_XDISPLAY(gdkWindow);
 #if (MOZ_WIDGET_GTK == 2)
   mWsInfo.colormap = GDK_COLORMAP_XCOLORMAP(gdk_drawable_get_colormap(gdkWindow));
   GdkVisual* gdkVisual = gdk_drawable_get_visual(gdkWindow);
   mWsInfo.depth = gdkVisual->depth;
 #else
-  mWsInfo.colormap = None;
+  mWsInfo.colormap = X11None;
   GdkVisual* gdkVisual = gdk_window_get_visual(gdkWindow);
   mWsInfo.depth = gdk_visual_get_depth(gdkVisual);
 #endif
   mWsInfo.visual = GDK_VISUAL_XVISUAL(gdkVisual);
     
   return NS_OK;
 }
 
--- a/dom/plugins/ipc/PluginInstanceChild.cpp
+++ b/dom/plugins/ipc/PluginInstanceChild.cpp
@@ -1277,17 +1277,17 @@ PluginInstanceChild::AnswerNPP_SetWindow
                 // workaround https://bugzilla.gnome.org/show_bug.cgi?id=607061
                 // See wrap_gtk_plug_embedded in PluginModuleChild.cpp.
                 g_object_set_data(G_OBJECT(socket_window),
                                   "moz-existed-before-set-window",
                                   GUINT_TO_POINTER(1));
             }
         }
 
-        if (aWindow.visualID != None
+        if (aWindow.visualID != X11None
             && gtk_check_version(2, 12, 10) != nullptr) { // older
             // Workaround for a bug in Gtk+ (prior to 2.12.10) where deleting
             // a foreign GdkColormap will also free the XColormap.
             // http://git.gnome.org/browse/gtk+/log/gdk/x11/gdkcolor-x11.c?id=GTK_2_12_10
             GdkVisual *gdkvisual = gdkx_visual_get(aWindow.visualID);
             GdkColormap *gdkcolor =
                 gdk_x11_colormap_foreign_new(gdkvisual, aWindow.colormap);
 
--- a/dom/plugins/test/testplugin/nptest_gtk2.cpp
+++ b/dom/plugins/test/testplugin/nptest_gtk2.cpp
@@ -76,33 +76,33 @@ pluginInstanceInit(InstanceData* instanc
 #ifdef MOZ_X11
   instanceData->platformData = static_cast<PlatformData*>
     (NPN_MemAlloc(sizeof(PlatformData)));
   if (!instanceData->platformData)
     return NPERR_OUT_OF_MEMORY_ERROR;
 
   instanceData->platformData->display = nullptr;
   instanceData->platformData->visual = nullptr;
-  instanceData->platformData->colormap = None;  
+  instanceData->platformData->colormap = X11None;
   instanceData->platformData->plug = nullptr;
 
   return NPERR_NO_ERROR;
 #else
   // we only support X11 here, since thats what the plugin system uses
   return NPERR_INCOMPATIBLE_VERSION_ERROR;
 #endif
 }
 
 void
 pluginInstanceShutdown(InstanceData* instanceData)
 {
   if (instanceData->hasWidget) {
     Window window = reinterpret_cast<XID>(instanceData->window.window);
 
-    if (window != None) {
+    if (window != X11None) {
       // This window XID should still be valid.
       // See bug 429604 and bug 454756.
       XWindowAttributes attributes;
       if (!XGetWindowAttributes(instanceData->platformData->display, window,
                                 &attributes))
         g_error("XGetWindowAttributes failed at plugin instance shutdown");
     }
   }
--- a/gfx/2d/BorrowedContext.h
+++ b/gfx/2d/BorrowedContext.h
@@ -6,16 +6,17 @@
 #ifndef _MOZILLA_GFX_BORROWED_CONTEXT_H
 #define _MOZILLA_GFX_BORROWED_CONTEXT_H
 
 #include "2D.h"
 
 #ifdef MOZ_X11
 #include <X11/extensions/Xrender.h>
 #include <X11/Xlib.h>
+#include "X11UndefineNone.h"
 #endif
 
 struct _cairo;
 typedef struct _cairo cairo_t;
 
 namespace mozilla {
 
 namespace gfx {
@@ -82,26 +83,26 @@ private:
  * to see if it succeeded. The DrawTarget should not be used while
  * the drawable is borrowed. */
 class BorrowedXlibDrawable
 {
 public:
   BorrowedXlibDrawable()
     : mDT(nullptr),
       mDisplay(nullptr),
-      mDrawable(None),
+      mDrawable(X11None),
       mScreen(nullptr),
       mVisual(nullptr),
       mXRenderFormat(nullptr)
   {}
 
   explicit BorrowedXlibDrawable(DrawTarget *aDT)
     : mDT(nullptr),
       mDisplay(nullptr),
-      mDrawable(None),
+      mDrawable(X11None),
       mScreen(nullptr),
       mVisual(nullptr),
       mXRenderFormat(nullptr)
   {
     Init(aDT);
   }
 
   // We can optionally Init after construction in
--- a/gfx/2d/DrawTargetCairo.cpp
+++ b/gfx/2d/DrawTargetCairo.cpp
@@ -2158,17 +2158,17 @@ DrawTargetCairo::Draw3DTransformedSurfac
   XRenderSetPictureTransform(display, srcPict, (XTransform*)&xform);
 
   Picture dstPict = XRenderCreatePicture(display,
                                          cairo_xlib_surface_get_drawable(xformSurf),
                                          cairo_xlib_surface_get_xrender_format(xformSurf),
                                          0, nullptr);
 
   XRenderComposite(display, PictOpSrc,
-                   srcPict, None, dstPict,
+                   srcPict, X11None, dstPict,
                    0, 0, 0, 0, 0, 0,
                    xformBounds.width, xformBounds.height);
 
   XRenderFreePicture(display, srcPict);
   XRenderFreePicture(display, dstPict);
 
   cairo_device_release(device);
   cairo_surface_mark_dirty(xformSurf);
@@ -2308,17 +2308,17 @@ BorrowedCairoContext::ReturnCairoContext
 
 #ifdef MOZ_X11
 bool
 BorrowedXlibDrawable::Init(DrawTarget* aDT)
 {
   MOZ_ASSERT(aDT, "Caller should check for nullptr");
   MOZ_ASSERT(!mDT, "Can't initialize twice!");
   mDT = aDT;
-  mDrawable = None;
+  mDrawable = X11None;
 
 #ifdef CAIRO_HAS_XLIB_SURFACE
   if (aDT->GetBackendType() != BackendType::CAIRO ||
       aDT->IsDualDrawTarget() ||
       aDT->IsTiledDrawTarget()) {
     return false;
   }
 
@@ -2351,15 +2351,15 @@ BorrowedXlibDrawable::Init(DrawTarget* a
 
 void
 BorrowedXlibDrawable::Finish()
 {
   DrawTargetCairo* cairoDT = static_cast<DrawTargetCairo*>(mDT);
   cairo_surface_t* surf = cairo_get_group_target(cairoDT->mContext);
   cairo_surface_mark_dirty(surf);
   if (mDrawable) {
-    mDrawable = None;
+    mDrawable = X11None;
   }
 }
 #endif
 
 } // namespace gfx
 } // namespace mozilla
--- a/gfx/gl/GLContextProviderGLX.cpp
+++ b/gfx/gl/GLContextProviderGLX.cpp
@@ -6,16 +6,17 @@
 #ifdef MOZ_WIDGET_GTK
 #include <gdk/gdk.h>
 #include <gdk/gdkx.h>
 #define GET_NATIVE_WINDOW(aWidget) GDK_WINDOW_XID((GdkWindow*) aWidget->GetNativeData(NS_NATIVE_WINDOW))
 #endif
 
 #include <X11/Xlib.h>
 #include <X11/Xutil.h>
+#include "X11UndefineNone.h"
 
 #include "mozilla/MathAlgorithms.h"
 #include "mozilla/StaticPtr.h"
 #include "mozilla/widget/CompositorWidget.h"
 #include "mozilla/Unused.h"
 
 #include "prenv.h"
 #include "GLContextProvider.h"
@@ -303,36 +304,36 @@ GLXLibrary::SupportsVideoSync()
 
     return mHasVideoSync;
 }
 
 GLXPixmap
 GLXLibrary::CreatePixmap(gfxASurface* aSurface)
 {
     if (!SupportsTextureFromPixmap(aSurface)) {
-        return None;
+        return X11None;
     }
 
     gfxXlibSurface* xs = static_cast<gfxXlibSurface*>(aSurface);
     const XRenderPictFormat* format = xs->XRenderFormat();
     if (!format || format->type != PictTypeDirect) {
-        return None;
+        return X11None;
     }
     const XRenderDirectFormat& direct = format->direct;
     int alphaSize = FloorLog2(direct.alphaMask + 1);
     NS_ASSERTION((1 << alphaSize) - 1 == direct.alphaMask,
                  "Unexpected render format with non-adjacent alpha bits");
 
     int attribs[] = { LOCAL_GLX_DOUBLEBUFFER, False,
                       LOCAL_GLX_DRAWABLE_TYPE, LOCAL_GLX_PIXMAP_BIT,
                       LOCAL_GLX_ALPHA_SIZE, alphaSize,
                       (alphaSize ? LOCAL_GLX_BIND_TO_TEXTURE_RGBA_EXT
                        : LOCAL_GLX_BIND_TO_TEXTURE_RGB_EXT), True,
                       LOCAL_GLX_RENDER_TYPE, LOCAL_GLX_RGBA_BIT,
-                      None };
+                      X11None };
 
     int numConfigs = 0;
     Display* display = xs->XDisplay();
     int xscreen = DefaultScreen(display);
 
     ScopedXFree<GLXFBConfig> cfgs(xChooseFBConfig(display,
                                                   xscreen,
                                                   attribs,
@@ -346,17 +347,17 @@ GLXLibrary::CreatePixmap(gfxASurface* aS
         static_cast<unsigned long>(direct.greenMask) << direct.green;
     unsigned long blueMask =
         static_cast<unsigned long>(direct.blueMask) << direct.blue;
     // This is true if the Pixmap has bits for alpha or unused bits.
     bool haveNonColorBits =
         ~(redMask | greenMask | blueMask) != -1UL << format->depth;
 
     for (int i = 0; i < numConfigs; i++) {
-        int id = None;
+        int id = X11None;
         sGLXLibrary.xGetFBConfigAttrib(display, cfgs[i], LOCAL_GLX_VISUAL_ID, &id);
         Visual* visual;
         int depth;
         FindVisualAndDepth(display, id, &visual, &depth);
         if (!visual ||
             visual->c_class != TrueColor ||
             visual->red_mask != redMask ||
             visual->green_mask != greenMask ||
@@ -419,24 +420,24 @@ GLXLibrary::CreatePixmap(gfxASurface* aS
         matchIndex = i;
         break;
     }
     if (matchIndex == -1) {
         // GLX can't handle A8 surfaces, so this is not really unexpected. The
         // caller should deal with this situation.
         NS_WARN_IF_FALSE(format->depth == 8,
                          "[GLX] Couldn't find a FBConfig matching Pixmap format");
-        return None;
+        return X11None;
     }
 
     int pixmapAttribs[] = { LOCAL_GLX_TEXTURE_TARGET_EXT, LOCAL_GLX_TEXTURE_2D_EXT,
                             LOCAL_GLX_TEXTURE_FORMAT_EXT,
                             (alphaSize ? LOCAL_GLX_TEXTURE_FORMAT_RGBA_EXT
                              : LOCAL_GLX_TEXTURE_FORMAT_RGB_EXT),
-                            None};
+                            X11None};
 
     GLXPixmap glxpixmap = xCreatePixmap(display,
                                         cfgs[matchIndex],
                                         xs->XDrawable(),
                                         pixmapAttribs);
 
     return glxpixmap;
 }
@@ -895,17 +896,17 @@ GLContextGLX::~GLContextGLX()
     if (!mOwnsContext) {
         return;
     }
 
     // see bug 659842 comment 76
 #ifdef DEBUG
     bool success =
 #endif
-    mGLX->xMakeCurrent(mDisplay, None, nullptr);
+    mGLX->xMakeCurrent(mDisplay, X11None, nullptr);
     MOZ_ASSERT(success,
                "glXMakeCurrent failed to release GL context before we call "
                "glXDestroyContext!");
 
     mGLX->xDestroyContext(mDisplay, mContext);
 
     if (mDeleteDrawable) {
         mGLX->xDestroyPixmap(mDisplay, mDrawable);
@@ -1237,17 +1238,17 @@ GLContextGLX::FindFBConfigForWindow(Disp
         return false;
     }
     const VisualID windowVisualID = XVisualIDFromVisual(windowAttrs.visual);
 #ifdef DEBUG
     printf("[GLX] window %lx has VisualID 0x%lx\n", window, windowVisualID);
 #endif
 
     for (int i = 0; i < numConfigs; i++) {
-        int visid = None;
+        int visid = X11None;
         sGLXLibrary.xGetFBConfigAttrib(display, cfgs[i], LOCAL_GLX_VISUAL_ID, &visid);
         if (!visid) {
             continue;
         }
         if (sGLXLibrary.IsATI()) {
             int depth;
             Visual* visual;
             FindVisualAndDepth(display, visid, &visual, &depth);
--- a/gfx/layers/ipc/ShadowLayerUtilsX11.cpp
+++ b/gfx/layers/ipc/ShadowLayerUtilsX11.cpp
@@ -6,16 +6,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "ShadowLayerUtilsX11.h"
 #include <X11/X.h>                      // for Drawable, XID
 #include <X11/Xlib.h>                   // for Display, Visual, etc
 #include <X11/extensions/Xrender.h>     // for XRenderPictFormat, etc
 #include <X11/extensions/render.h>      // for PictFormat
 #include "cairo-xlib.h"
+#include "X11UndefineNone.h"
 #include <stdint.h>                     // for uint32_t
 #include "GLDefs.h"                     // for GLenum
 #include "gfxPlatform.h"                // for gfxPlatform
 #include "gfxXlibSurface.h"             // for gfxXlibSurface
 #include "gfx2DGlue.h"                  // for Moz2D transistion helpers
 #include "mozilla/X11Util.h"            // for DefaultXDisplay, FinishX, etc
 #include "mozilla/gfx/Point.h"          // for IntSize
 #include "mozilla/layers/CompositableForwarder.h"
@@ -60,17 +61,17 @@ GetXRenderPictFormatFromId(Display* aDis
   tmplate.id = aFormatId;
   return XRenderFindFormat(aDisplay, PictFormatID, &tmplate, 0);
 }
 
 SurfaceDescriptorX11::SurfaceDescriptorX11(gfxXlibSurface* aSurf,
                                            bool aForwardGLX)
   : mId(aSurf->XDrawable())
   , mSize(aSurf->GetSize())
-  , mGLXPixmap(None)
+  , mGLXPixmap(X11None)
 {
   const XRenderPictFormat *pictFormat = aSurf->XRenderFormat();
   if (pictFormat) {
     mFormat = pictFormat->id;
   } else {
     mFormat = cairo_xlib_surface_get_visual(aSurf->CairoSurface())->visualid;
   }
 
@@ -81,17 +82,17 @@ SurfaceDescriptorX11::SurfaceDescriptorX
 #endif
 }
 
 SurfaceDescriptorX11::SurfaceDescriptorX11(Drawable aDrawable, XID aFormatID,
                                            const gfx::IntSize& aSize)
   : mId(aDrawable)
   , mFormat(aFormatID)
   , mSize(aSize)
-  , mGLXPixmap(None)
+  , mGLXPixmap(X11None)
 { }
 
 already_AddRefed<gfxXlibSurface>
 SurfaceDescriptorX11::OpenForeign() const
 {
   Display* display = DefaultXDisplay();
   Screen* screen = DefaultScreenOfDisplay(display);
 
new file mode 100644
--- /dev/null
+++ b/gfx/src/X11UndefineNone.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_GFX_X11UNDEFINENONE_H_
+#define MOZILLA_GFX_X11UNDEFINENONE_H_
+
+// The header <X11/X.h> defines "None" as a macro that expands to "0L".
+// This is terrible because many enumerations have an enumerator named "None".
+// To work around this, we undefine the macro "None", and define a replacement
+// macro named "X11None".
+// Include this header after including X11 headers, where necessary.
+#ifdef None
+#  undef None
+#  define X11None 0L
+// <X11/X.h> also defines "RevertToNone" as a macro that expands to "(int)None".
+// Since we are undefining "None", that stops working. To keep it working,
+// we undefine "RevertToNone" and redefine it in terms of "X11None".
+#  ifdef RevertToNone
+#    undef RevertToNone
+#    define RevertToNone (int)X11None
+#  endif
+#endif
+
+#endif /* MOZILLA_GFX_X11UNDEFINENONE_H_ */
--- a/gfx/src/X11Util.cpp
+++ b/gfx/src/X11Util.cpp
@@ -24,17 +24,17 @@ FindVisualAndDepth(Display* aDisplay, Vi
             if (visual->visualid == aVisualID) {
                 *aVisual = visual;
                 *aDepth = d_info->depth;
                 return;
             }
         }
     }
 
-    NS_ASSERTION(aVisualID == None, "VisualID not on Screen.");
+    NS_ASSERTION(aVisualID == X11None, "VisualID not on Screen.");
     *aVisual = nullptr;
     *aDepth = 0;
     return;
 }
 
 void
 FinishX(Display* aDisplay)
 {
--- a/gfx/src/X11Util.h
+++ b/gfx/src/X11Util.h
@@ -8,16 +8,17 @@
 #ifndef mozilla_X11Util_h
 #define mozilla_X11Util_h
 
 // Utilities common to all X clients, regardless of UI toolkit.
 
 #if defined(MOZ_WIDGET_GTK)
 #  include <gdk/gdk.h>
 #  include <gdk/gdkx.h>
+#  include "X11UndefineNone.h"
 #else
 #  error Unknown toolkit
 #endif
 
 #include <string.h>                     // for memset
 #include "mozilla/Scoped.h"             // for SCOPED_TEMPLATE
 
 namespace mozilla {
--- a/gfx/src/moz.build
+++ b/gfx/src/moz.build
@@ -34,16 +34,17 @@ EXPORTS += [
     'nsRegion.h',
     'nsRegionFwd.h',
     'nsRenderingContext.h',
     'nsSize.h',
     'nsThemeConstants.h',
     'nsTransform2D.h',
     'PingPongRegion.h',
     'RegionBuilder.h',
+    'X11UndefineNone.h'
 ]
 
 EXPORTS.mozilla += [
     'AppUnits.h',
     'ArrayView.h',
 ]
 
 EXPORTS.mozilla.gfx += [
--- a/gfx/thebes/gfxXlibSurface.cpp
+++ b/gfx/thebes/gfxXlibSurface.cpp
@@ -20,59 +20,59 @@
 #include "mozilla/CheckedInt.h"
 
 using namespace mozilla;
 using namespace mozilla::gfx;
 
 gfxXlibSurface::gfxXlibSurface(Display *dpy, Drawable drawable, Visual *visual)
     : mPixmapTaken(false), mDisplay(dpy), mDrawable(drawable)
 #if defined(GL_PROVIDER_GLX)
-    , mGLXPixmap(None)
+    , mGLXPixmap(X11None)
 #endif
 {
     const gfx::IntSize size = DoSizeQuery();
     cairo_surface_t *surf = cairo_xlib_surface_create(dpy, drawable, visual, size.width, size.height);
     Init(surf);
 }
 
 gfxXlibSurface::gfxXlibSurface(Display *dpy, Drawable drawable, Visual *visual, const gfx::IntSize& size)
     : mPixmapTaken(false), mDisplay(dpy), mDrawable(drawable)
 #if defined(GL_PROVIDER_GLX)
-    , mGLXPixmap(None)
+    , mGLXPixmap(X11None)
 #endif
 {
     NS_ASSERTION(Factory::CheckSurfaceSize(size, XLIB_IMAGE_SIDE_SIZE_LIMIT),
                  "Bad size");
 
     cairo_surface_t *surf = cairo_xlib_surface_create(dpy, drawable, visual, size.width, size.height);
     Init(surf);
 }
 
 gfxXlibSurface::gfxXlibSurface(Screen *screen, Drawable drawable, XRenderPictFormat *format,
                                const gfx::IntSize& size)
     : mPixmapTaken(false), mDisplay(DisplayOfScreen(screen)),
       mDrawable(drawable)
 #if defined(GL_PROVIDER_GLX)
-      , mGLXPixmap(None)
+      , mGLXPixmap(X11None)
 #endif
 {
     NS_ASSERTION(Factory::CheckSurfaceSize(size, XLIB_IMAGE_SIDE_SIZE_LIMIT),
                  "Bad Size");
 
     cairo_surface_t *surf =
         cairo_xlib_surface_create_with_xrender_format(mDisplay, drawable,
                                                       screen, format,
                                                       size.width, size.height);
     Init(surf);
 }
 
 gfxXlibSurface::gfxXlibSurface(cairo_surface_t *csurf)
     : mPixmapTaken(false)
 #if defined(GL_PROVIDER_GLX)
-      , mGLXPixmap(None)
+      , mGLXPixmap(X11None)
 #endif
 {
     NS_PRECONDITION(cairo_surface_status(csurf) == 0,
                     "Not expecting an error surface");
 
     mDrawable = cairo_xlib_surface_get_drawable(csurf);
     mDisplay = cairo_xlib_surface_get_display(csurf);
 
@@ -92,19 +92,19 @@ gfxXlibSurface::~gfxXlibSurface()
     }
 }
 
 static Drawable
 CreatePixmap(Screen *screen, const gfx::IntSize& size, unsigned int depth,
              Drawable relatedDrawable)
 {
     if (!Factory::CheckSurfaceSize(size, XLIB_IMAGE_SIDE_SIZE_LIMIT))
-        return None;
+        return X11None;
 
-    if (relatedDrawable == None) {
+    if (relatedDrawable == X11None) {
         relatedDrawable = RootWindowOfScreen(screen);
     }
     Display *dpy = DisplayOfScreen(screen);
     // X gives us a fatal error if we try to create a pixmap of width
     // or height 0
     return XCreatePixmap(dpy, relatedDrawable,
                          std::max(1, size.width), std::max(1, size.height),
                          depth);
@@ -269,17 +269,17 @@ gfxXlibSurface::CreateSimilarSurface(gfx
 }
 
 void
 gfxXlibSurface::Finish()
 {
 #if defined(GL_PROVIDER_GLX)
     if (mPixmapTaken && mGLXPixmap) {
         gl::sGLXLibrary.DestroyPixmap(mDisplay, mGLXPixmap);
-        mGLXPixmap = None;
+        mGLXPixmap = X11None;
     }
 #endif
     gfxASurface::Finish();
 }
 
 const gfx::IntSize
 gfxXlibSurface::GetSize() const
 {
--- a/gfx/thebes/gfxXlibSurface.h
+++ b/gfx/thebes/gfxXlibSurface.h
@@ -5,16 +5,17 @@
 
 #ifndef GFX_XLIBSURFACE_H
 #define GFX_XLIBSURFACE_H
 
 #include "gfxASurface.h"
 
 #include <X11/extensions/Xrender.h>
 #include <X11/Xlib.h>
+#include "X11UndefineNone.h"
 
 #if defined(GL_PROVIDER_GLX)
 #include "GLXLibrary.h"
 #endif
 
 #include "nsSize.h"
 
 // Although the dimension parameters in the xCreatePixmapReq wire protocol are
@@ -41,23 +42,23 @@ public:
     explicit gfxXlibSurface(cairo_surface_t *csurf);
 
     // create a new Pixmap and wrapper surface.
     // |relatedDrawable| provides a hint to the server for determining whether
     // the pixmap should be in video or system memory.  It must be on
     // |screen| (if specified).
     static already_AddRefed<gfxXlibSurface>
     Create(Screen *screen, Visual *visual, const mozilla::gfx::IntSize& size,
-           Drawable relatedDrawable = None);
+           Drawable relatedDrawable = X11None);
     static cairo_surface_t *
     CreateCairoSurface(Screen *screen, Visual *visual, const mozilla::gfx::IntSize& size,
-                       Drawable relatedDrawable = None);
+                       Drawable relatedDrawable = X11None);
     static already_AddRefed<gfxXlibSurface>
     Create(Screen* screen, XRenderPictFormat *format, const mozilla::gfx::IntSize& size,
-           Drawable relatedDrawable = None);
+           Drawable relatedDrawable = X11None);
 
     virtual ~gfxXlibSurface();
 
     virtual already_AddRefed<gfxASurface>
     CreateSimilarSurface(gfxContentType aType,
                          const mozilla::gfx::IntSize& aSize) override;
     virtual void Finish() override;
 
--- a/layout/style/ServoStyleSet.cpp
+++ b/layout/style/ServoStyleSet.cpp
@@ -477,17 +477,19 @@ ServoStyleSet::StyleDocument(bool aLeave
     doc->UnsetHasDirtyDescendantsForServo();
   }
 }
 
 void
 ServoStyleSet::StyleNewSubtree(nsIContent* aContent)
 {
   MOZ_ASSERT(aContent->IsDirtyForServo());
-  Servo_RestyleSubtree(aContent, mRawSet.get());
+  if (aContent->IsElement() || aContent->IsNodeOfType(nsINode::eTEXT)) {
+    Servo_RestyleSubtree(aContent, mRawSet.get());
+  }
   ClearDirtyBits(aContent);
 }
 
 void
 ServoStyleSet::StyleNewChildren(nsIContent* aParent)
 {
   MOZ_ASSERT(aParent->HasDirtyDescendantsForServo());
   Servo_RestyleSubtree(aParent, mRawSet.get());
--- a/media/mtransport/test/ice_unittest.cpp
+++ b/media/mtransport/test/ice_unittest.cpp
@@ -758,17 +758,17 @@ class IceTestPeer : public sigslot::has_
         RefPtr<NrIceMediaStream> aStream = ice_ctx_->ctx()->GetStream(i);
         if (!aStream || aStream->HasParsedAttributes()) {
           continue;
         }
         std::vector<std::string> candidates =
             remote->GetCandidates(i);
 
         for (size_t j=0; j<candidates.size(); ++j) {
-          std::cerr << name_ << " Candidate: " + candidates[j] << std::endl;
+          std::cerr << name_ << " Adding remote candidate: " + candidates[j] << std::endl;
         }
         res = aStream->ParseAttributes(candidates);
         ASSERT_TRUE(NS_SUCCEEDED(res));
       }
     } else {
       // Parse empty attributes and then trickle them out later
       for (size_t i=0; i<ice_ctx_->ctx()->GetStreamCount(); ++i) {
         RefPtr<NrIceMediaStream> aStream = ice_ctx_->ctx()->GetStream(i);
--- a/media/mtransport/third_party/nICEr/src/ice/ice_candidate_pair.c
+++ b/media/mtransport/third_party/nICEr/src/ice/ice_candidate_pair.c
@@ -176,17 +176,18 @@ int nr_ice_candidate_pair_unfreeze(nr_ic
     nr_ice_candidate_pair_set_state(pctx,pair,NR_ICE_PAIR_STATE_WAITING);
 
     return(0);
   }
 
 static void nr_ice_candidate_pair_stun_cb(NR_SOCKET s, int how, void *cb_arg)
   {
     int r,_status;
-    nr_ice_cand_pair *pair=cb_arg,*orig_pair;
+    nr_ice_cand_pair *pair=cb_arg;
+    nr_ice_cand_pair *actual_pair=0;
     nr_ice_candidate *cand=0;
     nr_stun_message *sres;
     nr_transport_addr *request_src;
     nr_transport_addr *request_dst;
     nr_transport_addr *response_src;
     nr_transport_addr response_dst;
     nr_stun_message_attribute *attr;
 
@@ -251,58 +252,65 @@ static void nr_ice_candidate_pair_stun_c
           nr_ice_candidate_pair_set_state(pair->pctx,pair,NR_ICE_PAIR_STATE_SUCCEEDED);
         }
         else if(pair->stun_client->state == NR_STUN_CLIENT_STATE_DONE) {
           /* OK, this didn't correspond to a pair on the check list, but
              it probably matches one of our candidates */
 
           cand=TAILQ_FIRST(&pair->local->component->candidates);
           while(cand){
-            if(!nr_transport_addr_cmp(&cand->addr,&pair->stun_client->results.ice_binding_response.mapped_addr,NR_TRANSPORT_ADDR_CMP_MODE_ALL))
+            if(!nr_transport_addr_cmp(&cand->addr,&pair->stun_client->results.ice_binding_response.mapped_addr,NR_TRANSPORT_ADDR_CMP_MODE_ALL)) {
+              r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s): found pre-existing local candidate of type %d for mapped address %s", pair->pctx->label,cand->type,cand->addr.as_string);
+              assert(cand->type != HOST);
               break;
+            }
 
             cand=TAILQ_NEXT(cand,entry_comp);
           }
 
-          /* OK, nothing found, must be peer reflexive */
           if(!cand) {
+            /* OK, nothing found, must be a new peer reflexive */
             if (pair->pctx->ctx->flags & NR_ICE_CTX_FLAGS_RELAY_ONLY) {
               /* Any STUN response with a reflexive address in it is unwanted
                  when we'll send on relay only. Bail since cand is used below. */
               goto done;
             }
             if(r=nr_ice_candidate_create(pair->pctx->ctx,
               pair->local->component,pair->local->isock,pair->local->osock,
               PEER_REFLEXIVE,pair->local->tcp_type,0,pair->local->component->component_id,&cand))
               ABORT(r);
             if(r=nr_transport_addr_copy(&cand->addr,&pair->stun_client->results.ice_binding_response.mapped_addr))
               ABORT(r);
             cand->state=NR_ICE_CAND_STATE_INITIALIZED;
             TAILQ_INSERT_TAIL(&pair->local->component->candidates,cand,entry_comp);
+          } else {
+            /* Check if we have a pair for this candidate already. */
+            if(r=nr_ice_media_stream_find_pair(pair->remote->stream, cand, pair->remote, &actual_pair)) {
+              r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s): no pair exists for %s and %s", pair->pctx->label,cand->addr.as_string, pair->remote->addr.as_string);
+            }
           }
 
-          /* Note: we stomp the existing pair! */
-          orig_pair=pair;
-          if(r=nr_ice_candidate_pair_create(pair->pctx,cand,pair->remote,
-            &pair))
-            ABORT(r);
+          if(!actual_pair) {
+            if(r=nr_ice_candidate_pair_create(pair->pctx,cand,pair->remote, &actual_pair))
+              ABORT(r);
 
-          nr_ice_candidate_pair_set_state(pair->pctx,pair,NR_ICE_PAIR_STATE_SUCCEEDED);
+            if(r=nr_ice_component_insert_pair(actual_pair->remote->component,actual_pair))
+              ABORT(r);
 
-          if(r=nr_ice_component_insert_pair(pair->remote->component,pair))
-            ABORT(r);
+            /* If the original pair was nominated, make us nominated too. */
+            if(pair->peer_nominated)
+              actual_pair->peer_nominated=1;
 
-          /* If the original pair was nominated, make us nominated,
-             since we replace him*/
-          if(orig_pair->peer_nominated)
-            pair->peer_nominated=1;
+            /* Now mark the orig pair failed */
+            nr_ice_candidate_pair_set_state(pair->pctx,pair,NR_ICE_PAIR_STATE_FAILED);
+          }
 
-
-          /* Now mark the orig pair failed */
-          nr_ice_candidate_pair_set_state(orig_pair->pctx,orig_pair,NR_ICE_PAIR_STATE_FAILED);
+          assert(actual_pair);
+          nr_ice_candidate_pair_set_state(actual_pair->pctx,actual_pair,NR_ICE_PAIR_STATE_SUCCEEDED);
+          pair=actual_pair;
 
         }
 
         /* Should we set nominated? */
         if(pair->pctx->controlling){
           if(pair->pctx->ctx->flags & NR_ICE_CTX_FLAGS_AGGRESSIVE_NOMINATION)
             pair->nominated=1;
         }
--- a/media/mtransport/third_party/nICEr/src/ice/ice_media_stream.c
+++ b/media/mtransport/third_party/nICEr/src/ice/ice_media_stream.c
@@ -908,8 +908,26 @@ void nr_ice_media_stream_role_change(nr_
     /* Re-insert into the check list */
     TAILQ_FOREACH_SAFE(pair,&old_checklist,check_queue_entry,temp_pair) {
       TAILQ_REMOVE(&old_checklist,pair,check_queue_entry);
       nr_ice_candidate_pair_role_change(pair);
       nr_ice_candidate_pair_insert(&stream->check_list,pair);
     }
   }
 
+int nr_ice_media_stream_find_pair(nr_ice_media_stream *str, nr_ice_candidate *lcand, nr_ice_candidate *rcand, nr_ice_cand_pair **pair)
+  {
+    nr_ice_cand_pair_head *head = &str->check_list;
+    nr_ice_cand_pair *c1;
+
+    c1=TAILQ_FIRST(head);
+    while(c1){
+      if(c1->local == lcand &&
+         c1->remote == rcand) {
+        *pair=c1;
+        return(0);
+      }
+
+      c1=TAILQ_NEXT(c1,check_queue_entry);
+    }
+
+    return(R_NOT_FOUND);
+  }
--- a/media/mtransport/third_party/nICEr/src/ice/ice_media_stream.h
+++ b/media/mtransport/third_party/nICEr/src/ice/ice_media_stream.h
@@ -87,16 +87,17 @@ int nr_ice_media_stream_unfreeze_pairs_f
 int nr_ice_media_stream_dump_state(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream,FILE *out);
 int nr_ice_media_stream_component_nominated(nr_ice_media_stream *stream,nr_ice_component *component);
 int nr_ice_media_stream_component_failed(nr_ice_media_stream *stream,nr_ice_component *component);
 int nr_ice_media_stream_set_state(nr_ice_media_stream *str, int state);
 int nr_ice_media_stream_get_best_candidate(nr_ice_media_stream *str, int component, nr_ice_candidate **candp);
 int nr_ice_media_stream_send(nr_ice_peer_ctx *pctx, nr_ice_media_stream *str, int component, UCHAR *data, int len);
 int nr_ice_media_stream_get_active(nr_ice_peer_ctx *pctx, nr_ice_media_stream *str, int component, nr_ice_candidate **local, nr_ice_candidate **remote);
 int nr_ice_media_stream_find_component(nr_ice_media_stream *str, int comp_id, nr_ice_component **compp);
+int nr_ice_media_stream_find_pair(nr_ice_media_stream *str, nr_ice_candidate *local, nr_ice_candidate *remote, nr_ice_cand_pair **pair);
 int nr_ice_media_stream_addrs(nr_ice_peer_ctx *pctx, nr_ice_media_stream *str, int component, nr_transport_addr *local, nr_transport_addr *remote);
 int
 nr_ice_peer_ctx_parse_media_stream_attribute(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream, char *attr);
 int nr_ice_media_stream_get_consent_status(nr_ice_media_stream *stream, int component_id, int *can_send, struct timeval *ts);
 int nr_ice_media_stream_disable_component(nr_ice_media_stream *stream, int component_id);
 int nr_ice_media_stream_pair_new_trickle_candidate(nr_ice_peer_ctx *pctx, nr_ice_media_stream *pstream, nr_ice_candidate *cand);
 void nr_ice_media_stream_role_change(nr_ice_media_stream *stream);
 
--- a/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/IntentHelper.java
@@ -7,17 +7,16 @@ package org.mozilla.gecko;
 
 import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.util.ActivityResultHandler;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.JSONUtils;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.WebActivityMapper;
 import org.mozilla.gecko.widget.ExternalIntentDuringPrivateBrowsingPromptFragment;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.annotation.TargetApi;
 import android.content.Context;
@@ -43,17 +42,16 @@ import java.util.Locale;
 public final class IntentHelper implements GeckoEventListener,
                                            NativeEventListener {
 
     private static final String LOGTAG = "GeckoIntentHelper";
     private static final String[] EVENTS = {
         "Intent:GetHandlers",
         "Intent:Open",
         "Intent:OpenForResult",
-        "WebActivity:Open"
     };
 
     private static final String[] NATIVE_EVENTS = {
         "Intent:OpenNoHandler",
     };
 
     // via http://developer.android.com/distribute/tools/promote/linking.html
     private static String MARKET_INTENT_URI_PACKAGE_PREFIX = "market://details?id=";
@@ -411,18 +409,16 @@ public final class IntentHelper implemen
     public void handleMessage(String event, JSONObject message) {
         try {
             if (event.equals("Intent:GetHandlers")) {
                 getHandlers(message);
             } else if (event.equals("Intent:Open")) {
                 open(message);
             } else if (event.equals("Intent:OpenForResult")) {
                 openForResult(message);
-            } else if (event.equals("WebActivity:Open")) {
-                openWebActivity(message);
             }
         } catch (JSONException e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
     private void getHandlers(JSONObject message) throws JSONException {
         final Intent intent = getOpenURIIntent(activity,
@@ -567,21 +563,16 @@ public final class IntentHelper implemen
      * @param encodedUri The encoded uri. While the page does not open correctly without specifying
      *                   a uri parameter, it happily accepts the empty String so this argument may
      *                   be the empty String.
      */
     private String getUnknownProtocolErrorPageUri(final String encodedUri) {
         return UNKNOWN_PROTOCOL_URI_PREFIX + encodedUri;
     }
 
-    private void openWebActivity(JSONObject message) throws JSONException {
-        final Intent intent = WebActivityMapper.getIntentForWebActivity(message.getJSONObject("activity"));
-        ActivityHandlerHelper.startIntentForActivity(activity, intent, new ResultHandler(message));
-    }
-
     private static class ResultHandler implements ActivityResultHandler {
         private final JSONObject message;
 
         public ResultHandler(JSONObject message) {
             this.message = message;
         }
 
         @Override
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -134,17 +134,16 @@ gujar.sources += [geckoview_source_dir +
     'util/PrefUtils.java',
     'util/ProxySelector.java',
     'util/RawResource.java',
     'util/StringUtils.java',
     'util/ThreadUtils.java',
     'util/UIAsyncTask.java',
     'util/UUIDUtil.java',
     'util/WeakReferenceHandler.java',
-    'util/WebActivityMapper.java',
     'util/WindowUtils.java',
 ]]
 gujar.extra_jars = [
     CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'],
     CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'],
     CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'],
     'constants.jar',
     'gecko-mozglue.jar',
deleted file mode 100644
--- a/mobile/android/components/AndroidActivitiesGlue.js
+++ /dev/null
@@ -1,31 +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"
-
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Messaging.jsm");
-
-function ActivitiesGlue() { }
-
-ActivitiesGlue.prototype = {
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIActivityUIGlue]),
-  classID: Components.ID("{e4deb5f6-d5e3-4fce-bc53-901dd9951c48}"),
-
-  // Ignore aActivities results on Android, go straight to Android intents.
-  chooseActivity: function ap_chooseActivity(aOptions, aActivities, aCallback) {
-    Messaging.sendRequestForResult({
-      type: "WebActivity:Open",
-      activity: { name: aOptions.name, data: aOptions.data }
-    }).then((result) => {
-      aCallback.handleEvent(Ci.nsIActivityUIGlueCallback.NATIVE_ACTIVITY, result);
-    });
-  }
-};
-
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ActivitiesGlue]);
--- a/mobile/android/components/MobileComponents.manifest
+++ b/mobile/android/components/MobileComponents.manifest
@@ -110,17 +110,13 @@ component {a78d7e59-b558-4321-a3d6-dffe2
 contract @mozilla.org/snippets;1 {a78d7e59-b558-4321-a3d6-dffe2f1e76dd}
 category browser-delayed-startup-finished Snippets @mozilla.org/snippets;1
 category update-timer Snippets @mozilla.org/snippets;1,getService,snippets-update-timer,browser.snippets.updateInterval,86400
 
 # ColorPicker.js
 component {430b987f-bb9f-46a3-99a5-241749220b29} ColorPicker.js
 contract @mozilla.org/colorpicker;1 {430b987f-bb9f-46a3-99a5-241749220b29}
 
-# AndroidActivitiesGlue.js
-component {e4deb5f6-d5e3-4fce-bc53-901dd9951c48} AndroidActivitiesGlue.js
-contract @mozilla.org/dom/activities/ui-glue;1 {e4deb5f6-d5e3-4fce-bc53-901dd9951c48}
-
 # PersistentNotificationHandler.js
 component {75390fe7-f8a3-423a-b3b1-258d7eabed40} PersistentNotificationHandler.js
 contract @mozilla.org/persistent-notification-handler;1 {75390fe7-f8a3-423a-b3b1-258d7eabed40}
 category persistent-notification-click PersistentNotificationHandler @mozilla.org/persistent-notification-handler;1
 category persistent-notification-close PersistentNotificationHandler @mozilla.org/persistent-notification-handler;1
--- a/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html
@@ -13,17 +13,17 @@
 <script type="text/javascript">
 "use strict";
 
 let dataURI = "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
 
 let image = atob(dataURI);
 const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
 
-function backgroundScript() {
+function background() {
   browser.test.assertTrue("pageAction" in browser, "Namespace 'pageAction' exists in browser");
   browser.test.assertTrue("show" in browser.pageAction, "API method 'show' exists in browser.pageAction");
 
   // TODO: Use the Tabs API to obtain the tab ids for showing pageActions.
   let tabId = 1;
   browser.test.onMessage.addListener(msg => {
     if (msg === "pageAction-show") {
       browser.pageAction.show(tabId).then(() => {
@@ -41,17 +41,17 @@ function backgroundScript() {
     browser.test.sendMessage("page-action-clicked");
   });
 
   browser.test.sendMessage("ready");
 }
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension({
-    background: "(" + backgroundScript.toString() + ")()",
+    background,
     manifest: {
       "name": "PageAction Extension",
       "page_action": {
         "default_title": "Page Action",
         "default_icon": {
           "18": "extension.png",
         },
       },
--- a/mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html
@@ -16,17 +16,17 @@
 Cu.import("resource://gre/modules/Services.jsm");
 
 let dataURI = "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
 
 let image = atob(dataURI);
 const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
 
 add_task(function* test_contentscript() {
-  function backgroundScript() {
+  function background() {
     // TODO: Use the Tabs API to obtain the tab ids for showing pageActions.
     let tabId = 1;
     let onClickedListenerEnabled = false;
 
     browser.test.onMessage.addListener((msg, details) => {
       if (msg === "page-action-show") {
         // TODO: switch to using .show(tabId).then(...) once bug 1270742 lands.
         browser.pageAction.show(tabId).then(() => {
@@ -66,33 +66,33 @@ add_task(function* test_contentscript() 
         if (details.location == location.href) {
           window.close();
         }
       }
     });
   }
 
   let extension = ExtensionTestUtils.loadExtension({
-    background: `(${backgroundScript}())`,
+    background,
     manifest: {
       "name": "PageAction Extension",
       "page_action": {
         "default_title": "Page Action",
         "default_popup": "default.html",
         "default_icon": {
           "18": "extension.png",
         },
       },
     },
     files: {
-      "default.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
+      "default.html": `<html><head><meta charset="utf-8"><script src="popup.js"><\/script></head></html>`,
       "extension.png": IMAGE_ARRAYBUFFER,
-      "a.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
-      "b.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
-      "popup.js": `(${popupScript})()`,
+      "a.html": `<html><head><meta charset="utf-8"><script src="popup.js"><\/script></head></html>`,
+      "b.html": `<html><head><meta charset="utf-8"><script src="popup.js"><\/script></head></html>`,
+      "popup.js": popupScript,
     },
   });
 
   let tabClosedPromise = () => {
     return new Promise(resolve => {
       let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
       let BrowserApp = chromeWin.BrowserApp;
 
--- a/mobile/android/components/moz.build
+++ b/mobile/android/components/moz.build
@@ -8,17 +8,16 @@ XPIDL_SOURCES += [
     'SessionStore.idl',
 ]
 
 XPIDL_MODULE = 'MobileComponents'
 
 EXTRA_COMPONENTS += [
     'AboutRedirector.js',
     'AddonUpdateService.js',
-    'AndroidActivitiesGlue.js',
     'BlocklistPrompt.js',
     'BrowserCLH.js',
     'ColorPicker.js',
     'ContentDispatchChooser.js',
     'ContentPermissionPrompt.js',
     'DirectoryProvider.js',
     'FilePicker.js',
     'FxAccountsPush.js',
deleted file mode 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/WebActivityMapper.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * 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/. */
-
-package org.mozilla.gecko.util;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Environment;
-import android.provider.MediaStore;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-
-public final class WebActivityMapper {
-    private static final String LOGTAG = "Gecko";
-
-    private static final Map<String, WebActivityMapping> activityMap = new HashMap<String, WebActivityMapping>();
-    static {
-        activityMap.put("dial", new DialMapping());
-        activityMap.put("open", new OpenMapping());
-        activityMap.put("pick", new PickMapping());
-        activityMap.put("send", new SendMapping());
-        activityMap.put("view", new ViewMapping());
-        activityMap.put("record", new RecordMapping());
-    };
-
-    private static abstract class WebActivityMapping {
-        protected JSONObject mData;
-
-        public void setData(JSONObject data) {
-            mData = data;
-        }
-
-        // Cannot return null
-        public abstract String getAction();
-
-        public String getMime() throws JSONException {
-            return null;
-        }
-
-        public String getUri() throws JSONException {
-            return null;
-        }
-
-        public void putExtras(Intent intent) throws JSONException {}
-    }
-
-    /**
-     * Provides useful defaults for mime type and uri.
-     */
-    private static abstract class BaseMapping extends WebActivityMapping {
-        /**
-         * If 'type' is present in data object, uses the value as the MIME type.
-         */
-        @Override
-        public String getMime() throws JSONException {
-            return mData.optString("type", null);
-        }
-
-        /**
-         * If 'uri' or 'url' is present in data object, uses the respective value as the Uri.
-         */
-        @Override
-        public String getUri() throws JSONException {
-            // Will return uri or url if present.
-            String uri = mData.optString("uri", null);
-            return uri != null ? uri : mData.optString("url", null);
-        }
-    }
-
-    public static Intent getIntentForWebActivity(JSONObject message) throws JSONException {
-        final String name = message.getString("name").toLowerCase();
-        final JSONObject data = message.getJSONObject("data");
-
-        Log.w(LOGTAG, "Activity is: " + name);
-        final WebActivityMapping mapping = activityMap.get(name);
-        if (mapping == null) {
-            Log.w(LOGTAG, "No mapping found!");
-            return null;
-        }
-
-        mapping.setData(data);
-
-        final Intent intent = new Intent(mapping.getAction());
-
-        final String mime = mapping.getMime();
-        if (!TextUtils.isEmpty(mime)) {
-            intent.setType(mime);
-        }
-
-        final String uri = mapping.getUri();
-        if (!TextUtils.isEmpty(uri)) {
-            intent.setData(Uri.parse(uri));
-        }
-
-        mapping.putExtras(intent);
-
-        return intent;
-    }
-
-    private static class DialMapping extends WebActivityMapping {
-        @Override
-        public String getAction() {
-            return Intent.ACTION_DIAL;
-        }
-
-        @Override
-        public String getUri() throws JSONException {
-            return "tel:" + mData.getString("number");
-        }
-    }
-
-    private static class OpenMapping extends BaseMapping {
-        @Override
-        public String getAction() {
-            return Intent.ACTION_VIEW;
-        }
-    }
-
-    private static class PickMapping extends BaseMapping {
-        @Override
-        public String getAction() {
-            return Intent.ACTION_GET_CONTENT;
-        }
-
-        @Override
-        public String getMime() throws JSONException {
-            // bug 1007112 - pick action needs a mimetype to work
-            String mime = mData.optString("type", null);
-            return !TextUtils.isEmpty(mime) ? mime : "*/*";
-        }
-    }
-
-    private static class SendMapping extends BaseMapping {
-        @Override
-        public String getAction() {
-            return Intent.ACTION_SEND;
-        }
-
-        @Override
-        public void putExtras(Intent intent) throws JSONException {
-            optPutExtra("text", Intent.EXTRA_TEXT, intent);
-            optPutExtra("html_text", Intent.EXTRA_HTML_TEXT, intent);
-            optPutExtra("stream", Intent.EXTRA_STREAM, intent);
-        }
-
-        private void optPutExtra(String key, String extraName, Intent intent) {
-            final String extraValue = mData.optString(key);
-            if (!TextUtils.isEmpty(extraValue)) {
-                intent.putExtra(extraName, extraValue);
-            }
-        }
-    }
-
-    private static class ViewMapping extends BaseMapping {
-        @Override
-        public String getAction() {
-            return Intent.ACTION_VIEW;
-        }
-
-        @Override
-        public String getMime() {
-            // MozActivity adds a type 'url' here, we don't want to set the MIME to 'url'.
-            String type = mData.optString("type", null);
-            if ("url".equals(type) || "uri".equals(type)) {
-                return null;
-            } else {
-                return type;
-            }
-        }
-    }
-
-    private static class RecordMapping extends WebActivityMapping {
-        @Override
-        public String getAction() {
-            String type = mData.optString("type", null);
-            if ("photos".equals(type)) {
-                return "android.media.action.IMAGE_CAPTURE";
-            } else if ("videos".equals(type)) {
-                return "android.media.action.VIDEO_CAPTURE";
-            }
-            return null;
-        }
-
-        // Add an extra to specify where to save the picture/video.
-        @Override
-        public void putExtras(Intent intent) {
-            final String action = getAction();
-
-            final String dirType = action == "android.media.action.IMAGE_CAPTURE"
-                ? Environment.DIRECTORY_PICTURES
-                : Environment.DIRECTORY_MOVIES;
-
-            final String ext = action == "android.media.action.IMAGE_CAPTURE"
-                ? ".jpg"
-                : ".mp4";
-
-            File destDir = Environment.getExternalStoragePublicDirectory(dirType);
-
-            try {
-                File dest = File.createTempFile(
-                    "capture", /* prefix */
-                    ext,       /* suffix */
-                    destDir    /* directory */
-                );
-                intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(dest));
-            } catch (Exception e) {
-                Log.w(LOGTAG, "Failed to add extra for " + action + " : " + e);
-            }
-        }
-    }
-}
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -109,27 +109,25 @@
 @BINPATH@/components/content_geckomediaplugins.xpt
 @BINPATH@/components/content_html.xpt
 @BINPATH@/components/content_webrtc.xpt
 @BINPATH@/components/content_xslt.xpt
 @BINPATH@/components/cookie.xpt
 @BINPATH@/components/directory.xpt
 @BINPATH@/components/docshell.xpt
 @BINPATH@/components/dom.xpt
-@BINPATH@/components/dom_activities.xpt
 @BINPATH@/components/dom_apps.xpt
 @BINPATH@/components/dom_newapps.xpt
 @BINPATH@/components/dom_base.xpt
 @BINPATH@/components/dom_canvas.xpt
 @BINPATH@/components/dom_core.xpt
 @BINPATH@/components/dom_css.xpt
 @BINPATH@/components/dom_events.xpt
 @BINPATH@/components/dom_geolocation.xpt
 @BINPATH@/components/dom_media.xpt
-@BINPATH@/components/dom_messages.xpt
 @BINPATH@/components/dom_network.xpt
 @BINPATH@/components/dom_notification.xpt
 @BINPATH@/components/dom_html.xpt
 @BINPATH@/components/dom_offline.xpt
 @BINPATH@/components/dom_json.xpt
 @BINPATH@/components/dom_power.xpt
 #ifdef MOZ_ANDROID_GCM
 @BINPATH@/components/dom_push.xpt
@@ -367,23 +365,16 @@
 @BINPATH@/components/XULStore.manifest
 @BINPATH@/components/Webapps.js
 @BINPATH@/components/Webapps.manifest
 @BINPATH@/components/AppsService.js
 @BINPATH@/components/AppsService.manifest
 @BINPATH@/components/htmlMenuBuilder.js
 @BINPATH@/components/htmlMenuBuilder.manifest
 
-@BINPATH@/components/Activities.manifest
-@BINPATH@/components/AndroidActivitiesGlue.js
-@BINPATH@/components/ActivityProxy.js
-@BINPATH@/components/ActivityRequestHandler.js
-@BINPATH@/components/ActivityWrapper.js
-@BINPATH@/components/ActivityMessageConfigurator.js
-
 @BINPATH@/components/SystemMessageInternal.js
 @BINPATH@/components/SystemMessageManager.js
 @BINPATH@/components/SystemMessageCache.js
 @BINPATH@/components/SystemMessageManager.manifest
 
 @BINPATH@/components/InstallPackagedWebapp.manifest
 @BINPATH@/components/InstallPackagedWebapp.js
 
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient.java
@@ -1,21 +1,24 @@
 /* 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/. */
 
 package org.mozilla.gecko.background.fxa;
 
 import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.RecoveryEmailStatusResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
-import org.mozilla.gecko.background.fxa.FxAccountClient20.RecoveryEmailStatusResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.TwoKeys;
 import org.mozilla.gecko.fxa.FxAccountDevice;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 
+import java.util.List;
+
 public interface FxAccountClient {
   public void accountStatus(String uid, RequestDelegate<AccountStatusResponse> requestDelegate);
   public void recoveryEmailStatus(byte[] sessionToken, RequestDelegate<RecoveryEmailStatusResponse> requestDelegate);
   public void keys(byte[] keyFetchToken, RequestDelegate<TwoKeys> requestDelegate);
   public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate<String> requestDelegate);
   public void registerOrUpdateDevice(byte[] sessionToken, FxAccountDevice device, RequestDelegate<FxAccountDevice> requestDelegate);
   public void deviceList(byte[] sessionToken, RequestDelegate<FxAccountDevice[]> requestDelegate);
+  public void notifyDevices(byte[] sessionToken, List<String> deviceIds, ExtendedJSONObject payload, Long TTL, RequestDelegate<ExtendedJSONObject> requestDelegate);
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient20.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient20.java
@@ -1,14 +1,16 @@
 /* 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/. */
 
 package org.mozilla.gecko.background.fxa;
 
+import android.support.annotation.NonNull;
+
 import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientMalformedResponseException;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.Locales;
 import org.mozilla.gecko.fxa.FxAccountDevice;
@@ -28,16 +30,17 @@ import java.io.UnsupportedEncodingExcept
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URLEncoder;
 import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.concurrent.Executor;
 
 import javax.crypto.Mac;
 
 import ch.boye.httpclientandroidlib.HttpEntity;
@@ -827,17 +830,16 @@ public class FxAccountClient20 implement
     try {
       HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
     } catch (Exception e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     final BaseResource resource;
-    final ExtendedJSONObject body;
     try {
       resource = getBaseResource("account/devices");
     } catch (URISyntaxException | UnsupportedEncodingException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     resource.delegate = new ResourceDelegate<FxAccountDevice[]>(resource, delegate, ResponseType.JSON_ARRAY, tokenId, reqHMACKey) {
@@ -853,9 +855,60 @@ public class FxAccountClient20 implement
         } catch (Exception e) {
           delegate.handleError(e);
         }
       }
     };
 
     resource.get();
   }
+
+  @Override
+  public void notifyDevices(@NonNull byte[] sessionToken, @NonNull List<String> deviceIds, ExtendedJSONObject payload, Long TTL, RequestDelegate<ExtendedJSONObject> delegate) {
+    final byte[] tokenId = new byte[32];
+    final byte[] reqHMACKey = new byte[32];
+    final byte[] requestKey = new byte[32];
+    try {
+      HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
+    } catch (Exception e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    final BaseResource resource;
+    final ExtendedJSONObject body = createNotifyDevicesBody(deviceIds, payload, TTL);
+    try {
+      resource = getBaseResource("account/devices/notify");
+    } catch (URISyntaxException | UnsupportedEncodingException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    resource.delegate = new ResourceDelegate<ExtendedJSONObject>(resource, delegate, ResponseType.JSON_OBJECT, tokenId, reqHMACKey) {
+      @Override
+      public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+        try {
+          delegate.handleSuccess(body);
+        } catch (Exception e) {
+          delegate.handleError(e);
+        }
+      }
+    };
+
+    post(resource, body);
+  }
+
+  @NonNull
+  @SuppressWarnings("unchecked")
+  private ExtendedJSONObject createNotifyDevicesBody(@NonNull List<String> deviceIds, ExtendedJSONObject payload, Long TTL) {
+    final ExtendedJSONObject body = new ExtendedJSONObject();
+    final JSONArray to = new JSONArray();
+    to.addAll(deviceIds);
+    body.put("to", to);
+    if (payload != null) {
+      body.put("payload", payload);
+    }
+    if (TTL != null) {
+      body.put("TTL", TTL);
+    }
+    return body;
+  }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
@@ -14,19 +14,18 @@ import android.util.Log;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.background.fxa.FxAccountRemoteError;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount.InvalidFxAState;
 import org.mozilla.gecko.fxa.login.State;
-import org.mozilla.gecko.fxa.login.State.StateLabel;
-import org.mozilla.gecko.fxa.login.TokensAndKeysState;
 import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 
 import java.io.UnsupportedEncodingException;
 import java.lang.ref.WeakReference;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
@@ -107,17 +106,21 @@ public class FxAccountDeviceRegistrator 
   }
 
   private static void doFxaRegistration(final Context context, final Bundle subscription, final boolean allowRecursion) throws InvalidFxAState {
     String pushCallback = subscription.getString("pushCallback");
     String pushPublicKey = subscription.getString("pushPublicKey");
     String pushAuthKey = subscription.getString("pushAuthKey");
 
     final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
-    final byte[] sessionToken = getSessionToken(fxAccount);
+    if (fxAccount == null) {
+      Log.e(LOG_TAG, "AndroidFxAccount is null");
+      return;
+    }
+    final byte[] sessionToken = fxAccount.getSessionToken();
     final FxAccountDevice device;
     String deviceId = fxAccount.getDeviceId();
     String clientName = getClientName(fxAccount, context);
     if (TextUtils.isEmpty(deviceId)) {
       Log.i(LOG_TAG, "Attempting registration for a new device");
       device = FxAccountDevice.forRegister(clientName, "mobile", pushCallback, pushPublicKey, pushAuthKey);
     } else {
       Log.i(LOG_TAG, "Attempting registration for an existing device");
@@ -175,27 +178,16 @@ public class FxAccountDeviceRegistrator 
           new SharedPreferencesClientsDataDelegate(fxAccount.getSyncPrefs(), context);
       return clientsDataDelegate.getClientName();
     } catch (UnsupportedEncodingException | GeneralSecurityException e) {
       Log.e(LOG_TAG, "Unable to get client name.", e);
       return null;
     }
   }
 
-  @Nullable
-  private static byte[] getSessionToken(final AndroidFxAccount fxAccount) throws InvalidFxAState {
-    State state = fxAccount.getState();
-    StateLabel stateLabel = state.getStateLabel();
-    if (stateLabel == StateLabel.Cohabiting || stateLabel == StateLabel.Married) {
-      TokensAndKeysState tokensAndKeysState = (TokensAndKeysState) state;
-      return tokensAndKeysState.getSessionToken();
-    }
-    throw new InvalidFxAState("Cannot get sessionToken: not in a TokensAndKeysState state");
-  }
-
   private static void handleTokenError(final FxAccountClientRemoteException error,
                                        final FxAccountClient fxAccountClient,
                                        final AndroidFxAccount fxAccount) {
     Log.i(LOG_TAG, "Recovering from invalid token error: ", error);
     logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
     fxAccountClient.accountStatus(fxAccount.getState().uid,
         new RequestDelegate<AccountStatusResponse>() {
       @Override
@@ -282,17 +274,9 @@ public class FxAccountDeviceRegistrator 
     // We have no choice but to use reflection here, sorry :(
     Class<?> eventDispatcher = Class.forName("org.mozilla.gecko.EventDispatcher");
     Method getInstance = eventDispatcher.getMethod("getInstance");
     Object instance = getInstance.invoke(null);
     Method registerBackgroundThreadListener = eventDispatcher.getMethod("registerBackgroundThreadListener",
             BundleEventListener.class, String[].class);
     registerBackgroundThreadListener.invoke(instance, this, new String[] { "FxAccountsPush:Subscribe:Response" });
   }
-
-  public static class InvalidFxAState extends Exception {
-    private static final long serialVersionUID = -8537626959811195978L;
-
-    public InvalidFxAState(String message) {
-      super(message);
-    }
-  }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountPushHandler.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountPushHandler.java
@@ -2,24 +2,28 @@ package org.mozilla.gecko.fxa;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.content.Context;
 import android.os.Bundle;
 import android.text.TextUtils;
 import android.util.Log;
 
+import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 
 public class FxAccountPushHandler {
     private static final String LOG_TAG = "FxAccountPush";
 
     private static final String COMMAND_DEVICE_DISCONNECTED = "fxaccounts:device_disconnected";
+    private static final String COMMAND_COLLECTION_CHANGED = "sync:collection_changed";
+
+    private static final String CLIENTS_COLLECTION = "clients";
 
     // Forbid instantiation
     private FxAccountPushHandler() {}
 
     public static void handleFxAPushMessage(Context context, Bundle bundle) {
         Log.i(LOG_TAG, "Handling FxA Push Message");
         String rawMessage = bundle.getString("message");
         JSONObject message = null;
@@ -40,25 +44,45 @@ public class FxAccountPushHandler {
         }
         try {
             String command = message.getString("command");
             JSONObject data = message.getJSONObject("data");
             switch (command) {
                 case COMMAND_DEVICE_DISCONNECTED:
                     handleDeviceDisconnection(context, data);
                     break;
+                case COMMAND_COLLECTION_CHANGED:
+                    handleCollectionChanged(context, data);
+                    break;
                 default:
                     Log.d(LOG_TAG, "No handler defined for FxA Push command " + command);
                     break;
             }
         } catch (JSONException e) {
             Log.e(LOG_TAG, "Error while handling FxA push notification", e);
         }
     }
 
+    private static void handleCollectionChanged(Context context, JSONObject data) throws JSONException {
+        JSONArray collections = data.getJSONArray("collections");
+        int len = collections.length();
+        for (int i = 0; i < len; i++) {
+            if (collections.getString(i).equals(CLIENTS_COLLECTION)) {
+                final Account account = FirefoxAccounts.getFirefoxAccount(context);
+                if (account == null) {
+                    Log.e(LOG_TAG, "The account does not exist anymore");
+                    return;
+                }
+                final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+                fxAccount.requestImmediateSync(new String[] { CLIENTS_COLLECTION }, null);
+                return;
+            }
+        }
+    }
+
     private static void handleDeviceDisconnection(Context context, JSONObject data) throws JSONException {
         final Account account = FirefoxAccounts.getFirefoxAccount(context);
         if (account == null) {
             Log.e(LOG_TAG, "The account does not exist anymore");
             return;
         }
         final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
         if (!fxAccount.getDeviceId().equals(data.getString("id"))) {
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
@@ -25,16 +25,17 @@ import org.mozilla.gecko.background.comm
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.login.State.StateLabel;
 import org.mozilla.gecko.fxa.login.StateFactory;
+import org.mozilla.gecko.fxa.login.TokensAndKeysState;
 import org.mozilla.gecko.fxa.sync.FxAccountProfileService;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.setup.Constants;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URISyntaxException;
@@ -602,16 +603,34 @@ public class AndroidFxAccount {
       StateLabel stateLabel = StateLabel.valueOf(stateLabelString);
       Logger.debug(LOG_TAG, "Account is in state " + stateLabel);
       return StateFactory.fromJSONObject(stateLabel, new ExtendedJSONObject(stateString));
     } catch (Exception e) {
       throw new IllegalStateException("could not get state", e);
     }
   }
 
+  public byte[] getSessionToken() throws InvalidFxAState {
+    State state = getState();
+    StateLabel stateLabel = state.getStateLabel();
+    if (stateLabel == StateLabel.Cohabiting || stateLabel == StateLabel.Married) {
+      TokensAndKeysState tokensAndKeysState = (TokensAndKeysState) state;
+      return tokensAndKeysState.getSessionToken();
+    }
+    throw new InvalidFxAState("Cannot get sessionToken: not in a TokensAndKeysState state");
+  }
+
+  public static class InvalidFxAState extends Exception {
+    private static final long serialVersionUID = -8537626959811195978L;
+
+    public InvalidFxAState(String message) {
+      super(message);
+    }
+  }
+
   /**
    * <b>For debugging only!</b>
    */
   public void dump() {
     if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
       return;
     }
     ExtendedJSONObject o = toJSONObject();
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SyncClientsEngineStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/SyncClientsEngineStage.java
@@ -1,29 +1,37 @@
 /* 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/. */
 
 package org.mozilla.gecko.sync.stage;
 
 import android.accounts.Account;
 import android.content.Context;
+import android.support.annotation.NonNull;
 import android.text.TextUtils;
+import android.util.Log;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountClient;
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.fxa.FxAccountClientException;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.sync.CommandProcessor;
 import org.mozilla.gecko.sync.CommandProcessor.Command;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.HTTPFailureException;
 import org.mozilla.gecko.sync.NoCollectionKeysSetException;
@@ -49,28 +57,29 @@ import ch.boye.httpclientandroidlib.Http
 
 public class SyncClientsEngineStage extends AbstractSessionManagingSyncStage {
   private static final String LOG_TAG = "SyncClientsEngineStage";
 
   public static final String COLLECTION_NAME       = "clients";
   public static final String STAGE_NAME            = COLLECTION_NAME;
   public static final int CLIENTS_TTL_REFRESH      = 604800000;   // 7 days in milliseconds.
   public static final int MAX_UPLOAD_FAILURE_COUNT = 5;
+  public static final long NOTIFY_TAB_SENT_TTL_SECS = TimeUnit.SECONDS.convert(1L, TimeUnit.HOURS); // 1 hour
 
   protected final ClientRecordFactory factory = new ClientRecordFactory();
   protected ClientUploadDelegate clientUploadDelegate;
   protected ClientDownloadDelegate clientDownloadDelegate;
 
   // Be sure to use this safely via getClientsDatabaseAccessor/closeDataAccessor.
   protected ClientsDatabaseAccessor db;
 
   protected volatile boolean shouldWipe;
   protected volatile boolean shouldUploadLocalRecord;     // Set if, e.g., we received commands or need to refresh our version.
   protected final AtomicInteger uploadAttemptsCount = new AtomicInteger();
-  protected final List<ClientRecord> toUpload = new ArrayList<ClientRecord>();
+  protected final List<ClientRecord> modifiedClientsToUpload = new ArrayList<ClientRecord>();
 
   protected int getClientsCount() {
     return getClientsDatabaseAccessor().clientsCount();
   }
 
   protected synchronized ClientsDatabaseAccessor getClientsDatabaseAccessor() {
     if (db == null) {
       db = new ClientsDatabaseAccessor(session.getContext());
@@ -146,23 +155,90 @@ public class SyncClientsEngineStage exte
       Logger.debug(LOG_TAG, "Server response asserts " + response.weaveRecords() + " records.");
 
       // TODO: persist the response timestamp to know whether to download next time (Bug 726055).
       clientUploadDelegate = new ClientUploadDelegate();
       clientsDelegate.setClientsCount(clientsCount);
 
       // If we upload remote records, checkAndUpload() will be called upon
       // upload success in the delegate. Otherwise call checkAndUpload() now.
-      if (toUpload.size() > 0) {
+      if (modifiedClientsToUpload.size() > 0) {
+        // modifiedClientsToUpload is cleared in uploadRemoteRecords, save what we need here
+        final List<String> devicesToNotify = new ArrayList<>();
+        for (ClientRecord record : modifiedClientsToUpload) {
+          if (!TextUtils.isEmpty(record.fxaDeviceId)) {
+            devicesToNotify.add(record.fxaDeviceId);
+          }
+        }
+
+        // This method is synchronous, there's no risk of notifying the clients
+        // before we actually uploaded the records
         uploadRemoteRecords();
+
+        // Notify the clients who got their record written
+        notifyClients(devicesToNotify);
+
         return;
       }
       checkAndUpload();
     }
 
+    private void notifyClients(final List<String> devicesToNotify) {
+      final ExecutorService executor = Executors.newSingleThreadExecutor();
+      final Context context = session.getContext();
+      final Account account = FirefoxAccounts.getFirefoxAccount(context);
+      if (account == null) {
+        Log.e(LOG_TAG, "Can't notify other clients: no account");
+        return;
+      }
+      final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+      final ExtendedJSONObject payload = createNotifyDevicesPayload();
+
+      final byte[] sessionToken;
+      try {
+        sessionToken = fxAccount.getSessionToken();
+      } catch (AndroidFxAccount.InvalidFxAState invalidFxAState) {
+        Log.e(LOG_TAG, "Could not get session token", invalidFxAState);
+        return;
+      }
+
+      // API doc : https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#post-v1accountdevicesnotify
+      final FxAccountClient fxAccountClient = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
+      fxAccountClient.notifyDevices(sessionToken, devicesToNotify, payload, NOTIFY_TAB_SENT_TTL_SECS, new FxAccountClient20.RequestDelegate<ExtendedJSONObject>() {
+        @Override
+        public void handleError(Exception e) {
+          Log.e(LOG_TAG, "Error while notifying devices", e);
+        }
+
+        @Override
+        public void handleFailure(FxAccountClientException.FxAccountClientRemoteException e) {
+          Log.e(LOG_TAG, "Error while notifying devices", e);
+        }
+
+        @Override
+        public void handleSuccess(ExtendedJSONObject result) {
+          Log.i(LOG_TAG, devicesToNotify.size() + " devices notified");
+        }
+      });
+    }
+
+    @NonNull
+    @SuppressWarnings("unchecked")
+    private ExtendedJSONObject createNotifyDevicesPayload() {
+      final ExtendedJSONObject payload = new ExtendedJSONObject();
+      payload.put("version", 1);
+      payload.put("command", "sync:collection_changed");
+      final ExtendedJSONObject data = new ExtendedJSONObject();
+      final JSONArray collections = new JSONArray();
+      collections.add("clients");
+      data.put("collections", collections);
+      payload.put("data", data);
+      return payload;
+    }
+
     @Override
     public void handleRequestFailure(SyncStorageResponse response) {
       BaseResource.consumeEntity(response); // We don't need the response at all, and any exception handling shouldn't need the response body.
       localAccountGUIDDownloaded = false;
 
       try {
         Logger.info(LOG_TAG, "Client upload failed. Aborting sync.");
         session.abort(new HTTPFailureException(response), "Client download failed.");
@@ -285,17 +361,17 @@ public class SyncClientsEngineStage exte
       // If upload failed because of `ifUnmodifiedSince` then there are new
       // commands uploaded to our record. We must download and process them first.
       if (!shouldUploadLocalRecord ||
           statusCode == HttpStatus.SC_PRECONDITION_FAILED ||
           uploadAttemptsCount.incrementAndGet() > MAX_UPLOAD_FAILURE_COUNT) {
 
         Logger.debug(LOG_TAG, "Client upload failed. Aborting sync.");
         if (!currentlyUploadingLocalRecord) {
-          toUpload.clear(); // These will be redownloaded.
+          modifiedClientsToUpload.clear(); // These will be redownloaded.
         }
         BaseResource.consumeEntity(response); // The exception thrown should need the response body.
         session.abort(new HTTPFailureException(response), "Client upload failed.");
         return;
       }
       Logger.trace(LOG_TAG, "Retrying upload…");
       // Preconditions:
       // shouldUploadLocalRecord == true &&
@@ -469,41 +545,41 @@ public class SyncClientsEngineStage exte
 
     for (Command command : commands) {
       JSONObject jsonCommand = command.asJSONObject();
       if (record.commands == null) {
         record.commands = new JSONArray();
       }
       record.commands.add(jsonCommand);
     }
-    toUpload.add(record);
+    modifiedClientsToUpload.add(record);
   }
 
   @SuppressWarnings("unchecked")
   protected void uploadRemoteRecords() {
-    Logger.trace(LOG_TAG, "In uploadRemoteRecords. Uploading " + toUpload.size() + " records" );
+    Logger.trace(LOG_TAG, "In uploadRemoteRecords. Uploading " + modifiedClientsToUpload.size() + " records" );
 
-    for (ClientRecord r : toUpload) {
+    for (ClientRecord r : modifiedClientsToUpload) {
       Logger.trace(LOG_TAG, ">> Uploading record " + r.guid + ": " + r.name);
     }
 
-    if (toUpload.size() == 1) {
-      ClientRecord record = toUpload.get(0);
+    if (modifiedClientsToUpload.size() == 1) {
+      ClientRecord record = modifiedClientsToUpload.get(0);
       Logger.debug(LOG_TAG, "Only 1 remote record to upload.");
       Logger.debug(LOG_TAG, "Record last modified: " + record.lastModified);
       CryptoRecord cryptoRecord = encryptClientRecord(record);
       if (cryptoRecord != null) {
         clientUploadDelegate.setUploadDetails(false);
         this.uploadClientRecord(cryptoRecord);
       }
       return;
     }
 
     JSONArray cryptoRecords = new JSONArray();
-    for (ClientRecord record : toUpload) {
+    for (ClientRecord record : modifiedClientsToUpload) {
       Logger.trace(LOG_TAG, "Record " + record.guid + " is being uploaded" );
 
       CryptoRecord cryptoRecord = encryptClientRecord(record);
       cryptoRecords.add(cryptoRecord.toJSONObject());
     }
     Logger.debug(LOG_TAG, "Uploading records: " + cryptoRecords.size());
     clientUploadDelegate.setUploadDetails(false);
     this.uploadClientRecords(cryptoRecords);
@@ -542,17 +618,17 @@ public class SyncClientsEngineStage exte
       session.abort(e, encryptionFailure);
     }
     return null;
   }
 
   public void clearRecordsToUpload() {
     try {
       getClientsDatabaseAccessor().wipeCommandsTable();
-      toUpload.clear();
+      modifiedClientsToUpload.clear();
     } finally {
       closeDataAccessor();
     }
   }
 
   protected void downloadClientRecords() {
     shouldWipe = true;
     clientDownloadDelegate = makeClientDownloadDelegate();
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java
@@ -760,36 +760,36 @@ public class TestClientsEngineStage exte
   public void testAddCommandsToUnversionedClient() throws NullCursorException {
     db = new TestAddCommandsMockClientsDatabaseAccessor();
 
     final ClientRecord remoteRecord = new ClientRecord();
     remoteRecord.version = null;
     final String expectedGUID = remoteRecord.guid;
 
     this.addCommands(remoteRecord);
-    assertEquals(1, toUpload.size());
+    assertEquals(1, modifiedClientsToUpload.size());
 
-    final ClientRecord recordToUpload = toUpload.get(0);
+    final ClientRecord recordToUpload = modifiedClientsToUpload.get(0);
     assertEquals(4, recordToUpload.commands.size());
     assertEquals(expectedGUID, recordToUpload.guid);
     assertEquals(null, recordToUpload.version);
   }
 
   @Test
   public void testAddCommandsToVersionedClient() throws NullCursorException {
     db = new TestAddCommandsMockClientsDatabaseAccessor();
 
     final ClientRecord remoteRecord = new ClientRecord();
     remoteRecord.version = "12a1";
     final String expectedGUID = remoteRecord.guid;
 
     this.addCommands(remoteRecord);
-    assertEquals(1, toUpload.size());
+    assertEquals(1, modifiedClientsToUpload.size());
 
-    final ClientRecord recordToUpload = toUpload.get(0);
+    final ClientRecord recordToUpload = modifiedClientsToUpload.get(0);
     assertEquals(4, recordToUpload.commands.size());
     assertEquals(expectedGUID, recordToUpload.guid);
     assertEquals("12a1", recordToUpload.version);
   }
 
   @Test
   public void testLastModifiedTimestamp() throws NullCursorException {
     // If we uploaded a record a moment ago, we shouldn't upload another.
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
@@ -1,16 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.fxa.login;
 
 import android.text.TextUtils;
 
 import org.mozilla.gecko.background.fxa.FxAccountClient;
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.RecoveryEmailStatusResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.TwoKeys;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.background.fxa.FxAccountRemoteError;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
@@ -18,16 +19,17 @@ import org.mozilla.gecko.fxa.FxAccountDe
 import org.mozilla.gecko.browserid.MockMyIDTokenFactory;
 import org.mozilla.gecko.browserid.RSACryptoImplementation;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 
 import java.io.UnsupportedEncodingException;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
 import ch.boye.httpclientandroidlib.HttpStatus;
 import ch.boye.httpclientandroidlib.ProtocolVersion;
 import ch.boye.httpclientandroidlib.entity.StringEntity;
 import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
 
@@ -211,9 +213,14 @@ public class MockFxAccountClient impleme
     if (!user.verified) {
       handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT, "user is unverified");
       return;
     }
     Collection<FxAccountDevice> devices = user.devices.values();
     FxAccountDevice[] devicesArray = devices.toArray(new FxAccountDevice[devices.size()]);
     requestDelegate.handleSuccess(devicesArray);
   }
+
+  @Override
+  public void notifyDevices(byte[] sessionToken, List<String> deviceIds, ExtendedJSONObject payload, Long TTL, RequestDelegate<ExtendedJSONObject> requestDelegate) {
+    requestDelegate.handleSuccess(new ExtendedJSONObject());
+  }
 }
--- a/old-configure.in
+++ b/old-configure.in
@@ -2489,17 +2489,17 @@ AC_SUBST(MOZ_B2G_VERSION)
 dnl ========================================================
 dnl Ensure Android SDK and build-tools versions depending on
 dnl mobile target.
 dnl ========================================================
 
 if test -z "$gonkdir" ; then
     case "$MOZ_BUILD_APP" in
     mobile/android)
-        MOZ_ANDROID_SDK(23, 23.0.3)
+        MOZ_ANDROID_SDK(23, "23.0.3 23.0.1")
         ;;
     esac
 fi
 
 dnl ========================================================
 dnl =
 dnl = Toolkit Options
 dnl =
--- a/python/mozboot/mozboot/bootstrap.py
+++ b/python/mozboot/mozboot/bootstrap.py
@@ -412,17 +412,17 @@ def current_firefox_checkout(check_outpu
 
     path = os.getcwd()
     while path:
         hg_dir = os.path.join(path, '.hg')
         git_dir = os.path.join(path, '.git')
         if hg and os.path.exists(hg_dir):
             # Verify the hg repo is a Firefox repo by looking at rev 0.
             try:
-                node = check_output([hg, 'log', '-r', '0', '-T', '{node}'], cwd=path)
+                node = check_output([hg, 'log', '-r', '0', '--template', '{node}'], cwd=path)
                 if node in HG_ROOT_REVISIONS:
                     return 'hg'
                 # Else the root revision is different. There could be nested
                 # repos. So keep traversing the parents.
             except subprocess.CalledProcessError:
                 pass
 
         # TODO check git remotes or `git rev-parse -q --verify $sha1^{commit}`
--- a/python/mozboot/mozboot/centosfedora.py
+++ b/python/mozboot/mozboot/centosfedora.py
@@ -68,16 +68,17 @@ class CentOSFedoraBootstrapper(BaseBoots
                 'python2-devel',
             ]
 
             self.browser_packages += [
                 'gcc-c++',
             ]
 
             self.mobile_android_packages += [
+                'java-1.8.0-openjdk-devel',
                 'ncurses-devel.i686',
                 'libstdc++.i686',
                 'zlib-devel.i686',
             ]
 
     def install_system_packages(self):
         self.dnf_groupinstall(*self.group_packages)
         self.dnf_install(*self.packages)
@@ -125,16 +126,25 @@ class CentOSFedoraBootstrapper(BaseBoots
         self.sdk_url = 'https://dl.google.com/android/android-sdk_r24.0.1-linux.tgz'
         self.ndk_url = android.android_ndk_url('linux')
 
         android.ensure_android_sdk_and_ndk(path=mozbuild_path,
                                            sdk_path=self.sdk_path, sdk_url=self.sdk_url,
                                            ndk_path=self.ndk_path, ndk_url=self.ndk_url,
                                            artifact_mode=artifact_mode)
 
+        # Most recent version of build-tools appears to be 23.0.1 on Fedora
+        packages = [p for p in android.ANDROID_PACKAGES if not p.startswith('build-tools')]
+        packages.append('build-tools-23.0.1')
+
+        # 3. We expect the |android| tool to be at
+        # ~/.mozbuild/android-sdk-linux/tools/android.
+        android_tool = os.path.join(self.sdk_path, 'tools', 'android')
+        android.ensure_android_packages(android_tool=android_tool, packages=packages)
+
     def suggest_mobile_android_mozconfig(self, artifact_mode=False):
         import android
         android.suggest_mozconfig(sdk_path=self.sdk_path,
                                   ndk_path=self.ndk_path,
                                   artifact_mode=artifact_mode)
 
     def suggest_mobile_android_artifact_mode_mozconfig(self):
         self.suggest_mobile_android_mozconfig(artifact_mode=True)
--- a/python/mozlint/mozlint/vcs.py
+++ b/python/mozlint/mozlint/vcs.py
@@ -44,17 +44,17 @@ class VCSFiles(object):
         return self.vcs == 'git'
 
     def _run(self, cmd):
         files = subprocess.check_output(cmd).split()
         return [os.path.join(self.root, f) for f in files]
 
     def by_rev(self, rev):
         if self.is_hg:
-            return self._run(['hg', 'log', '-T', '{files % "\\n{file}"}', '-r', rev])
+            return self._run(['hg', 'log', '--template', '{files % "\\n{file}"}', '-r', rev])
         elif self.is_git:
             return self._run(['git', 'diff', '--name-only', rev])
         return []
 
     def by_workdir(self):
         if self.is_hg:
             return self._run(['hg', 'status', '-amn'])
         elif self.is_git:
--- a/testing/firefox-ui/tests/puppeteer/test_appinfo.py
+++ b/testing/firefox-ui/tests/puppeteer/test_appinfo.py
@@ -11,17 +11,18 @@ class TestAppInfo(FirefoxTestCase):
     def test_valid_properties(self):
         binary = self.marionette.bin
         version_info = mozversion.get_version(binary=binary)
 
         self.assertEqual(self.appinfo.ID, version_info['application_id'])
         self.assertEqual(self.appinfo.name, version_info['application_name'])
         self.assertEqual(self.appinfo.vendor, version_info['application_vendor'])
         self.assertEqual(self.appinfo.version, version_info['application_version'])
-        self.assertEqual(self.appinfo.platformBuildID, version_info['platform_buildid'])
+        # Bug 1298328 - Platform buildid mismatch due to incremental builds
+        # self.assertEqual(self.appinfo.platformBuildID, version_info['platform_buildid'])
         self.assertEqual(self.appinfo.platformVersion, version_info['platform_version'])
         self.assertIsNotNone(self.appinfo.locale)
         self.assertIsNotNone(self.appinfo.user_agent)
         self.assertIsNotNone(self.appinfo.XPCOMABI)
 
     def test_invalid_properties(self):
         with self.assertRaises(AttributeError):
             self.appinfo.unknown
--- a/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js
+++ b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js
@@ -78,16 +78,33 @@ ExtensionTestUtils.loadExtension = funct
       } else {
         messageQueue.add([msg, ...args]);
         checkMessages();
       }
 
     },
   };
 
+  // Mimic serialization of functions as done in `Extension.generateXPI` and
+  // `Extension.generateZipFile` because functions are dropped when `ext` object
+  // is sent to the main process via the message manager.
+  ext = Object.assign({}, ext);
+  if (ext.files) {
+    ext.files = Object.assign({}, ext.files);
+    for (let filename of Object.keys(ext.files)) {
+      let file = ext.files[filename];
+      if (typeof file == "function") {
+        ext.files[filename] = `(${file})();`
+      }
+    }
+  }
+  if (typeof ext.background == "function") {
+    ext.background = `(${ext.background})();`
+  }
+
   var extension = SpecialPowers.loadExtension(ext, handler);
 
   registerCleanup(() => {
     if (extension.state == "pending" || extension.state == "running") {
       SimpleTest.ok(false, "Extension left running at test shutdown")
       return extension.unload();
     } else if (extension.state == "unloading") {
       SimpleTest.ok(false, "Extension not fully unloaded at test shutdown")
--- a/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/getComputedStyle.html
+++ b/testing/web-platform/tests/web-animations/interfaces/AnimationEffectTiming/getComputedStyle.html
@@ -102,22 +102,18 @@ test(function(t) {
   anim.currentTime = 1000;
   assert_equals(getComputedStyle(div).opacity, '0.9',
                 'set currentTime before endTime');
 
   anim.currentTime = 5000;
   assert_equals(getComputedStyle(div).opacity, '0.5',
                 'set currentTime same as endTime');
 
-  anim.currentTime = 9999;
-  assert_equals(getComputedStyle(div).opacity, '0.5',
-                'set currentTime during duration');
-
   anim.currentTime = 10000;
-  assert_equals(getComputedStyle(div).opacity, '0.5',
+  assert_equals(getComputedStyle(div).opacity, '0',
                 'set currentTime after endTime');
 }, 'change currentTime when fill forwards and endDelay is negative');
 
 test(function(t) {
   var div = createDiv(t);
   var anim = div.animate({ opacity: [ 0, 1 ] },
                          { duration: 10000,
                            direction: 'normal' });
--- a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/getComputedTiming.html
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/getComputedTiming.html
@@ -184,20 +184,20 @@ var gEndTimeTests = [
     input:    { duration: Infinity, iterations: 10, delay: -1000 },
     expected: Infinity },
   { desc:     "an non-zero duration and negative delay",
     input:    { duration: 1000, iterations: 2, delay: -1000 },
     expected: 1000 },
   { desc:     "an non-zero duration and negative delay greater than active " +
               "duration",
     input:    { duration: 1000, iterations: 2, delay: -3000 },
-    expected: -1000 },
+    expected: 0 },
   { desc:     "a zero duration and negative delay",
     input:    { duration: 0, iterations: 2, delay: -1000 },
-    expected: -1000 }
+    expected: 0 }
 ];
 
 gEndTimeTests.forEach(function(stest) {
   test(function(t) {
     var effect = new KeyframeEffectReadOnly(target,
                                             { left: ["10px", "20px"] },
                                             stest.input);
 
--- a/testing/web-platform/tests/web-animations/timing-model/animation-effects/active-time.html
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/active-time.html
@@ -95,17 +95,17 @@ test(function(t) {
   assert_times_equal(anim.effect.getComputedTiming().progress, 0.5);
 }, 'Active time in after phase with forwards fill and negative end delay'
    + ' is the active duration + end delay');
 
 test(function(t) {
   var anim = createDiv(t).animate(null, { duration: 1000,
                                           iterations: 2.3,
                                           delay: 500,
-                                          endDelay: -3000,
+                                          endDelay: -2500,
                                           fill: 'forwards' });
   anim.finish();
   assert_equals(anim.effect.getComputedTiming().currentIteration, 0);
   assert_equals(anim.effect.getComputedTiming().progress, 0);
 }, 'Active time in after phase with forwards fill and negative end delay'
    + ' greater in magnitude than the active duration is zero');
 
 test(function(t) {
--- a/testing/web-platform/tests/web-animations/timing-model/animation-effects/phases-and-states.html
+++ b/testing/web-platform/tests/web-animations/timing-model/animation-effects/phases-and-states.html
@@ -68,17 +68,17 @@ test(function(t) {
     assert_phase_at_time(animation, test.phase, test.currentTime);
   });
 }, 'Phase calculation for an animation effect with a positive start delay');
 
 test(function(t) {
   var animation = createDiv(t).animate(null, { duration: 1, delay: -1 });
 
   [ { currentTime: -2, phase: 'before' },
-    { currentTime: -1, phase: 'active' },
+    { currentTime: -1, phase: 'before' },
     { currentTime:  0, phase: 'after'  } ]
   .forEach(function(test) {
     assert_phase_at_time(animation, test.phase, test.currentTime);
   });
 }, 'Phase calculation for an animation effect with a negative start delay');
 
 test(function(t) {
   var animation = createDiv(t).animate(null, { duration: 1, endDelay: 1 });
@@ -116,57 +116,61 @@ test(function(t) {
   });
 }, 'Phase calculation for an animation effect with a negative end delay equal'
    + ' in magnitude to the active duration');
 
 test(function(t) {
   var animation = createDiv(t).animate(null, { duration: 1, endDelay: -2 });
 
   [ { currentTime: -2, phase: 'before' },
-    { currentTime: -1, phase: 'after'  } ]
+    { currentTime: -1, phase: 'before' },
+    { currentTime:  0, phase: 'after'  } ]
   .forEach(function(test) {
     assert_phase_at_time(animation, test.phase, test.currentTime);
   });
 }, 'Phase calculation for an animation effect with a negative end delay'
    + ' greater in magnitude than the active duration');
 
 test(function(t) {
   var animation = createDiv(t).animate(null, { duration: 2,
                                                delay: 1,
                                                endDelay: -1 });
 
-  [ { currentTime: 0,   phase: 'before' },
-    { currentTime: 1,   phase: 'active' },
+  [ { currentTime: 0, phase: 'before' },
+    { currentTime: 1, phase: 'active' },
     { currentTime: 2, phase: 'after'  } ]
   .forEach(function(test) {
     assert_phase_at_time(animation, test.phase, test.currentTime);
   });
 }, 'Phase calculation for an animation effect with a positive start delay'
    + ' and a negative end delay lesser in magnitude than the active duration');
 
 test(function(t) {
   var animation = createDiv(t).animate(null, { duration: 1,
                                                delay: -1,
                                                endDelay: -1 });
 
-  [ { currentTime: -2,   phase: 'before' },
-    { currentTime: -1,   phase: 'after'  } ]
+  [ { currentTime: -2, phase: 'before' },
+    { currentTime: -1, phase: 'before' },
+    { currentTime:  0, phase: 'after'  } ]
   .forEach(function(test) {
     assert_phase_at_time(animation, test.phase, test.currentTime);
   });
 }, 'Phase calculation for an animation effect with a negative start delay'
    + ' and a negative end delay equal in magnitude to the active duration');
 
 test(function(t) {
   var animation = createDiv(t).animate(null, { duration: 1,
                                                delay: -1,
                                                endDelay: -2 });
 
-  [ { currentTime: -3,   phase: 'before' },
-    { currentTime: -2,   phase: 'after'  } ]
+  [ { currentTime: -3, phase: 'before' },
+    { currentTime: -2, phase: 'before' },
+    { currentTime: -1, phase: 'before' },
+    { currentTime:  0, phase: 'after'  } ]
   .forEach(function(test) {
     assert_phase_at_time(animation, test.phase, test.currentTime);
   });
 }, 'Phase calculation for an animation effect with a negative start delay'
    + ' and a negative end delay equal greater in magnitude than the active'
    + ' duration');
 
 test(function(t) {
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html
@@ -24,29 +24,28 @@ const {
 /**
  * This test is asserting that ext-backgroundPage.js successfully sets its
  * debug global in the AddonWrapper provided by XPIProvider.jsm
  *
  * It does _not_ test any functionality in devtools and does not guarantee
  * debugging is actually working correctly end-to-end.
  */
 
-function backgroundScript() {
+function background() {
   window.testThing = "test!";
   browser.test.notifyPass("background script ran");
 }
 
 const ID = "debug@tests.mozilla.org";
 let extensionData = {
   useAddonManager: "temporary",
-  background: "(" + backgroundScript.toString() + ")()",
+  background,
   manifest: {
     applications: {gecko: {id: ID}},
   },
-  files: {},
 };
 
 add_task(function* () {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
 
   yield extension.awaitFinish("background script ran");
 
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_page.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_page.html
@@ -21,17 +21,17 @@ Cu.import("resource://testing-common/Tes
 add_task(function* testAlertNotShownInBackgroundWindow() {
   ok(!Services.wm.getEnumerator("alert:alert").hasMoreElements(),
      "Alerts should not be present at the start of the test.");
 
   let consoleOpened = TestUtils.topicObserved("web-console-created");
 
 
   let extension = ExtensionTestUtils.loadExtension({
-    background: "new " + function() {
+    background: function() {
       browser.test.log("background script executed");
 
       alert("I am an alert in the background.");
 
       browser.test.notifyPass("alertCalled");
     },
   });
 
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_contentscript_unrecognizedprop_warning.html
@@ -12,17 +12,17 @@
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 const BASE = "http://mochi.test:8888/chrome/toolkit/components/extensions/test/mochitest";
 
 add_task(function* test_contentscript() {
-  function backgroundScript() {
+  function background() {
     browser.runtime.onMessage.addListener((msg) => {
       if (msg == "loaded") {
         browser.tabs.query({active: true, currentWindow: true}).then((tabs) => {
           // NOTE: we're removing the tab from here because doing a win.close()
           // from the chrome test code is raising a "TypeError: can 't access
           // dead object" exception.
           browser.tabs.remove(tabs[0].id);
 
@@ -42,20 +42,20 @@ add_task(function* test_contentscript() 
         {
           "matches": ["http://mochi.test/*/file_sample.html"],
           "js": ["content_script.js"],
           "run_at": "document_idle",
           "unrecognized_property": "with-a-random-value",
         },
       ],
     },
-    background: "(" + backgroundScript.toString() + ")()",
+    background,
 
     files: {
-      "content_script.js": "(" + contentScript.toString() + ")()",
+      "content_script.js": contentScript,
     },
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
 
   SimpleTest.waitForExplicitFinish();
   let waitForConsole = new Promise(resolve => {
     SimpleTest.monitorConsole(resolve, [{
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_eventpage_warning.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_eventpage_warning.html
@@ -20,20 +20,20 @@ function createEventPageExtension(eventP
     browser.test.sendMessage("running", 1);
   }
 
   return ExtensionTestUtils.loadExtension({
     manifest: {
       "background": eventPage,
     },
     files: {
-      "event-page-script.js": `(${eventPageScript})()`,
+      "event-page-script.js": eventPageScript,
       "event-page.html": `<html><head>
         <meta charset="utf-8">
-        <script src="event-page-script.js"></${"script"}>
+        <script src="event-page-script.js"><\/script>
       </head></html>`,
     },
   });
 }
 
 add_task(function* test_eventpages() {
   // Used in other tests to prevent the monitorConsole to grip.
   SimpleTest.waitForExplicitFinish();
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_shutdown_cleanup.html
@@ -22,17 +22,17 @@ const {GlobalManager} = Cu.import("resou
 
 /* eslint-disable mozilla/balanced-listeners */
 
 add_task(function* testShutdownCleanup() {
   is(GlobalManager.initialized, false,
      "GlobalManager start as not initialized");
 
   let extension = ExtensionTestUtils.loadExtension({
-    background: "new " + function() {
+    background: function() {
       browser.test.notifyPass("background page loaded");
     },
   });
 
   yield extension.startup();
 
   yield extension.awaitFinish("background page loaded");
 
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html
@@ -101,17 +101,17 @@ add_task(function* test_uninstall() {
   yield SpecialPowers.pushPrefEnv({
     set: [["extensions.webextensions.keepUuidOnUninstall", true]],
   });
   yield SpecialPowers.pushPrefEnv({
     set: [["extensions.webextensions.keepStorageOnUninstall", true]],
   });
 
   let extension = ExtensionTestUtils.loadExtension({
-    background: `(${writeData})()`,
+    background: writeData,
     manifest: {
       applications: {gecko: {id: ID}},
       permissions: ["storage"],
     },
     useAddonManager: "temporary",
   });
 
   yield extension.startup();
@@ -120,17 +120,17 @@ add_task(function* test_uninstall() {
 
   // Check that we can still see data we wrote to storage but clear the
   // "leave storage" flag so our storaged gets cleared on uninstall.
   // This effectively tests the keepUuidOnUninstall logic, which ensures
   // that when we read storage again and check that it is cleared, that
   // it is actually a meaningful test!
   yield SpecialPowers.popPrefEnv();
   extension = ExtensionTestUtils.loadExtension({
-    background: `(${readData})()`,
+    background: readData,
     manifest: {
       applications: {gecko: {id: ID}},
       permissions: ["storage"],
     },
     useAddonManager: "temporary",
   });
 
   yield extension.startup();
@@ -138,17 +138,17 @@ add_task(function* test_uninstall() {
   is(results.matchLocalStorage, true, "localStorage data is still present");
   is(results.matchIDB, true, "indexedDB data is still present");
   is(results.matchBrowserStorage, true, "browser.storage.local data is still present");
 
   yield extension.unload();
 
   // Read again.  This time, our data should be gone.
   extension = ExtensionTestUtils.loadExtension({
-    background: `(${readData})()`,
+    background: readData,
     manifest: {
       applications: {gecko: {id: ID}},
       permissions: ["storage"],
     },
     useAddonManager: "temporary",
   });
 
   yield extension.startup();
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_trustworthy_origin.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_trustworthy_origin.html
@@ -20,23 +20,22 @@
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gContentSecurityManager",
                                    "@mozilla.org/contentsecuritymanager;1",
                                    "nsIContentSecurityManager");
 
 add_task(function* () {
-  function backgroundScript() {
+  function background() {
     browser.test.sendMessage("ready", browser.runtime.getURL("/test.html"));
   }
 
   let extensionData = {
-    background: "(" + backgroundScript.toString() + ")()",
-    manifest: {},
+    background,
     files: {
       "test.html": `<html><head></head><body></body></html>`,
     },
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
 
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webnavigation_resolved_urls.html
@@ -10,17 +10,17 @@
   <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 add_task(function* webnav_unresolved_uri_on_expected_URI_scheme() {
-  function backgroundScript() {
+  function background() {
     let checkURLs;
 
     browser.webNavigation.onCompleted.addListener((msg) => {
       if (checkURLs.length > 0) {
         let expectedURL = checkURLs.shift();
         browser.test.assertEq(expectedURL, msg.url, "Got the expected URL");
         browser.tabs.remove(msg.tabId).then(() => {
           browser.test.sendMessage("next");
@@ -38,17 +38,17 @@ add_task(function* webnav_unresolved_uri
   }
 
   let extensionData = {
     manifest: {
       permissions: [
         "webNavigation",
       ],
     },
-    background: "new " + backgroundScript,
+    background,
     files: {
       "tab.html": `<!DOCTYPE html>
         <html>
           <head>
            <meta charset="utf-8">
           </head>
         </html>
       `,
--- a/toolkit/components/extensions/test/mochitest/test_ext_background_api_injection.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_api_injection.html
@@ -10,17 +10,17 @@
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 add_task(function* testBackgroundWindow() {
   let extension = ExtensionTestUtils.loadExtension({
-    background: "new " + function() {
+    background: function() {
       const BASE = "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest";
 
       browser.test.log("background script executed");
       window.location = `${BASE}/file_privilege_escalation.html`;
     },
   });
 
   let awaitConsole = new Promise(resolve => {
--- a/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_canvas.html
@@ -27,17 +27,17 @@ add_task(function* test_background_canva
       browser.test.notifyPass("background-canvas");
     } catch (e) {
       browser.test.fail(`Error: ${e} :: ${e.stack}`);
       browser.test.notifyFail("background-canvas");
     }
   }
 
   let extensionData = {
-    background: `(${background})()`,
+    background,
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
 
   yield extension.startup();
   yield extension.awaitFinish("background-canvas");
   yield extension.unload();
 });
--- a/toolkit/components/extensions/test/mochitest/test_ext_background_generated_url.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_generated_url.html
@@ -22,17 +22,17 @@ add_task(function* test_url_of_generated
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       background: {
         scripts: ["bg.js"],
       },
       web_accessible_resources: ["_generated_background_page.html"],
     },
     files: {
-      "bg.js": `(${backgroundScript})();`,
+      "bg.js": backgroundScript,
     },
   });
 
   yield extension.startup();
   const EXPECTED_URL = yield extension.awaitMessage("script done");
 
   let win = window.open(EXPECTED_URL);
   ok(win, "Should open new tab at URL: " + EXPECTED_URL);
--- a/toolkit/components/extensions/test/mochitest/test_ext_background_teardown.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_background_teardown.html
@@ -9,30 +9,30 @@
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
 </head>
 <body>
 
 <script>
 "use strict";
 
 add_task(function* test_background_reload_and_unload() {
-  function backgroundScript() {
+  function background() {
     browser.test.onMessage.addListener(msg => {
       browser.test.assertEq("reload-background", msg);
       location.reload();
     });
     browser.test.sendMessage("background-url", location.href);
   }
 
   let chromeScript = SpecialPowers.loadChromeScript(
       SimpleTest.getTestFileURL("file_teardown_test.js"));
   yield chromeScript.promiseOneMessage("chromescript-startup");
 
   let extensionData = {
-    background: `(${backgroundScript})();`,
+    background,
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   function* getContextEvents() {
     chromeScript.sendAsyncMessage("get-context-events");
     let contextEvents = yield chromeScript.promiseOneMessage("context-events");
     return contextEvents.filter(event => event.extensionId == extension.id);
   }
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript.html
@@ -9,17 +9,17 @@
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 add_task(function* test_contentscript() {
-  function backgroundScript() {
+  function background() {
     browser.runtime.onMessage.addListener(([msg, expectedState, readyState], sender) => {
       if (msg == "chrome-namespace-ok") {
         browser.test.sendMessage(msg);
         return;
       }
 
       browser.test.assertEq(msg, "script-run", "message type is correct");
       browser.test.assertEq(readyState, expectedState, "readyState is correct");
@@ -64,23 +64,23 @@ add_task(function* test_contentscript() 
         },
         {
           "matches": ["http://mochi.test/*/file_sample.html"],
           "js": ["content_script.js"],
           "run_at": "document_idle",
         },
       ],
     },
-    background: "(" + backgroundScript.toString() + ")()",
+    background,
 
     files: {
-      "content_script_start.js": "(" + contentScriptStart.toString() + ")()",
-      "content_script_end.js": "(" + contentScriptEnd.toString() + ")()",
-      "content_script_idle.js": "(" + contentScriptIdle.toString() + ")()",
-      "content_script.js": "(" + contentScript.toString() + ")()",
+      "content_script_start.js": contentScriptStart,
+      "content_script_end.js": contentScriptEnd,
+      "content_script_idle.js": contentScriptIdle,
+      "content_script.js": contentScript,
     },
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
 
   let loadingCount = 0;
   let interactiveCount = 0;
   let completeCount = 0;
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_api_injection.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_api_injection.html
@@ -47,18 +47,18 @@ add_task(function* test_contentscript_ap
         },
       ],
       "web_accessible_resources": [
         "content_script_iframe.html",
       ],
     },
 
     files: {
-      "content_script.js": "new " + contentScript,
-      "content_script_iframe.js": "new " + contentScriptIframe,
+      "content_script.js": contentScript,
+      "content_script_iframe.js": contentScriptIframe,
       "content_script_iframe.html": document.querySelector("#test-asset").textContent,
     },
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
 
   let awaitConsole = new Promise(resolve => {
     let chromeScript = SpecialPowers.loadChromeScript(
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_context.html
@@ -31,17 +31,17 @@ add_task(function* test_contentscript_co
     manifest: {
       content_scripts: [{
         "matches": ["http://example.com/"],
         "js": ["content_script.js"],
       }],
     },
 
     files: {
-      "content_script.js": `(${contentScript})()`,
+      "content_script.js": contentScript,
     },
   });
 
   yield extension.startup();
 
   let win = window.open("http://example.com/");
   yield extension.awaitMessage("content-script-ready");
   yield extension.awaitMessage("content-script-show");
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html
@@ -20,17 +20,17 @@
     </head>
   </html>
 </textarea>
 
 <script type="text/javascript">
 "use strict";
 
 add_task(function* test_contentscript_create_iframe() {
-  function backgroundScript() {
+  function background() {
     browser.runtime.onMessage.addListener((msg, sender) => {
       let {name, availableAPIs, manifest, testGetManifest} = msg;
       let hasExtTabsAPI = availableAPIs.indexOf("tabs") > 0;
       let hasExtWindowsAPI = availableAPIs.indexOf("windows") > 0;
 
       browser.test.assertFalse(hasExtTabsAPI, "the created iframe should not be able to use privileged APIs (tabs)");
       browser.test.assertFalse(hasExtWindowsAPI, "the created iframe should not be able to use privileged APIs (windows)");
 
@@ -92,22 +92,22 @@ add_task(function* test_contentscript_cr
           "run_at": "document_idle",
         },
       ],
       web_accessible_resources: [
         "content_script_iframe.html",
       ],
     },
 
-    background: "(" + backgroundScript + ")()",
+    background,
 
     files: {
-      "content_script.js": "new " + contentScript,
+      "content_script.js": contentScript,
       "content_script_iframe.html": document.querySelector("#test-asset").textContent,
-      "content_script_iframe.js": "new " + contentScriptIframe,
+      "content_script_iframe.js": contentScriptIframe,
     },
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
 
   let contentScriptIframeCreatedPromise = new Promise(resolve => {
     extension.onMessage("content-script-iframe-loaded", () => { resolve(); });
   });
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_devtools_metadata.html
@@ -13,17 +13,17 @@
 <script type="text/javascript">
 "use strict";
 
 add_task(function* test_contentscript_devtools_sandbox_metadata() {
   function contentScript() {
     browser.runtime.sendMessage("contentScript.executed");
   }
 
-  function backgroundScript() {
+  function background() {
     browser.runtime.onMessage.addListener((msg) => {
       if (msg == "contentScript.executed") {
         browser.test.notifyPass("contentScript.executed");
       }
     });
   }
 
   let extensionData = {
@@ -32,19 +32,19 @@ add_task(function* test_contentscript_de
         {
           "matches": ["http://mochi.test/*/file_sample.html"],
           "js": ["content_script.js"],
           "run_at": "document_idle",
         },
       ],
     },
 
-    background: "new " + backgroundScript,
+    background,
     files: {
-      "content_script.js": "new " + contentScript,
+      "content_script.js": contentScript,
     },
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
 
   yield extension.startup();
 
   let win = window.open("file_sample.html");
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_exporthelpers.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_exporthelpers.html
@@ -69,17 +69,17 @@ add_task(function* test_contentscript_ex
       content_scripts: [{
         js: ["contentscript.js"],
         matches: ["http://mochi.test/*/file_sample.html"],
         run_at: "document_start",
       }],
     },
 
     files: {
-      "contentscript.js": `(${contentScript})();`,
+      "contentscript.js": contentScript,
     },
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
 
   yield extension.startup();
 
   let win = window.open("file_sample.html");
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_teardown.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_teardown.html
@@ -12,36 +12,36 @@
 
 <script>
 "use strict";
 
 add_task(function* test_contentscript_reload_and_unload() {
   function contentScript() {
     browser.test.sendMessage("contentscript-run");
   }
-  function backgroundScript() {
+  function background() {
     let removedTabs = 0;
     browser.tabs.onRemoved.addListener(() => {
       browser.test.assertEq(1, ++removedTabs,
           "Expected only one tab to be removed during the test");
       browser.test.sendMessage("tab-closed");
     });
   }
 
   let extensionData = {
-    background: `(${backgroundScript})();`,
+    background,
     manifest: {
       content_scripts: [{
         "matches": ["http://mochi.test/*/file_sample.html"],
         "js": ["contentscript.js"],
       }],
     },
 
     files: {
-      "contentscript.js": `(${contentScript})();`,
+      "contentscript.js": contentScript,
     },
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
 
   let chromeScript = SpecialPowers.loadChromeScript(
       SimpleTest.getTestFileURL("file_teardown_test.js"));
--- a/toolkit/components/extensions/test/mochitest/test_ext_cookies.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies.html
@@ -9,17 +9,17 @@
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 add_task(function* test_cookies() {
-  function backgroundScript() {
+  function background() {
     function assertExpected(expected, cookie) {
       for (let key of Object.keys(cookie)) {
         browser.test.assertTrue(key in expected, `found property ${key}`);
         browser.test.assertEq(expected[key], cookie[key], `property value for ${key} is correct`);
       }
       browser.test.assertEq(Object.keys(expected).length, Object.keys(cookie).length, "all expected properties found");
     }
 
@@ -163,17 +163,17 @@ add_task(function* test_cookies() {
       browser.test.assertEq("", cookie.name, "default name set");
       browser.test.assertEq("", cookie.value, "default value set");
       browser.test.assertEq(true, cookie.session, "no expiry date created session cookie");
       browser.test.notifyPass("cookies");
     });
   }
 
   let extension = ExtensionTestUtils.loadExtension({
-    background: `(${backgroundScript})()`,
+    background,
     manifest: {
       permissions: ["cookies", "*://example.org/"],
     },
   });
 
   yield extension.startup();
   info("extension loaded");
   yield extension.awaitFinish("cookies");
--- a/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_cookies_expiry.html
@@ -37,17 +37,17 @@ add_task(function* test_cookies_expiry()
     }, 1000);
   }
 
   let domain = ".example.com";
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "permissions": ["http://example.com/", "cookies"],
     },
-    background: `(${background})()`,
+    background,
   });
 
   let cookieSvc = SpecialPowers.Services.cookies;
 
   let cookie = {
     host: domain,
     name: "first",
     path: "/",
--- a/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_exclude_include_globs.html
@@ -9,17 +9,17 @@
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 add_task(function* test_contentscript() {
-  function backgroundScript() {
+  function background() {
     browser.runtime.onMessage.addListener(([script], sender) => {
       browser.test.sendMessage("run", {script});
       browser.test.sendMessage("run-" + script);
     });
     browser.test.sendMessage("running");
   }
 
   function contentScriptAll() {
@@ -48,22 +48,22 @@ add_task(function* test_contentscript() 
         },
         {
           "matches": ["http://example.org/", "http://*.example.org/"],
           "exclude_globs": ["*test1*"],
           "js": ["content_script_excludes_test1.js"],
         },
       ],
     },
-    background: "(" + backgroundScript.toString() + ")()",
+    background,
 
     files: {
-      "content_script_all.js": "(" + contentScriptAll.toString() + ")()",
-      "content_script_includes_test1.js": "(" + contentScriptIncludesTest1.toString() + ")()",
-      "content_script_excludes_test1.js": "(" + contentScriptExcludesTest1.toString() + ")()",
+      "content_script_all.js": contentScriptAll,
+      "content_script_includes_test1.js": contentScriptIncludesTest1,
+      "content_script_excludes_test1.js": contentScriptExcludesTest1,
     },
 
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
 
   let ran = 0;
   extension.onMessage("run", ({script}) => {
--- a/toolkit/components/extensions/test/mochitest/test_ext_generate.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_generate.html
@@ -8,31 +8,31 @@
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-function backgroundScript() {
+function background() {
   browser.test.log("running background script");
 
   browser.test.onMessage.addListener((x, y) => {
     browser.test.assertEq(x, 10, "x is 10");
     browser.test.assertEq(y, 20, "y is 20");
 
     browser.test.notifyPass("background test passed");
   });
 
   browser.test.sendMessage("running", 1);
 }
 
 let extensionData = {
-  background: "(" + backgroundScript.toString() + ")()",
+  background,
 };
 
 add_task(function* test_background() {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   info("load complete");
   let [, x] = yield Promise.all([extension.startup(), extension.awaitMessage("running")]);
   is(x, 1, "got correct value from extension");
   info("startup complete");
--- a/toolkit/components/extensions/test/mochitest/test_ext_geturl.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_geturl.html
@@ -8,17 +8,17 @@
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-function backgroundScript() {
+function background() {
   browser.runtime.onMessage.addListener(([url1, url2]) => {
     let url3 = browser.runtime.getURL("test_file.html");
     let url4 = browser.extension.getURL("test_file.html");
 
     browser.test.assertTrue(url1 !== undefined, "url1 defined");
 
     browser.test.assertTrue(url1.startsWith("moz-extension://"), "url1 has correct scheme");
     browser.test.assertTrue(url1.endsWith("test_file.html"), "url1 has correct leaf name");
@@ -33,27 +33,27 @@ function backgroundScript() {
 
 function contentScript() {
   let url1 = browser.runtime.getURL("test_file.html");
   let url2 = browser.extension.getURL("test_file.html");
   browser.runtime.sendMessage([url1, url2]);
 }
 
 let extensionData = {
-  background: "(" + backgroundScript.toString() + ")()",
+  background,
   manifest: {
     "content_scripts": [{
       "matches": ["http://mochi.test/*/file_sample.html"],
       "js": ["content_script.js"],
       "run_at": "document_idle",
     }],
   },
 
   files: {
-    "content_script.js": "(" + contentScript.toString() + ")()",
+    "content_script.js": contentScript,
   },
 };
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
 
   let win = window.open("file_sample.html");
--- a/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
@@ -208,20 +208,20 @@ add_task(function* test_get_accept_langu
     manifest: {
       "content_scripts": [{
         "matches": ["http://mochi.test/*/file_sample.html"],
         "run_at": "document_start",
         "js": ["content_script.js"],
       }],
     },
 
-    background: `(${background})()`,
+    background,
 
     files: {
-      "content_script.js": `(${content})()`,
+      "content_script.js": content,
     },
   });
 
   let win = window.open("file_sample.html");
 
   yield extension.startup();
   yield extension.awaitMessage("ready");
 
@@ -326,17 +326,17 @@ add_task(function* test_detect_language(
     "of winkels nie en slegs oornagbesoekers word toegelaat bateleur";
   // String with intermixed French/English text
   const fr_en_string = "France is the largest country in Western Europe and the third-largest in Europe as a whole. " +
     "A accès aux chiens et aux frontaux qui lui ont été il peut consulter et modifier ses collections et exporter " +
     "Cet article concerne le pays européen aujourd’hui appelé République française. Pour d’autres usages du nom France, " +
     "Pour une aide rapide et effective, veuiller trouver votre aide dans le menu ci-dessus." +
     "Motoring events began soon after the construction of the first successful gasoline-fueled automobiles. The quick brown fox jumped over the lazy dog";
 
-  function backgroundScript() {
+  function background() {
     function checkResult(source, result, expected) {
       browser.test.assertEq(expected.isReliable, result.isReliable, "result.confident is true");
       browser.test.assertEq(
         expected.languages.length,
         result.languages.length,
         `result.languages contains the expected number of languages in ${source}`);
       expected.languages.forEach((lang, index) => {
         browser.test.assertEq(
@@ -382,20 +382,20 @@ add_task(function* test_detect_language(
     manifest: {
       "content_scripts": [{
         "matches": ["http://mochi.test/*/file_sample.html"],
         "run_at": "document_start",
         "js": ["content_script.js"],
       }],
     },
 
-    background: `(${backgroundScript})()`,
+    background,
 
     files: {
-      "content_script.js": `(${content})()`,
+      "content_script.js": content,
     },
   });
 
   let win = window.open("file_sample.html");
 
   yield extension.startup();
   yield extension.awaitMessage("ready");
 
--- a/toolkit/components/extensions/test/mochitest/test_ext_i18n_css.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_i18n_css.html
@@ -10,17 +10,17 @@
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 add_task(function* test_i18n_css() {
   let extension = ExtensionTestUtils.loadExtension({
-    background: "new " + function() {
+    background: function() {
       function fetch(url) {
         return new Promise((resolve, reject) => {
           let xhr = new XMLHttpRequest();
           xhr.open("GET", url);
           xhr.onload = () => { resolve(xhr.responseText); };
           xhr.onerror = reject;
           xhr.send();
         });
--- a/toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_inIncognitoContext_window.html
@@ -23,23 +23,22 @@ add_task(function* test_in_incognito_con
     browser.windows.create({url: browser.runtime.getURL("/tab.html"), incognito: true});
   }
 
   function tabScript() {
     browser.runtime.sendMessage(browser.extension.inIncognitoContext);
   }
 
   let extension = ExtensionTestUtils.loadExtension({
-    background: `(${background})()`,
-    manifest: {},
+    background,
     files: {
-      "tab.js": `(${tabScript})()`,
+      "tab.js": tabScript,
       "tab.html": `<!DOCTYPE html><html><head>
         <meta charset="utf-8">
-        <script src="tab.js"></${"script"}>
+        <script src="tab.js"><\/script>
       </head></html>`,
     },
   });
 
   yield extension.startup();
   yield extension.awaitFinish("inIncognitoContext");
   yield extension.unload();
 });
--- a/toolkit/components/extensions/test/mochitest/test_ext_jsversion.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_jsversion.html
@@ -13,49 +13,49 @@
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 add_task(function* test_versioned_js() {
   // We need to deal with escaping the close script tags.
   // May as well consolidate it into one place.
-  let script = attrs => `<script ${attrs}></${"script"}>`;
+  let script = attrs => `<script ${attrs}><\/script>`;
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "background": {"page": "background.html"},
     },
 
     files: {
       "background.html": `
         <meta charset="utf-8">
         ${script('src="background.js" type="application/javascript"')}
         ${script('src="background-1.js" type="application/javascript;version=1.8"')}
         ${script('src="background-2.js" type="application/javascript;version=latest"')}
         ${script('src="background-3.js" type="application/javascript"')}
       `,
 
-      "background.js": "new " + function() {
+      "background.js": function() {
         window.reportResult = msg => {
           browser.test.assertEq(
             msg, "background-script-3",
             "Expected a message only from the unversioned background script.");
 
           browser.test.sendMessage("finished");
         };
       },
 
-      "background-1.js": "new " + function() {
+      "background-1.js": function() {
         window.reportResult("background-script-1");
       },
-      "background-2.js": "new " + function() {
+      "background-2.js": function() {
         window.reportResult("background-script-2");
       },
-      "background-3.js": "new " + function() {
+      "background-3.js": function() {
         window.reportResult("background-script-3");
       },
     },
   });
 
   let messages = [/Versioned JavaScript.*not supported in WebExtension.*developer\.mozilla\.org/,
                   /Versioned JavaScript.*not supported in WebExtension.*developer\.mozilla\.org/];
 
--- a/toolkit/components/extensions/test/mochitest/test_ext_notifications.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_notifications.html
@@ -14,44 +14,44 @@
 
 // A 1x1 PNG image.
 // Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain)
 let image = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
                  "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=");
 const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
 
 add_task(function* test_notification() {
-  function backgroundScript() {
+  function background() {
     let opts = {
       type: "basic",
       title: "Testing Notification",
       message: "Carry on",
     };
 
     browser.notifications.create(opts).then(id => {
       browser.test.sendMessage("running", id);
       browser.test.notifyPass("background test passed");
     });
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["notifications"],
     },
-    background: `(${backgroundScript})()`,
+    background,
   });
   yield extension.startup();
   let x = yield extension.awaitMessage("running");
   is(x, "0", "got correct id from notifications.create");
   yield extension.awaitFinish();
   yield extension.unload();
 });
 
 add_task(function* test_notification_events() {
-  function backgroundScript() {
+  function background() {
     let opts = {
       type: "basic",
       title: "Testing Notification",
       message: "Carry on",
     };
 
     // Test an ignored listener.
     browser.notifications.onButtonClicked.addListener(function() {});
@@ -72,29 +72,29 @@ add_task(function* test_notification_eve
       browser.test.sendMessage("running", id);
     });
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["notifications"],
     },
-    background: `(${backgroundScript})()`,
+    background,
   });
   yield extension.startup();
   let x = yield extension.awaitMessage("closed");
   is(x, "5", "got correct id from onClosed listener");
   x = yield extension.awaitMessage("running");
   is(x, "5", "got correct id from notifications.create");
   yield extension.awaitFinish();
   yield extension.unload();
 });
 
 add_task(function* test_notification_clear() {
-  function backgroundScript() {
+  function background() {
     let opts = {
       type: "basic",
       title: "Testing Notification",
       message: "Carry on",
     };
 
     browser.notifications.onClosed.addListener(id => {
       browser.test.sendMessage("closed", id);
@@ -107,49 +107,49 @@ add_task(function* test_notification_cle
       browser.test.notifyPass("background test passed");
     });
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["notifications"],
     },
-    background: `(${backgroundScript})()`,
+    background,
   });
   yield extension.startup();
   let x = yield extension.awaitMessage("closed");
   is(x, "99", "got correct id from onClosed listener");
   x = yield extension.awaitMessage("cleared");
   is(x, true, "got correct boolean from notifications.clear");
   yield extension.awaitFinish();
   yield extension.unload();
 });
 
 add_task(function* test_notifications_empty_getAll() {
-  function backgroundScript() {
+  function background() {
     browser.notifications.getAll().then(notifications => {
       browser.test.assertEq("object", typeof notifications, "getAll() returned an object");
       browser.test.assertEq(0, Object.keys(notifications).length, "the object has no properties");
       browser.test.notifyPass("getAll empty");
     });
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["notifications"],
     },
-    background: `(${backgroundScript})()`,
+    background,
   });
   yield extension.startup();
   yield extension.awaitFinish("getAll empty");
   yield extension.unload();
 });
 
 add_task(function* test_notifications_populated_getAll() {
-  function backgroundScript() {
+  function background() {
     let opts = {
       type: "basic",
       iconUrl: "a.png",
       title: "Testing Notification",
       message: "Carry on",
     };
 
     browser.notifications.create("p1", opts).then(() => {
@@ -171,28 +171,28 @@ add_task(function* test_notifications_po
       browser.test.notifyPass("getAll populated");
     });
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["notifications"],
     },
-    background: `(${backgroundScript})()`,
+    background,
     files: {
       "a.png": IMAGE_ARRAYBUFFER,
     },
   });
   yield extension.startup();
   yield extension.awaitFinish("getAll populated");
   yield extension.unload();
 });
 
 add_task(function* test_buttons_unsupported() {
-  function backgroundScript() {
+  function background() {
     let opts = {
       type: "basic",
       title: "Testing Notification",
       message: "Carry on",
       buttons: [{title: "Button title"}],
     };
 
     let exception = {};
@@ -208,17 +208,17 @@ add_task(function* test_buttons_unsuppor
     );
     browser.test.notifyPass("buttons-unsupported");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["notifications"],
     },
-    background: `(${backgroundScript})()`,
+    background,
   });
   yield extension.startup();
   yield extension.awaitFinish("buttons-unsupported");
   yield extension.unload();
 });
 
 </script>
 
--- a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect.html
@@ -8,17 +8,17 @@
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-function backgroundScript() {
+function background() {
   browser.runtime.onConnect.addListener(port => {
     browser.test.assertEq(port.name, "ernie", "port name correct");
     browser.test.assertTrue(port.sender.url.endsWith("file_sample.html"), "URL correct");
     browser.test.assertTrue(port.sender.tab.url.endsWith("file_sample.html"), "tab URL correct");
 
     let expected = "message 1";
     port.onMessage.addListener(msg => {
       browser.test.assertEq(msg, expected, "message is expected");
@@ -43,28 +43,28 @@ function contentScript() {
     if (msg == "message 2") {
       port.postMessage("message 3");
       port.disconnect();
     }
   });
 }
 
 let extensionData = {
-  background: "(" + backgroundScript.toString() + ")()",
+  background,
   manifest: {
     "permissions": ["tabs"],
     "content_scripts": [{
       "matches": ["http://mochi.test/*/file_sample.html"],
       "js": ["content_script.js"],
       "run_at": "document_start",
     }],
   },
 
   files: {
-    "content_script.js": "(" + contentScript.toString() + ")()",
+    "content_script.js": contentScript,
   },
 };
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
 
   let win = window.open("file_sample.html");
--- a/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_connect_twoway.html
@@ -9,17 +9,17 @@
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
 </head>
 <body>
 
 <script>
 "use strict";
 
 add_task(function* test_connect_bidirectionally_and_postMessage() {
-  function backgroundScript() {
+  function background() {
     let onConnectCount = 0;
     browser.runtime.onConnect.addListener(port => {
       // 3. onConnect by connect() from CS.
       browser.test.assertEq("from-cs", port.name);
       browser.test.assertEq(1, ++onConnectCount,
           "BG onConnect should be called once");
 
       let tabId = port.sender.tab.id;
@@ -93,25 +93,25 @@ add_task(function* test_connect_bidirect
         "CS port.onMessage should be called once");
 
       // 10. should trigger port.onMessage in BG.
       port.postMessage("from CS to port");
     });
   }
 
   let extensionData = {
-    background: `(${backgroundScript})();`,
+    background,
     manifest: {
       content_scripts: [{
         js: ["contentscript.js"],
         matches: ["http://mochi.test/*/file_sample.html"],
       }],
     },
     files: {
-      "contentscript.js": `(${contentScript})();`,
+      "contentscript.js": contentScript,
     },
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
   info("extension loaded");
 
   yield extension.awaitMessage("ready");
--- a/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_disconnect.html
@@ -8,17 +8,17 @@
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-function backgroundScript() {
+function background() {
   browser.runtime.onConnect.addListener(port => {
     browser.test.assertEq(port.name, "ernie", "port name correct");
     port.onDisconnect.addListener(() => {
       // Closing an already-disconnected port is a no-op.
       port.disconnect();
       port.disconnect();
       browser.test.sendMessage("disconnected");
     });
@@ -26,28 +26,28 @@ function backgroundScript() {
   });
 }
 
 function contentScript() {
   browser.runtime.connect({name: "ernie"});
 }
 
 let extensionData = {
-  background: "(" + backgroundScript.toString() + ")()",
+  background,
   manifest: {
     "permissions": ["tabs"],
     "content_scripts": [{
       "matches": ["http://mochi.test/*/file_sample.html"],
       "js": ["content_script.js"],
       "run_at": "document_idle",
     }],
   },
 
   files: {
-    "content_script.js": "(" + contentScript.toString() + ")()",
+    "content_script.js": contentScript,
   },
 };
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
 
   let win = window.open("file_sample.html");
--- a/toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html
@@ -31,20 +31,20 @@ add_task(function* test_runtime_id() {
       applications: {gecko: {id}},
       "content_scripts": [{
         "matches": ["http://mochi.test/*/file_sample.html"],
         "run_at": "document_start",
         "js": ["content_script.js"],
       }],
     },
 
-    background: `(${background})()`,
+    background,
 
     files: {
-      "content_script.js": `(${content})()`,
+      "content_script.js": content,
     },
   });
 
   yield extension.startup();
 
   let backgroundId = yield extension.awaitMessage("background-id");
   is(backgroundId, id, "runtime.id from background script is correct");
   let win = window.open("file_sample.html");
--- a/toolkit/components/extensions/test/mochitest/test_ext_sandbox_var.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sandbox_var.html
@@ -8,41 +8,41 @@
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-function backgroundScript() {
+function background() {
   browser.runtime.onMessage.addListener(result => {
     browser.test.assertEq(result, 12, "x is 12");
     browser.test.notifyPass("background test passed");
   });
 }
 
 function contentScript() {
   window.x = 12;
   browser.runtime.onMessage.addListener(function() {});
   browser.runtime.sendMessage(window.x);
 }
 
 let extensionData = {
-  background: "(" + backgroundScript.toString() + ")()",
+  background,
   manifest: {
     "content_scripts": [{
       "matches": ["http://mochi.test/*/file_sample.html"],
       "js": ["content_script.js"],
       "run_at": "document_idle",
     }],
   },
 
   files: {
-    "content_script.js": "(" + contentScript.toString() + ")()",
+    "content_script.js": contentScript,
   },
 };
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
 
   let win = window.open("file_sample.html");
--- a/toolkit/components/extensions/test/mochitest/test_ext_schema.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_schema.html
@@ -18,17 +18,17 @@ add_task(function* testEmptySchema() {
   function background() {
     browser.test.assertTrue(!("manifest" in browser), "browser.manifest is not defined");
     browser.test.assertTrue("storage" in browser, "browser.storage should be defined");
     browser.test.assertTrue(!("contextMenus" in browser), "browser.contextMenus should not be defined");
     browser.test.notifyPass("schema");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
-    background: `(${background})()`,
+    background,
     manifest: {
       permissions: ["storage"],
     },
   });
 
   yield extension.startup();
   yield extension.awaitFinish("schema");
   yield extension.unload();
@@ -41,17 +41,17 @@ add_task(function* testUnknownProperties
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["unknownPermission"],
 
       unknown_property: {},
     },
 
-    background: `(${background})()`,
+    background,
   });
 
   let messages = [
     {message: /processing permissions\.0: Unknown permission "unknownPermission"/},
     {message: /processing unknown_property: An unexpected property was found in the WebExtension manifest/},
   ];
 
   let waitForConsole = new Promise(resolve => {
--- a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_doublereply.html
@@ -8,17 +8,17 @@
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-function backgroundScript() {
+function background() {
   // Add two listeners that both send replies. We're supposed to ignore all but one
   // of them. Which one is chosen is non-deterministic.
 
   browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
     browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
 
     if (msg == "getreply") {
       sendReply("reply1");
@@ -62,28 +62,28 @@ function contentScript() {
     if (resp != "reply1" && resp != "reply2") {
       return; // test failed
     }
     browser.runtime.sendMessage("done");
   });
 }
 
 let extensionData = {
-  background: "(" + backgroundScript.toString() + ")()",
+  background,
   manifest: {
     "permissions": ["tabs"],
     "content_scripts": [{
       "matches": ["http://mochi.test/*/file_sample.html"],
       "js": ["content_script.js"],
       "run_at": "document_start",
     }],
   },
 
   files: {
-    "content_script.js": "(" + contentScript.toString() + ")()",
+    "content_script.js": contentScript,
   },
 };
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
 
   let win = window.open("file_sample.html");
--- a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_no_receiver.html
@@ -17,17 +17,17 @@ function loadContentScriptExtension(cont
   let extensionData = {
     manifest: {
       "content_scripts": [{
         "js": ["contentscript.js"],
         "matches": ["http://mochi.test/*/file_sample.html"],
       }],
     },
     files: {
-      "contentscript.js": `(${contentScript})();`,
+      "contentscript.js": contentScript,
     },
   };
   return ExtensionTestUtils.loadExtension(extensionData);
 }
 
 add_task(function* test_content_script_sendMessage_without_listener() {
   function contentScript() {
     browser.runtime.sendMessage("msg").then(reply => {
--- a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply.html
@@ -8,17 +8,17 @@
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-function backgroundScript() {
+function background() {
   browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
     browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
 
     if (msg == 0) {
       sendReply("reply1");
     } else if (msg == 1) {
       window.setTimeout(function() {
         sendReply("reply2");
@@ -40,28 +40,28 @@ function contentScript() {
         return; // test failed
       }
       browser.runtime.sendMessage(2);
     });
   });
 }
 
 let extensionData = {
-  background: "(" + backgroundScript.toString() + ")()",
+  background,
   manifest: {
     "permissions": ["tabs"],
     "content_scripts": [{
       "matches": ["http://mochi.test/*/file_sample.html"],
       "js": ["content_script.js"],
       "run_at": "document_idle",
     }],
   },
 
   files: {
-    "content_script.js": "(" + contentScript.toString() + ")()",
+    "content_script.js": contentScript,
   },
 };
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
 
   let win = window.open("file_sample.html");
--- a/toolkit/components/extensions/test/mochitest/test_ext_storage_content.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_content.html
@@ -174,17 +174,17 @@ let extensionData = {
       "js": ["content_script.js"],
       "run_at": "document_idle",
     }],
 
     permissions: ["storage"],
   },
 
   files: {
-    "content_script.js": "(" + contentScript.toString() + ")()",
+    "content_script.js": contentScript,
   },
 };
 
 add_task(function* test_contentscript() {
   let win = window.open("file_sample.html");
   yield waitForLoad(win);
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
--- a/toolkit/components/extensions/test/mochitest/test_ext_storage_tab.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_storage_tab.html
@@ -81,28 +81,28 @@ add_task(function* test_multiple_pages()
         return browser.storage.local.set({key: {foo: {bar: "baz"}}});
       }
     });
 
     browser.runtime.sendMessage("tab-ready");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
-    background: `(${background})()`,
+    background,
 
     files: {
       "tab.html": `<!DOCTYPE html>
         <html>
           <head>
             <meta charset="utf-8">
-            <script src="tab.js"></${"script"}>
+            <script src="tab.js"><\/script>
           </head>
         </html>`,
 
-      "tab.js": `(${tab})()`,
+      "tab.js": tab,
     },
 
     manifest: {
       permissions: ["storage"],
     },
   });
 
   yield extension.startup();
--- a/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_subframes_privileges.html
@@ -9,17 +9,17 @@
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 add_task(function* test_webext_tab_subframe_privileges() {
-  function backgroundScript() {
+  function background() {
     browser.runtime.onMessage.addListener(({msg, success, tabId, error}) => {
       if (msg == "webext-tab-subframe-privileges") {
         if (success) {
           browser.tabs.remove(tabId)
             .then(() => browser.test.notifyPass(msg));
         } else {
           browser.test.log(`Got an unexpected error: ${error}`);
           browser.tabs.query({active: true})
@@ -52,33 +52,33 @@ add_task(function* test_webext_tab_subfr
         msg: "webext-tab-subframe-privileges",
         success: false,
         error: `Privileged APIs missing in WebExtension tab sub-frame`,
       });
     }
   }
 
   let extensionData = {
-    background: "new " + backgroundScript,
+    background,
     files: {
       "tab.html": `<!DOCTYPE>
           <head>
             <meta charset="utf-8">
           </head>
           <body>
             <iframe src="tab-subframe.html"></iframe>
           </body>
         </html>`,
       "tab-subframe.html": `<!DOCTYPE>
           <head>
             <meta charset="utf-8">
-            <script src="tab-subframe.js"></${"script"}>
+            <script src="tab-subframe.js"><\/script>
           </head>
         </html>`,
-      "tab-subframe.js": `(${tabSubframeScript})()`,
+      "tab-subframe.js": tabSubframeScript,
     },
   };
   let extension = ExtensionTestUtils.loadExtension(extensionData);
 
   yield extension.startup();
 
   yield extension.awaitFinish("webext-tab-subframe-privileges");
   yield extension.unload();
@@ -104,32 +104,32 @@ add_task(function* test_webext_backgroun
          </head>
          <body>
            <iframe src="background-subframe.html"></iframe>
          </body>
        </html>`,
       "background-subframe.html": `<!DOCTYPE>
          <head>
            <meta charset="utf-8">
-           <script src="background-subframe.js"></${"script"}>
+           <script src="background-subframe.js"><\/script>
          </head>
        </html>`,
-      "background-subframe.js": `(${backgroundSubframeScript})()`,
+      "background-subframe.js": backgroundSubframeScript,
     },
   };
   let extension = ExtensionTestUtils.loadExtension(extensionData);
 
   yield extension.startup();
 
   yield extension.awaitFinish("webext-background-subframe-privileges");
   yield extension.unload();
 });
 
 add_task(function* test_webext_contentscript_iframe_subframe_privileges() {
-  function backgroundScript() {
+  function background() {
     browser.runtime.onMessage.addListener(({name, hasTabsAPI, hasStorageAPI}) => {
       if (name == "contentscript-iframe-loaded") {
         browser.test.assertFalse(hasTabsAPI,
                                  "Subframe of a content script privileged iframes has no access to privileged APIs");
         browser.test.assertTrue(hasStorageAPI,
                                  "Subframe of a content script privileged iframes has access to content script APIs");
 
         browser.test.notifyPass("webext-contentscript-subframe-privileges");
@@ -147,44 +147,44 @@ add_task(function* test_webext_contentsc
 
   function contentScript() {
     let iframe = document.createElement("iframe");
     iframe.setAttribute("src", browser.runtime.getURL("/contentscript-iframe.html"));
     document.body.appendChild(iframe);
   }
 
   let extensionData = {
-    background: "new " + backgroundScript,
+    background,
     manifest: {
       "permissions": ["storage"],
       "content_scripts": [{
         "matches": ["http://example.com/*"],
         "js": ["contentscript.js"],
       }],
       web_accessible_resources: [
         "contentscript-iframe.html",
       ],
     },
     files: {
-      "contentscript.js": `(${contentScript})()`,
+      "contentscript.js": contentScript,
       "contentscript-iframe.html": `<!DOCTYPE>
          <head>
            <meta charset="utf-8">
          </head>
          <body>
            <iframe src="contentscript-iframe-subframe.html"></iframe>
          </body>
        </html>`,
       "contentscript-iframe-subframe.html": `<!DOCTYPE>
          <head>
            <meta charset="utf-8">
-           <script src="contentscript-iframe-subframe.js"></${"script"}>
+           <script src="contentscript-iframe-subframe.js"><\/script>
          </head>
        </html>`,
-      "contentscript-iframe-subframe.js": `(${subframeScript})()`,
+      "contentscript-iframe-subframe.js": subframeScript,
     },
   };
   let extension = ExtensionTestUtils.loadExtension(extensionData);
 
   yield extension.startup();
 
   let win = window.open("http://example.com");
 
--- a/toolkit/components/extensions/test/mochitest/test_ext_tab_teardown.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_tab_teardown.html
@@ -59,17 +59,17 @@ function* runTabReloadAndCloseTest(exten
       "ExtensionContext URL at closing tab should be tab URL");
 
   chromeScript.sendAsyncMessage("cleanup");
   chromeScript.destroy();
   yield extension.unload();
 }
 
 add_task(function* test_extension_page_tabs_create_reload_and_close() {
-  function backgroundScript() {
+  function background() {
     let tabId;
     browser.test.onMessage.addListener(msg => {
       if (msg === "open extension page") {
         chrome.tabs.create({url: "page.html"}, tab => {
           tabId = tab.id;
         });
       } else if (msg === "reload extension page") {
         chrome.tabs.reload(tabId);
@@ -81,35 +81,35 @@ add_task(function* test_extension_page_t
     });
   }
 
   function pageScript() {
     browser.test.sendMessage("extension page loaded", document.URL);
   }
 
   let extensionData = {
-    background: `(${backgroundScript})();`,
+    background,
     files: {
       "page.html": `<!DOCTYPE html><meta charset="utf-8"><script src="page.js"><\/script>`,
-      "page.js": `(${pageScript})();`,
+      "page.js": pageScript,
     },
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
 
   yield* runTabReloadAndCloseTest(extension);
 });
 
 add_task(function* test_extension_page_window_open_reload_and_close() {
   // This tests whether a context that is opened via window.open is properly
   // disposed when the tab closes.
   // The background page cannot use window.open (bugzil.la/1282021), so we open
   // another extension page that manages the window.open-tab for testing.
-  function backgroundScript() {
+  function background() {
     chrome.tabs.create({url: "window.open.html"});
   }
 
   function windowOpenScript() {
     let win;
     browser.test.onMessage.addListener(msg => {
       if (msg === "open extension page") {
         win = window.open("page.html");
@@ -126,22 +126,22 @@ add_task(function* test_extension_page_w
     browser.test.sendMessage("setup-intermediate-tab");
   }
 
   function pageScript() {
     browser.test.sendMessage("extension page loaded", document.URL);
   }
 
   let extensionData = {
-    background: `(${backgroundScript})();`,
+    background,
     files: {
       "page.html": `<!DOCTYPE html><meta charset="utf-8"><script src="page.js"><\/script>`,
-      "page.js": `(${pageScript})();`,
+      "page.js": pageScript,
       "window.open.html": `<!DOCTYPE html><meta charset="utf-8"><script src="window.open.js"><\/script>`,
-      "window.open.js": `(${windowOpenScript})();`,
+      "window.open.js": windowOpenScript,
     },
   };
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
   yield extension.awaitMessage("setup-intermediate-tab");
   yield* runTabReloadAndCloseTest(extension);
 });
 </script>
--- a/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html
@@ -136,43 +136,43 @@ add_task(function* test_web_accessible_r
       ],
 
       "web_accessible_resources": [
         "/accessible.html",
         "wild*.html",
       ],
     },
 
-    background: `(${background})()`,
+    background,
 
     files: {
-      "content_script.js": `(${contentScript})()`,
+      "content_script.js": contentScript,
 
       "accessible.html": `<html><head>
         <meta charset="utf-8">
-        <script src="accessible.js"></${"script"}>
+        <script src="accessible.js"><\/script>
       </head></html>`,
 
       "accessible.js": 'browser.runtime.sendMessage(["page-script", location.href]);',
 
       "inaccessible.html": `<html><head>
         <meta charset="utf-8">
-        <script src="inaccessible.js"></${"script"}>
+        <script src="inaccessible.js"><\/script>
       </head></html>`,
 
       "inaccessible.js": 'browser.runtime.sendMessage(["page-script", location.href]);',
 
       "wild1.html": `<html><head>
         <meta charset="utf-8">
-        <script src="wild.js"></${"script"}>
+        <script src="wild.js"><\/script>
       </head></html>`,
 
       "wild2.htm": `<html><head>
         <meta charset="utf-8">
-        <script src="wild.js"></${"script"}>
+        <script src="wild.js"><\/script>
       </head></html>`,
 
       "wild.js": 'browser.runtime.sendMessage(["page-script", location.href]);',
     },
   });
 
   yield extension.startup();
 
@@ -226,21 +226,21 @@ add_task(function* test_web_accessible_r
         "run_at": "document_start",
         "js": ["content_script_helper.js", "content_script.js"],
       }],
       "web_accessible_resources": [
         "image.png",
         "test_script.js",
       ],
     },
-    background: `(${background})()`,
+    background,
     files: {
       "content_script_helper.js": `${testImageLoading}`,
-      "content_script.js": `(${content})()`,
-      "test_script.js": `(${testScript})()`,
+      "content_script.js": content,
+      "test_script.js": testScript,
       "image.png": IMAGE_ARRAYBUFFER,
     },
   });
 
   // This is used to watch the blocked data bounce off CSP.
   function examiner() {
     SpecialPowers.addObserver(this, "csp-on-violate-policy", false);
   }
@@ -321,21 +321,21 @@ add_task(function* test_web_accessible_r
         "run_at": "document_start",
         "js": ["content_script_helper.js", "content_script.js"],
       }],
       "web_accessible_resources": [
         "image.png",
         "test_script.js",
       ],
     },
-    background: `(${background})()`,
+    background,
     files: {
       "content_script_helper.js": `${testImageLoading}`,
-      "content_script.js": `(${content})()`,
-      "test_script.js": `(${testScript})()`,
+      "content_script.js": content,
+      "test_script.js": testScript,
       "image.png": IMAGE_ARRAYBUFFER,
     },
   });
 
   SpecialPowers.setBoolPref("security.mixed_content.block_display_content", true);
 
   yield Promise.all([extension.startup(), extension.awaitMessage("background-ready")]);
 
--- a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
@@ -127,17 +127,17 @@ add_task(function* webnav_transitions_pr
   }
 
   let extensionData = {
     manifest: {
       permissions: [
         "webNavigation",
       ],
     },
-    background: `(${backgroundScriptTransitions})()`,
+    background: backgroundScriptTransitions,
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
 
   extension.onMessage("received", ({url, event, details}) => {
     received.push({url, event, details});
 
     if (event == waitingEvent && url == waitingURL) {
@@ -344,17 +344,17 @@ add_task(function* webnav_transitions_pr
 
 add_task(function* webnav_ordering() {
   let extensionData = {
     manifest: {
       permissions: [
         "webNavigation",
       ],
     },
-    background: "(" + backgroundScript.toString() + ")()",
+    background: backgroundScript,
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
 
   extension.onMessage("received", ({url, event}) => {
     received.push({url, event});
 
     if (event == waitingEvent && url == waitingURL) {
@@ -510,17 +510,17 @@ add_task(function* webnav_error_event() 
   }
 
   let extensionData = {
     manifest: {
       permissions: [
         "webNavigation",
       ],
     },
-    background: `(${backgroundScriptErrorEvent})()`,
+    background: backgroundScriptErrorEvent,
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
 
   extension.onMessage("received", ({url, event, details}) => {
     received.push({url, event, details});
 
     if (event == waitingEvent && url == waitingURL) {
--- a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation_filters.html
@@ -9,17 +9,17 @@
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 add_task(function* test_webnav_unresolved_uri_on_expected_URI_scheme() {
-  function backgroundScript() {
+  function background() {
     let lastTest;
 
     function cleanupTestListeners() {
       if (lastTest) {
         let {event, okListener, failListener} = lastTest;
         lastTest = null;
         browser.test.log(`Cleanup previous test event listeners`);
         browser.webNavigation[event].removeListener(okListener);
@@ -65,17 +65,17 @@ add_task(function* test_webnav_unresolve
   }
 
   let extensionData = {
     manifest: {
       permissions: [
         "webNavigation",
       ],
     },
-    background: "new " + backgroundScript,
+    background,
   };
 
   let extension = ExtensionTestUtils.loadExtension(extensionData);
 
   yield extension.startup();
 
   yield extension.awaitMessage("ready");
 
@@ -252,17 +252,17 @@ add_task(function* test_webnav_unresolve
   info("WebNavigation event filters test scenarios completed.");
 
   yield extension.unload();
 
   win.close();
 });
 
 add_task(function* test_webnav_empty_filter_validation_error() {
-  function backgroundScript() {
+  function background() {
     let catchedException;
 
     try {
       browser.webNavigation.onCompleted.addListener(
         // Empty callback (not really used)
         () => {},
         // Empty filter (which should raise a validation error exception).
         {url: []}
@@ -282,17 +282,17 @@ add_task(function* test_webnav_empty_fil
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: [
         "webNavigation",
       ],
     },
-    background: "new " + backgroundScript,
+    background,
   });
 
   yield extension.startup();
 
   yield extension.awaitFinish("webNav.emptyFilterValidationError");
 
   yield extension.unload();
 });
--- a/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts.js
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts.js
@@ -50,17 +50,17 @@ add_task(function* test_install_addon_th
     yield new Promise(resolve => setTimeout(resolve, 100));
     yield addon.run(topic, 10, realListener);
     // Waiting a little – listeners are buffered.
     yield new Promise(resolve => setTimeout(resolve, 100));
 
     Assert.ok(realListener.triggered, `1. The real listener was triggered ${topic}`);
     Assert.ok(universalListener.triggered, `1. The universal listener was triggered ${topic}`);
     Assert.ok(!fakeListener.triggered, `1. The fake listener was not triggered ${topic}`);
-    Assert.ok(realListener.result >= 200000, `1. jank is at least 300ms (${realListener.result/1000}ms) ${topic}`);
+    Assert.ok(realListener.result >= addon.jankThreshold, `1. jank is at least ${addon.jankThreshold/1000}ms (${realListener.result/1000}ms) ${topic}`);
 
     info(`Attempting to remove a performance listener incorrectly, check that this does not hurt our real listener ${topic}`);
     Assert.throws(() => PerformanceWatcher.removePerformanceListener({addonId: addon.addonId}, () => {}));
     Assert.throws(() => PerformanceWatcher.removePerformanceListener({addonId: addon.addonId + "-unbound-id-" + Math.random()}, realListener.listener));
 
     yield addon.run(topic, 10, realListener);
     // Waiting a little – listeners are buffered.
     yield new Promise(resolve => setTimeout(resolve, 300));
--- a/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts_2.js
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_addonPerformanceAlerts_2.js
@@ -13,13 +13,13 @@ add_task(function* test_watch_addon_then
       }
       throw new Error(`I shouldn't have been called with addon ${group.addonId}`);
     });
 
     info("Now install the add-on, *after* having installed the listener");
     let addon = new AddonBurner(addonId);
 
     Assert.ok((yield addon.run(topic, 10, realListener)), `5. The real listener was triggered ${topic}`);
-    Assert.ok(realListener.result >= 200000, `5. jank is at least 200ms (${realListener.result}µs) ${topic}`);
+    Assert.ok(realListener.result >= addon.jankThreshold, `5. jank is at least ${addon.jankThreshold/1000}ms (${realListener.result}µs) ${topic}`);
     realListener.unregister();
     addon.dispose();
   }
 });
--- a/toolkit/components/perfmonitoring/tests/browser/browser_webpagePerformanceAlerts.js
+++ b/toolkit/components/perfmonitoring/tests/browser/browser_webpagePerformanceAlerts.js
@@ -3,17 +3,17 @@
 /**
  * Tests for PerformanceWatcher watching slow web pages.
  */
 
  /**
   * Simulate a slow webpage.
   */
 function WebpageBurner() {
-  CPUBurner.call(this, "http://example.com/browser/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html?test=" + Math.random());
+  CPUBurner.call(this, "http://example.com/browser/toolkit/components/perfmonitoring/tests/browser/browser_compartments.html?test=" + Math.random(), 300000);
 }
 WebpageBurner.prototype = Object.create(CPUBurner.prototype);
 WebpageBurner.prototype.promiseBurnContentCPU = function() {
   return promiseContentResponse(this._browser, "test-performance-watcher:burn-content-cpu", {});
 };
 
 function WebpageListener(windowId, accept) {
   info(`Creating WebpageListener for ${windowId}`);
--- a/toolkit/components/perfmonitoring/tests/browser/head.js
+++ b/toolkit/components/perfmonitoring/tests/browser/head.js
@@ -3,46 +3,48 @@
 
 var { utils: Cu, interfaces: Ci, classes: Cc } = Components;
 
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/AddonManager.jsm", this);
 Cu.import("resource://gre/modules/AddonWatcher.jsm", this);
 Cu.import("resource://gre/modules/PerformanceWatcher.jsm", this);
 Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://testing-common/ContentTaskUtils.jsm", this);
 
 /**
  * Base class for simulating slow addons/webpages.
  */
-function CPUBurner(url) {
+function CPUBurner(url, jankThreshold) {
   info(`CPUBurner: Opening tab for ${url}\n`);
   this.url = url;
   this.tab = gBrowser.addTab(url);
+  this.jankThreshold = jankThreshold;
   let browser = this.tab.linkedBrowser;
   this._browser = browser;
   ContentTask.spawn(this._browser, null, CPUBurner.frameScript);
   this.promiseInitialized = BrowserTestUtils.browserLoaded(browser);
 }
 CPUBurner.prototype = {
   get windowId() {
     return this._browser.outerWindowID;
   },
   /**
-   * Burn CPU until it triggers a listener.
+   * Burn CPU until it triggers a listener with the specified jank threshold.
    */
   run: Task.async(function*(burner, max, listener) {
     listener.reset();
     for (let i = 0; i < max; ++i) {
       yield new Promise(resolve => setTimeout(resolve, 50));
       try {
         yield this[burner]();
       } catch (ex) {
         return false;
       }
-      if (listener.triggered) {
+      if (listener.triggered && listener.result >= this.jankThreshold) {
         return true;
       }
     }
     return false;
   }),
   dispose: function() {
     info(`CPUBurner: Closing tab for ${this.url}\n`);
     gBrowser.removeTab(this.tab);
@@ -156,17 +158,18 @@ AlertListener.prototype = {
     this.result = null;
   },
 };
 
 /**
  * Simulate a slow add-on.
  */
 function AddonBurner(addonId = "fake add-on id: " + Math.random()) {
-  CPUBurner.call(this, `http://example.com/?uri=${addonId}`)
+  this.jankThreshold = 200000;
+  CPUBurner.call(this, `http://example.com/?uri=${addonId}`, this.jankThreshold);
   this._addonId = addonId;
   this._sandbox = Components.utils.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), { addonId: this._addonId });
   this._CPOWBurner = null;
 
   this._promiseCPOWBurner = new Promise(resolve => {
     this._browser.messageManager.addMessageListener("test-performance-watcher:cpow-init", msg => {
       // Note that we cannot resolve Promises with CPOWs now that they
       // have been outlawed in bug 1233497, so we stash it in the
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -8204,52 +8204,16 @@
     "n_buckets": 20,
     "description": "Tracking how ServiceWorkerRegistrar loads data before the first content is shown. File bugs in Core::DOM in case of a Telemetry regression."
   },
   "SERVICE_WORKER_REQUEST_PASSTHROUGH": {
     "expires_in_version": "50",
     "kind": "boolean",
     "description": "Intercepted fetch sending back same Request object. File bugs in Core::DOM in case of a Telemetry regression."
   },
-  "LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS": {
-    "expires_in_version": "never",
-    "kind": "boolean",
-    "description": "Stores 1 if generating a call URL succeeded, and 0 if it failed."
-  },
-  "LOOP_CLIENT_CALL_URL_SHARED": {
-    "expires_in_version": "never",
-    "kind": "boolean",
-    "description": "Stores 1 every time the URL is copied or shared."
-  },
-  "LOOP_ROOM_CREATE": {
-    "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
-    "expires_in_version": "54",
-    "kind": "enumerated",
-    "n_values": 4,
-    "releaseChannelCollection": "opt-out",
-    "description": "Number of times a room create action is performed (0=CREATE_SUCCESS, 1=CREATE_FAIL)"
-  },
-  "LOOP_COPY_PANEL_ACTIONS": {
-    "alert_emails": ["firefox-dev@mozilla.org", "edilee@mozilla.com"],
-    "expires_in_version": "54",
-    "kind": "enumerated",
-    "n_values": 5,
-    "releaseChannelCollection": "opt-out",
-    "bug_numbers": [1239965, 1259506],
-    "description": "Number of times each of the following copy panel actions are triggered: 0=SHOWN, 1=NO_AGAIN, 2=NO_NEVER, 3=YES_AGAIN, 4=YES_NEVER"
-  },
-  "LOOP_ACTIVITY_COUNTER": {
-    "alert_emails": ["firefox-dev@mozilla.org", "mbanner@mozilla.com"],
-    "expires_in_version": "54",
-    "kind": "enumerated",
-    "n_values": 5,
-    "releaseChannelCollection": "opt-out",
-    "bug_numbers": [1208416],
-    "description": "Number of times an action is performed by an user (0=OPEN_PANEL, 1=OPEN_CONVERSATION, 2=ROOM_OPEN, 3=ROOM_SHARE, 4=ROOM_DELETE)"
-  },
   "E10S_STATUS": {
     "alert_emails": ["firefox-dev@mozilla.org"],
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values": 12,
     "releaseChannelCollection": "opt-out",
     "bug_numbers": [1241294],
     "description": "Why e10s is enabled or disabled (0=ENABLED_BY_USER, 1=ENABLED_BY_DEFAULT, 2=DISABLED_BY_USER, 3=DISABLED_IN_SAFE_MODE, 4=DISABLED_FOR_ACCESSIBILITY, 5=DISABLED_FOR_MAC_GFX, 6=DISABLED_FOR_BIDI, 7=DISABLED_FOR_ADDONS, 8=FORCE_DISABLED, 9=DISABLED_FOR_XPLAYERS, 10=DISABLED_FOR_OS_VERSION)"
--- a/toolkit/components/telemetry/histogram-whitelists.json
+++ b/toolkit/components/telemetry/histogram-whitelists.json
@@ -402,18 +402,16 @@
     "LOCALDOMSTORAGE_SHUTDOWN_DATABASE_MS",
     "LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS",
     "LONG_REFLOW_INTERRUPTIBLE",
     "LOOP_AUDIO_QUALITY_INBOUND_BANDWIDTH_KBITS",
     "LOOP_AUDIO_QUALITY_OUTBOUND_BANDWIDTH_KBITS",
     "LOOP_AUDIO_QUALITY_OUTBOUND_RTT",
     "LOOP_CALL_DURATION",
     "LOOP_CALL_TYPE",
-    "LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS",
-    "LOOP_CLIENT_CALL_URL_SHARED",
     "LOOP_DATACHANNEL_NEGOTIATED",
     "LOOP_GET_USER_MEDIA_TYPE",
     "LOOP_ICE_ADD_CANDIDATE_ERRORS_GIVEN_FAILURE",
     "LOOP_ICE_ADD_CANDIDATE_ERRORS_GIVEN_SUCCESS",
     "LOOP_ICE_FAILURE_TIME",
     "LOOP_ICE_FINAL_CONNECTION_STATE",
     "LOOP_ICE_LATE_TRICKLE_ARRIVAL_TIME",
     "LOOP_ICE_ON_TIME_TRICKLE_ARRIVAL_TIME",
@@ -1297,34 +1295,31 @@
     "LOCALDOMSTORAGE_SHUTDOWN_DATABASE_MS",
     "LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS",
     "LONG_REFLOW_INTERRUPTIBLE",
     "LOOP_AUDIO_QUALITY_INBOUND_BANDWIDTH_KBITS",
     "LOOP_AUDIO_QUALITY_OUTBOUND_BANDWIDTH_KBITS",
     "LOOP_AUDIO_QUALITY_OUTBOUND_RTT",
     "LOOP_CALL_DURATION",
     "LOOP_CALL_TYPE",
-    "LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS",
-    "LOOP_CLIENT_CALL_URL_SHARED",
     "LOOP_DATACHANNEL_NEGOTIATED",
     "LOOP_GET_USER_MEDIA_TYPE",
     "LOOP_ICE_ADD_CANDIDATE_ERRORS_GIVEN_FAILURE",
     "LOOP_ICE_ADD_CANDIDATE_ERRORS_GIVEN_SUCCESS",
     "LOOP_ICE_FAILURE_TIME",
     "LOOP_ICE_FINAL_CONNECTION_STATE",
     "LOOP_ICE_LATE_TRICKLE_ARRIVAL_TIME",
     "LOOP_ICE_ON_TIME_TRICKLE_ARRIVAL_TIME",
     "LOOP_ICE_SUCCESS_RATE",
     "LOOP_ICE_SUCCESS_TIME",
     "LOOP_MAX_AUDIO_RECEIVE_TRACK",
     "LOOP_MAX_AUDIO_SEND_TRACK",
     "LOOP_MAX_VIDEO_RECEIVE_TRACK",
     "LOOP_MAX_VIDEO_SEND_TRACK",
     "LOOP_RENEGOTIATIONS",
-    "LOOP_ROOM_CREATE",
     "LOOP_VIDEO_DECODER_BITRATE_AVG_PER_CALL_KBPS",
     "LOOP_VIDEO_DECODER_BITRATE_STD_DEV_PER_CALL_KBPS",
     "LOOP_VIDEO_DECODER_DISCARDED_PACKETS_PER_CALL_PPM",
     "LOOP_VIDEO_DECODER_FRAMERATE_10X_STD_DEV_PER_CALL",
     "LOOP_VIDEO_DECODER_FRAMERATE_AVG_PER_CALL",
     "LOOP_VIDEO_DECODE_ERROR_TIME_PERMILLE",
     "LOOP_VIDEO_ENCODER_BITRATE_AVG_PER_CALL_KBPS",
     "LOOP_VIDEO_ENCODER_BITRATE_STD_DEV_PER_CALL_KBPS",
--- a/widget/gtk/nsClipboard.cpp
+++ b/widget/gtk/nsClipboard.cpp
@@ -1025,17 +1025,17 @@ wait_for_text(GtkClipboard *clipboard)
     return static_cast<gchar*>(context->Wait());
 }
 
 static GdkFilterReturn
 selection_request_filter(GdkXEvent *gdk_xevent, GdkEvent *event, gpointer data)
 {
     XEvent *xevent = static_cast<XEvent*>(gdk_xevent);
     if (xevent->xany.type == SelectionRequest) {
-        if (xevent->xselectionrequest.requestor == None)
+        if (xevent->xselectionrequest.requestor == X11None)
             return GDK_FILTER_REMOVE;
 
         GdkDisplay *display = gdk_x11_lookup_xdisplay(
                 xevent->xselectionrequest.display);
         if (!display)
             return GDK_FILTER_REMOVE;
 
         GdkWindow *window = gdk_x11_window_foreign_new_for_display(display,
--- a/widget/gtk/nsWindow.cpp
+++ b/widget/gtk/nsWindow.cpp
@@ -447,17 +447,17 @@ nsWindow::nsWindow()
     mSizeState           = nsSizeMode_Normal;
     mLastSizeMode        = nsSizeMode_Normal;
     mSizeConstraints.mMaxSize = GetSafeWindowSize(mSizeConstraints.mMaxSize);
 
 #ifdef MOZ_X11
     mOldFocusWindow      = 0;
 
     mXDisplay = nullptr;
-    mXWindow  = None;
+    mXWindow  = X11None;
     mXVisual  = nullptr;
     mXDepth   = 0;
 #endif /* MOZ_X11 */
     mPluginType          = PluginType_NONE;
 
     if (!gGlobalsInitialized) {
         gGlobalsInitialized = true;
 
@@ -4582,17 +4582,17 @@ nsWindow::ClearTransparencyBitmap()
 
 #ifdef MOZ_X11
     if (!mGdkWindow)
         return;
 
     Display* xDisplay = GDK_WINDOW_XDISPLAY(mGdkWindow);
     Window xWindow = gdk_x11_window_get_xid(mGdkWindow);
 
-    XShapeCombineMask(xDisplay, xWindow, ShapeBounding, 0, 0, None, ShapeSet);
+    XShapeCombineMask(xDisplay, xWindow, ShapeBounding, 0, 0, X11None, ShapeSet);
 #endif
 }
 
 nsresult
 nsWindow::UpdateTranslucentWindowAlphaInternal(const nsIntRect& aRect,
                                                uint8_t* aAlphas, int32_t aStride)
 {
     if (!mShell) {
--- a/widget/tests/test_keycodes.xul
+++ b/widget/tests/test_keycodes.xul
@@ -3239,26 +3239,26 @@ function* runXULKeyTests()
                           modifiers:{metaKey:1}, chars:"'", unmodifiedChars:"'"},
                          "reservedUnshiftedKey");
     yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_Quote,
                           modifiers:{metaKey:1, shiftKey:1}, chars:"\"", unmodifiedChars:"'"},
                          "reservedShiftedKey");
   }
   else if (IS_WIN) {
     yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1,
-                          modifiers:{ctrlKey:1}, chars:";"},
+                          modifiers:{ctrlKey:1}, chars:""},
                          "unshiftedKey");
     yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_1,
-                          modifiers:{ctrlKey:1, shiftKey:1}, chars:";"},
+                          modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
                          "shiftedKey");
     yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7,
-                          modifiers:{ctrlKey:1}, chars:"'"},
+                          modifiers:{ctrlKey:1}, chars:""},
                          "reservedUnshiftedKey");
     yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_OEM_7,
-                          modifiers:{ctrlKey:1, shiftKey:1}, chars:"'"},
+                          modifiers:{ctrlKey:1, shiftKey:1}, chars:""},
                          "reservedShiftedKey");
   }
 
   // 429160
   if (IS_MAC) {
     yield testKeyElement({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_F,
                           modifiers:{metaKey:1, altKey:1}, chars:"\u0192", unmodifiedChars:"f"},
                          "commandOptionF");
@@ -3434,17 +3434,17 @@ function* runReservedKeyTests()
 
   if (IS_MAC) {
     // Cmd+T is reserved for opening new tab.
     yield testReservedKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:MAC_VK_ANSI_T,
                            modifiers:{metaKey:1}, chars:"t", unmodifiedChars:"t"});
   } else if (IS_WIN) {
     // Ctrl+T is reserved for opening new tab.
     yield testReservedKey({layout:KEYBOARD_LAYOUT_EN_US, keyCode:WIN_VK_T,
-                           modifiers:{ctrlKey:1}, chars:"t"});
+                           modifiers:{ctrlKey:1}, chars:"\u0014"});
   }
 
   finializeKeyElementTest();
 }
 
 function* runTextInputTests()
 {
   var textbox = document.getElementById("textbox");
--- a/widget/windows/KeyboardLayout.cpp
+++ b/widget/windows/KeyboardLayout.cpp
@@ -690,16 +690,17 @@ NativeKey::NativeKey(nsWindowBase* aWidg
   , mModKeyState(aModKeyState)
   , mVirtualKeyCode(0)
   , mOriginalVirtualKeyCode(0)
   , mShiftedLatinChar(0)
   , mUnshiftedLatinChar(0)
   , mScanCode(0)
   , mIsExtended(false)
   , mIsDeadKey(false)
+  , mIsFollowedByNonControlCharMessage(false)
   , mFakeCharMsgs(aFakeCharMsgs && aFakeCharMsgs->Length() ?
                     aFakeCharMsgs : nullptr)
 {
   MOZ_ASSERT(aWidget);
   MOZ_ASSERT(mDispatcher);
   KeyboardLayout* keyboardLayout = KeyboardLayout::GetInstance();
   mKeyboardLayout = keyboardLayout->GetLayout();
   if (aOverrideKeyboardLayout && mKeyboardLayout != aOverrideKeyboardLayout) {
@@ -874,28 +875,35 @@ NativeKey::NativeKey(nsWindowBase* aWidg
   }
 
   if (!mVirtualKeyCode) {
     mVirtualKeyCode = mOriginalVirtualKeyCode;
   }
 
   mDOMKeyCode =
     keyboardLayout->ConvertNativeKeyCodeToDOMKeyCode(mOriginalVirtualKeyCode);
-  mKeyNameIndex =
+  // Be aware, keyboard utilities can change non-printable keys to printable
+  // keys.  In such case, we should make the key value as a printable key.
+  mIsFollowedByNonControlCharMessage =
+    IsKeyDownMessage() && IsFollowedByNonControlCharMessage();
+  mKeyNameIndex = mIsFollowedByNonControlCharMessage ?
+    KEY_NAME_INDEX_USE_STRING :
     keyboardLayout->ConvertNativeKeyCodeToKeyNameIndex(mOriginalVirtualKeyCode);
   mCodeNameIndex =
     KeyboardLayout::ConvertScanCodeToCodeNameIndex(
       GetScanCodeWithExtendedFlag());
 
   keyboardLayout->InitNativeKey(*this, mModKeyState);
 
   mIsDeadKey =
     (IsFollowedByDeadCharMessage() ||
      keyboardLayout->IsDeadKey(mOriginalVirtualKeyCode, mModKeyState));
-  mIsPrintableKey = KeyboardLayout::IsPrintableCharKey(mOriginalVirtualKeyCode);
+  mIsPrintableKey =
+    mKeyNameIndex == KEY_NAME_INDEX_USE_STRING ||
+    KeyboardLayout::IsPrintableCharKey(mOriginalVirtualKeyCode);
 
   if (IsKeyDownMessage()) {
     // Compute some strings which may be inputted by the key with various
     // modifier state if this key event won't cause text input actually.
     // They will be used for setting mAlternativeCharCodes in the callback
     // method which will be called by TextEventDispatcher.
     if (NeedsToHandleWithoutFollowingCharMessages()) {
       ComputeInputtingStringWithKeyboardLayout();
@@ -1050,16 +1058,34 @@ NativeKey::IsFollowedByDeadCharMessage()
                                PM_NOREMOVE | PM_NOYIELD)) {
       return false;
     }
   }
   return IsDeadCharMessage(nextMsg);
 }
 
 bool
+NativeKey::IsFollowedByNonControlCharMessage() const
+{
+  MSG nextMsg;
+  if (mFakeCharMsgs) {
+    nextMsg = mFakeCharMsgs->ElementAt(0).GetCharMsg(mMsg.hwnd);
+  } else if (IsKeyMessageOnPlugin()) {
+    return false;
+  } else {
+    if (!WinUtils::PeekMessage(&nextMsg, mMsg.hwnd, WM_KEYFIRST, WM_KEYLAST,
+                               PM_NOREMOVE | PM_NOYIELD)) {
+      return false;
+    }
+  }
+  return nextMsg.message == WM_CHAR &&
+         !IsControlChar(static_cast<char16_t>(nextMsg.wParam));
+}
+
+bool
 NativeKey::IsIMEDoingKakuteiUndo() const
 {
   // Following message pattern is caused by "Kakutei-Undo" of ATOK or WXG:
   // ---------------------------------------------------------------------------
   // WM_KEYDOWN              * n (wParam = VK_BACK, lParam = 0x1)
   // WM_KEYUP                * 1 (wParam = VK_BACK, lParam = 0xC0000001) # ATOK
   // WM_IME_STARTCOMPOSITION * 1 (wParam = 0x0, lParam = 0x0)
   // WM_IME_COMPOSITION      * 1 (wParam = 0x0, lParam = 0x1BF)
@@ -1838,16 +1864,23 @@ NativeKey::NeedsToHandleWithoutFollowing
   }
 
   // If inputting two or more characters, should be dispatched after removing
   // whole following char messages.
   if (mCommittedCharsAndModifiers.mLength > 1) {
     return true;
   }
 
+  // If keydown message is followed by WM_CHAR whose wParam isn't a control
+  // character, we should dispatch keypress event with the char message
+  // even with any modifier state.
+  if (mIsFollowedByNonControlCharMessage) {
+    return false;
+  }
+
   // If any modifier keys which may cause printable keys becoming non-printable
   // are not pressed, we don't need special handling for the key.
   if (!mModKeyState.IsControl() && !mModKeyState.IsAlt() &&
       !mModKeyState.IsWin()) {
     return false;
   }
 
   // If the key event causes dead key event, we don't need to dispatch keypress
@@ -2517,16 +2550,43 @@ KeyboardLayout::IsDeadKey(uint8_t aVirtu
   if (virtualKeyIndex < 0) {
     return false;
   }
 
   return mVirtualKeys[virtualKeyIndex].IsDeadKey(
            VirtualKey::ModifiersToShiftState(aModKeyState.GetModifiers()));
 }
 
+bool
+KeyboardLayout::IsSysKey(uint8_t aVirtualKey,
+                         const ModifierKeyState& aModKeyState) const
+{
+  // If Alt key is not pressed, it's never a system key combination.
+  // Additionally, if Ctrl key is pressed, it's never a system key combination
+  // too.
+  if (!aModKeyState.IsAlt() || aModKeyState.IsControl()) {
+    return false;
+  }
+
+  int32_t virtualKeyIndex = GetKeyIndex(aVirtualKey);
+  if (virtualKeyIndex < 0) {
+    return true;
+  }
+
+  UniCharsAndModifiers inputCharsAndModifiers =
+    GetUniCharsAndModifiers(aVirtualKey, aModKeyState);
+  if (inputCharsAndModifiers.IsEmpty()) {
+    return true;
+  }
+
+  // If the Alt key state isn't consumed, that means that the key with Alt
+  // doesn't cause text input.  So, the combination is a system key.
+  return inputCharsAndModifiers.mModifiers[0] != MODIFIER_ALT;
+}
+
 void
 KeyboardLayout::InitNativeKey(NativeKey& aNativeKey,
                               const ModifierKeyState& aModKeyState)
 {
   if (mIsPendingToRestoreKeyboardLayout) {
     LoadLayout(::GetKeyboardLayout(0));
   }
 
@@ -3425,18 +3485,20 @@ KeyboardLayout::SynthesizeNativeKeyEvent
     UINT scanCode =
       ComputeScanCodeForVirtualKeyCode(keySpecific ? keySpecific : key);
     LPARAM lParam = static_cast<LPARAM>(scanCode << 16);
     // Add extended key flag to the lParam for right control key and right alt
     // key.
     if (keySpecific == VK_RCONTROL || keySpecific == VK_RMENU) {
       lParam |= 0x1000000;
     }
-    MSG keyDownMsg = WinUtils::InitMSG(WM_KEYDOWN, key, lParam,
-                                       aWidget->GetWindowHandle());
+    bool makeSysKeyMsg = IsSysKey(key, modKeyState);
+    MSG keyDownMsg =
+      WinUtils::InitMSG(makeSysKeyMsg ? WM_SYSKEYDOWN : WM_KEYDOWN,
+                        key, lParam, aWidget->GetWindowHandle());
     if (i == keySequence.Length() - 1) {
       bool makeDeadCharMsg =
         (IsDeadKey(key, modKeyState) && aCharacters.IsEmpty());
       nsAutoString chars(aCharacters);
       if (makeDeadCharMsg) {
         UniCharsAndModifiers deadChars =
           GetUniCharsAndModifiers(key, modKeyState);
         chars = deadChars.ToString();
@@ -3447,16 +3509,17 @@ KeyboardLayout::SynthesizeNativeKeyEvent
         NativeKey nativeKey(aWidget, keyDownMsg, modKeyState);
         nativeKey.HandleKeyDownMessage();
       } else {
         AutoTArray<NativeKey::FakeCharMsg, 10> fakeCharMsgs;
         for (uint32_t j = 0; j < chars.Length(); j++) {
           NativeKey::FakeCharMsg* fakeCharMsg = fakeCharMsgs.AppendElement();
           fakeCharMsg->mCharCode = chars.CharAt(j);
           fakeCharMsg->mScanCode = scanCode;
+          fakeCharMsg->mIsSysKey = makeSysKeyMsg;
           fakeCharMsg->mIsDeadKey = makeDeadCharMsg;
         }
         NativeKey nativeKey(aWidget, keyDownMsg, modKeyState, 0, &fakeCharMsgs);
         bool dispatched;
         nativeKey.HandleKeyDownMessage(&dispatched);
         // If some char messages are not consumed, let's emulate the widget
         // receiving the message directly.
         for (uint32_t j = 1; j < fakeCharMsgs.Length(); j++) {
@@ -3485,17 +3548,20 @@ KeyboardLayout::SynthesizeNativeKeyEvent
     UINT scanCode =
       ComputeScanCodeForVirtualKeyCode(keySpecific ? keySpecific : key);
     LPARAM lParam = static_cast<LPARAM>(scanCode << 16);
     // Add extended key flag to the lParam for right control key and right alt
     // key.
     if (keySpecific == VK_RCONTROL || keySpecific == VK_RMENU) {
       lParam |= 0x1000000;
     }
-    MSG keyUpMsg = WinUtils::InitMSG(WM_KEYUP, key, lParam,
+    // Don't use WM_SYSKEYUP for Alt keyup.
+    bool makeSysKeyMsg = IsSysKey(key, modKeyState) && key != VK_MENU;
+    MSG keyUpMsg = WinUtils::InitMSG(makeSysKeyMsg ? WM_SYSKEYUP : WM_KEYUP,
+                                     key, lParam,
                                      aWidget->GetWindowHandle());
     NativeKey nativeKey(aWidget, keyUpMsg, modKeyState);
     nativeKey.HandleKeyUpMessage();
   }
 
   // Restore old key state and layout
   ::SetKeyboardState(originalKbdState);
   RestoreLayout();
--- a/widget/windows/KeyboardLayout.h
+++ b/widget/windows/KeyboardLayout.h
@@ -175,29 +175,37 @@ class MOZ_STACK_CLASS NativeKey final
 {
   friend class KeyboardLayout;
 
 public:
   struct FakeCharMsg
   {
     UINT mCharCode;
     UINT mScanCode;
+    bool mIsSysKey;
     bool mIsDeadKey;
     bool mConsumed;
 
-    FakeCharMsg() :
-      mCharCode(0), mScanCode(0), mIsDeadKey(false), mConsumed(false)
+    FakeCharMsg()
+      : mCharCode(0)
+      , mScanCode(0)
+      , mIsSysKey(false)
+      , mIsDeadKey(false)
+      , mConsumed(false)
     {
     }
 
     MSG GetCharMsg(HWND aWnd) const
     {
       MSG msg;
       msg.hwnd = aWnd;
-      msg.message = mIsDeadKey ? WM_DEADCHAR : WM_CHAR;
+      msg.message = mIsDeadKey && mIsSysKey ? WM_SYSDEADCHAR :
+                                 mIsDeadKey ? WM_DEADCHAR :
+                                  mIsSysKey ? WM_SYSCHAR :
+                                              WM_CHAR;
       msg.wParam = static_cast<WPARAM>(mCharCode);
       msg.lParam = static_cast<LPARAM>(mScanCode << 16);
       msg.time = 0;
       msg.pt.x = msg.pt.y = 0;
       return msg;
     }
   };
 
@@ -300,16 +308,20 @@ private:
   // mIsPrintableKey is true if the key may be a printable key without
   // any modifier keys.  Otherwise, false.
   // Please note that the event may not cause any text input even if this
   // is true.  E.g., it might be dead key state or Ctrl key may be pressed.
   bool    mIsPrintableKey;
   // mIsOverridingKeyboardLayout is true if the instance temporarily overriding
   // keyboard layout with specified by the constructor.
   bool    mIsOverridingKeyboardLayout;
+  // mIsFollowedByNonControlCharMessage may be true when mMsg is a keydown
+  // message.  When the keydown message is followed by a char message, this
+  // is true.
+  bool    mIsFollowedByNonControlCharMessage;
 
   nsTArray<FakeCharMsg>* mFakeCharMsgs;
 
   // When a keydown event is dispatched at handling WM_APPCOMMAND, the computed
   // virtual keycode is set to this.  Even if we consume WM_APPCOMMAND message,
   // Windows may send WM_KEYDOWN and WM_KEYUP message for them.
   // At that time, we should not dispatch key events for them.
   static uint8_t sDispatchedKeyOfAppCommand;
@@ -407,16 +419,17 @@ private:
   {
     return IsSysCharMessage(aMSG.message);
   }
   bool IsSysCharMessage(UINT aMessage) const
   {
     return (aMessage == WM_SYSCHAR || aMessage == WM_SYSDEADCHAR);
   }
   bool MayBeSameCharMessage(const MSG& aCharMsg1, const MSG& aCharMsg2) const;
+  bool IsFollowedByNonControlCharMessage() const;
   bool IsFollowedByDeadCharMessage() const;
   bool IsKeyMessageOnPlugin() const
   {
     return (mMsg.message == MOZ_WM_KEYDOWN ||
             mMsg.message == MOZ_WM_KEYUP);
   }
 
   /**
@@ -580,16 +593,23 @@ public:
   /**
    * IsDeadKey() returns true if aVirtualKey is a dead key with aModKeyState.
    * This method isn't stateful.
    */
   bool IsDeadKey(uint8_t aVirtualKey,
                  const ModifierKeyState& aModKeyState) const;
 
   /**
+   * IsSysKey() returns true if aVirtualKey with aModKeyState causes WM_SYSKEY*
+   * or WM_SYS*CHAR messages.
+   */
+  bool IsSysKey(uint8_t aVirtualKey,
+                const ModifierKeyState& aModKeyState) const;
+
+  /**
    * GetUniCharsAndModifiers() returns characters which is inputted by the
    * aVirtualKey with aModKeyState.  This method isn't stateful.
    */
   UniCharsAndModifiers GetUniCharsAndModifiers(
                          uint8_t aVirtualKey,
                          const ModifierKeyState& aModKeyState) const;
 
   /**