Merge mozilla-central to mozilla-inbound. a=merge CLOSED TREE
authorBogdan Tara <btara@mozilla.com>
Fri, 23 Nov 2018 23:41:49 +0200
changeset 507140 7e7bf210330b1a491e30a75917d8b9aafa790458
parent 507139 d0b1dc88d15d0e3bf4adf8c491feab0e5f833a90 (current diff)
parent 507099 8264fe75578f62fa4f14d48ec8ca86d109e8ddf5 (diff)
child 507141 e8eb60450f8c25c2985316e2896aa0574ba658d5
push id1905
push userffxbld-merge
push dateMon, 21 Jan 2019 12:33:13 +0000
treeherdermozilla-release@c2fca1944d8c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound. a=merge CLOSED TREE
dom/base/nsXMLNameSpaceMap.cpp
dom/base/nsXMLNameSpaceMap.h
dom/media/webspeech/recognition/SpeechStreamListener.cpp
dom/media/webspeech/recognition/SpeechStreamListener.h
media/webrtc/signaling/test/FakeLogging.h
media/webrtc/signaling/test/FakeMediaStreams.h
media/webrtc/signaling/test/FakeMediaStreamsImpl.h
media/webrtc/signaling/test/FakePCObserver.h
media/webrtc/signaling/test/signaling_unittests.cpp
netwerk/base/nsINetworkProperties.idl
--- a/browser/base/content/browser-contentblocking.js
+++ b/browser/base/content/browser-contentblocking.js
@@ -222,38 +222,20 @@ var ThirdPartyCookies = {
 
 var ContentBlocking = {
   // If the user ignores the doorhanger, we stop showing it after some time.
   MAX_INTROS: 20,
   PREF_ANIMATIONS_ENABLED: "toolkit.cosmeticAnimations.enabled",
   PREF_REPORT_BREAKAGE_ENABLED: "browser.contentblocking.reportBreakage.enabled",
   PREF_REPORT_BREAKAGE_URL: "browser.contentblocking.reportBreakage.url",
   PREF_INTRO_COUNT_CB: "browser.contentblocking.introCount",
-  PREF_CB_CATEGORY: "browser.contentblocking.category",
-  // The prefs inside CATEGORY_PREFS set expected behavior for each CB category.
-  // A null value means that pref is default.
-  CATEGORY_PREFS: {
-    strict: [
-      [TrackingProtection.PREF_TRACKING_TABLE, null],
-      [ThirdPartyCookies.PREF_ENABLED, Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN],
-      [TrackingProtection.PREF_ENABLED_IN_PRIVATE_WINDOWS, true],
-      [TrackingProtection.PREF_ENABLED_GLOBALLY, true],
-    ],
-    standard: [
-      [TrackingProtection.PREF_TRACKING_TABLE, null],
-      [ThirdPartyCookies.PREF_ENABLED, null],
-      [TrackingProtection.PREF_ENABLED_IN_PRIVATE_WINDOWS, null],
-      [TrackingProtection.PREF_ENABLED_GLOBALLY, null],
-    ],
-  },
   content: null,
   icon: null,
   activeTooltipText: null,
   disabledTooltipText: null,
-  switchingCategory: false,
 
   get prefIntroCount() {
     return this.PREF_INTRO_COUNT_CB;
   },
 
   get appMenuLabel() {
     delete this.appMenuLabel;
     return this.appMenuLabel = document.getElementById("appMenu-tp-label");
@@ -337,38 +319,26 @@ var ContentBlocking = {
 
     this.appMenuLabel.setAttribute("label", this.strings.appMenuTitle);
     this.appMenuLabel.setAttribute("tooltiptext", this.strings.appMenuTooltip);
 
     this.activeTooltipText =
       gNavigatorBundle.getString("trackingProtection.icon.activeTooltip");
     this.disabledTooltipText =
       gNavigatorBundle.getString("trackingProtection.icon.disabledTooltip");
-
-    this.matchCBCategory = this.matchCBCategory.bind(this);
-    this.updateCBCategory = this.updateCBCategory.bind(this);
-    this.matchCBCategory();
-    Services.prefs.addObserver(TrackingProtection.PREF_ENABLED_GLOBALLY, this.matchCBCategory);
-    Services.prefs.addObserver(TrackingProtection.PREF_ENABLED_IN_PRIVATE_WINDOWS, this.matchCBCategory);
-    Services.prefs.addObserver(ThirdPartyCookies.PREF_ENABLED, this.matchCBCategory);
-    Services.prefs.addObserver(this.PREF_CB_CATEGORY, this.updateCBCategory);
   },
 
   uninit() {
     for (let blocker of this.blockers) {
       if (blocker.uninit) {
         blocker.uninit();
       }
     }
 
     Services.prefs.removeObserver(this.PREF_ANIMATIONS_ENABLED, this.updateAnimationsEnabled);
-    Services.prefs.removeObserver(TrackingProtection.PREF_ENABLED_GLOBALLY, this.matchCBCategory);
-    Services.prefs.removeObserver(TrackingProtection.PREF_ENABLED_IN_PRIVATE_WINDOWS, this.matchCBCategory);
-    Services.prefs.removeObserver(ThirdPartyCookies.PREF_ENABLED, this.matchCBCategory);
-    Services.prefs.removeObserver(this.PREF_CB_CATEGORY, this.updateCBCategory);
   },
 
   hideIdentityPopupAndReload() {
     this.identityPopup.hidePopup();
     BrowserReload();
   },
 
   openPreferences(origin) {
@@ -635,94 +605,9 @@ var ContentBlocking = {
       },
     ];
 
     let panelTarget = await UITour.getTarget(window, "trackingProtection");
     UITour.initForBrowser(gBrowser.selectedBrowser, window);
     UITour.showInfo(window, panelTarget, introTitle, introDescription, undefined, buttons,
                     { closeButtonCallback: () => this.dontShowIntroPanelAgain() });
   },
-
-  /**
-   * Checks if CB prefs match perfectly with one of our pre-defined categories.
-   */
-  prefsMatch(category) {
-    // The category pref must be either unset, or match.
-    if (Services.prefs.prefHasUserValue(this.PREF_CB_CATEGORY) &&
-        Services.prefs.getStringPref(this.PREF_CB_CATEGORY) != category) {
-      return false;
-    }
-    for (let [pref, value] of this.CATEGORY_PREFS[category]) {
-      if (!value) {
-        if (Services.prefs.prefHasUserValue(pref)) {
-          return false;
-        }
-      } else {
-        let prefType = Services.prefs.getPrefType(pref);
-        if ((prefType == Services.prefs.PREF_BOOL && Services.prefs.getBoolPref(pref) != value) ||
-            (prefType == Services.prefs.PREF_INT && Services.prefs.getIntPref(pref) != value) ||
-            (prefType == Services.prefs.PREF_STRING && Services.prefs.getStringPref(pref) != value)) {
-          return false;
-        }
-      }
-    }
-    return true;
-  },
-
-  async matchCBCategory() {
-    if (this.switchingCategory) {
-      return;
-    }
-    // If PREF_CB_CATEGORY is not set match users to a Content Blocking category. Check if prefs fit
-    // perfectly into strict or standard, otherwise match with custom. If PREF_CB_CATEGORY has previously been set,
-    // a change of one of these prefs necessarily puts us in "custom".
-    if (this.prefsMatch("standard")) {
-      Services.prefs.setStringPref(this.PREF_CB_CATEGORY, "standard");
-    } else if (this.prefsMatch("strict")) {
-      Services.prefs.setStringPref(this.PREF_CB_CATEGORY, "strict");
-    } else {
-      Services.prefs.setStringPref(this.PREF_CB_CATEGORY, "custom");
-    }
-  },
-
-  updateCBCategory() {
-    if (this.switchingCategory) {
-      return;
-    }
-    // Turn on switchingCategory flag, to ensure that when the individual prefs that change as a result
-    // of the category change do not trigger yet another category change.
-    this.switchingCategory = true;
-    let value = Services.prefs.getStringPref(this.PREF_CB_CATEGORY);
-    this.setPrefsToCategory(value);
-    this.switchingCategory = false;
-  },
-
-  /**
-   * Sets all user-exposed content blocking preferences to values that match the selected category.
-   */
-  setPrefsToCategory(category) {
-    // Leave prefs as they were if we are switching to "custom" category.
-    if (category == "custom") {
-      return;
-    }
-
-    for (let [pref, value] of this.CATEGORY_PREFS[category]) {
-      // this.setPrefIfNotLocked(pref[0], pref[1]);
-      if (!Services.prefs.prefIsLocked(pref)) {
-        if (!value) {
-          Services.prefs.clearUserPref(pref);
-        } else {
-          switch (Services.prefs.getPrefType(pref)) {
-          case Services.prefs.PREF_BOOL:
-            Services.prefs.setBoolPref(pref, value);
-            break;
-          case Services.prefs.PREF_INT:
-            Services.prefs.setIntPref(pref, value);
-            break;
-          case Services.prefs.PREF_STRING:
-            Services.prefs.setStringPref(pref, value);
-            break;
-          }
-        }
-      }
-    }
-  },
 };
--- a/browser/base/content/browser-data-submission-info-bar.js
+++ b/browser/base/content/browser-data-submission-info-bar.js
@@ -1,29 +1,28 @@
 /* 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/. */
 
-const LOGGER_NAME = "Toolkit.Telemetry";
-const LOGGER_PREFIX = "DataNotificationInfoBar::";
 /**
  * Represents an info bar that shows a data submission notification.
  */
 var gDataNotificationInfoBar = {
   _OBSERVERS: [
     "datareporting:notify-data-policy:request",
     "datareporting:notify-data-policy:close",
   ],
 
   _DATA_REPORTING_NOTIFICATION: "data-reporting",
 
   get _log() {
-    let Log = ChromeUtils.import("resource://gre/modules/Log.jsm", {}).Log;
+    let { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm", {});
     delete this._log;
-    return this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
+    return this._log = Log.repository.getLoggerWithMessagePrefix(
+      "Toolkit.Telemetry", "DataNotificationInfoBar::");
   },
 
   init() {
     window.addEventListener("unload", () => {
       for (let o of this._OBSERVERS) {
         Services.obs.removeObserver(this, o);
       }
     });
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -527,24 +527,17 @@ var PlacesCommandHook = {
     } else {
       organizer.PlacesOrganizer.selectLeftPaneContainerByHierarchy(item);
       organizer.focus();
     }
   },
 
   searchBookmarks() {
     focusAndSelectUrlBar();
-    for (let char of ["*", " "]) {
-      let code = char.charCodeAt(0);
-      gURLBar.inputField.dispatchEvent(new KeyboardEvent("keypress", {
-        keyCode: code,
-        charCode: code,
-        bubbles: true,
-      }));
-    }
+    gURLBar.typeRestrictToken(UrlbarTokenizer.RESTRICT.BOOKMARK);
   },
 };
 
 ChromeUtils.defineModuleGetter(this, "RecentlyClosedTabsAndWindowsMenuUtils",
   "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm");
 
 // View for the history menu.
 function HistoryMenu(aPopupShowingEvent) {
--- a/browser/base/content/browser-tabsintitlebar.js
+++ b/browser/base/content/browser-tabsintitlebar.js
@@ -1,19 +1,23 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-var TabsInTitlebar = {
+var TabsInTitlebar;
+
+{ // start private TabsInTitlebar scope
+
+TabsInTitlebar = {
   init() {
     this._readPref();
     Services.prefs.addObserver(this._prefName, this);
 
-    gDragSpaceObserver.init();
+    dragSpaceObserver.init();
     this._initialized = true;
     this._update();
   },
 
   allowedBy(condition, allow) {
     if (allow) {
       if (condition in this._disallowed) {
         delete this._disallowed[condition];
@@ -82,30 +86,23 @@ var TabsInTitlebar = {
       }
     }
 
     ToolbarIconColor.inferFromText("tabsintitlebar", allowed);
   },
 
   uninit() {
     Services.prefs.removeObserver(this._prefName, this);
-    gDragSpaceObserver.uninit();
+    dragSpaceObserver.uninit();
   },
 };
 
-function onTitlebarMaxClick() {
-  if (window.windowState == window.STATE_MAXIMIZED)
-    window.restore();
-  else
-    window.maximize();
-}
-
 // Adds additional drag space to the window by listening to
 // the corresponding preference.
-var gDragSpaceObserver = {
+let dragSpaceObserver = {
   pref: "browser.tabs.extraDragSpace",
 
   init() {
     Services.prefs.addObserver(this.pref, this);
     this.observe();
   },
 
   uninit() {
@@ -115,8 +112,17 @@ var gDragSpaceObserver = {
   observe() {
     if (Services.prefs.getBoolPref(this.pref)) {
       document.documentElement.setAttribute("extradragspace", "true");
     } else {
       document.documentElement.removeAttribute("extradragspace");
     }
   },
 };
+
+} // end private TabsInTitlebar scope
+
+function onTitlebarMaxClick() {
+  if (window.windowState == window.STATE_MAXIMIZED)
+    window.restore();
+  else
+    window.maximize();
+}
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -64,16 +64,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   SitePermissions: "resource:///modules/SitePermissions.jsm",
   TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
   TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm",
   Translation: "resource:///modules/translation/Translation.jsm",
   UITour: "resource:///modules/UITour.jsm",
   UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
   UrlbarInput: "resource:///modules/UrlbarInput.jsm",
   UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+  UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
   UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
   UrlbarValueFormatter: "resource:///modules/UrlbarValueFormatter.jsm",
   Utils: "resource://gre/modules/sessionstore/Utils.jsm",
   Weave: "resource://services-sync/main.js",
   WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
   fxAccounts: "resource://gre/modules/FxAccounts.jsm",
   webrtcUI: "resource:///modules/webrtcUI.jsm",
   ZoomUI: "resource:///modules/ZoomUI.jsm",
@@ -4030,16 +4031,18 @@ const BrowserSearch = {
         Services.obs.addObserver(observer, "browser-delayed-startup-finished");
       }
       return;
     }
 
     let focusUrlBarIfSearchFieldIsNotActive = function(aSearchBar) {
       if (!aSearchBar || document.activeElement != aSearchBar.textbox.inputField) {
         focusAndSelectUrlBar(true);
+        // Limit the results to search suggestions, like the search bar.
+        gURLBar.typeRestrictToken(UrlbarTokenizer.RESTRICT.SEARCH);
       }
     };
 
     let searchBar = this.searchBar;
     let placement = CustomizableUI.getPlacementOfWidget("search-container");
     let focusSearchBar = () => {
       searchBar = this.searchBar;
       searchBar.select();
@@ -7737,36 +7740,27 @@ function duplicateTabIn(aTab, where, del
       break;
   }
 }
 
 var MousePosTracker = {
   _listeners: new Set(),
   _x: 0,
   _y: 0,
-  _mostRecentEvent: null,
 
   /**
-   * Registers a listener, and then waits for the next refresh
-   * driver tick before running the listener to see if the
-   * mouse is within the listener's target rect.
+   * Registers a listener.
    *
    * @param listener (object)
    *        A listener is expected to expose the following properties:
    *
    *        getMouseTargetRect (function)
    *          Returns the rect that the MousePosTracker needs to alert
    *          the listener about if the mouse happens to be within it.
    *
-   *        onTrackingStarted (function, optional)
-   *          Called after the next refresh driver tick after listening,
-   *          when the mouse's initial position relative to the MouseTargetRect
-   *          can be computed. If the listener is removed before the refresh
-   *          driver tick, this might never be called.
-   *
    *        onMouseEnter (function, optional)
    *          The function to be called if the mouse enters the rect
    *          returned by getMouseTargetRect. MousePosTracker always
    *          runs this inside of a requestAnimationFrame, since it
    *          assumes that the notification is used to update the DOM.
    *
    *        onMouseLeave (function, optional)
    *          The function to be called if the mouse exits the rect
@@ -7777,103 +7771,58 @@ var MousePosTracker = {
   addListener(listener) {
     if (this._listeners.has(listener)) {
       return;
     }
 
     listener._hover = false;
     this._listeners.add(listener);
 
-    // We're adding some asynchronicity here, during which the listener
-    // might be removed. At each step, we need to ensure that the listener
-    // is still registered before proceeding.
-    window.promiseDocumentFlushed(() => {
-      if (this._listeners.has(listener)) {
-        this._callListeners([listener]);
-        window.requestAnimationFrame(() => {
-          if (this._listeners.has(listener) && listener.onTrackingStarted) {
-            listener.onTrackingStarted();
-          }
-        });
-      }
-    });
+    this._callListener(listener);
   },
 
   removeListener(listener) {
     this._listeners.delete(listener);
   },
 
   handleEvent(event) {
-    let firstEvent = !this._mostRecentEvent;
-    this._mostRecentEvent = event;
-
-    if (firstEvent) {
-      window.promiseDocumentFlushed(() => {
-        this.onDocumentFlushed();
-        this._mostRecentEvent = null;
-      });
-    }
-  },
-
-  onDocumentFlushed() {
-    let event = this._mostRecentEvent;
-
-    if (event) {
-      let fullZoom = window.windowUtils.fullZoom;
-      this._x = event.screenX / fullZoom - window.mozInnerScreenX;
-      this._y = event.screenY / fullZoom - window.mozInnerScreenY;
-
-      this._callListeners(this._listeners);
-    }
-  },
-
-  _callListeners(listeners) {
-    let functionsToCall = [];
-    for (let listener of listeners) {
-      let rect;
+    let fullZoom = window.windowUtils.fullZoom;
+    this._x = event.screenX / fullZoom - window.mozInnerScreenX;
+    this._y = event.screenY / fullZoom - window.mozInnerScreenY;
+
+    this._listeners.forEach(listener => {
       try {
-        rect = listener.getMouseTargetRect();
+        this._callListener(listener);
       } catch (e) {
         Cu.reportError(e);
-        continue;
-      }
-
-      let hover = this._x >= rect.left &&
-                  this._x <= rect.right &&
-                  this._y >= rect.top &&
-                  this._y <= rect.bottom;
-
-      if (hover == listener._hover) {
-        continue;
-      }
-
-      listener._hover = hover;
-      if (hover) {
-        if (listener.onMouseEnter) {
-          functionsToCall.push(listener.onMouseEnter.bind(listener));
-        }
-      } else if (listener.onMouseLeave) {
-        functionsToCall.push(listener.onMouseLeave.bind(listener));
-      }
-    }
-
-    // _callListeners is being called from within a promiseDocumentFlushed,
-    // where we are expressly forbidden from dirtying styles or layout. Since
-    // the onMouseEnter or onMouseLeave functions are liable to do such
-    // dirtying, we run them inside a requestAnimationFrame callback instead.
-    window.requestAnimationFrame(() => {
-      for (let fn of functionsToCall) {
-        try {
-          fn();
-        } catch (e) {
-          Cu.reportError(e);
-        }
       }
     });
   },
+
+  _callListener(listener) {
+    let rect = listener.getMouseTargetRect();
+    let hover = this._x >= rect.left &&
+                this._x <= rect.right &&
+                this._y >= rect.top &&
+                this._y <= rect.bottom;
+
+    if (hover == listener._hover) {
+      return;
+    }
+
+    listener._hover = hover;
+
+    if (hover) {
+      if (listener.onMouseEnter) {
+        listener.onMouseEnter();
+      }
+    } else if (listener.onMouseLeave) {
+      listener.onMouseLeave();
+    }
+  },
 };
 
 var ToolbarIconColor = {
   _windowState: {
     "active": false,
     "fullscreen": false,
     "tabsintitlebar": false,
   },
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -5253,18 +5253,16 @@ TabProgressListener.prototype.QueryInter
   ["nsIWebProgressListener",
    "nsIWebProgressListener2",
    "nsISupportsWeakReference"]);
 
 } // end private scope for gBrowser
 
 var StatusPanel = {
   get panel() {
-    window.addEventListener("resize", this);
-
     delete this.panel;
     return this.panel = document.getElementById("statuspanel");
   },
 
   get isVisible() {
     return !this.panel.hasAttribute("inactive");
   },
 
@@ -5312,84 +5310,58 @@ var StatusPanel = {
       this.panel.style.minWidth =
         window.windowUtils
               .getBoundsWithoutFlushing(this.panel).width + "px";
     } else {
       this.panel.style.minWidth = "";
     }
 
     if (val) {
-      this._mouseTargetRect = null;
       this._labelElement.value = val;
+      this.panel.removeAttribute("inactive");
       MousePosTracker.addListener(this);
-      // The inactive state for the panel will be removed in onTrackingStarted,
-      // once the initial position of the mouse relative to the StatusPanel
-      // is figured out (to avoid both flicker and sync flushing).
     } else {
       this.panel.setAttribute("inactive", "true");
       MousePosTracker.removeListener(this);
     }
 
     return val;
   },
 
-  onTrackingStarted() {
-    this.panel.removeAttribute("inactive");
-  },
-
   getMouseTargetRect() {
-    if (!this._mouseTargetRect) {
-      this._calcMouseTargetRect();
-    }
-    return this._mouseTargetRect;
+    let container = this.panel.parentNode;
+    let alignRight = document.documentElement.matches(":-moz-locale-dir(rtl)");
+    let panelRect = window.windowUtils.getBoundsWithoutFlushing(this.panel);
+    let containerRect = window.windowUtils.getBoundsWithoutFlushing(container);
+
+    return {
+      top:    panelRect.top,
+      bottom: panelRect.bottom,
+      left:   alignRight ? containerRect.right - panelRect.width : containerRect.left,
+      right:  alignRight ? containerRect.right : containerRect.left + panelRect.width,
+    };
   },
 
   onMouseEnter() {
     this._mirror();
   },
 
   onMouseLeave() {
     this._mirror();
   },
 
-  handleEvent(event) {
-    if (!this.isVisible) {
-      return;
-    }
-    switch (event.type) {
-      case "resize":
-        this._mouseTargetRect = null;
-        break;
-    }
-  },
-
-  _calcMouseTargetRect() {
-    let container = this.panel.parentNode;
-    let alignRight = (getComputedStyle(container).direction == "rtl");
-    let panelRect = this.panel.getBoundingClientRect();
-    let containerRect = container.getBoundingClientRect();
-
-    this._mouseTargetRect = {
-      top:    panelRect.top,
-      bottom: panelRect.bottom,
-      left:   alignRight ? containerRect.right - panelRect.width : containerRect.left,
-      right:  alignRight ? containerRect.right : containerRect.left + panelRect.width,
-    };
-  },
-
   _mirror() {
     if (this.panel.hasAttribute("mirror")) {
       this.panel.removeAttribute("mirror");
     } else {
       this.panel.setAttribute("mirror", "true");
     }
 
     if (!this.panel.hasAttribute("sizelimit")) {
       this.panel.setAttribute("sizelimit", "true");
-      this._mouseTargetRect = null;
     }
   },
 };
 
 var TabBarVisibility = {
   _initialUpdateDone: false,
 
   update() {
--- a/browser/base/content/test/trackingUI/browser_trackingUI_animation_2.js
+++ b/browser/base/content/test/trackingUI/browser_trackingUI_animation_2.js
@@ -1,8 +1,9 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
 /*
  * Test that the Content Blocking icon is properly animated in the identity
  * block when loading tabs and switching between tabs.
  * See also Bug 1175858.
  */
 
 const TP_PREF = "privacy.trackingprotection.enabled";
 const TP_PB_PREF = "privacy.trackingprotection.enabled";
@@ -25,17 +26,17 @@ function waitForSecurityChange(tabbrowse
   return new Promise(resolve => {
     let n = 0;
     let listener = {
       onSecurityChange() {
         n = n + 1;
         info("Received onSecurityChange event " + n + " of " + numChanges);
         if (n >= numChanges) {
           tabbrowser.removeProgressListener(listener);
-          resolve();
+          resolve(n);
         }
       },
     };
     tabbrowser.addProgressListener(listener);
   });
 }
 
 async function testTrackingProtectionAnimation(tabbrowser) {
@@ -80,76 +81,84 @@ async function testTrackingProtectionAni
   securityChanged = waitForSecurityChange(tabbrowser);
   tabbrowser.selectedTab = trackingCookiesTab;
   await securityChanged;
 
   ok(ContentBlocking.iconBox.hasAttribute("active"), "iconBox active");
   ok(!ContentBlocking.iconBox.hasAttribute("animate"), "iconBox not animating");
 
   info("Reload tracking cookies tab");
-  securityChanged = waitForSecurityChange(tabbrowser, 4);
+  securityChanged = waitForSecurityChange(tabbrowser, 3);
   tabbrowser.reload();
   await securityChanged;
 
   ok(ContentBlocking.iconBox.hasAttribute("active"), "iconBox active");
   ok(ContentBlocking.iconBox.hasAttribute("animate"), "iconBox animating");
   await BrowserTestUtils.waitForEvent(ContentBlocking.animatedIcon, "animationend");
 
   info("Reload tracking tab");
-  securityChanged = waitForSecurityChange(tabbrowser, 5);
+  securityChanged = waitForSecurityChange(tabbrowser, 4);
   tabbrowser.selectedTab = trackingTab;
   tabbrowser.reload();
   await securityChanged;
 
   ok(ContentBlocking.iconBox.hasAttribute("active"), "iconBox active");
   ok(ContentBlocking.iconBox.hasAttribute("animate"), "iconBox animating");
   await BrowserTestUtils.waitForEvent(ContentBlocking.animatedIcon, "animationend");
 
   info("Inject tracking cookie inside tracking tab");
-  securityChanged = waitForSecurityChange(tabbrowser, 2);
+  securityChanged = waitForSecurityChange(tabbrowser);
+  let timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
   await ContentTask.spawn(tabbrowser.selectedBrowser, {},
                           function() {
     content.postMessage("cookie", "*");
   });
-  await securityChanged;
+  let result = await Promise.race([securityChanged, timeoutPromise]);
+  is(result, undefined, "No securityChange events should be received");
 
   ok(ContentBlocking.iconBox.hasAttribute("active"), "iconBox active");
   ok(!ContentBlocking.iconBox.hasAttribute("animate"), "iconBox not animating");
 
   info("Inject tracking element inside tracking tab");
   securityChanged = waitForSecurityChange(tabbrowser);
+  timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
   await ContentTask.spawn(tabbrowser.selectedBrowser, {},
                           function() {
     content.postMessage("tracking", "*");
   });
-  await securityChanged;
+  result = await Promise.race([securityChanged, timeoutPromise]);
+  is(result, undefined, "No securityChange events should be received");
 
   ok(ContentBlocking.iconBox.hasAttribute("active"), "iconBox active");
   ok(!ContentBlocking.iconBox.hasAttribute("animate"), "iconBox not animating");
 
   tabbrowser.selectedTab = trackingCookiesTab;
 
   info("Inject tracking cookie inside tracking cookies tab");
-  securityChanged = waitForSecurityChange(tabbrowser, 2);
+  securityChanged = waitForSecurityChange(tabbrowser);
+  timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
   await ContentTask.spawn(tabbrowser.selectedBrowser, {},
                           function() {
     content.postMessage("cookie", "*");
   });
-  await securityChanged;
+  result = await Promise.race([securityChanged, timeoutPromise]);
+  is(result, undefined, "No securityChange events should be received");
 
   ok(ContentBlocking.iconBox.hasAttribute("active"), "iconBox active");
   ok(!ContentBlocking.iconBox.hasAttribute("animate"), "iconBox not animating");
 
   info("Inject tracking element inside tracking cookies tab");
-  securityChanged = waitForSecurityChange(tabbrowser, 2);
+  securityChanged = waitForSecurityChange(tabbrowser);
+  timeoutPromise = new Promise(resolve => setTimeout(resolve, 500));
   await ContentTask.spawn(tabbrowser.selectedBrowser, {},
                           function() {
     content.postMessage("tracking", "*");
   });
-  await securityChanged;
+  result = await Promise.race([securityChanged, timeoutPromise]);
+  is(result, undefined, "No securityChange events should be received");
 
   ok(ContentBlocking.iconBox.hasAttribute("active"), "iconBox active");
   ok(!ContentBlocking.iconBox.hasAttribute("animate"), "iconBox not animating");
 
   while (tabbrowser.tabs.length > 1) {
     tabbrowser.removeCurrentTab();
   }
 }
--- a/browser/base/content/test/trackingUI/browser_trackingUI_trackers_subview.js
+++ b/browser/base/content/test/trackingUI/browser_trackingUI_trackers_subview.js
@@ -1,8 +1,9 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/trackingPage.html";
 
 const TP_PREF = "privacy.trackingprotection.enabled";
@@ -22,17 +23,17 @@ function waitForSecurityChange(counter) 
   return new Promise(resolve => {
     let webProgressListener = {
       onStateChange: () => {},
       onStatusChange: () => {},
       onLocationChange: () => {},
       onSecurityChange: (webProgress, request, oldState, state) => {
         if (--counter == 0) {
           gBrowser.removeProgressListener(webProgressListener);
-          resolve();
+          resolve(counter);
         }
       },
       onProgressChange: () => {},
       QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener]),
     };
 
     gBrowser.addProgressListener(webProgressListener);
   });
@@ -62,23 +63,25 @@ async function assertSitesListed(blocked
     let mainView = document.getElementById("identity-popup-mainView");
     viewShown = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
     let backButton = trackersView.querySelector(".subviewbutton-back");
     backButton.click();
     await viewShown;
 
     ok(true, "Main view was shown");
 
-    let change = waitForSecurityChange(2);
+    let change = waitForSecurityChange(1);
+    let timeoutPromise = new Promise(resolve => setTimeout(resolve, 1000));
 
     await ContentTask.spawn(browser, {}, function() {
       content.postMessage("more-tracking", "*");
     });
 
-    await change;
+    let result = await Promise.race([change, timeoutPromise]);
+    is(result, undefined, "No securityChange events should be received");
 
     viewShown = BrowserTestUtils.waitForEvent(trackersView, "ViewShown");
     categoryItem.click();
     await viewShown;
 
     ok(true, "Trackers view was shown");
 
     listItems = Array.from(document.querySelectorAll(".identity-popup-trackersView-list-item"));
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1705,16 +1705,30 @@ file, You can obtain one at http://mozil
           // possible.  If handleText() returned false, then manually start a
           // new search here.
           if (!this._onInputHandledText) {
             this.gotResultForCurrentQuery = false;
             this.controller.startSearch(value);
           }
         ]]></body>
       </method>
+
+      <method name="typeRestrictToken">
+        <parameter name="char"/>
+        <body><![CDATA[
+          for (let c of [char, " "]) {
+            let code = c.charCodeAt(0);
+            gURLBar.inputField.dispatchEvent(new KeyboardEvent("keypress", {
+              keyCode: code,
+              charCode: code,
+              bubbles: true,
+            }));
+          }
+        ]]></body>
+      </method>
     </implementation>
 
     <handlers>
       <handler event="keydown"><![CDATA[
         if (this._noActionKeys.has(event.keyCode) &&
             this.popup.selectedIndex >= 0 &&
             !this._pressedNoActionKeys.has(event.keyCode)) {
           if (this._pressedNoActionKeys.size == 0) {
@@ -2488,18 +2502,24 @@ file, You can obtain one at http://mozil
             // because the result may have already been added but only now is
             // being selected, and we need to check gotResultForCurrentQuery
             // because the result may be from the previous search and already
             // selected and is now being reused.
             if (selectHeuristic || !this.input.gotResultForCurrentQuery) {
               this.input.formatValue();
 
               // Also, hide the one-off search buttons if the user is using, or
-              // starting to use, an "@engine" search engine alias.
-              this.toggleOneOffSearches(this.input.value.trim()[0] != "@");
+              // starting to use, an "@engine" search engine alias, or typed
+              // only the search restriction character.
+              let trimmedValue = this.input.value.trim();
+              this.toggleOneOffSearches(
+                trimmedValue[0] != "@" &&
+                (trimmedValue[0] != UrlbarTokenizer.RESTRICT.SEARCH ||
+                 trimmedValue.length != 1)
+              );
             }
 
             // If this is the first time we get the result from the current
             // search and we are not in the private context, we can speculatively
             // connect to the intended site as a performance optimization.
             if (!this.input.gotResultForCurrentQuery &&
                 this.input.speculativeConnectEnabled &&
                 !this.input.inPrivateContext &&
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -989,16 +989,22 @@ BrowserGlue.prototype = {
     os.removeObserver(this, "handle-xul-text-link");
     os.removeObserver(this, "profile-before-change");
     os.removeObserver(this, "keyword-search");
     os.removeObserver(this, "browser-search-engine-modified");
     os.removeObserver(this, "flash-plugin-hang");
     os.removeObserver(this, "xpi-signature-changed");
     os.removeObserver(this, "sync-ui-state:update");
     os.removeObserver(this, "shield-init-complete");
+
+    Services.prefs.removeObserver("privacy.trackingprotection.enabled", this._matchCBCategory);
+    Services.prefs.removeObserver("privacy.trackingprotection.pbmode.enabled", this._matchCBCategory);
+    Services.prefs.removeObserver("urlclassifier.trackingTable", this._matchCBCategory);
+    Services.prefs.removeObserver("network.cookie.cookieBehavior", this._matchCBCategory);
+    Services.prefs.removeObserver(ContentBlockingCategoriesPrefs.PREF_CB_CATEGORY, this._updateCBCategory);
   },
 
   // runs on startup, before the first command line handler is invoked
   // (i.e. before the first window is opened)
   _beforeUIStartup: function BG__beforeUIStartup() {
     SessionStartup.init();
 
     if (Services.prefs.prefHasUserValue(PREF_PDFJS_ENABLED_CACHE_STATE)) {
@@ -1359,16 +1365,31 @@ BrowserGlue.prototype = {
 
     PageActions.init();
 
     this._firstWindowTelemetry(aWindow);
     this._firstWindowLoaded();
 
     // Set the default favicon size for UI views that use the page-icon protocol.
     PlacesUtils.favicons.setDefaultIconURIPreferredSize(16 * aWindow.devicePixelRatio);
+
+    this._matchCBCategory();
+    Services.prefs.addObserver("privacy.trackingprotection.enabled", this._matchCBCategory);
+    Services.prefs.addObserver("privacy.trackingprotection.pbmode.enabled", this._matchCBCategory);
+    Services.prefs.addObserver("urlclassifier.trackingTable", this._matchCBCategory);
+    Services.prefs.addObserver("network.cookie.cookieBehavior", this._matchCBCategory);
+    Services.prefs.addObserver(ContentBlockingCategoriesPrefs.PREF_CB_CATEGORY, this._updateCBCategory);
+  },
+
+  _matchCBCategory() {
+    ContentBlockingCategoriesPrefs.matchCBCategory();
+  },
+
+  _updateCBCategory() {
+    ContentBlockingCategoriesPrefs.updateCBCategory();
   },
 
   _recordContentBlockingTelemetry() {
     let recordIdentityPopupEvents = Services.prefs.getBoolPref("security.identitypopup.recordEventElemetry");
     Services.telemetry.setEventRecordingEnabled("security.ui.identitypopup", recordIdentityPopupEvents);
 
     let tpEnabled = Services.prefs.getBoolPref("privacy.trackingprotection.enabled");
     Services.telemetry.getHistogramById("TRACKING_PROTECTION_ENABLED").add(tpEnabled);
@@ -2952,16 +2973,121 @@ BrowserGlue.prototype = {
   classID:          Components.ID("{eab9012e-5f74-4cbc-b2b5-a590235513cc}"),
 
   QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver,
                                           Ci.nsISupportsWeakReference]),
 
   _xpcom_factory: XPCOMUtils.generateSingletonFactory(BrowserGlue),
 };
 
+var ContentBlockingCategoriesPrefs = {
+  PREF_CB_CATEGORY: "browser.contentblocking.category",
+  // The prefs inside CATEGORY_PREFS set expected value for each CB category.
+  // A null value means that pref is default.
+  CATEGORY_PREFS: {
+    strict: [
+      ["urlclassifier.trackingTable", null],
+      ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN],
+      ["privacy.trackingprotection.pbmode.enabled", true],
+      ["privacy.trackingprotection.enabled", true],
+    ],
+    standard: [
+      ["urlclassifier.trackingTable", null],
+      ["network.cookie.cookieBehavior", null],
+      ["privacy.trackingprotection.pbmode.enabled", null],
+      ["privacy.trackingprotection.enabled", null],
+    ],
+  },
+  switchingCategory: false,
+
+  /**
+   * Checks if CB prefs match perfectly with one of our pre-defined categories.
+   */
+  prefsMatch(category) {
+    // The category pref must be either unset, or match.
+    if (Services.prefs.prefHasUserValue(this.PREF_CB_CATEGORY) &&
+        Services.prefs.getStringPref(this.PREF_CB_CATEGORY) != category) {
+      return false;
+    }
+    for (let [pref, value] of this.CATEGORY_PREFS[category]) {
+      if (!value) {
+        if (Services.prefs.prefHasUserValue(pref)) {
+          return false;
+        }
+      } else {
+        let prefType = Services.prefs.getPrefType(pref);
+        if ((prefType == Services.prefs.PREF_BOOL && Services.prefs.getBoolPref(pref) != value) ||
+            (prefType == Services.prefs.PREF_INT && Services.prefs.getIntPref(pref) != value) ||
+            (prefType == Services.prefs.PREF_STRING && Services.prefs.getStringPref(pref) != value)) {
+          return false;
+        }
+      }
+    }
+    return true;
+  },
+
+  matchCBCategory() {
+    if (this.switchingCategory) {
+      return;
+    }
+    // If PREF_CB_CATEGORY is not set match users to a Content Blocking category. Check if prefs fit
+    // perfectly into strict or standard, otherwise match with custom. If PREF_CB_CATEGORY has previously been set,
+    // a change of one of these prefs necessarily puts us in "custom".
+    if (this.prefsMatch("standard")) {
+      Services.prefs.setStringPref(this.PREF_CB_CATEGORY, "standard");
+    } else if (this.prefsMatch("strict")) {
+      Services.prefs.setStringPref(this.PREF_CB_CATEGORY, "strict");
+    } else {
+      Services.prefs.setStringPref(this.PREF_CB_CATEGORY, "custom");
+    }
+  },
+
+  updateCBCategory() {
+    if (this.switchingCategory) {
+      return;
+    }
+    // Turn on switchingCategory flag, to ensure that when the individual prefs that change as a result
+    // of the category change do not trigger yet another category change.
+    this.switchingCategory = true;
+    let value = Services.prefs.getStringPref(this.PREF_CB_CATEGORY);
+    this.setPrefsToCategory(value);
+    this.switchingCategory = false;
+  },
+
+  /**
+   * Sets all user-exposed content blocking preferences to values that match the selected category.
+   */
+  setPrefsToCategory(category) {
+    // Leave prefs as they were if we are switching to "custom" category.
+    if (category == "custom") {
+      return;
+    }
+
+    for (let [pref, value] of this.CATEGORY_PREFS[category]) {
+      if (!Services.prefs.prefIsLocked(pref)) {
+        if (!value) {
+          Services.prefs.clearUserPref(pref);
+        } else {
+          switch (Services.prefs.getPrefType(pref)) {
+          case Services.prefs.PREF_BOOL:
+            Services.prefs.setBoolPref(pref, value);
+            break;
+          case Services.prefs.PREF_INT:
+            Services.prefs.setIntPref(pref, value);
+            break;
+          case Services.prefs.PREF_STRING:
+            Services.prefs.setStringPref(pref, value);
+            break;
+          }
+        }
+      }
+    }
+  },
+};
+
 /**
  * ContentPermissionIntegration is responsible for showing the user
  * simple permission prompts when content requests additional
  * capabilities.
  *
  * While there are some built-in permission prompts, createPermissionPrompt
  * can also be overridden by system add-ons or tests to provide new ones.
  *
--- a/browser/modules/test/browser/browser_UsageTelemetry_urlbar.js
+++ b/browser/modules/test/browser/browser_UsageTelemetry_urlbar.js
@@ -192,17 +192,17 @@ add_task(async function test_simpleQuery
   let resultIndexes = resultIndexHist.snapshot();
   checkHistogramResults(resultIndexes, 0, "FX_URLBAR_SELECTED_RESULT_INDEX");
 
   let resultTypes = resultTypeHist.snapshot();
   checkHistogramResults(resultTypes,
     URLBAR_SELECTED_RESULT_TYPES.searchengine,
     "FX_URLBAR_SELECTED_RESULT_TYPE");
 
-  let resultIndexByType = resultIndexByTypeHist.snapshot("searchengine");
+  let resultIndexByType = resultIndexByTypeHist.snapshot().searchengine;
   checkHistogramResults(resultIndexByType,
     0,
     "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE");
 
   let resultMethods = resultMethodHist.snapshot();
   checkHistogramResults(resultMethods,
     URLBAR_SELECTED_RESULT_METHODS.enter,
     "FX_URLBAR_SELECTED_RESULT_METHOD");
@@ -247,17 +247,17 @@ add_task(async function test_searchAlias
   let resultIndexes = resultIndexHist.snapshot();
   checkHistogramResults(resultIndexes, 0, "FX_URLBAR_SELECTED_RESULT_INDEX");
 
   let resultTypes = resultTypeHist.snapshot();
   checkHistogramResults(resultTypes,
     URLBAR_SELECTED_RESULT_TYPES.searchengine,
     "FX_URLBAR_SELECTED_RESULT_TYPE");
 
-  let resultIndexByType = resultIndexByTypeHist.snapshot("searchengine");
+  let resultIndexByType = resultIndexByTypeHist.snapshot().searchengine;
   checkHistogramResults(resultIndexByType,
     0,
     "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE");
 
   let resultMethods = resultMethodHist.snapshot();
   checkHistogramResults(resultMethods,
     URLBAR_SELECTED_RESULT_METHODS.enter,
     "FX_URLBAR_SELECTED_RESULT_METHOD");
@@ -333,17 +333,17 @@ add_task(async function test_oneOff_ente
   let resultIndexes = resultIndexHist.snapshot();
   checkHistogramResults(resultIndexes, 0, "FX_URLBAR_SELECTED_RESULT_INDEX");
 
   let resultTypes = resultTypeHist.snapshot();
   checkHistogramResults(resultTypes,
     URLBAR_SELECTED_RESULT_TYPES.searchengine,
     "FX_URLBAR_SELECTED_RESULT_TYPE");
 
-  let resultIndexByType = resultIndexByTypeHist.snapshot("searchengine");
+  let resultIndexByType = resultIndexByTypeHist.snapshot().searchengine;
   checkHistogramResults(resultIndexByType,
     0,
     "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE");
 
   let resultMethods = resultMethodHist.snapshot();
   checkHistogramResults(resultMethods,
     URLBAR_SELECTED_RESULT_METHODS.enter,
     "FX_URLBAR_SELECTED_RESULT_METHOD");
@@ -445,17 +445,17 @@ add_task(async function test_suggestion_
     let resultIndexes = resultIndexHist.snapshot();
     checkHistogramResults(resultIndexes, 3, "FX_URLBAR_SELECTED_RESULT_INDEX");
 
     let resultTypes = resultTypeHist.snapshot();
     checkHistogramResults(resultTypes,
       URLBAR_SELECTED_RESULT_TYPES.searchsuggestion,
       "FX_URLBAR_SELECTED_RESULT_TYPE");
 
-    let resultIndexByType = resultIndexByTypeHist.snapshot("searchsuggestion");
+    let resultIndexByType = resultIndexByTypeHist.snapshot().searchsuggestion;
     checkHistogramResults(resultIndexByType,
       3,
       "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE");
 
     let resultMethods = resultMethodHist.snapshot();
     checkHistogramResults(resultMethods,
       URLBAR_SELECTED_RESULT_METHODS.click,
       "FX_URLBAR_SELECTED_RESULT_METHOD");
--- a/devtools/client/aboutdebugging-new/src/actions/runtimes.js
+++ b/devtools/client/aboutdebugging-new/src/actions/runtimes.js
@@ -35,17 +35,17 @@ const {
   USB_RUNTIMES_UPDATED,
   WATCH_RUNTIME_FAILURE,
   WATCH_RUNTIME_START,
   WATCH_RUNTIME_SUCCESS,
 } = require("../constants");
 
 async function getRuntimeInfo(runtime, clientWrapper) {
   const { type } = runtime;
-  const { brandName: name, channel, deviceName, version } =
+  const { name, channel, deviceName, version } =
     await clientWrapper.getDeviceDescription();
   const icon =
     (channel === "release" || channel === "beta" || channel === "aurora")
       ? `chrome://devtools/skin/images/aboutdebugging-firefox-${ channel }.svg`
       : "chrome://devtools/skin/images/aboutdebugging-firefox-nightly.svg";
 
   return {
     icon,
--- a/devtools/client/aboutdebugging-new/src/modules/client-wrapper.js
+++ b/devtools/client/aboutdebugging-new/src/modules/client-wrapper.js
@@ -54,17 +54,25 @@ class ClientWrapper {
       this.client.mainRoot.off(evt, listener);
     } else {
       this.client.removeListener(evt, listener);
     }
   }
 
   async getDeviceDescription() {
     const deviceFront = await this.client.mainRoot.getFront("device");
-    return deviceFront.getDescription();
+    const { brandName, channel, deviceName, version } =
+      await deviceFront.getDescription();
+    // Only expose a specific set of properties.
+    return {
+      channel,
+      deviceName,
+      name: brandName,
+      version,
+    };
   }
 
   async setPreference(prefName, value) {
     const prefType = PREF_TO_TYPE[prefName];
     const preferenceFront = await this.client.mainRoot.getFront("preference");
     switch (prefType) {
       case PREF_TYPES.BOOL:
         return preferenceFront.setBoolPref(prefName, value);
--- a/devtools/client/aboutdebugging-new/test/browser/browser.ini
+++ b/devtools/client/aboutdebugging-new/test/browser/browser.ini
@@ -26,8 +26,9 @@ skip-if = (os == 'linux' && bits == 32) 
 [browser_aboutdebugging_sidebar_network_runtimes.js]
 [browser_aboutdebugging_sidebar_usb_runtime.js]
 [browser_aboutdebugging_sidebar_usb_runtime_connect.js]
 [browser_aboutdebugging_sidebar_usb_runtime_refresh.js]
 [browser_aboutdebugging_sidebar_usb_status.js]
 skip-if = (os == 'linux' && bits == 32) # ADB start() fails on linux 32, see Bug 1499638
 [browser_aboutdebugging_tab_favicons.js]
 [browser_aboutdebugging_thisfirefox.js]
+[browser_aboutdebugging_thisfirefox_runtime_info.js]
--- a/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_addons_usb_runtime.js
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_addons_usb_runtime.js
@@ -14,18 +14,18 @@ Services.scriptloader.loadSubScript(CHRO
 add_task(async function() {
   const usbMocks = new UsbMocks();
   usbMocks.enableMocks();
   registerCleanupFunction(() => usbMocks.disableMocks());
 
   const { document, tab } = await openAboutDebugging();
 
   const usbClient = usbMocks.createRuntime(RUNTIME_ID, {
-    appName: RUNTIME_APP_NAME,
     deviceName: RUNTIME_DEVICE_NAME,
+    name: RUNTIME_APP_NAME,
   });
   usbMocks.emitUpdate();
 
   await connectToRuntime(RUNTIME_DEVICE_NAME, RUNTIME_APP_NAME, document);
 
   const extensionPane = getDebugTargetPane("Extensions", document);
   info("Check an empty target pane message is displayed");
   ok(extensionPane.querySelector(".js-debug-target-list-empty"),
--- a/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_debug-target-pane_usb_runtime.js
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_debug-target-pane_usb_runtime.js
@@ -16,18 +16,18 @@ add_task(async function() {
   usbMocks.enableMocks();
   registerCleanupFunction(() => {
     usbMocks.disableMocks();
   });
 
   const { document, tab } = await openAboutDebugging();
 
   usbMocks.createRuntime(RUNTIME_ID, {
-    appName: RUNTIME_APP_NAME,
     deviceName: RUNTIME_DEVICE_NAME,
+    name: RUNTIME_APP_NAME,
   });
   usbMocks.emitUpdate();
 
   await connectToRuntime(RUNTIME_DEVICE_NAME, RUNTIME_APP_NAME, document);
 
   const SUPPORTED_TARGET_PANES = [
     "Extensions",
     "Tabs",
--- a/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_sidebar_usb_runtime_refresh.js
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_sidebar_usb_runtime_refresh.js
@@ -18,18 +18,18 @@ add_task(async function() {
   const usbMocks = new UsbMocks();
   usbMocks.enableMocks();
   registerCleanupFunction(() => usbMocks.disableMocks());
 
   const { document, tab } = await openAboutDebugging();
 
   info("Create a first runtime and connect to it");
   usbMocks.createRuntime(RUNTIME_ID, {
-    appName: RUNTIME_APP_NAME,
     deviceName: RUNTIME_DEVICE_NAME,
+    name: RUNTIME_APP_NAME,
   });
   usbMocks.emitUpdate();
 
   await connectToRuntime(RUNTIME_DEVICE_NAME, RUNTIME_APP_NAME, document);
 
   info("Create a second runtime and click on Refresh Devices");
   usbMocks.createRuntime(OTHER_RUNTIME_ID, {
     deviceName: OTHER_RUNTIME_APP_NAME,
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_thisfirefox_runtime_info.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from mocks/head-client-wrapper-mock.js */
+Services.scriptloader.loadSubScript(
+  CHROME_URL_ROOT + "mocks/head-client-wrapper-mock.js", this);
+/* import-globals-from mocks/head-runtime-client-factory-mock.js */
+Services.scriptloader.loadSubScript(
+  CHROME_URL_ROOT + "mocks/head-runtime-client-factory-mock.js", this);
+
+/**
+ * Check that the runtime info is correctly displayed for ThisFirefox.
+ * Also acts as basic sanity check for the default mock of the this-firefox client.
+ */
+
+add_task(async function() {
+  // Setup a mock for our runtime client factory to return the default THIS_FIREFOX client
+  // when the client for the this-firefox runtime is requested.
+  const runtimeClientFactoryMock = createRuntimeClientFactoryMock();
+  const thisFirefoxClient = createThisFirefoxClientMock();
+  runtimeClientFactoryMock.createClientForRuntime = runtime => {
+    const { RUNTIMES } = require("devtools/client/aboutdebugging-new/src/constants");
+    if (runtime.id === RUNTIMES.THIS_FIREFOX) {
+      return { clientWrapper: thisFirefoxClient };
+    }
+    throw new Error("Unexpected runtime id " + runtime.id);
+  };
+
+  info("Enable mocks");
+  enableRuntimeClientFactoryMock(runtimeClientFactoryMock);
+  registerCleanupFunction(() => {
+    disableRuntimeClientFactoryMock();
+  });
+
+  const { document, tab } = await openAboutDebugging();
+
+  info("Check that the 'This Firefox' mock is properly displayed");
+  const thisFirefoxRuntimeInfo = document.querySelector(".js-runtime-info");
+  ok(thisFirefoxRuntimeInfo, "Runtime info for this-firefox runtime is displayed");
+  const runtimeInfoText = thisFirefoxRuntimeInfo.textContent;
+  ok(runtimeInfoText.includes("Firefox"),
+    "this-firefox runtime info shows the correct runtime name: " + runtimeInfoText);
+  ok(runtimeInfoText.includes("63.0"),
+    "this-firefox runtime info shows the correct version number: " + runtimeInfoText);
+
+  await removeTab(tab);
+});
--- a/devtools/client/aboutdebugging-new/test/browser/mocks/head-usb-mocks.js
+++ b/devtools/client/aboutdebugging-new/test/browser/mocks/head-usb-mocks.js
@@ -64,17 +64,17 @@ class UsbMocks {
 
   /**
    * Creates a USB runtime for which a client conenction can be established.
    * @param {String} id
    *        The id of the runtime.
    * @param {Object} optional object used to create the fake runtime & device
    *        - deviceName: {String} Device name
    *        - shortName: {String} Short name for the device
-   *        - appName: {String} Application name, for instance "Firefox"
+   *        - name: {String} Application name, for instance "Firefox"
    *        - channel: {String} Release channel, for instance "release", "nightly"
    *        - version: {String} Version, for instance "63.0a"
    *        - socketPath: {String} (should only be used for connecting, so not here)
    * @return {Object} Returns the mock client created for this runtime so that methods
    * can be overridden on it.
    */
   createRuntime(id, runtimeInfo = {}) {
     // Add a new runtime to the list of scanned runtimes.
@@ -84,17 +84,17 @@ class UsbMocks {
       deviceName: runtimeInfo.deviceName || "test device name",
       shortName: runtimeInfo.shortName || "testshort",
     });
 
     // Add a valid client that can be returned for this particular runtime id.
     const mockUsbClient = createClientMock();
     mockUsbClient.getDeviceDescription = () => {
       return {
-        brandName: runtimeInfo.appName || "TestBrand",
+        name: runtimeInfo.name || "TestBrand",
         channel: runtimeInfo.channel || "release",
         version: runtimeInfo.version || "1.0",
       };
     };
     this._clients[id] = mockUsbClient;
 
     return mockUsbClient;
   }
--- a/devtools/client/accessibility/components/Accessible.js
+++ b/devtools/client/accessibility/components/Accessible.js
@@ -155,25 +155,25 @@ class Accessible extends Component {
     this.setState({ expanded });
   }
 
   showHighlighter(nodeFront) {
     if (!gToolbox) {
       return;
     }
 
-    gToolbox.highlighterUtils.highlightNodeFront(nodeFront);
+    gToolbox.highlighter.highlight(nodeFront);
   }
 
   hideHighlighter() {
     if (!gToolbox) {
       return;
     }
 
-    gToolbox.highlighterUtils.unhighlight();
+    gToolbox.highlighter.unhighlight();
   }
 
   showAccessibleHighlighter(accessible) {
     const { walker, dispatch } = this.props;
     dispatch(unhighlight());
 
     if (!accessible || !walker) {
       return;
--- a/devtools/client/framework/components/ToolboxToolbar.js
+++ b/devtools/client/framework/components/ToolboxToolbar.js
@@ -252,17 +252,20 @@ class ToolboxToolbar extends Component {
       {
         id,
         disabled,
         menuId: id + "-panel",
         doc: toolbox.doc,
         className: `devtools-button command-button ${isChecked ? "checked" : ""}`,
         ref: "frameMenuButton",
         title: description,
-        onCloseButton: toolbox.highlighterUtils.unhighlight,
+        onCloseButton: async () => {
+          await toolbox.initInspector();
+          toolbox.highlighter.unhighlight();
+        },
       },
       this.createFrameList
     );
   }
 
   clickFrameButton(event) {
     const { toolbox } = this.props;
     toolbox.onSelectFrame(event.target.id);
--- a/devtools/client/framework/toolbox-highlighter-utils.js
+++ b/devtools/client/framework/toolbox-highlighter-utils.js
@@ -1,16 +1,15 @@
 /* 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 promise = require("promise");
-const flags = require("devtools/shared/flags");
 
 /**
  * Client-side highlighter shared module.
  * To be used by toolbox panels that need to highlight DOM elements.
  *
  * Highlighting and selecting elements is common enough that it needs to be at
  * toolbox level, accessible by any panel that needs it.
  * That's why the toolbox is the one that initializes the inspector and
@@ -33,20 +32,16 @@ exports.getHighlighterUtils = function(t
   }
 
   // Exported API properties will go here
   const exported = {};
 
   // Is the highlighter currently in pick mode
   let isPicking = false;
 
-  // Is the box model already displayed, used to prevent dispatching
-  // unnecessary requests, especially during toolbox shutdown
-  let isNodeFrontHighlighted = false;
-
   /**
    * Release this utils, nullifying the references to the toolbox
    */
   exported.release = function() {
     toolbox = null;
   };
 
   /**
@@ -174,87 +169,16 @@ exports.getHighlighterUtils = function(t
    * gets the focus.
    */
   function onPickerNodeCanceled() {
     cancelPicker();
     toolbox.win.focus();
   }
 
   /**
-   * Show the box model highlighter on a node in the content page.
-   * The node needs to be a NodeFront, as defined by the inspector actor
-   * @see devtools/server/actors/inspector/inspector.js
-   * @param {NodeFront} nodeFront The node to highlight
-   * @param {Object} options
-   * @return A promise that resolves when the node has been highlighted
-   */
-  const highlightNodeFront = exported.highlightNodeFront = requireInspector(
-  async function(nodeFront, options = {}) {
-    if (!nodeFront) {
-      return;
-    }
-
-    isNodeFrontHighlighted = true;
-    await toolbox.highlighter.showBoxModel(nodeFront, options);
-
-    toolbox.emit("node-highlight", nodeFront);
-  });
-
-  /**
-   * This is a convenience method in case you don't have a nodeFront but a
-   * valueGrip. This is often the case with VariablesView properties.
-   * This method will simply translate the grip into a nodeFront and call
-   * highlightNodeFront, so it has the same signature.
-   * @see highlightNodeFront
-   */
-  exported.highlightDomValueGrip =
-    requireInspector(async function(valueGrip, options = {}) {
-      const nodeFront = await gripToNodeFront(valueGrip);
-      if (nodeFront) {
-        await highlightNodeFront(nodeFront, options);
-      } else {
-        throw new Error("The ValueGrip passed could not be translated to a NodeFront");
-      }
-    });
-
-  /**
-   * Translate a debugger value grip into a node front usable by the inspector
-   * @param {ValueGrip}
-   * @return a promise that resolves to the node front when done
-   */
-  const gripToNodeFront = exported.gripToNodeFront = requireInspector(
-  async function(grip) {
-    return toolbox.walker.getNodeActorFromObjectActor(grip.actor);
-  });
-
-  /**
-   * Hide the highlighter.
-   * @param {Boolean} forceHide Only really matters in test mode (when
-   * flags.testing is true). In test mode, hovering over several nodes
-   * in the markup view doesn't hide/show the highlighter to ease testing. The
-   * highlighter stays visible at all times, except when the mouse leaves the
-   * markup view, which is when this param is passed to true
-   * @return a promise that resolves when the highlighter is hidden
-   */
-  exported.unhighlight = async function(forceHide = false) {
-    forceHide = forceHide || !flags.testing;
-
-    if (isNodeFrontHighlighted && forceHide && toolbox.highlighter) {
-      isNodeFrontHighlighted = false;
-      await toolbox.highlighter.hideBoxModel();
-    }
-
-    // unhighlight is called when destroying the toolbox, which means that by
-    // now, the toolbox reference might have been nullified already.
-    if (toolbox) {
-      toolbox.emit("node-unhighlight");
-    }
-  };
-
-  /**
    * If the main, box-model, highlighter isn't enough, or if multiple highlighters
    * are needed in parallel, this method can be used to return a new instance of a
    * highlighter actor, given a type.
    * The type of the highlighter passed must be known by the server.
    * The highlighter actor returned will have the show(nodeFront) and hide()
    * methods and needs to be released by the consumer when not needed anymore.
    * @return Promise a promise that resolves to the highlighter
    */
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -2374,17 +2374,17 @@ Toolbox.prototype = {
   onHighlightFrame: async function(frameId) {
     // Need to initInspector to check presence of getNodeActorFromWindowID
     // and use the highlighter later
     await this.initInspector();
 
     // Only enable frame highlighting when the top level document is targeted
     if (this.rootFrameSelected) {
       const frameActor = await this.walker.getNodeActorFromWindowID(frameId);
-      this.highlighterUtils.highlightNodeFront(frameActor);
+      this.highlighter.highlight(frameActor);
     }
   },
 
   /**
    * A handler for 'frameUpdate' packets received from the backend.
    * Following properties might be set on the packet:
    *
    * destroyAll {Boolean}: All frames have been destroyed.
--- a/devtools/client/inspector/animation/test/browser_animation_animation-target_highlight.js
+++ b/devtools/client/inspector/animation/test/browser_animation_animation-target_highlight.js
@@ -20,23 +20,23 @@ add_task(async function() {
   const {
     animationInspector,
     inspector,
     panel,
     toolbox,
   } = await openAnimationInspector();
 
   info("Check highlighting when mouse over on a target node");
-  const onHighlight = toolbox.once("node-highlight");
+  const onHighlight = toolbox.highlighter.once("node-highlight");
   mouseOverOnTargetNode(animationInspector, panel, 0);
   let nodeFront = await onHighlight;
   assertNodeFront(nodeFront, "DIV", "ball animated");
 
   info("Check unhighlighting when mouse out on a target node");
-  const onUnhighlight = toolbox.once("node-unhighlight");
+  const onUnhighlight = toolbox.highlighter.once("node-unhighlight");
   mouseOutOnTargetNode(animationInspector, panel, 0);
   await onUnhighlight;
   ok(true, "Unhighlighted the targe node");
 
   info("Check node is highlighted when the inspect icon is clicked");
   let onHighlighterShown = inspector.highlighters.once("box-model-highlighter-shown");
   await clickOnInspectIcon(animationInspector, panel, 0);
   nodeFront = await onHighlighterShown;
@@ -50,21 +50,21 @@ add_task(async function() {
   ok(panel.querySelectorAll(".animation-target")[0].classList.contains("highlighting"),
     "The highlighted element still should have 'highlighting' class");
 
   info("Check no highlight event occur by mouse over locked target");
   let highlightEventCount = 0;
   const highlightEventCounter = () => {
     highlightEventCount += 1;
   };
-  toolbox.on("node-highlight", highlightEventCounter);
+  toolbox.highlighter.on("node-highlight", highlightEventCounter);
   mouseOverOnTargetNode(animationInspector, panel, 0);
   await wait(500);
   is(highlightEventCount, 0, "Highlight event should not occur");
-  toolbox.off("node-highlight", highlightEventCounter);
+  toolbox.highlighter.off("node-highlight", highlightEventCounter);
 
   info("Highlighting another animation target");
   onHighlighterShown = inspector.highlighters.once("box-model-highlighter-shown");
   await clickOnInspectIcon(animationInspector, panel, 1);
   nodeFront = await onHighlighterShown;
   assertNodeFront(nodeFront, "DIV", "ball multi");
 
   info("Check the highlighted state of the animation targets");
--- a/devtools/client/inspector/animation/test/head.js
+++ b/devtools/client/inspector/animation/test/head.js
@@ -267,17 +267,17 @@ const clickOnSummaryGraph = async functi
  *        #animation-container element.
  * @param {Number} index
  *        The index of the AnimationTargetComponent to click on.
  */
 const clickOnTargetNode = async function(animationInspector, panel, index) {
   info(`Click on a target node in animation target component[${ index }]`);
   const targetEl = panel.querySelectorAll(".animation-target .objectBox")[index];
   targetEl.scrollIntoView(false);
-  const onHighlight = animationInspector.inspector.toolbox.once("node-highlight");
+  const onHighlight = animationInspector.inspector.highlighter.once("node-highlight");
   const onAnimationTargetUpdated = animationInspector.once("animation-target-rendered");
   EventUtils.synthesizeMouseAtCenter(targetEl, {}, targetEl.ownerGlobal);
   await onAnimationTargetUpdated;
   await waitForSummaryAndDetail(animationInspector);
   await onHighlight;
 };
 
 /**
--- a/devtools/client/inspector/boxmodel/box-model.js
+++ b/devtools/client/inspector/boxmodel/box-model.js
@@ -194,18 +194,17 @@ BoxModel.prototype = {
   /**
    * Hides the box-model highlighter on the currently selected element.
    */
   onHideBoxModelHighlighter() {
     if (!this.inspector) {
       return;
     }
 
-    const toolbox = this.inspector.toolbox;
-    toolbox.highlighterUtils.unhighlight();
+    this.inspector.highlighter.unhighlight();
   },
 
   /**
    * Hides the geometry editor and updates the box moodel store with the new
    * geometry editor enabled state.
    */
   onHideGeometryEditor() {
     const { markup, selection, toolbox } = this.inspector;
@@ -333,20 +332,18 @@ BoxModel.prototype = {
    * @param  {Object} options
    *         Options passed to the highlighter actor.
    */
   onShowBoxModelHighlighter(options = {}) {
     if (!this.inspector) {
       return;
     }
 
-    const toolbox = this.inspector.toolbox;
     const nodeFront = this.inspector.selection.nodeFront;
-
-    toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
+    this.inspector.highlighter.highlight(nodeFront, options);
   },
 
   /**
    * Handler for the inspector sidebar select event. Starts tracking reflows if the
    * layout panel is visible. Otherwise, stop tracking reflows. Finally, refresh the box
    * model view if it is visible.
    */
   onSidebarSelect() {
--- a/devtools/client/inspector/boxmodel/test/browser_boxmodel_guides.js
+++ b/devtools/client/inspector/boxmodel/test/browser_boxmodel_guides.js
@@ -42,17 +42,17 @@ add_task(async function() {
 });
 
 async function testGuideOnLayoutHover(elt, expectedRegion, inspector) {
   info("Synthesizing mouseover on the boxmodel-view");
   EventUtils.synthesizeMouse(elt, 50, 2, {type: "mouseover"},
     elt.ownerDocument.defaultView);
 
   info("Waiting for the node-highlight event from the toolbox");
-  await inspector.toolbox.once("node-highlight");
+  await inspector.highlighter.once("node-highlight");
 
   // Wait for the next event tick to make sure the remaining part of the
   // test is executed after finishing synthesizing mouse event.
   await new Promise(executeSoon);
 
   is(highlightedNodeFront, inspector.selection.nodeFront,
     "The right nodeFront was highlighted");
   is(highlighterOptions.region, expectedRegion,
--- a/devtools/client/inspector/breadcrumbs.js
+++ b/devtools/client/inspector/breadcrumbs.js
@@ -573,17 +573,17 @@ HTMLBreadcrumbs.prototype = {
     }
   },
 
   /**
    * On mouse out, make sure to unhighlight.
    * @param {DOMEvent} event.
    */
   handleMouseOut: function(event) {
-    this.inspector.toolbox.highlighterUtils.unhighlight();
+    this.inspector.highlighter.unhighlight();
   },
 
   /**
    * Handle a keyboard shortcut supported by the breadcrumbs widget.
    *
    * @param {String} name
    *        Name of the keyboard shortcut received.
    * @param {DOMEvent} event
@@ -718,17 +718,17 @@ HTMLBreadcrumbs.prototype = {
       button.focus();
     };
 
     button.onBreadcrumbsClick = () => {
       this.selection.setNodeFront(node, { reason: "breadcrumbs" });
     };
 
     button.onBreadcrumbsHover = () => {
-      this.inspector.toolbox.highlighterUtils.highlightNodeFront(node);
+      this.inspector.highlighter.highlight(node);
     };
 
     return button;
   },
 
   /**
    * Connecting the end of the breadcrumbs to a node.
    * @param {NodeFront} node The node to reach.
--- a/devtools/client/inspector/extensions/extension-sidebar.js
+++ b/devtools/client/inspector/extensions/extension-sidebar.js
@@ -67,42 +67,28 @@ class ExtensionSidebar {
             return new ObjectClient(this.inspector.toolbox.target.client, object);
           },
           releaseActor: (actor) => {
             if (!actor) {
               return;
             }
             this.inspector.toolbox.target.client.release(actor);
           },
-          highlightDomElement: (grip, options = {}) => {
-            const { highlighterUtils } = this.inspector.toolbox;
-
-            if (!highlighterUtils) {
-              return null;
-            }
-
-            return highlighterUtils.highlightDomValueGrip(grip, options);
+          highlightDomElement: async (grip, options = {}) => {
+            const { highlighter } = this.inspector;
+            const nodeFront = await this.inspector.walker.gripToNodeFront(grip);
+            return highlighter.highlight(nodeFront, options);
           },
           unHighlightDomElement: (forceHide = false) => {
-            const { highlighterUtils } = this.inspector.toolbox;
-
-            if (!highlighterUtils) {
-              return null;
-            }
-
-            return highlighterUtils.unhighlight(forceHide);
+            const { highlighter } = this.inspector;
+            return highlighter.unhighlight(forceHide);
           },
           openNodeInInspector: async (grip) => {
-            const { highlighterUtils } = this.inspector.toolbox;
-
-            if (!highlighterUtils) {
-              return null;
-            }
-
-            const front = await highlighterUtils.gripToNodeFront(grip);
+            const { walker } = this.inspector;
+            const front = await walker.gripToNodeFront(grip);
             const onInspectorUpdated = this.inspector.once("inspector-updated");
             const onNodeFrontSet = this.inspector.toolbox.selection.setNodeFront(front, {
               reason: "inspector-extension-sidebar",
             });
 
             return Promise.all([onNodeFrontSet, onInspectorUpdated]);
           },
         },
--- a/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
+++ b/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
@@ -178,26 +178,26 @@ add_task(async function testSidebarDOMNo
   assertObjectInspector(sidebarPanelContent, {
     expectedDOMNodes: 1,
     expectedOpenInspectors: 1,
   });
 
   // Test highlight DOMNode on mouseover.
   info("Highlight the node by moving the cursor on it");
 
-  const onNodeHighlight = toolbox.once("node-highlight");
+  const onNodeHighlight = toolbox.highlighter.once("node-highlight");
 
   moveMouseOnObjectInspectorDOMNode(sidebarPanelContent);
 
   const nodeFront = await onNodeHighlight;
   is(nodeFront.displayName, "body", "The correct node was highlighted");
 
   // Test unhighlight DOMNode on mousemove.
   info("Unhighlight the node by moving away from the node");
-  const onNodeUnhighlight = toolbox.once("node-unhighlight");
+  const onNodeUnhighlight = toolbox.highlighter.once("node-unhighlight");
 
   moveMouseOnPanelCenter(sidebarPanelContent);
 
   await onNodeUnhighlight;
   info("node-unhighlight event was fired when moving away from the node");
 
   inspectedWindowFront.destroy();
 });
@@ -211,18 +211,18 @@ add_task(async function testSidebarDOMNo
   inspector.selection.setNodeFront(null);
   let nodeFront = await onceNewNodeFront;
   is(nodeFront, undefined, "The inspector selection should have been unselected");
 
   info("Select the ObjectInspector DOMNode in the inspector panel by clicking on it");
 
   // Once we click the open-inspector icon we expect a new node front to be selected
   // and the node to have been highlighted and unhighlighted.
-  const onNodeHighlight = toolbox.once("node-highlight");
-  const onNodeUnhighlight = toolbox.once("node-unhighlight");
+  const onNodeHighlight = toolbox.highlighter.once("node-highlight");
+  const onNodeUnhighlight = toolbox.highlighter.once("node-unhighlight");
   onceNewNodeFront = inspector.selection.once("new-node-front");
 
   clickOpenInspectorIcon(sidebarPanelContent);
 
   nodeFront = await onceNewNodeFront;
   is(nodeFront.displayName, "body", "The correct node has been selected");
   nodeFront = await onNodeHighlight;
   is(nodeFront.displayName, "body", "The correct node was highlighted");
--- a/devtools/client/inspector/flexbox/test/browser_flexbox_container_element_rep.js
+++ b/devtools/client/inspector/flexbox/test/browser_flexbox_container_element_rep.js
@@ -15,17 +15,17 @@ add_task(async function() {
 
   const onFlexContainerRepRendered = waitForDOM(doc, ".flex-header-content .objectBox");
   await selectNode("#container", inspector);
   const [flexContainerRep] = await onFlexContainerRepRendered;
 
   ok(flexContainerRep, "The flex container element rep is rendered.");
 
   info("Listen to node-highlight event and mouse over the rep");
-  const onHighlight = toolbox.once("node-highlight");
+  const onHighlight = toolbox.highlighter.once("node-highlight");
   EventUtils.synthesizeMouse(flexContainerRep, 10, 5, {type: "mouseover"},
     doc.defaultView);
   const nodeFront = await onHighlight;
 
   ok(nodeFront, "nodeFront was returned from highlighting the node.");
   is(nodeFront.tagName, "DIV", "The highlighted node has the correct tagName.");
   is(nodeFront.attributes[0].name, "id",
     "The highlighted node has the correct attributes.");
--- a/devtools/client/inspector/grids/test/browser_grids_grid-list-element-rep.js
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-list-element-rep.js
@@ -25,17 +25,17 @@ add_task(async function() {
   const { store } = inspector;
 
   const gridList = doc.querySelector("#grid-list");
   const elementRep = gridList.children[0].querySelector(".open-inspector");
   info("Scrolling into the view the #grid element node rep.");
   elementRep.scrollIntoView();
 
   info("Listen to node-highlight event and mouse over the widget");
-  const onHighlight = toolbox.once("node-highlight");
+  const onHighlight = toolbox.highlighter.once("node-highlight");
   EventUtils.synthesizeMouse(elementRep, 10, 5, {type: "mouseover"}, doc.defaultView);
   const nodeFront = await onHighlight;
 
   ok(nodeFront, "nodeFront was returned from highlighting the node.");
   is(nodeFront.tagName, "DIV", "The highlighted node has the correct tagName.");
   is(nodeFront.attributes[0].name, "id",
     "The highlighted node has the correct attributes.");
   is(nodeFront.attributes[0].value, "grid", "The highlighted node has the correct id.");
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -2478,33 +2478,33 @@ Inspector.prototype = {
    *
    * @param  {NodeFront} nodeFront
    *         The node to highlight.
    * @param  {Object} options
    *         Options passed to the highlighter actor.
    */
   onShowBoxModelHighlighterForNode(nodeFront, options) {
     const toolbox = this.toolbox;
-    toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
+    toolbox.highlighter.highlight(nodeFront, options);
   },
 
   /**
    * Returns a value indicating whether a node can be deleted.
    *
    * @param {NodeFront} nodeFront
    *        The node to test for deletion
    */
   isDeletable(nodeFront) {
     return !(nodeFront.isDocumentElement ||
            nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE ||
            nodeFront.isAnonymous);
   },
 
   async inspectNodeActor(nodeActor, inspectFromAnnotation) {
-    const nodeFront = await this.walker.getNodeActorFromObjectActor(nodeActor);
+    const nodeFront = await this.walker.gripToNodeFront({ actor: nodeActor });
     if (!nodeFront) {
       console.error("The object cannot be linked to the inspector, the " +
                     "corresponding nodeFront could not be found.");
       return false;
     }
 
     const isAttached = await this.walker.isInDOMTree(nodeFront);
     if (!isAttached) {
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -450,31 +450,31 @@ MarkupView.prototype = {
    *
    * @param  {NodeFront} nodeFront
    *         The node to show the highlighter for
    * @return {Promise} Resolves when the highlighter for this nodeFront is
    *         shown, taking into account that there could already be highlighter
    *         requests queued up
    */
   _showBoxModel: function(nodeFront) {
-    return this.toolbox.highlighterUtils.highlightNodeFront(nodeFront)
+    return this.toolbox.highlighter.highlight(nodeFront)
       .catch(this._handleRejectionIfNotDestroyed);
   },
 
   /**
    * Hide the box model highlighter on a given node front
    *
    * @param  {Boolean} forceHide
-   *         See toolbox-highlighter-utils/unhighlight
+   *         See highlighterFront method `unhighlight`
    * @return {Promise} Resolves when the highlighter for this nodeFront is
    *         hidden, taking into account that there could already be highlighter
    *         requests queued up
    */
   _hideBoxModel: function(forceHide) {
-    return this.toolbox.highlighterUtils.unhighlight(forceHide)
+    return this.toolbox.highlighter.unhighlight(forceHide)
       .catch(this._handleRejectionIfNotDestroyed);
   },
 
   _briefBoxModelTimer: null,
 
   _clearBriefBoxModelTimer: function() {
     if (this._briefBoxModelTimer) {
       clearTimeout(this._briefBoxModelTimer);
--- a/devtools/client/inspector/markup/test/browser_markup_events_click_to_close.js
+++ b/devtools/client/inspector/markup/test/browser_markup_events_click_to_close.js
@@ -64,17 +64,17 @@ add_task(async function() {
     "The tooltip is still hidden after waiting for one second");
 
   info("Open the tooltip on evHolder2 again");
   onShown = tooltip.once("shown");
   EventUtils.synthesizeMouseAtCenter(evHolder2, {}, inspector.markup.win);
   await onShown;
 
   info("Click on the computed view tab");
-  const onHighlighterHidden = toolbox.once("node-unhighlight");
+  const onHighlighterHidden = toolbox.highlighter.once("node-unhighlight");
   const onTabComputedViewSelected = inspector.sidebar.once("computedview-selected");
   const computedViewTab = inspector.panelDoc.querySelector("#computedview-tab");
   EventUtils.synthesizeMouseAtCenter(computedViewTab, {},
     inspector.panelDoc.defaultView);
 
   await onTabComputedViewSelected;
   info("computed view was selected");
 
--- a/devtools/client/inspector/markup/test/browser_markup_keybindings_04.js
+++ b/devtools/client/inspector/markup/test/browser_markup_keybindings_04.js
@@ -35,17 +35,17 @@ add_task(async function() {
 });
 
 function assertNodeSelected(inspector, tagName) {
   is(inspector.selection.nodeFront.tagName.toLowerCase(), tagName,
     `The <${tagName}> node is selected`);
 }
 
 function selectPreviousNodeWithArrowUp(inspector) {
-  const onNodeHighlighted = inspector.toolbox.once("node-highlight");
+  const onNodeHighlighted = inspector.highlighter.once("node-highlight");
   const onUpdated = inspector.once("inspector-updated");
   EventUtils.synthesizeKey("KEY_ArrowUp");
   return Promise.all([onUpdated, onNodeHighlighted]);
 }
 
 async function selectWithElementPicker(inspector, testActor) {
   await startPicker(inspector.toolbox);
 
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -203,16 +203,17 @@ skip-if = (os == "win" && debug) # bug 9
 [browser_rules_mark_overridden_01.js]
 [browser_rules_mark_overridden_02.js]
 [browser_rules_mark_overridden_03.js]
 [browser_rules_mark_overridden_04.js]
 [browser_rules_mark_overridden_05.js]
 [browser_rules_mark_overridden_06.js]
 [browser_rules_mark_overridden_07.js]
 [browser_rules_mathml-element.js]
+[browser_rules_media-queries_reload.js]
 [browser_rules_media-queries.js]
 [browser_rules_multiple-properties-duplicates.js]
 [browser_rules_multiple-properties-priority.js]
 [browser_rules_multiple-properties-unfinished_01.js]
 [browser_rules_multiple-properties-unfinished_02.js]
 [browser_rules_multiple_properties_01.js]
 [browser_rules_multiple_properties_02.js]
 [browser_rules_non_ascii.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_media-queries_reload.js
@@ -0,0 +1,65 @@
+/* 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/ */
+
+"use strict";
+
+// Test that applicable media queries are updated in the Rule view after reloading
+// the page and resizing the window.
+
+const TEST_URI = `
+  <style type='text/css'>
+    @media all and (max-width: 500px) {
+      div {
+        color: red;
+      }
+    }
+    @media all and (min-width: 500px) {
+      div {
+        color: green;
+      }
+    }
+  </style>
+  <div></div>
+`;
+
+add_task(async function() {
+  await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  const { inspector, view: ruleView, testActor, toolbox } = await openRuleView();
+  const hostWindow = toolbox.win.parent;
+
+  const originalWidth = hostWindow.outerWidth;
+  const originalHeight = hostWindow.outerHeight;
+
+  await selectNode("div", inspector);
+
+  info("Resize window so the media query for small viewports applies");
+  let onRuleViewRefreshed = ruleView.once("ruleview-refreshed");
+  let onResize = once(hostWindow, "resize");
+  hostWindow.resizeTo(400, 400);
+  await onResize;
+  await testActor.reflow();
+  await onRuleViewRefreshed;
+  let rule = getRuleViewRuleEditor(ruleView, 1).rule;
+  is(rule.textProps[0].value, "red", "Small viewport media query inspected");
+
+  info("Reload the current page");
+  await reloadPage(inspector, testActor);
+  await selectNode("div", inspector);
+
+  info("Resize window so the media query for large viewports applies");
+  onRuleViewRefreshed = ruleView.once("ruleview-refreshed");
+  onResize = once(hostWindow, "resize");
+  hostWindow.resizeTo(800, 800);
+  await onResize;
+  await testActor.reflow();
+  await onRuleViewRefreshed;
+  info("Reselect the rule after page reload.");
+  rule = getRuleViewRuleEditor(ruleView, 1).rule;
+  is(rule.textProps[0].value, "green", "Large viewport media query inspected");
+
+  info("Resize window to original dimentions");
+  onResize = once(hostWindow, "resize");
+  hostWindow.resizeTo(originalWidth, originalHeight);
+  await onResize;
+});
--- a/devtools/client/inspector/test/browser_inspector_breadcrumbs_highlight_hover.js
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_highlight_hover.js
@@ -9,36 +9,36 @@
 add_task(async function() {
   info("Loading the test document and opening the inspector");
   const {toolbox, inspector, testActor} = await openInspectorForURL(
     "data:text/html;charset=utf-8,<h1>foo</h1><span>bar</span>");
   info("Selecting the test node");
   await selectNode("span", inspector);
   const bcButtons = inspector.breadcrumbs.container;
 
-  let onNodeHighlighted = toolbox.once("node-highlight");
+  let onNodeHighlighted = toolbox.highlighter.once("node-highlight");
   let button = bcButtons.childNodes[1];
   EventUtils.synthesizeMouseAtCenter(button, {type: "mousemove"},
     button.ownerDocument.defaultView);
   await onNodeHighlighted;
 
   let isVisible = await testActor.isHighlighting();
   ok(isVisible, "The highlighter is shown on a markup container hover");
 
   ok((await testActor.assertHighlightedNode("body")),
      "The highlighter highlights the right node");
 
-  const onNodeUnhighlighted = toolbox.once("node-unhighlight");
+  const onNodeUnhighlighted = toolbox.highlighter.once("node-unhighlight");
   // move outside of the breadcrumb trail to trigger unhighlight
   EventUtils.synthesizeMouseAtCenter(inspector.addNodeButton,
     {type: "mousemove"},
     inspector.addNodeButton.ownerDocument.defaultView);
   await onNodeUnhighlighted;
 
-  onNodeHighlighted = toolbox.once("node-highlight");
+  onNodeHighlighted = toolbox.highlighter.once("node-highlight");
   button = bcButtons.childNodes[2];
   EventUtils.synthesizeMouseAtCenter(button, {type: "mousemove"},
     button.ownerDocument.defaultView);
   await onNodeHighlighted;
 
   isVisible = await testActor.isHighlighting();
   ok(isVisible, "The highlighter is shown on a markup container hover");
 
--- a/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js
+++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_keyboard_trap.js
@@ -51,17 +51,17 @@ add_task(async function() {
   const {breadcrumbs} = inspector;
 
   await selectNode("#i2", inspector);
 
   info("Clicking on the corresponding breadcrumbs node to focus it");
   const container = doc.getElementById("inspector-breadcrumbs");
 
   const button = container.querySelector("button[checked]");
-  const onHighlight = toolbox.once("node-highlight");
+  const onHighlight = toolbox.highlighter.once("node-highlight");
   button.click();
   await onHighlight;
 
   // Ensure a breadcrumb is focused.
   is(doc.activeElement, container, "Focus is on selected breadcrumb");
   is(container.getAttribute("aria-activedescendant"), button.id,
     "aria-activedescendant is set correctly");
 
--- a/devtools/client/inspector/test/browser_inspector_highlighter-comments.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-comments.js
@@ -44,17 +44,17 @@ add_task(async function() {
   await hoverElement("#id4");
   await assertHighlighterHidden();
 
   info("Hovering over a text node and waiting for highlighter to appear.");
   await hoverTextNode("Visible text node");
   await assertHighlighterShownOnTextNode("body", 14);
 
   function hoverContainer(container) {
-    const promise = inspector.toolbox.once("node-highlight");
+    const promise = inspector.highlighter.once("node-highlight");
 
     container.tagLine.scrollIntoView();
     EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"},
         markupView.doc.defaultView);
 
     return promise;
   }
 
--- a/devtools/client/inspector/test/browser_inspector_highlighter-zoom.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-zoom.js
@@ -16,41 +16,41 @@ const TEST_LEVELS = [2, 1, .5];
 // node, for the values given.
 const expectedStyle = (w, h, z) =>
         (z !== 1 ? `transform-origin:top left; transform:scale(${1 / z}); ` : "") +
         `position:absolute; width:${w * z}px;height:${h * z}px; ` +
         "overflow:hidden";
 
 add_task(async function() {
   const {inspector, testActor} = await openInspectorForURL(TEST_URL);
-  const highlighterUtils = inspector.toolbox.highlighterUtils;
+  const highlighter = inspector.highlighter;
 
   const div = await getNodeFront("div", inspector);
 
   for (const level of TEST_LEVELS) {
     info(`Zoom to level ${level}`);
     await testActor.zoomPageTo(level, false);
 
     info("Highlight the test node");
-    await highlighterUtils.highlightNodeFront(div);
+    await highlighter.highlight(div);
 
     const isVisible = await testActor.isHighlighting();
     ok(isVisible, `The highlighter is visible at zoom level ${level}`);
 
     await testActor.isNodeCorrectlyHighlighted("div", is);
 
     info("Check that the highlighter root wrapper node was scaled down");
 
     const style = await getElementsNodeStyle(testActor);
     const { width, height } = await testActor.getWindowDimensions();
     is(style, expectedStyle(width, height, level),
       "The style attribute of the root element is correct");
 
     info("Unhighlight the node");
-    await highlighterUtils.unhighlight();
+    await highlighter.unhighlight();
   }
 });
 
 async function getElementsNodeStyle(testActor) {
   const value =
     await testActor.getHighlighterNodeAttribute("box-model-elements", "style");
   return value;
 }
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -323,17 +323,17 @@ async function(selector, inspector, expe
  * is shown on the corresponding node
  */
 var hoverContainer = async function(selector, inspector) {
   info("Hovering over the markup-container for node " + selector);
 
   const nodeFront = await getNodeFront(selector, inspector);
   const container = getContainerForNodeFront(nodeFront, inspector);
 
-  const highlit = inspector.toolbox.once("node-highlight");
+  const highlit = inspector.highlighter.once("node-highlight");
   EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousemove"},
     inspector.markup.doc.defaultView);
   return highlit;
 };
 
 /**
  * Simulate a click on the markup-container (a line in the markup-view)
  * that corresponds to the selector passed.
--- a/devtools/client/shared/widgets/VariablesView.jsm
+++ b/devtools/client/shared/widgets/VariablesView.jsm
@@ -2756,17 +2756,17 @@ Variable.prototype = extend(Scope.protot
 
     event && event.stopPropagation();
 
     return (async function() {
       await this.toolbox.initInspector();
 
       let nodeFront = this._nodeFront;
       if (!nodeFront) {
-        nodeFront = await this.toolbox.walker.getNodeActorFromObjectActor(this._valueGrip.actor);
+        nodeFront = await this.toolbox.walker.gripToNodeFront(this._valueGrip);
       }
 
       if (nodeFront) {
         await this.toolbox.selectTool("inspector");
 
         const inspectorReady = defer();
         this.toolbox.getPanel("inspector").once("inspector-updated", inspectorReady.resolve);
         await this.toolbox.selection.setNodeFront(nodeFront, { reason: "variables-view" });
@@ -2774,38 +2774,33 @@ Variable.prototype = extend(Scope.protot
       }
     }.bind(this))();
   },
 
   /**
    * In case this variable is a DOMNode and part of a variablesview that has been
    * linked to the toolbox's inspector, then highlight the corresponding node
    */
-  highlightDomNode: function() {
+  highlightDomNode: async function() {
     if (this.toolbox) {
-      if (this._nodeFront) {
-        // If the nodeFront has been retrieved before, no need to ask the server
-        // again for it
-        this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront);
-        return;
+      await this.toolbox.initInspector();
+      if (!this._nodeFront) {
+        this.nodeFront = await this.toolbox.walker.gripToNodeFront(this._valueGrip);
       }
-
-      this.toolbox.highlighterUtils.highlightDomValueGrip(this._valueGrip).then(front => {
-        this._nodeFront = front;
-      });
+      await this.toolbox.highlighter.highlight(this._nodeFront);
     }
   },
 
   /**
    * Unhighlight a previously highlit node
    * @see highlightDomNode
    */
   unhighlightDomNode: function() {
     if (this.toolbox) {
-      this.toolbox.highlighterUtils.unhighlight();
+      this.toolbox.highlighter.unhighlight();
     }
   },
 
   /**
    * Sets a variable's configurable, enumerable and writable attributes,
    * and specifies if it's a 'this', '<exception>', '<return>' or '__proto__'
    * reference.
    */
--- a/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js
+++ b/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js
@@ -125,20 +125,18 @@ EventTooltip.prototype = {
         }
       }
 
       filename.textContent = text;
       filename.setAttribute("title", title);
       header.appendChild(filename);
 
       if (!listener.hide.debugger) {
-        const debuggerIcon = doc.createElementNS(XHTML_NS, "img");
+        const debuggerIcon = doc.createElementNS(XHTML_NS, "div");
         debuggerIcon.className = "event-tooltip-debugger-icon";
-        debuggerIcon.setAttribute("src",
-          "chrome://devtools/skin/images/jump-definition.svg");
         const openInDebugger = L10N.getStr("eventsTooltip.openInDebugger");
         debuggerIcon.setAttribute("title", openInDebugger);
         header.appendChild(debuggerIcon);
       }
 
       const attributesContainer = doc.createElementNS(XHTML_NS, "div");
       attributesContainer.className = "event-tooltip-attributes-container";
       header.appendChild(attributesContainer);
--- a/devtools/client/themes/layout.css
+++ b/devtools/client/themes/layout.css
@@ -489,16 +489,26 @@ html[dir="rtl"] .flex-item-list .devtool
 .flex-item-sizing .name {
   font-weight: 600;
   grid-column: 1;
   display: grid;
   grid-template-columns: max-content max-content;
   gap: .5em;
 }
 
+.flex-item-sizing .flexibility .name,
+.flex-item-sizing .max .name,
+.flex-item-sizing .min .name {
+  color: var(--theme-highlight-red);
+}
+
+.flex-item-sizing .base .name {
+  color: var(--theme-highlight-blue);
+}
+
 .flex-item-sizing .value {
   text-align: end;
   font-weight: 600;
   direction: ltr;
 }
 
 .flex-item-sizing .value .unit {
   color: var(--theme-comment);
--- a/devtools/client/themes/tooltips.css
+++ b/devtools/client/themes/tooltips.css
@@ -474,17 +474,22 @@
 }
 
 .event-header:not(:first-child) {
   border-width: 1px 0 0 0;
 }
 
 .event-tooltip-debugger-icon {
   -moz-context-properties: stroke;
-   stroke: currentColor;
+  stroke: currentColor;
+  background: url(resource://devtools/client/shared/components/reps/images/jump-definition.svg);
+}
+
+.theme-dark .event-tooltip-debugger-icon {
+  filter: invert(70%);
 }
 
 .devtools-tooltip-events-container {
   height: 100%;
   overflow-y: auto;
 }
 
 .event-tooltip-event-type,
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_nodes_highlight.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_nodes_highlight.js
@@ -19,33 +19,34 @@ const HTML = `
     </script>
   </html>
 `;
 const TEST_URI = "data:text/html;charset=utf-8," + encodeURI(HTML);
 
 add_task(async function() {
   const hud = await openNewTabAndConsole(TEST_URI);
   const toolbox = gDevTools.getToolbox(hud.target);
+  await toolbox.initInspector();
 
   await ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
     content.wrappedJSObject.logNode("h1");
   });
 
   const msg = await waitFor(() => findMessage(hud, "<h1>"));
   const node = msg.querySelector(".objectBox-node");
   ok(node !== null, "Node was logged as expected");
   const view = node.ownerDocument.defaultView;
 
   info("Highlight the node by moving the cursor on it");
-  const onNodeHighlight = toolbox.once("node-highlight");
+  const onNodeHighlight = toolbox.highlighter.once("node-highlight");
   EventUtils.synthesizeMouseAtCenter(node, {type: "mousemove"}, view);
 
   const nodeFront = await onNodeHighlight;
   is(nodeFront.displayName, "h1", "The correct node was highlighted");
 
   info("Unhighlight the node by moving away from the node");
-  const onNodeUnhighlight = toolbox.once("node-unhighlight");
+  const onNodeUnhighlight = toolbox.highlighter.once("node-unhighlight");
   const btn = toolbox.doc.getElementById("toolbox-meatball-menu-button");
   EventUtils.synthesizeMouseAtCenter(btn, {type: "mousemove"}, view);
 
   await onNodeUnhighlight;
   ok(true, "node-unhighlight event was fired when moving away from the node");
 });
--- a/devtools/client/webconsole/webconsole-output-wrapper.js
+++ b/devtools/client/webconsole/webconsole-output-wrapper.js
@@ -204,29 +204,32 @@ WebConsoleOutputWrapper.prototype = {
             );
           }),
           openNetworkPanel: (requestId) => {
             return this.toolbox.selectTool("netmonitor").then((panel) => {
               return panel.panelWin.Netmonitor.inspectRequest(requestId);
             });
           },
           sourceMapService: this.toolbox ? this.toolbox.sourceMapURLService : null,
-          highlightDomElement: (grip, options = {}) => {
-            return this.toolbox.highlighterUtils
-              ? this.toolbox.highlighterUtils.highlightDomValueGrip(grip, options)
-              : null;
+          highlightDomElement: async (grip, options = {}) => {
+            if (!this.toolbox.highlighter) {
+              return null;
+            }
+            await this.toolbox.initInspector();
+            const nodeFront = await this.toolbox.walker.gripToNodeFront(grip);
+            return this.toolbox.highlighter.highlight(nodeFront, options);
           },
           unHighlightDomElement: (forceHide = false) => {
-            return this.toolbox.highlighterUtils
-              ? this.toolbox.highlighterUtils.unhighlight(forceHide)
+            return this.toolbox.highlighter
+              ? this.toolbox.highlighter.unhighlight(forceHide)
               : null;
           },
           openNodeInInspector: async (grip) => {
             const onSelectInspector = this.toolbox.selectTool("inspector", "inspect_dom");
-            const onGripNodeToFront = this.toolbox.highlighterUtils.gripToNodeFront(grip);
+            const onGripNodeToFront = this.toolbox.walker.gripToNodeFront(grip);
             const [
               front,
               inspector,
             ] = await Promise.all([onGripNodeToFront, onSelectInspector]);
 
             const onInspectorUpdated = inspector.once("inspector-updated");
             const onNodeFrontSet = this.toolbox.selection
               .setNodeFront(front, { reason: "console" });
--- a/devtools/server/actors/reflow.js
+++ b/devtools/server/actors/reflow.js
@@ -461,31 +461,46 @@ ReflowObserver.prototype.QueryInterface 
  * Reports window resize events on the targetActor's windows.
  * @extends Observable
  * @param {BrowsingContextTargetActor} targetActor
  * @param {Function} callback Executed everytime a resize occurs
  */
 class WindowResizeObserver extends Observable {
   constructor(targetActor, callback) {
     super(targetActor, callback);
+
+    this.onNavigate = this.onNavigate.bind(this);
     this.onResize = this.onResize.bind(this);
+
+    this.targetActor.on("navigate", this.onNavigate);
   }
 
   _startListeners() {
     this.listenerTarget.addEventListener("resize", this.onResize);
   }
 
   _stopListeners() {
     this.listenerTarget.removeEventListener("resize", this.onResize);
   }
 
+  onNavigate() {
+    if (this.isObserving) {
+      this._stopListeners();
+      this._startListeners();
+    }
+  }
+
   onResize() {
     this.notifyCallback();
   }
 
+  destroy() {
+    this.targetActor.off("navigate", this.onNavigate);
+  }
+
   get listenerTarget() {
     // For the rootActor, return its window.
     if (this.targetActor.isRootActor) {
       return this.targetActor.window;
     }
 
     // Otherwise, get the targetActor's chromeEventHandler.
     return this.targetActor.chromeEventHandler;
--- a/devtools/shared/fronts/highlighters.js
+++ b/devtools/shared/fronts/highlighters.js
@@ -1,35 +1,75 @@
 /* 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 { FrontClassWithSpec, custom } = require("devtools/shared/protocol");
+const flags = require("devtools/shared/flags");
 const {
   customHighlighterSpec,
   highlighterSpec,
 } = require("devtools/shared/specs/highlighters");
 
 const HighlighterFront = FrontClassWithSpec(highlighterSpec, {
+  isNodeFrontHighlighted: false,
   // Update the object given a form representation off the wire.
   form: function(json) {
     this.actorID = json.actor;
     // FF42+ HighlighterActors starts exposing custom form, with traits object
     this.traits = json.traits || {};
   },
 
   pick: custom(function(doFocus) {
     if (doFocus && this.pickAndFocus) {
       return this.pickAndFocus();
     }
     return this._pick();
   }, {
     impl: "_pick",
   }),
+
+  /**
+   * Show the box model highlighter on a node in the content page.
+   * The node needs to be a NodeFront, as defined by the inspector actor
+   * @see devtools/server/actors/inspector/inspector.js
+   * @param {NodeFront} nodeFront The node to highlight
+   * @param {Object} options
+   * @return A promise that resolves when the node has been highlighted
+   */
+  highlight: async function(nodeFront, options = {}) {
+    if (!nodeFront) {
+      return;
+    }
+
+    this.isNodeFrontHighlighted = true;
+    await this.showBoxModel(nodeFront, options);
+    this.emit("node-highlight", nodeFront);
+  },
+
+  /**
+   * Hide the highlighter.
+   * @param {Boolean} forceHide Only really matters in test mode (when
+   * flags.testing is true). In test mode, hovering over several nodes
+   * in the markup view doesn't hide/show the highlighter to ease testing. The
+   * highlighter stays visible at all times, except when the mouse leaves the
+   * markup view, which is when this param is passed to true
+   * @return a promise that resolves when the highlighter is hidden
+   */
+  unhighlight: async function(forceHide = false) {
+    forceHide = forceHide || !flags.testing;
+
+    if (this.isNodeFrontHighlighted && forceHide) {
+      this.isNodeFrontHighlighted = false;
+      await this.hideBoxModel();
+    }
+
+    this.emit("node-unhighlight");
+  },
 });
 
 exports.HighlighterFront = HighlighterFront;
 
 const CustomHighlighterFront = FrontClassWithSpec(customHighlighterSpec, {
   _isShown: false,
 
   show: custom(function(...args) {
--- a/devtools/shared/fronts/inspector.js
+++ b/devtools/shared/fronts/inspector.js
@@ -167,23 +167,24 @@ const WalkerFront = FrontClassWithSpec(w
   querySelector: custom(function(queryNode, selector) {
     return this._querySelector(queryNode, selector).then(response => {
       return response.node;
     });
   }, {
     impl: "_querySelector",
   }),
 
-  getNodeActorFromObjectActor: custom(function(objectActorID) {
-    return this._getNodeActorFromObjectActor(objectActorID).then(response => {
-      return response ? response.node : null;
-    });
-  }, {
-    impl: "_getNodeActorFromObjectActor",
-  }),
+  gripToNodeFront: async function(grip) {
+    const response = await this.getNodeActorFromObjectActor(grip.actor);
+    const nodeFront = response ? response.node : null;
+    if (!nodeFront) {
+      throw new Error("The ValueGrip passed could not be translated to a NodeFront");
+    }
+    return nodeFront;
+  },
 
   getNodeActorFromWindowID: custom(function(windowID) {
     return this._getNodeActorFromWindowID(windowID).then(response => {
       return response ? response.node : null;
     });
   }, {
     impl: "_getNodeActorFromWindowID",
   }),
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -1430,22 +1430,16 @@ Navigator::RequestMIDIAccess(const MIDIO
   if (!mWindow) {
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return nullptr;
   }
   MIDIAccessManager* accessMgr = MIDIAccessManager::Get();
   return accessMgr->RequestMIDIAccess(mWindow, aOptions, aRv);
 }
 
-nsINetworkProperties*
-Navigator::GetNetworkProperties()
-{
-  return GetConnection(IgnoreErrors());
-}
-
 network::Connection*
 Navigator::GetConnection(ErrorResult& aRv)
 {
   if (!mConnection) {
     if (!mWindow) {
       aRv.Throw(NS_ERROR_UNEXPECTED);
       return nullptr;
     }
--- a/dom/base/Navigator.h
+++ b/dom/base/Navigator.h
@@ -18,17 +18,16 @@
 #include "nsString.h"
 #include "nsTArray.h"
 #include "mozilla/dom/MediaKeySystemAccessManager.h"
 
 class nsPluginArray;
 class nsMimeTypeArray;
 class nsPIDOMWindowInner;
 class nsIDOMNavigatorSystemMessages;
-class nsINetworkProperties;
 class nsIPrincipal;
 class nsIURI;
 
 namespace mozilla {
 namespace dom {
 class BodyExtractorBase;
 class Geolocation;
 class systemMessageCallback;
@@ -178,17 +177,16 @@ public:
   void GetGamepads(nsTArray<RefPtr<Gamepad> >& aGamepads, ErrorResult& aRv);
   GamepadServiceTest* RequestGamepadServiceTest();
   already_AddRefed<Promise> GetVRDisplays(ErrorResult& aRv);
   void GetActiveVRDisplays(nsTArray<RefPtr<VRDisplay>>& aDisplays) const;
   VRServiceTest* RequestVRServiceTest();
   bool IsWebVRContentDetected() const;
   bool IsWebVRContentPresenting() const;
   void RequestVRPresentation(VRDisplay& aDisplay);
-  nsINetworkProperties* GetNetworkProperties();
   already_AddRefed<Promise> RequestMIDIAccess(const MIDIOptions& aOptions, ErrorResult& aRv);
 
   Presentation* GetPresentation(ErrorResult& aRv);
 
   bool SendBeacon(const nsAString& aUrl,
                   const Nullable<fetch::BodyInit>& aData,
                   ErrorResult& aRv);
 
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -112,17 +112,16 @@ EXPORTS += [
     'nsTextFragment.h',
     'nsTraversal.h',
     'nsTreeSanitizer.h',
     'nsViewportInfo.h',
     'nsWindowMemoryReporter.h',
     'nsWindowSizes.h',
     'nsWrapperCache.h',
     'nsWrapperCacheInlines.h',
-    'nsXMLNameSpaceMap.h',
     'XPathGenerator.h',
 ]
 
 if CONFIG['MOZ_WEBRTC']:
     EXPORTS += [
         'nsDOMDataChannel.h',
         'nsDOMDataChannelDeclarations.h',
     ]
@@ -358,17 +357,16 @@ UNIFIED_SOURCES += [
     'nsTraversal.cpp',
     'nsTreeSanitizer.cpp',
     'nsViewportInfo.cpp',
     'nsWindowMemoryReporter.cpp',
     'nsWindowRoot.cpp',
     'nsWrapperCache.cpp',
     'nsXHTMLContentSerializer.cpp',
     'nsXMLContentSerializer.cpp',
-    'nsXMLNameSpaceMap.cpp',
     'ParentProcessMessageManager.cpp',
     'Pose.cpp',
     'PostMessageEvent.cpp',
     'ProcessMessageManager.cpp',
     'ResponsiveImageSelector.cpp',
     'SameProcessMessageQueue.cpp',
     'ScreenLuminance.cpp',
     'ScreenOrientation.cpp',
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -913,18 +913,20 @@ nsGlobalWindowInner::nsGlobalWindowInner
     mShowFocusRingForContent(false),
     mFocusByKeyOccurred(false),
     mHasGamepad(false),
     mHasVREvents(false),
     mHasVRDisplayActivateEvents(false),
     mHasSeenGamepadInput(false),
     mSuspendDepth(0),
     mFreezeDepth(0),
+#ifdef DEBUG
+    mSerial(0),
+#endif
     mFocusMethod(0),
-    mSerial(0),
     mIdleRequestCallbackCounter(1),
     mIdleRequestExecutor(nullptr),
     mDialogAbuseCount(0),
     mAreDialogsEnabled(true),
     mObservingDidRefresh(false),
     mIteratingDocumentFlushedResolvers(false),
     mCanSkipCCGeneration(0),
     mBeforeUnloadListenerCount(0)
@@ -972,18 +974,16 @@ nsGlobalWindowInner::nsGlobalWindowInner
       mTabChild = docShell->GetTabChild();
     }
   }
 
   // We could have failed the first time through trying
   // to create the entropy collector, so we should
   // try to get one until we succeed.
 
-  mSerial = nsContentUtils::InnerOrOuterWindowCreated();
-
   static bool sFirstTime = true;
   if (sFirstTime) {
     sFirstTime = false;
     TimeoutManager::Initialize();
     Preferences::AddBoolVarCache(&gIdleObserversAPIFuzzTimeDisabled,
                                  "dom.idle-observers-api.fuzz_time.disabled",
                                  false);
   }
@@ -995,16 +995,18 @@ nsGlobalWindowInner::nsGlobalWindowInner
       // If this fails to open, Dump() knows to just go to stdout on null.
       gDumpFile = fopen(fname.get(), "wb+");
     } else {
       gDumpFile = stdout;
     }
   }
 
 #ifdef DEBUG
+  mSerial = nsContentUtils::InnerOrOuterWindowCreated();
+
   if (!PR_GetEnv("MOZ_QUIET")) {
     printf_stderr("++DOMWINDOW == %d (%p) [pid = %d] [serial = %d] [outer = %p]\n",
                   nsContentUtils::GetCurrentInnerOrOuterWindowCount(),
                   static_cast<void*>(ToCanonicalSupports(this)),
                   getpid(),
                   mSerial,
                   static_cast<void*>(ToCanonicalSupports(aOuterWindow)));
   }
@@ -1929,23 +1931,16 @@ nsGlobalWindowInner::ExecutionReady()
 
   rv = mClientSource->WindowExecutionReady(AsInner());
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
 void
-nsGlobalWindowInner::SetOpenerWindow(nsPIDOMWindowOuter* aOpener,
-                                     bool aOriginalOpener)
-{
-  FORWARD_TO_OUTER_VOID(SetOpenerWindow, (aOpener, aOriginalOpener));
-}
-
-void
 nsGlobalWindowInner::UpdateParentTarget()
 {
   // NOTE: This method is identical to
   // nsGlobalWindowOuter::UpdateParentTarget(). IF YOU UPDATE THIS METHOD,
   // UPDATE THE OTHER ONE TOO!
 
   // Try to get our frame element's tab child global (its in-process message
   // manager).  If that fails, fall back to the chrome event handler's tab
@@ -2713,33 +2708,23 @@ bool
 nsPIDOMWindowInner::HasOpenWebSockets() const
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   return mNumOfOpenWebSockets ||
          (mTopInnerWindow && mTopInnerWindow->mNumOfOpenWebSockets);
 }
 
-bool
-nsPIDOMWindowInner::GetAudioCaptured() const
-{
-  return mAudioCaptured;
-}
-
-nsresult
+void
 nsPIDOMWindowInner::SetAudioCapture(bool aCapture)
 {
-  mAudioCaptured = aCapture;
-
   RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate();
   if (service) {
     service->SetWindowAudioCaptured(GetOuterWindow(), mWindowID, aCapture);
   }
-
-  return NS_OK;
 }
 
 // nsISpeechSynthesisGetter
 
 #ifdef MOZ_WEBSPEECH
 SpeechSynthesis*
 nsGlobalWindowInner::GetSpeechSynthesis(ErrorResult& aError)
 {
@@ -3256,17 +3241,17 @@ nsGlobalWindowInner::GetOpener(JSContext
   aError = nsContentUtils::WrapNative(aCx, opener, aRetval);
 }
 
 void
 nsGlobalWindowInner::SetOpener(JSContext* aCx, JS::Handle<JS::Value> aOpener,
                                ErrorResult& aError)
 {
   if (aOpener.isNull()) {
-    SetOpenerWindow(nullptr, false);
+    FORWARD_TO_OUTER_VOID(SetOpenerWindow, (nullptr, false));
     return;
   }
 
   // If something other than null is passed, just define aOpener on our inner
   // window's JS object, wrapped into the current compartment so that for Xrays
   // we define on the Xray expando object, but don't set it on the outer window,
   // so that it'll get reset on navigation.  This is just like replaceable
   // properties, but we're not quite readonly.
@@ -4400,46 +4385,26 @@ nsGlobalWindowInner::GetExistingListener
 {
   return mListenerManager;
 }
 
 //*****************************************************************************
 // nsGlobalWindowInner::nsPIDOMWindow
 //*****************************************************************************
 
-nsPIDOMWindowOuter*
-nsGlobalWindowInner::GetPrivateRoot()
-{
-  nsGlobalWindowOuter* outer = GetOuterWindowInternal();
-  if (!outer) {
-    NS_WARNING("No outer window available!");
-    return nullptr;
-  }
-  return outer->GetPrivateRoot();
-}
-
 Location*
 nsGlobalWindowInner::GetLocation()
 {
   if (!mLocation) {
     mLocation = new dom::Location(this, GetDocShell());
   }
 
   return mLocation;
 }
 
-bool
-nsGlobalWindowInner::IsTopLevelWindowActive()
-{
-  if (GetOuterWindowInternal()) {
-    return GetOuterWindowInternal()->IsTopLevelWindowActive();
-  }
-  return false;
-}
-
 void
 nsGlobalWindowInner::MaybeUpdateTouchState()
 {
   if (mMayHaveTouchEventListener) {
     nsCOMPtr<nsIObserverService> observerService =
       services::GetObserverService();
 
     if (observerService) {
@@ -8169,17 +8134,16 @@ NextWindowID();
 nsPIDOMWindowInner::nsPIDOMWindowInner(nsPIDOMWindowOuter *aOuterWindow)
 : mMutationBits(0), mActivePeerConnections(0), mIsDocumentLoaded(false),
   mIsHandlingResizeEvent(false),
   mMayHavePaintEventListener(false), mMayHaveTouchEventListener(false),
   mMayHaveSelectionChangeEventListener(false),
   mMayHaveMouseEnterLeaveEventListener(false),
   mMayHavePointerEnterLeaveEventListener(false),
   mMayHaveTextEventListenerInDefaultGroup(false),
-  mAudioCaptured(false),
   mOuterWindow(aOuterWindow),
   // Make sure no actual window ends up with mWindowID == 0
   mWindowID(NextWindowID()), mHasNotifiedGlobalCreated(false),
   mMarkedCCGeneration(0),
   mHasTriedToCacheTopInnerWindow(false),
   mNumOfIndexedDBDatabases(0),
   mNumOfOpenWebSockets(0),
   mEvent(nullptr)
--- a/dom/base/nsGlobalWindowInner.h
+++ b/dom/base/nsGlobalWindowInner.h
@@ -334,22 +334,16 @@ public:
   bool DispatchEvent(mozilla::dom::Event& aEvent,
                      mozilla::dom::CallerType aCallerType,
                      mozilla::ErrorResult& aRv) override;
 
   void GetEventTargetParent(mozilla::EventChainPreVisitor& aVisitor) override;
 
   nsresult PostHandleEvent(mozilla::EventChainPostVisitor& aVisitor) override;
 
-  // nsPIDOMWindow
-  virtual nsPIDOMWindowOuter* GetPrivateRoot() override;
-
-  // Outer windows only.
-  virtual bool IsTopLevelWindowActive() override;
-
   virtual PopupControlState GetPopupControlState() const override;
 
   void Suspend();
   void Resume();
   virtual bool IsSuspended() const override;
 
   // Calling Freeze() on a window will automatically Suspend() it.  In
   // addition, the window and its children are further treated as no longer
@@ -381,19 +375,16 @@ public:
   void NoteDOMContentLoaded();
 
   virtual nsresult FireDelayedDOMEvents() override;
 
   virtual nsresult SetNewDocument(nsIDocument *aDocument,
                                   nsISupports *aState,
                                   bool aForceReuseInnerWindow) override;
 
-  virtual void SetOpenerWindow(nsPIDOMWindowOuter* aOpener,
-                               bool aOriginalOpener) override;
-
   virtual void MaybeUpdateTouchState() override;
 
   // Inner windows only.
   void RefreshRealmPrincipal();
 
   // For accessing protected field mFullscreen
   friend class FullscreenTransitionTask;
 
@@ -509,20 +500,16 @@ public:
   virtual void EnableDeviceSensor(uint32_t aType) override;
   virtual void DisableDeviceSensor(uint32_t aType) override;
 
 #if defined(MOZ_WIDGET_ANDROID)
   virtual void EnableOrientationChangeListener() override;
   virtual void DisableOrientationChangeListener() override;
 #endif
 
-  virtual uint32_t GetSerial() override {
-    return mSerial;
-  }
-
   void AddSizeOfIncludingThis(nsWindowSizes& aWindowSizes) const;
 
   void NotifyIdleObserver(IdleObserverHolder* aIdleObserverHolder,
                           bool aCallOnidle);
   nsresult HandleIdleActiveEvent();
   bool ContainsIdleObserver(mozilla::dom::MozIdleObserver& aIdleObserver,
                             uint32_t timeInS);
   void HandleIdleObserverCallback();
@@ -1425,21 +1412,23 @@ protected:
 
   nsCOMPtr<nsIPrincipal> mDocumentPrincipal;
   // mTabChild is only ever populated in the content process.
   nsCOMPtr<nsITabChild>  mTabChild;
 
   uint32_t mSuspendDepth;
   uint32_t mFreezeDepth;
 
+#ifdef DEBUG
+  uint32_t mSerial;
+#endif
+
   // the method that was used to focus mFocusedNode
   uint32_t mFocusMethod;
 
-  uint32_t mSerial;
-
   // The current idle request callback handle
   uint32_t mIdleRequestCallbackCounter;
   IdleRequests mIdleRequestCallbacks;
   RefPtr<IdleRequestExecutor> mIdleRequestExecutor;
 
 #ifdef DEBUG
   nsCOMPtr<nsIURI> mLastOpenedURI;
 #endif
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -837,18 +837,18 @@ nsGlobalWindowOuter::nsGlobalWindowOuter
     mIsPopupSpam(false),
     mBlockScriptedClosingFlag(false),
     mWasOffline(false),
     mCreatingInnerWindow(false),
     mIsChrome(false),
     mAllowScriptsToClose(false),
     mTopLevelOuterContentWindow(false),
     mHasStorageAccess(false),
+#ifdef DEBUG
     mSerial(0),
-#ifdef DEBUG
     mSetOpenerWindowCalled(false),
 #endif
     mCleanedUp(false),
 #ifdef DEBUG
     mIsValidatingTabGroup(false),
 #endif
     mCanSkipCCGeneration(0),
     mAutoActivateVRDisplayID(0)
@@ -863,19 +863,19 @@ nsGlobalWindowOuter::nsGlobalWindowOuter
   // |this| is an outer window. Outer windows start out frozen and
   // remain frozen until they get an inner window.
   MOZ_ASSERT(IsFrozen());
 
   // We could have failed the first time through trying
   // to create the entropy collector, so we should
   // try to get one until we succeed.
 
+#ifdef DEBUG
   mSerial = nsContentUtils::InnerOrOuterWindowCreated();
 
-#ifdef DEBUG
   if (!PR_GetEnv("MOZ_QUIET")) {
     printf_stderr("++DOMWINDOW == %d (%p) [pid = %d] [serial = %d] [outer = %p]\n",
                   nsContentUtils::GetCurrentInnerOrOuterWindowCount(),
                   static_cast<void*>(ToCanonicalSupports(this)),
                   getpid(),
                   mSerial,
                   nullptr);
   }
@@ -5389,16 +5389,21 @@ nsGlobalWindowOuter::NotifyContentBlocki
   }
   const uint32_t oldState = state;
   if (aBlocked) {
     state |= aState;
   } else if (unblocked) {
     state &= ~aState;
   }
 
+  if (state == oldState) {
+    // Avoid dispatching repeated notifications when nothing has changed
+    return;
+  }
+
   eventSink->OnSecurityChange(aChannel, oldState, state, doc->GetContentBlockingLog());
 }
 
 //static
 bool
 nsGlobalWindowOuter::SameLoadingURI(nsIDocument *aDoc, nsIChannel *aChannel)
 {
   nsCOMPtr<nsIURI> docURI = aDoc->GetDocumentURI();
--- a/dom/base/nsGlobalWindowOuter.h
+++ b/dom/base/nsGlobalWindowOuter.h
@@ -485,20 +485,16 @@ public:
                         const nsAString& aPopupWindowFeatures) override;
 
   virtual void
   NotifyContentBlockingState(unsigned aState,
                              nsIChannel* aChannel,
                              bool aBlocked,
                              nsIURI* aURIHint) override;
 
-  virtual uint32_t GetSerial() override {
-    return mSerial;
-  }
-
   void AddSizeOfIncludingThis(nsWindowSizes& aWindowSizes) const;
 
   void AllowScriptsToClose()
   {
     mAllowScriptsToClose = true;
   }
 
   // Outer windows only.
@@ -1128,19 +1124,19 @@ protected:
   RefPtr<nsDOMWindowList>     mFrames;
   RefPtr<nsDOMWindowUtils>      mWindowUtils;
   nsString                      mStatus;
 
   RefPtr<mozilla::dom::Storage> mLocalStorage;
 
   nsCOMPtr<nsIPrincipal> mDocumentPrincipal;
 
+#ifdef DEBUG
   uint32_t mSerial;
 
-#ifdef DEBUG
   bool mSetOpenerWindowCalled;
   nsCOMPtr<nsIURI> mLastOpenedURI;
 #endif
 
   bool mCleanedUp;
 
   // It's useful when we get matched EnterModalState/LeaveModalState calls, in
   // which case the outer window is responsible for unsuspending events on the
--- a/dom/base/nsPIDOMWindow.h
+++ b/dom/base/nsPIDOMWindow.h
@@ -191,18 +191,17 @@ public:
   inline bool IsLoading() const;
   inline bool IsHandlingResizeEvent() const;
 
   bool AddAudioContext(mozilla::dom::AudioContext* aAudioContext);
   void RemoveAudioContext(mozilla::dom::AudioContext* aAudioContext);
   void MuteAudioContexts();
   void UnmuteAudioContexts();
 
-  bool GetAudioCaptured() const;
-  nsresult SetAudioCapture(bool aCapture);
+  void SetAudioCapture(bool aCapture);
 
   mozilla::dom::Performance* GetPerformance();
 
   void QueuePerformanceNavigationTiming();
 
   bool HasMutationListeners(uint32_t aMutationEventType) const
   {
     if (!mOuterWindow) {
@@ -350,18 +349,16 @@ public:
   mozilla::Maybe<mozilla::dom::ServiceWorkerDescriptor> GetController() const;
 
   void NoteCalledRegisterForServiceWorkerScope(const nsACString& aScope);
 
   void NoteDOMContentLoaded();
 
   mozilla::dom::TabGroup* TabGroup();
 
-  virtual nsPIDOMWindowOuter* GetPrivateRoot() = 0;
-
   virtual mozilla::dom::CustomElementRegistry* CustomElements() = 0;
 
   // XXX: This is called on inner windows
   virtual nsPIDOMWindowOuter* GetScriptableTop() = 0;
   virtual nsPIDOMWindowOuter* GetScriptableParent() = 0;
   virtual already_AddRefed<nsPIWindowRoot> GetTopWindowRoot() = 0;
 
   mozilla::dom::EventTarget* GetChromeEventHandler() const
@@ -369,18 +366,16 @@ public:
     return mChromeEventHandler;
   }
 
   virtual nsresult RegisterIdleObserver(
     mozilla::dom::MozIdleObserver& aIdleObserver) = 0;
   virtual nsresult UnregisterIdleObserver(
     mozilla::dom::MozIdleObserver& aIdleObserver) = 0;
 
-  virtual bool IsTopLevelWindowActive() = 0;
-
   mozilla::dom::EventTarget* GetParentTarget()
   {
     if (!mParentTarget) {
       UpdateParentTarget();
     }
     return mParentTarget;
   }
 
@@ -429,26 +424,16 @@ public:
    *
    * aDocument must not be null.
    */
   virtual nsresult SetNewDocument(nsIDocument *aDocument,
                                   nsISupports *aState,
                                   bool aForceReuseInnerWindow) = 0;
 
   /**
-   * Set the opener window.  aOriginalOpener is true if and only if this is the
-   * original opener for the window.  That is, it can only be true at most once
-   * during the life cycle of a window, and then only the first time
-   * SetOpenerWindow is called.  It might never be true, of course, if the
-   * window does not have an opener when it's created.
-   */
-  virtual void SetOpenerWindow(nsPIDOMWindowOuter* aOpener,
-                               bool aOriginalOpener) = 0;
-
-  /**
    * Call this to indicate that some node (this window, its document,
    * or content in that document) has a paint event listener.
    */
   void SetHasPaintEventListeners()
   {
     mMayHavePaintEventListener = true;
   }
 
@@ -574,23 +559,16 @@ public:
   /**
    * Tell this window that there is an observer for gamepad input
    *
    * Inner windows only.
    */
   virtual void SetHasGamepadEventListener(bool aHasGamepad = true) = 0;
 
   /**
-   * NOTE! This function *will* be called on multiple threads so the
-   * implementation must not do any AddRef/Release or other actions that will
-   * mutate internal state.
-   */
-  virtual uint32_t GetSerial() = 0;
-
-  /**
    * Return the window id of this window
    */
   uint64_t WindowID() const { return mWindowID; }
 
   // WebIDL-ish APIs
   void MarkUncollectableForCCGeneration(uint32_t aGeneration)
   {
     mMarkedCCGeneration = aGeneration;
@@ -694,18 +672,16 @@ protected:
   bool mMayHaveTouchEventListener;
   bool mMayHaveSelectionChangeEventListener;
   bool mMayHaveMouseEnterLeaveEventListener;
   bool mMayHavePointerEnterLeaveEventListener;
   // Only for telemetry probe so that you can remove this after the
   // telemetry stops working.
   bool mMayHaveTextEventListenerInDefaultGroup;
 
-  bool mAudioCaptured;
-
   // Our inner window's outer window.
   nsCOMPtr<nsPIDOMWindowOuter> mOuterWindow;
 
   // The element within the document that is currently focused when this
   // window is active.
   RefPtr<mozilla::dom::Element> mFocusedElement;
 
   // The AudioContexts created for the current document, if any.
@@ -1105,23 +1081,16 @@ public:
    * This function serves double-duty for passing both |arguments| and
    * |dialogArguments| back from nsWindowWatcher to nsGlobalWindow. For the
    * latter, the array is an array of length 0 whose only element is a
    * DialogArgumentsHolder representing the JS value passed to showModalDialog.
    */
   virtual nsresult SetArguments(nsIArray *aArguments) = 0;
 
   /**
-   * NOTE! This function *will* be called on multiple threads so the
-   * implementation must not do any AddRef/Release or other actions that will
-   * mutate internal state.
-   */
-  virtual uint32_t GetSerial() = 0;
-
-  /**
    * Return the window id of this window
    */
   uint64_t WindowID() const { return mWindowID; }
 
   /**
    * Dispatch a custom event with name aEventName targeted at this window.
    * Returns whether the default action should be performed.
    *
deleted file mode 100644
--- a/dom/base/nsXMLNameSpaceMap.cpp
+++ /dev/null
@@ -1,110 +0,0 @@
-/* -*- 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/. */
-
-/*
- * A class for keeping track of prefix-to-namespace-id mappings
- */
-
-#include "nsXMLNameSpaceMap.h"
-#include "nsContentUtils.h"
-#include "nsGkAtoms.h"
-#include "nsNameSpaceManager.h"
-#include "mozilla/dom/NameSpaceConstants.h"
-
-template <>
-class nsDefaultComparator <nsNameSpaceEntry, nsAtom*> {
-  public:
-    bool Equals(const nsNameSpaceEntry& aEntry, nsAtom* const& aPrefix) const {
-      return aEntry.prefix == aPrefix;
-    }
-};
-
-template <>
-class nsDefaultComparator <nsNameSpaceEntry, int32_t> {
-  public:
-    bool Equals(const nsNameSpaceEntry& aEntry, const int32_t& aNameSpace) const {
-      return aEntry.nameSpaceID == aNameSpace;
-    }
-};
-
-
-/* static */ nsXMLNameSpaceMap*
-nsXMLNameSpaceMap::Create(bool aForXML)
-{
-  nsXMLNameSpaceMap *map = new nsXMLNameSpaceMap();
-  NS_ENSURE_TRUE(map, nullptr);
-
-  if (aForXML) {
-    nsresult rv1 = map->AddPrefix(nsGkAtoms::xmlns,
-                                  kNameSpaceID_XMLNS);
-    nsresult rv2 = map->AddPrefix(nsGkAtoms::xml, kNameSpaceID_XML);
-
-    if (NS_FAILED(rv1) || NS_FAILED(rv2)) {
-      delete map;
-      map = nullptr;
-    }
-  }
-
-  return map;
-}
-
-nsXMLNameSpaceMap::nsXMLNameSpaceMap()
-  : mNameSpaces(4)
-{
-}
-
-nsresult
-nsXMLNameSpaceMap::AddPrefix(nsAtom *aPrefix, int32_t aNameSpaceID)
-{
-  if (!mNameSpaces.Contains(aPrefix) && !mNameSpaces.AppendElement(aPrefix)) {
-    return NS_ERROR_OUT_OF_MEMORY;
-  }
-  mNameSpaces[mNameSpaces.IndexOf(aPrefix)].nameSpaceID = aNameSpaceID;
-  return NS_OK;
-}
-
-nsresult
-nsXMLNameSpaceMap::AddPrefix(nsAtom *aPrefix, nsString &aURI)
-{
-  int32_t id;
-  nsresult rv = nsContentUtils::NameSpaceManager()->RegisterNameSpace(aURI,
-                                                                      id);
-
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  return AddPrefix(aPrefix, id);
-}
-
-int32_t
-nsXMLNameSpaceMap::FindNameSpaceID(nsAtom *aPrefix) const
-{
-  size_t index = mNameSpaces.IndexOf(aPrefix);
-  if (index != mNameSpaces.NoIndex) {
-    return mNameSpaces[index].nameSpaceID;
-  }
-
-  // The default mapping for no prefix is no namespace.  If a non-null prefix
-  // was specified and we didn't find it, we return an error.
-
-  return aPrefix ? kNameSpaceID_Unknown : kNameSpaceID_None;
-}
-
-nsAtom*
-nsXMLNameSpaceMap::FindPrefix(int32_t aNameSpaceID) const
-{
-  size_t index = mNameSpaces.IndexOf(aNameSpaceID);
-  if (index != mNameSpaces.NoIndex) {
-    return mNameSpaces[index].prefix;
-  }
-
-  return nullptr;
-}
-
-void
-nsXMLNameSpaceMap::Clear()
-{
-  mNameSpaces.Clear();
-}
deleted file mode 100644
--- a/dom/base/nsXMLNameSpaceMap.h
+++ /dev/null
@@ -1,75 +0,0 @@
-/* -*- 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 nsXMLNameSpaceMap_h_
-#define nsXMLNameSpaceMap_h_
-
-#include "nsString.h"
-#include "nsTArray.h"
-#include "nsCOMPtr.h"
-#include "nsAtom.h"
-
-struct nsNameSpaceEntry
-{
-  explicit nsNameSpaceEntry(nsAtom* aPrefix)
-    : prefix(aPrefix) {}
-
-  RefPtr<nsAtom> prefix;
-  MOZ_INIT_OUTSIDE_CTOR int32_t nameSpaceID;
-};
-
-/**
- * nsXMLNameSpaceMap contains a set of prefixes which are mapped onto
- * namespaces.  It allows the set to be searched by prefix or by namespace ID.
- */
-class nsXMLNameSpaceMap
-{
-public:
-  /**
-   * Allocates a new nsXMLNameSpaceMap (with new()) and if aForXML is
-   * true initializes it with the xmlns and xml namespaces.
-   */
-  static nsXMLNameSpaceMap* Create(bool aForXML);
-
-  /**
-   * Add a prefix and its corresponding namespace ID to the map.
-   * Passing a null |aPrefix| corresponds to the default namespace, which may
-   * be set to something other than kNameSpaceID_None.
-   */
-  nsresult AddPrefix(nsAtom *aPrefix, int32_t aNameSpaceID);
-
-  /**
-   * Add a prefix and a namespace URI to the map.  The URI will be converted
-   * to its corresponding namespace ID.
-   */
-  nsresult AddPrefix(nsAtom *aPrefix, nsString &aURI);
-
-  /*
-   * Returns the namespace ID for the given prefix, if it is in the map.
-   * If |aPrefix| is null and is not in the map, then a null namespace
-   * (kNameSpaceID_None) is returned.  If |aPrefix| is non-null and is not in
-   * the map, then kNameSpaceID_Unknown is returned.
-   */
-  int32_t FindNameSpaceID(nsAtom *aPrefix) const;
-
-  /**
-   * If the given namespace ID is in the map, then the first prefix which
-   * maps to that namespace is returned.  Otherwise, null is returned.
-   */
-  nsAtom* FindPrefix(int32_t aNameSpaceID) const;
-
-  /* Removes all prefix mappings. */
-  void Clear();
-
-  ~nsXMLNameSpaceMap() { Clear(); }
-
-private:
-  nsXMLNameSpaceMap();  // use Create() to create new instances
-
-  nsTArray<nsNameSpaceEntry> mNameSpaces;
-};
-
-#endif
--- a/dom/canvas/test/captureStream_common.js
+++ b/dom/canvas/test/captureStream_common.js
@@ -123,22 +123,18 @@ CaptureStreamTestHelper.prototype = {
     while (true) {
       await Promise.race([
         new Promise(resolve => video.addEventListener("timeupdate", resolve, { once: true })),
         cancel,
       ]);
       if (aborted) {
         throw await cancel;
       }
-      try {
-        if (test(this.getPixel(video, offsetX, offsetY, width, height))) {
-          return;
-        }
-      } catch (e) {
-        info("Waiting for pixel but no video available: " + e + "\n" + e.stack);
+      if (test(this.getPixel(video, offsetX, offsetY, width, height))) {
+        return;
       }
     }
   },
 
   /*
    * Returns a promise that resolves when the top left pixel of |video| matches
    * on all channels. Use |threshold| for fuzzy matching the color on each
    * channel, in the range [0,255]. 0 means exact match, 255 accepts anything.
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -393,41 +393,45 @@ public:
       CanBubble::eNo, Cancelable::eNo);
   }
 };
 
 /**
  * This listener observes the first video frame to arrive with a non-empty size,
  * and calls HTMLMediaElement::UpdateInitialMediaSize() with that size.
  */
-class HTMLMediaElement::StreamSizeListener
+class HTMLMediaElement::VideoFrameListener
   : public DirectMediaStreamTrackListener
 {
 public:
-  explicit StreamSizeListener(HTMLMediaElement* aElement)
+  explicit VideoFrameListener(HTMLMediaElement* aElement)
     : mElement(aElement)
     , mMainThreadEventTarget(aElement->MainThreadEventTarget())
     , mInitialSizeFound(false)
   {
+    MOZ_ASSERT(NS_IsMainThread());
     MOZ_ASSERT(mElement);
     MOZ_ASSERT(mMainThreadEventTarget);
   }
 
-  void Forget() { mElement = nullptr; }
+  void Forget()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    mElement = nullptr;
+  }
 
   void ReceivedSize(gfx::IntSize aSize)
   {
     MOZ_ASSERT(NS_IsMainThread());
 
     if (!mElement) {
       return;
     }
 
-    RefPtr<HTMLMediaElement> deathGrip = mElement;
-    deathGrip->UpdateInitialMediaSize(aSize);
+    mElement->UpdateInitialMediaSize(aSize);
   }
 
   void NotifyRealtimeTrackData(MediaStreamGraph* aGraph,
                                StreamTime aTrackOffset,
                                const MediaSegment& aMedia) override
   {
     if (mInitialSizeFound) {
       return;
@@ -442,38 +446,159 @@ public:
     for (VideoSegment::ConstChunkIterator c(video); !c.IsEnded(); c.Next()) {
       if (c->mFrame.GetIntrinsicSize() != gfx::IntSize(0, 0)) {
         mInitialSizeFound = true;
         // 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.
         mMainThreadEventTarget->Dispatch(NewRunnableMethod<gfx::IntSize>(
-          "dom::HTMLMediaElement::StreamSizeListener::ReceivedSize",
+          "dom::HTMLMediaElement::VideoFrameListener::ReceivedSize",
           this,
-          &StreamSizeListener::ReceivedSize,
+          &VideoFrameListener::ReceivedSize,
           c->mFrame.GetIntrinsicSize()));
         return;
       }
     }
   }
 
 private:
   // These fields may only be accessed on the main thread
-  HTMLMediaElement* mElement;
+  WeakPtr<HTMLMediaElement> mElement;
   // We hold mElement->MainThreadEventTarget() here because the mElement could
   // be reset in Forget().
   nsCOMPtr<nsISerialEventTarget> mMainThreadEventTarget;
 
   // 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;
 };
 
+class HTMLMediaElement::StreamCaptureTrackSource
+  : public MediaStreamTrackSource
+  , public MediaStreamTrackSource::Sink
+{
+public:
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(StreamCaptureTrackSource,
+                                           MediaStreamTrackSource)
+
+  StreamCaptureTrackSource(HTMLMediaElement* aElement,
+                           MediaStreamTrackSource* aCapturedTrackSource,
+                           DOMMediaStream* aOwningStream,
+                           TrackID aDestinationTrackID)
+    : MediaStreamTrackSource(aCapturedTrackSource->GetPrincipal(), nsString())
+    , mElement(aElement)
+    , mCapturedTrackSource(aCapturedTrackSource)
+    , mOwningStream(aOwningStream)
+    , mDestinationTrackID(aDestinationTrackID)
+  {
+    MOZ_ASSERT(mElement);
+    MOZ_ASSERT(mCapturedTrackSource);
+    MOZ_ASSERT(mOwningStream);
+    MOZ_ASSERT(IsTrackIDExplicit(mDestinationTrackID));
+
+    mCapturedTrackSource->RegisterSink(this);
+  }
+
+  void Destroy() override
+  {
+    if (mCapturedTrackSource) {
+      mCapturedTrackSource->UnregisterSink(this);
+      mCapturedTrackSource = nullptr;
+    }
+  }
+
+  MediaSourceEnum GetMediaSource() const override
+  {
+    return MediaSourceEnum::Other;
+  }
+
+  CORSMode GetCORSMode() const override
+  {
+    if (!mCapturedTrackSource) {
+      // This could happen during shutdown.
+      return CORS_NONE;
+    }
+
+    return mCapturedTrackSource->GetCORSMode();
+  }
+
+  void Stop() override
+  {
+    if (mElement && mElement->mSrcStream) {
+      // Only notify if we're still playing the source stream. GC might have
+      // cleared it before the track sources.
+      mElement->NotifyOutputTrackStopped(mOwningStream, mDestinationTrackID);
+    }
+    mElement = nullptr;
+    mOwningStream = nullptr;
+
+    Destroy();
+  }
+
+  /**
+   * Do not keep the track source alive. The source lifetime is controlled by
+   * its associated tracks.
+   */
+  bool KeepsSourceAlive() const override { return false; }
+
+  /**
+   * Do not keep the track source on. It is controlled by its associated tracks.
+   */
+  bool Enabled() const override { return false; }
+
+  void Disable() override {}
+
+  void Enable() override {}
+
+  void PrincipalChanged() override
+  {
+    if (!mCapturedTrackSource) {
+      // This could happen during shutdown.
+      return;
+    }
+
+    mPrincipal = mCapturedTrackSource->GetPrincipal();
+    MediaStreamTrackSource::PrincipalChanged();
+  }
+
+  void MutedChanged(bool aNewState) override
+  {
+    if (!mCapturedTrackSource) {
+      // This could happen during shutdown.
+      return;
+    }
+
+    MediaStreamTrackSource::MutedChanged(aNewState);
+  }
+
+private:
+  virtual ~StreamCaptureTrackSource() = default;
+
+  RefPtr<HTMLMediaElement> mElement;
+  RefPtr<MediaStreamTrackSource> mCapturedTrackSource;
+  RefPtr<DOMMediaStream> mOwningStream;
+  TrackID mDestinationTrackID;
+};
+
+NS_IMPL_ADDREF_INHERITED(HTMLMediaElement::StreamCaptureTrackSource,
+                         MediaStreamTrackSource)
+NS_IMPL_RELEASE_INHERITED(HTMLMediaElement::StreamCaptureTrackSource,
+                          MediaStreamTrackSource)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
+  HTMLMediaElement::StreamCaptureTrackSource)
+NS_INTERFACE_MAP_END_INHERITING(MediaStreamTrackSource)
+NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLMediaElement::StreamCaptureTrackSource,
+                                   MediaStreamTrackSource,
+                                   mElement,
+                                   mCapturedTrackSource,
+                                   mOwningStream)
+
 /**
  * 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.
  */
 class HTMLMediaElement::MediaLoadListener final
   : public nsIStreamListener
@@ -1464,17 +1589,16 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSrcAttrStream)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSourcePointer)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLoadBlockedDoc)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSourceLoadCandidate)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioChannelWrapper)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mErrorSink->mError)
   for (uint32_t i = 0; i < tmp->mOutputStreams.Length(); ++i) {
     NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutputStreams[i].mStream)
-    NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutputStreams[i].mPreCreatedTracks)
   }
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPlayed);
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextTrackManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioTrackList)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVideoTrackList)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaKeys)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIncomingMediaKeys)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectedVideoStreamTrack)
@@ -1497,16 +1621,19 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_IN
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourcePointer)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mLoadBlockedDoc)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourceLoadCandidate)
   if (tmp->mAudioChannelWrapper) {
     tmp->mAudioChannelWrapper->Shutdown();
   }
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioChannelWrapper)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mErrorSink->mError)
+  for (OutputMediaStream& s : tmp->mOutputStreams) {
+    s.mStream->SetFinishedOnInactive(true);
+  }
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputStreams)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPlayed)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextTrackManager)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioTrackList)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mVideoTrackList)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaKeys)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mIncomingMediaKeys)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectedVideoStreamTrack)
@@ -1682,17 +1809,17 @@ HTMLMediaElement::SetSrcObject(DOMMediaS
   UpdateAudioChannelPlayingState();
   DoLoad();
 }
 
 bool
 HTMLMediaElement::Ended()
 {
   return (mDecoder && mDecoder->IsEnded()) ||
-         (mSrcStream && !mSrcStream->Active());
+         (mSrcStream && mSrcStreamPlaybackEnded);
 }
 
 void
 HTMLMediaElement::GetCurrentSrc(nsAString& aCurrentSrc)
 {
   nsAutoCString src;
   GetCurrentSpec(src);
   CopyUTF8toUTF16(src, aCurrentSrc);
@@ -1712,23 +1839,19 @@ HTMLMediaElement::ShutdownDecoder()
 {
   RemoveMediaElementFromURITable();
   NS_ASSERTION(mDecoder, "Must have decoder to shut down");
 
   mWaitingForKeyListener.DisconnectIfExists();
   if (mMediaSource) {
     mMediaSource->CompletePendingTransactions();
   }
-  for (OutputMediaStream& out : mOutputStreams) {
-    if (!out.mCapturingDecoder) {
-      continue;
-    }
-    out.mNextAvailableTrackID = std::max<TrackID>(
-      mDecoder->NextAvailableTrackIDFor(out.mStream->GetInputStream()),
-      out.mNextAvailableTrackID);
+  if (!mOutputStreams.IsEmpty()) {
+    mNextAvailableMediaDecoderOutputTrackID =
+      mDecoder->GetNextOutputStreamTrackID();
   }
   mDecoder->Shutdown();
   DDUNLINKCHILD(mDecoder.get());
   mDecoder = nullptr;
   ReportAudioTrackSilenceProportionTelemetry();
 }
 
 void
@@ -1751,21 +1874,21 @@ HTMLMediaElement::AbortExistingLoads()
 
   if (mChannelLoader) {
     mChannelLoader->Cancel();
     mChannelLoader = nullptr;
   }
 
   bool fireTimeUpdate = false;
 
-  // We need to remove StreamSizeListener before VideoTracks get emptied.
-  if (mMediaStreamSizeListener) {
-    mSelectedVideoStreamTrack->RemoveDirectListener(mMediaStreamSizeListener);
-    mMediaStreamSizeListener->Forget();
-    mMediaStreamSizeListener = nullptr;
+  // We need to remove VideoFrameListener before VideoTracks get emptied.
+  if (mVideoFrameListener) {
+    mSelectedVideoStreamTrack->RemoveDirectListener(mVideoFrameListener);
+    mVideoFrameListener->Forget();
+    mVideoFrameListener = nullptr;
   }
 
   // When aborting the existing loads, empty the objects in audio track list and
   // video track list, no events (in particular, no removetrack events) are
   // fired as part of this. Ending MediaStream sends track ended notifications,
   // so we empty the track lists prior.
   if (AudioTracks()) {
     AudioTracks()->EmptyTracks();
@@ -2191,30 +2314,30 @@ HTMLMediaElement::NotifyMediaTrackEnable
     mDisableVideo = false;
   } else {
     MOZ_ASSERT(false, "Unknown track type");
   }
 
   if (mSrcStream) {
     if (aTrack->AsVideoTrack()) {
       MOZ_ASSERT(!mSelectedVideoStreamTrack);
-      MOZ_ASSERT(!mMediaStreamSizeListener);
+      MOZ_ASSERT(!mVideoFrameListener);
 
       mSelectedVideoStreamTrack = aTrack->AsVideoTrack()->GetVideoStreamTrack();
       VideoFrameContainer* container = GetVideoFrameContainer();
       if (mSrcStreamIsPlaying && container) {
         mSelectedVideoStreamTrack->AddVideoOutput(container);
       }
       HTMLVideoElement* self = static_cast<HTMLVideoElement*>(this);
       if (self->VideoWidth() <= 1 && self->VideoHeight() <= 1) {
         // MediaInfo uses dummy values of 1 for width and height to
         // mark video as valid. We need a new stream size listener
         // if size is 0x0 or 1x1.
-        mMediaStreamSizeListener = new StreamSizeListener(this);
-        mSelectedVideoStreamTrack->AddDirectListener(mMediaStreamSizeListener);
+        mVideoFrameListener = new VideoFrameListener(this);
+        mSelectedVideoStreamTrack->AddDirectListener(mVideoFrameListener);
       }
     }
 
     if (mReadyState == HAVE_NOTHING) {
       // No MediaStreamTracks are captured until we have metadata.
       return;
     }
     for (OutputMediaStream& ms : mOutputStreams) {
@@ -2262,21 +2385,21 @@ HTMLMediaElement::NotifyMediaTrackDisabl
 
       if (shouldMute) {
         SetMutedInternal(mMuted | MUTED_BY_AUDIO_TRACK);
       }
     }
   } else if (aTrack->AsVideoTrack()) {
     if (mSrcStream) {
       MOZ_ASSERT(mSelectedVideoStreamTrack);
-      if (mSelectedVideoStreamTrack && mMediaStreamSizeListener) {
+      if (mSelectedVideoStreamTrack && mVideoFrameListener) {
         mSelectedVideoStreamTrack->RemoveDirectListener(
-          mMediaStreamSizeListener);
-        mMediaStreamSizeListener->Forget();
-        mMediaStreamSizeListener = nullptr;
+          mVideoFrameListener);
+        mVideoFrameListener->Forget();
+        mVideoFrameListener = nullptr;
       }
       VideoFrameContainer* container = GetVideoFrameContainer();
       if (mSrcStreamIsPlaying && container) {
         mSelectedVideoStreamTrack->RemoveVideoOutput(container);
       }
       mSelectedVideoStreamTrack = nullptr;
     }
   }
@@ -2321,38 +2444,16 @@ HTMLMediaElement::NotifyMediaTrackDisabl
                  "The same MediaTrack was forwarded to the output stream more "
                  "than once. This shouldn't happen.");
     }
 #endif
   }
 }
 
 void
-HTMLMediaElement::NotifyMediaStreamTracksAvailable(DOMMediaStream* aStream)
-{
-  if (!mSrcStream || mSrcStream != aStream) {
-    return;
-  }
-
-  LOG(LogLevel::Debug, ("MediaElement %p MediaStream tracks available", this));
-
-  mSrcStreamTracksAvailable = true;
-
-  bool videoHasChanged = IsVideo() && HasVideo() != !VideoTracks()->IsEmpty();
-
-  if (videoHasChanged) {
-    // We are a video element and HasVideo() changed so update the screen
-    // wakelock
-    NotifyOwnerDocumentActivityChanged();
-  }
-
-  UpdateReadyStateInternal();
-}
-
-void
 HTMLMediaElement::DealWithFailedElement(nsIContent* aSourceElement)
 {
   if (mShuttingDown) {
     return;
   }
 
   DispatchAsyncSourceError(aSourceElement);
   mMainThreadEventTarget->Dispatch(
@@ -2699,20 +2800,21 @@ HTMLMediaElement::Seeking() const
 {
   return mDecoder && mDecoder->IsSeeking();
 }
 
 double
 HTMLMediaElement::CurrentTime() const
 {
   if (MediaStream* stream = GetSrcMediaStream()) {
-    if (mSrcStreamPausedCurrentTime >= 0) {
-      return mSrcStreamPausedCurrentTime;
-    }
-    return stream->StreamTimeToSeconds(stream->GetCurrentTime());
+    MediaStreamGraph* graph = stream->Graph();
+    GraphTime currentTime = mSrcStreamPausedGraphTime == GRAPH_TIME_MAX ?
+      graph->CurrentTime() - mSrcStreamGraphTimeOffset :
+      mSrcStreamPausedGraphTime;
+    return stream->StreamTimeToSeconds(currentTime);
   }
 
   if (mDefaultPlaybackStartPosition == 0.0 && mDecoder) {
     return mDecoder->GetCurrentTime();
   }
 
   return mDefaultPlaybackStartPosition;
 }
@@ -3115,269 +3217,16 @@ HTMLMediaElement::SetMuted(bool aMuted)
 
   DispatchAsyncEvent(NS_LITERAL_STRING("volumechange"));
 
   // We allow inaudible autoplay. But changing our mute status may make this
   // media audible. So pause if we are no longer supposed to be autoplaying.
   PauseIfShouldNotBePlaying();
 }
 
-class HTMLMediaElement::StreamCaptureTrackSource
-  : public MediaStreamTrackSource
-  , public MediaStreamTrackSource::Sink
-{
-public:
-  NS_DECL_ISUPPORTS_INHERITED
-  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(StreamCaptureTrackSource,
-                                           MediaStreamTrackSource)
-
-  StreamCaptureTrackSource(HTMLMediaElement* aElement,
-                           MediaStreamTrackSource* aCapturedTrackSource,
-                           DOMMediaStream* aOwningStream,
-                           TrackID aDestinationTrackID)
-    : MediaStreamTrackSource(aCapturedTrackSource->GetPrincipal(), nsString())
-    , mElement(aElement)
-    , mCapturedTrackSource(aCapturedTrackSource)
-    , mOwningStream(aOwningStream)
-    , mDestinationTrackID(aDestinationTrackID)
-  {
-    MOZ_ASSERT(mElement);
-    MOZ_ASSERT(mCapturedTrackSource);
-    MOZ_ASSERT(mOwningStream);
-    MOZ_ASSERT(IsTrackIDExplicit(mDestinationTrackID));
-
-    mCapturedTrackSource->RegisterSink(this);
-  }
-
-  void Destroy() override
-  {
-    if (mCapturedTrackSource) {
-      mCapturedTrackSource->UnregisterSink(this);
-      mCapturedTrackSource = nullptr;
-    }
-  }
-
-  MediaSourceEnum GetMediaSource() const override
-  {
-    return MediaSourceEnum::Other;
-  }
-
-  CORSMode GetCORSMode() const override
-  {
-    if (!mCapturedTrackSource) {
-      // This could happen during shutdown.
-      return CORS_NONE;
-    }
-
-    return mCapturedTrackSource->GetCORSMode();
-  }
-
-  void Stop() override
-  {
-    if (mElement && mElement->mSrcStream) {
-      // Only notify if we're still playing the source stream. GC might have
-      // cleared it before the track sources.
-      mElement->NotifyOutputTrackStopped(mOwningStream, mDestinationTrackID);
-    }
-    mElement = nullptr;
-    mOwningStream = nullptr;
-
-    Destroy();
-  }
-
-  /**
-   * Do not keep the track source alive. The source lifetime is controlled by
-   * its associated tracks.
-   */
-  bool KeepsSourceAlive() const override { return false; }
-
-  /**
-   * Do not keep the track source on. It is controlled by its associated tracks.
-   */
-  bool Enabled() const override { return false; }
-
-  void Disable() override {}
-
-  void Enable() override {}
-
-  void PrincipalChanged() override
-  {
-    if (!mCapturedTrackSource) {
-      // This could happen during shutdown.
-      return;
-    }
-
-    mPrincipal = mCapturedTrackSource->GetPrincipal();
-    MediaStreamTrackSource::PrincipalChanged();
-  }
-
-  void MutedChanged(bool aNewState) override
-  {
-    if (!mCapturedTrackSource) {
-      // This could happen during shutdown.
-      return;
-    }
-
-    MediaStreamTrackSource::MutedChanged(aNewState);
-  }
-
-private:
-  virtual ~StreamCaptureTrackSource() {}
-
-  RefPtr<HTMLMediaElement> mElement;
-  RefPtr<MediaStreamTrackSource> mCapturedTrackSource;
-  RefPtr<DOMMediaStream> mOwningStream;
-  TrackID mDestinationTrackID;
-};
-
-NS_IMPL_ADDREF_INHERITED(HTMLMediaElement::StreamCaptureTrackSource,
-                         MediaStreamTrackSource)
-NS_IMPL_RELEASE_INHERITED(HTMLMediaElement::StreamCaptureTrackSource,
-                          MediaStreamTrackSource)
-NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
-  HTMLMediaElement::StreamCaptureTrackSource)
-NS_INTERFACE_MAP_END_INHERITING(MediaStreamTrackSource)
-NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLMediaElement::StreamCaptureTrackSource,
-                                   MediaStreamTrackSource,
-                                   mElement,
-                                   mCapturedTrackSource,
-                                   mOwningStream)
-
-class HTMLMediaElement::DecoderCaptureTrackSource
-  : public MediaStreamTrackSource
-  , public DecoderPrincipalChangeObserver
-{
-public:
-  NS_DECL_ISUPPORTS_INHERITED
-  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DecoderCaptureTrackSource,
-                                           MediaStreamTrackSource)
-
-  explicit DecoderCaptureTrackSource(HTMLMediaElement* aElement)
-    : MediaStreamTrackSource(
-        nsCOMPtr<nsIPrincipal>(aElement->GetCurrentPrincipal()).get(),
-        nsString())
-    , mElement(aElement)
-  {
-    MOZ_ASSERT(mElement);
-    mElement->AddDecoderPrincipalChangeObserver(this);
-  }
-
-  void Destroy() override
-  {
-    if (mElement) {
-      DebugOnly<bool> res =
-        mElement->RemoveDecoderPrincipalChangeObserver(this);
-      NS_ASSERTION(res,
-                   "Removing decoder principal changed observer failed. "
-                   "Had it already been removed?");
-      mElement = nullptr;
-    }
-  }
-
-  MediaSourceEnum GetMediaSource() const override
-  {
-    return MediaSourceEnum::Other;
-  }
-
-  CORSMode GetCORSMode() const override
-  {
-    if (!mElement) {
-      MOZ_ASSERT(false, "Should always have an element if in use");
-      return CORS_NONE;
-    }
-
-    return mElement->GetCORSMode();
-  }
-
-  void Stop() override
-  {
-    // We don't notify the source that a track was stopped since it will keep
-    // producing tracks until the element ends. The decoder also needs the
-    // tracks it created to be live at the source since the decoder's clock is
-    // based on MediaStreams during capture.
-  }
-
-  void Disable() override {}
-
-  void Enable() override {}
-
-  void NotifyDecoderPrincipalChanged() override
-  {
-    nsCOMPtr<nsIPrincipal> newPrincipal = mElement->GetCurrentPrincipal();
-    if (nsContentUtils::CombineResourcePrincipals(&mPrincipal, newPrincipal)) {
-      PrincipalChanged();
-    }
-  }
-
-protected:
-  virtual ~DecoderCaptureTrackSource() {}
-
-  RefPtr<HTMLMediaElement> mElement;
-};
-
-NS_IMPL_ADDREF_INHERITED(HTMLMediaElement::DecoderCaptureTrackSource,
-                         MediaStreamTrackSource)
-NS_IMPL_RELEASE_INHERITED(HTMLMediaElement::DecoderCaptureTrackSource,
-                          MediaStreamTrackSource)
-NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
-  HTMLMediaElement::DecoderCaptureTrackSource)
-NS_INTERFACE_MAP_END_INHERITING(MediaStreamTrackSource)
-NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLMediaElement::DecoderCaptureTrackSource,
-                                   MediaStreamTrackSource,
-                                   mElement)
-
-class HTMLMediaElement::CaptureStreamTrackSourceGetter
-  : public MediaStreamTrackSourceGetter
-{
-public:
-  NS_DECL_ISUPPORTS_INHERITED
-  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(CaptureStreamTrackSourceGetter,
-                                           MediaStreamTrackSourceGetter)
-
-  explicit CaptureStreamTrackSourceGetter(HTMLMediaElement* aElement)
-    : mElement(aElement)
-  {
-  }
-
-  already_AddRefed<dom::MediaStreamTrackSource> GetMediaStreamTrackSource(
-    TrackID aInputTrackID) override
-  {
-    if (mElement && mElement->mSrcStream) {
-      NS_ERROR("Captured media element playing a stream adds tracks explicitly "
-               "on main thread.");
-      return nullptr;
-    }
-
-    // We can return a new source each time here, even for different streams,
-    // since the sources don't keep any internal state and all of them call
-    // through to the same HTMLMediaElement.
-    // If this changes (after implementing Stop()?) we'll have to ensure we
-    // return the same source for all requests to the same TrackID, and only
-    // have one getter.
-    return do_AddRef(new DecoderCaptureTrackSource(mElement));
-  }
-
-protected:
-  virtual ~CaptureStreamTrackSourceGetter() {}
-
-  RefPtr<HTMLMediaElement> mElement;
-};
-
-NS_IMPL_ADDREF_INHERITED(HTMLMediaElement::CaptureStreamTrackSourceGetter,
-                         MediaStreamTrackSourceGetter)
-NS_IMPL_RELEASE_INHERITED(HTMLMediaElement::CaptureStreamTrackSourceGetter,
-                          MediaStreamTrackSourceGetter)
-NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
-  HTMLMediaElement::CaptureStreamTrackSourceGetter)
-NS_INTERFACE_MAP_END_INHERITING(MediaStreamTrackSourceGetter)
-NS_IMPL_CYCLE_COLLECTION_INHERITED(
-  HTMLMediaElement::CaptureStreamTrackSourceGetter,
-  MediaStreamTrackSourceGetter,
-  mElement)
-
 void
 HTMLMediaElement::SetCapturedOutputStreamsEnabled(bool aEnabled)
 {
   for (OutputMediaStream& ms : mOutputStreams) {
     if (ms.mCapturingDecoder) {
       MOZ_ASSERT(!ms.mCapturingMediaStream);
       continue;
     }
@@ -3529,22 +3378,19 @@ HTMLMediaElement::CaptureStreamInternal(
 
   // We don't support routing to a different graph.
   if (!mOutputStreams.IsEmpty() &&
       aGraph != mOutputStreams[0].mStream->GetInputStream()->Graph()) {
     return nullptr;
   }
 
   OutputMediaStream* out = mOutputStreams.AppendElement();
-  MediaStreamTrackSourceGetter* getter =
-    new CaptureStreamTrackSourceGetter(this);
   nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow();
-  out->mStream =
-    DOMMediaStream::CreateTrackUnionStreamAsInput(window, aGraph, getter);
-  out->mStream->SetInactiveOnFinish();
+  out->mStream = DOMMediaStream::CreateTrackUnionStreamAsInput(window, aGraph);
+  out->mStream->SetFinishedOnInactive(false);
   out->mFinishWhenEnded =
     aFinishBehavior == StreamCaptureBehavior::FINISH_WHEN_ENDED;
   out->mCapturingAudioOnly =
     aStreamCaptureType == StreamCaptureType::CAPTURE_AUDIO;
 
   if (aStreamCaptureType == StreamCaptureType::CAPTURE_AUDIO) {
     if (mSrcStream) {
       // We don't support applying volume and mute to the captured stream, when
@@ -3561,55 +3407,27 @@ 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(),
-      out->mNextAvailableTrackID,
-      aFinishBehavior == StreamCaptureBehavior::FINISH_WHEN_ENDED);
+    mDecoder->AddOutputStream(out->mStream);
   } else if (mSrcStream) {
     out->mCapturingMediaStream = true;
   }
 
   if (mReadyState == HAVE_NOTHING) {
     // Do not expose the tracks until we have metadata.
     RefPtr<DOMMediaStream> result = out->mStream;
     return result.forget();
   }
 
-  if (mDecoder) {
-    if (HasAudio()) {
-      TrackID audioTrackId = out->mNextAvailableTrackID++;
-      RefPtr<MediaStreamTrackSource> trackSource =
-        getter->GetMediaStreamTrackSource(audioTrackId);
-      RefPtr<MediaStreamTrack> track = out->mStream->CreateDOMTrack(
-        audioTrackId, MediaSegment::AUDIO, trackSource);
-      out->mPreCreatedTracks.AppendElement(track);
-      out->mStream->AddTrackInternal(track);
-      LOG(LogLevel::Debug,
-          ("Created audio track %d for captured decoder", audioTrackId));
-    }
-    if (IsVideo() && HasVideo() && !out->mCapturingAudioOnly) {
-      TrackID videoTrackId = out->mNextAvailableTrackID++;
-      RefPtr<MediaStreamTrackSource> trackSource =
-        getter->GetMediaStreamTrackSource(videoTrackId);
-      RefPtr<MediaStreamTrack> track = out->mStream->CreateDOMTrack(
-        videoTrackId, MediaSegment::VIDEO, trackSource);
-      out->mPreCreatedTracks.AppendElement(track);
-      out->mStream->AddTrackInternal(track);
-      LOG(LogLevel::Debug,
-          ("Created video track %d for captured decoder", videoTrackId));
-    }
-  }
-
   if (mSrcStream) {
     MOZ_DIAGNOSTIC_ASSERT(AudioTracks(), "Element can't have been unlinked");
     for (size_t i = 0; i < AudioTracks()->Length(); ++i) {
       AudioTrack* t = (*AudioTracks())[i];
       if (t->Enabled()) {
         AddCaptureMediaTrackToOutputStream(t, *out, false);
       }
     }
@@ -5125,26 +4943,32 @@ HTMLMediaElement::FinishDecoderSetup(Med
       [](const GenericPromise::ResolveOrRejectValue& aValue) {
         MOZ_ASSERT(aValue.IsResolve() && !aValue.ResolveValue());
       });
 #else
     ;
 #endif
   }
 
+  // Set CORSMode now before any streams are added. It won't change over time.
+  mDecoder->SetOutputStreamCORSMode(mCORSMode);
+
+  if (!mOutputStreams.IsEmpty()) {
+    mDecoder->SetNextOutputStreamTrackID(
+      mNextAvailableMediaDecoderOutputTrackID);
+  }
+
   for (OutputMediaStream& ms : mOutputStreams) {
     if (ms.mCapturingMediaStream) {
       MOZ_ASSERT(!ms.mCapturingDecoder);
       continue;
     }
 
     ms.mCapturingDecoder = true;
-    aDecoder->AddOutputStream(ms.mStream->GetInputStream()->AsProcessedStream(),
-                              ms.mNextAvailableTrackID,
-                              ms.mFinishWhenEnded);
+    aDecoder->AddOutputStream(ms.mStream);
   }
 
   if (mMediaKeys) {
     if (mMediaKeys->GetCDMProxy()) {
       mDecoder->SetCDMProxy(mMediaKeys->GetCDMProxy());
     } else {
       // CDM must have crashed.
       ShutdownDecoder();
@@ -5170,199 +4994,105 @@ HTMLMediaElement::FinishDecoderSetup(Med
     if (!mPausedForInactiveDocumentOrChannel) {
       mDecoder->Play();
     }
   }
 
   return NS_OK;
 }
 
-class HTMLMediaElement::StreamListener : public MediaStreamListener
-{
-public:
-  StreamListener(HTMLMediaElement* aElement, const char* aName)
-    : mElement(aElement)
-    , mHaveCurrentData(false)
-    , mFinished(false)
-    , mMutex(aName)
-    , mPendingNotifyOutput(false)
-  {
-  }
-  void Forget()
-  {
-    if (mElement) {
-      HTMLMediaElement* element = mElement;
-      mElement = nullptr;
-      element->UpdateReadyStateInternal();
-    }
-  }
-
-  // Main thread
-
-  MediaDecoderOwner::NextFrameStatus NextFrameStatus()
-  {
-    if (!mElement || !mHaveCurrentData || mFinished) {
-      return MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE;
-    }
-    return MediaDecoderOwner::NEXT_FRAME_AVAILABLE;
-  }
-
-  void DoNotifyOutput()
-  {
-    {
-      MutexAutoLock lock(mMutex);
-      mPendingNotifyOutput = false;
-    }
-    if (mElement && mHaveCurrentData) {
-      RefPtr<HTMLMediaElement> kungFuDeathGrip = mElement;
-      kungFuDeathGrip->FireTimeUpdate(true);
-    }
-  }
-  void DoNotifyHaveCurrentData()
-  {
-    mHaveCurrentData = true;
-    if (mElement) {
-      RefPtr<HTMLMediaElement> kungFuDeathGrip = mElement;
-      kungFuDeathGrip->FirstFrameLoaded();
-      kungFuDeathGrip->UpdateReadyStateInternal();
-    }
-    DoNotifyOutput();
-  }
-
-  // These notifications run on the media graph thread so we need to
-  // dispatch events to the main thread.
-  virtual void NotifyHasCurrentData(MediaStreamGraph* aGraph) override
-  {
-    MutexAutoLock lock(mMutex);
-    aGraph->DispatchToMainThreadAfterStreamStateUpdate(NewRunnableMethod(
-      "dom::HTMLMediaElement::StreamListener::DoNotifyHaveCurrentData",
-      this,
-      &StreamListener::DoNotifyHaveCurrentData));
-  }
-  virtual void NotifyOutput(MediaStreamGraph* aGraph,
-                            GraphTime aCurrentTime) override
-  {
-    MutexAutoLock lock(mMutex);
-    if (mPendingNotifyOutput)
-      return;
-    mPendingNotifyOutput = true;
-    aGraph->DispatchToMainThreadAfterStreamStateUpdate(
-      NewRunnableMethod("dom::HTMLMediaElement::StreamListener::DoNotifyOutput",
-                        this,
-                        &StreamListener::DoNotifyOutput));
-  }
-
-private:
-  // These fields may only be accessed on the main thread
-  HTMLMediaElement* mElement;
-  bool mHaveCurrentData;
-  bool mFinished;
-
-  // mMutex protects the fields below; they can be accessed on any thread
-  Mutex mMutex;
-  bool mPendingNotifyOutput;
-};
-
-class HTMLMediaElement::MediaStreamTracksAvailableCallback
-  : public OnTracksAvailableCallback
-{
-public:
-  explicit MediaStreamTracksAvailableCallback(HTMLMediaElement* aElement)
-    : OnTracksAvailableCallback()
-    , mElement(aElement)
-  {
-  }
-  virtual void NotifyTracksAvailable(DOMMediaStream* aStream) override
-  {
-    NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
-
-    if (!mElement) {
-      return;
-    }
-    mElement->NotifyMediaStreamTracksAvailable(aStream);
-  }
-
-private:
-  WeakPtr<HTMLMediaElement> mElement;
-};
-
 class HTMLMediaElement::MediaStreamTrackListener
   : public DOMMediaStream::TrackListener
 {
 public:
   explicit MediaStreamTrackListener(HTMLMediaElement* aElement)
     : mElement(aElement)
   {
   }
 
   void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override
   {
+    if (!mElement) {
+      return;
+    }
     mElement->NotifyMediaStreamTrackAdded(aTrack);
   }
 
   void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override
   {
+    if (!mElement) {
+      return;
+    }
     mElement->NotifyMediaStreamTrackRemoved(aTrack);
   }
 
   void NotifyActive() override
   {
+    if (!mElement) {
+      return;
+    }
     LOG(LogLevel::Debug,
         ("%p, mSrcStream %p became active",
-         mElement,
+         mElement.get(),
          mElement->mSrcStream.get()));
     mElement->CheckAutoplayDataReady();
   }
 
   void NotifyInactive() override
   {
+    if (!mElement) {
+      return;
+    }
+    if (mElement->IsPlaybackEnded()) {
+      return;
+    }
     LOG(LogLevel::Debug,
         ("%p, mSrcStream %p became inactive",
-         mElement,
+         mElement.get(),
          mElement->mSrcStream.get()));
     MOZ_ASSERT(!mElement->mSrcStream->Active());
-    if (mElement->mMediaStreamListener) {
-      mElement->mMediaStreamListener->Forget();
-    }
     mElement->PlaybackEnded();
+    mElement->UpdateReadyStateInternal();
   }
 
 protected:
-  HTMLMediaElement* const mElement;
+  const WeakPtr<HTMLMediaElement> mElement;
 };
 
 void
 HTMLMediaElement::UpdateSrcMediaStreamPlaying(uint32_t aFlags)
 {
   if (!mSrcStream) {
     return;
   }
   // We might be in cycle collection with mSrcStream->GetPlaybackStream()
   // already returning null due to unlinking.
 
   MediaStream* stream = GetSrcMediaStream();
+  MediaStreamGraph* graph = stream ? stream->Graph() : nullptr;
   bool shouldPlay = !(aFlags & REMOVING_SRC_STREAM) && !mPaused &&
                     !mPausedForInactiveDocumentOrChannel && stream;
   if (shouldPlay == mSrcStreamIsPlaying) {
     return;
   }
   mSrcStreamIsPlaying = shouldPlay;
 
   LOG(LogLevel::Debug,
       ("MediaElement %p %s playback of DOMMediaStream %p",
        this,
        shouldPlay ? "Setting up" : "Removing",
        mSrcStream.get()));
 
   if (shouldPlay) {
-    mSrcStreamPausedCurrentTime = -1;
-
-    mMediaStreamListener =
-      new StreamListener(this, "HTMLMediaElement::mMediaStreamListener");
-    stream->AddListener(mMediaStreamListener);
+    mSrcStreamPlaybackEnded = false;
+    mSrcStreamGraphTimeOffset =
+      graph->CurrentTime() - mSrcStreamPausedGraphTime;
+    mSrcStreamPausedGraphTime = GRAPH_TIME_MAX;
+
+    mWatchManager.Watch(graph->CurrentTime(), &HTMLMediaElement::UpdateSrcStreamTime);
 
     stream->AddAudioOutput(this);
     SetVolumeInternal();
     if (mSink.second()) {
       NS_WARNING("setSinkId() when playing a MediaStream is not supported yet and will be ignored");
     }
 
     VideoFrameContainer* container = GetVideoFrameContainer();
@@ -5371,93 +5101,106 @@ HTMLMediaElement::UpdateSrcMediaStreamPl
     }
 
     SetCapturedOutputStreamsEnabled(true); // Unmute
     // If the input is a media stream, we don't check its data and always regard
     // it as audible when it's playing.
     SetAudibleState(true);
   } else {
     if (stream) {
-      mSrcStreamPausedCurrentTime = CurrentTime();
-
-      stream->RemoveListener(mMediaStreamListener);
+      mSrcStreamPausedGraphTime = graph->CurrentTime() - mSrcStreamGraphTimeOffset;
+
+      mWatchManager.Unwatch(graph->CurrentTime(), &HTMLMediaElement::UpdateSrcStreamTime);
 
       stream->RemoveAudioOutput(this);
       VideoFrameContainer* container = GetVideoFrameContainer();
       if (mSelectedVideoStreamTrack && container) {
         mSelectedVideoStreamTrack->RemoveVideoOutput(container);
       }
 
       SetCapturedOutputStreamsEnabled(false); // Mute
     }
     // If stream is null, then DOMMediaStream::Destroy must have been
     // called and that will remove all listeners/outputs.
-
-    mMediaStreamListener->Forget();
-    mMediaStreamListener = nullptr;
-  }
+  }
+}
+
+void
+HTMLMediaElement::UpdateSrcStreamTime()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(mSrcStreamPausedGraphTime == GRAPH_TIME_MAX);
+  MOZ_ASSERT(!mPaused);
+  MOZ_ASSERT(!mSrcStreamPlaybackEnded);
+  FireTimeUpdate(true);
 }
 
 void
 HTMLMediaElement::SetupSrcMediaStreamPlayback(DOMMediaStream* aStream)
 {
-  NS_ASSERTION(!mSrcStream && !mMediaStreamListener &&
-                 !mMediaStreamSizeListener,
+  NS_ASSERTION(!mSrcStream && !mVideoFrameListener,
                "Should have been ended already");
 
   mSrcStream = aStream;
 
   nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow();
   if (!window) {
     return;
   }
 
+  mSrcStreamPausedGraphTime = 0;
+  if (MediaStream* stream = GetSrcMediaStream()) {
+    if (MediaStreamGraph* graph = stream->Graph()) {
+      // The current graph time will represent 0 for this media element.
+      mSrcStreamPausedGraphTime = graph->CurrentTime();
+    }
+  }
+
   UpdateSrcMediaStreamPlaying();
 
   // If we pause this media element, track changes in the underlying stream
   // will continue to fire events at this element and alter its track list.
   // That's simpler than delaying the events, but probably confusing...
   nsTArray<RefPtr<MediaStreamTrack>> tracks;
   mSrcStream->GetTracks(tracks);
   for (const RefPtr<MediaStreamTrack>& track : tracks) {
     NotifyMediaStreamTrackAdded(track);
   }
 
-  mSrcStream->OnTracksAvailable(new MediaStreamTracksAvailableCallback(this));
-  mMediaStreamTrackListener = new MediaStreamTrackListener(this);
-  mSrcStream->RegisterTrackListener(mMediaStreamTrackListener);
+  mMediaStreamTrackListener = MakeUnique<MediaStreamTrackListener>(this);
+  mSrcStream->RegisterTrackListener(mMediaStreamTrackListener.get());
 
   mSrcStream->AddPrincipalChangeObserver(this);
   mSrcStreamVideoPrincipal = mSrcStream->GetVideoPrincipal();
 
   ChangeNetworkState(NETWORK_IDLE);
   ChangeDelayLoadStatus(false);
   CheckAutoplayDataReady();
 
-  // FirstFrameLoaded() will be called when the stream has current data.
+  // FirstFrameLoaded() will be called when the stream has tracks.
 }
 
 void
 HTMLMediaElement::EndSrcMediaStreamPlayback()
 {
   MOZ_ASSERT(mSrcStream);
 
   UpdateSrcMediaStreamPlaying(REMOVING_SRC_STREAM);
 
-  if (mMediaStreamSizeListener) {
+  if (mVideoFrameListener) {
     MOZ_ASSERT(mSelectedVideoStreamTrack);
     if (mSelectedVideoStreamTrack) {
-      mSelectedVideoStreamTrack->RemoveDirectListener(mMediaStreamSizeListener);
-    }
-    mMediaStreamSizeListener->Forget();
+      mSelectedVideoStreamTrack->RemoveDirectListener(mVideoFrameListener);
+    }
+    mVideoFrameListener->Forget();
   }
   mSelectedVideoStreamTrack = nullptr;
-  mMediaStreamSizeListener = nullptr;
-
-  mSrcStream->UnregisterTrackListener(mMediaStreamTrackListener);
+  mVideoFrameListener = nullptr;
+
+  mSrcStream->UnregisterTrackListener(mMediaStreamTrackListener.get());
   mMediaStreamTrackListener = nullptr;
   mSrcStreamTracksAvailable = false;
 
   mSrcStream->RemovePrincipalChangeObserver(this);
   mSrcStreamVideoPrincipal = nullptr;
 
   for (OutputMediaStream& ms : mOutputStreams) {
     for (auto pair : ms.mTrackPorts) {
@@ -5536,16 +5279,34 @@ HTMLMediaElement::NotifyMediaStreamTrack
     // video track when there is no selected track.
     if (VideoTracks()->SelectedIndex() == -1) {
       MOZ_ASSERT(!mSelectedVideoStreamTrack);
       videoTrack->SetEnabledInternal(true, MediaTrack::FIRE_NO_EVENTS);
     }
   }
 
   UpdateReadyStateInternal();
+
+  if (!mSrcStreamTracksAvailable) {
+    mAbstractMainThread->Dispatch(NS_NewRunnableFunction(
+      "HTMLMediaElement::NotifyMediaStreamTrackAdded->FirstFrameLoaded",
+      [this, self = RefPtr<HTMLMediaElement>(this), stream = mSrcStream]()
+      {
+        if (!mSrcStream || mSrcStream != stream) {
+          return;
+        }
+
+        LOG(LogLevel::Debug, ("MediaElement %p MediaStream tracks available", this));
+
+        mSrcStreamTracksAvailable = true;
+
+        FirstFrameLoaded();
+        UpdateReadyStateInternal();
+      }));
+  }
 }
 
 void
 HTMLMediaElement::NotifyMediaStreamTrackRemoved(
   const RefPtr<MediaStreamTrack>& aTrack)
 {
   MOZ_ASSERT(aTrack);
 
@@ -5780,25 +5541,30 @@ HTMLMediaElement::PlaybackEnded()
 
   NS_ASSERTION(!mDecoder || mDecoder->IsEnded(),
                "Decoder fired ended, but not in ended state");
 
   // Discard all output streams that have finished now.
   for (int32_t i = mOutputStreams.Length() - 1; i >= 0; --i) {
     if (mOutputStreams[i].mFinishWhenEnded) {
       LOG(LogLevel::Debug,
-          ("Playback ended. Removing output stream %p",
+          ("Playback ended. Letting output stream %p go inactive",
            mOutputStreams[i].mStream.get()));
+      mOutputStreams[i].mStream->SetFinishedOnInactive(true);
+      if (mOutputStreams[i].mCapturingDecoder) {
+        mDecoder->RemoveOutputStream(mOutputStreams[i].mStream);
+      }
       mOutputStreams.RemoveElementAt(i);
     }
   }
 
   if (mSrcStream) {
     LOG(LogLevel::Debug,
         ("%p, got duration by reaching the end of the resource", this));
+    mSrcStreamPlaybackEnded = true;
     DispatchAsyncEvent(NS_LITERAL_STRING("durationchange"));
   }
 
   if (HasAttr(kNameSpaceID_None, nsGkAtoms::loop)) {
     SetCurrentTime(0);
     return;
   }
 
@@ -6117,18 +5883,20 @@ HTMLMediaElement::UpdateReadyStateIntern
     LOG(LogLevel::Debug,
         ("MediaElement %p UpdateReadyStateInternal() "
          "NEXT_FRAME_UNAVAILABLE_SEEKING; Forcing HAVE_METADATA",
          this));
     ChangeReadyState(HAVE_METADATA);
     return;
   }
 
-  if (IsVideo() && HasVideo() && !IsPlaybackEnded() && GetImageContainer() &&
-      !GetImageContainer()->HasCurrentImage()) {
+  if (IsVideo() &&
+      VideoTracks() && !VideoTracks()->IsEmpty() &&
+      !IsPlaybackEnded() &&
+      GetImageContainer() && !GetImageContainer()->HasCurrentImage()) {
     // Don't advance if we are playing video, but don't have a video frame.
     // Also, if video became available after advancing to HAVE_CURRENT_DATA
     // while we are still playing, we need to revert to HAVE_METADATA until
     // a video frame is available.
     LOG(LogLevel::Debug,
         ("MediaElement %p UpdateReadyStateInternal() "
          "Playing video but no video frame; Forcing HAVE_METADATA",
          this));
@@ -6601,17 +6369,23 @@ HTMLMediaElement::IsPotentiallyPlaying()
 }
 
 bool
 HTMLMediaElement::IsPlaybackEnded() const
 {
   // TODO:
   //   the current playback position is equal to the effective end of the media
   //   resource. See bug 449157.
-  return mReadyState >= HAVE_METADATA && mDecoder && mDecoder->IsEnded();
+  if (mDecoder) {
+    return mReadyState >= HAVE_METADATA && mDecoder->IsEnded();
+  } else if (mSrcStream) {
+    return mReadyState >= HAVE_METADATA && mSrcStreamPlaybackEnded;
+  } else {
+    return false;
+  }
 }
 
 already_AddRefed<nsIPrincipal>
 HTMLMediaElement::GetCurrentPrincipal()
 {
   if (mDecoder) {
     return mDecoder->GetCurrentPrincipal();
   }
@@ -6636,35 +6410,16 @@ HTMLMediaElement::GetCurrentVideoPrincip
 }
 
 void
 HTMLMediaElement::NotifyDecoderPrincipalChanged()
 {
   RefPtr<nsIPrincipal> principal = GetCurrentPrincipal();
 
   mDecoder->UpdateSameOriginStatus(!principal || IsCORSSameOrigin());
-
-  for (DecoderPrincipalChangeObserver* observer :
-       mDecoderPrincipalChangeObservers) {
-    observer->NotifyDecoderPrincipalChanged();
-  }
-}
-
-void
-HTMLMediaElement::AddDecoderPrincipalChangeObserver(
-  DecoderPrincipalChangeObserver* aObserver)
-{
-  mDecoderPrincipalChangeObservers.AppendElement(aObserver);
-}
-
-bool
-HTMLMediaElement::RemoveDecoderPrincipalChangeObserver(
-  DecoderPrincipalChangeObserver* aObserver)
-{
-  return mDecoderPrincipalChangeObservers.RemoveElement(aObserver);
 }
 
 void
 HTMLMediaElement::Invalidate(bool aImageSizeChanged,
                              Maybe<nsIntSize>& aNewIntrinsicSize,
                              bool aForceInvalidate)
 {
   nsIFrame* frame = GetPrimaryFrame();
@@ -6709,28 +6464,28 @@ HTMLMediaElement::UpdateMediaSize(const 
 
 void
 HTMLMediaElement::UpdateInitialMediaSize(const nsIntSize& aSize)
 {
   if (!mMediaInfo.HasVideo()) {
     UpdateMediaSize(aSize);
   }
 
-  if (!mMediaStreamSizeListener) {
+  if (!mVideoFrameListener) {
     return;
   }
 
   if (!mSelectedVideoStreamTrack) {
     MOZ_ASSERT(false);
     return;
   }
 
-  mSelectedVideoStreamTrack->RemoveDirectListener(mMediaStreamSizeListener);
-  mMediaStreamSizeListener->Forget();
-  mMediaStreamSizeListener = nullptr;
+  mSelectedVideoStreamTrack->RemoveDirectListener(mVideoFrameListener);
+  mVideoFrameListener->Forget();
+  mVideoFrameListener = nullptr;
 }
 
 void
 HTMLMediaElement::SuspendOrResumeElement(bool aPauseElement,
                                          bool aSuspendEvents)
 {
   LOG(LogLevel::Debug,
       ("%p SuspendOrResumeElement(pause=%d, suspendEvents=%d) hidden=%d",
@@ -6828,19 +6583,20 @@ HTMLMediaElement::AddRemoveSelfReference
   // element alive if it was playing and producing audio output --- right now
   // that's covered by the !mPaused check.
   nsIDocument* ownerDoc = OwnerDoc();
 
   // See the comment at the top of this file for the explanation of this
   // boolean expression.
   bool needSelfReference =
     !mShuttingDown && ownerDoc->IsActive() &&
-    (mDelayingLoadEvent || (!mPaused && mDecoder && !mDecoder->IsEnded()) ||
-     (!mPaused && mSrcStream && !mSrcStream->IsFinished()) ||
-     (mDecoder && mDecoder->IsSeeking()) || CanActivateAutoplay() ||
+    (mDelayingLoadEvent ||
+     (!mPaused && !Ended()) ||
+     (mDecoder && mDecoder->IsSeeking()) ||
+     CanActivateAutoplay() ||
      (mMediaSource ? mProgressTimer : mNetworkState == NETWORK_LOADING));
 
   if (needSelfReference != mHasSelfReference) {
     mHasSelfReference = needSelfReference;
     RefPtr<HTMLMediaElement> self = this;
     if (needSelfReference) {
       // The shutdown observer will hold a strong reference to us. This
       // will do to keep us alive. We need to know about shutdown so that
@@ -7623,18 +7379,22 @@ HTMLMediaElement::GetOrCreateTextTrackMa
   return mTextTrackManager;
 }
 
 MediaDecoderOwner::NextFrameStatus
 HTMLMediaElement::NextFrameStatus()
 {
   if (mDecoder) {
     return mDecoder->NextFrameStatus();
-  } else if (mMediaStreamListener) {
-    return mMediaStreamListener->NextFrameStatus();
+  }
+  if (mSrcStream) {
+    if (mSrcStreamTracksAvailable && !mSrcStreamPlaybackEnded) {
+      return NEXT_FRAME_AVAILABLE;
+    }
+    return NEXT_FRAME_UNAVAILABLE;
   }
   return NEXT_FRAME_UNINITIALIZED;
 }
 
 void
 HTMLMediaElement::SetDecoder(MediaDecoder* aDecoder)
 {
   MOZ_ASSERT(aDecoder); // Use ShutdownDecoder() to clear.
@@ -7829,21 +7589,21 @@ HTMLMediaElement::AudioCaptureStreamChan
   } else if (!aCapture && mCaptureStreamPort) {
     if (mDecoder) {
       ProcessedMediaStream* ps =
         mCaptureStreamPort->GetSource()->AsProcessedStream();
       MOZ_ASSERT(ps);
 
       for (uint32_t i = 0; i < mOutputStreams.Length(); i++) {
         if (mOutputStreams[i].mStream->GetPlaybackStream() == ps) {
+          mDecoder->RemoveOutputStream(mOutputStreams[i].mStream);
           mOutputStreams.RemoveElementAt(i);
           break;
         }
       }
-      mDecoder->RemoveOutputStream(ps);
     }
     mCaptureStreamPort->Destroy();
     mCaptureStreamPort = nullptr;
   }
 }
 
 void
 HTMLMediaElement::NotifyCueDisplayStatesChanged()
@@ -8158,31 +7918,16 @@ HTMLMediaElement::RemoveMediaTracks()
     mAudioTrackList->RemoveTracks();
   }
 
   if (mVideoTrackList) {
     mVideoTrackList->RemoveTracks();
   }
 
   mMediaTracksConstructed = false;
-
-  for (OutputMediaStream& ms : mOutputStreams) {
-    if (!ms.mCapturingDecoder) {
-      continue;
-    }
-    for (RefPtr<MediaStreamTrack>& t : ms.mPreCreatedTracks) {
-      if (t->Ended()) {
-        continue;
-      }
-      mAbstractMainThread->Dispatch(NewRunnableMethod(
-        "dom::HTMLMediaElement::RemoveMediaTracks",
-        t, &MediaStreamTrack::OverrideEnded));
-    }
-    ms.mPreCreatedTracks.Clear();
-  }
 }
 
 class MediaElementGMPCrashHelper : public GMPCrashHelper
 {
 public:
   explicit MediaElementGMPCrashHelper(HTMLMediaElement* aElement)
     : mElement(aElement)
   {
--- a/dom/html/HTMLMediaElement.h
+++ b/dom/html/HTMLMediaElement.h
@@ -20,17 +20,17 @@
 #include "mozilla/Attributes.h"
 #include "mozilla/dom/TextTrackManager.h"
 #include "mozilla/WeakPtr.h"
 #include "mozilla/dom/MediaKeys.h"
 #include "mozilla/StateWatching.h"
 #include "nsGkAtoms.h"
 #include "PrincipalChangeObserver.h"
 #include "nsStubMutationObserver.h"
-#include "MediaSegment.h" // for PrincipalHandle
+#include "MediaSegment.h" // for PrincipalHandle, GraphTime
 
 // X.h on Linux #defines CurrentTime as 0L, so we have to #undef it here.
 #ifdef CurrentTime
 #undef CurrentTime
 #endif
 
 #include "mozilla/dom/HTMLMediaElementBinding.h"
 
@@ -295,42 +295,17 @@ public:
   // the image container be empty with no live video tracks, we return nullptr.
   already_AddRefed<nsIPrincipal> GetCurrentVideoPrincipal();
 
   // called to notify that the principal of the decoder's media resource has changed.
   void NotifyDecoderPrincipalChanged() final;
 
   void GetEMEInfo(nsString& aEMEInfo);
 
-  // An interface for observing principal changes on the media elements
-  // MediaDecoder. This will also be notified if the active CORSMode changes.
-  class DecoderPrincipalChangeObserver
-  {
-  public:
-    virtual void NotifyDecoderPrincipalChanged() = 0;
-  };
-
-  /**
-   * Add a DecoderPrincipalChangeObserver to this media element.
-   *
-   * Ownership of the DecoderPrincipalChangeObserver remains with the caller,
-   * and it's the caller's responsibility to remove the observer before it dies.
-   */
-  void AddDecoderPrincipalChangeObserver(DecoderPrincipalChangeObserver* aObserver);
-
-  /**
-   * Remove an added DecoderPrincipalChangeObserver from this media element.
-   *
-   * Returns true if it was successfully removed.
-   */
-  bool RemoveDecoderPrincipalChangeObserver(DecoderPrincipalChangeObserver* aObserver);
-
   class StreamCaptureTrackSource;
-  class DecoderCaptureTrackSource;
-  class CaptureStreamTrackSourceGetter;
 
   // Update the visual size of the media. Called from the decoder on the
   // main thread when/if the size changes.
   void UpdateMediaSize(const nsIntSize& aSize);
   // Like UpdateMediaSize, but only updates the size if no size has yet
   // been set.
   void UpdateInitialMediaSize(const nsIntSize& aSize);
 
@@ -363,21 +338,16 @@ public:
 
   /**
    * Called by one of our associated MediaTrackLists (audio/video) when an
    * AudioTrack is disabled or a VideoTrack is unselected.
    */
   void NotifyMediaTrackDisabled(MediaTrack* aTrack);
 
   /**
-   * Called when tracks become available to the source media stream.
-   */
-  void NotifyMediaStreamTracksAvailable(DOMMediaStream* aStream);
-
-  /**
    * Called when a captured MediaStreamTrack is stopped so we can clean up its
    * MediaInputPort.
    */
   void NotifyOutputTrackStopped(DOMMediaStream* aOwningStream,
                                 TrackID aDestinationTrackID);
 
   /**
    * Returns the current load ID. Asynchronous events store the ID that was
@@ -841,20 +811,18 @@ public:
 
 protected:
   virtual ~HTMLMediaElement();
 
   class AudioChannelAgentCallback;
   class ChannelLoader;
   class ErrorSink;
   class MediaLoadListener;
-  class MediaStreamTracksAvailableCallback;
   class MediaStreamTrackListener;
-  class StreamListener;
-  class StreamSizeListener;
+  class VideoFrameListener;
   class ShutdownObserver;
 
   MediaDecoderOwner::NextFrameStatus NextFrameStatus();
 
   void SetDecoder(MediaDecoder* aDecoder);
 
   // Holds references to the DOM wrappers for the MediaStreams that we're
   // writing to.
@@ -864,21 +832,16 @@ protected:
 
     RefPtr<DOMMediaStream> mStream;
     TrackID mNextAvailableTrackID;
     bool mFinishWhenEnded;
     bool mCapturingAudioOnly;
     bool mCapturingDecoder;
     bool mCapturingMediaStream;
 
-    // The following members are keeping state for a captured MediaDecoder.
-    // Tracks that were created on main thread before MediaDecoder fed them
-    // to the MediaStreamGraph.
-    nsTArray<RefPtr<MediaStreamTrack>> mPreCreatedTracks;
-
     // The following members are keeping state for a captured MediaStream.
     nsTArray<Pair<nsString, RefPtr<MediaInputPort>>> mTrackPorts;
   };
 
   void PlayInternal(bool aHandlingUserInput);
 
   /** Use this method to change the mReadyState member, so required
    * events can be fired.
@@ -929,16 +892,22 @@ protected:
   void EndSrcMediaStreamPlayback();
   /**
    * Ensure we're playing mSrcStream if and only if we're not paused.
    */
   enum { REMOVING_SRC_STREAM = 0x1 };
   void UpdateSrcMediaStreamPlaying(uint32_t aFlags = 0);
 
   /**
+   * mSrcStream's graph's CurrentTime() has been updated. It might be time to
+   * fire "timeupdate".
+   */
+  void UpdateSrcStreamTime();
+
+  /**
    * Called by our DOMMediaStream::TrackListener when a new MediaStreamTrack has
    * been added to the playback stream of |mSrcStream|.
    */
   void NotifyMediaStreamTrackAdded(const RefPtr<MediaStreamTrack>& aTrack);
 
   /**
    * Called by our DOMMediaStream::TrackListener when a MediaStreamTrack in
    * |mSrcStream|'s playback stream has ended.
@@ -1400,20 +1369,16 @@ protected:
 
   // The DocGroup-specific nsISerialEventTarget of this HTML element on the main
   // thread.
   nsCOMPtr<nsISerialEventTarget> mMainThreadEventTarget;
 
   // The DocGroup-specific AbstractThread::MainThread() of this HTML element.
   RefPtr<AbstractThread> mAbstractMainThread;
 
-  // Observers listening to changes to the mDecoder principal.
-  // Used by streams captured from this element.
-  nsTArray<DecoderPrincipalChangeObserver*> mDecoderPrincipalChangeObservers;
-
   // A reference to the VideoFrameContainer which contains the current frame
   // of video to display.
   RefPtr<VideoFrameContainer> mVideoFrameContainer;
 
   // Holds a reference to the DOM wrapper for the MediaStream that has been
   // set in the src attribute.
   RefPtr<DOMMediaStream> mSrcAttrStream;
 
@@ -1423,34 +1388,42 @@ protected:
   // Holds a reference to the DOM wrapper for the MediaStream that we're
   // actually playing.
   // At most one of mDecoder and mSrcStream can be non-null.
   RefPtr<DOMMediaStream> mSrcStream;
 
   // True once mSrcStream's initial set of tracks are known.
   bool mSrcStreamTracksAvailable = false;
 
-  // If non-negative, the time we should return for currentTime while playing
-  // mSrcStream.
-  double mSrcStreamPausedCurrentTime = -1;
+  // While mPaused is true and mSrcStream is set, this is the value to use for
+  // CurrentTime(). Otherwise this is set to GRAPH_TIME_MAX.
+  GraphTime mSrcStreamPausedGraphTime = GRAPH_TIME_MAX;
+
+  // The offset in GraphTime that this media element started playing the
+  // playback stream of mSrcStream.
+  GraphTime mSrcStreamGraphTimeOffset = 0;
+
+  // True once PlaybackEnded() is called and we're playing a MediaStream.
+  // Reset to false if we start playing mSrcStream again.
+  bool mSrcStreamPlaybackEnded = false;
 
   // Holds a reference to the stream connecting this stream to the capture sink.
   RefPtr<MediaInputPort> mCaptureStreamPort;
 
   // Holds references to the DOM wrappers for the MediaStreams that we're
   // writing to.
   nsTArray<OutputMediaStream> mOutputStreams;
 
-  // Holds a reference to the MediaStreamListener attached to mSrcStream's
-  // playback stream.
-  RefPtr<StreamListener> mMediaStreamListener;
-  // Holds a reference to the size-getting MediaStreamListener attached to
-  // mSrcStream.
-  RefPtr<StreamSizeListener> mMediaStreamSizeListener;
-  // The selected video stream track which contained mMediaStreamSizeListener.
+  // The next track id to use for a captured MediaDecoder.
+  TrackID mNextAvailableMediaDecoderOutputTrackID = 1;
+
+  // Holds a reference to the size-getting track listener attached to
+  // mSelectedVideoStreamTrack.
+  RefPtr<VideoFrameListener> mVideoFrameListener;
+  // The currently selected video stream track.
   RefPtr<VideoStreamTrack> mSelectedVideoStreamTrack;
 
   const RefPtr<ShutdownObserver> mShutdownObserver;
 
   // Holds a reference to the MediaSource, if any, referenced by the src
   // attribute on the media element.
   RefPtr<MediaSource> mSrcMediaSource;
 
@@ -1753,17 +1726,17 @@ protected:
   bool mDisableVideo = false;
 
   RefPtr<TextTrackManager> mTextTrackManager;
 
   RefPtr<AudioTrackList> mAudioTrackList;
 
   RefPtr<VideoTrackList> mVideoTrackList;
 
-  nsAutoPtr<MediaStreamTrackListener> mMediaStreamTrackListener;
+  UniquePtr<MediaStreamTrackListener> mMediaStreamTrackListener;
 
   // The principal guarding mVideoFrameContainer access when playing a
   // MediaStream.
   nsCOMPtr<nsIPrincipal> mSrcStreamVideoPrincipal;
 
   // True if UnbindFromTree() is called on the element.
   // Note this flag is false when the element is in a phase after creation and
   // before attaching to the DOM tree.
--- a/dom/media/AudioCaptureStream.cpp
+++ b/dom/media/AudioCaptureStream.cpp
@@ -25,20 +25,17 @@ using namespace mozilla::dom;
 using namespace mozilla::gfx;
 
 namespace mozilla {
 
 // We are mixing to mono until PeerConnection can accept stereo
 static const uint32_t MONO = 1;
 
 AudioCaptureStream::AudioCaptureStream(TrackID aTrackId)
-    : ProcessedMediaStream(),
-      mTrackId(aTrackId),
-      mStarted(false),
-      mTrackCreated(false) {
+    : ProcessedMediaStream(), mTrackId(aTrackId), mStarted(false) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_COUNT_CTOR(AudioCaptureStream);
   mMixer.AddCallback(this);
 }
 
 AudioCaptureStream::~AudioCaptureStream() {
   MOZ_COUNT_DTOR(AudioCaptureStream);
   mMixer.RemoveCallback(this);
@@ -61,27 +58,16 @@ void AudioCaptureStream::Start() {
 void AudioCaptureStream::ProcessInput(GraphTime aFrom, GraphTime aTo,
                                       uint32_t aFlags) {
   if (!mStarted) {
     return;
   }
 
   uint32_t inputCount = mInputs.Length();
   StreamTracks::Track* track = EnsureTrack(mTrackId);
-  // Notify the DOM everything is in order.
-  if (!mTrackCreated) {
-    for (uint32_t i = 0; i < mListeners.Length(); i++) {
-      MediaStreamListener* l = mListeners[i];
-      AudioSegment tmp;
-      l->NotifyQueuedTrackChanges(Graph(), mTrackId, 0,
-                                  TrackEventCommand::TRACK_EVENT_CREATED, tmp);
-      l->NotifyFinishedTrackCreation(Graph());
-    }
-    mTrackCreated = true;
-  }
 
   if (IsFinishedOnGraphThread()) {
     return;
   }
 
   // If the captured stream is connected back to a object on the page (be it an
   // HTMLMediaElement with a stream as source, or an AudioContext), a cycle
   // situation occur. This can work if it's an AudioContext with at least one
--- a/dom/media/CanvasCaptureMediaStream.cpp
+++ b/dom/media/CanvasCaptureMediaStream.cpp
@@ -17,74 +17,85 @@
 #include "Tracing.h"
 
 using namespace mozilla::layers;
 using namespace mozilla::gfx;
 
 namespace mozilla {
 namespace dom {
 
-class OutputStreamDriver::StreamListener : public MediaStreamListener {
+class OutputStreamDriver::TrackListener : public MediaStreamTrackListener {
  public:
-  explicit StreamListener(OutputStreamDriver* aDriver, TrackID aTrackId,
-                          PrincipalHandle aPrincipalHandle,
-                          SourceMediaStream* aSourceStream)
+  TrackListener(TrackID aTrackId, const PrincipalHandle& aPrincipalHandle,
+                SourceMediaStream* aSourceStream)
       : mEnded(false),
         mSourceStream(aSourceStream),
         mTrackId(aTrackId),
         mPrincipalHandle(aPrincipalHandle),
         mMutex("CanvasCaptureMediaStream OutputStreamDriver::StreamListener") {
     MOZ_ASSERT(mSourceStream);
   }
 
-  void EndStream() { mEnded = true; }
+  void Forget() {
+    EndTrack();
+    mSourceStream->EndTrack(mTrackId);
+
+    MutexAutoLock lock(mMutex);
+    mImage = nullptr;
+  }
+
+  void EndTrack() { mEnded = true; }
 
   void SetImage(const RefPtr<layers::Image>& aImage, const TimeStamp& aTime) {
     MutexAutoLock lock(mMutex);
     mImage = aImage;
     mImageTime = aTime;
   }
 
-  void NotifyPull(MediaStreamGraph* aGraph, StreamTime aDesiredTime) override {
+  void NotifyPull(MediaStreamGraph* aGraph, StreamTime aEndOfAppendedData,
+                  StreamTime aDesiredTime) override {
     // Called on the MediaStreamGraph thread.
     TRACE_AUDIO_CALLBACK_COMMENT("SourceMediaStream %p track %i",
                                  mSourceStream.get(), mTrackId);
     MOZ_ASSERT(mSourceStream);
-    StreamTime delta =
-        aDesiredTime - mSourceStream->GetEndOfAppendedData(mTrackId);
-    if (delta > 0) {
-      MutexAutoLock lock(mMutex);
+    StreamTime delta = aDesiredTime - aEndOfAppendedData;
+    MOZ_ASSERT(delta > 0);
+
+    MutexAutoLock lock(mMutex);
 
-      RefPtr<Image> image = mImage;
-      IntSize size = image ? image->GetSize() : IntSize(0, 0);
-      VideoSegment segment;
-      segment.AppendFrame(image.forget(), delta, size, mPrincipalHandle, false,
-                          mImageTime);
+    RefPtr<Image> image = mImage;
+    IntSize size = image ? image->GetSize() : IntSize(0, 0);
+    VideoSegment segment;
+    segment.AppendFrame(image.forget(), delta, size, mPrincipalHandle, false,
+                        mImageTime);
 
-      mSourceStream->AppendToTrack(mTrackId, &segment);
-    }
+    mSourceStream->AppendToTrack(mTrackId, &segment);
 
     if (mEnded) {
-      mSourceStream->EndAllTrackAndFinish();
+      mSourceStream->EndTrack(mTrackId);
     }
   }
 
-  void NotifyEvent(MediaStreamGraph* aGraph,
-                   MediaStreamGraphEvent aEvent) override {
-    if (aEvent == MediaStreamGraphEvent::EVENT_REMOVED) {
-      EndStream();
-      mSourceStream->EndAllTrackAndFinish();
+  void NotifyEnded() override {
+    Forget();
 
-      MutexAutoLock lock(mMutex);
-      mImage = nullptr;
-    }
+    mSourceStream->Graph()->DispatchToMainThreadAfterStreamStateUpdate(
+        NS_NewRunnableFunction(
+            "OutputStreamDriver::TrackListener::RemoveTrackListener",
+            [self = RefPtr<TrackListener>(this), this]() {
+              if (!mSourceStream->IsDestroyed()) {
+                mSourceStream->RemoveTrackListener(this, mTrackId);
+              }
+            }));
   }
 
+  void NotifyRemoved() override { Forget(); }
+
  protected:
-  ~StreamListener() {}
+  ~TrackListener() = default;
 
  private:
   Atomic<bool> mEnded;
   const RefPtr<SourceMediaStream> mSourceStream;
   const TrackID mTrackId;
   const PrincipalHandle mPrincipalHandle;
 
   Mutex mMutex;
@@ -93,43 +104,41 @@ class OutputStreamDriver::StreamListener
   TimeStamp mImageTime;
 };
 
 OutputStreamDriver::OutputStreamDriver(SourceMediaStream* aSourceStream,
                                        const TrackID& aTrackId,
                                        const PrincipalHandle& aPrincipalHandle)
     : FrameCaptureListener(),
       mSourceStream(aSourceStream),
-      mStreamListener(
-          new StreamListener(this, aTrackId, aPrincipalHandle, aSourceStream)) {
+      mTrackListener(
+          new TrackListener(aTrackId, aPrincipalHandle, aSourceStream)) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(mSourceStream);
-  mSourceStream->AddListener(mStreamListener);
-  mSourceStream->AddTrack(aTrackId, 0, new VideoSegment());
+  mSourceStream->AddTrack(aTrackId, new VideoSegment());
+  mSourceStream->AddTrackListener(mTrackListener, aTrackId);
   mSourceStream->AdvanceKnownTracksTime(STREAM_TIME_MAX);
   mSourceStream->SetPullEnabled(true);
 
   // All CanvasCaptureMediaStreams shall at least get one frame.
   mFrameCaptureRequested = true;
 }
 
 OutputStreamDriver::~OutputStreamDriver() {
   MOZ_ASSERT(NS_IsMainThread());
-  if (mStreamListener) {
-    // MediaStreamGraph will keep the listener alive until it can finish the
-    // stream on the next NotifyPull().
-    mStreamListener->EndStream();
-  }
+  // MediaStreamGraph will keep the listener alive until it can end the track in
+  // the graph on the next NotifyPull().
+  mTrackListener->EndTrack();
 }
 
+void OutputStreamDriver::EndTrack() { mTrackListener->EndTrack(); }
+
 void OutputStreamDriver::SetImage(const RefPtr<layers::Image>& aImage,
                                   const TimeStamp& aTime) {
-  if (mStreamListener) {
-    mStreamListener->SetImage(aImage, aTime);
-  }
+  mTrackListener->SetImage(aImage, aTime);
 }
 
 // ----------------------------------------------------------------------
 
 class TimerDriver : public OutputStreamDriver {
  public:
   explicit TimerDriver(SourceMediaStream* aSourceStream, const double& aFPS,
                        const TrackID& aTrackId,
@@ -211,19 +220,17 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED(Canva
 NS_IMPL_ADDREF_INHERITED(CanvasCaptureMediaStream, DOMMediaStream)
 NS_IMPL_RELEASE_INHERITED(CanvasCaptureMediaStream, DOMMediaStream)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CanvasCaptureMediaStream)
 NS_INTERFACE_MAP_END_INHERITING(DOMMediaStream)
 
 CanvasCaptureMediaStream::CanvasCaptureMediaStream(nsPIDOMWindowInner* aWindow,
                                                    HTMLCanvasElement* aCanvas)
-    : DOMMediaStream(aWindow, nullptr),
-      mCanvas(aCanvas),
-      mOutputStreamDriver(nullptr) {}
+    : DOMMediaStream(aWindow), mCanvas(aCanvas), mOutputStreamDriver(nullptr) {}
 
 CanvasCaptureMediaStream::~CanvasCaptureMediaStream() {
   if (mOutputStreamDriver) {
     mOutputStreamDriver->Forget();
   }
 }
 
 JSObject* CanvasCaptureMediaStream::WrapObject(
@@ -272,14 +279,15 @@ FrameCaptureListener* CanvasCaptureMedia
   return mOutputStreamDriver;
 }
 
 void CanvasCaptureMediaStream::StopCapture() {
   if (!mOutputStreamDriver) {
     return;
   }
 
+  mOutputStreamDriver->EndTrack();
   mOutputStreamDriver->Forget();
   mOutputStreamDriver = nullptr;
 }
 
 }  // namespace dom
 }  // namespace mozilla
--- a/dom/media/CanvasCaptureMediaStream.h
+++ b/dom/media/CanvasCaptureMediaStream.h
@@ -41,21 +41,21 @@ class OutputStreamFrameListener;
  *                                       ________________________
  *  ________   FrameCaptureRequested?   |                        |
  * |        | ------------------------> |   OutputStreamDriver   |
  * | Canvas |  SetFrameCapture()        | (FrameCaptureListener) |
  * |________| ------------------------> |________________________|
  *                                                  |
  *                                                  | SetImage()
  *                                                  v
- *                                         ___________________
- *                                        |   StreamListener  |
- * ---------------------------------------| (All image access |----------------
- *     === MediaStreamGraph Thread ===    |   Mutex Guarded)  |
- *                                        |___________________|
+ *                                         ____________________
+ *                                        |Stream/TrackListener|
+ * ---------------------------------------| (All image access  |---------------
+ *     === MediaStreamGraph Thread ===    |   Mutex Guarded)   |
+ *                                        |____________________|
  *                                              ^       |
  *                                 NotifyPull() |       | AppendToTrack()
  *                                              |       v
  *                                      ___________________________
  *                                     |                           |
  *                                     |  MSG / SourceMediaStream  |
  *                                     |___________________________|
  * ----------------------------------------------------------------------------
@@ -75,28 +75,35 @@ class OutputStreamDriver : public FrameC
 
   /*
    * Sub classes can SetImage() to update the image being appended to the
    * output stream. It will be appended on the next NotifyPull from MSG.
    */
   void SetImage(const RefPtr<layers::Image>& aImage, const TimeStamp& aTime);
 
   /*
+   * Ends the track in mSourceStream when we know there won't be any more images
+   * requested for it.
+   */
+  void EndTrack();
+
+  /*
    * Makes sure any internal resources this driver is holding that may create
    * reference cycles are released.
    */
   virtual void Forget() {}
 
  protected:
   virtual ~OutputStreamDriver();
   class StreamListener;
+  class TrackListener;
 
  private:
-  RefPtr<SourceMediaStream> mSourceStream;
-  RefPtr<StreamListener> mStreamListener;
+  const RefPtr<SourceMediaStream> mSourceStream;
+  const RefPtr<TrackListener> mTrackListener;
 };
 
 class CanvasCaptureMediaStream : public DOMMediaStream {
  public:
   CanvasCaptureMediaStream(nsPIDOMWindowInner* aWindow,
                            HTMLCanvasElement* aCanvas);
 
   NS_DECL_ISUPPORTS_INHERITED
--- a/dom/media/DOMMediaStream.cpp
+++ b/dom/media/DOMMediaStream.cpp
@@ -99,195 +99,16 @@ already_AddRefed<Pledge<bool>> DOMMediaS
   rejected->Reject(NS_ERROR_FAILURE);
   return rejected.forget();
 }
 
 NS_IMPL_CYCLE_COLLECTION(DOMMediaStream::TrackPort, mTrack)
 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(DOMMediaStream::TrackPort, AddRef)
 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(DOMMediaStream::TrackPort, Release)
 
-NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaStreamTrackSourceGetter)
-NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaStreamTrackSourceGetter)
-NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaStreamTrackSourceGetter)
-  NS_INTERFACE_MAP_ENTRY(nsISupports)
-NS_INTERFACE_MAP_END
-NS_IMPL_CYCLE_COLLECTION_0(MediaStreamTrackSourceGetter)
-
-/**
- * Listener registered on the Owned stream to detect added and ended owned
- * tracks for keeping the list of MediaStreamTracks in sync with the tracks
- * added and ended directly at the source.
- */
-class DOMMediaStream::OwnedStreamListener : public MediaStreamListener {
- public:
-  explicit OwnedStreamListener(DOMMediaStream* aStream) : mStream(aStream) {}
-
-  void Forget() { mStream = nullptr; }
-
-  void DoNotifyTrackCreated(MediaStreamGraph* aGraph, TrackID aTrackID,
-                            MediaSegment::Type aType, MediaStream* aInputStream,
-                            TrackID aInputTrackID) {
-    MOZ_ASSERT(NS_IsMainThread());
-
-    if (!mStream) {
-      return;
-    }
-
-    MediaStreamTrack* track =
-        mStream->FindOwnedDOMTrack(aInputStream, aInputTrackID, aTrackID);
-
-    if (track) {
-      LOG(LogLevel::Debug, ("DOMMediaStream %p Track %d from owned stream %p "
-                            "bound to MediaStreamTrack %p.",
-                            mStream, aTrackID, aInputStream, track));
-      return;
-    }
-
-    // Track had not been created on main thread before, create it now.
-    NS_WARNING_ASSERTION(
-        !mStream->mTracks.IsEmpty(),
-        "A new track was detected on the input stream; creating a "
-        "corresponding "
-        "MediaStreamTrack. Initial tracks should be added manually to "
-        "immediately and synchronously be available to JS.");
-    RefPtr<MediaStreamTrackSource> source;
-    if (mStream->mTrackSourceGetter) {
-      source = mStream->mTrackSourceGetter->GetMediaStreamTrackSource(aTrackID);
-    }
-    if (!source) {
-      NS_ASSERTION(false,
-                   "Dynamic track created without an explicit TrackSource");
-      nsPIDOMWindowInner* window = mStream->GetParentObject();
-      nsIDocument* doc = window ? window->GetExtantDoc() : nullptr;
-      nsIPrincipal* principal = doc ? doc->NodePrincipal() : nullptr;
-      source = new BasicTrackSource(principal);
-    }
-
-    RefPtr<MediaStreamTrack> newTrack =
-        mStream->CreateDOMTrack(aTrackID, aType, source);
-    aGraph->AbstractMainThread()->Dispatch(
-        NewRunnableMethod<RefPtr<MediaStreamTrack>>(
-            "DOMMediaStream::AddTrackInternal", mStream,
-            &DOMMediaStream::AddTrackInternal, newTrack));
-  }
-
-  void DoNotifyTrackEnded(MediaStreamGraph* aGraph, MediaStream* aInputStream,
-                          TrackID aInputTrackID, TrackID aTrackID) {
-    MOZ_ASSERT(NS_IsMainThread());
-
-    if (!mStream) {
-      return;
-    }
-
-    RefPtr<MediaStreamTrack> track =
-        mStream->FindOwnedDOMTrack(aInputStream, aInputTrackID, aTrackID);
-    NS_ASSERTION(track,
-                 "Owned MediaStreamTracks must be known by the DOMMediaStream");
-    if (track) {
-      LOG(LogLevel::Debug, ("DOMMediaStream %p MediaStreamTrack %p ended at "
-                            "the source. Marking it ended.",
-                            mStream, track.get()));
-      aGraph->AbstractMainThread()->Dispatch(
-          NewRunnableMethod("dom::MediaStreamTrack::OverrideEnded", track,
-                            &MediaStreamTrack::OverrideEnded));
-    }
-  }
-
-  void NotifyQueuedTrackChanges(MediaStreamGraph* aGraph, TrackID aID,
-                                StreamTime aTrackOffset,
-                                TrackEventCommand aTrackEvents,
-                                const MediaSegment& aQueuedMedia,
-                                MediaStream* aInputStream,
-                                TrackID aInputTrackID) override {
-    if (aTrackEvents & TrackEventCommand::TRACK_EVENT_CREATED) {
-      aGraph->DispatchToMainThreadAfterStreamStateUpdate(
-          NewRunnableMethod<MediaStreamGraph*, TrackID, MediaSegment::Type,
-                            RefPtr<MediaStream>, TrackID>(
-              "DOMMediaStream::OwnedStreamListener::DoNotifyTrackCreated", this,
-              &OwnedStreamListener::DoNotifyTrackCreated, aGraph, aID,
-              aQueuedMedia.GetType(), aInputStream, aInputTrackID));
-    } else if (aTrackEvents & TrackEventCommand::TRACK_EVENT_ENDED) {
-      aGraph->DispatchToMainThreadAfterStreamStateUpdate(
-          NewRunnableMethod<MediaStreamGraph*, RefPtr<MediaStream>, TrackID,
-                            TrackID>(
-              "DOMMediaStream::OwnedStreamListener::DoNotifyTrackEnded", this,
-              &OwnedStreamListener::DoNotifyTrackEnded, aGraph, aInputStream,
-              aInputTrackID, aID));
-    }
-  }
-
- private:
-  // These fields may only be accessed on the main thread
-  DOMMediaStream* mStream;
-};
-
-/**
- * Listener registered on the Playback stream to detect when tracks end and when
- * all new tracks this iteration have been created - for when several tracks are
- * queued by the source and committed all at once.
- */
-class DOMMediaStream::PlaybackStreamListener : public MediaStreamListener {
- public:
-  explicit PlaybackStreamListener(DOMMediaStream* aStream) : mStream(aStream) {}
-
-  void Forget() {
-    MOZ_ASSERT(NS_IsMainThread());
-    mStream = nullptr;
-  }
-
-  void DoNotifyFinishedTrackCreation() {
-    MOZ_ASSERT(NS_IsMainThread());
-
-    if (!mStream) {
-      return;
-    }
-
-    // The owned stream listener adds its tracks after another main thread
-    // dispatch. We have to do the same to notify of created tracks to stay
-    // in sync. (Or NotifyTracksCreated is called before tracks are added).
-    MOZ_ASSERT(mStream->GetPlaybackStream());
-    mStream->GetPlaybackStream()->Graph()->AbstractMainThread()->Dispatch(
-        NewRunnableMethod("DOMMediaStream::NotifyTracksCreated", mStream,
-                          &DOMMediaStream::NotifyTracksCreated));
-  }
-
-  void DoNotifyFinished() {
-    MOZ_ASSERT(NS_IsMainThread());
-
-    if (!mStream) {
-      return;
-    }
-
-    mStream->GetPlaybackStream()->Graph()->AbstractMainThread()->Dispatch(
-        NewRunnableMethod("DOMMediaStream::NotifyFinished", mStream,
-                          &DOMMediaStream::NotifyFinished));
-  }
-
-  // The methods below are called on the MediaStreamGraph thread.
-
-  void NotifyFinishedTrackCreation(MediaStreamGraph* aGraph) override {
-    aGraph->DispatchToMainThreadAfterStreamStateUpdate(NewRunnableMethod(
-        "DOMMediaStream::PlaybackStreamListener::DoNotifyFinishedTrackCreation",
-        this, &PlaybackStreamListener::DoNotifyFinishedTrackCreation));
-  }
-
-  void NotifyEvent(MediaStreamGraph* aGraph,
-                   MediaStreamGraphEvent event) override {
-    if (event == MediaStreamGraphEvent::EVENT_FINISHED) {
-      aGraph->DispatchToMainThreadAfterStreamStateUpdate(NewRunnableMethod(
-          "DOMMediaStream::PlaybackStreamListener::DoNotifyFinished", this,
-          &PlaybackStreamListener::DoNotifyFinished));
-    }
-  }
-
- private:
-  // These fields may only be accessed on the main thread
-  DOMMediaStream* mStream;
-};
-
 class DOMMediaStream::PlaybackTrackListener : public MediaStreamTrackConsumer {
  public:
   explicit PlaybackTrackListener(DOMMediaStream* aStream) : mStream(aStream) {}
 
   NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PlaybackTrackListener)
   NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(PlaybackTrackListener)
 
   void NotifyEnded(MediaStreamTrack* aTrack) override {
@@ -321,29 +142,27 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(DOMMediaS
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DOMMediaStream,
                                                 DOMEventTargetHelper)
   tmp->Destroy();
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwnedTracks)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mTracks)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsumersToKeepAlive)
-  NS_IMPL_CYCLE_COLLECTION_UNLINK(mTrackSourceGetter)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPlaybackTrackListener)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPrincipal)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mVideoPrincipal)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DOMMediaStream,
                                                   DOMEventTargetHelper)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwnedTracks)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTracks)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConsumersToKeepAlive)
-  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTrackSourceGetter)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPlaybackTrackListener)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPrincipal)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVideoPrincipal)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_ADDREF_INHERITED(DOMMediaStream, DOMEventTargetHelper)
 NS_IMPL_RELEASE_INHERITED(DOMMediaStream, DOMEventTargetHelper)
 
@@ -355,29 +174,27 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED(DOMAu
                                    mStreamNode)
 
 NS_IMPL_ADDREF_INHERITED(DOMAudioNodeMediaStream, DOMMediaStream)
 NS_IMPL_RELEASE_INHERITED(DOMAudioNodeMediaStream, DOMMediaStream)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMAudioNodeMediaStream)
 NS_INTERFACE_MAP_END_INHERITING(DOMMediaStream)
 
-DOMMediaStream::DOMMediaStream(nsPIDOMWindowInner* aWindow,
-                               MediaStreamTrackSourceGetter* aTrackSourceGetter)
+DOMMediaStream::DOMMediaStream(nsPIDOMWindowInner* aWindow)
     : mWindow(aWindow),
       mInputStream(nullptr),
       mOwnedStream(nullptr),
       mPlaybackStream(nullptr),
       mTracksPendingRemoval(0),
-      mTrackSourceGetter(aTrackSourceGetter),
       mPlaybackTrackListener(MakeAndAddRef<PlaybackTrackListener>(this)),
       mTracksCreated(false),
       mNotifiedOfMediaStreamGraphShutdown(false),
       mActive(false),
-      mSetInactiveOnFinish(false),
+      mFinishedOnInactive(true),
       mCORSMode(CORS_NONE) {
   nsresult rv;
   nsCOMPtr<nsIUUIDGenerator> uuidgen =
       do_GetService("@mozilla.org/uuid-generator;1", &rv);
 
   if (NS_SUCCEEDED(rv) && uuidgen) {
     nsID uuid;
     memset(&uuid, 0, sizeof(uuid));
@@ -389,30 +206,16 @@ DOMMediaStream::DOMMediaStream(nsPIDOMWi
     }
   }
 }
 
 DOMMediaStream::~DOMMediaStream() { Destroy(); }
 
 void DOMMediaStream::Destroy() {
   LOG(LogLevel::Debug, ("DOMMediaStream %p Being destroyed.", this));
-  if (mOwnedListener) {
-    if (mOwnedStream) {
-      mOwnedStream->RemoveListener(mOwnedListener);
-    }
-    mOwnedListener->Forget();
-    mOwnedListener = nullptr;
-  }
-  if (mPlaybackListener) {
-    if (mPlaybackStream) {
-      mPlaybackStream->RemoveListener(mPlaybackListener);
-    }
-    mPlaybackListener->Forget();
-    mPlaybackListener = nullptr;
-  }
   for (const RefPtr<TrackPort>& info : mTracks) {
     // We must remove ourselves from each track's principal change observer list
     // before we die. CC may have cleared info->mTrack so guard against it.
     MediaStreamTrack* track = info->GetTrack();
     if (track) {
       track->RemovePrincipalChangeObserver(this);
       if (!track->Ended()) {
         track->RemoveConsumer(mPlaybackTrackListener);
@@ -434,17 +237,16 @@ void DOMMediaStream::Destroy() {
   if (mOwnedStream) {
     mOwnedStream->UnregisterUser();
     mOwnedStream = nullptr;
   }
   if (mInputStream) {
     mInputStream->UnregisterUser();
     mInputStream = nullptr;
   }
-  mRunOnTracksAvailable.Clear();
   mTrackListeners.Clear();
 }
 
 JSObject* DOMMediaStream::WrapObject(JSContext* aCx,
                                      JS::Handle<JSObject*> aGivenProto) {
   return dom::MediaStream_Binding::Wrap(aCx, this, aGivenProto);
 }
 
@@ -480,19 +282,17 @@ JSObject* DOMMediaStream::WrapObject(JSC
     ErrorResult& aRv) {
   nsCOMPtr<nsPIDOMWindowInner> ownerWindow =
       do_QueryInterface(aGlobal.GetAsSupports());
   if (!ownerWindow) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
-  // Streams created from JS cannot have dynamically created tracks.
-  MediaStreamTrackSourceGetter* getter = nullptr;
-  RefPtr<DOMMediaStream> newStream = new DOMMediaStream(ownerWindow, getter);
+  auto newStream = MakeRefPtr<DOMMediaStream>(ownerWindow);
 
   for (MediaStreamTrack& track : aTracks) {
     if (!newStream->GetPlaybackStream()) {
       MOZ_RELEASE_ASSERT(track.Graph());
       newStream->InitPlaybackStreamCommon(track.Graph());
     }
     newStream->AddTrack(track);
   }
@@ -687,61 +487,21 @@ void DOMMediaStream::RemoveTrack(MediaSt
   if (!aTrack.Ended()) {
     BlockPlaybackTrack(toRemove);
     NotifyTrackRemoved(&aTrack);
   }
 
   LOG(LogLevel::Debug, ("DOMMediaStream %p Removed track %p", this, &aTrack));
 }
 
-class ClonedStreamSourceGetter : public MediaStreamTrackSourceGetter {
- public:
-  NS_DECL_ISUPPORTS_INHERITED
-  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ClonedStreamSourceGetter,
-                                           MediaStreamTrackSourceGetter)
-
-  explicit ClonedStreamSourceGetter(DOMMediaStream* aStream)
-      : mStream(aStream) {}
-
-  already_AddRefed<MediaStreamTrackSource> GetMediaStreamTrackSource(
-      TrackID aInputTrackID) override {
-    MediaStreamTrack* sourceTrack =
-        mStream->FindOwnedDOMTrack(mStream->GetOwnedStream(), aInputTrackID);
-    MOZ_RELEASE_ASSERT(sourceTrack);
-
-    return do_AddRef(&sourceTrack->GetSource());
-  }
-
- protected:
-  virtual ~ClonedStreamSourceGetter() {}
-
-  RefPtr<DOMMediaStream> mStream;
-};
-
-NS_IMPL_ADDREF_INHERITED(ClonedStreamSourceGetter, MediaStreamTrackSourceGetter)
-NS_IMPL_RELEASE_INHERITED(ClonedStreamSourceGetter,
-                          MediaStreamTrackSourceGetter)
-NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ClonedStreamSourceGetter)
-NS_INTERFACE_MAP_END_INHERITING(MediaStreamTrackSourceGetter)
-NS_IMPL_CYCLE_COLLECTION_INHERITED(ClonedStreamSourceGetter,
-                                   MediaStreamTrackSourceGetter, mStream)
-
 already_AddRefed<DOMMediaStream> DOMMediaStream::Clone() {
-  return CloneInternal(TrackForwardingOption::CURRENT);
-}
-
-already_AddRefed<DOMMediaStream> DOMMediaStream::CloneInternal(
-    TrackForwardingOption aForwarding) {
-  RefPtr<DOMMediaStream> newStream =
-      new DOMMediaStream(GetParentObject(), new ClonedStreamSourceGetter(this));
+  auto newStream = MakeRefPtr<DOMMediaStream>(GetParentObject());
 
   LOG(LogLevel::Info,
-      ("DOMMediaStream %p created clone %p, forwarding %s tracks", this,
-       newStream.get(),
-       aForwarding == TrackForwardingOption::ALL ? "all" : "current"));
+      ("DOMMediaStream %p created clone %p", this, newStream.get()));
 
   MOZ_RELEASE_ASSERT(mPlaybackStream);
   MOZ_RELEASE_ASSERT(mPlaybackStream->Graph());
   MediaStreamGraph* graph = mPlaybackStream->Graph();
 
   // We initiate the owned and playback streams first, since we need to create
   // all existing DOM tracks before we add the generic input port from
   // mInputStream to mOwnedStream (see AllocateInputPort wrt. destination
@@ -756,37 +516,16 @@ already_AddRefed<DOMMediaStream> DOMMedi
 
     LOG(LogLevel::Debug,
         ("DOMMediaStream %p forwarding external track %p to clone %p", this,
          &track, newStream.get()));
     RefPtr<MediaStreamTrack> trackClone =
         newStream->CloneDOMTrack(track, allocatedTrackID++);
   }
 
-  if (aForwarding == TrackForwardingOption::ALL) {
-    // Set up an input port from our input stream to the new DOM stream's owned
-    // stream, to allow for dynamically added tracks at the source to appear in
-    // the clone. The clone may treat mInputStream as its own mInputStream but
-    // ownership remains with us.
-    newStream->mInputStream = mInputStream;
-    if (mInputStream) {
-      // We have already set up track-locked input ports for all existing DOM
-      // tracks, so now we need to block those in the generic input port to
-      // avoid ending up with double instances of them.
-      nsTArray<TrackID> tracksToBlock;
-      for (const RefPtr<TrackPort>& info : mOwnedTracks) {
-        tracksToBlock.AppendElement(info->GetTrack()->mTrackID);
-      }
-
-      newStream->mInputStream->RegisterUser();
-      newStream->mOwnedPort = newStream->mOwnedStream->AllocateInputPort(
-          mInputStream, TRACK_ANY, TRACK_ANY, 0, 0, &tracksToBlock);
-    }
-  }
-
   return newStream.forget();
 }
 
 bool DOMMediaStream::Active() const { return mActive; }
 
 MediaStreamTrack* DOMMediaStream::GetTrackById(const nsAString& aId) const {
   for (const RefPtr<TrackPort>& info : mTracks) {
     nsString id;
@@ -831,18 +570,16 @@ TrackRate DOMMediaStream::GraphRate() {
   if (mInputStream) {
     return mInputStream->GraphRate();
   }
 
   MOZ_ASSERT(false, "Not hooked up to a graph");
   return 0;
 }
 
-void DOMMediaStream::SetInactiveOnFinish() { mSetInactiveOnFinish = true; }
-
 void DOMMediaStream::InitSourceStream(MediaStreamGraph* aGraph) {
   InitInputStreamCommon(aGraph->CreateSourceStream(), aGraph);
   InitOwnedStreamCommon(aGraph);
   InitPlaybackStreamCommon(aGraph);
 }
 
 void DOMMediaStream::InitTrackUnionStream(MediaStreamGraph* aGraph) {
   InitInputStreamCommon(aGraph->CreateTrackUnionStream(), aGraph);
@@ -883,63 +620,50 @@ void DOMMediaStream::InitOwnedStreamComm
              "Owned stream must be initialized before playback stream");
 
   mOwnedStream = aGraph->CreateTrackUnionStream();
   mOwnedStream->QueueSetAutofinish(true);
   mOwnedStream->RegisterUser();
   if (mInputStream) {
     mOwnedPort = mOwnedStream->AllocateInputPort(mInputStream);
   }
-
-  // Setup track listeners
-  mOwnedListener = new OwnedStreamListener(this);
-  mOwnedStream->AddListener(mOwnedListener);
 }
 
 void DOMMediaStream::InitPlaybackStreamCommon(MediaStreamGraph* aGraph) {
   mPlaybackStream = aGraph->CreateTrackUnionStream();
   mPlaybackStream->QueueSetAutofinish(true);
   mPlaybackStream->RegisterUser();
   if (mOwnedStream) {
     mPlaybackPort = mPlaybackStream->AllocateInputPort(mOwnedStream);
   }
 
-  mPlaybackListener = new PlaybackStreamListener(this);
-  mPlaybackStream->AddListener(mPlaybackListener);
-
   LOG(LogLevel::Debug, ("DOMMediaStream %p Initiated with mInputStream=%p, "
                         "mOwnedStream=%p, mPlaybackStream=%p",
                         this, mInputStream, mOwnedStream, mPlaybackStream));
 }
 
 already_AddRefed<DOMMediaStream> DOMMediaStream::CreateSourceStreamAsInput(
-    nsPIDOMWindowInner* aWindow, MediaStreamGraph* aGraph,
-    MediaStreamTrackSourceGetter* aTrackSourceGetter) {
-  RefPtr<DOMMediaStream> stream =
-      new DOMMediaStream(aWindow, aTrackSourceGetter);
+    nsPIDOMWindowInner* aWindow, MediaStreamGraph* aGraph) {
+  auto stream = MakeRefPtr<DOMMediaStream>(aWindow);
   stream->InitSourceStream(aGraph);
   return stream.forget();
 }
 
 already_AddRefed<DOMMediaStream> DOMMediaStream::CreateTrackUnionStreamAsInput(
-    nsPIDOMWindowInner* aWindow, MediaStreamGraph* aGraph,
-    MediaStreamTrackSourceGetter* aTrackSourceGetter) {
-  RefPtr<DOMMediaStream> stream =
-      new DOMMediaStream(aWindow, aTrackSourceGetter);
+    nsPIDOMWindowInner* aWindow, MediaStreamGraph* aGraph) {
+  auto stream = MakeRefPtr<DOMMediaStream>(aWindow);
   stream->InitTrackUnionStream(aGraph);
   return stream.forget();
 }
 
 already_AddRefed<DOMMediaStream>
 DOMMediaStream::CreateAudioCaptureStreamAsInput(nsPIDOMWindowInner* aWindow,
                                                 nsIPrincipal* aPrincipal,
                                                 MediaStreamGraph* aGraph) {
-  // Audio capture doesn't create tracks dynamically
-  MediaStreamTrackSourceGetter* getter = nullptr;
-  RefPtr<DOMMediaStream> stream = new DOMMediaStream(aWindow, getter);
+  auto stream = MakeRefPtr<DOMMediaStream>(aWindow);
   stream->InitAudioCaptureStream(aPrincipal, aGraph);
   return stream.forget();
 }
 
 void DOMMediaStream::PrincipalChanged(MediaStreamTrack* aTrack) {
   MOZ_ASSERT(aTrack);
   NS_ASSERTION(HasTrack(*aTrack), "Principal changed for an unknown track");
   LOG(LogLevel::Info,
@@ -1170,46 +894,16 @@ MediaStreamTrack* DOMMediaStream::FindPl
   return nullptr;
 }
 
 DOMMediaStream::TrackPort* DOMMediaStream::FindPlaybackTrackPort(
     const MediaStreamTrack& aTrack) const {
   return FindTrackPortAmongTracks(aTrack, mTracks);
 }
 
-void DOMMediaStream::OnTracksAvailable(OnTracksAvailableCallback* aRunnable) {
-  if (mNotifiedOfMediaStreamGraphShutdown) {
-    // No more tracks will ever be added, so just delete the callback now.
-    delete aRunnable;
-    return;
-  }
-  mRunOnTracksAvailable.AppendElement(aRunnable);
-  CheckTracksAvailable();
-}
-
-void DOMMediaStream::NotifyTracksCreated() {
-  mTracksCreated = true;
-  CheckTracksAvailable();
-}
-
-void DOMMediaStream::NotifyFinished() {
-  if (!mSetInactiveOnFinish) {
-    return;
-  }
-
-  if (!mActive) {
-    // This can happen if the stream never became active.
-    return;
-  }
-
-  MOZ_ASSERT(!ContainsLiveTracks(mTracks));
-  mActive = false;
-  NotifyInactive();
-}
-
 void DOMMediaStream::NotifyActive() {
   LOG(LogLevel::Info, ("DOMMediaStream %p NotifyActive(). ", this));
 
   MOZ_ASSERT(mActive);
   for (int32_t i = mTrackListeners.Length() - 1; i >= 0; --i) {
     mTrackListeners[i]->NotifyActive();
   }
 }
@@ -1218,43 +912,45 @@ void DOMMediaStream::NotifyInactive() {
   LOG(LogLevel::Info, ("DOMMediaStream %p NotifyInactive(). ", this));
 
   MOZ_ASSERT(!mActive);
   for (int32_t i = mTrackListeners.Length() - 1; i >= 0; --i) {
     mTrackListeners[i]->NotifyInactive();
   }
 }
 
-void DOMMediaStream::CheckTracksAvailable() {
-  if (!mTracksCreated) {
-    return;
-  }
-  nsTArray<nsAutoPtr<OnTracksAvailableCallback>> callbacks;
-  callbacks.SwapElements(mRunOnTracksAvailable);
-
-  for (uint32_t i = 0; i < callbacks.Length(); ++i) {
-    callbacks[i]->NotifyTracksAvailable(this);
-  }
-}
-
 void DOMMediaStream::RegisterTrackListener(TrackListener* aListener) {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (mNotifiedOfMediaStreamGraphShutdown) {
     // No more tracks will ever be added, so just do nothing.
     return;
   }
   mTrackListeners.AppendElement(aListener);
 }
 
 void DOMMediaStream::UnregisterTrackListener(TrackListener* aListener) {
   MOZ_ASSERT(NS_IsMainThread());
   mTrackListeners.RemoveElement(aListener);
 }
 
+void DOMMediaStream::SetFinishedOnInactive(bool aFinishedOnInactive) {
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (mFinishedOnInactive == aFinishedOnInactive) {
+    return;
+  }
+
+  mFinishedOnInactive = aFinishedOnInactive;
+
+  if (mFinishedOnInactive && !ContainsLiveTracks(mTracks)) {
+    NotifyTrackRemoved(nullptr);
+  }
+}
+
 void DOMMediaStream::NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (mTracksPendingRemoval > 0) {
     // If there are tracks pending removal we may not degrade the current
     // principals until those tracks have been confirmed removed from the
     // playback stream. Instead combine with the new track and the (potentially)
     // degraded principal will be calculated when it's safe.
@@ -1292,35 +988,41 @@ void DOMMediaStream::NotifyTrackAdded(co
     NotifyActive();
   }
 }
 
 void DOMMediaStream::NotifyTrackRemoved(
     const RefPtr<MediaStreamTrack>& aTrack) {
   MOZ_ASSERT(NS_IsMainThread());
 
-  aTrack->RemoveConsumer(mPlaybackTrackListener);
-  aTrack->RemovePrincipalChangeObserver(this);
+  if (aTrack) {
+    // aTrack may be null to allow HTMLMediaElement::MozCaptureStream streams
+    // to be played until the source media element has ended. The source media
+    // element will then call NotifyTrackRemoved(nullptr) to signal that we can
+    // go inactive, regardless of the timing of the last track ending.
+
+    aTrack->RemoveConsumer(mPlaybackTrackListener);
+    aTrack->RemovePrincipalChangeObserver(this);
 
-  for (int32_t i = mTrackListeners.Length() - 1; i >= 0; --i) {
-    mTrackListeners[i]->NotifyTrackRemoved(aTrack);
+    for (int32_t i = mTrackListeners.Length() - 1; i >= 0; --i) {
+      mTrackListeners[i]->NotifyTrackRemoved(aTrack);
+    }
+
+    // Don't call RecomputePrincipal here as the track may still exist in the
+    // playback stream in the MediaStreamGraph. It will instead be called when
+    // the track has been confirmed removed by the graph. See
+    // BlockPlaybackTrack().
+
+    if (!mActive) {
+      NS_ASSERTION(false, "Shouldn't remove a live track if already inactive");
+      return;
+    }
   }
 
-  // Don't call RecomputePrincipal here as the track may still exist in the
-  // playback stream in the MediaStreamGraph. It will instead be called when the
-  // track has been confirmed removed by the graph. See BlockPlaybackTrack().
-
-  if (!mActive) {
-    NS_ASSERTION(false, "Shouldn't remove a live track if already inactive");
-    return;
-  }
-
-  if (mSetInactiveOnFinish) {
-    // For compatibility with mozCaptureStream we in some cases do not go
-    // inactive until the playback stream finishes.
+  if (!mFinishedOnInactive) {
     return;
   }
 
   // Check if we became inactive.
   if (!ContainsLiveTracks(mTracks)) {
     mActive = false;
     NotifyInactive();
   }
@@ -1360,17 +1062,17 @@ void DOMMediaStream::NotifyPlaybackTrack
                           "finish. Recomputing principal.",
                           this));
     RecomputePrincipal();
   }
 }
 
 DOMAudioNodeMediaStream::DOMAudioNodeMediaStream(nsPIDOMWindowInner* aWindow,
                                                  AudioNode* aNode)
-    : DOMMediaStream(aWindow, nullptr), mStreamNode(aNode) {}
+    : DOMMediaStream(aWindow), mStreamNode(aNode) {}
 
 DOMAudioNodeMediaStream::~DOMAudioNodeMediaStream() {}
 
 already_AddRefed<DOMAudioNodeMediaStream>
 DOMAudioNodeMediaStream::CreateTrackUnionStreamAsInput(
     nsPIDOMWindowInner* aWindow, AudioNode* aNode, MediaStreamGraph* aGraph) {
   RefPtr<DOMAudioNodeMediaStream> stream =
       new DOMAudioNodeMediaStream(aWindow, aNode);
--- a/dom/media/DOMMediaStream.h
+++ b/dom/media/DOMMediaStream.h
@@ -56,44 +56,19 @@ class Pledge;
 
 #define NS_DOMMEDIASTREAM_IID                        \
   {                                                  \
     0x8cb65468, 0x66c0, 0x444e, {                    \
       0x89, 0x9f, 0x89, 0x1d, 0x9e, 0xd2, 0xbe, 0x7c \
     }                                                \
   }
 
-class OnTracksAvailableCallback {
- public:
-  virtual ~OnTracksAvailableCallback() {}
-  virtual void NotifyTracksAvailable(DOMMediaStream* aStream) = 0;
-};
-
 /**
- * Interface through which a DOMMediaStream can query its producer for a
- * MediaStreamTrackSource. This will be used whenever a track occurs in the
- * DOMMediaStream's owned stream that has not yet been created on the main
- * thread (see DOMMediaStream::CreateOwnDOMTrack).
- */
-class MediaStreamTrackSourceGetter : public nsISupports {
-  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
-  NS_DECL_CYCLE_COLLECTION_CLASS(MediaStreamTrackSourceGetter)
-
- public:
-  MediaStreamTrackSourceGetter() {}
-
-  virtual already_AddRefed<dom::MediaStreamTrackSource>
-  GetMediaStreamTrackSource(TrackID aInputTrackID) = 0;
-
- protected:
-  virtual ~MediaStreamTrackSourceGetter() {}
-};
 
 // clang-format off
-/**
  * DOM wrapper for MediaStreams.
  *
  * To account for track operations such as clone(), addTrack() and
  * removeTrack(), a DOMMediaStream wraps three internal (and chained)
  * MediaStreams:
  *   1. mInputStream
  *      - Controlled by the owner/source of the DOMMediaStream.
  *        It's a stream of the type indicated by
@@ -309,18 +284,17 @@ class DOMMediaStream
     RefPtr<MediaInputPort> mInputPort;
     RefPtr<MediaStreamTrack> mTrack;
 
     // Defines if we've been given ownership of the input port or if it's owned
     // externally. The owner is responsible for destroying the port.
     const InputPortOwnership mOwnership;
   };
 
-  DOMMediaStream(nsPIDOMWindowInner* aWindow,
-                 MediaStreamTrackSourceGetter* aTrackSourceGetter);
+  explicit DOMMediaStream(nsPIDOMWindowInner* aWindow);
 
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DOMMediaStream, DOMEventTargetHelper)
   NS_DECLARE_STATIC_IID_ACCESSOR(NS_DOMMEDIASTREAM_IID)
 
   nsPIDOMWindowInner* GetParentObject() const { return mWindow; }
   virtual JSObject* WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aGivenProto) override;
@@ -347,39 +321,25 @@ class DOMMediaStream
   void GetAudioTracks(nsTArray<RefPtr<AudioStreamTrack>>& aTracks) const;
   void GetAudioTracks(nsTArray<RefPtr<MediaStreamTrack>>& aTracks) const;
   void GetVideoTracks(nsTArray<RefPtr<VideoStreamTrack>>& aTracks) const;
   void GetVideoTracks(nsTArray<RefPtr<MediaStreamTrack>>& aTracks) const;
   void GetTracks(nsTArray<RefPtr<MediaStreamTrack>>& aTracks) const;
   MediaStreamTrack* GetTrackById(const nsAString& aId) const;
   void AddTrack(MediaStreamTrack& aTrack);
   void RemoveTrack(MediaStreamTrack& aTrack);
-
-  /** Identical to CloneInternal(TrackForwardingOption::EXPLICIT) */
   already_AddRefed<DOMMediaStream> Clone();
 
   bool Active() const;
 
   IMPL_EVENT_HANDLER(addtrack)
   IMPL_EVENT_HANDLER(removetrack)
 
   // NON-WebIDL
 
-  /**
-   * Option to provide to CloneInternal() of which tracks should be forwarded
-   * from the source stream (`this`) to the returned stream clone.
-   *
-   * CURRENT forwards the tracks currently in the source stream's track set.
-   * ALL     forwards like EXPLICIT plus any and all future tracks originating
-   *         from the same input stream as the source DOMMediaStream (`this`).
-   */
-  enum class TrackForwardingOption { CURRENT, ALL };
-  already_AddRefed<DOMMediaStream> CloneInternal(
-      TrackForwardingOption aForwarding);
-
   MediaStreamTrack* GetOwnedTrackById(const nsAString& aId);
 
   /**
    * Returns true if this DOMMediaStream has aTrack in its mPlaybackStream.
    */
   bool HasTrack(const MediaStreamTrack& aTrack) const;
 
   /**
@@ -430,21 +390,16 @@ class DOMMediaStream
    */
   virtual MediaStream* GetCameraStream() const { return nullptr; }
 
   /**
    * Legacy method that returns true when the playback stream has finished.
    */
   bool IsFinished() const;
 
-  /**
-   * Becomes inactive only when the playback stream has finished.
-   */
-  void SetInactiveOnFinish();
-
   TrackRate GraphRate();
 
   /**
    * Returns a principal indicating who may access this stream. The stream
    * contents can only be accessed by principals subsuming this principal.
    */
   nsIPrincipal* GetPrincipal() { return mPrincipal; }
 
@@ -480,26 +435,24 @@ class DOMMediaStream
   // need to surface this to content.
   void AssignId(const nsAString& aID) { mID = aID; }
 
   /**
    * Create a DOMMediaStream whose underlying input stream is a
    * SourceMediaStream.
    */
   static already_AddRefed<DOMMediaStream> CreateSourceStreamAsInput(
-      nsPIDOMWindowInner* aWindow, MediaStreamGraph* aGraph,
-      MediaStreamTrackSourceGetter* aTrackSourceGetter = nullptr);
+      nsPIDOMWindowInner* aWindow, MediaStreamGraph* aGraph);
 
   /**
    * Create a DOMMediaStream whose underlying input stream is a
    * TrackUnionStream.
    */
   static already_AddRefed<DOMMediaStream> CreateTrackUnionStreamAsInput(
-      nsPIDOMWindowInner* aWindow, MediaStreamGraph* aGraph,
-      MediaStreamTrackSourceGetter* aTrackSourceGetter = nullptr);
+      nsPIDOMWindowInner* aWindow, MediaStreamGraph* aGraph);
 
   /**
    * Create an DOMMediaStream whose underlying input stream is an
    * AudioCaptureStream.
    */
   static already_AddRefed<DOMMediaStream> CreateAudioCaptureStreamAsInput(
       nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal,
       MediaStreamGraph* aGraph);
@@ -528,26 +481,16 @@ class DOMMediaStream
   /**
    * Creates a MediaStreamTrack cloned from aTrack, adds it to mTracks and
    * returns it.
    * aCloneTrackID is the TrackID the new track will get in mOwnedStream.
    */
   already_AddRefed<MediaStreamTrack> CloneDOMTrack(MediaStreamTrack& aTrack,
                                                    TrackID aCloneTrackID);
 
-  // When the initial set of tracks has been added, run
-  // aCallback->NotifyTracksAvailable.
-  // It is allowed to do anything, including run script.
-  // aCallback may run immediately during this call if tracks are already
-  // available!
-  // We only care about track additions, we'll fire the notification even if
-  // some of the tracks have been removed.
-  // Takes ownership of aCallback.
-  void OnTracksAvailable(OnTracksAvailableCallback* aCallback);
-
   /**
    * Add an nsISupports object that this stream will keep alive as long as
    * the stream itself is alive.
    */
   void AddConsumerToKeepAlive(nsISupports* aConsumer) {
     mConsumersToKeepAlive.AppendElement(aConsumer);
   }
 
@@ -556,16 +499,20 @@ class DOMMediaStream
   // being destroyed, so we don't hold on to a dead pointer. Main thread only.
   void RegisterTrackListener(TrackListener* aListener);
 
   // Unregisters a track listener from this MediaStream. The caller must call
   // UnregisterTrackListener before being destroyed, so we don't hold on to
   // a dead pointer. Main thread only.
   void UnregisterTrackListener(TrackListener* aListener);
 
+  // Tells this MediaStream whether it can go inactive as soon as no tracks
+  // are live anymore.
+  void SetFinishedOnInactive(bool aFinishedOnInactive);
+
  protected:
   virtual ~DOMMediaStream();
 
   void Destroy();
   void InitSourceStream(MediaStreamGraph* aGraph);
   void InitTrackUnionStream(MediaStreamGraph* aGraph);
   void InitAudioCaptureStream(nsIPrincipal* aPrincipal,
                               MediaStreamGraph* aGraph);
@@ -582,47 +529,32 @@ class DOMMediaStream
   void InitOwnedStreamCommon(MediaStreamGraph* aGraph);
 
   // Sets up a new TrackUnionStream as mPlaybackStream and connects it to
   // mOwnedStream with a TRACK_ANY MediaInputPort if available.
   // If this DOMMediaStream should have an owned stream (producer or clone),
   // it has to be initiated before the playback stream.
   void InitPlaybackStreamCommon(MediaStreamGraph* aGraph);
 
-  void CheckTracksAvailable();
-
-  // Called when MediaStreamGraph has finished an iteration where tracks were
-  // created.
-  void NotifyTracksCreated();
-
-  // Called when our playback stream has finished in the MediaStreamGraph.
-  void NotifyFinished();
-
   // Dispatches NotifyActive() to all registered track listeners.
   void NotifyActive();
 
   // Dispatches NotifyInactive() to all registered track listeners.
   void NotifyInactive();
 
   // Dispatches NotifyTrackAdded() to all registered track listeners.
   void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack);
 
   // Dispatches NotifyTrackRemoved() to all registered track listeners.
   void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack);
 
   // Dispatches "addtrack" or "removetrack".
   nsresult DispatchTrackEvent(const nsAString& aName,
                               const RefPtr<MediaStreamTrack>& aTrack);
 
-  class OwnedStreamListener;
-  friend class OwnedStreamListener;
-
-  class PlaybackStreamListener;
-  friend class PlaybackStreamListener;
-
   class PlaybackTrackListener;
   friend class PlaybackTrackListener;
 
   /**
    * Block a track in our playback stream. Calls NotifyPlaybackTrackBlocked()
    * after the MediaStreamGraph has applied the block and the track is no longer
    * live.
    */
@@ -670,53 +602,38 @@ class DOMMediaStream
 
   // MediaStreamTracks corresponding to tracks in our mPlaybackStream.
   AutoTArray<RefPtr<TrackPort>, 2> mTracks;
 
   // Number of MediaStreamTracks that have been removed on main thread but are
   // waiting to be removed on MediaStreamGraph thread.
   size_t mTracksPendingRemoval;
 
-  // The interface through which we can query the stream producer for
-  // track sources.
-  RefPtr<MediaStreamTrackSourceGetter> mTrackSourceGetter;
-
-  // Listener tracking changes to mOwnedStream. We use this to notify the
-  // MediaStreamTracks we own about state changes.
-  RefPtr<OwnedStreamListener> mOwnedListener;
-
-  // Listener tracking changes to mPlaybackStream. This drives state changes
-  // in this DOMMediaStream and notifications to mTrackListeners.
-  RefPtr<PlaybackStreamListener> mPlaybackListener;
-
   // Listener tracking when live MediaStreamTracks in mTracks end.
   RefPtr<PlaybackTrackListener> mPlaybackTrackListener;
 
-  nsTArray<nsAutoPtr<OnTracksAvailableCallback>> mRunOnTracksAvailable;
-
   // Set to true after MediaStreamGraph has created tracks for mPlaybackStream.
   bool mTracksCreated;
 
   nsString mID;
 
   // Keep these alive while the stream is alive.
   nsTArray<nsCOMPtr<nsISupports>> mConsumersToKeepAlive;
 
   bool mNotifiedOfMediaStreamGraphShutdown;
 
   // The track listeners subscribe to changes in this stream's track set.
   nsTArray<TrackListener*> mTrackListeners;
 
   // True if this stream has live tracks.
   bool mActive;
 
-  // True if this stream only sets mActive to false when its playback stream
-  // finishes. This is a hack to maintain legacy functionality for playing a
-  // HTMLMediaElement::MozCaptureStream(). See bug 1302379.
-  bool mSetInactiveOnFinish;
+  // For compatibility with mozCaptureStream, we in some cases do not go
+  // inactive until the MediaDecoder lets us. (Remove this in Bug 1302379)
+  bool mFinishedOnInactive;
 
  private:
   void NotifyPrincipalChanged();
   // Principal identifying who may access the collected contents of this stream.
   // If null, this stream can be used by anyone because it has no content yet.
   nsCOMPtr<nsIPrincipal> mPrincipal;
   // Video principal is used by video element as access is requested to its
   // image data.
--- a/dom/media/MediaDecoder.cpp
+++ b/dom/media/MediaDecoder.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "MediaDecoder.h"
 
+#include "DOMMediaStream.h"
 #include "ImageContainer.h"
 #include "Layers.h"
 #include "MediaDecoderStateMachine.h"
 #include "MediaFormatReader.h"
 #include "MediaResource.h"
 #include "MediaShutdownManager.h"
 #include "VideoFrameContainer.h"
 #include "VideoUtils.h"
@@ -234,39 +235,50 @@ void MediaDecoder::SetVolume(double aVol
 }
 
 RefPtr<GenericPromise> MediaDecoder::SetSink(AudioDeviceInfo* aSink) {
   MOZ_ASSERT(NS_IsMainThread());
   AbstractThread::AutoEnter context(AbstractMainThread());
   return GetStateMachine()->InvokeSetSink(aSink);
 }
 
-void MediaDecoder::AddOutputStream(ProcessedMediaStream* aStream,
-                                   TrackID aNextAvailableTrackID,
-                                   bool aFinishWhenEnded) {
+void MediaDecoder::SetOutputStreamCORSMode(CORSMode aCORSMode) {
+  MOZ_ASSERT(NS_IsMainThread());
+  AbstractThread::AutoEnter context(AbstractMainThread());
+  mDecoderStateMachine->SetOutputStreamCORSMode(aCORSMode);
+}
+
+void MediaDecoder::AddOutputStream(DOMMediaStream* aStream) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(mDecoderStateMachine, "Must be called after Load().");
   AbstractThread::AutoEnter context(AbstractMainThread());
-  mDecoderStateMachine->AddOutputStream(aStream, aNextAvailableTrackID,
-                                        aFinishWhenEnded);
+  mDecoderStateMachine->EnsureOutputStreamManager(
+      aStream->GetInputStream()->Graph(), ToMaybe(mInfo.get()));
+  mDecoderStateMachine->AddOutputStream(aStream);
 }
 
-void MediaDecoder::RemoveOutputStream(MediaStream* aStream) {
+void MediaDecoder::RemoveOutputStream(DOMMediaStream* aStream) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(mDecoderStateMachine, "Must be called after Load().");
   AbstractThread::AutoEnter context(AbstractMainThread());
   mDecoderStateMachine->RemoveOutputStream(aStream);
 }
 
-TrackID MediaDecoder::NextAvailableTrackIDFor(
-    MediaStream* aOutputStream) const {
+void MediaDecoder::SetNextOutputStreamTrackID(TrackID aNextTrackID) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(mDecoderStateMachine, "Must be called after Load().");
   AbstractThread::AutoEnter context(AbstractMainThread());
-  return mDecoderStateMachine->NextAvailableTrackIDFor(aOutputStream);
+  mDecoderStateMachine->SetNextOutputStreamTrackID(aNextTrackID);
+}
+
+TrackID MediaDecoder::GetNextOutputStreamTrackID() {
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(mDecoderStateMachine, "Must be called after Load().");
+  AbstractThread::AutoEnter context(AbstractMainThread());
+  return mDecoderStateMachine->GetNextOutputStreamTrackID();
 }
 
 double MediaDecoder::GetDuration() {
   MOZ_ASSERT(NS_IsMainThread());
   AbstractThread::AutoEnter context(AbstractMainThread());
   return mDuration;
 }
 
@@ -302,17 +314,16 @@ MediaDecoder::MediaDecoder(MediaDecoderI
       INIT_MIRROR(mCurrentPosition, TimeUnit::Zero()),
       INIT_MIRROR(mStateMachineDuration, NullableTimeUnit()),
       INIT_MIRROR(mIsAudioDataAudible, false),
       INIT_CANONICAL(mVolume, aInit.mVolume),
       INIT_CANONICAL(mPreservesPitch, aInit.mPreservesPitch),
       INIT_CANONICAL(mLooping, aInit.mLooping),
       INIT_CANONICAL(mPlayState, PLAY_STATE_LOADING),
       INIT_CANONICAL(mSameOriginMedia, false),
-      INIT_CANONICAL(mMediaPrincipalHandle, PRINCIPAL_HANDLE_NONE),
       mVideoDecodingOberver(
           new BackgroundVideoDecodingPermissionObserver(this)),
       mIsBackgroundVideoDecodingAllowed(false),
       mTelemetryReported(false),
       mContainerType(aInit.mContainerType) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(mAbstractMainThread);
   MediaMemoryTracker::AddMediaDecoder(this);
@@ -737,16 +748,18 @@ void MediaDecoder::DecodeError(const Med
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
   GetOwner()->DecodeError(aError);
 }
 
 void MediaDecoder::UpdateSameOriginStatus(bool aSameOrigin) {
   MOZ_ASSERT(NS_IsMainThread());
   AbstractThread::AutoEnter context(AbstractMainThread());
+  nsCOMPtr<nsIPrincipal> principal = GetCurrentPrincipal();
+  mDecoderStateMachine->SetOutputStreamPrincipal(principal);
   mSameOriginMedia = aSameOrigin;
 }
 
 bool MediaDecoder::IsSeeking() const {
   MOZ_ASSERT(NS_IsMainThread());
   return mLogicallySeeking;
 }
 
@@ -784,18 +797,16 @@ void MediaDecoder::PlaybackEnded() {
   InvalidateWithFlags(VideoFrameContainer::INVALIDATE_FORCE);
   GetOwner()->PlaybackEnded();
 }
 
 void MediaDecoder::NotifyPrincipalChanged() {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
   AbstractThread::AutoEnter context(AbstractMainThread());
-  nsCOMPtr<nsIPrincipal> newPrincipal = GetCurrentPrincipal();
-  mMediaPrincipalHandle = MakePrincipalHandle(newPrincipal);
   GetOwner()->NotifyDecoderPrincipalChanged();
 }
 
 void MediaDecoder::OnSeekResolved() {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
   AbstractThread::AutoEnter context(AbstractMainThread());
   mLogicallySeeking = false;
--- a/dom/media/MediaDecoder.h
+++ b/dom/media/MediaDecoder.h
@@ -14,16 +14,17 @@
 #include "MediaEventSource.h"
 #include "MediaMetadataManager.h"
 #include "MediaPromiseDefs.h"
 #include "MediaResource.h"
 #include "MediaStatistics.h"
 #include "MediaStreamGraph.h"
 #include "SeekTarget.h"
 #include "TimeUnits.h"
+#include "TrackID.h"
 #include "mozilla/Atomics.h"
 #include "mozilla/CDMProxy.h"
 #include "mozilla/MozPromise.h"
 #include "mozilla/ReentrantMonitor.h"
 #include "mozilla/StateMirroring.h"
 #include "mozilla/StateWatching.h"
 #include "nsAutoPtr.h"
 #include "nsCOMPtr.h"
@@ -35,16 +36,17 @@ class nsIPrincipal;
 
 namespace mozilla {
 
 namespace dom {
 class MediaMemoryInfo;
 }
 
 class AbstractThread;
+class DOMMediaStream;
 class FrameStatistics;
 class VideoFrameContainer;
 class MediaFormatReader;
 class MediaDecoderStateMachine;
 struct MediaPlaybackEvent;
 
 enum class Visibility : uint8_t;
 
@@ -160,26 +162,32 @@ class MediaDecoder : public DecoderDocto
 
   // All MediaStream-related data is protected by mReentrantMonitor.
   // We have at most one DecodedStreamData per MediaDecoder. Its stream
   // is used as the input for each ProcessedMediaStream created by calls to
   // captureStream(UntilEnded). Seeking creates a new source stream, as does
   // replaying after the input as ended. In the latter case, the new source is
   // not connected to streams created by captureStreamUntilEnded.
 
+  // Sets the CORSMode for MediaStreamTracks that will be created by us.
+  void SetOutputStreamCORSMode(CORSMode aCORSMode);
+
   // Add an output stream. All decoder output will be sent to the stream.
   // The stream is initially blocked. The decoder is responsible for unblocking
   // it while it is playing back.
-  virtual void AddOutputStream(ProcessedMediaStream* aStream,
-                               TrackID aNextAvailableTrackID,
-                               bool aFinishWhenEnded);
+  void AddOutputStream(DOMMediaStream* aStream);
   // Remove an output stream added with AddOutputStream.
-  virtual void RemoveOutputStream(MediaStream* aStream);
-  // The next TrackID that can be used without risk of a collision.
-  virtual TrackID NextAvailableTrackIDFor(MediaStream* aOutputStream) const;
+  void RemoveOutputStream(DOMMediaStream* aStream);
+
+  // Set the TrackID to be used as the initial id by the next DecodedStream
+  // sink.
+  void SetNextOutputStreamTrackID(TrackID aNextTrackID);
+  // Get the next TrackID to be allocated by DecodedStream,
+  // or the last set TrackID if there is no DecodedStream sink.
+  TrackID GetNextOutputStreamTrackID();
 
   // Return the duration of the video in seconds.
   virtual double GetDuration();
 
   // Return true if the stream is infinite.
   bool IsInfinite() const;
 
   // Return true if we are currently seeking in the media resource.
@@ -608,20 +616,16 @@ class MediaDecoder : public DecoderDocto
 
   // This can only be changed on the main thread.
   PlayState mNextState = PLAY_STATE_PAUSED;
 
   // True if the media is same-origin with the element. Data can only be
   // passed to MediaStreams when this is true.
   Canonical<bool> mSameOriginMedia;
 
-  // An identifier for the principal of the media. Used to track when
-  // main-thread induced principal changes get reflected on MSG thread.
-  Canonical<PrincipalHandle> mMediaPrincipalHandle;
-
   // We can allow video decoding in background when we match some special
   // conditions, eg. when the cursor is hovering over the tab. This observer is
   // used to listen the related events.
   RefPtr<BackgroundVideoDecodingPermissionObserver> mVideoDecodingOberver;
 
   // True if we want to resume video decoding even the media element is in the
   // background.
   bool mIsBackgroundVideoDecodingAllowed;
@@ -631,19 +635,16 @@ class MediaDecoder : public DecoderDocto
   AbstractCanonical<bool>* CanonicalPreservesPitch() {
     return &mPreservesPitch;
   }
   AbstractCanonical<bool>* CanonicalLooping() { return &mLooping; }
   AbstractCanonical<PlayState>* CanonicalPlayState() { return &mPlayState; }
   AbstractCanonical<bool>* CanonicalSameOriginMedia() {
     return &mSameOriginMedia;
   }
-  AbstractCanonical<PrincipalHandle>* CanonicalMediaPrincipalHandle() {
-    return &mMediaPrincipalHandle;
-  }
 
  private:
   // Notify owner when the audible state changed
   void NotifyAudibleStateChanged();
 
   bool mTelemetryReported;
   const MediaContainerType mContainerType;
   bool mCanPlayThrough = false;
--- a/dom/media/MediaDecoderOwner.h
+++ b/dom/media/MediaDecoderOwner.h
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 #ifndef MediaDecoderOwner_h_
 #define MediaDecoderOwner_h_
 
 #include "mozilla/UniquePtr.h"
 #include "MediaInfo.h"
 #include "MediaSegment.h"
 #include "nsSize.h"
+#include "TrackID.h"
 
 class nsIDocument;
 
 namespace mozilla {
 
 class AbstractThread;
 class GMPCrashHelper;
 class VideoFrameContainer;
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -20,16 +20,17 @@
 #include "mozilla/Sprintf.h"
 #include "mozilla/StaticPrefs.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/TaskQueue.h"
 #include "mozilla/Tuple.h"
 #include "nsIMemoryReporter.h"
 #include "nsPrintfCString.h"
 #include "nsTArray.h"
+#include "DOMMediaStream.h"
 #include "ImageContainer.h"
 #include "MediaDecoder.h"
 #include "MediaDecoderStateMachine.h"
 #include "MediaShutdownManager.h"
 #include "MediaTimer.h"
 #include "ReaderProxy.h"
 #include "TimeUnits.h"
 #include "VideoUtils.h"
@@ -2574,17 +2575,16 @@ RefPtr<ShutdownPromise> MediaDecoderStat
 
   // Disconnect canonicals and mirrors before shutting down our task queue.
   master->mBuffered.DisconnectIfConnected();
   master->mPlayState.DisconnectIfConnected();
   master->mVolume.DisconnectIfConnected();
   master->mPreservesPitch.DisconnectIfConnected();
   master->mLooping.DisconnectIfConnected();
   master->mSameOriginMedia.DisconnectIfConnected();
-  master->mMediaPrincipalHandle.DisconnectIfConnected();
 
   master->mDuration.DisconnectAll();
   master->mCurrentPosition.DisconnectAll();
   master->mIsAudioDataAudible.DisconnectAll();
 
   // Shut down the watch manager to stop further notifications.
   master->mWatchManager.Shutdown();
 
@@ -2615,27 +2615,26 @@ MediaDecoderStateMachine::MediaDecoderSt
       mReader(new ReaderProxy(mTaskQueue, aReader)),
       mPlaybackRate(1.0),
       mAmpleAudioThreshold(detail::AMPLE_AUDIO_THRESHOLD),
       mAudioCaptured(false),
       mMinimizePreroll(aDecoder->GetMinimizePreroll()),
       mSentFirstFrameLoadedEvent(false),
       mVideoDecodeSuspended(false),
       mVideoDecodeSuspendTimer(mTaskQueue),
-      mOutputStreamManager(new OutputStreamManager()),
+      mOutputStreamManager(nullptr),
       mVideoDecodeMode(VideoDecodeMode::Normal),
       mIsMSE(aDecoder->IsMSE()),
       mSeamlessLoopingAllowed(false),
       INIT_MIRROR(mBuffered, TimeIntervals()),
       INIT_MIRROR(mPlayState, MediaDecoder::PLAY_STATE_LOADING),
       INIT_MIRROR(mVolume, 1.0),
       INIT_MIRROR(mPreservesPitch, true),
       INIT_MIRROR(mLooping, false),
       INIT_MIRROR(mSameOriginMedia, false),
-      INIT_MIRROR(mMediaPrincipalHandle, PRINCIPAL_HANDLE_NONE),
       INIT_CANONICAL(mDuration, NullableTimeUnit()),
       INIT_CANONICAL(mCurrentPosition, TimeUnit::Zero()),
       INIT_CANONICAL(mIsAudioDataAudible, false),
       mSetSinkRequestsCount(0) {
   MOZ_COUNT_CTOR(MediaDecoderStateMachine);
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
 
   InitVideoQueuePrefs();
@@ -2657,17 +2656,16 @@ void MediaDecoderStateMachine::Initializ
 
   // Connect mirrors.
   mBuffered.Connect(mReader->CanonicalBuffered());
   mPlayState.Connect(aDecoder->CanonicalPlayState());
   mVolume.Connect(aDecoder->CanonicalVolume());
   mPreservesPitch.Connect(aDecoder->CanonicalPreservesPitch());
   mLooping.Connect(aDecoder->CanonicalLooping());
   mSameOriginMedia.Connect(aDecoder->CanonicalSameOriginMedia());
-  mMediaPrincipalHandle.Connect(aDecoder->CanonicalMediaPrincipalHandle());
 
   // Initialize watchers.
   mWatchManager.Watch(mBuffered,
                       &MediaDecoderStateMachine::BufferedRangeUpdated);
   mWatchManager.Watch(mVolume, &MediaDecoderStateMachine::VolumeChanged);
   mWatchManager.Watch(mPreservesPitch,
                       &MediaDecoderStateMachine::PreservesPitchChanged);
   mWatchManager.Watch(mPlayState, &MediaDecoderStateMachine::PlayStateChanged);
@@ -2695,23 +2693,23 @@ media::MediaSink* MediaDecoderStateMachi
         self->mTaskQueue, self.get(),
         &MediaDecoderStateMachine::AudioAudibleChanged);
     return audioSink;
   };
   return new AudioSinkWrapper(mTaskQueue, mAudioQueue, audioSinkCreator);
 }
 
 already_AddRefed<media::MediaSink> MediaDecoderStateMachine::CreateMediaSink(
-    bool aAudioCaptured) {
+    bool aAudioCaptured, OutputStreamManager* aManager) {
+  MOZ_ASSERT_IF(aAudioCaptured, aManager);
   RefPtr<media::MediaSink> audioSink =
-      aAudioCaptured ? new DecodedStream(
-                           mTaskQueue, mAbstractMainThread, mAudioQueue,
-                           mVideoQueue, mOutputStreamManager,
-                           mSameOriginMedia.Ref(), mMediaPrincipalHandle.Ref())
-                     : CreateAudioSink();
+      aAudioCaptured
+          ? new DecodedStream(mTaskQueue, mAbstractMainThread, mAudioQueue,
+                              mVideoQueue, aManager, mSameOriginMedia.Ref())
+          : CreateAudioSink();
 
   RefPtr<media::MediaSink> mediaSink =
       new VideoSink(mTaskQueue, audioSink, mVideoQueue, mVideoFrameContainer,
                     *mFrameStats, sVideoQueueSendToCompositorSize);
   return mediaSink.forget();
 }
 
 TimeUnit MediaDecoderStateMachine::GetDecodedAudioDuration() {
@@ -2791,17 +2789,17 @@ nsresult MediaDecoderStateMachine::Init(
   mVideoQueueListener = VideoQueue().PopFrontEvent().Connect(
       mTaskQueue, this, &MediaDecoderStateMachine::OnVideoPopped);
 
   mMetadataManager.Connect(mReader->TimedMetadataEvent(), OwnerThread());
 
   mOnMediaNotSeekable = mReader->OnMediaNotSeekable().Connect(
       OwnerThread(), this, &MediaDecoderStateMachine::SetMediaNotSeekable);
 
-  mMediaSink = CreateMediaSink(mAudioCaptured);
+  mMediaSink = CreateMediaSink(mAudioCaptured, mOutputStreamManager);
 
   nsresult rv = mReader->Init();
   NS_ENSURE_SUCCESS(rv, rv);
 
   mReader->SetCanonicalDuration(&mDuration);
 
   return NS_OK;
 }
@@ -3324,17 +3322,18 @@ void MediaDecoderStateMachine::FinishDec
   mReader->ReadUpdatedMetadata(mInfo.ptr());
 
   EnqueueFirstFrameLoadedEvent();
 }
 
 RefPtr<ShutdownPromise> MediaDecoderStateMachine::BeginShutdown() {
   MOZ_ASSERT(NS_IsMainThread());
   if (mOutputStreamManager) {
-    mOutputStreamManager->Clear();
+    mNextOutputStreamTrackID = mOutputStreamManager->NextTrackID();
+    mOutputStreamManager->Disconnect();
   }
   return InvokeAsync(OwnerThread(), this, __func__,
                      &MediaDecoderStateMachine::Shutdown);
 }
 
 RefPtr<ShutdownPromise> MediaDecoderStateMachine::FinishShutdown() {
   MOZ_ASSERT(OnTaskQueue());
   LOG("Shutting down state machine task queue");
@@ -3627,17 +3626,18 @@ void MediaDecoderStateMachine::OnMediaSi
     return;
   }
 
   // Otherwise notify media decoder/element about this error for it makes
   // no sense to play an audio-only file without sound output.
   DecodeError(MediaResult(NS_ERROR_DOM_MEDIA_MEDIASINK_ERR, __func__));
 }
 
-void MediaDecoderStateMachine::SetAudioCaptured(bool aCaptured) {
+void MediaDecoderStateMachine::SetAudioCaptured(bool aCaptured,
+                                                OutputStreamManager* aManager) {
   MOZ_ASSERT(OnTaskQueue());
 
   if (aCaptured == mAudioCaptured) {
     return;
   }
 
   // Rest these flags so they are consistent with the status of the sink.
   // TODO: Move these flags into MediaSink to improve cohesion so we don't need
@@ -3648,17 +3648,17 @@ void MediaDecoderStateMachine::SetAudioC
   // Backup current playback parameters.
   MediaSink::PlaybackParams params = mMediaSink->GetPlaybackParams();
 
   // Stop and shut down the existing sink.
   StopMediaSink();
   mMediaSink->Shutdown();
 
   // Create a new sink according to whether audio is captured.
-  mMediaSink = CreateMediaSink(aCaptured);
+  mMediaSink = CreateMediaSink(aCaptured, aManager);
 
   // Restore playback parameters.
   mMediaSink->SetPlaybackParams(params);
 
   mAudioCaptured = aCaptured;
 
   // Don't buffer as much when audio is captured because we don't need to worry
   // about high latency audio devices.
@@ -3712,48 +3712,104 @@ MediaDecoderStateMachine::RequestDebugIn
           "MediaDecoderStateMachine::RequestDebugInfo",
           [self, p]() { p->Resolve(self->GetDebugInfo(), __func__); }),
       AbstractThread::TailDispatch);
   MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
   Unused << rv;
   return p.forget();
 }
 
-void MediaDecoderStateMachine::AddOutputStream(ProcessedMediaStream* aStream,
-                                               TrackID aNextAvailableTrackID,
-                                               bool aFinishWhenEnded) {
+void MediaDecoderStateMachine::SetOutputStreamPrincipal(
+    const nsCOMPtr<nsIPrincipal>& aPrincipal) {
+  MOZ_ASSERT(NS_IsMainThread());
+  mOutputStreamPrincipal = aPrincipal;
+  if (mOutputStreamManager) {
+    mOutputStreamManager->SetPrincipal(mOutputStreamPrincipal);
+  }
+}
+
+void MediaDecoderStateMachine::SetOutputStreamCORSMode(CORSMode aCORSMode) {
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(mOutputStreamCORSMode == CORS_NONE);
+  MOZ_ASSERT(!mOutputStreamManager);
+  mOutputStreamCORSMode = aCORSMode;
+}
+
+void MediaDecoderStateMachine::AddOutputStream(DOMMediaStream* aStream) {
   MOZ_ASSERT(NS_IsMainThread());
   LOG("AddOutputStream aStream=%p!", aStream);
-  mOutputStreamManager->Add(aStream, aNextAvailableTrackID, aFinishWhenEnded);
-  nsCOMPtr<nsIRunnable> r = NewRunnableMethod<bool>(
-      "MediaDecoderStateMachine::SetAudioCaptured", this,
-      &MediaDecoderStateMachine::SetAudioCaptured, true);
+  mOutputStreamManager->Add(aStream);
+  nsCOMPtr<nsIRunnable> r =
+      NS_NewRunnableFunction("MediaDecoderStateMachine::SetAudioCaptured",
+                             [self = RefPtr<MediaDecoderStateMachine>(this),
+                              manager = mOutputStreamManager]() {
+                               self->SetAudioCaptured(true, manager);
+                             });
   nsresult rv = OwnerThread()->Dispatch(r.forget());
   MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
   Unused << rv;
 }
 
-void MediaDecoderStateMachine::RemoveOutputStream(MediaStream* aStream) {
+void MediaDecoderStateMachine::RemoveOutputStream(DOMMediaStream* aStream) {
   MOZ_ASSERT(NS_IsMainThread());
   LOG("RemoveOutputStream=%p!", aStream);
   mOutputStreamManager->Remove(aStream);
   if (mOutputStreamManager->IsEmpty()) {
-    nsCOMPtr<nsIRunnable> r = NewRunnableMethod<bool>(
-        "MediaDecoderStateMachine::SetAudioCaptured", this,
-        &MediaDecoderStateMachine::SetAudioCaptured, false);
+    mOutputStreamManager->Disconnect();
+    mOutputStreamManager = nullptr;
+    nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+        "MediaDecoderStateMachine::SetAudioCaptured",
+        [self = RefPtr<MediaDecoderStateMachine>(this)]() {
+          self->SetAudioCaptured(false);
+        });
     nsresult rv = OwnerThread()->Dispatch(r.forget());
     MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
     Unused << rv;
   }
 }
 
-TrackID MediaDecoderStateMachine::NextAvailableTrackIDFor(
-    MediaStream* aOutputStream) const {
+void MediaDecoderStateMachine::EnsureOutputStreamManager(
+    MediaStreamGraph* aGraph, const Maybe<MediaInfo>& aLoadedInfo) {
   MOZ_ASSERT(NS_IsMainThread());
-  return mOutputStreamManager->NextAvailableTrackIDFor(aOutputStream);
+  if (mOutputStreamManager) {
+    return;
+  }
+  LOG("Starting output track allocations at id %d", mNextOutputStreamTrackID);
+  mOutputStreamManager = new OutputStreamManager(
+      aGraph->CreateSourceStream(), mNextOutputStreamTrackID,
+      mOutputStreamPrincipal, mOutputStreamCORSMode, mAbstractMainThread);
+  if (!aLoadedInfo) {
+    return;
+  }
+  TrackID mirroredTrackIDAllocation = mNextOutputStreamTrackID;
+  if (aLoadedInfo->HasAudio()) {
+    mOutputStreamManager->AddTrack(mirroredTrackIDAllocation++,
+                                   MediaSegment::AUDIO);
+    LOG("Pre-created audio track with id %d", mirroredTrackIDAllocation - 1);
+  }
+  if (aLoadedInfo->HasVideo()) {
+    mOutputStreamManager->AddTrack(mirroredTrackIDAllocation++,
+                                   MediaSegment::VIDEO);
+    LOG("Pre-created video track with id %d", mirroredTrackIDAllocation - 1);
+  }
+}
+
+void MediaDecoderStateMachine::SetNextOutputStreamTrackID(
+    TrackID aNextTrackID) {
+  MOZ_ASSERT(NS_IsMainThread());
+  LOG("SetNextOutputStreamTrackID aNextTrackID=%d", aNextTrackID);
+  mNextOutputStreamTrackID = aNextTrackID;
+}
+
+TrackID MediaDecoderStateMachine::GetNextOutputStreamTrackID() {
+  MOZ_ASSERT(NS_IsMainThread());
+  if (mOutputStreamManager) {
+    return mOutputStreamManager->NextTrackID();
+  }
+  return mNextOutputStreamTrackID;
 }
 
 class VideoQueueMemoryFunctor : public nsDequeFunctor {
  public:
   VideoQueueMemoryFunctor() : mSize(0) {}
 
   MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf);
 
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -103,16 +103,17 @@ namespace mozilla {
 
 namespace media {
 class MediaSink;
 }
 
 class AbstractThread;
 class AudioSegment;
 class DecodedStream;
+class DOMMediaStream;
 class OutputStreamManager;
 class ReaderProxy;
 class TaskQueue;
 
 extern LazyLogModule gMediaDecoderLog;
 
 struct MediaPlaybackEvent {
   enum EventType {
@@ -182,21 +183,35 @@ class MediaDecoderStateMachine
     DECODER_STATE_SHUTDOWN
   };
 
   // Returns the state machine task queue.
   TaskQueue* OwnerThread() const { return mTaskQueue; }
 
   RefPtr<MediaDecoder::DebugInfoPromise> RequestDebugInfo();
 
-  void AddOutputStream(ProcessedMediaStream* aStream,
-                       TrackID aNextAvailableTrackID, bool aFinishWhenEnded);
-  // Remove an output stream added with AddOutputStream.
-  void RemoveOutputStream(MediaStream* aStream);
-  TrackID NextAvailableTrackIDFor(MediaStream* aOutputStream) const;
+  void SetOutputStreamPrincipal(const nsCOMPtr<nsIPrincipal>& aPrincipal);
+  void SetOutputStreamCORSMode(CORSMode aCORSMode);
+  // If an OutputStreamManager does not exist, one will be created and tracks
+  // matching aLoadedInfo will be created ahead of being created by the
+  // DecodedStream sink.
+  void EnsureOutputStreamManager(MediaStreamGraph* aGraph,
+                                 const Maybe<MediaInfo>& aLoadedInfo);
+  // Add an output stream to the output stream manager. The manager must have
+  // been created through EnsureOutputStreamManager() before this.
+  void AddOutputStream(DOMMediaStream* aStream);
+  // Remove an output stream added with AddOutputStream. If the last output
+  // stream was removed, we will also tear down the OutputStreamManager.
+  void RemoveOutputStream(DOMMediaStream* aStream);
+  // Set the TrackID to be used as the initial id by the next DecodedStream
+  // sink.
+  void SetNextOutputStreamTrackID(TrackID aNextTrackID);
+  // Get the next TrackID to be allocated by DecodedStream,
+  // or the last set TrackID if there is no DecodedStream sink.
+  TrackID GetNextOutputStreamTrackID();
 
   // Seeks to the decoder to aTarget asynchronously.
   RefPtr<MediaDecoder::SeekPromise> InvokeSeek(const SeekTarget& aTarget);
 
   void DispatchSetPlaybackRate(double aPlaybackRate) {
     OwnerThread()->DispatchStateChange(NewRunnableMethod<double>(
         "MediaDecoderStateMachine::SetPlaybackRate", this,
         &MediaDecoderStateMachine::SetPlaybackRate, aPlaybackRate));
@@ -302,17 +317,20 @@ class MediaDecoderStateMachine
   // on the appropriate threads.
   bool OnTaskQueue() const;
 
   // Initialization that needs to happen on the task queue. This is the first
   // task that gets run on the task queue, and is dispatched from the MDSM
   // constructor immediately after the task queue is created.
   void InitializationTask(MediaDecoder* aDecoder);
 
-  void SetAudioCaptured(bool aCaptured);
+  // Sets the audio-captured state and recreates the media sink if needed.
+  // A manager must be passed in if setting the audio-captured state to true.
+  void SetAudioCaptured(bool aCaptured,
+                        OutputStreamManager* aManager = nullptr);
 
   RefPtr<MediaDecoder::SeekPromise> Seek(const SeekTarget& aTarget);
 
   RefPtr<ShutdownPromise> Shutdown();
 
   RefPtr<ShutdownPromise> FinishShutdown();
 
   // Update the playback position. This can result in a timeupdate event
@@ -422,17 +440,19 @@ class MediaDecoderStateMachine
 
   // Update playback position and trigger next update by default time period.
   // Called on the state machine thread.
   void UpdatePlaybackPositionPeriodically();
 
   media::MediaSink* CreateAudioSink();
 
   // Always create mediasink which contains an AudioSink or StreamSink inside.
-  already_AddRefed<media::MediaSink> CreateMediaSink(bool aAudioCaptured);
+  // A manager must be passed in if aAudioCaptured is true.
+  already_AddRefed<media::MediaSink> CreateMediaSink(
+      bool aAudioCaptured, OutputStreamManager* aManager = nullptr);
 
   // Stops the media sink and shut it down.
   // The decoder monitor must be held with exactly one lock count.
   // Called on the state machine thread.
   void StopMediaSink();
 
   // Create and start the media sink.
   // The decoder monitor must be held with exactly one lock count.
@@ -652,17 +672,28 @@ class MediaDecoderStateMachine
 
   // True if the media is seekable only in buffered ranges.
   bool mMediaSeekableOnlyInBufferedRanges = false;
 
   // Track enabling video decode suspension via timer
   DelayedScheduler mVideoDecodeSuspendTimer;
 
   // Data about MediaStreams that are being fed by the decoder.
-  const RefPtr<OutputStreamManager> mOutputStreamManager;
+  // Main thread only.
+  RefPtr<OutputStreamManager> mOutputStreamManager;
+
+  // Principal used by output streams. Main thread only.
+  nsCOMPtr<nsIPrincipal> mOutputStreamPrincipal;
+
+  // CORSMode used by output streams. Main thread only.
+  CORSMode mOutputStreamCORSMode = CORS_NONE;
+
+  // The next TrackID to be used when a DecodedStream allocates a track.
+  // Main thread only.
+  TrackID mNextOutputStreamTrackID = 1;
 
   // Track the current video decode mode.
   VideoDecodeMode mVideoDecodeMode;
 
   // Track the complete & error for audio/video separately
   MozPromiseRequestHolder<GenericPromise> mMediaSinkAudioPromise;
   MozPromiseRequestHolder<GenericPromise> mMediaSinkVideoPromise;
 
@@ -712,20 +743,16 @@ class MediaDecoderStateMachine
   // Whether to seek back to the start of the media resource
   // upon reaching the end.
   Mirror<bool> mLooping;
 
   // True if the media is same-origin with the element. Data can only be
   // passed to MediaStreams when this is true.
   Mirror<bool> mSameOriginMedia;
 
-  // An identifier for the principal of the media. Used to track when
-  // main-thread induced principal changes get reflected on MSG thread.
-  Mirror<PrincipalHandle> mMediaPrincipalHandle;
-
   // Duration of the media. This is guaranteed to be non-null after we finish
   // decoding the first frame.
   Canonical<media::NullableTimeUnit> mDuration;
 
   // The time of the current frame, corresponding to the "current
   // playback position" in HTML5. This is referenced from 0, which is the
   // initial playback position.
   Canonical<media::TimeUnit> mCurrentPosition;
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -3,17 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "MediaManager.h"
 
 #include "AllocationHandle.h"
 #include "AudioDeviceInfo.h"
-#include "MediaStreamGraph.h"
+#include "MediaStreamGraphImpl.h"
 #include "MediaTimer.h"
 #include "mozilla/dom/MediaStreamTrack.h"
 #include "mozilla/dom/MediaDeviceInfo.h"
 #include "MediaStreamListener.h"
 #include "nsArray.h"
 #include "nsContentUtils.h"
 #include "nsGlobalWindow.h"
 #include "nsHashPropertyBag.h"
@@ -218,19 +218,24 @@ using dom::Promise;
 using dom::Sequence;
 using media::NewRunnableFrom;
 using media::NewTaskFrom;
 using media::Pledge;
 using media::Refcountable;
 
 static Atomic<bool> sHasShutdown;
 
+class SourceTrackListener;
+
 struct DeviceState {
-  DeviceState(const RefPtr<MediaDevice>& aDevice, bool aOffWhileDisabled)
-      : mOffWhileDisabled(aOffWhileDisabled), mDevice(aDevice) {
+  DeviceState(const RefPtr<MediaDevice>& aDevice, bool aOffWhileDisabled,
+              RefPtr<SourceTrackListener> aListener)
+      : mOffWhileDisabled(aOffWhileDisabled),
+        mDevice(aDevice),
+        mListener(std::move(aListener)) {
     MOZ_ASSERT(mDevice);
   }
 
   // true if we have stopped mDevice, this is a terminal state.
   // MainThread only.
   bool mStopped = false;
 
   // true if mDevice is currently enabled, i.e., turned on and capturing.
@@ -259,16 +264,20 @@ struct DeviceState {
   // disabled. When the timer fires we initiate Stop()ing mDevice.
   // If set we allow dynamically stopping and starting mDevice.
   // Any thread.
   const RefPtr<MediaTimer> mDisableTimer = new MediaTimer();
 
   // The underlying device we keep state for. Always non-null.
   // Threadsafe access, but see method declarations for individual constraints.
   const RefPtr<MediaDevice> mDevice;
+
+  // The track listener for the track hooked up to mDevice.
+  // Main thread only.
+  RefPtr<SourceTrackListener> mListener;
 };
 
 /**
  * This mimics the capture state from nsIMediaManagerService.
  */
 enum class CaptureState : uint16_t {
   Off = nsIMediaManagerService::STATE_NOCAPTURE,
   Enabled = nsIMediaManagerService::STATE_CAPTURE_ENABLED,
@@ -419,29 +428,24 @@ class SourceListener : public SupportsWe
   MediaDevice* GetVideoDevice() const {
     return mVideoDeviceState ? mVideoDeviceState->mDevice.get() : nullptr;
   }
 
   /**
    * Called on MediaStreamGraph thread when MSG asks us for more data from
    * input devices.
    */
-  void NotifyPull(MediaStreamGraph* aGraph, StreamTime aDesiredTime);
+  void Pull(TrackID aTrackID, StreamTime aEndOfAppendedData,
+            StreamTime aDesiredTime);
 
   /**
-   * Called on main thread after MediaStreamGraph notifies us that our
-   * MediaStream was marked finish in the graph.
+   * Called on main thread after MediaStreamGraph notifies us that one of our
+   * track listeners was removed as listener from its track in the graph.
    */
-  void NotifyFinished();
-
-  /**
-   * Called on main thread after MediaStreamGraph notifies us that we
-   * were removed as listener from the MediaStream in the graph.
-   */
-  void NotifyRemoved();
+  void NotifyRemoved(TrackID aTrackID);
 
   bool Activated() const { return mStream; }
 
   bool Stopped() const { return mStopped; }
 
   bool CapturingVideo() const;
 
   bool CapturingAudio() const;
@@ -451,80 +455,16 @@ class SourceListener : public SupportsWe
   RefPtr<ApplyConstraintsPromise> ApplyConstraintsToTrack(
       nsPIDOMWindowInner* aWindow, TrackID aTrackID,
       const dom::MediaTrackConstraints& aConstraints,
       dom::CallerType aCallerType);
 
   PrincipalHandle GetPrincipalHandle() const;
 
  private:
-  /**
-   * Wrapper class for the MediaStreamListener part of SourceListener.
-   *
-   * This is required since MediaStreamListener and SupportsWeakPtr
-   * both implement refcounting.
-   */
-  class SourceStreamListener : public MediaStreamListener {
-   public:
-    explicit SourceStreamListener(SourceListener* aSourceListener)
-        : mSourceListener(aSourceListener) {}
-
-    void NotifyPull(MediaStreamGraph* aGraph,
-                    StreamTime aDesiredTime) override {
-      mSourceListener->NotifyPull(aGraph, aDesiredTime);
-    }
-
-    void NotifyEvent(MediaStreamGraph* aGraph,
-                     MediaStreamGraphEvent aEvent) override {
-      nsCOMPtr<nsIEventTarget> target;
-
-      switch (aEvent) {
-        case MediaStreamGraphEvent::EVENT_FINISHED:
-          target = GetMainThreadEventTarget();
-          if (NS_WARN_IF(!target)) {
-            NS_ASSERTION(false,
-                         "Mainthread not available; running on current thread");
-            // Ensure this really *was* MainThread (NS_GetCurrentThread won't
-            // work)
-            MOZ_RELEASE_ASSERT(mSourceListener->mMainThreadCheck ==
-                               GetCurrentVirtualThread());
-            mSourceListener->NotifyFinished();
-            return;
-          }
-          target->Dispatch(NewRunnableMethod("SourceListener::NotifyFinished",
-                                             mSourceListener,
-                                             &SourceListener::NotifyFinished),
-                           NS_DISPATCH_NORMAL);
-          break;
-        case MediaStreamGraphEvent::EVENT_REMOVED:
-          target = GetMainThreadEventTarget();
-          if (NS_WARN_IF(!target)) {
-            NS_ASSERTION(false,
-                         "Mainthread not available; running on current thread");
-            // Ensure this really *was* MainThread (NS_GetCurrentThread won't
-            // work)
-            MOZ_RELEASE_ASSERT(mSourceListener->mMainThreadCheck ==
-                               GetCurrentVirtualThread());
-            mSourceListener->NotifyRemoved();
-            return;
-          }
-          target->Dispatch(NewRunnableMethod("SourceListener::NotifyRemoved",
-                                             mSourceListener,
-                                             &SourceListener::NotifyRemoved),
-                           NS_DISPATCH_NORMAL);
-          break;
-        default:
-          break;
-      }
-    }
-
-   private:
-    RefPtr<SourceListener> mSourceListener;
-  };
-
   virtual ~SourceListener() = default;
 
   /**
    * Returns a pointer to the device state for aTrackID.
    *
    * This is intended for internal use where we need to figure out which state
    * corresponds to aTrackID, not for availability checks. As such, we assert
    * that the device does indeed exist.
@@ -532,43 +472,80 @@ class SourceListener : public SupportsWe
    * Since this is a raw pointer and the state lifetime depends on the
    * SourceListener's lifetime, it's internal use only.
    */
   DeviceState& GetDeviceStateFor(TrackID aTrackID) const;
 
   // true after this listener has had all devices stopped. MainThread only.
   bool mStopped;
 
-  // true after the stream this listener is listening to has finished in the
-  // MediaStreamGraph. MainThread only.
-  bool mFinished;
-
   // true after this listener has been removed from its MediaStream.
   // MainThread only.
   bool mRemoved;
 
   // never ever indirect off this; just for assertions
   PRThread* mMainThreadCheck;
 
+  // For access to mMainThreadCheck
+  friend class SourceTrackListener;
+
   // Set in Register() on main thread, then read from any thread.
   PrincipalHandle mPrincipalHandle;
 
   // Weak pointer to the window listener that owns us. MainThread only.
   GetUserMediaWindowListener* mWindowListener;
 
   // Accessed from MediaStreamGraph thread, MediaManager thread, and MainThread
   // No locking needed as they're set on Activate() and never assigned to again.
   UniquePtr<DeviceState> mAudioDeviceState;
   UniquePtr<DeviceState> mVideoDeviceState;
-  RefPtr<SourceMediaStream> mStream;             // threadsafe refcnt
-  RefPtr<SourceStreamListener> mStreamListener;  // threadsafe refcnt
+  RefPtr<SourceMediaStream> mStream;  // threadsafe refcnt
 };
 
 /**
- * This class represents a WindowID and handles all MediaStreamListeners
+ * Wrapper class for the MediaStreamTrackListener part of SourceListener.
+ *
+ * This is required since MediaStreamTrackListener and SupportsWeakPtr
+ * both implement refcounting.
+ */
+class SourceTrackListener : public MediaStreamTrackListener {
+ public:
+  SourceTrackListener(SourceListener* aSourceListener, TrackID aTrackID)
+      : mSourceListener(aSourceListener), mTrackID(aTrackID) {}
+
+  void NotifyPull(MediaStreamGraph* aGraph, StreamTime aEndOfAppendedData,
+                  StreamTime aDesiredTime) override {
+    mSourceListener->Pull(mTrackID, aEndOfAppendedData, aDesiredTime);
+  }
+
+  void NotifyEnded() override { NotifyRemoved(); }
+
+  void NotifyRemoved() override {
+    nsCOMPtr<nsIEventTarget> target = GetMainThreadEventTarget();
+    if (NS_WARN_IF(!target)) {
+      NS_ASSERTION(false,
+                   "Mainthread not available; running on current thread");
+      // Ensure this really *was* MainThread (NS_GetCurrentThread won't work)
+      MOZ_RELEASE_ASSERT(mSourceListener->mMainThreadCheck ==
+                         GetCurrentVirtualThread());
+      mSourceListener->NotifyRemoved(mTrackID);
+      return;
+    }
+    target->Dispatch(NewRunnableMethod<TrackID>(
+        "SourceListener::NotifyRemoved", mSourceListener,
+        &SourceListener::NotifyRemoved, mTrackID));
+  }
+
+ private:
+  const RefPtr<SourceListener> mSourceListener;
+  const TrackID mTrackID;
+};
+
+/**
+ * This class represents a WindowID and handles all MediaStreamTrackListeners
  * (here subclassed as SourceListeners) used to feed GetUserMedia source
  * streams. It proxies feedback from them into messages for browser chrome.
  * The SourceListeners are used to Start() and Stop() the underlying
  * MediaEngineSource when MediaStreams are assigned and deassigned in content.
  */
 class GetUserMediaWindowListener {
   friend MediaManager;
 
@@ -1100,23 +1077,25 @@ nsresult MediaDevice::Stop() {
 
 nsresult MediaDevice::Deallocate() {
   MOZ_ASSERT(MediaManager::IsInMediaThread());
   MOZ_ASSERT(mSource);
   return mSource->Deallocate(mAllocationHandle);
 }
 
 void MediaDevice::Pull(const RefPtr<SourceMediaStream>& aStream,
-                       TrackID aTrackID, StreamTime aDesiredTime,
+                       TrackID aTrackID, StreamTime aEndOfAppendedData,
+                       StreamTime aDesiredTime,
                        const PrincipalHandle& aPrincipal) {
   // This is on the graph thread, but mAllocationHandle is safe since we never
   // change it after it's been set, which is guaranteed to happen before
   // registering the listener for pulls.
   MOZ_ASSERT(mSource);
-  mSource->Pull(mAllocationHandle, aStream, aTrackID, aDesiredTime, aPrincipal);
+  mSource->Pull(mAllocationHandle, aStream, aTrackID, aEndOfAppendedData,
+                aDesiredTime, aPrincipal);
 }
 
 dom::MediaSourceEnum MediaDevice::GetMediaSource() const {
   // Threadsafe because mSource is const. GetMediaSource() might have other
   // requirements.
   MOZ_ASSERT(mSource);
   return mSource->GetMediaSource();
 }
@@ -1128,54 +1107,16 @@ static bool IsOn(const OwningBooleanOrMe
 static const MediaTrackConstraints& GetInvariant(
     const OwningBooleanOrMediaTrackConstraints& aUnion) {
   static const MediaTrackConstraints empty;
   return aUnion.IsMediaTrackConstraints() ? aUnion.GetAsMediaTrackConstraints()
                                           : empty;
 }
 
 /**
- * This class is only needed since fake tracks are added dynamically.
- * Instead of refactoring to add them explicitly we let the DOMMediaStream
- * query us for the source as they become available.
- * Since they are used only for testing the API surface, we make them very
- * simple.
- */
-class FakeTrackSourceGetter : public MediaStreamTrackSourceGetter {
- public:
-  NS_DECL_ISUPPORTS_INHERITED
-  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FakeTrackSourceGetter,
-                                           MediaStreamTrackSourceGetter)
-
-  explicit FakeTrackSourceGetter(nsIPrincipal* aPrincipal)
-      : mPrincipal(aPrincipal) {}
-
-  already_AddRefed<dom::MediaStreamTrackSource> GetMediaStreamTrackSource(
-      TrackID aInputTrackID) override {
-    NS_ASSERTION(kAudioTrack != aInputTrackID,
-                 "Only fake tracks should appear dynamically");
-    NS_ASSERTION(kVideoTrack != aInputTrackID,
-                 "Only fake tracks should appear dynamically");
-    return do_AddRef(new BasicTrackSource(mPrincipal));
-  }
-
- protected:
-  virtual ~FakeTrackSourceGetter() {}
-
-  nsCOMPtr<nsIPrincipal> mPrincipal;
-};
-
-NS_IMPL_ADDREF_INHERITED(FakeTrackSourceGetter, MediaStreamTrackSourceGetter)
-NS_IMPL_RELEASE_INHERITED(FakeTrackSourceGetter, MediaStreamTrackSourceGetter)
-NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FakeTrackSourceGetter)
-NS_INTERFACE_MAP_END_INHERITING(MediaStreamTrackSourceGetter)
-NS_IMPL_CYCLE_COLLECTION_INHERITED(FakeTrackSourceGetter,
-                                   MediaStreamTrackSourceGetter, mPrincipal)
-
-/**
  * Creates a MediaStream, attaches a listener and fires off a success callback
  * to the DOM with the stream. We also pass in the error callback so it can
  * be released correctly.
  *
  * All of this must be done on the main thread!
  *
  * Note that the various GetUserMedia Runnable classes currently allow for
  * two streams.  If we ever need to support getting more than two streams
@@ -1205,51 +1146,86 @@ class GetUserMediaStreamRunnable : publi
         mWindowListener(aWindowListener),
         mSourceListener(aSourceListener),
         mPrincipalInfo(aPrincipalInfo),
         mPeerIdentity(aPeerIdentity),
         mManager(MediaManager::GetInstance()) {}
 
   ~GetUserMediaStreamRunnable() {}
 
-  class TracksAvailableCallback : public OnTracksAvailableCallback {
+  class TracksCreatedListener : public MediaStreamTrackListener {
    public:
-    TracksAvailableCallback(
+    TracksCreatedListener(
         MediaManager* aManager,
         const nsMainThreadPtrHandle<MediaManager::GetUserMediaSuccessCallback>&
             aSuccess,
-        const RefPtr<GetUserMediaWindowListener>& aWindowListener,
-        DOMMediaStream* aStream)
+        GetUserMediaWindowListener* aWindowListener, DOMMediaStream* aStream,
+        MediaStreamTrack* aTrack)
         : mWindowListener(aWindowListener),
           mOnSuccess(aSuccess),
           mManager(aManager),
-          mStream(aStream) {}
-    void NotifyTracksAvailable(DOMMediaStream* aStream) override {
-      // We're on the main thread, so no worries here.
-      if (!mManager->IsWindowListenerStillActive(mWindowListener)) {
+          mGraph(aTrack->GraphImpl()),
+          mStream(new nsMainThreadPtrHolder<DOMMediaStream>(
+              "TracksCreatedListener::mStream", aStream)),
+          mTrack(new nsMainThreadPtrHolder<MediaStreamTrack>(
+              "TracksCreatedListener::mTrack", aTrack)) {}
+    void NotifyOutput(MediaStreamGraph* aGraph,
+                      StreamTime aCurrentTrackTime) override {
+      // It's enough to know that one of the tracks have output, as both tracks
+      // are guaranteed to be created in the graph at this point.
+
+      if (mDispatchedTracksCreated) {
         return;
       }
-
-      // This is safe since we're on main-thread, and the windowlist can only
-      // be invalidated from the main-thread (see OnNavigation)
-      LOG(("Returning success for getUserMedia()"));
-      CallOnSuccess(mOnSuccess, *aStream);
+      mDispatchedTracksCreated = true;
+      nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+          "TracksCreatedListener::NotifyOutput Notifier",
+          [self = RefPtr<TracksCreatedListener>(this), this]() {
+            mTrack->RemoveListener(this);
+
+            if (!mManager->IsWindowListenerStillActive(mWindowListener)) {
+              return;
+            }
+
+            // This is safe since we're on main-thread, and the windowlist can
+            // only be invalidated from the main-thread (see OnNavigation)
+            LOG(("Returning success for getUserMedia()"));
+            CallOnSuccess(mOnSuccess, *mStream);
+          });
+      // DispatchToMainThreadAfterStreamStateUpdate will make the runnable run
+      // in stable state. But since the runnable runs JS we need to make a
+      // double dispatch.
+      mGraph->DispatchToMainThreadAfterStreamStateUpdate(NS_NewRunnableFunction(
+          "TracksCreatedListener::NotifyOutput Stable State Notifier",
+          [graph = mGraph, r = std::move(r)]() mutable {
+            graph->Dispatch(r.forget());
+          }));
     }
-    RefPtr<GetUserMediaWindowListener> mWindowListener;
-    nsMainThreadPtrHandle<MediaManager::GetUserMediaSuccessCallback> mOnSuccess;
-    RefPtr<MediaManager> mManager;
-    // Keep the DOMMediaStream alive until the NotifyTracksAvailable callback
-    // has fired, otherwise we might immediately destroy the DOMMediaStream and
+    void NotifyRemoved() override {
+      mGraph->Dispatch(NS_NewRunnableFunction(
+          "TracksCreatedListener::NotifyRemoved CycleBreaker",
+          [self = RefPtr<TracksCreatedListener>(this)]() {
+            self->mTrack->RemoveListener(self);
+          }));
+    }
+    const RefPtr<GetUserMediaWindowListener> mWindowListener;
+    const nsMainThreadPtrHandle<MediaManager::GetUserMediaSuccessCallback>
+        mOnSuccess;
+    const RefPtr<MediaManager> mManager;
+    const RefPtr<MediaStreamGraphImpl> mGraph;
+    // Keep the DOMMediaStream alive until the success callback has been called,
+    // otherwise we might immediately destroy the DOMMediaStream and
     // shut down the underlying MediaStream prematurely.
-    // This creates a cycle which is broken when NotifyTracksAvailable
-    // is fired (which will happen unless the browser shuts down,
-    // since we only add this callback when we've successfully appended
-    // the desired tracks in the MediaStreamGraph) or when
-    // DOMMediaStream::NotifyMediaStreamGraphShutdown is called.
-    RefPtr<DOMMediaStream> mStream;
+    // This creates a cycle which is broken when we're destroyed, i.e., either
+    // when we've called the success callback and thus removed the listener from
+    // the graph, or on graph shutdown.
+    nsMainThreadPtrHandle<DOMMediaStream> mStream;
+    nsMainThreadPtrHandle<MediaStreamTrack> mTrack;
+    // Graph thread only.
+    bool mDispatchedTracksCreated = false;
   };
 
   NS_IMETHOD
   Run() override {
     MOZ_ASSERT(NS_IsMainThread());
     LOG(("GetUserMediaStreamRunnable::Run()"));
     nsGlobalWindowInner* globalWindow =
         nsGlobalWindowInner::GetInnerWindowWithId(mWindowID);
@@ -1264,36 +1240,34 @@ class GetUserMediaStreamRunnable : publi
     }
 
     MediaStreamGraph::GraphDriverType graphDriverType =
         mAudioDevice ? MediaStreamGraph::AUDIO_THREAD_DRIVER
                      : MediaStreamGraph::SYSTEM_THREAD_DRIVER;
     MediaStreamGraph* msg = MediaStreamGraph::GetInstance(
         graphDriverType, window, MediaStreamGraph::REQUEST_DEFAULT_SAMPLE_RATE);
 
-    nsMainThreadPtrHandle<DOMMediaStream> domStream;
+    RefPtr<DOMMediaStream> domStream;
     RefPtr<SourceMediaStream> stream;
     // AudioCapture is a special case, here, in the sense that we're not really
     // using the audio source and the SourceMediaStream, which acts as
     // placeholders. We re-route a number of stream internaly in the MSG and mix
     // them down instead.
     if (mAudioDevice &&
         mAudioDevice->GetMediaSource() == MediaSourceEnum::AudioCapture) {
       NS_WARNING(
           "MediaCaptureWindowState doesn't handle "
           "MediaSourceEnum::AudioCapture. This must be fixed with UX "
           "before shipping.");
       // It should be possible to pipe the capture stream to anything. CORS is
       // not a problem here, we got explicit user content.
       nsCOMPtr<nsIPrincipal> principal =
           window->GetExtantDoc()->NodePrincipal();
-      domStream = new nsMainThreadPtrHolder<DOMMediaStream>(
-          "GetUserMediaStreamRunnable::AudioCaptureDOMStreamMainThreadHolder",
-          DOMMediaStream::CreateAudioCaptureStreamAsInput(window, principal,
-                                                          msg));
+      domStream = DOMMediaStream::CreateAudioCaptureStreamAsInput(
+          window, principal, msg);
 
       stream = msg->CreateSourceStream();  // Placeholder
       msg->RegisterCaptureStreamForWindow(
           mWindowID, domStream->GetInputStream()->AsProcessedStream());
       window->SetAudioCapture(true);
     } else {
       class LocalTrackSource : public MediaStreamTrackSource {
        public:
@@ -1415,20 +1389,17 @@ class GetUserMediaStreamRunnable : publi
             window->GetExtantDoc()->NodePrincipal());
       } else {
         principal = window->GetExtantDoc()->NodePrincipal();
       }
 
       // Normal case, connect the source stream to the track union stream to
       // avoid us blocking. Pass a simple TrackSourceGetter for potential
       // fake tracks. Apart from them gUM never adds tracks dynamically.
-      domStream = new nsMainThreadPtrHolder<DOMMediaStream>(
-          "GetUserMediaStreamRunnable::DOMMediaStreamMainThreadHolder",
-          DOMMediaStream::CreateSourceStreamAsInput(
-              window, msg, new FakeTrackSourceGetter(principal)));
+      domStream = DOMMediaStream::CreateSourceStreamAsInput(window, msg);
       stream = domStream->GetInputStream()->AsSourceStream();
 
       if (mAudioDevice) {
         nsString audioDeviceName;
         mAudioDevice->GetName(audioDeviceName);
         const MediaSourceEnum source = mAudioDevice->GetMediaSource();
         RefPtr<MediaStreamTrackSource> audioSource =
             new LocalTrackSource(principal, audioDeviceName, mSourceListener,
@@ -1468,40 +1439,36 @@ class GetUserMediaStreamRunnable : publi
     }
 
     // Activate our source listener. We'll call Start() on the source when we
     // get a callback that the MediaStream has started consuming. The listener
     // is freed when the page is invalidated (on navigation or close).
     mWindowListener->Activate(mSourceListener, stream, mAudioDevice,
                               mVideoDevice);
 
-    // Note: includes JS callbacks; must be released on MainThread
-    typedef Refcountable<UniquePtr<TracksAvailableCallback>> Callback;
-    nsMainThreadPtrHandle<Callback> callback(new nsMainThreadPtrHolder<
-                                             Callback>(
-        "GetUserMediaStreamRunnable::TracksAvailableCallbackMainThreadHolder",
-        MakeAndAddRef<Callback>(new TracksAvailableCallback(
-            mManager, mOnSuccess, mWindowListener, domStream))));
+    nsTArray<RefPtr<MediaStreamTrack>> tracks(2);
+    domStream->GetTracks(tracks);
+    RefPtr<MediaStreamTrack> track = tracks[0];
+    auto tracksCreatedListener = MakeRefPtr<TracksCreatedListener>(
+        mManager, mOnSuccess, mWindowListener, domStream, track);
 
     // Dispatch to the media thread to ask it to start the sources,
     // because that can take a while.
     // Pass ownership of domStream through the lambda to the nested chrome
     // notification lambda to ensure it's kept alive until that lambda runs or
     // is discarded.
     mSourceListener->InitializeAsync()->Then(
         GetMainThreadSerialEventTarget(), __func__,
-        [manager = mManager, domStream, callback,
-         windowListener = mWindowListener]() {
+        [manager = mManager, windowListener = mWindowListener, track,
+         tracksCreatedListener]() {
           LOG(
               ("GetUserMediaStreamRunnable::Run: starting success callback "
                "following InitializeAsync()"));
           // Initiating and starting devices succeeded.
-          // onTracksAvailableCallback must be added to domStream on main
-          // thread.
-          domStream->OnTracksAvailable(callback->release());
+          track->AddListener(tracksCreatedListener);
           windowListener->ChromeAffectingStateChanged();
           manager->SendPendingGUMRequest();
         },
         [manager = mManager, windowID = mWindowID,
          onFailure =
              std::move(mOnFailure)](const RefPtr<MediaMgrError>& error) {
           LOG(
               ("GetUserMediaStreamRunnable::Run: starting failure callback "
@@ -4117,17 +4084,16 @@ bool MediaManager::IsActivelyCapturingOr
     }
   }
   return audio == nsIPermissionManager::ALLOW_ACTION ||
          video == nsIPermissionManager::ALLOW_ACTION;
 }
 
 SourceListener::SourceListener()
     : mStopped(false),
-      mFinished(false),
       mRemoved(false),
       mMainThreadCheck(nullptr),
       mPrincipalHandle(PRINCIPAL_HANDLE_NONE),
       mWindowListener(nullptr) {}
 
 void SourceListener::Register(GetUserMediaWindowListener* aListener) {
   LOG(("SourceListener %p registering with window listener %p", this,
        aListener));
@@ -4148,35 +4114,36 @@ void SourceListener::Activate(SourceMedi
   LOG(("SourceListener %p activating audio=%p video=%p", this, aAudioDevice,
        aVideoDevice));
 
   MOZ_ASSERT(!mStopped, "Cannot activate stopped source listener");
   MOZ_ASSERT(!Activated(), "Already activated");
 
   mMainThreadCheck = GetCurrentVirtualThread();
   mStream = aStream;
-  mStreamListener = new SourceStreamListener(this);
   if (aAudioDevice) {
     mAudioDeviceState = MakeUnique<DeviceState>(
         aAudioDevice,
         aAudioDevice->GetMediaSource() == dom::MediaSourceEnum::Microphone &&
             Preferences::GetBool(
                 "media.getusermedia.microphone.off_while_disabled.enabled",
-                true));
+                true),
+        MakeRefPtr<SourceTrackListener>(this, kAudioTrack));
+    mStream->AddTrackListener(mAudioDeviceState->mListener, kAudioTrack);
   }
 
   if (aVideoDevice) {
     mVideoDeviceState = MakeUnique<DeviceState>(
         aVideoDevice,
         aVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Camera &&
             Preferences::GetBool(
-                "media.getusermedia.camera.off_while_disabled.enabled", true));
+                "media.getusermedia.camera.off_while_disabled.enabled", true),
+        MakeRefPtr<SourceTrackListener>(this, kVideoTrack));
+    mStream->AddTrackListener(mVideoDeviceState->mListener, kVideoTrack);
   }
-
-  mStream->AddListener(mStreamListener);
 }
 
 RefPtr<SourceListener::InitPromise> SourceListener::InitializeAsync() {
   MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
   MOZ_DIAGNOSTIC_ASSERT(!mStopped);
 
   RefPtr<InitPromise> init = MediaManager::PostTask<InitPromise>(
       __func__,
@@ -4299,52 +4266,57 @@ void SourceListener::Stop() {
   MOZ_ASSERT(mStream, "Can't end tracks. No source stream.");
 
   if (mAudioDeviceState && !mAudioDeviceState->mStopped) {
     StopTrack(kAudioTrack);
   }
   if (mVideoDeviceState && !mVideoDeviceState->mStopped) {
     StopTrack(kVideoTrack);
   }
-
-  MediaManager::PostTask(NewTaskFrom([source = mStream]() {
-    MOZ_ASSERT(MediaManager::IsInMediaThread());
-    source->EndAllTrackAndFinish();
-  }));
 }
 
 void SourceListener::Remove() {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (mAudioDeviceState) {
     mAudioDeviceState->mDisableTimer->Cancel();
   }
   if (mVideoDeviceState) {
     mVideoDeviceState->mDisableTimer->Cancel();
   }
 
   if (!mStream || mRemoved) {
     return;
   }
 
-  LOG(("SourceListener %p removed on purpose, mFinished = %d", this,
-       (int)mFinished));
+  LOG(("SourceListener %p removed on purpose", this));
   mRemoved = true;  // RemoveListener is async, avoid races
   mWindowListener = nullptr;
 
   // If it's destroyed, don't call - listener will be removed and we'll be
   // notified!
   if (!mStream->IsDestroyed()) {
     // We disable pulling before removing so we don't risk having live tracks
     // without a listener attached - that wouldn't produce data and would be
     // illegal to the graph.
     mStream->SetPullEnabled(false);
-    mStream->RemoveListener(mStreamListener);
+    if (mAudioDeviceState) {
+      mStream->RemoveTrackListener(mAudioDeviceState->mListener, kAudioTrack);
+    }
+    if (mVideoDeviceState) {
+      mStream->RemoveTrackListener(mVideoDeviceState->mListener, kVideoTrack);
+    }
   }
-  mStreamListener = nullptr;
+
+  if (mAudioDeviceState) {
+    mAudioDeviceState->mListener = nullptr;
+  }
+  if (mVideoDeviceState) {
+    mVideoDeviceState->mListener = nullptr;
+  }
 }
 
 void SourceListener::StopTrack(TrackID aTrackID) {
   MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
   MOZ_ASSERT(Activated(), "No device to stop");
   MOZ_ASSERT(aTrackID == kAudioTrack || aTrackID == kVideoTrack,
              "Unknown track id");
   DeviceState& state = GetDeviceStateFor(aTrackID);
@@ -4587,54 +4559,45 @@ void SourceListener::StopSharing() {
   }
 }
 
 SourceMediaStream* SourceListener::GetSourceStream() {
   NS_ASSERTION(mStream, "Getting stream from never-activated SourceListener");
   return mStream;
 }
 
-// Proxy NotifyPull() to sources
-void SourceListener::NotifyPull(MediaStreamGraph* aGraph,
-                                StreamTime aDesiredTime) {
-  if (mAudioDeviceState) {
-    mAudioDeviceState->mDevice->Pull(mStream, kAudioTrack, aDesiredTime,
-                                     mPrincipalHandle);
-  }
-  if (mVideoDeviceState) {
-    mVideoDeviceState->mDevice->Pull(mStream, kVideoTrack, aDesiredTime,
-                                     mPrincipalHandle);
-  }
+// Proxy Pull() to the right source
+void SourceListener::Pull(TrackID aTrackID, StreamTime aEndOfAppendedData,
+                          StreamTime aDesiredTime) {
+  DeviceState& state = GetDeviceStateFor(aTrackID);
+  state.mDevice->Pull(mStream, aTrackID, aEndOfAppendedData, aDesiredTime,
+                      mPrincipalHandle);
 }
 
-void SourceListener::NotifyFinished() {
+void SourceListener::NotifyRemoved(TrackID aTrackID) {
   MOZ_ASSERT(NS_IsMainThread());
-  mFinished = true;
-  if (!mWindowListener) {
-    // Removed explicitly before finished.
+  LOG(("Track %d for SourceListener %p removed", aTrackID, this));
+
+  StopTrack(aTrackID);
+
+  if (!mStopped) {
+    // There are more live tracks that need to be stopped before removal.
     return;
   }
 
-  LOG(("SourceListener %p NotifyFinished", this));
-
-  Stop();  // we know it's been activated
+  if (!mWindowListener) {
+    // Removed explicitly before MSG's notification.
+    return;
+  }
+
   mWindowListener->Remove(this);
-}
-
-void SourceListener::NotifyRemoved() {
-  MOZ_ASSERT(NS_IsMainThread());
-  LOG(("SourceListener removed, mFinished = %d", (int)mFinished));
-  mRemoved = true;
-
-  if (Activated() && !mFinished) {
-    NotifyFinished();
-  }
-
-  mWindowListener = nullptr;
-  mStreamListener = nullptr;
+
+  MOZ_ASSERT(!mWindowListener);
+  MOZ_ASSERT_IF(mAudioDeviceState, !mAudioDeviceState->mListener);
+  MOZ_ASSERT_IF(mVideoDeviceState, !mVideoDeviceState->mListener);
 }
 
 bool SourceListener::CapturingVideo() const {
   MOZ_ASSERT(NS_IsMainThread());
   return Activated() && mVideoDeviceState && !mVideoDeviceState->mStopped &&
          (!mVideoDeviceState->mDevice->mSource->IsFake() ||
           Preferences::GetBool("media.navigator.permission.fake"));
 }
--- a/dom/media/MediaManager.h
+++ b/dom/media/MediaManager.h
@@ -95,17 +95,18 @@ class MediaDevice : public nsIMediaDevic
   nsresult Reconfigure(const dom::MediaTrackConstraints& aConstraints,
                        const MediaEnginePrefs& aPrefs,
                        const char** aOutBadConstraint);
   nsresult FocusOnSelectedSource();
   nsresult Stop();
   nsresult Deallocate();
 
   void Pull(const RefPtr<SourceMediaStream>& aStream, TrackID aTrackID,
-            StreamTime aDesiredTime, const PrincipalHandle& aPrincipal);
+            StreamTime aEndOfAppendedData, StreamTime aDesiredTime,
+            const PrincipalHandle& aPrincipal);
 
   void GetSettings(dom::MediaTrackSettings& aOutSettings) const;
 
   dom::MediaSourceEnum GetMediaSource() const;
 
  protected:
   virtual ~MediaDevice() = default;
 
--- a/dom/media/MediaRecorder.cpp
+++ b/dom/media/MediaRecorder.cpp
@@ -343,28 +343,16 @@ class MediaRecorder::Session : public Pr
 
       return NS_OK;
     }
 
    private:
     RefPtr<Session> mSession;
   };
 
-  // To ensure that MediaRecorder has tracks to record.
-  class TracksAvailableCallback : public OnTracksAvailableCallback {
-   public:
-    explicit TracksAvailableCallback(Session* aSession) : mSession(aSession) {}
-
-    virtual void NotifyTracksAvailable(DOMMediaStream* aStream) {
-      mSession->MediaStreamReady(aStream);
-    }
-
-   private:
-    RefPtr<Session> mSession;
-  };
   // Main thread task.
   // To delete RecordingSession object.
   class DestroyRunnable : public Runnable {
    public:
     explicit DestroyRunnable(Session* aSession)
         : Runnable("dom::MediaRecorder::Session::DestroyRunnable"),
           mSession(aSession) {}
 
@@ -470,21 +458,21 @@ class MediaRecorder::Session : public Pr
    protected:
     RefPtr<TaskQueue> mEncoderThread;
     RefPtr<Session> mSession;
   };
 
   friend class EncoderErrorNotifierRunnable;
   friend class PushBlobRunnable;
   friend class DestroyRunnable;
-  friend class TracksAvailableCallback;
 
  public:
   Session(MediaRecorder* aRecorder, int32_t aTimeSlice)
       : mRecorder(aRecorder),
+        mMediaStreamReady(false),
         mTimeSlice(aTimeSlice),
         mRunningState(RunningState::Idling) {
     MOZ_ASSERT(NS_IsMainThread());
 
     mMaxMemory = Preferences::GetUint("media.recorder.max_memory",
                                       MAX_ALLOW_MEMORY_BUFFER);
     mLastBlobTimeStamp = TimeStamp::Now();
   }
@@ -496,20 +484,32 @@ class MediaRecorder::Session : public Pr
       DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR);
     }
   }
 
   void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override {
     LOG(LogLevel::Warning,
         ("Session.NotifyTrackAdded %p Raising error due to track set change",
          this));
-    DoSessionEndTask(NS_ERROR_ABORT);
+    if (mMediaStreamReady) {
+      DoSessionEndTask(NS_ERROR_ABORT);
+    }
+
+    NS_DispatchToMainThread(
+        NewRunnableMethod("MediaRecorder::Session::MediaStreamReady", this,
+                          &Session::MediaStreamReady));
+    return;
   }
 
   void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override {
+    if (!mMediaStreamReady) {
+      // We haven't chosen the track set to record yet.
+      return;
+    }
+
     if (aTrack->Ended()) {
       // TrackEncoder will pickup tracks that end itself.
       return;
     }
 
     MOZ_ASSERT(mEncoder);
     if (mEncoder) {
       mEncoder->RemoveMediaStreamTrack(aTrack);
@@ -526,19 +526,24 @@ class MediaRecorder::Session : public Pr
     MOZ_ASSERT(NS_IsMainThread());
 
     DOMMediaStream* domStream = mRecorder->Stream();
     if (domStream) {
       // The callback reports back when tracks are available and can be
       // attached to MediaEncoder. This allows `recorder.start()` before any
       // tracks are available. We have supported this historically and have
       // mochitests assuming this behavior.
-      TracksAvailableCallback* tracksAvailableCallback =
-          new TracksAvailableCallback(this);
-      domStream->OnTracksAvailable(tracksAvailableCallback);
+      mMediaStream = domStream;
+      mMediaStream->RegisterTrackListener(this);
+      nsTArray<RefPtr<MediaStreamTrack>> tracks(2);
+      mMediaStream->GetTracks(tracks);
+      for (const auto& track : tracks) {
+        // Notify of existing tracks, as the stream doesn't do this by itself.
+        NotifyTrackAdded(track);
+      }
       return;
     }
 
     if (mRecorder->mAudioNode) {
       // Check that we may access the audio node's content.
       if (!AudioNodePrincipalSubsumes()) {
         LOG(LogLevel::Warning,
             ("Session.Start AudioNode principal check failed"));
@@ -699,29 +704,33 @@ class MediaRecorder::Session : public Pr
       }
     } else if (aDestroyRunnable) {
       if (NS_FAILED(NS_DispatchToMainThread(aDestroyRunnable))) {
         MOZ_ASSERT(false, "NS_DispatchToMainThread DestroyRunnable failed");
       }
     }
   }
 
-  void MediaStreamReady(DOMMediaStream* aStream) {
-    MOZ_RELEASE_ASSERT(aStream);
+  void MediaStreamReady() {
+    if (!mMediaStream) {
+      // Already shut down. This can happen because MediaStreamReady is async.
+      return;
+    }
+
+    if (mMediaStreamReady) {
+      return;
+    }
 
     if (!mRunningState.isOk() ||
         mRunningState.unwrap() != RunningState::Idling) {
       return;
     }
 
-    mMediaStream = aStream;
-    aStream->RegisterTrackListener(this);
-
     nsTArray<RefPtr<mozilla::dom::MediaStreamTrack>> tracks;
-    aStream->GetTracks(tracks);
+    mMediaStream->GetTracks(tracks);
     uint8_t trackTypes = 0;
     int32_t audioTracks = 0;
     int32_t videoTracks = 0;
     for (auto& track : tracks) {
       if (track->Ended()) {
         continue;
       }
 
@@ -733,44 +742,49 @@ class MediaRecorder::Session : public Pr
       } else if (track->AsVideoStreamTrack()) {
         ++videoTracks;
         trackTypes |= ContainerWriter::CREATE_VIDEO_TRACK;
       } else {
         MOZ_CRASH("Unexpected track type");
       }
     }
 
+    if (trackTypes == 0) {
+      MOZ_ASSERT(audioTracks == 0);
+      MOZ_ASSERT(videoTracks == 0);
+      return;
+    }
+
+    mMediaStreamReady = true;
+
     if (audioTracks > 1 || videoTracks > 1) {
       // When MediaRecorder supports multiple tracks, we should set up a single
       // MediaInputPort from the input stream, and let main thread check
       // track principals async later.
       nsPIDOMWindowInner* window = mRecorder->GetParentObject();
       nsIDocument* document = window ? window->GetExtantDoc() : nullptr;
       nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
                                       NS_LITERAL_CSTRING("Media"), document,
                                       nsContentUtils::eDOM_PROPERTIES,
                                       "MediaRecorderMultiTracksNotSupported");
       DoSessionEndTask(NS_ERROR_ABORT);
       return;
     }
 
-    NS_ASSERTION(trackTypes != 0,
-                 "TracksAvailableCallback without any tracks available");
-
     // Check that we may access the tracks' content.
     if (!MediaStreamTracksPrincipalSubsumes()) {
-      LOG(LogLevel::Warning, ("Session.NotifyTracksAvailable MediaStreamTracks "
+      LOG(LogLevel::Warning, ("Session.MediaTracksReady MediaStreamTracks "
                               "principal check failed"));
       DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR);
       return;
     }
 
     LOG(LogLevel::Debug,
-        ("Session.NotifyTracksAvailable track type = (%d)", trackTypes));
-    InitEncoder(trackTypes, aStream->GraphRate());
+        ("Session.MediaTracksReady track type = (%d)", trackTypes));
+    InitEncoder(trackTypes, mMediaStream->GraphRate());
   }
 
   void ConnectMediaStreamTrack(MediaStreamTrack& aTrack) {
     for (auto& track : mMediaStreamTracks) {
       if (track->AsAudioStreamTrack() && aTrack.AsAudioStreamTrack()) {
         // We only allow one audio track. See bug 1276928.
         return;
       }
@@ -1158,16 +1172,19 @@ class MediaRecorder::Session : public Pr
 
   // Hold reference to MediaRecorder that ensure MediaRecorder is alive
   // if there is an active session. Access ONLY on main thread.
   RefPtr<MediaRecorder> mRecorder;
 
   // Stream currently recorded.
   RefPtr<DOMMediaStream> mMediaStream;
 
+  // True after we have decided on the track set to use for the recording.
+  bool mMediaStreamReady;
+
   // Tracks currently recorded. This should be a subset of mMediaStream's track
   // set.
   nsTArray<RefPtr<MediaStreamTrack>> mMediaStreamTracks;
 
   // Runnable thread for reading data from MediaEncoder.
   RefPtr<TaskQueue> mEncoderThread;
   // MediaEncoder pipeline.
   RefPtr<MediaEncoder> mEncoder;
--- a/dom/media/MediaStreamGraph.cpp
+++ b/dom/media/MediaStreamGraph.cpp
@@ -50,17 +50,16 @@ LazyLogModule gMediaStreamGraphLog("Medi
 #ifdef LOG
 #undef LOG
 #endif  // LOG
 #define LOG(type, msg) MOZ_LOG(gMediaStreamGraphLog, type, msg)
 
 enum SourceMediaStream::TrackCommands : uint32_t {
   TRACK_CREATE = TrackEventCommand::TRACK_EVENT_CREATED,
   TRACK_END = TrackEventCommand::TRACK_EVENT_ENDED,
-  TRACK_UNUSED = TrackEventCommand::TRACK_EVENT_UNUSED,
 };
 
 /**
  * A hash table containing the graph instances, one per document.
  *
  * The key is a hash of nsPIDOMWindowInner, see `WindowToHash`.
  */
 static nsDataHashtable<nsUint32HashKey, MediaStreamGraphImpl*> gGraphs;
@@ -146,66 +145,65 @@ GraphTime MediaStreamGraphImpl::Iteratio
   MOZ_ASSERT(OnGraphThreadOrNotRunning());
   return CurrentDriver()->IterationEnd();
 }
 
 void MediaStreamGraphImpl::UpdateCurrentTimeForStreams(
     GraphTime aPrevCurrentTime) {
   MOZ_ASSERT(OnGraphThread());
   for (MediaStream* stream : AllStreams()) {
-    bool isAnyBlocked = stream->mStartBlocking < mStateComputedTime;
-    bool isAnyUnblocked = stream->mStartBlocking > aPrevCurrentTime;
+    // Shouldn't have already notified of finish *and* have output!
+    MOZ_ASSERT_IF(stream->mStartBlocking > aPrevCurrentTime,
+                  !stream->mNotifiedFinished);
 
     // Calculate blocked time and fire Blocked/Unblocked events
     GraphTime blockedTime = mStateComputedTime - stream->mStartBlocking;
     NS_ASSERTION(blockedTime >= 0, "Error in blocking time");
     stream->AdvanceTimeVaryingValuesToCurrentTime(mStateComputedTime,
                                                   blockedTime);
     LOG(LogLevel::Verbose,
         ("%p: MediaStream %p bufferStartTime=%f blockedTime=%f", this, stream,
          MediaTimeToSeconds(stream->mTracksStartTime),
          MediaTimeToSeconds(blockedTime)));
     stream->mStartBlocking = mStateComputedTime;
 
-    if (isAnyUnblocked && stream->mNotifiedBlocked) {
-      for (uint32_t j = 0; j < stream->mListeners.Length(); ++j) {
-        MediaStreamListener* l = stream->mListeners[j];
-        l->NotifyBlockingChanged(this, MediaStreamListener::UNBLOCKED);
-      }
-      stream->mNotifiedBlocked = false;
-    }
-    if (isAnyBlocked && !stream->mNotifiedBlocked) {
-      for (uint32_t j = 0; j < stream->mListeners.Length(); ++j) {
-        MediaStreamListener* l = stream->mListeners[j];
-        l->NotifyBlockingChanged(this, MediaStreamListener::BLOCKED);
-      }
-      stream->mNotifiedBlocked = true;
-    }
-
-    if (isAnyUnblocked) {
-      NS_ASSERTION(
-          !stream->mNotifiedFinished,
-          "Shouldn't have already notified of finish *and* have output!");
-      for (uint32_t j = 0; j < stream->mListeners.Length(); ++j) {
-        MediaStreamListener* l = stream->mListeners[j];
-        l->NotifyOutput(this, mProcessedTime);
+    for (StreamTracks::TrackIter track(stream->mTracks); !track.IsEnded();
+         track.Next()) {
+      StreamTime streamCurrentTime =
+          stream->GraphTimeToStreamTime(mStateComputedTime);
+      if (track->IsEnded() && track->GetEnd() <= streamCurrentTime) {
+        if (!track->NotifiedEnded()) {
+          // Playout of this track ended and listeners have not been notified.
+          track->NotifyEnded();
+          for (const TrackBound<MediaStreamTrackListener>& listener :
+               stream->mTrackListeners) {
+            if (listener.mTrackID == track->GetID()) {
+              listener.mListener->NotifyOutput(this, track->GetEnd());
+              listener.mListener->NotifyEnded();
+            }
+          }
+        }
+      } else {
+        for (const TrackBound<MediaStreamTrackListener>& listener :
+             stream->mTrackListeners) {
+          if (listener.mTrackID == track->GetID()) {
+            listener.mListener->NotifyOutput(
+                this, streamCurrentTime - track->GetStart());
+          }
+        }
       }
     }
 
     // The stream is fully finished when all of its track data has been played
     // out.
     if (stream->mFinished && !stream->mNotifiedFinished &&
         mProcessedTime >= stream->StreamTimeToGraphTime(
                               stream->GetStreamTracks().GetAllTracksEnd())) {
       stream->mNotifiedFinished = true;
       SetStreamOrderDirty();
-      for (uint32_t j = 0; j < stream->mListeners.Length(); ++j) {
-        MediaStreamListener* l = stream->mListeners[j];
-        l->NotifyEvent(this, MediaStreamGraphEvent::EVENT_FINISHED);
-      }
     }
   }
 }
 
 template <typename C, typename Chunk>
 void MediaStreamGraphImpl::ProcessChunkMetadataForInterval(MediaStream* aStream,
                                                            TrackID aTrackID,
                                                            C& aSegment,
@@ -310,24 +308,32 @@ bool MediaStreamGraphImpl::AudioTrackPre
 
   bool audioTrackPresent = false;
   for (MediaStream* stream : mStreams) {
     if (stream->AsAudioNodeStream()) {
       audioTrackPresent = true;
       break;
     }
 
-    if (!StreamTracks::TrackIter(stream->GetStreamTracks(), MediaSegment::AUDIO)
-             .IsEnded()) {
-      audioTrackPresent = true;
+    for (StreamTracks::TrackIter it(stream->GetStreamTracks()); !it.IsEnded();
+         it.Next()) {
+      if (it->GetType() == MediaSegment::AUDIO && !it->NotifiedEnded()) {
+        audioTrackPresent = true;
+        break;
+      }
+    }
+
+    if (audioTrackPresent) {
       break;
     }
 
     if (SourceMediaStream* source = stream->AsSourceStream()) {
-      audioTrackPresent = source->HasPendingAudioTrack();
+      if (source->HasPendingAudioTrack()) {
+        audioTrackPresent = true;
+      }
     }
 
     if (audioTrackPresent) {
       break;
     }
   }
 
   // XXX For some reason, there are race conditions when starting an audio input
@@ -558,26 +564,16 @@ void MediaStreamGraphImpl::UpdateStreamO
         ++orderedStreamCount;
       }
     }
   }
 
   MOZ_ASSERT(orderedStreamCount == mFirstCycleBreaker);
 }
 
-void MediaStreamGraphImpl::NotifyHasCurrentData(MediaStream* aStream) {
-  if (!aStream->mNotifiedHasCurrentData && aStream->mHasCurrentData) {
-    for (uint32_t j = 0; j < aStream->mListeners.Length(); ++j) {
-      MediaStreamListener* l = aStream->mListeners[j];
-      l->NotifyHasCurrentData(this);
-    }
-    aStream->mNotifiedHasCurrentData = true;
-  }
-}
-
 void MediaStreamGraphImpl::CreateOrDestroyAudioStreams(MediaStream* aStream) {
   MOZ_ASSERT(OnGraphThread());
   MOZ_ASSERT(mRealtime,
              "Should only attempt to create audio streams in real-time mode");
 
   if (aStream->mAudioOutputs.IsEmpty()) {
     aStream->mAudioOutputStreams.Clear();
     return;
@@ -1088,16 +1084,17 @@ void MediaStreamGraphImpl::PrepareUpdate
       StreamUpdate* update = mStreamUpdates.AppendElement();
       update->mStream = stream;
       // No blocking to worry about here, since we've passed
       // UpdateCurrentTimeForStreams.
       update->mNextMainThreadCurrentTime =
           stream->GraphTimeToStreamTime(mProcessedTime);
       update->mNextMainThreadFinished = stream->mNotifiedFinished;
     }
+    mNextMainThreadGraphTime = mProcessedTime;
     if (!mPendingUpdateRunnables.IsEmpty()) {
       mUpdateRunnables.AppendElements(std::move(mPendingUpdateRunnables));
     }
   }
 
   // If this is the final update, then a stable state event will soon be
   // posted just before this thread finishes, and so there is no need to also
   // post here.
@@ -1192,17 +1189,17 @@ void MediaStreamGraphImpl::UpdateGraph(G
 
   UpdateStreamOrder();
 
   bool ensureNextIteration = false;
 
   for (MediaStream* stream : mStreams) {
     if (SourceMediaStream* is = stream->AsSourceStream()) {
       ensureNextIteration |= is->PullNewData(aEndBlockingDecisions);
-      is->ExtractPendingInput();
+      is->ExtractPendingInput(mStateComputedTime);
     }
     if (stream->mFinished) {
       // The stream's not suspended, and since it's finished, underruns won't
       // stop it playing out. So there's no blocking other than what we impose
       // here.
       GraphTime endTime = stream->GetStreamTracks().GetAllTracksEnd() +
                           stream->mTracksStartTime;
       if (endTime <= mStateComputedTime) {
@@ -1230,20 +1227,22 @@ void MediaStreamGraphImpl::UpdateGraph(G
           if (i->IsEnded()) {
             continue;
           }
           if (i->GetEnd() <
               stream->GraphTimeToStreamTime(aEndBlockingDecisions)) {
             LOG(LogLevel::Error,
                 ("%p: SourceMediaStream %p track %u (%s) is live and pulled, "
                  "but wasn't fed "
-                 "enough data. Listeners=%zu. Track-end=%f, Iteration-end=%f",
+                 "enough data. TrackListeners=%zu. Track-end=%f, "
+                 "Iteration-end=%f",
                  this, stream, i->GetID(),
                  (i->GetType() == MediaSegment::AUDIO ? "audio" : "video"),
-                 stream->mListeners.Length(), MediaTimeToSeconds(i->GetEnd()),
+                 stream->mTrackListeners.Length(),
+                 MediaTimeToSeconds(i->GetEnd()),
                  MediaTimeToSeconds(
                      stream->GraphTimeToStreamTime(aEndBlockingDecisions))));
             MOZ_DIAGNOSTIC_ASSERT(false,
                                   "A non-finished SourceMediaStream wasn't fed "
                                   "enough data by NotifyPull");
           }
         }
       }
@@ -1309,17 +1308,16 @@ void MediaStreamGraphImpl::Process() {
                            ProcessedMediaStream::ALLOW_FINISH);
           NS_ASSERTION(
               stream->mTracks.GetEnd() >=
                   GraphTimeToStreamTimeWithBlocking(stream, mStateComputedTime),
               "Stream did not produce enough data");
         }
       }
     }
-    NotifyHasCurrentData(stream);
     // Only playback audio and video in real-time mode
     if (mRealtime) {
       CreateOrDestroyAudioStreams(stream);
       if (CurrentDriver()->AsAudioCallbackDriver()) {
         StreamTime ticksPlayedForThisStream = PlayAudio(stream);
         if (!ticksPlayed) {
           ticksPlayed = ticksPlayedForThisStream;
         } else {
@@ -1529,16 +1527,19 @@ class MediaStreamGraphShutDownRunnable :
       if (SourceMediaStream* source = stream->AsSourceStream()) {
         // Finishing a SourceStream prevents new data from being appended.
         source->FinishOnGraphThread();
       }
       stream->GetStreamTracks().Clear();
       stream->RemoveAllListenersImpl();
     }
 
+    MOZ_ASSERT(mGraph->mUpdateRunnables.IsEmpty());
+    mGraph->mPendingUpdateRunnables.Clear();
+
     mGraph->mForceShutdownTicket = nullptr;
 
     // We can't block past the final LIFECYCLE_WAITING_FOR_STREAM_DESTRUCTION
     // stage, since completion of that stage requires all streams to be freed,
     // which requires shutdown to proceed.
 
     if (mGraph->IsEmpty()) {
       // mGraph is no longer needed, so delete it.
@@ -1631,16 +1632,18 @@ void MediaStreamGraphImpl::RunInStableSt
     for (uint32_t i = 0; i < mStreamUpdates.Length(); ++i) {
       StreamUpdate* update = &mStreamUpdates[i];
       if (update->mStream) {
         ApplyStreamUpdate(update);
       }
     }
     mStreamUpdates.Clear();
 
+    mMainThreadGraphTime = mNextMainThreadGraphTime;
+
     if (mCurrentTaskMessageQueue.IsEmpty()) {
       if (LifecycleStateRef() == LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP &&
           IsEmpty()) {
         // Complete shutdown. First, ensure that this graph is no longer used.
         // A new graph graph will be created if one is needed.
         // Asynchronously clean up old graph. We don't want to do this
         // synchronously because it spins the event loop waiting for threads
         // to shut down, and we don't want to do that in a stable state handler.
@@ -1835,19 +1838,17 @@ void MediaStreamGraphImpl::Dispatch(alre
 }
 
 MediaStream::MediaStream()
     : mTracksStartTime(0),
       mStartBlocking(GRAPH_TIME_MAX),
       mSuspendedCount(0),
       mFinished(false),
       mNotifiedFinished(false),
-      mNotifiedBlocked(false),
       mHasCurrentData(false),
-      mNotifiedHasCurrentData(false),
       mMainThreadCurrentTime(0),
       mMainThreadFinished(false),
       mFinishedNotificationSent(false),
       mMainThreadDestroyed(false),
       mNrOfMainThreadUsers(0),
       mGraph(nullptr) {
   MOZ_COUNT_CTOR(MediaStream);
 }
@@ -1863,23 +1864,23 @@ size_t MediaStream::SizeOfExcludingThis(
   size_t amount = 0;
 
   // Not owned:
   // - mGraph - Not reported here
   // - mConsumers - elements
   // Future:
   // - mVideoOutputs - elements
   // - mLastPlayedVideoFrame
-  // - mListeners - elements
+  // - mTrackListeners - elements
   // - mAudioOutputStream - elements
 
   amount += mTracks.SizeOfExcludingThis(aMallocSizeOf);
   amount += mAudioOutputs.ShallowSizeOfExcludingThis(aMallocSizeOf);
   amount += mVideoOutputs.ShallowSizeOfExcludingThis(aMallocSizeOf);
-  amount += mListeners.ShallowSizeOfExcludingThis(aMallocSizeOf);
+  amount += mTrackListeners.ShallowSizeOfExcludingThis(aMallocSizeOf);
   amount += mMainThreadListeners.ShallowSizeOfExcludingThis(aMallocSizeOf);
   amount += mDisabledTracks.ShallowSizeOfExcludingThis(aMallocSizeOf);
   amount += mConsumers.ShallowSizeOfExcludingThis(aMallocSizeOf);
 
   return amount;
 }
 
 size_t MediaStream::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
@@ -1972,40 +1973,24 @@ void MediaStream::FinishOnGraphThread() 
 
 StreamTracks::Track* MediaStream::FindTrack(TrackID aID) const {
   return mTracks.FindTrack(aID);
 }
 
 StreamTracks::Track* MediaStream::EnsureTrack(TrackID aTrackId) {
   StreamTracks::Track* track = mTracks.FindTrack(aTrackId);
   if (!track) {
-    nsAutoPtr<MediaSegment> segment(new AudioSegment());
-    for (uint32_t j = 0; j < mListeners.Length(); ++j) {
-      MediaStreamListener* l = mListeners[j];
-      l->NotifyQueuedTrackChanges(Graph(), aTrackId, 0,
-                                  TrackEventCommand::TRACK_EVENT_CREATED,
-                                  *segment);
-      // TODO If we ever need to ensure several tracks at once, we will have to
-      // change this.
-      l->NotifyFinishedTrackCreation(Graph());
-    }
-    track = &mTracks.AddTrack(aTrackId, 0, segment.forget());
+    track = &mTracks.AddTrack(aTrackId, 0, new AudioSegment());
   }
   return track;
 }
 
 void MediaStream::RemoveAllListenersImpl() {
   GraphImpl()->AssertOnGraphThreadOrNotRunning();
 
-  auto streamListeners(mListeners);
-  for (auto& l : streamListeners) {
-    l->NotifyEvent(GraphImpl(), MediaStreamGraphEvent::EVENT_REMOVED);
-  }
-  mListeners.Clear();
-
   auto trackListeners(mTrackListeners);
   for (auto& l : trackListeners) {
     l.mListener->NotifyRemoved();
   }
   mTrackListeners.Clear();
 
   RemoveAllDirectListenersImpl();
 
@@ -2221,93 +2206,16 @@ void MediaStream::Resume() {
   // This can happen if this method has been called asynchronously, and the
   // stream has been destroyed since then.
   if (mMainThreadDestroyed) {
     return;
   }
   GraphImpl()->AppendMessage(MakeUnique<Message>(this));
 }
 
-void MediaStream::AddListenerImpl(
-    already_AddRefed<MediaStreamListener> aListener) {
-  MediaStreamListener* listener = *mListeners.AppendElement() = aListener;
-  listener->NotifyBlockingChanged(
-      GraphImpl(), mNotifiedBlocked ? MediaStreamListener::BLOCKED
-                                    : MediaStreamListener::UNBLOCKED);
-
-  for (StreamTracks::TrackIter it(mTracks); !it.IsEnded(); it.Next()) {
-    MediaStream* inputStream = nullptr;
-    TrackID inputTrackID = TRACK_INVALID;
-    if (ProcessedMediaStream* ps = AsProcessedStream()) {
-      // The only ProcessedMediaStream where we should have listeners is
-      // TrackUnionStream - it's what's used as owned stream in DOMMediaStream,
-      // the only main-thread exposed stream type.
-      // TrackUnionStream guarantees that each of its tracks has an input track.
-      // Other types do not implement GetInputStreamFor() and will return null.
-      inputStream = ps->GetInputStreamFor(it->GetID());
-      if (!inputStream && it->IsEnded()) {
-        // If this track has no input anymore we assume there's no data for the
-        // current iteration either and thus no need to expose it to a listener.
-        continue;
-      }
-      MOZ_ASSERT(inputStream);
-      inputTrackID = ps->GetInputTrackIDFor(it->GetID());
-      MOZ_ASSERT(IsTrackIDExplicit(inputTrackID));
-    }
-
-    uint32_t flags = TrackEventCommand::TRACK_EVENT_CREATED;
-    if (it->IsEnded()) {
-      flags |= TrackEventCommand::TRACK_EVENT_ENDED;
-    }
-    nsAutoPtr<MediaSegment> segment(it->GetSegment()->CreateEmptyClone());
-    listener->NotifyQueuedTrackChanges(Graph(), it->GetID(), it->GetEnd(),
-                                       static_cast<TrackEventCommand>(flags),
-                                       *segment, inputStream, inputTrackID);
-  }
-  if (mNotifiedFinished) {
-    listener->NotifyEvent(GraphImpl(), MediaStreamGraphEvent::EVENT_FINISHED);
-  }
-  if (mNotifiedHasCurrentData) {
-    listener->NotifyHasCurrentData(GraphImpl());
-  }
-}
-
-void MediaStream::AddListener(MediaStreamListener* aListener) {
-  class Message : public ControlMessage {
-   public:
-    Message(MediaStream* aStream, MediaStreamListener* aListener)
-        : ControlMessage(aStream), mListener(aListener) {}
-    void Run() override { mStream->AddListenerImpl(mListener.forget()); }
-    RefPtr<MediaStreamListener> mListener;
-  };
-  GraphImpl()->AppendMessage(MakeUnique<Message>(this, aListener));
-}
-
-void MediaStream::RemoveListenerImpl(MediaStreamListener* aListener) {
-  // wouldn't need this if we could do it in the opposite order
-  RefPtr<MediaStreamListener> listener(aListener);
-  mListeners.RemoveElement(aListener);
-  listener->NotifyEvent(GraphImpl(), MediaStreamGraphEvent::EVENT_REMOVED);
-}
-
-void MediaStream::RemoveListener(MediaStreamListener* aListener) {
-  class Message : public ControlMessage {
-   public:
-    Message(MediaStream* aStream, MediaStreamListener* aListener)
-        : ControlMessage(aStream), mListener(aListener) {}
-    void Run() override { mStream->RemoveListenerImpl(mListener); }
-    RefPtr<MediaStreamListener> mListener;
-  };
-  // If the stream is destroyed the Listeners have or will be
-  // removed.
-  if (!IsDestroyed()) {
-    GraphImpl()->AppendMessage(MakeUnique<Message>(this, aListener));
-  }
-}
-
 void MediaStream::AddTrackListenerImpl(
     already_AddRefed<MediaStreamTrackListener> aListener, TrackID aTrackID) {
   TrackBound<MediaStreamTrackListener>* l = mTrackListeners.AppendElement();
   l->mListener = aListener;
   l->mTrackID = aTrackID;
 
   StreamTracks::Track* track = FindTrack(aTrackID);
   if (!track) {
@@ -2600,186 +2508,143 @@ void SourceMediaStream::SetPullEnabled(b
       mStream->mPullEnabled = mEnabled;
     }
     SourceMediaStream* mStream;
     bool mEnabled;
   };
   GraphImpl()->AppendMessage(MakeUnique<Message>(this, aEnabled));
 }
 
-bool SourceMediaStream::PullNewData(StreamTime aDesiredUpToTime) {
+bool SourceMediaStream::PullNewData(GraphTime aDesiredUpToTime) {
   TRACE_AUDIO_CALLBACK_COMMENT("SourceMediaStream %p", this);
   MutexAutoLock lock(mMutex);
   if (!mPullEnabled || mFinished) {
     return false;
   }
   // Compute how much stream time we'll need assuming we don't block
   // the stream at all.
   StreamTime t = GraphTimeToStreamTime(aDesiredUpToTime);
   StreamTime current = mTracks.GetEnd();
   LOG(LogLevel::Verbose,
       ("%p: Calling NotifyPull aStream=%p t=%f current end=%f", GraphImpl(),
        this, GraphImpl()->MediaTimeToSeconds(t),
        GraphImpl()->MediaTimeToSeconds(current)));
   if (t <= current) {
     return false;
   }
-  for (uint32_t j = 0; j < mListeners.Length(); ++j) {
-    MediaStreamListener* l = mListeners[j];
-    {
-      MutexAutoUnlock unlock(mMutex);
-      l->NotifyPull(GraphImpl(), t);
+  for (const TrackData& track : mUpdateTracks) {
+    if (track.mCommands & TrackEventCommand::TRACK_EVENT_ENDED) {
+      continue;
+    }
+    current = track.mEndOfFlushedData + track.mData->GetDuration();
+    if (t <= current) {
+      continue;
+    }
+    MutexAutoUnlock unlock(mMutex);
+    for (TrackBound<MediaStreamTrackListener>& l : mTrackListeners) {
+      if (l.mTrackID == track.mID) {
+        l.mListener->NotifyPull(Graph(), current, t);
+      }
     }
   }
   return true;
 }
 
-void SourceMediaStream::ExtractPendingInput() {
+void SourceMediaStream::ExtractPendingInput(GraphTime aCurrentTime) {
   MutexAutoLock lock(mMutex);
 
   bool finished = mFinishPending;
-  bool shouldNotifyTrackCreated = false;
+  StreamTime streamCurrentTime = GraphTimeToStreamTime(aCurrentTime);
 
   for (int32_t i = mUpdateTracks.Length() - 1; i >= 0; --i) {
     SourceMediaStream::TrackData* data = &mUpdateTracks[i];
     ApplyTrackDisabling(data->mID, data->mData);
     // Dealing with NotifyQueuedTrackChanges and NotifyQueuedAudioData part.
 
     // The logic is different from the manipulating of aStream->mTracks part.
     // So it is not combined with the manipulating of aStream->mTracks part.
     StreamTime offset =
         (data->mCommands & SourceMediaStream::TRACK_CREATE)
-            ? data->mStart
+            ? streamCurrentTime
             : mTracks.FindTrack(data->mID)->GetSegment()->GetDuration();
 
-    // Audio case.
-    if (data->mData->GetType() == MediaSegment::AUDIO) {
-      if (data->mCommands) {
-        MOZ_ASSERT(!(data->mCommands & SourceMediaStream::TRACK_UNUSED));
-        for (MediaStreamListener* l : mListeners) {
-          if (data->mCommands & SourceMediaStream::TRACK_END) {
-            l->NotifyQueuedAudioData(
-                GraphImpl(), data->mID, offset,
-                *(static_cast<AudioSegment*>(data->mData.get())));
-          }
-          l->NotifyQueuedTrackChanges(
-              GraphImpl(), data->mID, offset,
-              static_cast<TrackEventCommand>(data->mCommands), *data->mData);
-          if (data->mCommands & SourceMediaStream::TRACK_CREATE) {
-            l->NotifyQueuedAudioData(
-                GraphImpl(), data->mID, offset,
-                *(static_cast<AudioSegment*>(data->mData.get())));
-          }
-        }
-      } else {
-        for (MediaStreamListener* l : mListeners) {
-          l->NotifyQueuedAudioData(
-              GraphImpl(), data->mID, offset,
-              *(static_cast<AudioSegment*>(data->mData.get())));
-        }
-      }
-    }
-
-    // Video case.
-    if (data->mData->GetType() == MediaSegment::VIDEO) {
-      if (data->mCommands) {
-        MOZ_ASSERT(!(data->mCommands & SourceMediaStream::TRACK_UNUSED));
-        for (MediaStreamListener* l : mListeners) {
-          l->NotifyQueuedTrackChanges(
-              GraphImpl(), data->mID, offset,
-              static_cast<TrackEventCommand>(data->mCommands), *data->mData);
-        }
-      }
-    }
-
     for (TrackBound<MediaStreamTrackListener>& b : mTrackListeners) {
       if (b.mTrackID != data->mID) {
         continue;
       }
       b.mListener->NotifyQueuedChanges(GraphImpl(), offset, *data->mData);
-      if (data->mCommands & SourceMediaStream::TRACK_END) {
-        b.mListener->NotifyEnded();
-      }
     }
     if (data->mCommands & SourceMediaStream::TRACK_CREATE) {
       MediaSegment* segment = data->mData.forget();
       LOG(LogLevel::Debug,
           ("%p: SourceMediaStream %p creating track %d, start %" PRId64
            ", initial end %" PRId64,
-           GraphImpl(), this, data->mID, int64_t(data->mStart),
+           GraphImpl(), this, data->mID, int64_t(streamCurrentTime),
            int64_t(segment->GetDuration())));
 
       data->mEndOfFlushedData += segment->GetDuration();
-      mTracks.AddTrack(data->mID, data->mStart, segment);
+      segment->InsertNullDataAtStart(streamCurrentTime);
+      mTracks.AddTrack(data->mID, streamCurrentTime, segment);
       // The track has taken ownership of data->mData, so let's replace
       // data->mData with an empty clone.
       data->mData = segment->CreateEmptyClone();
       data->mCommands &= ~SourceMediaStream::TRACK_CREATE;
-      shouldNotifyTrackCreated = true;
     } else if (data->mData->GetDuration() > 0) {
       MediaSegment* dest = mTracks.FindTrack(data->mID)->GetSegment();
       LOG(LogLevel::Verbose,
           ("%p: SourceMediaStream %p track %d, advancing end from %" PRId64
            " to %" PRId64,
            GraphImpl(), this, data->mID, int64_t(dest->GetDuration()),
            int64_t(dest->GetDuration() + data->mData->GetDuration())));
       data->mEndOfFlushedData += data->mData->GetDuration();
       dest->AppendFrom(data->mData);
     }
     if (data->mCommands & SourceMediaStream::TRACK_END) {
       mTracks.FindTrack(data->mID)->SetEnded();
       mUpdateTracks.RemoveElementAt(i);
     }
   }
-  if (shouldNotifyTrackCreated) {
-    for (MediaStreamListener* l : mListeners) {
-      l->NotifyFinishedTrackCreation(GraphImpl());
-    }
-  }
   if (!mFinished) {
     mTracks.AdvanceKnownTracksTime(mUpdateKnownTracksTime);
   }
 
   if (mTracks.GetEnd() > 0) {
     mHasCurrentData = true;
   }
 
   if (finished) {
     FinishOnGraphThread();
   }
 }
 
 void SourceMediaStream::AddTrackInternal(TrackID aID, TrackRate aRate,
-                                         StreamTime aStart,
                                          MediaSegment* aSegment,
                                          uint32_t aFlags) {
   MutexAutoLock lock(mMutex);
   nsTArray<TrackData>* track_data =
       (aFlags & ADDTRACK_QUEUED) ? &mPendingTracks : &mUpdateTracks;
   TrackData* data = track_data->AppendElement();
   LOG(LogLevel::Debug,
       ("%p: AddTrackInternal: %lu/%lu", GraphImpl(),
        (long)mPendingTracks.Length(), (long)mUpdateTracks.Length()));
   data->mID = aID;
   data->mInputRate = aRate;
   data->mResamplerChannelCount = 0;
-  data->mStart = aStart;
-  data->mEndOfFlushedData = aStart;
+  data->mEndOfFlushedData = 0;
   data->mCommands = TRACK_CREATE;
   data->mData = aSegment;
   ResampleAudioToGraphSampleRate(data, aSegment);
   if (!(aFlags & ADDTRACK_QUEUED) && GraphImpl()) {
     GraphImpl()->EnsureNextIteration();
   }
 }
 
 void SourceMediaStream::AddAudioTrack(TrackID aID, TrackRate aRate,
-                                      StreamTime aStart, AudioSegment* aSegment,
-                                      uint32_t aFlags) {
-  AddTrackInternal(aID, aRate, aStart, aSegment, aFlags);
+                                      AudioSegment* aSegment, uint32_t aFlags) {
+  AddTrackInternal(aID, aRate, aSegment, aFlags);
 }
 
 void SourceMediaStream::FinishAddTracks() {
   MutexAutoLock lock(mMutex);
   mUpdateTracks.AppendElements(std::move(mPendingTracks));
   LOG(LogLevel::Debug,
       ("%p: FinishAddTracks: %lu/%lu", GraphImpl(),
        (long)mPendingTracks.Length(), (long)mUpdateTracks.Length()));
@@ -2817,21 +2682,21 @@ void SourceMediaStream::AdvanceTimeVaryi
     GraphTime aCurrentTime, GraphTime aBlockedTime) {
   MutexAutoLock lock(mMutex);
   mTracksStartTime += aBlockedTime;
   mStreamTracksStartTimeStamp +=
       TimeDuration::FromSeconds(GraphImpl()->MediaTimeToSeconds(aBlockedTime));
   mTracks.ForgetUpTo(aCurrentTime - mTracksStartTime);
 }
 
-bool SourceMediaStream::AppendToTrack(TrackID aID, MediaSegment* aSegment,
-                                      MediaSegment* aRawSegment) {
+StreamTime SourceMediaStream::AppendToTrack(TrackID aID, MediaSegment* aSegment,
+                                            MediaSegment* aRawSegment) {
   MutexAutoLock lock(mMutex);
   // ::EndAllTrackAndFinished() can end these before the sources notice
-  bool appended = false;
+  StreamTime appended = 0;
   auto graph = GraphImpl();
   if (!mFinished && graph) {
     TrackData* track = FindDataForTrack(aID);
     if (track) {
       // Data goes into mData, and on the next iteration of the MSG moves
       // into the track's segment after NotifyQueuedTrackChanges().  This adds
       // 0-10ms of delay before data gets to direct listeners.
       // Indirect listeners (via subsequent TrackUnion nodes) are synced to
@@ -2840,18 +2705,18 @@ bool SourceMediaStream::AppendToTrack(Tr
       // Apply track disabling before notifying any consumers directly
       // or inserting into the graph
       ApplyTrackDisabling(aID, aSegment, aRawSegment);
 
       ResampleAudioToGraphSampleRate(track, aSegment);
 
       // Must notify first, since AppendFrom() will empty out aSegment
       NotifyDirectConsumers(track, aRawSegment ? aRawSegment : aSegment);
+      appended = aSegment->GetDuration();
       track->mData->AppendFrom(aSegment);  // note: aSegment is now dead
-      appended = true;
       GraphImpl()->EnsureNextIteration();
     } else {
       aSegment->Clear();
     }
   }
   return appended;
 }
 
@@ -2867,37 +2732,16 @@ void SourceMediaStream::NotifyDirectCons
     }
     StreamTime offset = 0;  // FIX! need a separate StreamTime.... or the end of
                             // the internal buffer
     source.mListener->NotifyRealtimeTrackDataAndApplyTrackDisabling(
         Graph(), offset, *aSegment);
   }
 }
 
-// These handle notifying all the listeners of an event
-void SourceMediaStream::NotifyListenersEventImpl(MediaStreamGraphEvent aEvent) {
-  for (uint32_t j = 0; j < mListeners.Length(); ++j) {
-    MediaStreamListener* l = mListeners[j];
-    l->NotifyEvent(GraphImpl(), aEvent);
-  }
-}
-
-void SourceMediaStream::NotifyListenersEvent(MediaStreamGraphEvent aNewEvent) {
-  class Message : public ControlMessage {
-   public:
-    Message(SourceMediaStream* aStream, MediaStreamGraphEvent aEvent)
-        : ControlMessage(aStream), mEvent(aEvent) {}
-    void Run() override {
-      mStream->AsSourceStream()->NotifyListenersEventImpl(mEvent);
-    }
-    MediaStreamGraphEvent mEvent;
-  };
-  GraphImpl()->AppendMessage(MakeUnique<Message>(this, aNewEvent));
-}
-
 void SourceMediaStream::AddDirectTrackListenerImpl(
     already_AddRefed<DirectMediaStreamTrackListener> aListener,
     TrackID aTrackID) {
   MOZ_ASSERT(IsTrackIDExplicit(aTrackID));
   MutexAutoLock lock(mMutex);
 
   RefPtr<DirectMediaStreamTrackListener> listener = aListener;
   LOG(LogLevel::Debug, ("%p: Adding direct track listener %p bound to track %d "
@@ -3290,17 +3134,18 @@ MediaStreamGraphImpl::MediaStreamGraphIm
       mAbstractMainThread(aMainThread),
       mSelfRef(this),
       mOutputChannels(std::min<uint32_t>(8, CubebUtils::MaxNumberOfChannels())),
       mGlobalVolume(CubebUtils::GetVolumeScale())
 #ifdef DEBUG
       ,
       mCanRunMessagesSynchronously(false)
 #endif
-{
+      ,
+      mMainThreadGraphTime(0, "MediaStreamGraphImpl::mMainThreadGraphTime") {
   if (mRealtime) {
     if (aDriverRequested == AUDIO_THREAD_DRIVER) {
       // Always start with zero input channels.
       mDriver = new AudioCallbackDriver(this, 0);
     } else {
       mDriver = new SystemClockDriver(this);
     }
 
@@ -3957,13 +3802,19 @@ already_AddRefed<MediaInputPort> MediaSt
     }
   }
   return nullptr;
 }
 
 void MediaStreamGraph::DispatchToMainThreadAfterStreamStateUpdate(
     already_AddRefed<nsIRunnable> aRunnable) {
   AssertOnGraphThreadOrNotRunning();
-  *mPendingUpdateRunnables.AppendElement() =
+  *static_cast<MediaStreamGraphImpl*>(this)
+       ->mPendingUpdateRunnables.AppendElement() =
       AbstractMainThread()->CreateDirectTaskDrainer(std::move(aRunnable));
 }
 
+Watchable<mozilla::GraphTime>& MediaStreamGraphImpl::CurrentTime() {
+  MOZ_ASSERT(NS_IsMainThread());
+  return mMainThreadGraphTime;
+}
+
 }  // namespace mozilla
--- a/dom/media/MediaStreamGraph.h
+++ b/dom/media/MediaStreamGraph.h
@@ -9,16 +9,17 @@
 #include "AudioStream.h"
 #include "MainThreadUtils.h"
 #include "MediaStreamTypes.h"
 #include "StreamTracks.h"
 #include "VideoSegment.h"
 #include "mozilla/LinkedList.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/Mutex.h"
+#include "mozilla/StateWatching.h"
 #include "mozilla/TaskQueue.h"
 #include "nsAutoPtr.h"
 #include "nsAutoRef.h"
 #include "nsIRunnable.h"
 #include "nsTArray.h"
 #include <speex/speex_resampler.h>
 
 class nsIRunnable;
@@ -175,17 +176,16 @@ struct AudioNodeSizes {
 
 class AudioNodeEngine;
 class AudioNodeExternalInputStream;
 class AudioNodeStream;
 class AudioSegment;
 class DirectMediaStreamTrackListener;
 class MediaInputPort;
 class MediaStreamGraphImpl;
-class MediaStreamListener;
 class MediaStreamTrackListener;
 class MediaStreamVideoSink;
 class ProcessedMediaStream;
 class SourceMediaStream;
 class TrackUnionStream;
 
 /**
  * Helper struct for binding a track listener to a specific TrackID.
@@ -314,18 +314,16 @@ class MediaStream : public mozilla::Link
   // Explicitly suspend. Useful for example if a media element is pausing
   // and we need to stop its stream emitting its buffered data. As soon as the
   // Suspend message reaches the graph, the stream stops processing. It
   // ignores its inputs and produces silence/no video until Resumed. Its
   // current time does not advance.
   virtual void Suspend();
   virtual void Resume();
   // Events will be dispatched by calling methods of aListener.
-  virtual void AddListener(MediaStreamListener* aListener);
-  virtual void RemoveListener(MediaStreamListener* aListener);
   virtual void AddTrackListener(MediaStreamTrackListener* aListener,
                                 TrackID aTrackID);
   virtual void RemoveTrackListener(MediaStreamTrackListener* aListener,
                                    TrackID aTrackID);
 
   /**
    * Adds aListener to the source stream of track aTrackID in this stream.
    * When the MediaStreamGraph processes the added listener, it will traverse
@@ -427,18 +425,16 @@ class MediaStream : public mozilla::Link
   void DumpTrackInfo() const { return mTracks.DumpTrackInfo(); }
 #endif
   void SetAudioOutputVolumeImpl(void* aKey, float aVolume);
   void AddAudioOutputImpl(void* aKey);
   void RemoveAudioOutputImpl(void* aKey);
   void AddVideoOutputImpl(already_AddRefed<MediaStreamVideoSink> aSink,
                           TrackID aID);
   void RemoveVideoOutputImpl(MediaStreamVideoSink* aSink, TrackID aID);
-  void AddListenerImpl(already_AddRefed<MediaStreamListener> aListener);
-  void RemoveListenerImpl(MediaStreamListener* aListener);
 
   /**
    * Removes all direct listeners and signals to them that they have been
    * uninstalled.
    */
   virtual void RemoveAllDirectListenersImpl() {}
   void RemoveAllListenersImpl();
   virtual void AddTrackListenerImpl(
@@ -575,17 +571,16 @@ class MediaStream : public mozilla::Link
     void* mKey;
     float mVolume;
   };
   nsTArray<AudioOutput> mAudioOutputs;
   nsTArray<TrackBound<MediaStreamVideoSink>> mVideoOutputs;
   // We record the last played video frame to avoid playing the frame again
   // with a different frame id.
   VideoFrame mLastPlayedVideoFrame;
-  nsTArray<RefPtr<MediaStreamListener>> mListeners;
   nsTArray<TrackBound<MediaStreamTrackListener>> mTrackListeners;
   nsTArray<MainThreadMediaStreamListener*> mMainThreadListeners;
   // List of disabled TrackIDs and their associated disabled mode.
   // They can either by disabled by frames being replaced by black, or by
   // retaining the previous frame.
   nsTArray<DisabledTrack> mDisabledTracks;
 
   // GraphTime at which this stream starts blocking.
@@ -625,31 +620,22 @@ class MediaStream : public mozilla::Link
    */
   bool mFinished;
   /**
    * When true, mFinished is true and we've played all the data in this stream
    * and fired NotifyFinished notifications.
    */
   bool mNotifiedFinished;
   /**
-   * When true, the last NotifyBlockingChanged delivered to the listeners
-   * indicated that the stream is blocked.
-   */
-  bool mNotifiedBlocked;
-  /**
    * True if some data can be present by this stream if/when it's unblocked.
    * Set by the stream itself on the MediaStreamGraph thread. Only changes
    * from false to true once a stream has data, since we won't
    * unblock it until there's more data.
    */
   bool mHasCurrentData;
-  /**
-   * True if mHasCurrentData is true and we've notified listeners.
-   */
-  bool mNotifiedHasCurrentData;
 
   // Main-thread views of state
   StreamTime mMainThreadCurrentTime;
   bool mMainThreadFinished;
   bool mFinishedNotificationSent;
   bool mMainThreadDestroyed;
   int mNrOfMainThreadUsers;
 
@@ -668,17 +654,17 @@ class SourceMediaStream : public MediaSt
   explicit SourceMediaStream();
 
   SourceMediaStream* AsSourceStream() override { return this; }
 
   // Main thread only
 
   /**
    * Enable or disable pulling. When pulling is enabled, NotifyPull
-   * gets called on MediaStreamListeners for this stream during the
+   * gets called on MediaStream/TrackListeners for this stream during the
    * MediaStreamGraph control loop. Pulling is initially disabled.
    * Due to unavoidable race conditions, after a call to SetPullEnabled(false)
    * it is still possible for a NotifyPull to occur.
    */
   void SetPullEnabled(bool aEnabled);
 
   // Users of audio inputs go through the stream so it can track when the
   // last stream referencing an input goes away, so it can close the cubeb
@@ -690,70 +676,61 @@ class SourceMediaStream : public MediaSt
   void CloseAudioInput(Maybe<CubebUtils::AudioDeviceID>& aID,
                        AudioDataListener* aListener);
 
   // MediaStreamGraph thread only
   void DestroyImpl() override;
 
   // Call these on any thread.
   /**
-   * Call all MediaStreamListeners to request new data via the NotifyPull API
-   * (if enabled).
+   * Call all MediaStreamTrackListeners to request new data via the NotifyPull
+   * API (if enabled).
    * aDesiredUpToTime (in): end time of new data requested.
    *
    * Returns true if new data is about to be added.
    */
-  bool PullNewData(StreamTime aDesiredUpToTime);
+  bool PullNewData(GraphTime aDesiredUpToTime);
 
   /**
    * Extract any state updates pending in the stream, and apply them.
    */
-  void ExtractPendingInput();
-
-  /**
-   * These add/remove DirectListeners, which allow bypassing the graph and any
-   * synchronization delays for e.g. PeerConnection, which wants the data ASAP
-   * and lets the far-end handle sync and playout timing.
-   */
-  void NotifyListenersEventImpl(MediaStreamGraphEvent aEvent);
-  void NotifyListenersEvent(MediaStreamGraphEvent aEvent);
+  void ExtractPendingInput(GraphTime aCurrentTime);
 
   enum {
     ADDTRACK_QUEUED = 0x01  // Queue track add until FinishAddTracks()
   };
   /**
-   * Add a new track to the stream starting at the given base time (which
-   * must be greater than or equal to the last time passed to
-   * AdvanceKnownTracksTime). Takes ownership of aSegment. aSegment should
-   * contain data starting after aStart.
+   * Add a new track to the stream starting at the stream's current time
+   * (which must be greater than or equal to the last time passed to
+   * AdvanceKnownTracksTime). Takes ownership of aSegment.
    */
-  void AddTrack(TrackID aID, StreamTime aStart, MediaSegment* aSegment,
-                uint32_t aFlags = 0) {
-    AddTrackInternal(aID, GraphRate(), aStart, aSegment, aFlags);
+  void AddTrack(TrackID aID, MediaSegment* aSegment, uint32_t aFlags = 0) {
+    AddTrackInternal(aID, GraphRate(), aSegment, aFlags);
   }
 
   /**
    * Like AddTrack, but resamples audio from aRate to the graph rate.
    */
-  void AddAudioTrack(TrackID aID, TrackRate aRate, StreamTime aStart,
-                     AudioSegment* aSegment, uint32_t aFlags = 0);
+  void AddAudioTrack(TrackID aID, TrackRate aRate, AudioSegment* aSegment,
+                     uint32_t aFlags = 0);
 
   /**
    * Call after a series of AddTrack or AddAudioTrack calls to implement
    * any pending track adds.
    */
   void FinishAddTracks();
 
   /**
    * Append media data to a track. Ownership of aSegment remains with the
-   * caller, but aSegment is emptied. Returns false if the data was not appended
-   * because no such track exists or the stream was already finished.
+   * caller, but aSegment is emptied. Returns 0 if the data was not appended
+   * because no such track exists or the stream was already finished. Returns
+   * the duration of the appended data in the graph's track rate otherwise.
    */
-  virtual bool AppendToTrack(TrackID aID, MediaSegment* aSegment,
-                             MediaSegment* aRawSegment = nullptr);
+  virtual StreamTime AppendToTrack(TrackID aID, MediaSegment* aSegment,
+                                   MediaSegment* aRawSegment = nullptr);
   /**
    * Get the stream time of the end of the data that has been appended so far.
    * Can be called from any thread but won't be useful if it can race with
    * an AppendToTrack call, so should probably just be called from the thread
    * that also calls AppendToTrack.
    */
   StreamTime GetEndOfAppendedData(TrackID aID);
   /**
@@ -826,17 +803,16 @@ class SourceMediaStream : public MediaSt
   struct TrackData {
     TrackID mID;
     // Sample rate of the input data.
     TrackRate mInputRate;
     // Resampler if the rate of the input track does not match the
     // MediaStreamGraph's.
     nsAutoRef<SpeexResamplerState> mResampler;
     int mResamplerChannelCount;
-    StreamTime mStart;
     // End-time of data already flushed to the track (excluding mData)
     StreamTime mEndOfFlushedData;
     // Each time the track updates are flushed to the media graph thread,
     // the segment buffer is emptied.
     nsAutoPtr<MediaSegment> mData;
     // Each time the track updates are flushed to the media graph thread,
     // this is cleared.
     uint32_t mCommands;
@@ -848,18 +824,18 @@ class SourceMediaStream : public MediaSt
                                       MediaSegment* aSegment);
 
   void AddDirectTrackListenerImpl(
       already_AddRefed<DirectMediaStreamTrackListener> aListener,
       TrackID aTrackID) override;
   void RemoveDirectTrackListenerImpl(DirectMediaStreamTrackListener* aListener,
                                      TrackID aTrackID) override;
 
-  void AddTrackInternal(TrackID aID, TrackRate aRate, StreamTime aStart,
-                        MediaSegment* aSegment, uint32_t aFlags);
+  void AddTrackInternal(TrackID aID, TrackRate aRate, MediaSegment* aSegment,
+                        uint32_t aFlags);
 
   TrackData* FindDataForTrack(TrackID aID) {
     mMutex.AssertCurrentThreadOwns();
     for (uint32_t i = 0; i < mUpdateTracks.Length(); ++i) {
       if (mUpdateTracks[i].mID == aID) {
         return &mUpdateTracks[i];
       }
     }
@@ -1318,20 +1294,23 @@ class MediaStreamGraph {
    */
   void StartNonRealtimeProcessing(uint32_t aTicksToProcess);
 
   /**
    * 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
+   * Should only be called during MediaStreamTrackListener callbacks or during
    * ProcessedMediaStream::ProcessInput().
+   *
+   * Note that if called during shutdown the runnable will be ignored and
+   * released on main thread.
    */
-  virtual void DispatchToMainThreadAfterStreamStateUpdate(
+  void DispatchToMainThreadAfterStreamStateUpdate(
       already_AddRefed<nsIRunnable> aRunnable);
 
   /**
    * Returns graph sample rate in Hz.
    */
   TrackRate GraphRate() const { return mSampleRate; }
 
   void RegisterCaptureStreamForWindow(uint64_t aWindowId,
@@ -1339,30 +1318,33 @@ class MediaStreamGraph {
   void UnregisterCaptureStreamForWindow(uint64_t aWindowId);
   already_AddRefed<MediaInputPort> ConnectToCaptureStream(
       uint64_t aWindowId, MediaStream* aMediaStream);
 
   void AssertOnGraphThreadOrNotRunning() const {
     MOZ_ASSERT(OnGraphThreadOrNotRunning());
   }
 
+  /**
+   * Returns a watchable of the graph's main-thread observable graph time.
+   * Main thread only.
+   */
+  virtual Watchable<GraphTime>& CurrentTime() = 0;
+
  protected:
   explicit MediaStreamGraph(TrackRate aSampleRate) : mSampleRate(aSampleRate) {
     MOZ_COUNT_CTOR(MediaStreamGraph);
   }
   virtual ~MediaStreamGraph() { MOZ_COUNT_DTOR(MediaStreamGraph); }
 
   // Intended only for assertions, either on graph thread or not running (in
   // which case we must be on the main thread).
   bool OnGraphThreadOrNotRunning() const;
   bool OnGraphThread() const;
 
-  // Media graph thread only
-  nsTArray<nsCOMPtr<nsIRunnable>> mPendingUpdateRunnables;
-
   /**
    * Sample rate at which this graph runs. For real time graphs, this is
    * the rate of the audio mixer. For offline graphs, this is the rate specified
    * at construction.
    */
   TrackRate mSampleRate;
 };
 
--- a/dom/media/MediaStreamGraphImpl.h
+++ b/dom/media/MediaStreamGraphImpl.h
@@ -251,17 +251,17 @@ class MediaStreamGraphImpl : public Medi
   /**
    * Do all the processing and play the audio and video, from
    * mProcessedTime to mStateComputedTime.
    */
   void Process();
 
   /**
    * For use during ProcessedMediaStream::ProcessInput() or
-   * MediaStreamListener callbacks, when graph state cannot be changed.
+   * MediaStreamTrackListener callbacks, when graph state cannot be changed.
    * Schedules |aMessage| to run after processing, at a time when graph state
    * can be changed.  Graph thread.
    */
   void RunMessageAfterProcessing(UniquePtr<ControlMessage> aMessage);
 
   /**
    * Called when a suspend/resume/close operation has been completed, on the
    * graph thread.
@@ -336,20 +336,16 @@ class MediaStreamGraphImpl : public Medi
   /**
    * Given a graph time aTime, convert it to a stream time taking into
    * account the time during which aStream is scheduled to be blocked.
    */
   StreamTime GraphTimeToStreamTimeWithBlocking(const MediaStream* aStream,
                                                GraphTime aTime) const;
 
   /**
-   * Call NotifyHaveCurrentData on aStream's listeners.
-   */
-  void NotifyHasCurrentData(MediaStream* aStream);
-  /**
    * If aStream needs an audio stream but doesn't have one, create it.
    * If aStream doesn't need an audio stream but has one, destroy it.
    */
   void CreateOrDestroyAudioStreams(MediaStream* aStream);
   /**
    * Queue audio (mix of stream audio and silence for blocked intervals)
    * to the audio output stream. Returns the number of frames played.
    */
@@ -540,16 +536,18 @@ class MediaStreamGraphImpl : public Medi
 
   // Capture Stream API. This allows to get a mixed-down output for a window.
   void RegisterCaptureStreamForWindow(uint64_t aWindowId,
                                       ProcessedMediaStream* aCaptureStream);
   void UnregisterCaptureStreamForWindow(uint64_t aWindowId);
   already_AddRefed<MediaInputPort> ConnectToCaptureStream(
       uint64_t aWindowId, MediaStream* aMediaStream);
 
+  Watchable<GraphTime>& CurrentTime() override;
+
   class StreamSet {
    public:
     class iterator {
      public:
       explicit iterator(MediaStreamGraphImpl& aGraph)
           : mGraph(&aGraph), mArrayNum(-1), mArrayIndex(0) {
         ++(*this);
       }
@@ -645,16 +643,21 @@ class MediaStreamGraphImpl : public Medi
   /**
    * Date of the last time we updated the main thread with the graph state.
    */
   TimeStamp mLastMainThreadUpdate;
   /**
    * Number of active MediaInputPorts
    */
   int32_t mPortCount;
+  /**
+   * Runnables to run after the next update to main thread state, but that are
+   * still waiting for the next iteration to finish.
+   */
+  nsTArray<nsCOMPtr<nsIRunnable>> mPendingUpdateRunnables;
 
   /**
    * Devices to use for cubeb input & output, or nullptr for default device.
    * A MediaStreamGraph always has an output (even if silent).
    * If `mInputDeviceUsers.Count() != 0`, this MediaStreamGraph wants audio
    * input.
    *
    * In any case, the number of channels to use can be queried (on the graph
@@ -874,13 +877,25 @@ class MediaStreamGraphImpl : public Medi
   const float mGlobalVolume;
 
 #ifdef DEBUG
   /**
    * Used to assert when AppendMessage() runs ControlMessages synchronously.
    */
   bool mCanRunMessagesSynchronously;
 #endif
+
+  /**
+   * The graph's main-thread observable graph time.
+   * Updated by the stable state runnable after each iteration.
+   */
+  Watchable<GraphTime> mMainThreadGraphTime;
+
+  /**
+   * Set based on mProcessedTime at end of iteration.
+   * Read by stable state runnable on main thread. Protected by mMonitor.
+   */
+  GraphTime mNextMainThreadGraphTime = 0;
 };
 
 }  // namespace mozilla
 
 #endif /* MEDIASTREAMGRAPHIMPL_H_ */
--- a/dom/media/MediaStreamListener.h
+++ b/dom/media/MediaStreamListener.h
@@ -15,123 +15,16 @@ namespace mozilla {
 
 class AudioSegment;
 class MediaStream;
 class MediaStreamGraph;
 class MediaStreamVideoSink;
 class VideoSegment;
 
 /**
- * This is a base class for media graph thread listener callbacks.
- * Override methods to be notified of audio or video data or changes in stream
- * state.
- *
- * This can be used by stream recorders or network connections that receive
- * stream input. It could also be used for debugging.
- *
- * All notification methods are called from the media graph thread. Overriders
- * of these methods are responsible for all synchronization. Beware!
- * These methods are called without the media graph monitor held, so
- * reentry into media graph methods is possible, although very much discouraged!
- * You should do something non-blocking and non-reentrant (e.g. dispatch an
- * event to some thread) and return.
- * The listener is not allowed to add/remove any listeners from the stream.
- *
- * When a listener is first attached, we guarantee to send a
- * NotifyBlockingChanged callback to notify of the initial blocking state. Also,
- * if a listener is attached to a stream that has already finished, we'll call
- * NotifyFinished.
- */
-class MediaStreamListener {
- protected:
-  // Protected destructor, to discourage deletion outside of Release():
-  virtual ~MediaStreamListener() {}
-
- public:
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaStreamListener)
-
-  /**
-   * When a SourceMediaStream has pulling enabled, and the MediaStreamGraph
-   * control loop is ready to pull, this gets called. A NotifyPull
-   * implementation is allowed to call the SourceMediaStream methods that alter
-   * track data. It is not allowed to make other MediaStream API calls,
-   * including calls to add or remove MediaStreamListeners. It is not allowed to
-   * block for any length of time. aDesiredTime is the stream time we would like
-   * to get data up to. Data beyond this point will not be played until
-   * NotifyPull runs again, so there's not much point in providing it. Note that
-   * if the stream is blocked for some reason, then data before aDesiredTime may
-   * not be played immediately.
-   */
-  virtual void NotifyPull(MediaStreamGraph* aGraph, StreamTime aDesiredTime) {}
-
-  enum Blocking { BLOCKED, UNBLOCKED };
-  /**
-   * Notify that the blocking status of the stream changed. The initial state
-   * is assumed to be BLOCKED.
-   */
-  virtual void NotifyBlockingChanged(MediaStreamGraph* aGraph,
-                                     Blocking aBlocked) {}
-
-  /**
-   * Notify that the stream has data in each track
-   * for the stream's current time. Once this state becomes true, it will
-   * always be true since we block stream time from progressing to times where
-   * there isn't data in each track.
-   */
-  virtual void NotifyHasCurrentData(MediaStreamGraph* aGraph) {}
-
-  /**
-   * Notify that the stream output is advancing. aCurrentTime is the graph's
-   * current time. MediaStream::GraphTimeToStreamTime can be used to get the
-   * stream time.
-   */
-  virtual void NotifyOutput(MediaStreamGraph* aGraph, GraphTime aCurrentTime) {}
-
-  /**
-   * Notify that an event has occurred on the Stream
-   */
-  virtual void NotifyEvent(MediaStreamGraph* aGraph,
-                           MediaStreamGraphEvent aEvent) {}
-
-  /**
-   * Notify that changes to one of the stream tracks have been queued.
-   * aTrackEvents can be any combination of TRACK_EVENT_CREATED and
-   * TRACK_EVENT_ENDED. aQueuedMedia is the data being added to the track
-   * at aTrackOffset (relative to the start of the stream).
-   * aInputStream and aInputTrackID will be set if the changes originated
-   * from an input stream's track. In practice they will only be used for
-   * ProcessedMediaStreams.
-   */
-  virtual void NotifyQueuedTrackChanges(MediaStreamGraph* aGraph, TrackID aID,
-                                        StreamTime aTrackOffset,
-                                        TrackEventCommand aTrackEvents,
-                                        const MediaSegment& aQueuedMedia,
-                                        MediaStream* aInputStream = nullptr,
-                                        TrackID aInputTrackID = TRACK_INVALID) {
-  }
-
-  /**
-   * Notify queued audio data. Only audio data need to be queued. The video data
-   * will be notified by MediaStreamVideoSink::SetCurrentFrame.
-   */
-  virtual void NotifyQueuedAudioData(MediaStreamGraph* aGraph, TrackID aID,
-                                     StreamTime aTrackOffset,
-                                     const AudioSegment& aQueuedMedia,
-                                     MediaStream* aInputStream = nullptr,
-                                     TrackID aInputTrackID = TRACK_INVALID) {}
-
-  /**
-   * Notify that all new tracks this iteration have been created.
-   * This is to ensure that tracks added atomically to MediaStreamGraph
-   * are also notified of atomically to MediaStreamListeners.
-   */
-  virtual void NotifyFinishedTrackCreation(MediaStreamGraph* aGraph) {}
-};
-
-/**
  * This is a base class for media graph thread listener callbacks locked to
  * specific tracks. Override methods to be notified of audio or video data or
  * changes in track state.
  *
  * All notification methods are called from the media graph thread. Overriders
  * of these methods are responsible for all synchronization. Beware!
  * These methods are called without the media graph monitor held, so
  * reentry into media graph methods is possible, although very much discouraged!
@@ -142,25 +35,63 @@ class MediaStreamListener {
  *
  * If a listener is attached to a track that has already ended, we guarantee
  * to call NotifyEnded.
  */
 class MediaStreamTrackListener {
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaStreamTrackListener)
 
  public:
+  /**
+   * When a SourceMediaStream has pulling enabled, and the MediaStreamGraph
+   * control loop is ready to pull, this gets called for each track in the
+   * SourceMediaStream that is lacking data for the current iteration.
+   * A NotifyPull implementation is allowed to call the SourceMediaStream
+   * methods that alter track data.
+   *
+   * It is not allowed to make other MediaStream API calls, including
+   * calls to add or remove MediaStreamTrackListeners. It is not allowed to
+   * block for any length of time.
+   *
+   * aEndOfAppendedData is the duration of the data that has already been
+   * appended to this track, in stream time.
+   *
+   * aDesiredTime is the stream time we should append data up to. Data
+   * beyond this point will not be played until NotifyPull runs again, so
+   * there's not much point in providing it. Note that if the stream is blocked
+   * for some reason, then data before aDesiredTime may not be played
+   * immediately.
+   */
+  virtual void NotifyPull(MediaStreamGraph* aGraph,
+                          StreamTime aEndOfAppendedData,
+                          StreamTime aDesiredTime) {}
+
   virtual void NotifyQueuedChanges(MediaStreamGraph* aGraph,
                                    StreamTime aTrackOffset,
                                    const MediaSegment& aQueuedMedia) {}
 
   virtual void NotifyPrincipalHandleChanged(
       MediaStreamGraph* aGraph, const PrincipalHandle& aNewPrincipalHandle) {}
 
+  /**
+   * Notify that the stream output is advancing. aCurrentTrackTime is the number
+   * of samples that has been played out for this track in stream time.
+   */
+  virtual void NotifyOutput(MediaStreamGraph* aGraph,
+                            StreamTime aCurrentTrackTime) {}
+
+  /**
+   * Notify that this track has been ended and all data has been played out.
+   */
   virtual void NotifyEnded() {}
 
+  /**
+   * Notify that this track listener has been removed from the graph, either
+   * after shutdown or RemoveTrackListener.
+   */
   virtual void NotifyRemoved() {}
 
  protected:
   virtual ~MediaStreamTrackListener() {}
 };
 
 /**
  * This is a base class for media graph thread listener direct callbacks from
--- a/dom/media/MediaStreamTrack.cpp
+++ b/dom/media/MediaStreamTrack.cpp
@@ -50,89 +50,152 @@ auto MediaStreamTrackSource::ApplyConstr
   RefPtr<PledgeVoid> p = new PledgeVoid();
   p->Reject(new MediaStreamError(aWindow,
                                  MediaStreamError::Name::OverconstrainedError,
                                  NS_LITERAL_STRING("")));
   return p.forget();
 }
 
 /**
- * PrincipalHandleListener monitors changes in PrincipalHandle of the media
- * flowing through the MediaStreamGraph.
+ * MSGListener monitors state changes of the media flowing through the
+ * MediaStreamGraph.
+ *
+ *
+ * For changes to PrincipalHandle the following applies:
  *
  * When the main thread principal for a MediaStreamTrack changes, its principal
  * will be set to the combination of the previous principal and the new one.
  *
  * As a PrincipalHandle change later happens on the MediaStreamGraph thread, we
  * will be notified. If the latest principal on main thread matches the
  * PrincipalHandle we just saw on MSG thread, we will set the track's principal
  * to the new one.
  *
  * We know at this point that the old principal has been flushed out and data
  * under it cannot leak to consumers.
  *
  * In case of multiple changes to the main thread state, the track's principal
  * will be a combination of its old principal and all the new ones until the
  * latest main thread principal matches the PrincipalHandle on the MSG thread.
  */
-class MediaStreamTrack::PrincipalHandleListener
-    : public MediaStreamTrackListener {
+class MediaStreamTrack::MSGListener : public MediaStreamTrackListener {
  public:
-  explicit PrincipalHandleListener(MediaStreamTrack* aTrack) : mTrack(aTrack) {}
-
-  void Forget() {
-    MOZ_ASSERT(NS_IsMainThread());
-    mTrack = nullptr;
+  explicit MSGListener(MediaStreamTrack* aTrack)
+      : mGraph(aTrack->GraphImpl()), mTrack(aTrack) {
+    MOZ_ASSERT(mGraph);
   }
 
   void DoNotifyPrincipalHandleChanged(
       const PrincipalHandle& aNewPrincipalHandle) {
     MOZ_ASSERT(NS_IsMainThread());
 
     if (!mTrack) {
       return;
     }
 
     mTrack->NotifyPrincipalHandleChanged(aNewPrincipalHandle);
   }
 
   void NotifyPrincipalHandleChanged(
       MediaStreamGraph* aGraph,
       const PrincipalHandle& aNewPrincipalHandle) override {
-    aGraph->DispatchToMainThreadAfterStreamStateUpdate(
+    mGraph->DispatchToMainThreadAfterStreamStateUpdate(
         NewRunnableMethod<StoreCopyPassByConstLRef<PrincipalHandle>>(
-            "dom::MediaStreamTrack::PrincipalHandleListener::"
+            "dom::MediaStreamTrack::MSGListener::"
             "DoNotifyPrincipalHandleChanged",
-            this, &PrincipalHandleListener::DoNotifyPrincipalHandleChanged,
+            this, &MSGListener::DoNotifyPrincipalHandleChanged,
             aNewPrincipalHandle));
   }
 
+  void NotifyRemoved() override {
+    // `mTrack` is a WeakPtr and must be destroyed on main thread.
+    // We dispatch ourselves to main thread here in case the MediaStreamGraph
+    // is holding the last reference to us.
+    mGraph->DispatchToMainThreadAfterStreamStateUpdate(
+        NS_NewRunnableFunction("MediaStreamTrack::MSGListener::mTrackReleaser",
+                               [self = RefPtr<MSGListener>(this)]() {}));
+  }
+
+  void DoNotifyEnded() {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    if (!mTrack) {
+      return;
+    }
+
+    mGraph->AbstractMainThread()->Dispatch(
+        NewRunnableMethod("MediaStreamTrack::OverrideEnded", mTrack.get(),
+                          &MediaStreamTrack::OverrideEnded));
+  }
+
+  void NotifyEnded() override {
+    mGraph->DispatchToMainThreadAfterStreamStateUpdate(
+        NewRunnableMethod("MediaStreamTrack::MSGListener::DoNotifyEnded", this,
+                          &MSGListener::DoNotifyEnded));
+  }
+
  protected:
-  // These fields may only be accessed on the main thread
-  MediaStreamTrack* mTrack;
+  const RefPtr<MediaStreamGraphImpl> mGraph;
+
+  // Main thread only.
+  WeakPtr<MediaStreamTrack> mTrack;
+};
+
+class TrackSink : public MediaStreamTrackSource::Sink {
+ public:
+  explicit TrackSink(MediaStreamTrack* aTrack) : mTrack(aTrack) {}
+
+  /**
+   * Keep the track source alive. This track and any clones are controlling the
+   * lifetime of the source by being registered as its sinks.
+   */
+  bool KeepsSourceAlive() const override { return true; }
+
+  bool Enabled() const override {
+    if (!mTrack) {
+      return false;
+    }
+    return mTrack->Enabled();
+  }
+
+  void PrincipalChanged() override {
+    if (mTrack) {
+      mTrack->PrincipalChanged();
+    }
+  }
+
+  void MutedChanged(bool aNewState) override {
+    if (mTrack) {
+      mTrack->MutedChanged(aNewState);
+    }
+  }
+
+ private:
+  WeakPtr<MediaStreamTrack> mTrack;
 };
 
 MediaStreamTrack::MediaStreamTrack(DOMMediaStream* aStream, TrackID aTrackID,
                                    TrackID aInputTrackID,
                                    MediaStreamTrackSource* aSource,
                                    const MediaTrackConstraints& aConstraints)
     : mOwningStream(aStream),
       mTrackID(aTrackID),
       mInputTrackID(aInputTrackID),
       mSource(aSource),
+      mSink(MakeUnique<TrackSink>(this)),
       mPrincipal(aSource->GetPrincipal()),
       mReadyState(MediaStreamTrackState::Live),
       mEnabled(true),
       mMuted(false),
       mConstraints(aConstraints) {
-  GetSource().RegisterSink(this);
+  GetSource().RegisterSink(mSink.get());
 
   if (GetOwnedStream()) {
-    mPrincipalHandleListener = new PrincipalHandleListener(this);
-    AddListener(mPrincipalHandleListener);
+    mMSGListener = new MSGListener(this);
+    AddListener(mMSGListener);
   }
 
   nsresult rv;
   nsCOMPtr<nsIUUIDGenerator> uuidgen =
       do_GetService("@mozilla.org/uuid-generator;1", &rv);
 
   nsID uuid;
   memset(&uuid, 0, sizeof(uuid));
@@ -143,25 +206,25 @@ MediaStreamTrack::MediaStreamTrack(DOMMe
   char chars[NSID_LENGTH];
   uuid.ToProvidedString(chars);
   mID = NS_ConvertASCIItoUTF16(chars);
 }
 
 MediaStreamTrack::~MediaStreamTrack() { Destroy(); }
 
 void MediaStreamTrack::Destroy() {
+  mReadyState = MediaStreamTrackState::Ended;
   if (mSource) {
-    mSource->UnregisterSink(this);
+    mSource->UnregisterSink(mSink.get());
   }
-  if (mPrincipalHandleListener) {
+  if (mMSGListener) {
     if (GetOwnedStream()) {
-      RemoveListener(mPrincipalHandleListener);
+      RemoveListener(mMSGListener);
     }
-    mPrincipalHandleListener->Forget();
-    mPrincipalHandleListener = nullptr;
+    mMSGListener = nullptr;
   }
   // Remove all listeners -- avoid iterating over the list we're removing from
   const nsTArray<RefPtr<MediaStreamTrackListener>> trackListeners(
       mTrackListeners);
   for (auto listener : trackListeners) {
     RemoveListener(listener);
   }
   // Do the same as above for direct listeners
@@ -233,17 +296,17 @@ void MediaStreamTrack::Stop() {
     return;
   }
 
   if (!mSource) {
     MOZ_ASSERT(false);
     return;
   }
 
-  mSource->UnregisterSink(this);
+  mSource->UnregisterSink(mSink.get());
 
   MOZ_ASSERT(mOwningStream,
              "Every MediaStreamTrack needs an owning DOMMediaStream");
   DOMMediaStream::TrackPort* port = mOwningStream->FindOwnedTrackPort(*this);
   MOZ_ASSERT(port,
              "A MediaStreamTrack must exist in its owning DOMMediaStream");
   RefPtr<Pledge<bool>> p =
       port->BlockSourceTrackId(mInputTrackID, BlockingMode::CREATION);
@@ -366,16 +429,26 @@ void MediaStreamTrack::NotifyPrincipalHa
     SetPrincipal(mPendingPrincipal);
     mPendingPrincipal = nullptr;
   }
 }
 
 void MediaStreamTrack::MutedChanged(bool aNewState) {
   MOZ_ASSERT(NS_IsMainThread());
 
+  /**
+   * 4.3.1 Life-cycle and Media flow - Media flow
+   * To set a track's muted state to newState, the User Agent MUST run the
+   * following steps:
+   *  1. Let track be the MediaStreamTrack in question.
+   *  2. Set track's muted attribute to newState.
+   *  3. If newState is true let eventName be mute, otherwise unmute.
+   *  4. Fire a simple event named eventName on track.
+   */
+
   if (mMuted == aNewState) {
     MOZ_ASSERT_UNREACHABLE("Muted state didn't actually change");
     return;
   }
 
   LOG(LogLevel::Info,
       ("MediaStreamTrack %p became %s", this, aNewState ? "muted" : "unmuted"));
 
@@ -425,36 +498,35 @@ void MediaStreamTrack::RemoveConsumer(Me
   // Remove destroyed consumers for cleanliness
   while (mConsumers.RemoveElement(nullptr)) {
     MOZ_ASSERT_UNREACHABLE("A consumer was not explicitly removed");
   }
 }
 
 already_AddRefed<MediaStreamTrack> MediaStreamTrack::Clone() {
   // MediaStreamTracks are currently governed by streams, so we need a dummy
-  // DOMMediaStream to own our track clone. The dummy will never see any
-  // dynamically created tracks (no input stream) so no need for a SourceGetter.
+  // DOMMediaStream to own our track clone.
   RefPtr<DOMMediaStream> newStream =
-      new DOMMediaStream(mOwningStream->GetParentObject(), nullptr);
+      new DOMMediaStream(mOwningStream->GetParentObject());
 
   MediaStreamGraph* graph = Graph();
   newStream->InitOwnedStreamCommon(graph);
   newStream->InitPlaybackStreamCommon(graph);
 
   return newStream->CloneDOMTrack(*this, mTrackID);
 }
 
 void MediaStreamTrack::SetReadyState(MediaStreamTrackState aState) {
   MOZ_ASSERT(!(mReadyState == MediaStreamTrackState::Ended &&
                aState == MediaStreamTrackState::Live),
              "We don't support overriding the ready state from ended to live");
 
   if (mReadyState == MediaStreamTrackState::Live &&
       aState == MediaStreamTrackState::Ended && mSource) {
-    mSource->UnregisterSink(this);
+    mSource->UnregisterSink(mSink.get());
   }
 
   mReadyState = aState;
 }
 
 void MediaStreamTrack::OverrideEnded() {
   MOZ_ASSERT(NS_IsMainThread());
 
@@ -464,17 +536,22 @@ void MediaStreamTrack::OverrideEnded() {
 
   LOG(LogLevel::Info, ("MediaStreamTrack %p ended", this));
 
   if (!mSource) {
     MOZ_ASSERT(false);
     return;
   }
 
-  mSource->UnregisterSink(this);
+  mSource->UnregisterSink(mSink.get());
+
+  if (mMSGListener) {
+    RemoveListener(mMSGListener);
+  }
+  mMSGListener = nullptr;
 
   mReadyState = MediaStreamTrackState::Ended;
 
   NotifyEnded();
 
   DispatchTrustedEvent(NS_LITERAL_STRING("ended"));
 }
 
--- a/dom/media/MediaStreamTrack.h
+++ b/dom/media/MediaStreamTrack.h
@@ -36,16 +36,17 @@ class ProcessedMediaStream;
 class RemoteSourceStreamInfo;
 class SourceStreamInfo;
 
 namespace dom {
 
 class AudioStreamTrack;
 class VideoStreamTrack;
 class MediaStreamError;
+class TrackSink;
 enum class CallerType : uint32_t;
 
 /**
  * Common interface through which a MediaStreamTrack can communicate with its
  * producer on the main thread.
  *
  * Kept alive by a strong ref in all MediaStreamTracks (original and clones)
  * sharing this source.
@@ -81,18 +82,30 @@ class MediaStreamTrackSource : public ns
      * call MediaStreamTrackSource::SinkEnabledStateChanged().
      *
      * Typically MediaStreamTrack returns the track's enabled state and other
      * Sinks (like HTMLMediaElement::StreamCaptureTrackSource) return false so
      * control over device state remains with tracks and their enabled state.
      */
     virtual bool Enabled() const = 0;
 
+    /**
+     * Called when the principal of the MediaStreamTrackSource where this sink
+     * is registered has changed.
+     */
     virtual void PrincipalChanged() = 0;
+
+    /**
+     * Called when the muted state of the MediaStreamTrackSource where this sink
+     * is registered has changed.
+     */
     virtual void MutedChanged(bool aNewState) = 0;
+
+   protected:
+    virtual ~Sink() = default;
   };
 
   MediaStreamTrackSource(nsIPrincipal* aPrincipal, const nsString& aLabel)
       : mPrincipal(aPrincipal), mLabel(aLabel), mStopped(false) {}
 
   /**
    * Use to clean up any resources that have to be cleaned before the
    * destructor is called. It is often too late in the destructor because
@@ -333,60 +346,62 @@ class MediaStreamTrackConsumer
    */
   virtual void NotifyEnded(MediaStreamTrack* aTrack){};
 };
 
 /**
  * Class representing a track in a DOMMediaStream.
  */
 class MediaStreamTrack : public DOMEventTargetHelper,
-                         public MediaStreamTrackSource::Sink {
+                         public SupportsWeakPtr<MediaStreamTrack> {
   // DOMMediaStream owns MediaStreamTrack instances, and requires access to
   // some internal state, e.g., GetInputStream(), GetOwnedStream().
   friend class mozilla::DOMMediaStream;
 
   // PeerConnection and friends need to know our owning DOMStream and track id.
   friend class mozilla::PeerConnectionImpl;
   friend class mozilla::PeerConnectionMedia;
   friend class mozilla::SourceStreamInfo;
   friend class mozilla::RemoteSourceStreamInfo;
 
-  class PrincipalHandleListener;
+  class MSGListener;
 
  public:
   /**
    * aTrackID is the MediaStreamGraph track ID for the track in the
    * MediaStream owned by aStream.
    */
   MediaStreamTrack(
       DOMMediaStream* aStream, TrackID aTrackID, TrackID aInputTrackID,
       MediaStreamTrackSource* aSource,
       const MediaTrackConstraints& aConstraints = MediaTrackConstraints());
 
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaStreamTrack,
                                            DOMEventTargetHelper)
 
+  MOZ_DECLARE_WEAKREFERENCE_TYPENAME(MediaStreamTrack)
+
   nsPIDOMWindowInner* GetParentObject() const;
   JSObject* WrapObject(JSContext* aCx,
                        JS::Handle<JSObject*> aGivenProto) override;
 
   virtual AudioStreamTrack* AsAudioStreamTrack() { return nullptr; }
   virtual VideoStreamTrack* AsVideoStreamTrack() { return nullptr; }
 
   virtual const AudioStreamTrack* AsAudioStreamTrack() const { return nullptr; }
   virtual const VideoStreamTrack* AsVideoStreamTrack() const { return nullptr; }
 
   // WebIDL
   virtual void GetKind(nsAString& aKind) = 0;
   void GetId(nsAString& aID) const;
   virtual void GetLabel(nsAString& aLabel, CallerType /* aCallerType */) {
     GetSource().GetLabel(aLabel);
   }
-  bool Enabled() const override { return mEnabled; }
+  bool Enabled() const { return mEnabled; }
   void SetEnabled(bool aEnabled);
   bool Muted() { return mMuted; }
   void Stop();
   void GetConstraints(dom::MediaTrackConstraints& aResult);
   void GetSettings(dom::MediaTrackSettings& aResult, CallerType aCallerType);
 
   already_AddRefed<Promise> ApplyConstraints(
       const dom::MediaTrackConstraints& aConstraints, CallerType aCallerType,
@@ -419,20 +434,19 @@ class MediaStreamTrack : public DOMEvent
   void OverrideEnded();
 
   /**
    * Get this track's principal.
    */
   nsIPrincipal* GetPrincipal() const { return mPrincipal; }
 
   /**
-   * Called by the PrincipalHandleListener when this track's PrincipalHandle
-   * changes on the MediaStreamGraph thread. When the PrincipalHandle matches
-   * the pending principal we know that the principal change has propagated to
-   * consumers.
+   * Called by the MSGListener when this track's PrincipalHandle changes on
+   * the MediaStreamGraph thread. When the PrincipalHandle matches the pending
+   * principal we know that the principal change has propagated to consumers.
    */
   void NotifyPrincipalHandleChanged(const PrincipalHandle& aPrincipalHandle);
 
   /**
    * Called when this track's readyState transitions to "ended".
    * Notifies all MediaStreamTrackConsumers that this track ended.
    */
   void NotifyEnded();
@@ -457,36 +471,25 @@ class MediaStreamTrack : public DOMEvent
                        "The track source is only removed on destruction");
     return *mSource;
   }
 
   // Webrtc allows the remote side to name tracks whatever it wants, and we
   // need to surface this to content.
   void AssignId(const nsAString& aID) { mID = aID; }
 
-  // Implementation of MediaStreamTrackSource::Sink
+  /**
+   * Called when mSource's principal has changed.
+   */
+  void PrincipalChanged();
 
   /**
-   * Keep the track source alive. This track and any clones are controlling the
-   * lifetime of the source by being registered as its sinks.
+   * Called when mSource's muted state has changed.
    */
-  bool KeepsSourceAlive() const override { return true; }
-
-  void PrincipalChanged() override;
-
-  /**
-   * 4.3.1 Life-cycle and Media flow - Media flow
-   * To set a track's muted state to newState, the User Agent MUST run the
-   * following steps:
-   *  1. Let track be the MediaStreamTrack in question.
-   *  2. Set track's muted attribute to newState.
-   *  3. If newState is true let eventName be mute, otherwise unmute.
-   *  4. Fire a simple event named eventName on track.
-   */
-  void MutedChanged(bool aNewState) override;
+  void MutedChanged(bool aNewState);
 
   /**
    * Add a PrincipalChangeObserver to this track.
    *
    * Returns true if it was successfully added.
    *
    * Ownership of the PrincipalChangeObserver remains with the caller, and it's
    * the caller's responsibility to remove the observer before it dies.
@@ -589,20 +592,21 @@ class MediaStreamTrack : public DOMEvent
       mPrincipalChangeObservers;
 
   nsTArray<WeakPtr<MediaStreamTrackConsumer>> mConsumers;
 
   RefPtr<DOMMediaStream> mOwningStream;
   TrackID mTrackID;
   TrackID mInputTrackID;
   RefPtr<MediaStreamTrackSource> mSource;
+  const UniquePtr<TrackSink> mSink;
   RefPtr<MediaStreamTrack> mOriginalTrack;
   nsCOMPtr<nsIPrincipal> mPrincipal;
   nsCOMPtr<nsIPrincipal> mPendingPrincipal;
-  RefPtr<PrincipalHandleListener> mPrincipalHandleListener;
+  RefPtr<MSGListener> mMSGListener;
   // Keep tracking MediaStreamTrackListener and DirectMediaStreamTrackListener,
   // so we can remove them in |Destory|.
   nsTArray<RefPtr<MediaStreamTrackListener>> mTrackListeners;
   nsTArray<RefPtr<DirectMediaStreamTrackListener>> mDirectTrackListeners;
   nsString mID;
   MediaStreamTrackState mReadyState;
   bool mEnabled;
   bool mMuted;
--- a/dom/media/StreamTracks.h
+++ b/dom/media/StreamTracks.h
@@ -49,17 +49,21 @@ class StreamTracks {
    * Tracks have a unique ID assigned at creation. This allows us to identify
    * the same track across StreamTrackss. A StreamTracks should never have
    * two tracks with the same ID (even if they don't overlap in time).
    * TODO Tracks can also be enabled and disabled over time.
    * Takes ownership of aSegment.
    */
   class Track final {
     Track(TrackID aID, StreamTime aStart, MediaSegment* aSegment)
-        : mStart(aStart), mSegment(aSegment), mID(aID), mEnded(false) {
+        : mStart(aStart),
+          mSegment(aSegment),
+          mID(aID),
+          mEnded(false),
+          mNotifiedEnded(false) {
       MOZ_COUNT_CTOR(Track);
 
       NS_ASSERTION(aID > TRACK_NONE, "Bad track ID");
       NS_ASSERTION(0 <= aStart && aStart <= aSegment->GetDuration(),
                    "Bad start position");
     }
 
    public:
@@ -74,18 +78,23 @@ class StreamTracks {
     }
 
     MediaSegment* GetSegment() const { return mSegment; }
     TrackID GetID() const { return mID; }
     bool IsEnded() const { return mEnded; }
     StreamTime GetStart() const { return mStart; }
     StreamTime GetEnd() const { return mSegment->GetDuration(); }
     MediaSegment::Type GetType() const { return mSegment->GetType(); }
+    bool NotifiedEnded() const { return mNotifiedEnded; }
 
     void SetEnded() { mEnded = true; }
+    void NotifyEnded() {
+      MOZ_ASSERT(mEnded);
+      mNotifiedEnded = true;
+    }
     void AppendFrom(Track* aTrack) {
       NS_ASSERTION(!mEnded, "Can't append to ended track");
       NS_ASSERTION(aTrack->mID == mID, "IDs must match");
       NS_ASSERTION(aTrack->mStart == 0, "Source track must start at zero");
       NS_ASSERTION(aTrack->mSegment->GetType() == GetType(),
                    "Track types must match");
 
       mSegment->AppendFrom(aTrack->mSegment);
@@ -114,16 +123,18 @@ class StreamTracks {
     StreamTime mStart;
     // The segment data starts at the start of the owning StreamTracks, i.e.,
     // there's mStart silence/no video at the beginning.
     nsAutoPtr<MediaSegment> mSegment;
     // Unique ID
     TrackID mID;
     // True when the track ends with the data in mSegment
     bool mEnded;
+    // True after NotifiedEnded() has been called.
+    bool mNotifiedEnded;
   };
 
   class MOZ_STACK_CLASS CompareTracksByID final {
    public:
     bool Equals(Track* aA, Track* aB) const {
       return aA->GetID() == aB->GetID();
     }
     bool LessThan(Track* aA, Track* aB) const {
@@ -184,17 +195,17 @@ class StreamTracks {
     mTracksDirty = true;
 
     if (mTracksKnownTime == STREAM_TIME_MAX) {
       // There exists code like
       // http://mxr.mozilla.org/mozilla-central/source/media/webrtc/signaling/src/mediapipeline/MediaPipeline.cpp?rev=96b197deb91e&mark=1292-1297#1292
       NS_WARNING(
           "Adding track to StreamTracks that should have no more tracks");
     } else {
-      NS_ASSERTION(mTracksKnownTime <= aStart, "Start time too early");
+      // NS_ASSERTION(mTracksKnownTime <= aStart, "Start time too early");
     }
     return *track;
   }
 
   void AdvanceKnownTracksTime(StreamTime aKnownTime) {
     NS_ASSERTION(aKnownTime >= mTracksKnownTime,
                  "Can't move tracks-known time earlier");
     mTracksKnownTime = aKnownTime;
--- a/dom/media/TrackUnionStream.cpp
+++ b/dom/media/TrackUnionStream.cpp
@@ -88,17 +88,16 @@ void TrackUnionStream::ProcessInput(Grap
       // XXX we really should check whether 'stream' has finished within time
       // aTo, not just that it's finishing when all its queued data eventually
       // runs out.
       allFinished = false;
     }
     if (!stream->HasCurrentData()) {
       allHaveCurrentData = false;
     }
-    bool trackAdded = false;
     for (StreamTracks::TrackIter tracks(stream->GetStreamTracks());
          !tracks.IsEnded(); tracks.Next()) {
       bool found = false;
       for (uint32_t j = 0; j < mTrackMap.Length(); ++j) {
         TrackMapEntry* map = &mTrackMap[j];
         if (map->mInputPort == inputs[i] &&
             map->mInputTrackID == tracks->GetID()) {
           bool trackFinished = false;
@@ -113,28 +112,22 @@ void TrackUnionStream::ProcessInput(Grap
           }
           mappedTracksFinished[j] = trackFinished;
           mappedTracksWithMatchingInputTracks[j] = true;
           break;
         }
       }
       if (!found && inputs[i]->AllowCreationOf(tracks->GetID())) {
         bool trackFinished = false;
-        trackAdded = true;
         uint32_t mapIndex = AddTrack(inputs[i], tracks.get(), aFrom);
         CopyTrackData(tracks.get(), mapIndex, aFrom, aTo, &trackFinished);
         mappedTracksFinished.AppendElement(trackFinished);
         mappedTracksWithMatchingInputTracks.AppendElement(true);
       }
     }
-    if (trackAdded) {
-      for (MediaStreamListener* l : mListeners) {
-        l->NotifyFinishedTrackCreation(Graph());
-      }
-    }
   }
   for (int32_t i = mTrackMap.Length() - 1; i >= 0; --i) {
     if (mappedTracksFinished[i]) {
       EndTrack(i);
     } else {
       allFinished = false;
     }
     if (!mappedTracksWithMatchingInputTracks[i]) {
@@ -205,22 +198,16 @@ uint32_t TrackUnionStream::AddTrack(Medi
 
   // Round up the track start time so the track, if anything, starts a
   // little later than the true time. This means we'll have enough
   // samples in our input stream to go just beyond the destination time.
   StreamTime outputStart = GraphTimeToStreamTimeWithBlocking(aFrom);
 
   nsAutoPtr<MediaSegment> segment;
   segment = aTrack->GetSegment()->CreateEmptyClone();
-  for (uint32_t j = 0; j < mListeners.Length(); ++j) {
-    MediaStreamListener* l = mListeners[j];
-    l->NotifyQueuedTrackChanges(Graph(), id, outputStart,
-                                TrackEventCommand::TRACK_EVENT_CREATED,
-                                *segment, aPort->GetSource(), aTrack->GetID());
-  }
   segment->AppendNullData(outputStart);
   StreamTracks::Track* track =
       &mTracks.AddTrack(id, outputStart, segment.forget());
   STREAM_LOG(LogLevel::Debug, ("TrackUnionStream %p added track %d for input "
                                "stream %p track %d, start ticks %lld",
                                this, track->GetID(), aPort->GetSource(),
                                aTrack->GetID(), (long long)outputStart));
 
@@ -259,31 +246,16 @@ uint32_t TrackUnionStream::AddTrack(Medi
 }
 
 void TrackUnionStream::EndTrack(uint32_t aIndex) {
   StreamTracks::Track* outputTrack =
       mTracks.FindTrack(mTrackMap[aIndex].mOutputTrackID);
   if (!outputTrack || outputTrack->IsEnded()) return;
   STREAM_LOG(LogLevel::Debug, ("TrackUnionStream %p ending track %d", this,
                                outputTrack->GetID()));
-  for (uint32_t j = 0; j < mListeners.Length(); ++j) {
-    MediaStreamListener* l = mListeners[j];
-    StreamTime offset = outputTrack->GetSegment()->GetDuration();
-    nsAutoPtr<MediaSegment> segment;
-    segment = outputTrack->GetSegment()->CreateEmptyClone();
-    l->NotifyQueuedTrackChanges(Graph(), outputTrack->GetID(), offset,
-                                TrackEventCommand::TRACK_EVENT_ENDED, *segment,
-                                mTrackMap[aIndex].mInputPort->GetSource(),
-                                mTrackMap[aIndex].mInputTrackID);
-  }
-  for (TrackBound<MediaStreamTrackListener>& b : mTrackListeners) {
-    if (b.mTrackID == outputTrack->GetID()) {
-      b.mListener->NotifyEnded();
-    }
-  }
   outputTrack->SetEnded();
 }
 
 void TrackUnionStream::CopyTrackData(StreamTracks::Track* aInputTrack,
                                      uint32_t aMapIndex, GraphTime aFrom,
                                      GraphTime aTo,
                                      bool* aOutputTrackFinished) {
   TrackMapEntry* map = &mTrackMap[aMapIndex];
@@ -340,26 +312,16 @@ void TrackUnionStream::CopyTrackData(Str
         StreamTime inputStart =
             source->GraphTimeToStreamTimeWithBlocking(interval.mStart);
         segment->AppendSlice(*aInputTrack->GetSegment(),
                              std::min(inputTrackEndPoint, inputStart),
                              std::min(inputTrackEndPoint, inputEnd));
       }
     }
     ApplyTrackDisabling(outputTrack->GetID(), segment);
-    for (uint32_t j = 0; j < mListeners.Length(); ++j) {
-      MediaStreamListener* l = mListeners[j];
-      // Separate Audio and Video.
-      if (segment->GetType() == MediaSegment::AUDIO) {
-        l->NotifyQueuedAudioData(Graph(), outputTrack->GetID(), outputStart,
-                                 *static_cast<AudioSegment*>(segment),
-                                 map->mInputPort->GetSource(),
-                                 map->mInputTrackID);
-      }
-    }
     for (TrackBound<MediaStreamTrackListener>& b : mTrackListeners) {
       if (b.mTrackID != outputTrack->GetID()) {
         continue;
       }
       b.mListener->NotifyQueuedChanges(Graph(), outputStart, *segment);
     }
     outputTrack->GetSegment()->AppendFrom(segment);
   }
--- a/dom/media/imagecapture/CaptureTask.cpp
+++ b/dom/media/imagecapture/CaptureTask.cpp
@@ -15,17 +15,17 @@
 
 namespace mozilla {
 
 class CaptureTask::MediaStreamEventListener : public MediaStreamTrackListener {
  public:
   explicit MediaStreamEventListener(CaptureTask* aCaptureTask)
       : mCaptureTask(aCaptureTask){};
 
-  // MediaStreamListener methods.
+  // MediaStreamTrackListener methods.
   void NotifyEnded() override {
     if (!mCaptureTask->mImageGrabbedOrTrackEnd) {
       mCaptureTask->PostTrackEndEvent();
     }
   }
 
  private:
   CaptureTask* mCaptureTask;
--- a/dom/media/mediasink/DecodedStream.cpp
+++ b/dom/media/mediasink/DecodedStream.cpp
@@ -3,16 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/AbstractThread.h"
 #include "mozilla/CheckedInt.h"
 #include "mozilla/gfx/Point.h"
 #include "mozilla/SyncRunnable.h"
+#include "nsProxyRelease.h"
 
 #include "AudioSegment.h"
 #include "DecodedStream.h"
 #include "MediaData.h"
 #include "MediaQueue.h"
 #include "MediaStreamGraph.h"
 #include "MediaStreamListener.h"
 #include "OutputStreamManager.h"
@@ -26,235 +27,302 @@ using media::TimeUnit;
 
 /*
  * A container class to make it easier to pass the playback info all the
  * way to DecodedStreamGraphListener from DecodedStream.
  */
 struct PlaybackInfoInit {
   TimeUnit mStartTime;
   MediaInfo mInfo;
+  TrackID mAudioTrackID;
+  TrackID mVideoTrackID;
 };
 
-class DecodedStreamGraphListener : public MediaStreamListener {
+class DecodedStreamGraphListener;
+
+class DecodedStreamTrackListener : public MediaStreamTrackListener {
  public:
-  DecodedStreamGraphListener(MediaStream* aStream,
-                             MozPromiseHolder<GenericPromise>&& aPromise,
+  DecodedStreamTrackListener(DecodedStreamGraphListener* aGraphListener,
+                             SourceMediaStream* aStream, TrackID aTrackID);
+
+  void NotifyOutput(MediaStreamGraph* aGraph,
+                    StreamTime aCurrentTrackTime) override;
+  void NotifyEnded() override;
+
+ private:
+  const RefPtr<DecodedStreamGraphListener> mGraphListener;
+  const RefPtr<SourceMediaStream> mStream;
+  const mozilla::TrackID mTrackID;
+};
+
+class DecodedStreamGraphListener {
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DecodedStreamGraphListener)
+ public:
+  DecodedStreamGraphListener(SourceMediaStream* aStream, TrackID aAudioTrackID,
+                             MozPromiseHolder<GenericPromise>&& aAudioEndHolder,
+                             TrackID aVideoTrackID,
+                             MozPromiseHolder<GenericPromise>&& aVideoEndHolder,
                              AbstractThread* aMainThread)
       : mMutex("DecodedStreamGraphListener::mMutex"),
-        mStream(aStream),
+        mAudioTrackListener(IsTrackIDExplicit(aAudioTrackID)
+                                ? MakeRefPtr<DecodedStreamTrackListener>(
+                                      this, aStream, aAudioTrackID)
+                                : nullptr),
+        mVideoTrackListener(IsTrackIDExplicit(aVideoTrackID)
+                                ? MakeRefPtr<DecodedStreamTrackListener>(
+                                      this, aStream, aVideoTrackID)
+                                : nullptr),
+        mAudioTrackID(aAudioTrackID),
+        mAudioEndHolder(std::move(aAudioEndHolder)),
+        mVideoTrackID(aVideoTrackID),
+        mVideoEndHolder(std::move(aVideoEndHolder)),
         mAbstractMainThread(aMainThread) {
-    mFinishPromise = std::move(aPromise);
+    if (mAudioTrackListener) {
+      aStream->AddTrackListener(mAudioTrackListener, mAudioTrackID);
+    } else {
+      mAudioEndHolder.ResolveIfExists(true, __func__);
+    }
+
+    if (mVideoTrackListener) {
+      aStream->AddTrackListener(mVideoTrackListener, mVideoTrackID);
+    } else {
+      mVideoEndHolder.ResolveIfExists(true, __func__);
+    }
   }
 
-  void NotifyOutput(MediaStreamGraph* aGraph, GraphTime aCurrentTime) override {
-    MutexAutoLock lock(mMutex);
-    if (mStream) {
-      int64_t t = mStream->StreamTimeToMicroseconds(
-          mStream->GraphTimeToStreamTime(aCurrentTime));
+  void NotifyOutput(const RefPtr<SourceMediaStream>& aStream, TrackID aTrackID,
+                    StreamTime aCurrentTrackTime) {
+    if (aTrackID != mAudioTrackID && mAudioTrackID != TRACK_NONE) {
+      // Only audio playout drives the clock forward, if present.
+      return;
+    }
+    if (aStream) {
+      int64_t t = aStream->StreamTimeToMicroseconds(aCurrentTrackTime);
       mOnOutput.Notify(t);
     }
   }
 
-  void NotifyEvent(MediaStreamGraph* aGraph,
-                   MediaStreamGraphEvent event) override {
-    if (event == MediaStreamGraphEvent::EVENT_FINISHED) {
-      aGraph->DispatchToMainThreadAfterStreamStateUpdate(NewRunnableMethod(
-          "DecodedStreamGraphListener::DoNotifyFinished", this,
-          &DecodedStreamGraphListener::DoNotifyFinished));
+  TrackID AudioTrackID() const { return mAudioTrackID; }
+
+  TrackID VideoTrackID() const { return mVideoTrackID; }
+
+  void DoNotifyTrackEnded(TrackID aTrackID) {
+    MOZ_ASSERT(NS_IsMainThread());
+    if (aTrackID == mAudioTrackID) {
+      mAudioEndHolder.ResolveIfExists(true, __func__);
+    } else if (aTrackID == mVideoTrackID) {
+      mVideoEndHolder.ResolveIfExists(true, __func__);
+    } else {
+      MOZ_CRASH("Unexpected track id");
     }
   }