merge mozilla-central to mozilla-inbound. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Wed, 24 May 2017 11:27:29 +0200
changeset 360444 7fc3bfbb3e59ab0e065cc6160b4af9def63b40e6
parent 360443 c73ef4420951a1fad4b9d91d2216c3e3d3f31baf (current diff)
parent 360366 291a11111bdd05c5cd55dd552da4b1285ceba9b2 (diff)
child 360445 9b786cc1d17a13dc8852a47373fad4ecf7195b3a
push id43332
push userryanvm@gmail.com
push dateWed, 24 May 2017 20:34:15 +0000
treeherderautoland@b39078a430e7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge, merge
milestone55.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. r=merge a=merge
browser/base/content/browser.js
browser/base/content/test/appUpdate/.eslintrc.js
browser/base/content/test/appUpdate/browser.ini
browser/base/content/test/appUpdate/browser_updatesBackgroundWindow.js
browser/base/content/test/appUpdate/browser_updatesBackgroundWindowFailures.js
browser/base/content/test/appUpdate/browser_updatesBasicPrompt.js
browser/base/content/test/appUpdate/browser_updatesBasicPromptNoStaging.js
browser/base/content/test/appUpdate/browser_updatesCantApply.js
browser/base/content/test/appUpdate/browser_updatesCompleteAndPartialPatchesWithBadCompleteSize.js
browser/base/content/test/appUpdate/browser_updatesCompleteAndPartialPatchesWithBadPartialSize.js
browser/base/content/test/appUpdate/browser_updatesCompleteAndPartialPatchesWithBadSizes.js
browser/base/content/test/appUpdate/browser_updatesCompletePatchApplyFailure.js
browser/base/content/test/appUpdate/browser_updatesCompletePatchWithBadCompleteSize.js
browser/base/content/test/appUpdate/browser_updatesDownloadFailures.js
browser/base/content/test/appUpdate/browser_updatesMalformedXml.js
browser/base/content/test/appUpdate/browser_updatesPartialPatchApplyFailure.js
browser/base/content/test/appUpdate/browser_updatesPartialPatchApplyFailureWithCompleteAvailable.js
browser/base/content/test/appUpdate/browser_updatesPartialPatchApplyFailureWithCompleteValidationFailure.js
browser/base/content/test/appUpdate/browser_updatesPartialPatchWithBadPartialSize.js
browser/base/content/test/appUpdate/downloadPage.html
browser/base/content/test/appUpdate/head.js
browser/base/content/test/appUpdate/testConstants.js
--- a/.eslintignore
+++ b/.eslintignore
@@ -73,17 +73,17 @@ browser/extensions/pdfjs/content/web**
 browser/extensions/pocket/content/panels/js/tmpl.js
 browser/extensions/pocket/content/panels/js/vendor/**
 browser/locales/**
 # generated or library files in activity-stream
 browser/extensions/activity-stream/data/content/activity-stream.bundle.js
 browser/extensions/activity-stream/vendor/**
 # imported from chromium
 browser/extensions/mortar/**
-
+
 # devtools/ exclusions
 devtools/client/canvasdebugger/**
 devtools/client/commandline/**
 devtools/client/debugger/**
 devtools/client/framework/**
 !devtools/client/framework/devtools.js
 !devtools/client/framework/devtools-browser.js
 !devtools/client/framework/selection.js
@@ -168,16 +168,17 @@ devtools/server/actors/utils/automation-
 # Ignore devtools files testing sourcemaps / code style
 devtools/client/debugger/test/mochitest/code_binary_search.js
 devtools/client/debugger/test/mochitest/code_math.min.js
 devtools/client/debugger/test/mochitest/code_math_bogus_map.js
 devtools/client/debugger/test/mochitest/code_ugly*
 devtools/client/debugger/test/mochitest/code_worker-source-map.js
 devtools/client/framework/test/code_ugly*
 devtools/client/inspector/markup/test/events_bundle.js
+devtools/client/netmonitor/test/xhr_bundle.js
 devtools/server/tests/unit/babel_and_browserify_script_with_source_map.js
 devtools/server/tests/unit/setBreakpoint*
 devtools/server/tests/unit/sourcemapped.js
 
 # dom/ exclusions
 dom/animation/**
 dom/archivereader/**
 dom/asmjscache/**
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -133,16 +133,20 @@ pref("app.update.log", false);
 pref("app.update.backgroundMaxErrors", 10);
 
 // Whether or not app updates are enabled
 pref("app.update.enabled", true);
 
 // Whether or not to use the doorhanger application update UI.
 pref("app.update.doorhanger", true);
 
+// Ids of the links to the "What's new" update documentation
+pref("app.update.link.updateAvailableWhatsNew", "update-available-whats-new");
+pref("app.update.link.updateManualWhatsNew", "update-manual-whats-new");
+
 // How many times we should let downloads fail before prompting the user to
 // download a fresh installer.
 pref("app.update.download.promptMaxAttempts", 2);
 
 // How many times we should let an elevation prompt fail before prompting the user to
 // download a fresh installer.
 pref("app.update.elevation.promptMaxAttempts", 2);
 
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -510,21 +510,16 @@ const gExtensionsNotifications = {
 
     button.addEventListener("click", callback);
     PanelUI.addonNotificationContainer.appendChild(button);
   },
 
   updateAlerts() {
     let sideloaded = ExtensionsUI.sideloaded;
     let updates = ExtensionsUI.updates;
-    if (sideloaded.size + updates.size == 0) {
-      PanelUI.removeNotification("addon-alert");
-    } else {
-      PanelUI.showBadgeOnlyNotification("addon-alert");
-    }
 
     let container = PanelUI.addonNotificationContainer;
 
     while (container.firstChild) {
       container.firstChild.remove();
     }
 
     let items = 0;
--- a/browser/base/content/browser-sync.js
+++ b/browser/base/content/browser-sync.js
@@ -138,17 +138,16 @@ var gSync = {
           return;
         }
         this.onClientsSynced();
         break;
     }
   },
 
   updateAllUI(state) {
-    this.updatePanelBadge(state);
     this.updatePanelPopup(state);
     this.updateStateBroadcasters(state);
     this.updateSyncButtonsTooltip(state);
     this.updateSyncStatus(state);
   },
 
   updatePanelPopup(state) {
     let defaultLabel = this.appMenuStatus.getAttribute("defaultlabel");
@@ -198,25 +197,16 @@ var gSync = {
         if (this.appMenuAvatar.style.listStyleImage === bgImage) {
           this.appMenuAvatar.style.removeProperty("list-style-image");
         }
       };
       img.src = state.avatarURL;
     }
   },
 
-  updatePanelBadge(state) {
-    if (state.status == UIState.STATUS_LOGIN_FAILED ||
-        state.status == UIState.STATUS_NOT_VERIFIED) {
-      PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
-    } else {
-      PanelUI.removeNotification("fxa-needs-authentication");
-    }
-  },
-
   updateStateBroadcasters(state) {
     const status = state.status;
 
     // Start off with a clean slate
     document.getElementById("sync-reauth-state").hidden = true;
     document.getElementById("sync-setup-state").hidden = true;
     document.getElementById("sync-syncnow-state").hidden = true;
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1632,18 +1632,16 @@ var gBrowserInit = {
     // initialize the sync UI
     gSync.init();
 
     if (AppConstants.MOZ_DATA_REPORTING)
       gDataNotificationInfoBar.init();
 
     gBrowserThumbnails.init();
 
-    gMenuButtonUpdateBadge.init();
-
     gExtensionsNotifications.init();
 
     let wasMinimized = window.windowState == window.STATE_MINIMIZED;
     window.addEventListener("sizemodechange", () => {
       let isMinimized = window.windowState == window.STATE_MINIMIZED;
       if (wasMinimized != isMinimized) {
         wasMinimized = isMinimized;
         UpdatePopupNotificationsVisibility();
@@ -1795,18 +1793,16 @@ var gBrowserInit = {
     CompactTheme.uninit();
 
     TrackingProtection.uninit();
 
     RefreshBlocker.uninit();
 
     CaptivePortalWatcher.uninit();
 
-    gMenuButtonUpdateBadge.uninit();
-
     SidebarUI.uninit();
 
     // Now either cancel delayedStartup, or clean up the services initialized from
     // it.
     if (this._boundDelayedStartup) {
       this._cancelDelayedStartup();
     } else {
       if (Win7Features)
@@ -2852,234 +2848,16 @@ function UpdatePopupNotificationsVisibil
   PopupNotifications.anchorVisibilityChange();
 }
 
 function PageProxyClickHandler(aEvent) {
   if (aEvent.button == 1 && gPrefService.getBoolPref("middlemouse.paste"))
     middleMousePaste(aEvent);
 }
 
-// Setup the hamburger button badges for updates, if enabled.
-var gMenuButtonUpdateBadge = {
-  kTopics: [
-    "update-staged",
-    "update-downloaded",
-    "update-available",
-    "update-error",
-  ],
-
-  timeouts: [],
-
-  get enabled() {
-    return Services.prefs.getBoolPref("app.update.doorhanger", false);
-  },
-
-  get badgeWaitTime() {
-    return Services.prefs.getIntPref("app.update.badgeWaitTime", 4 * 24 * 3600); // 4 days
-  },
-
-  init() {
-    if (this.enabled) {
-      this.kTopics.forEach(t => {
-        Services.obs.addObserver(this, t);
-      });
-    }
-  },
-
-  uninit() {
-    if (this.enabled) {
-      this.kTopics.forEach(t => {
-        Services.obs.removeObserver(this, t);
-      });
-    }
-
-    this.reset();
-  },
-
-  reset() {
-    PanelUI.removeNotification(/^update-/);
-    this.clearCallbacks();
-  },
-
-  clearCallbacks() {
-    this.timeouts.forEach(t => clearTimeout(t));
-    this.timeouts = [];
-  },
-
-  addTimeout(time, callback) {
-    this.timeouts.push(setTimeout(() => {
-      this.clearCallbacks();
-      callback();
-    }, time));
-  },
-
-  replaceReleaseNotes(update, whatsNewId) {
-    let whatsNewLink = document.getElementById(whatsNewId);
-    if (update && update.detailsURL) {
-      whatsNewLink.href = update.detailsURL;
-    } else {
-      whatsNewLink.href = Services.urlFormatter.formatURLPref("app.update.url.details");
-    }
-  },
-
-  requestRestart() {
-    let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
-                     .createInstance(Ci.nsISupportsPRBool);
-    Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
-
-    if (!cancelQuit.data) {
-      Services.startup.quit(Services.startup.eAttemptQuit | Services.startup.eRestart);
-    }
-  },
-
-  openManualUpdateUrl() {
-    let manualUpdateUrl = Services.urlFormatter.formatURLPref("app.update.url.manual");
-    openUILinkIn(manualUpdateUrl, "tab");
-  },
-
-  showUpdateNotification(type, dismissed, mainAction) {
-    let action = {
-      callback(fromDoorhanger) {
-        if (fromDoorhanger) {
-          Services.telemetry.getHistogramById("UPDATE_NOTIFICATION_MAIN_ACTION_DOORHANGER").add(type);
-        } else {
-          Services.telemetry.getHistogramById("UPDATE_NOTIFICATION_MAIN_ACTION_MENU").add(type);
-        }
-        mainAction();
-      }
-    };
-
-    let secondaryAction = {
-      callback() {
-        Services.telemetry.getHistogramById("UPDATE_NOTIFICATION_DISMISSED").add(type);
-      },
-      dismiss: true
-    };
-
-    PanelUI.showNotification("update-" + type, action, [secondaryAction], { dismissed });
-    Services.telemetry.getHistogramById("UPDATE_NOTIFICATION_SHOWN").add(type);
-  },
-
-  showRestartNotification(dismissed) {
-    this.showUpdateNotification("restart", dismissed, () => gMenuButtonUpdateBadge.requestRestart());
-  },
-
-  showUpdateAvailableNotification(update, dismissed) {
-    this.replaceReleaseNotes(update, "update-available-whats-new");
-    this.showUpdateNotification("available", dismissed, () => {
-      let updateService = Cc["@mozilla.org/updates/update-service;1"]
-                          .getService(Ci.nsIApplicationUpdateService);
-      updateService.downloadUpdate(update, true);
-    });
-  },
-
-  showManualUpdateNotification(update, dismissed) {
-    this.replaceReleaseNotes(update, "update-manual-whats-new");
-
-    this.showUpdateNotification("manual", dismissed, () => gMenuButtonUpdateBadge.openManualUpdateUrl());
-  },
-
-  handleUpdateError(update, status) {
-    switch (status) {
-      case "download-attempt-failed":
-        this.clearCallbacks();
-        this.showUpdateAvailableNotification(update, false);
-        break;
-      case "download-attempts-exceeded":
-        this.clearCallbacks();
-        this.showManualUpdateNotification(update, false);
-        break;
-      case "elevation-attempt-failed":
-        this.clearCallbacks();
-        this.showRestartNotification(update, false);
-        break;
-      case "elevation-attempts-exceeded":
-        this.clearCallbacks();
-        this.showManualUpdateNotification(update, false);
-        break;
-      case "check-attempts-exceeded":
-      case "unknown":
-        // Background update has failed, let's show the UI responsible for
-        // prompting the user to update manually.
-        this.clearCallbacks();
-        this.showManualUpdateNotification(update, false);
-        break;
-    }
-  },
-
-  handleUpdateStagedOrDownloaded(update, status) {
-    switch (status) {
-      case "applied":
-      case "pending":
-      case "applied-service":
-      case "pending-service":
-      case "success":
-        this.clearCallbacks();
-
-        let badgeWaitTimeMs = this.badgeWaitTime * 1000;
-        let doorhangerWaitTimeMs = update.promptWaitTime * 1000;
-
-        if (badgeWaitTimeMs < doorhangerWaitTimeMs) {
-          this.addTimeout(badgeWaitTimeMs, () => {
-            this.showRestartNotification(true);
-
-            // doorhangerWaitTimeMs is relative to when we initially received
-            // the event. Since we've already waited badgeWaitTimeMs, subtract
-            // that from doorhangerWaitTimeMs.
-            let remainingTime = doorhangerWaitTimeMs - badgeWaitTimeMs;
-            this.addTimeout(remainingTime, () => {
-              this.showRestartNotification(false);
-            });
-          });
-        } else {
-          this.addTimeout(doorhangerWaitTimeMs, () => {
-            this.showRestartNotification(false);
-          });
-        }
-        break;
-    }
-  },
-
-  handleUpdateAvailable(update, status) {
-    switch (status) {
-      case "show-prompt":
-        // If an update is available and had the showPrompt flag set, then
-        // show an update available doorhanger.
-        this.clearCallbacks();
-        this.showUpdateAvailableNotification(update, false);
-        break;
-      case "cant-apply":
-        this.clearCallbacks();
-        this.showManualUpdateNotification(update, false);
-        break;
-    }
-  },
-
-  observe(subject, topic, status) {
-    if (!this.enabled) {
-      return;
-    }
-
-    let update = subject && subject.QueryInterface(Ci.nsIUpdate);
-
-    switch (topic) {
-      case "update-available":
-        this.handleUpdateAvailable(update, status);
-        break;
-      case "update-staged":
-      case "update-downloaded":
-        this.handleUpdateStagedOrDownloaded(update, status);
-        break;
-      case "update-error":
-        this.handleUpdateError(update, status);
-        break;
-    }
-  }
-};
-
 // Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json
 const TLS_ERROR_REPORT_TELEMETRY_AUTO_CHECKED   = 2;
 const TLS_ERROR_REPORT_TELEMETRY_AUTO_UNCHECKED = 3;
 const TLS_ERROR_REPORT_TELEMETRY_MANUAL_SEND    = 4;
 const TLS_ERROR_REPORT_TELEMETRY_AUTO_SEND      = 5;
 
 const PREF_SSL_IMPACT_ROOTS = ["security.tls.version.", "security.ssl3."];
 
--- a/browser/base/content/moz.build
+++ b/browser/base/content/moz.build
@@ -23,19 +23,16 @@ with Files("newtab/**"):
     BUG_COMPONENT = ("Firefox", "New Tab Page")
 
 with Files("pageinfo/**"):
     BUG_COMPONENT = ("Firefox", "Page Info Window")
 
 with Files("test/alerts/**"):
     BUG_COMPONENT = ("Toolkit", "Notifications and Alerts")
 
-with Files("test/appUpdate/**"):
-    BUG_COMPONENT = ("Toolkit", "Application Update")
-
 with Files("test/captivePortal/**"):
     BUG_COMPONENT = ("Firefox", "General")
 
 with Files("test/chrome/**"):
     BUG_COMPONENT = ("Firefox", "General")
 
 with Files("test/forms/**"):
     BUG_COMPONENT = ("Core", "Layout: Form Controls")
--- a/browser/base/content/test/sync/browser.ini
+++ b/browser/base/content/test/sync/browser.ini
@@ -1,9 +1,10 @@
 [browser_sync.js]
 [browser_fxa_web_channel.js]
 support-files=
   browser_fxa_web_channel.html
+[browser_fxa_badge.js]
 [browser_aboutAccounts.js]
 skip-if = os == "linux" # Bug 958026
 support-files =
   content_aboutAccounts.js
   accounts_testRemoteCommands.html
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/sync/browser_fxa_badge.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/AppMenuNotifications.jsm");
+
+add_task(async function test_unconfigured_no_badge() {
+  const oldUIState = UIState.get;
+
+  UIState.get = () => ({
+    status: UIState.STATUS_NOT_CONFIGURED
+  });
+  Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+  checkFxABadge(false);
+
+  UIState.get = oldUIState;
+});
+
+add_task(async function test_signedin_no_badge() {
+  const oldUIState = UIState.get;
+
+  UIState.get = () => ({
+    status: UIState.STATUS_SIGNED_IN,
+    email: "foo@bar.com"
+  });
+  Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+  checkFxABadge(false);
+
+  UIState.get = oldUIState;
+});
+
+add_task(async function test_unverified_badge_shown() {
+  const oldUIState = UIState.get;
+
+  UIState.get = () => ({
+    status: UIState.STATUS_NOT_VERIFIED,
+    email: "foo@bar.com"
+  });
+  Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+  checkFxABadge(true);
+
+  UIState.get = oldUIState;
+});
+
+add_task(async function test_loginFailed_badge_shown() {
+  const oldUIState = UIState.get;
+
+  UIState.get = () => ({
+    status: UIState.STATUS_LOGIN_FAILED,
+    email: "foo@bar.com"
+  });
+  Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+  checkFxABadge(true);
+
+  UIState.get = oldUIState;
+});
+
+function checkFxABadge(shouldBeShown) {
+  let isShown = false;
+  for (let notification of AppMenuNotifications.notifications) {
+    if (notification.id == "fxa-needs-authentication") {
+      isShown = true;
+      break;
+    }
+  }
+  is(isShown, shouldBeShown, "Fxa badge shown matches expected value.");
+}
--- a/browser/base/content/test/sync/browser_sync.js
+++ b/browser/base/content/test/sync/browser_sync.js
@@ -21,17 +21,16 @@ add_task(async function test_ui_state_si
     displayName: "Foo Bar",
     avatarURL: "https://foo.bar",
     lastSync: new Date(),
     syncing: false
   };
 
   gSync.updateAllUI(state);
 
-  checkFxABadge(false);
   let statusBarTooltip = gSync.appMenuStatus.getAttribute("signedinTooltiptext");
   let lastSyncTooltip = gSync.formatLastSyncDate(new Date(state.lastSync));
   checkPanelUIStatusBar({
     label: "Foo Bar",
     tooltip: statusBarTooltip,
     fxastatus: "signedin",
     avatarURL: "https://foo.bar",
     syncing: false,
@@ -69,17 +68,16 @@ add_task(async function test_ui_state_sy
 
 add_task(async function test_ui_state_unconfigured() {
   let state = {
     status: UIState.STATUS_NOT_CONFIGURED
   };
 
   gSync.updateAllUI(state);
 
-  checkFxABadge(false);
   let signedOffLabel = gSync.appMenuStatus.getAttribute("defaultlabel");
   let statusBarTooltip = gSync.appMenuStatus.getAttribute("signedinTooltiptext");
   checkPanelUIStatusBar({
     label: signedOffLabel,
     tooltip: statusBarTooltip
   });
   checkRemoteTabsPanel("PanelUI-remotetabs-setupsync");
   checkMenuBarItem("sync-setup");
@@ -90,17 +88,16 @@ add_task(async function test_ui_state_un
     status: UIState.STATUS_NOT_VERIFIED,
     email: "foo@bar.com",
     lastSync: new Date(),
     syncing: false
   };
 
   gSync.updateAllUI(state);
 
-  checkFxABadge(true);
   let expectedLabel = gSync.appMenuStatus.getAttribute("unverifiedlabel");
   let tooltipText = gSync.fxaStrings.formatStringFromName("verifyDescription", [state.email], 1);
   checkPanelUIStatusBar({
     label: expectedLabel,
     tooltip: tooltipText,
     fxastatus: "unverified",
     avatarURL: null,
     syncing: false,
@@ -113,17 +110,16 @@ add_task(async function test_ui_state_un
 add_task(async function test_ui_state_loginFailed() {
   let state = {
     status: UIState.STATUS_LOGIN_FAILED,
     email: "foo@bar.com"
   };
 
   gSync.updateAllUI(state);
 
-  checkFxABadge(true);
   let expectedLabel = gSync.appMenuStatus.getAttribute("errorlabel");
   let tooltipText = gSync.fxaStrings.formatStringFromName("reconnectDescription", [state.email], 1);
   checkPanelUIStatusBar({
     label: expectedLabel,
     tooltip: tooltipText,
     fxastatus: "login-failed",
     avatarURL: null,
     syncing: false,
@@ -143,27 +139,16 @@ add_task(async function test_FormatLastS
 add_task(async function test_FormatLastSyncDateMonthAgo() {
   let monthAgo = new Date();
   monthAgo.setMonth(monthAgo.getMonth() - 1);
   let monthAgoString = gSync.formatLastSyncDate(monthAgo);
   is(monthAgoString, "Last sync: " + monthAgo.toLocaleDateString(undefined, {month: "long", day: "numeric"}),
      "The date is correctly formatted");
 });
 
-function checkFxABadge(shouldBeShown) {
-  let isShown = false;
-  for (let notification of PanelUI.notifications) {
-    if (notification.id == "fxa-needs-authentication") {
-      isShown = true;
-      break;
-    }
-  }
-  is(isShown, shouldBeShown, "the fxa badge has the right visibility");
-}
-
 function checkPanelUIStatusBar({label, tooltip, fxastatus, avatarURL, syncing, syncNowTooltip}) {
   let prefix = gPhotonStructure ? "appMenu" : "PanelUI"
   let labelNode = document.getElementById(`${prefix}-fxa-label`);
   let tooltipNode = document.getElementById(`${prefix}-fxa-status`);
   let statusNode = document.getElementById(`${prefix}-fxa-container`);
   let avatar = document.getElementById(`${prefix}-fxa-avatar`);
 
   is(labelNode.getAttribute("label"), label, "fxa label has the right value");
--- a/browser/base/moz.build
+++ b/browser/base/moz.build
@@ -36,33 +36,23 @@ BROWSER_CHROME_MANIFESTS += [
     'content/test/tabcrashed/browser.ini',
     'content/test/tabPrompts/browser.ini',
     'content/test/tabs/browser.ini',
     'content/test/urlbar/browser.ini',
     'content/test/webextensions/browser.ini',
     'content/test/webrtc/browser.ini',
 ]
 
-if CONFIG['MOZ_UPDATER']:
-    BROWSER_CHROME_MANIFESTS += ['content/test/appUpdate/browser.ini']
-
 DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
 DEFINES['MOZ_APP_VERSION_DISPLAY'] = CONFIG['MOZ_APP_VERSION_DISPLAY']
 
 DEFINES['APP_LICENSE_BLOCK'] = '%s/content/overrides/app-license.html' % SRCDIR
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3', 'cocoa'):
     DEFINES['CONTEXT_COPY_IMAGE_CONTENTS'] = 1
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'cocoa'):
     DEFINES['CAN_DRAW_IN_TITLEBAR'] = 1
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3'):
     DEFINES['MENUBAR_CAN_AUTOHIDE'] = 1
 
-TEST_HARNESS_FILES.testing.mochitest.browser.browser.base.content.test.appUpdate += [
-    '/toolkit/mozapps/update/tests/chrome/update.sjs',
-    '/toolkit/mozapps/update/tests/data/shared.js',
-    '/toolkit/mozapps/update/tests/data/sharedUpdateXML.js',
-    '/toolkit/mozapps/update/tests/data/simple.mar',
-]
-
 JAR_MANIFESTS += ['jar.mn']
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -5,16 +5,18 @@
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
                                   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler",
                                   "resource:///modules/ScrollbarSampler.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
                                   "resource://gre/modules/ShortcutUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppMenuNotifications",
+                                  "resource://gre/modules/AppMenuNotifications.jsm");
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "gPhotonStructure",
   "browser.photon.structure.enabled", false);
 
 /**
  * Maintains the state and dispatches events for the main menu panel.
  */
 
@@ -41,38 +43,39 @@ const PanelUI = {
 
       overflowFixedList: gPhotonStructure ? "widget-overflow-fixed-list" : "",
       overflowPanel: gPhotonStructure ? "widget-overflow" : "",
       navbar: "nav-bar",
     };
   },
 
   _initialized: false,
+  _notifications: null,
+
   init() {
     this._initElements();
 
-    this.notifications = [];
     this.menuButton.addEventListener("mousedown", this);
     this.menuButton.addEventListener("keypress", this);
     this._overlayScrollListenerBoundFn = this._overlayScrollListener.bind(this);
 
     Services.obs.addObserver(this, "fullscreen-nav-toolbox");
-    Services.obs.addObserver(this, "panelUI-notification-main-action");
-    Services.obs.addObserver(this, "panelUI-notification-dismissed");
+    Services.obs.addObserver(this, "appMenu-notifications");
 
     window.addEventListener("fullscreen", this);
     window.addEventListener("activate", this);
     window.matchMedia("(-moz-overlay-scrollbars)").addListener(this._overlayScrollListenerBoundFn);
     CustomizableUI.addListener(this);
 
     for (let event of this.kEvents) {
       this.notificationPanel.addEventListener(event, this);
     }
 
     this._initPhotonPanel();
+    Services.obs.notifyObservers(null, "appMenu-notifications-request", "refresh");
 
     this._initialized = true;
   },
 
   reinit() {
     this._removeEventListeners();
     // If the Photon pref changes, we need to re-init our element references.
     this._initElements();
@@ -137,18 +140,17 @@ const PanelUI = {
 
   uninit() {
     this._removeEventListeners();
     for (let event of this.kEvents) {
       this.notificationPanel.removeEventListener(event, this);
     }
 
     Services.obs.removeObserver(this, "fullscreen-nav-toolbox");
-    Services.obs.removeObserver(this, "panelUI-notification-main-action");
-    Services.obs.removeObserver(this, "panelUI-notification-dismissed");
+    Services.obs.removeObserver(this, "appMenu-notifications");
 
     window.removeEventListener("fullscreen", this);
     window.removeEventListener("activate", this);
     this.menuButton.removeEventListener("mousedown", this);
     this.menuButton.removeEventListener("keypress", this);
     window.matchMedia("(-moz-overlay-scrollbars)").removeListener(this._overlayScrollListenerBoundFn);
     CustomizableUI.removeListener(this);
     this._overlayScrollListenerBoundFn = null;
@@ -227,104 +229,41 @@ const PanelUI = {
         anchor = this._getPanelAnchor(anchor);
         this.panel.openPopup(anchor);
       }, (reason) => {
         console.error("Error showing the PanelUI menu", reason);
       });
     });
   },
 
-  showNotification(id, mainAction, secondaryActions = [], options = {}) {
-    let notification = new PanelUINotification(id, mainAction, secondaryActions, options);
-    let existingIndex = this.notifications.findIndex(n => n.id == id);
-    if (existingIndex != -1) {
-      this.notifications.splice(existingIndex, 1);
-    }
-
-    // We don't want to clobber doorhanger notifications just to show a badge,
-    // so don't dismiss any of them and the badge will show once the doorhanger
-    // gets resolved.
-    if (!options.badgeOnly && !options.dismissed) {
-      this.notifications.forEach(n => { n.dismissed = true; });
-    }
-
-    // Since notifications are generally somewhat pressing, the ideal case is that
-    // we never have two notifications at once. However, in the event that we do,
-    // it's more likely that the older notification has been sitting around for a
-    // bit, and so we don't want to hide the new notification behind it. Thus,
-    // we want our notifications to behave like a stack instead of a queue.
-    this.notifications.unshift(notification);
-    this._updateNotifications();
-    return notification;
-  },
-
-  showBadgeOnlyNotification(id) {
-    return this.showNotification(id, null, null, { badgeOnly: true });
-  },
-
-  removeNotification(id) {
-    let notifications;
-    if (typeof id == "string") {
-      notifications = this.notifications.filter(n => n.id == id);
-    } else {
-      // If it's not a string, assume RegExp
-      notifications = this.notifications.filter(n => id.test(n.id));
-    }
-    // _updateNotifications can be expensive if it forces attachment of XBL
-    // bindings that haven't been used yet, so return early if we haven't found
-    // any notification to remove, as callers may expect this removeNotification
-    // method to be a no-op for non-existent notifications.
-    if (!notifications.length) {
-      return;
-    }
-
-    notifications.forEach(n => {
-      this._removeNotification(n);
-    });
-    this._updateNotifications();
-  },
-
-  dismissNotification(id) {
-    let notifications;
-    if (typeof id == "string") {
-      notifications = this.notifications.filter(n => n.id == id);
-    } else {
-      // If it's not a string, assume RegExp
-      notifications = this.notifications.filter(n => id.test(n.id));
-    }
-
-    notifications.forEach(n => n.dismissed = true);
-    this._updateNotifications();
-  },
-
   /**
    * If the menu panel is being shown, hide it.
    */
   hide() {
     if (document.documentElement.hasAttribute("customizing")) {
       return;
     }
 
     this.panel.hidePopup();
   },
 
   observe(subject, topic, status) {
     switch (topic) {
       case "fullscreen-nav-toolbox":
-        this._updateNotifications();
-        break;
-      case "panelUI-notification-main-action":
-        if (subject != window) {
-          this.removeNotification(status);
+        if (this._notifications) {
+          this._updateNotifications(false);
         }
         break;
-      case "panelUI-notification-dismissed":
-        if (subject != window) {
-          this.dismissNotification(status);
+      case "appMenu-notifications":
+        // Don't initialize twice.
+        if (status == "init" && this._notifications) {
+          break;
         }
+        this._notifications = AppMenuNotifications.notifications;
+        this._updateNotifications(true);
         break;
     }
   },
 
   handleEvent(aEvent) {
     // Ignore context menus and menu button menus showing and hiding:
     if (aEvent.type.startsWith("popup") &&
         aEvent.target != this.panel) {
@@ -371,26 +310,16 @@ const PanelUI = {
   },
 
   get isNotificationPanelOpen() {
     let panelState = this.notificationPanel.state;
 
     return panelState == "showing" || panelState == "open";
   },
 
-  get activeNotification() {
-    if (this.notifications.length > 0) {
-      const doorhanger =
-        this.notifications.find(n => !n.dismissed && !n.options.badgeOnly);
-      return doorhanger || this.notifications[0];
-    }
-
-    return null;
-  },
-
   /**
    * Registering the menu panel is done lazily for performance reasons. This
    * method is exposed so that CustomizationMode can force panel-readyness in the
    * event that customization mode is started before the panel has been opened
    * by the user.
    *
    * @param aCustomizing (optional) set to true if this was called while entering
    *        customization mode. If that's the case, we trust that customization
@@ -744,64 +673,71 @@ const PanelUI = {
   },
 
   _hidePopup() {
     if (this.isNotificationPanelOpen) {
       this.notificationPanel.hidePopup();
     }
   },
 
-  _updateNotifications() {
-    if (!this.notifications.length) {
-      this._clearAllNotifications();
-      this._hidePopup();
+  _updateNotifications(notificationsChanged) {
+    let notifications = this._notifications;
+    if (!notifications || !notifications.length) {
+      if (notificationsChanged) {
+        this._clearAllNotifications();
+        this._hidePopup();
+      }
       return;
     }
 
     if (window.fullScreen && FullScreen.navToolboxHidden) {
       this._hidePopup();
       return;
     }
 
     let doorhangers =
-      this.notifications.filter(n => !n.dismissed && !n.options.badgeOnly);
+      notifications.filter(n => !n.dismissed && !n.options.badgeOnly);
 
     if (this.panel.state == "showing" || this.panel.state == "open") {
       // If the menu is already showing, then we need to dismiss all notifications
       // since we don't want their doorhangers competing for attention
       doorhangers.forEach(n => { n.dismissed = true; })
       this._hidePopup();
       this._clearBadge();
-      if (!this.notifications[0].options.badgeOnly) {
-        this._showBannerItem(this.notifications[0]);
+      if (!notifications[0].options.badgeOnly) {
+        this._showBannerItem(notifications[0]);
       }
     } else if (doorhangers.length > 0) {
       // Only show the doorhanger if the window is focused and not fullscreen
       if (window.fullScreen || Services.focus.activeWindow !== window) {
         this._hidePopup();
         this._showBadge(doorhangers[0]);
         this._showBannerItem(doorhangers[0]);
       } else {
         this._clearBadge();
         this._showNotificationPanel(doorhangers[0]);
       }
     } else {
       this._hidePopup();
-      this._showBadge(this.notifications[0]);
-      this._showBannerItem(this.notifications[0]);
+      this._showBadge(notifications[0]);
+      this._showBannerItem(notifications[0]);
     }
   },
 
   _showNotificationPanel(notification) {
     this._refreshNotificationPanel(notification);
 
     if (this.isNotificationPanelOpen) {
       return;
     }
 
+    if (notification.options.beforeShowDoorhanger) {
+      notification.options.beforeShowDoorhanger(document);
+    }
+
     let anchor = this._getPanelAnchor(this.menuButton);
 
     this.notificationPanel.hidden = false;
     this.notificationPanel.openPopup(anchor, "bottomcenter topright");
   },
 
   _clearNotificationPanel() {
     for (let popupnotification of this.notificationPanel.children) {
@@ -859,89 +795,41 @@ const PanelUI = {
 
   _clearBannerItem() {
     if (this._panelBannerItem) {
       this._panelBannerItem.notification = null;
       this._panelBannerItem.hidden = true;
     }
   },
 
-  _removeNotification(notification) {
-    // This notification may already be removed, in which case let's just fail
-    // silently.
-    let notifications = this.notifications;
-    if (!notifications)
-      return;
-
-    var index = notifications.indexOf(notification);
-    if (index == -1)
-      return;
-
-    // Remove the notification
-    notifications.splice(index, 1);
-  },
-
   _onNotificationButtonEvent(event, type) {
     let notificationEl = getNotificationFromElement(event.originalTarget);
 
     if (!notificationEl)
       throw "PanelUI._onNotificationButtonEvent: couldn't find notification element";
 
     if (!notificationEl.notification)
       throw "PanelUI._onNotificationButtonEvent: couldn't find notification";
 
     let notification = notificationEl.notification;
 
-    let action = notification.mainAction;
-
     if (type == "secondarybuttoncommand") {
-      action = notification.secondaryActions[0];
+      AppMenuNotifications.callSecondaryAction(window, notification);
+    } else {
+      AppMenuNotifications.callMainAction(window, notification, true);
     }
-
-    let dismiss = true;
-    if (action) {
-      try {
-        if (action === notification.mainAction) {
-          action.callback(true);
-          this._notify(notification.id, "main-action");
-        } else {
-          action.callback();
-        }
-      } catch (error) {
-        Cu.reportError(error);
-      }
-
-      dismiss = action.dismiss;
-    }
-
-    if (dismiss) {
-      notification.dismissed = true;
-      this._notify(notification.id, "dismissed");
-    } else {
-      this._removeNotification(notification);
-    }
-    this._updateNotifications();
   },
 
   _onBannerItemSelected(event) {
     let target = event.originalTarget;
     if (!target.notification)
       throw "menucommand target has no associated action/notification";
 
     event.stopPropagation();
-
-    try {
-      target.notification.mainAction.callback(false);
-      this._notify(target.notification.id, "main-action");
-    } catch (error) {
-      Cu.reportError(error);
-    }
-
-    this._removeNotification(target.notification);
-    this._updateNotifications();
+    AppMenuNotifications.callMainAction(window, target.notification, false);
   },
 
   _getPopupId(notification) { return "appMenu-" + notification.id + "-notification"; },
 
   _getBadgeStatus(notification) { return notification.id; },
 
   _getPanelAnchor(candidate) {
     let iconAnchor =
@@ -960,40 +848,28 @@ const PanelUI = {
       let keyId = button.getAttribute("key");
       let key = document.getElementById(keyId);
       if (!key) {
         continue;
       }
       button.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(key));
     }
   },
-
-  _notify(status, topic) {
-    Services.obs.notifyObservers(window, "panelUI-notification-" + topic, status);
-  }
 };
 
 XPCOMUtils.defineConstant(this, "PanelUI", PanelUI);
 
 /**
  * Gets the currently selected locale for display.
  * @return  the selected locale
  */
 function getLocale() {
   return Services.locale.getAppLocaleAsLangTag();
 }
 
-function PanelUINotification(id, mainAction, secondaryActions = [], options = {}) {
-  this.id = id;
-  this.mainAction = mainAction;
-  this.secondaryActions = secondaryActions;
-  this.options = options;
-  this.dismissed = this.options.dismissed || false;
-}
-
 function getNotificationFromElement(aElement) {
   // Need to find the associated notification object, which is a bit tricky
   // since it isn't associated with the element directly - this is kind of
   // gross and very dependent on the structure of the popupnotification
   // binding's content.
   let notificationEl;
   let parent = aElement;
   while (parent && (parent = aElement.ownerDocument.getBindingParent(parent))) {
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -146,14 +146,16 @@ skip-if = os == "mac"
 [browser_1096763_seen_widgets_post_reset.js]
 [browser_1161838_inserted_new_default_buttons.js]
 [browser_bootstrapped_custom_toolbar.js]
 [browser_customizemode_contextmenu_menubuttonstate.js]
 [browser_exit_background_customize_mode.js]
 [browser_overflow_use_subviews.js]
 [browser_panel_toggle.js]
 [browser_panelUINotifications.js]
+[browser_panelUINotifications_fullscreen.js]
+[browser_panelUINotifications_multiWindow.js]
 [browser_switch_to_customize_mode.js]
 [browser_synced_tabs_menu.js]
 [browser_check_tooltips_in_navbar.js]
 [browser_editcontrols_update.js]
 subsuite = clipboard
 [browser_remote_tabs_button.js]
--- a/browser/components/customizableui/test/browser_panelUINotifications.js
+++ b/browser/components/customizableui/test/browser_panelUINotifications.js
@@ -1,10 +1,12 @@
 "use strict";
 
+Cu.import("resource://gre/modules/AppMenuNotifications.jsm");
+
 /**
  * Tests that when we click on the main call-to-action of the doorhanger, the provided
  * action is called, and the doorhanger removed.
  */
 add_task(async function testMainActionCalled() {
   let options = {
     gBrowser: window.gBrowser,
     url: "about:blank"
@@ -13,17 +15,17 @@ add_task(async function testMainActionCa
   await BrowserTestUtils.withNewTab(options, function(browser) {
     let doc = browser.ownerDocument;
 
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
     let mainActionCalled = false;
     let mainAction = {
       callback: () => { mainActionCalled = true; }
     };
-    PanelUI.showNotification("update-manual", mainAction);
+    AppMenuNotifications.showNotification("update-manual", mainAction);
 
     isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
     let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     let doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
 
     let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
@@ -31,118 +33,16 @@ add_task(async function testMainActionCa
 
     ok(mainActionCalled, "Main action callback was called");
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
     is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
   });
 });
 
 /**
- * Tests that when we try to show a notification in a background window, it
- * does not display until the window comes back into the foreground. However,
- * it should display a badge.
- */
-add_task(async function testDoesNotShowDoorhangerForBackgroundWindow() {
-  let options = {
-    gBrowser: window.gBrowser,
-    url: "about:blank"
-  };
-
-  await BrowserTestUtils.withNewTab(options, async function(browser) {
-    let doc = browser.ownerDocument;
-
-    let win = await BrowserTestUtils.openNewBrowserWindow();
-    let mainActionCalled = false;
-    let mainAction = {
-      callback: () => { mainActionCalled = true; }
-    };
-    PanelUI.showNotification("update-manual", mainAction);
-    is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
-    is(PanelUI.menuButton.hasAttribute("badge-status"), true, "The background window has a badge.");
-
-    await BrowserTestUtils.closeWindow(win);
-    await SimpleTest.promiseFocus(window);
-    isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
-    let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
-    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
-    let doorhanger = notifications[0];
-    is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
-
-    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
-    button.click();
-
-    ok(mainActionCalled, "Main action callback was called");
-    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
-    is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
-  });
-});
-
-/**
- * Tests that when we try to show a notification in a background window and in
- * a foreground window, if the foreground window's main action is called, the
- * background window's doorhanger will be removed.
- */
-add_task(async function testBackgroundWindowNotificationsAreRemovedByForeground() {
-  let options = {
-    gBrowser: window.gBrowser,
-    url: "about:blank"
-  };
-
-  await BrowserTestUtils.withNewTab(options, async function(browser) {
-    let win = await BrowserTestUtils.openNewBrowserWindow();
-    PanelUI.showNotification("update-manual", {callback() {}});
-    win.PanelUI.showNotification("update-manual", {callback() {}});
-    let doc = win.gBrowser.ownerDocument;
-    let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
-    let doorhanger = notifications[0];
-    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
-    button.click();
-
-    await BrowserTestUtils.closeWindow(win);
-    await SimpleTest.promiseFocus(window);
-
-    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
-    is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
-  });
-});
-
-/**
- * Tests that when we try to show a notification in a background window and in
- * a foreground window, if the foreground window's doorhanger is dismissed,
- * the background window's doorhanger will also be dismissed once the window
- * regains focus.
- */
-add_task(async function testBackgroundWindowNotificationsAreDismissedByForeground() {
-  let options = {
-    gBrowser: window.gBrowser,
-    url: "about:blank"
-  };
-
-  await BrowserTestUtils.withNewTab(options, async function(browser) {
-    let win = await BrowserTestUtils.openNewBrowserWindow();
-    PanelUI.showNotification("update-manual", {callback() {}});
-    win.PanelUI.showNotification("update-manual", {callback() {}});
-    let doc = win.gBrowser.ownerDocument;
-    let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
-    let doorhanger = notifications[0];
-    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
-    button.click();
-
-    await BrowserTestUtils.closeWindow(win);
-    await SimpleTest.promiseFocus(window);
-
-    is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
-    is(PanelUI.menuButton.hasAttribute("badge-status"), true,
-       "The dismissed notification should still have a badge status");
-
-    PanelUI.removeNotification(/.*/);
-  });
-});
-
-/**
  * This tests that when we click the secondary action for a notification,
  * it will display the badge for that notification on the PanelUI menu button.
  * Once we click on this button, we should see an item in the menu which will
  * call our main action.
  */
 add_task(async function testSecondaryActionWorkflow() {
   let options = {
     gBrowser: window.gBrowser,
@@ -153,17 +53,17 @@ add_task(async function testSecondaryAct
     let doc = browser.ownerDocument;
 
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
 
     let mainActionCalled = false;
     let mainAction = {
       callback: () => { mainActionCalled = true; },
     };
-    PanelUI.showNotification("update-manual", mainAction);
+    AppMenuNotifications.showNotification("update-manual", mainAction);
 
     isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
     let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     let doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
 
     let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
@@ -181,39 +81,39 @@ add_task(async function testSecondaryAct
 
     await PanelUI.hide();
     is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is shown on PanelUI button.");
 
     await PanelUI.show();
     menuItem.click();
     ok(mainActionCalled, "Main action callback was called");
 
-    PanelUI.removeNotification(/.*/);
+    AppMenuNotifications.removeNotification(/.*/);
   });
 });
 
 /**
  * We want to ensure a few things with this:
  * - Adding a doorhanger will make a badge disappear
  * - once the notification for the doorhanger is resolved (removed, not just dismissed),
  *   then we display any other badges that are remaining.
  */
 add_task(async function testInteractionWithBadges() {
   await BrowserTestUtils.withNewTab("about:blank", async function(browser) {
     let doc = browser.ownerDocument;
 
-    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+    AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
     is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
 
     let mainActionCalled = false;
     let mainAction = {
       callback: () => { mainActionCalled = true; },
     };
-    PanelUI.showNotification("update-manual", mainAction);
+    AppMenuNotifications.showNotification("update-manual", mainAction);
 
     isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
     isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
     let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     let doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
 
@@ -229,96 +129,96 @@ add_task(async function testInteractionW
     let menuItem = PanelUI.mainView.querySelector(".panel-banner-item");
     is(menuItem.label, menuItem.getAttribute("label-update-manual"), "Showing correct label");
     is(menuItem.hidden, false, "update-manual menu item is showing.");
 
     menuItem.click();
     ok(mainActionCalled, "Main action callback was called");
 
     is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
-    PanelUI.removeNotification(/.*/);
+    AppMenuNotifications.removeNotification(/.*/);
     is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
   });
 });
 
 /**
  * This tests that adding a badge will not dismiss any existing doorhangers.
  */
 add_task(async function testAddingBadgeWhileDoorhangerIsShowing() {
   await BrowserTestUtils.withNewTab("about:blank", function(browser) {
     let doc = browser.ownerDocument;
 
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
     let mainActionCalled = false;
     let mainAction = {
       callback: () => { mainActionCalled = true; }
     };
-    PanelUI.showNotification("update-manual", mainAction);
-    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+    AppMenuNotifications.showNotification("update-manual", mainAction);
+    AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
 
     isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
     isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
     let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     let doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
 
     let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
     mainActionButton.click();
 
     ok(mainActionCalled, "Main action callback was called");
     is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
     is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
-    PanelUI.removeNotification(/.*/);
+    AppMenuNotifications.removeNotification(/.*/);
     is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
   });
 });
 
 /**
  * Tests that badges operate like a stack.
  */
 add_task(async function testMultipleBadges() {
   await BrowserTestUtils.withNewTab("about:blank", async function(browser) {
     let doc = browser.ownerDocument;
     let menuButton = doc.getElementById("PanelUI-menu-button");
 
     is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
     is(menuButton.hasAttribute("badge"), false, "Should not have the badge attribute set");
 
-    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+    AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
     is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
 
-    PanelUI.showBadgeOnlyNotification("update-succeeded");
+    AppMenuNotifications.showBadgeOnlyNotification("update-succeeded");
     is(menuButton.getAttribute("badge-status"), "update-succeeded", "Should have update-succeeded badge status (update > fxa)");
 
-    PanelUI.showBadgeOnlyNotification("update-failed");
+    AppMenuNotifications.showBadgeOnlyNotification("update-failed");
     is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
 
-    PanelUI.showBadgeOnlyNotification("download-severe");
+    AppMenuNotifications.showBadgeOnlyNotification("download-severe");
     is(menuButton.getAttribute("badge-status"), "download-severe", "Should have download-severe badge status");
 
-    PanelUI.showBadgeOnlyNotification("download-warning");
+    AppMenuNotifications.showBadgeOnlyNotification("download-warning");
     is(menuButton.getAttribute("badge-status"), "download-warning", "Should have download-warning badge status");
 
-    PanelUI.removeNotification(/^download-/);
+    AppMenuNotifications.removeNotification(/^download-/);
     is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
 
-    PanelUI.removeNotification(/^update-/);
+    AppMenuNotifications.removeNotification(/^update-/);
     is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
 
-    PanelUI.removeNotification(/^fxa-/);
+    AppMenuNotifications.removeNotification(/^fxa-/);
     is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
 
     await PanelUI.show();
     is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (Hamburger menu opened)");
     PanelUI.hide();
 
-    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
-    PanelUI.showBadgeOnlyNotification("update-succeeded");
-    PanelUI.removeNotification(/.*/);
+    AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
+    AppMenuNotifications.showBadgeOnlyNotification("update-succeeded");
+    AppMenuNotifications.removeNotification(/.*/);
     is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
   });
 });
 
 /**
  * Tests that non-badges also operate like a stack.
  */
 add_task(async function testMultipleNonBadges() {
@@ -331,28 +231,28 @@ add_task(async function testMultipleNonB
         called: false,
         callback: () => { updateManualAction.called = true; },
     };
     let updateRestartAction = {
         called: false,
         callback: () => { updateRestartAction.called = true; },
     };
 
-    PanelUI.showNotification("update-manual", updateManualAction);
+    AppMenuNotifications.showNotification("update-manual", updateManualAction);
 
     let notifications;
     let doorhanger;
 
     isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
     notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
 
-    PanelUI.showNotification("update-restart", updateRestartAction);
+    AppMenuNotifications.showNotification("update-restart", updateRestartAction);
 
     isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
     notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
     is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
     doorhanger = notifications[0];
     is(doorhanger.id, "appMenu-update-restart-notification", "PanelUI is displaying the update-restart notification.");
 
     let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
@@ -377,47 +277,8 @@ add_task(async function testMultipleNonB
     isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "update-manual badge is hidden on PanelUI button.");
     is(menuItem.label, menuItem.getAttribute("label-update-manual"), "Showing correct label");
     is(menuItem.hidden, false, "update-manual menu item is showing.");
 
     menuItem.click();
     ok(updateManualAction.called, "update-manual main action callback was called");
   });
 });
-
-add_task(async function testFullscreen() {
-  let doc = document;
-
-  is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
-  let mainActionCalled = false;
-  let mainAction = {
-    callback: () => { mainActionCalled = true; }
-  };
-  PanelUI.showNotification("update-manual", mainAction);
-
-  isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
-  let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
-  is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
-  let doorhanger = notifications[0];
-  is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
-
-  let popuphiddenPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popuphidden");
-  EventUtils.synthesizeKey("VK_F11", {});
-  await popuphiddenPromise;
-  await new Promise(executeSoon);
-  is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
-
-  window.FullScreen.showNavToolbox();
-  is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
-
-  let popupshownPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popupshown");
-  EventUtils.synthesizeKey("VK_F11", {});
-  await popupshownPromise;
-  await new Promise(executeSoon);
-  isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
-  isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is not displaying on PanelUI button.");
-
-  let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
-  mainActionButton.click();
-  ok(mainActionCalled, "Main action callback was called");
-  is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
-  is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
-});
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_panelUINotifications_fullscreen.js
@@ -0,0 +1,42 @@
+"use strict";
+
+Cu.import("resource://gre/modules/AppMenuNotifications.jsm");
+
+add_task(async function testFullscreen() {
+  let doc = document;
+
+  is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+  let mainActionCalled = false;
+  let mainAction = {
+    callback: () => { mainActionCalled = true; }
+  };
+  AppMenuNotifications.showNotification("update-manual", mainAction);
+
+  isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+  let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+  is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+  let doorhanger = notifications[0];
+  is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+  let popuphiddenPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popuphidden");
+  EventUtils.synthesizeKey("VK_F11", {});
+  await popuphiddenPromise;
+  await new Promise(executeSoon);
+  is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+  FullScreen.showNavToolbox();
+  is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
+
+  let popupshownPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popupshown");
+  EventUtils.synthesizeKey("VK_F11", {});
+  await popupshownPromise;
+  await new Promise(executeSoon);
+  isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+  isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is not displaying on PanelUI button.");
+
+  let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+  mainActionButton.click();
+  ok(mainActionCalled, "Main action callback was called");
+  is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+  is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js
@@ -0,0 +1,130 @@
+"use strict";
+
+Cu.import("resource://gre/modules/AppMenuNotifications.jsm");
+
+/**
+ * Tests that when we try to show a notification in a background window, it
+ * does not display until the window comes back into the foreground. However,
+ * it should display a badge.
+ */
+add_task(async function testDoesNotShowDoorhangerForBackgroundWindow() {
+  let options = {
+    gBrowser: window.gBrowser,
+    url: "about:blank"
+  };
+
+  await BrowserTestUtils.withNewTab(options, async function(browser) {
+    let doc = browser.ownerDocument;
+
+    let win = await BrowserTestUtils.openNewBrowserWindow();
+    let mainActionCalled = false;
+    let mainAction = {
+      callback: () => { mainActionCalled = true; }
+    };
+    AppMenuNotifications.showNotification("update-manual", mainAction);
+    is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
+    is(PanelUI.menuButton.hasAttribute("badge-status"), true, "The background window has a badge.");
+
+    await BrowserTestUtils.closeWindow(win);
+    await SimpleTest.promiseFocus(window);
+    isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+    let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    let doorhanger = notifications[0];
+    is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+    button.click();
+
+    ok(mainActionCalled, "Main action callback was called");
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+  });
+});
+
+/**
+ * Tests that when we try to show a notification in a background window and in
+ * a foreground window, if the foreground window's main action is called, the
+ * background window's doorhanger will be removed.
+ */
+add_task(async function testBackgroundWindowNotificationsAreRemovedByForeground() {
+  let options = {
+    gBrowser: window.gBrowser,
+    url: "about:blank"
+  };
+
+  await BrowserTestUtils.withNewTab(options, async function(browser) {
+    let win = await BrowserTestUtils.openNewBrowserWindow();
+    AppMenuNotifications.showNotification("update-manual", {callback() {}});
+    let doc = win.gBrowser.ownerDocument;
+    let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    let doorhanger = notifications[0];
+    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+    button.click();
+
+    await BrowserTestUtils.closeWindow(win);
+    await SimpleTest.promiseFocus(window);
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+  });
+});
+
+/**
+ * Tests that when we try to show a notification in a background window and in
+ * a foreground window, if the foreground window's doorhanger is dismissed,
+ * the background window's doorhanger will also be dismissed once the window
+ * regains focus.
+ */
+add_task(async function testBackgroundWindowNotificationsAreDismissedByForeground() {
+  let options = {
+    gBrowser: window.gBrowser,
+    url: "about:blank"
+  };
+
+  await BrowserTestUtils.withNewTab(options, async function(browser) {
+    let win = await BrowserTestUtils.openNewBrowserWindow();
+    AppMenuNotifications.showNotification("update-manual", {callback() {}});
+    let doc = win.gBrowser.ownerDocument;
+    let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    let doorhanger = notifications[0];
+    let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+    button.click();
+
+    await BrowserTestUtils.closeWindow(win);
+    await SimpleTest.promiseFocus(window);
+
+    is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
+    is(PanelUI.menuButton.hasAttribute("badge-status"), true,
+       "The dismissed notification should still have a badge status");
+
+    AppMenuNotifications.removeNotification(/.*/);
+  });
+});
+
+/**
+ * Tests that when we open a new window while a notification is showing, the
+ * notification also shows on the new window.
+ */
+add_task(async function testOpenWindowAfterShowingNotification() {
+  AppMenuNotifications.showNotification("update-manual", {callback() {}});
+
+  let win = await BrowserTestUtils.openNewBrowserWindow();
+  let doc = win.gBrowser.ownerDocument;
+  let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
+  is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+  let doorhanger = notifications[0];
+  let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+  button.click();
+
+  await BrowserTestUtils.closeWindow(win);
+  await SimpleTest.promiseFocus(window);
+
+  is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
+  is(PanelUI.menuButton.hasAttribute("badge-status"), true,
+     "The dismissed notification should still have a badge status");
+
+  AppMenuNotifications.removeNotification(/.*/);
+});
--- a/browser/components/downloads/DownloadsCommon.jsm
+++ b/browser/components/downloads/DownloadsCommon.jsm
@@ -36,16 +36,20 @@ const { classes: Cc, interfaces: Ci, uti
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppMenuNotifications",
+                                  "resource://gre/modules/AppMenuNotifications.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
+                                  "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
                                   "resource://gre/modules/Downloads.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
                                   "resource://gre/modules/DownloadUIHelper.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
                                   "resource://gre/modules/DownloadUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
@@ -1198,16 +1202,28 @@ DownloadsIndicatorDataCtor.prototype = {
    */
   _updateViews() {
     // Do not update the status indicators during batch loads of download items.
     if (this._loading) {
       return;
     }
 
     this._refreshProperties();
+
+    let widgetGroup = CustomizableUI.getWidget("downloads-button");
+    let inMenu = widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL;
+    if (inMenu) {
+      if (this._attention == DownloadsCommon.ATTENTION_NONE) {
+        AppMenuNotifications.removeNotification(/^download-/);
+      } else {
+        let badgeClass = "download-" + this._attention;
+        AppMenuNotifications.showBadgeOnlyNotification(badgeClass);
+      }
+    }
+
     this._views.forEach(this._updateView, this);
   },
 
   /**
    * Updates the specified view with the current aggregate values.
    *
    * @param aView
    *        DownloadsIndicatorView object to be updated.
--- a/browser/components/downloads/content/indicator.js
+++ b/browser/components/downloads/content/indicator.js
@@ -436,25 +436,18 @@ const DownloadsIndicatorView = {
     // For arrow-Styled indicator, suppress success attention if we have
     // progress in toolbar
     let suppressAttention = !inMenu &&
       this._attention == DownloadsCommon.ATTENTION_SUCCESS &&
       this._percentComplete >= 0;
 
     if (suppressAttention || this._attention == DownloadsCommon.ATTENTION_NONE) {
       this.indicator.removeAttribute("attention");
-      if (inMenu) {
-        PanelUI.removeNotification(/^download-/);
-      }
     } else {
       this.indicator.setAttribute("attention", this._attention);
-      if (inMenu) {
-        let badgeClass = "download-" + this._attention;
-        PanelUI.showBadgeOnlyNotification(badgeClass);
-      }
     }
   },
   _attention: DownloadsCommon.ATTENTION_NONE,
 
   // User interface event functions
 
   onWindowUnload() {
     // This function is registered as an event listener, we can't use "this".
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -17,44 +17,47 @@ Cu.import("resource://gre/modules/AsyncP
 XPCOMUtils.defineLazyServiceGetter(this, "WindowsUIUtils", "@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils");
 XPCOMUtils.defineLazyServiceGetter(this, "AlertsService", "@mozilla.org/alerts-service;1", "nsIAlertsService");
 XPCOMUtils.defineLazyGetter(this, "WeaveService", () =>
   Cc["@mozilla.org/weave/service;1"].getService().wrappedJSObject
 );
 
 // lazy module getters
 
-/* global AboutHome:false, AboutNewTab:false, AddonManager:false,
+/* global AboutHome:false, AboutNewTab:false, AddonManager:false, AppMenuNotifications:false,
           AsyncShutdown:false, AutoCompletePopup:false, BookmarkHTMLUtils:false,
           BookmarkJSONUtils:false, BrowserUITelemetry:false, BrowserUsageTelemetry:false,
           ContentClick:false, ContentPrefServiceParent:false, ContentSearch:false,
           DateTimePickerHelper:false, DirectoryLinksProvider:false,
           ExtensionsUI:false, Feeds:false,
           FileUtils:false, FormValidationHandler:false, Integration:false,
           LightweightThemeManager:false, LoginHelper:false, LoginManagerParent:false,
           NetUtil:false, NewTabUtils:false, OS:false,
           PageThumbs:false, PdfJs:false, PermissionUI:false, PlacesBackups:false,
           PlacesUtils:false, PluralForm:false, PrivateBrowsingUtils:false,
           ProcessHangMonitor:false, ReaderParent:false, RecentWindow:false,
           RemotePrompt:false, SessionStore:false,
           ShellService:false, SimpleServiceDiscovery:false, TabCrashHandler:false,
-          Task:false, UITour:false, WebChannel:false,
+          Task:false, UITour:false, UIState:false, UpdateListener:false, WebChannel:false,
           WindowsRegistry:false, webrtcUI:false */
 
+
+
 /**
  * IF YOU ADD OR REMOVE FROM THIS LIST, PLEASE UPDATE THE LIST ABOVE AS WELL.
  * XXX Bug 1325373 is for making eslint detect these automatically.
  */
 
 let initializedModules = {};
 
 [
   ["AboutHome", "resource:///modules/AboutHome.jsm", "init"],
   ["AboutNewTab", "resource:///modules/AboutNewTab.jsm"],
   ["AddonManager", "resource://gre/modules/AddonManager.jsm"],
+  ["AppMenuNotifications", "resource://gre/modules/AppMenuNotifications.jsm"],
   ["AsyncShutdown", "resource://gre/modules/AsyncShutdown.jsm"],
   ["AutoCompletePopup", "resource://gre/modules/AutoCompletePopup.jsm"],
   ["BookmarkHTMLUtils", "resource://gre/modules/BookmarkHTMLUtils.jsm"],
   ["BookmarkJSONUtils", "resource://gre/modules/BookmarkJSONUtils.jsm"],
   ["BrowserUITelemetry", "resource:///modules/BrowserUITelemetry.jsm"],
   ["BrowserUsageTelemetry", "resource:///modules/BrowserUsageTelemetry.jsm"],
   ["ContentClick", "resource:///modules/ContentClick.jsm"],
   ["ContentPrefServiceParent", "resource://gre/modules/ContentPrefServiceParent.jsm", "alwaysInit"],
@@ -83,17 +86,19 @@ let initializedModules = {};
   ["ReaderParent", "resource:///modules/ReaderParent.jsm"],
   ["RecentWindow", "resource:///modules/RecentWindow.jsm"],
   ["RemotePrompt", "resource:///modules/RemotePrompt.jsm"],
   ["SessionStore", "resource:///modules/sessionstore/SessionStore.jsm"],
   ["ShellService", "resource:///modules/ShellService.jsm"],
   ["SimpleServiceDiscovery", "resource://gre/modules/SimpleServiceDiscovery.jsm"],
   ["TabCrashHandler", "resource:///modules/ContentCrashHandlers.jsm"],
   ["Task", "resource://gre/modules/Task.jsm"],
+  ["UIState", "resource://services-sync/UIState.jsm"],
   ["UITour", "resource:///modules/UITour.jsm"],
+  ["UpdateListener", "resource://gre/modules/UpdateListener.jsm", "init"],
   ["WebChannel", "resource://gre/modules/WebChannel.jsm"],
   ["WindowsRegistry", "resource://gre/modules/WindowsRegistry.jsm"],
   ["webrtcUI", "resource:///modules/webrtcUI.jsm", "init"],
 ].forEach(([name, resource, init]) => {
   if (init) {
     XPCOMUtils.defineLazyGetter(this, name, () => {
       Cu.import(resource, initializedModules);
       initializedModules[name][init]();
@@ -119,16 +124,23 @@ XPCOMUtils.defineLazyGetter(this, "gBran
 
 XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
   return Services.strings.createBundle("chrome://browser/locale/browser.properties");
 });
 
 const global = this;
 
 const listeners = {
+  observers: {
+    "update-staged": ["UpdateListener"],
+    "update-downloaded": ["UpdateListener"],
+    "update-available": ["UpdateListener"],
+    "update-error": ["UpdateListener"],
+  },
+
   ppmm: {
     // PLEASE KEEP THIS LIST IN SYNC WITH THE LISTENERS ADDED IN ContentPrefServiceParent.init
     "ContentPrefs:FunctionCall": ["ContentPrefServiceParent"],
     "ContentPrefs:AddObserverForName": ["ContentPrefServiceParent"],
     "ContentPrefs:RemoveObserverForName": ["ContentPrefServiceParent"],
     // PLEASE KEEP THIS LIST IN SYNC WITH THE LISTENERS ADDED IN ContentPrefServiceParent.init
     "FeedConverter:addLiveBookmark": ["Feeds"],
     "WCCR:setAutoHandler": ["Feeds"],
@@ -160,29 +172,43 @@ const listeners = {
     "rtcpeer:CancelRequest": ["webrtcUI"],
     "rtcpeer:Request": ["webrtcUI"],
     "webrtc:CancelRequest": ["webrtcUI"],
     "webrtc:Request": ["webrtcUI"],
     "webrtc:StopRecording": ["webrtcUI"],
     "webrtc:UpdateBrowserIndicators": ["webrtcUI"],
   },
 
+  observe(subject, topic, data) {
+    for (let module of this.observers[topic]) {
+      try {
+        global[module].observe(subject, topic, data);
+      } catch (e) {
+        Cu.reportError(e);
+      }
+    }
+  },
+
   receiveMessage(modules, data) {
     let val;
     for (let module of modules[data.name]) {
       try {
         val = global[module].receiveMessage(data) || val;
       } catch (e) {
         Cu.reportError(e);
       }
     }
     return val;
   },
 
   init() {
+    for (let observer of Object.keys(this.observers)) {
+      Services.obs.addObserver(this, observer);
+    }
+
     let receiveMessageMM = this.receiveMessage.bind(this, this.mm);
     for (let message of Object.keys(this.mm)) {
       Services.mm.addMessageListener(message, receiveMessageMM);
     }
 
     let receiveMessagePPMM = this.receiveMessage.bind(this, this.ppmm);
     for (let message of Object.keys(this.ppmm)) {
       Services.ppmm.addMessageListener(message, receiveMessagePPMM);
@@ -463,16 +489,19 @@ BrowserGlue.prototype = {
               break;
             }
           }
         });
         break;
       case "test-initialize-sanitizer":
         this._sanitizer.onStartup();
         break;
+      case "sync-ui-state:update":
+        this._updateFxaBadges();
+        break;
     }
   },
 
   // initialization (called on application startup)
   _init: function BG__init() {
     let os = Services.obs;
     os.addObserver(this, "notifications-open-settings");
     os.addObserver(this, "prefservice:after-app-defaults");
@@ -501,16 +530,17 @@ BrowserGlue.prototype = {
     os.addObserver(this, "profile-before-change");
     if (AppConstants.MOZ_TELEMETRY_REPORTING) {
       os.addObserver(this, "keyword-search");
     }
     os.addObserver(this, "browser-search-engine-modified");
     os.addObserver(this, "restart-in-safe-mode");
     os.addObserver(this, "flash-plugin-hang");
     os.addObserver(this, "xpi-signature-changed");
+    os.addObserver(this, "sync-ui-state:update");
 
     this._flashHangCount = 0;
     this._firstWindowReady = new Promise(resolve => this._firstWindowLoaded = resolve);
 
     if (AppConstants.platform == "macosx") {
       // Handles prompting to inform about incompatibilites when accessibility
       // and e10s are active together.
       E10SAccessibilityCheck.init();
@@ -553,16 +583,17 @@ BrowserGlue.prototype = {
     os.removeObserver(this, "handle-xul-text-link");
     os.removeObserver(this, "profile-before-change");
     if (AppConstants.MOZ_TELEMETRY_REPORTING) {
       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");
   },
 
   _onAppDefaults: function BG__onAppDefaults() {
     // apply distribution customizations (prefs)
     // other customizations are applied in _finalUIStartup()
     this._distributionCustomizer.applyPrefDefaults();
   },
 
@@ -2311,16 +2342,26 @@ BrowserGlue.prototype = {
         win.openUILinkIn("https://support.mozilla.org/kb/flash-protected-mode-autodisabled", "tab");
       }
     }];
     let nb = win.document.getElementById("global-notificationbox");
     nb.appendNotification(message, "flash-hang", null,
                           nb.PRIORITY_INFO_MEDIUM, buttons);
   },
 
+  _updateFxaBadges() {
+    let state = UIState.get();
+    if (state.status == UIState.STATUS_LOGIN_FAILED ||
+        state.status == UIState.STATUS_NOT_VERIFIED) {
+      AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
+    } else {
+      AppMenuNotifications.removeNotification("fxa-needs-authentication");
+    }
+  },
+
   // for XPCOM
   classID:          Components.ID("{eab9012e-5f74-4cbc-b2b5-a590235513cc}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference,
                                          Ci.nsIBrowserGlue]),
 
   // redefine the default factory for XPCOMUtils
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -314,18 +314,17 @@
                 onsyncfrompreference="return gPrivacyPane.readSavePasswords();"
                 flex="1" />
       <button id="passwordExceptions"
               class="accessory-button"
               label="&passwordExceptions.label;"
               accesskey="&passwordExceptions.accesskey;"
               preference="pref.privacy.disable_button.view_passwords_exceptions"/>
     </hbox>
-    <hbox id="showPasswordBox">
-      <hbox id="showPasswordsBox"/>
+    <hbox id="showPasswordBox" pack="end">
       <button id="showPasswords"
               class="accessory-button"
               label="&savedLogins.label;" accesskey="&savedLogins.accesskey;"
               preference="pref.privacy.disable_button.view_passwords"/>
     </hbox>
   </vbox>
   <hbox id="masterPasswordRow">
     <checkbox id="useMasterPassword"
--- a/browser/modules/ExtensionsUI.jsm
+++ b/browser/modules/ExtensionsUI.jsm
@@ -9,16 +9,18 @@ this.EXPORTED_SYMBOLS = ["ExtensionsUI"]
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/EventEmitter.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
                                   "resource://gre/modules/AddonManager.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppMenuNotifications",
+                                  "resource://gre/modules/AppMenuNotifications.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
                                   "resource:///modules/RecentWindow.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyPreferenceGetter(this, "WEBEXT_PERMISSION_PROMPTS",
@@ -66,55 +68,64 @@ this.ExtensionsUI = {
       if (!this.sideloadListener) {
         this.sideloadListener = {
           onEnabled: addon => {
             if (!this.sideloaded.has(addon)) {
               return;
             }
 
             this.sideloaded.delete(addon);
-            this.emit("change");
+              this._updateNotifications();
 
             if (this.sideloaded.size == 0) {
               AddonManager.removeAddonListener(this.sideloadListener);
               this.sideloadListener = null;
             }
           },
         };
         AddonManager.addAddonListener(this.sideloadListener);
       }
 
       for (let addon of sideloaded) {
         this.sideloaded.add(addon);
       }
-      this.emit("change");
+        this._updateNotifications();
     } else {
       // This and all the accompanying about:newaddon code can eventually
       // be removed.  See bug 1331521.
       let win = RecentWindow.getMostRecentBrowserWindow();
       for (let addon of sideloaded) {
         win.openUILinkIn(`about:newaddon?id=${addon.id}`, "tab");
       }
     }
   },
 
+  _updateNotifications() {
+    if (this.sideloaded.size + this.updates.size == 0) {
+      AppMenuNotifications.removeNotification("addon-alert");
+    } else {
+      AppMenuNotifications.showBadgeOnlyNotification("addon-alert");
+    }
+    this.emit("change");
+  },
+
   showAddonsManager(browser, strings, icon, histkey) {
     let global = browser.selectedBrowser.ownerGlobal;
     return global.BrowserOpenAddonsMgr("addons://list/extension").then(aomWin => {
       let aomBrowser = aomWin.QueryInterface(Ci.nsIInterfaceRequestor)
                              .getInterface(Ci.nsIDocShell)
                              .chromeEventHandler;
       return this.showPermissionsPrompt(aomBrowser, strings, icon, histkey);
     });
   },
 
   showSideloaded(browser, addon) {
     addon.markAsSeen();
     this.sideloaded.delete(addon);
-    this.emit("change");
+    this._updateNotifications();
 
     let strings = this._buildStrings({
       addon,
       permissions: addon.userPermissions,
       type: "sideload",
     });
     this.showAddonsManager(browser, strings, addon.iconURL, "sideload")
         .then(answer => {
@@ -128,17 +139,17 @@ this.ExtensionsUI = {
           if (answer) {
             info.resolve();
           } else {
             info.reject();
           }
           // At the moment, this prompt will re-appear next time we do an update
           // check.  See bug 1332360 for proposal to avoid this.
           this.updates.delete(info);
-          this.emit("change");
+          this._updateNotifications();
         });
   },
 
   observe(subject, topic, data) {
     if (topic == "webextension-permission-prompt") {
       let {target, info} = subject.wrappedJSObject;
 
       // Dismiss the progress notification.  Note that this is bad if
@@ -200,17 +211,17 @@ this.ExtensionsUI = {
       let update = {
         strings,
         addon: info.addon,
         resolve: info.resolve,
         reject: info.reject,
       };
 
       this.updates.add(update);
-      this.emit("change");
+      this._updateNotifications();
     } else if (topic == "webextension-install-notify") {
       let {target, addon, callback} = subject.wrappedJSObject;
       this.showInstallNotification(target, addon).then(() => {
         if (callback) {
           callback();
         }
       });
     } else if (topic == "webextension-optional-permission-prompt") {
--- a/devtools/client/netmonitor/index.html
+++ b/devtools/client/netmonitor/index.html
@@ -40,17 +40,18 @@
           this.mount = document.querySelector("#mount");
           const connection = {
             tabConnection: {
               tabTarget: toolbox.target,
             },
             toolbox,
           };
           const App = createFactory(require("./src/components/app"));
-          render(Provider({ store }, App()), this.mount);
+          const sourceMapService = toolbox.sourceMapURLService;
+          render(Provider({ store }, App({ sourceMapService })), this.mount);
           return onFirefoxConnect(connection, actions, store.getState);
         },
 
         destroy() {
           unmountComponentAtNode(this.mount);
           return onDisconnect();
         }
       };
--- a/devtools/client/netmonitor/src/components/app.js
+++ b/devtools/client/netmonitor/src/components/app.js
@@ -16,25 +16,27 @@ const MonitorPanel = createFactory(requi
 const StatisticsPanel = createFactory(require("./statistics-panel"));
 
 const { div } = DOM;
 
 /*
  * App component
  * The top level component for representing main panel
  */
-function App({ statisticsOpen }) {
+function App({ statisticsOpen, sourceMapService }) {
   return (
     div({ className: "network-monitor" },
-      !statisticsOpen ? MonitorPanel() : StatisticsPanel()
+      !statisticsOpen ? MonitorPanel({sourceMapService}) : StatisticsPanel()
     )
   );
 }
 
 App.displayName = "App";
 
 App.propTypes = {
   statisticsOpen: PropTypes.bool.isRequired,
+  // Service to enable the source map feature.
+  sourceMapService: PropTypes.object,
 };
 
 module.exports = connect(
   (state) => ({ statisticsOpen: state.ui.statisticsOpen }),
 )(App);
--- a/devtools/client/netmonitor/src/components/monitor-panel.js
+++ b/devtools/client/netmonitor/src/components/monitor-panel.js
@@ -33,16 +33,18 @@ const MediaQueryList = window.matchMedia
 const MonitorPanel = createClass({
   displayName: "MonitorPanel",
 
   propTypes: {
     isEmpty: PropTypes.bool.isRequired,
     networkDetailsOpen: PropTypes.bool.isRequired,
     openNetworkDetails: PropTypes.func.isRequired,
     request: PropTypes.object,
+    // Service to enable the source map feature.
+    sourceMapService: PropTypes.object,
     updateRequest: PropTypes.func.isRequired,
   },
 
   getInitialState() {
     return {
       isVerticalSpliter: MediaQueryList.matches,
     };
   },
@@ -97,33 +99,36 @@ const MonitorPanel = createClass({
 
   onLayoutChange() {
     this.setState({
       isVerticalSpliter: MediaQueryList.matches,
     });
   },
 
   render() {
-    let { isEmpty, networkDetailsOpen } = this.props;
+    let { isEmpty, networkDetailsOpen, sourceMapService } = this.props;
     let initialWidth = Services.prefs.getIntPref(
         "devtools.netmonitor.panes-network-details-width");
     let initialHeight = Services.prefs.getIntPref(
         "devtools.netmonitor.panes-network-details-height");
     return (
       div({ className: "monitor-panel" },
         Toolbar(),
         SplitBox({
           className: "devtools-responsive-container",
           initialWidth: `${initialWidth}px`,
           initialHeight: `${initialHeight}px`,
           minSize: "50px",
           maxSize: "80%",
           splitterSize: "1px",
           startPanel: RequestList({ isEmpty }),
-          endPanel: networkDetailsOpen && NetworkDetailsPanel({ ref: "endPanel" }),
+          endPanel: networkDetailsOpen && NetworkDetailsPanel({
+            ref: "endPanel",
+            sourceMapService,
+          }),
           endPanelCollapsed: !networkDetailsOpen,
           endPanelControl: true,
           vert: this.state.isVerticalSpliter,
         }),
       )
     );
   }
 });
--- a/devtools/client/netmonitor/src/components/network-details-panel.js
+++ b/devtools/client/netmonitor/src/components/network-details-panel.js
@@ -22,28 +22,30 @@ const { div } = DOM;
 /*
  * Network details panel component
  */
 function NetworkDetailsPanel({
   activeTabId,
   cloneSelectedRequest,
   request,
   selectTab,
+  sourceMapService,
 }) {
   if (!request) {
     return null;
   }
 
   return (
     div({ className: "network-details-panel" },
       !request.isCustom ?
         TabboxPanel({
           activeTabId,
           request,
           selectTab,
+          sourceMapService,
         }) :
         CustomRequestPanel({
           cloneSelectedRequest,
           request,
         })
     )
   );
 }
@@ -51,16 +53,18 @@ function NetworkDetailsPanel({
 NetworkDetailsPanel.displayName = "NetworkDetailsPanel";
 
 NetworkDetailsPanel.propTypes = {
   activeTabId: PropTypes.string,
   cloneSelectedRequest: PropTypes.func.isRequired,
   open: PropTypes.bool,
   request: PropTypes.object,
   selectTab: PropTypes.func.isRequired,
+  // Service to enable the source map feature.
+  sourceMapService: PropTypes.object,
 };
 
 module.exports = connect(
   (state) => ({
     activeTabId: state.ui.detailsPanelSelectedTab,
     request: getSelectedRequest(state),
   }),
   (dispatch) => ({
--- a/devtools/client/netmonitor/src/components/stack-trace-panel.js
+++ b/devtools/client/netmonitor/src/components/stack-trace-panel.js
@@ -11,28 +11,31 @@ const {
 } = require("devtools/client/shared/vendor/react");
 const { viewSourceInDebugger } = require("../connector/index");
 
 const { div } = DOM;
 
 // Components
 const StackTrace = createFactory(require("devtools/client/shared/components/stack-trace"));
 
-function StackTracePanel({ request }) {
+function StackTracePanel({ request, sourceMapService }) {
   let { stacktrace } = request.cause;
 
   return (
     div({ className: "panel-container" },
       StackTrace({
         stacktrace,
         onViewSourceInDebugger: ({ url, line }) => viewSourceInDebugger(url, line),
+        sourceMapService,
       }),
     )
   );
 }
 
 StackTracePanel.displayName = "StackTracePanel";
 
 StackTracePanel.propTypes = {
   request: PropTypes.object.isRequired,
+  // Service to enable the source map feature.
+  sourceMapService: PropTypes.object,
 };
 
 module.exports = StackTracePanel;
--- a/devtools/client/netmonitor/src/components/tabbox-panel.js
+++ b/devtools/client/netmonitor/src/components/tabbox-panel.js
@@ -36,16 +36,17 @@ const TIMINGS_TITLE = L10N.getStr("netmo
  * Tabbox panel component
  * Display the network request details
  */
 function TabboxPanel({
   activeTabId,
   cloneSelectedRequest,
   request,
   selectTab,
+  sourceMapService,
 }) {
   if (!request) {
     return null;
   }
 
   return (
     Tabbar({
       activeTabId,
@@ -83,17 +84,17 @@ function TabboxPanel({
       },
         TimingsPanel({ request }),
       ),
       request.cause && request.cause.stacktrace && request.cause.stacktrace.length > 0 &&
       TabPanel({
         id: "stack-trace",
         title: STACK_TRACE_TITLE,
       },
-        StackTracePanel({ request }),
+        StackTracePanel({ request, sourceMapService }),
       ),
       request.securityState && request.securityState !== "insecure" &&
       TabPanel({
         id: "security",
         title: SECURITY_TITLE,
       },
         SecurityPanel({ request }),
       ),
@@ -103,16 +104,18 @@ function TabboxPanel({
 
 TabboxPanel.displayName = "TabboxPanel";
 
 TabboxPanel.propTypes = {
   activeTabId: PropTypes.string,
   cloneSelectedRequest: PropTypes.func.isRequired,
   request: PropTypes.object,
   selectTab: PropTypes.func.isRequired,
+  // Service to enable the source map feature.
+  sourceMapService: PropTypes.object,
 };
 
 module.exports = connect(
   (state) => ({
     activeTabId: state.ui.detailsPanelSelectedTab,
     request: getSelectedRequest(state),
   }),
   (dispatch) => ({
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -17,16 +17,17 @@ support-files =
   html_infinite-get-page.html
   html_json-b64.html
   html_json-basic.html
   html_json-custom-mime-test-page.html
   html_json-long-test-page.html
   html_json-malformed-test-page.html
   html_json-text-mime-test-page.html
   html_jsonp-test-page.html
+  html_maps-test-page.html
   html_navigate-test-page.html
   html_params-test-page.html
   html_post-data-test-page.html
   html_post-json-test-page.html
   html_post-raw-test-page.html
   html_post-raw-with-headers-test-page.html
   html_simple-test-page.html
   html_single-get-page.html
@@ -45,24 +46,28 @@ support-files =
   sjs_simple-test-server.sjs
   sjs_sorting-test-server.sjs
   sjs_status-codes-test-server.sjs
   sjs_truncate-test-server.sjs
   test-image.png
   service-workers/status-codes.html
   service-workers/status-codes-service-worker.js
   !/devtools/client/framework/test/shared-head.js
+  xhr_bundle.js
+  xhr_bundle.js.map
+  xhr_original.js
 
 [browser_net_accessibility-01.js]
 [browser_net_accessibility-02.js]
 [browser_net_api-calls.js]
 [browser_net_autoscroll.js]
 [browser_net_cached-status.js]
 [browser_net_cause.js]
 [browser_net_cause_redirect.js]
+[browser_net_cause_source_map.js]
 [browser_net_service-worker-status.js]
 [browser_net_charts-01.js]
 [browser_net_charts-02.js]
 [browser_net_charts-03.js]
 [browser_net_charts-04.js]
 [browser_net_charts-05.js]
 [browser_net_charts-06.js]
 [browser_net_charts-07.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_cause_source_map.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests if request cause is reported correctly when using source maps.
+ */
+
+const CAUSE_FILE_NAME = "html_maps-test-page.html";
+const CAUSE_URL = EXAMPLE_URL + CAUSE_FILE_NAME;
+
+const N_EXPECTED_REQUESTS = 4;
+
+add_task(function* () {
+  // the initNetMonitor function clears the network request list after the
+  // page is loaded. That's why we first load a bogus page from SIMPLE_URL,
+  // and only then load the real thing from CAUSE_URL - we want to catch
+  // all the requests the page is making, not only the XHRs.
+  // We can't use about:blank here, because initNetMonitor checks that the
+  // page has actually made at least one request.
+  let { tab, monitor } = yield initNetMonitor(SIMPLE_URL);
+
+  let { document, store, windowRequire } = monitor.panelWin;
+  let Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+
+  store.dispatch(Actions.batchEnable(false));
+  let waitPromise = waitForNetworkEvents(monitor, N_EXPECTED_REQUESTS);
+  tab.linkedBrowser.loadURI(CAUSE_URL);
+  yield waitPromise;
+
+  info("Clicking item and waiting for details panel to open");
+  waitPromise = waitForDOM(document, ".network-details-panel");
+  let xhrRequestItem = document.querySelectorAll(".request-list-item")[3];
+  EventUtils.sendMouseEvent({ type: "mousedown" }, xhrRequestItem);
+  yield waitPromise;
+
+  info("Clicking stack tab and waiting for stack panel to open");
+  waitPromise = waitForDOM(document, "#stack-trace-panel");
+  let stackTab = document.querySelector("#stack-trace-tab");
+  EventUtils.sendMouseEvent({ type: "click" }, stackTab);
+  yield waitPromise;
+
+  info("Waiting for source maps to be applied");
+  yield waitUntil(() => {
+    let frames = document.querySelectorAll(".frame-link");
+    return frames && frames.length >= 2 &&
+      frames[0].textContent.includes("xhr_original") &&
+      frames[1].textContent.includes("xhr_original");
+  });
+
+  let frames = document.querySelectorAll(".frame-link");
+  is(frames.length, 3, "should have 3 stack frames");
+  is(frames[0].textContent, `reallydoxhr xhr_original.js:6`);
+  is(frames[1].textContent, `doxhr xhr_original.js:10`);
+
+  yield teardown(monitor);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_maps-test-page.html
@@ -0,0 +1,24 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
+    <meta http-equiv="Pragma" content="no-cache" />
+    <meta http-equiv="Expires" content="0" />
+    <title>Network Monitor source maps test page</title>
+    <link rel="stylesheet" type="text/css" href="stylesheet_request" />
+  </head>
+
+  <body>
+    <script type="text/javascript" src="xhr_bundle.js" charset="utf-8"></script>
+    <script type="text/javascript">
+      "use strict";
+
+      /* globals doxhr */
+      doxhr();
+    </script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/xhr_bundle.js
@@ -0,0 +1,91 @@
+/******/ (function(modules) { // webpackBootstrap
+/******/ 	// The module cache
+/******/ 	var installedModules = {};
+/******/
+/******/ 	// The require function
+/******/ 	function __webpack_require__(moduleId) {
+/******/
+/******/ 		// Check if module is in cache
+/******/ 		if(installedModules[moduleId]) {
+/******/ 			return installedModules[moduleId].exports;
+/******/ 		}
+/******/ 		// Create a new module (and put it into the cache)
+/******/ 		var module = installedModules[moduleId] = {
+/******/ 			i: moduleId,
+/******/ 			l: false,
+/******/ 			exports: {}
+/******/ 		};
+/******/
+/******/ 		// Execute the module function
+/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ 		// Flag the module as loaded
+/******/ 		module.l = true;
+/******/
+/******/ 		// Return the exports of the module
+/******/ 		return module.exports;
+/******/ 	}
+/******/
+/******/
+/******/ 	// expose the modules object (__webpack_modules__)
+/******/ 	__webpack_require__.m = modules;
+/******/
+/******/ 	// expose the module cache
+/******/ 	__webpack_require__.c = installedModules;
+/******/
+/******/ 	// identity function for calling harmony imports with the correct context
+/******/ 	__webpack_require__.i = function(value) { return value; };
+/******/
+/******/ 	// define getter function for harmony exports
+/******/ 	__webpack_require__.d = function(exports, name, getter) {
+/******/ 		if(!__webpack_require__.o(exports, name)) {
+/******/ 			Object.defineProperty(exports, name, {
+/******/ 				configurable: false,
+/******/ 				enumerable: true,
+/******/ 				get: getter
+/******/ 			});
+/******/ 		}
+/******/ 	};
+/******/
+/******/ 	// getDefaultExport function for compatibility with non-harmony modules
+/******/ 	__webpack_require__.n = function(module) {
+/******/ 		var getter = module && module.__esModule ?
+/******/ 			function getDefault() { return module['default']; } :
+/******/ 			function getModuleExports() { return module; };
+/******/ 		__webpack_require__.d(getter, 'a', getter);
+/******/ 		return getter;
+/******/ 	};
+/******/
+/******/ 	// Object.prototype.hasOwnProperty.call
+/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ 	// __webpack_public_path__
+/******/ 	__webpack_require__.p = "";
+/******/
+/******/ 	// Load entry module and return exports
+/******/ 	return __webpack_require__(__webpack_require__.s = 0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+function reallydoxhr() {
+  let z = new XMLHttpRequest();
+  z.open("get", "test-image.png", true);
+  z.send();
+}
+
+function doxhr() {
+  reallydoxhr();
+}
+
+window.doxhr = doxhr;
+
+
+/***/ })
+/******/ ]);
+//# sourceMappingURL=xhr_bundle.js.map
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/xhr_bundle.js.map
@@ -0,0 +1,1 @@
+{"version":3,"sources":["webpack:///webpack/bootstrap 1f90f505700f55e4a0b4","webpack:///./xhr_original.js"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA,mDAA2C,cAAc;;AAEzD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;AAEA;AACA;;;;;;;;AChEA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA","file":"xhr_bundle.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// identity function for calling harmony imports with the correct context\n \t__webpack_require__.i = function(value) { return value; };\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 1f90f505700f55e4a0b4","\"use strict\";\n\nfunction reallydoxhr() {\n  let z = new XMLHttpRequest();\n  z.open(\"get\", \"test-image.png\", true);\n  z.send();\n}\n\nfunction doxhr() {\n  reallydoxhr();\n}\n\nwindow.doxhr = doxhr;\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./xhr_original.js\n// module id = 0\n// module chunks = 0"],"sourceRoot":""}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/xhr_original.js
@@ -0,0 +1,13 @@
+"use strict";
+
+function reallydoxhr() {
+  let z = new XMLHttpRequest();
+  z.open("get", "test-image.png", true);
+  z.send();
+}
+
+function doxhr() {
+  reallydoxhr();
+}
+
+window.doxhr = doxhr;
--- a/devtools/client/webconsole/new-console-output/test/mochitest/head.js
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/head.js
@@ -15,16 +15,22 @@ Services.scriptloader.loadSubScript(
   this);
 
 var WCUL10n = require("devtools/client/webconsole/webconsole-l10n");
 
 Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", true);
 registerCleanupFunction(function* () {
   Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
 
+  // Reset all filter prefs between tests. First flushPrefEnv in case one of the
+  // filter prefs has been pushed for the test
+  yield SpecialPowers.flushPrefEnv();
+  Services.prefs.getChildList("devtools.webconsole.filter").forEach(pref => {
+    Services.prefs.clearUserPref(pref);
+  });
   let browserConsole = HUDService.getBrowserConsole();
   if (browserConsole) {
     if (browserConsole.jsterm) {
       browserConsole.jsterm.clearOutput(true);
     }
     yield HUDService.toggleBrowserConsole();
   }
 });
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -1783,16 +1783,20 @@ HTMLInputElement::GetNonFileValueInterna
       }
       return;
   }
 }
 
 bool
 HTMLInputElement::IsValueEmpty() const
 {
+  if (GetValueMode() == VALUE_MODE_VALUE && IsSingleLineTextControl(false)) {
+    return !mInputData.mState->HasNonEmptyValue();
+  }
+
   nsAutoString value;
   GetNonFileValueInternal(value);
 
   return value.IsEmpty();
 }
 
 void
 HTMLInputElement::ClearFiles(bool aSetValueChanged)
--- a/dom/html/TextTrackManager.cpp
+++ b/dom/html/TextTrackManager.cpp
@@ -271,28 +271,28 @@ TextTrackManager::UpdateCueDisplay()
   }
 
   nsCOMPtr<nsIContent> overlay = videoFrame->GetCaptionOverlay();
   nsCOMPtr<nsIContent> controls = videoFrame->GetVideoControls();
   if (!overlay) {
     return;
   }
 
-  nsTArray<RefPtr<TextTrackCue> > activeCues;
-  mTextTracks->GetShowingCues(activeCues);
+  nsTArray<RefPtr<TextTrackCue> > showingCues;
+  mTextTracks->GetShowingCues(showingCues);
 
-  if (activeCues.Length() > 0) {
+  if (showingCues.Length() > 0) {
     WEBVTT_LOG("UpdateCueDisplay ProcessCues");
-    WEBVTT_LOGV("UpdateCueDisplay activeCues.Length() %" PRIuSIZE, activeCues.Length());
+    WEBVTT_LOGV("UpdateCueDisplay showingCues.Length() %" PRIuSIZE, showingCues.Length());
     RefPtr<nsVariantCC> jsCues = new nsVariantCC();
 
     jsCues->SetAsArray(nsIDataType::VTYPE_INTERFACE,
                        &NS_GET_IID(nsIDOMEventTarget),
-                       activeCues.Length(),
-                       static_cast<void*>(activeCues.Elements()));
+                       showingCues.Length(),
+                       static_cast<void*>(showingCues.Elements()));
     nsPIDOMWindowInner* window = mMediaElement->OwnerDoc()->GetInnerWindow();
     if (window) {
       sParserWrapper->ProcessCues(window, jsCues, overlay, controls);
     }
   } else if (overlay->Length() > 0) {
     WEBVTT_LOG("UpdateCueDisplay EmptyString");
     nsContentUtils::SetNodeTextContent(overlay, EmptyString(), true);
   }
--- a/dom/media/MediaCache.cpp
+++ b/dom/media/MediaCache.cpp
@@ -14,16 +14,17 @@
 #include "mozilla/Logging.h"
 #include "mozilla/Preferences.h"
 #include "FileBlockCache.h"
 #include "nsIObserverService.h"
 #include "nsISeekableStream.h"
 #include "nsIPrincipal.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/Services.h"
+#include "mozilla/Telemetry.h"
 #include <algorithm>
 
 namespace mozilla {
 
 #undef LOG
 #undef LOGI
 LazyLogModule gMediaCacheLog("MediaCache");
 #define LOG(...) MOZ_LOG(gMediaCacheLog, LogLevel::Debug, (__VA_ARGS__))
@@ -121,16 +122,27 @@ public:
   ~MediaCache() {
     NS_ASSERTION(mStreams.IsEmpty(), "Stream(s) still open!");
     Truncate();
     NS_ASSERTION(mIndex.Length() == 0, "Blocks leaked?");
     if (mFileCache) {
       mFileCache->Close();
       mFileCache = nullptr;
     }
+    LOG("MediaCache::~MediaCache(this=%p) MEDIACACHE_WATERMARK_KB=%u",
+        this, unsigned(mIndexWatermark * MediaCache::BLOCK_SIZE / 1024));
+    Telemetry::Accumulate(
+      Telemetry::HistogramID::MEDIACACHE_WATERMARK_KB,
+      uint32_t(mIndexWatermark * MediaCache::BLOCK_SIZE / 1024));
+    LOG("MediaCache::~MediaCache(this=%p) MEDIACACHE_BLOCKOWNERS_WATERMARK=%u",
+        this, unsigned(mBlockOwnersWatermark));
+    Telemetry::Accumulate(
+      Telemetry::HistogramID::MEDIACACHE_BLOCKOWNERS_WATERMARK,
+      mBlockOwnersWatermark);
+
     MOZ_COUNT_DTOR(MediaCache);
   }
 
   // Main thread only. Creates the backing cache file. If this fails,
   // then the cache is still in a semi-valid state; mFD will be null,
   // so all I/O on the cache file will fail.
   nsresult Init();
   // Shut down the global cache if it's no longer needed. We shut down
@@ -343,16 +355,20 @@ protected:
   // readers that need to block will Wait() on this monitor. When new
   // data becomes available in the cache, we NotifyAll() on this monitor.
   ReentrantMonitor         mReentrantMonitor;
   // This is only written while on the main thread and the monitor is held.
   // Thus, it can be safely read from the main thread or while holding the monitor.
   nsTArray<MediaCacheStream*> mStreams;
   // The Blocks describing the cache entries.
   nsTArray<Block> mIndex;
+  // Keep track for highest number of blocks used, for telemetry purposes.
+  int32_t mIndexWatermark = 0;
+  // Keep track for highest number of blocks owners, for telemetry purposes.
+  uint32_t mBlockOwnersWatermark = 0;
   // Writer which performs IO, asynchronously writing cache blocks.
   RefPtr<FileBlockCache> mFileCache;
   // The list of free blocks; they are not ordered.
   BlockList       mFreeBlocks;
   // True if an event to run Update() has been queued but not processed
   bool            mUpdateQueued;
 #ifdef DEBUG
   bool            mInUpdate;
@@ -715,16 +731,17 @@ MediaCache::FindBlockForIncomingData(Tim
     // b) the data we're going to store in the free block is not higher
     // priority than the data already stored in the free block.
     // The latter can lead us to go over the cache limit a bit.
     if ((mIndex.Length() < uint32_t(GetMaxBlocks()) || blockIndex < 0 ||
          PredictNextUseForIncomingData(aStream) >= PredictNextUse(aNow, blockIndex))) {
       blockIndex = mIndex.Length();
       if (!mIndex.AppendElement())
         return -1;
+      mIndexWatermark = std::max(mIndexWatermark, blockIndex + 1);
       mFreeBlocks.AddFirstBlock(blockIndex);
       return blockIndex;
     }
   }
 
   return blockIndex;
 }
 
@@ -929,16 +946,18 @@ MediaCache::AddBlockOwnerAsReadahead(int
                                        MediaCacheStream* aStream,
                                        int32_t aStreamBlockIndex)
 {
   Block* block = &mIndex[aBlockIndex];
   if (block->mOwners.IsEmpty()) {
     mFreeBlocks.RemoveBlock(aBlockIndex);
   }
   BlockOwner* bo = block->mOwners.AppendElement();
+  mBlockOwnersWatermark =
+    std::max(mBlockOwnersWatermark, uint32_t(block->mOwners.Length()));
   bo->mStream = aStream;
   bo->mStreamBlock = aStreamBlockIndex;
   aStream->mBlocks[aStreamBlockIndex] = aBlockIndex;
   bo->mClass = READAHEAD_BLOCK;
   InsertReadaheadBlock(bo, aBlockIndex);
 }
 
 void
@@ -1526,16 +1545,18 @@ MediaCache::AllocateAndWriteBlock(
     ResourceStreamIterator iter(aStream->mResourceID);
     while (MediaCacheStream* stream = iter.Next()) {
       BlockOwner* bo = block->mOwners.AppendElement();
       if (!bo) {
         // Roll back mOwners if any allocation fails.
         block->mOwners.Clear();
         return;
       }
+      mBlockOwnersWatermark =
+        std::max(mBlockOwnersWatermark, uint32_t(block->mOwners.Length()));
       bo->mStream = stream;
     }
 
     if (block->mOwners.IsEmpty()) {
       // This happens when all streams with the resource id are closed. We can
       // just return here now and discard the data.
       return;
     }
--- a/dom/media/TextTrackList.cpp
+++ b/dom/media/TextTrackList.cpp
@@ -38,19 +38,22 @@ TextTrackList::TextTrackList(nsPIDOMWind
 
 TextTrackList::~TextTrackList()
 {
 }
 
 void
 TextTrackList::GetShowingCues(nsTArray<RefPtr<TextTrackCue> >& aCues)
 {
+  // Only Subtitles and Captions can show on the screen.
   nsTArray< RefPtr<TextTrackCue> > cues;
   for (uint32_t i = 0; i < Length(); i++) {
-    if (mTextTracks[i]->Mode() == TextTrackMode::Showing) {
+    if (mTextTracks[i]->Mode() == TextTrackMode::Showing &&
+        (mTextTracks[i]->Kind() == TextTrackKind::Subtitles ||
+         mTextTracks[i]->Kind() == TextTrackKind::Captions)) {
       mTextTracks[i]->GetActiveCueArray(cues);
       aCues.AppendElements(cues);
     }
   }
 }
 
 JSObject*
 TextTrackList::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
--- a/dom/media/platforms/omx/OmxDataDecoder.cpp
+++ b/dom/media/platforms/omx/OmxDataDecoder.cpp
@@ -604,17 +604,17 @@ OmxDataDecoder::FillCodecConfigDataToOmx
   MOZ_ASSERT(mOmxState == OMX_StateIdle || mOmxState == OMX_StateExecuting);
 
 
   RefPtr<BufferData> inbuf = FindAvailableBuffer(OMX_DirInput);
   RefPtr<MediaByteBuffer> csc;
   if (mTrackInfo->IsAudio()) {
     csc = mTrackInfo->GetAsAudioInfo()->mCodecSpecificConfig;
   } else if (mTrackInfo->IsVideo()) {
-    csc = mTrackInfo->GetAsVideoInfo()->mCodecSpecificConfig;
+    csc = mTrackInfo->GetAsVideoInfo()->mExtraData;
   }
 
   MOZ_RELEASE_ASSERT(csc);
 
   // Some codecs like h264, its codec specific data is at the first packet, not in container.
   if (csc->Length()) {
     memcpy(inbuf->mBuffer->pBuffer,
            csc->Elements(),
--- a/dom/media/webaudio/BufferDecoder.cpp
+++ b/dom/media/webaudio/BufferDecoder.cpp
@@ -3,28 +3,25 @@
 /* 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 "BufferDecoder.h"
 
 #include "nsISupports.h"
 #include "MediaResource.h"
-#include "GMPCrashHelper.h"
 
 namespace mozilla {
 
 NS_IMPL_ISUPPORTS0(BufferDecoder)
 
 BufferDecoder::BufferDecoder(MediaResource* aResource,
-                             AbstractThread* aMainThread,
-                             GMPCrashHelper* aCrashHelper)
+                             AbstractThread* aMainThread)
   : mResource(aResource)
   , mAbstractMainThread(aMainThread)
-  , mCrashHelper(aCrashHelper)
 {
   MOZ_ASSERT(NS_IsMainThread());
 }
 
 BufferDecoder::~BufferDecoder()
 {
   // The dtor may run on any thread, we cannot be sure.
 }
@@ -64,21 +61,15 @@ BufferDecoder::GetImageContainer()
 
 MediaDecoderOwner*
 BufferDecoder::GetOwner() const
 {
   // unknown
   return nullptr;
 }
 
-already_AddRefed<GMPCrashHelper>
-BufferDecoder::GetCrashHelper()
-{
-  return do_AddRef(mCrashHelper);
-}
-
 AbstractThread*
 BufferDecoder::AbstractMainThread() const
 {
   return mAbstractMainThread;
 }
 
 } // namespace mozilla
--- a/dom/media/webaudio/BufferDecoder.h
+++ b/dom/media/webaudio/BufferDecoder.h
@@ -20,40 +20,36 @@ namespace mozilla {
  * a memory buffer.
  */
 class BufferDecoder final : public AbstractMediaDecoder
 {
 public:
   // This class holds a weak pointer to MediaResource.  It's the responsibility
   // of the caller to manage the memory of the MediaResource object.
   explicit BufferDecoder(MediaResource* aResource,
-                         AbstractThread* aMainThread,
-                         GMPCrashHelper* aCrashHelper);
+                         AbstractThread* aMainThread);
 
   NS_DECL_THREADSAFE_ISUPPORTS
 
   // This has to be called before decoding begins
   void BeginDecoding(TaskQueue* aTaskQueueIdentity);
 
   MediaResource* GetResource() const final override;
 
   void NotifyDecodedFrames(const FrameStatisticsData& aStats) final override;
 
   VideoFrameContainer* GetVideoFrameContainer() final override;
   layers::ImageContainer* GetImageContainer() final override;
 
   MediaDecoderOwner* GetOwner() const final override;
 
-  already_AddRefed<GMPCrashHelper> GetCrashHelper() override;
-
   AbstractThread* AbstractMainThread() const final override;
 
 private:
   virtual ~BufferDecoder();
   RefPtr<TaskQueue> mTaskQueueIdentity;
   RefPtr<MediaResource> mResource;
   const RefPtr<AbstractThread> mAbstractMainThread;
-  RefPtr<GMPCrashHelper> mCrashHelper;
 };
 
 } // namespace mozilla
 
 #endif /* BUFFER_DECODER_H_ */
--- a/dom/media/webaudio/MediaBufferDecoder.cpp
+++ b/dom/media/webaudio/MediaBufferDecoder.cpp
@@ -24,17 +24,16 @@
 #include "nsIScriptObjectPrincipal.h"
 #include "nsIScriptError.h"
 #include "nsMimeTypes.h"
 #include "VideoUtils.h"
 #include "WebAudioUtils.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/Telemetry.h"
 #include "nsPrintfCString.h"
-#include "GMPCrashHelper.h"
 
 namespace mozilla {
 
 extern LazyLogModule gMediaDecoderLog;
 
 using namespace dom;
 
 class ReportResultTask final : public Runnable
@@ -161,34 +160,16 @@ MediaDecodeTask::Run()
     break;
   case PhaseEnum::Done:
     break;
   }
 
   return NS_OK;
 }
 
-class BufferDecoderGMPCrashHelper : public GMPCrashHelper
-{
-public:
-  explicit BufferDecoderGMPCrashHelper(nsPIDOMWindowInner* aParent)
-    : mParent(do_GetWeakReference(aParent))
-  {
-    MOZ_ASSERT(NS_IsMainThread());
-  }
-  already_AddRefed<nsPIDOMWindowInner> GetPluginCrashedEventTarget() override
-  {
-    MOZ_ASSERT(NS_IsMainThread());
-    nsCOMPtr<nsPIDOMWindowInner> window = do_QueryReferent(mParent);
-    return window.forget();
-  }
-private:
-  nsWeakPtr mParent;
-};
-
 bool
 MediaDecodeTask::CreateReader()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   nsPIDOMWindowInner* parent = mDecodeJob.mContext->GetParentObject();
   MOZ_ASSERT(parent);
 
@@ -200,18 +181,17 @@ MediaDecodeTask::CreateReader()
 
   RefPtr<BufferMediaResource> resource =
     new BufferMediaResource(static_cast<uint8_t*> (mBuffer),
                             mLength, principal, mContainerType);
 
   MOZ_ASSERT(!mBufferDecoder);
   mMainThread =
     mDecodeJob.mContext->GetOwnerGlobal()->AbstractMainThreadFor(TaskCategory::Other);
-  mBufferDecoder = new BufferDecoder(resource, mMainThread,
-                                     new BufferDecoderGMPCrashHelper(parent));
+  mBufferDecoder = new BufferDecoder(resource, mMainThread);
 
   // If you change this list to add support for new decoders, please consider
   // updating HTMLMediaElement::CreateDecoder as well.
 
   mDecoderReader = DecoderTraits::CreateReader(mContainerType, mBufferDecoder);
 
   if (!mDecoderReader) {
     return false;
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -150,17 +150,16 @@ using namespace mozilla::layout;
 using namespace mozilla::gfx;
 
 #define GRID_ENABLED_PREF_NAME "layout.css.grid.enabled"
 #define GRID_TEMPLATE_SUBGRID_ENABLED_PREF_NAME "layout.css.grid-template-subgrid-value.enabled"
 #define WEBKIT_PREFIXES_ENABLED_PREF_NAME "layout.css.prefixes.webkit"
 #define DISPLAY_FLOW_ROOT_ENABLED_PREF_NAME "layout.css.display-flow-root.enabled"
 #define TEXT_ALIGN_UNSAFE_ENABLED_PREF_NAME "layout.css.text-align-unsafe-value.enabled"
 #define FLOAT_LOGICAL_VALUES_ENABLED_PREF_NAME "layout.css.float-logical-values.enabled"
-#define BG_CLIP_TEXT_ENABLED_PREF_NAME "layout.css.background-clip-text.enabled"
 
 // The time in number of frames that we estimate for a refresh driver
 // to be quiescent
 #define DEFAULT_QUIESCENT_FRAMES 2
 // The time (milliseconds) we estimate is needed between the end of an
 // idle time and the next Tick.
 #define DEFAULT_IDLE_PERIOD_TIME_LIMIT 1.0f
 
@@ -437,49 +436,16 @@ FloatLogicalValuesEnabledPrefChangeCallb
   MOZ_ASSERT(sIndexOfInlineStartInClearTable >= 0);
   nsCSSProps::kClearKTable[sIndexOfInlineStartInClearTable].mKeyword =
     isFloatLogicalValuesEnabled ? eCSSKeyword_inline_start : eCSSKeyword_UNKNOWN;
   MOZ_ASSERT(sIndexOfInlineEndInClearTable >= 0);
   nsCSSProps::kClearKTable[sIndexOfInlineEndInClearTable].mKeyword =
     isFloatLogicalValuesEnabled ? eCSSKeyword_inline_end : eCSSKeyword_UNKNOWN;
 }
 
-
-// When the pref "layout.css.background-clip-text.enabled" changes, this
-// function is invoked to let us update kBackgroundClipKTable, to selectively
-// disable or restore the entries for "text" in that table.
-static void
-BackgroundClipTextEnabledPrefChangeCallback(const char* aPrefName,
-                                            void* aClosure)
-{
-  NS_ASSERTION(strcmp(aPrefName, BG_CLIP_TEXT_ENABLED_PREF_NAME) == 0,
-               "Did you misspell " BG_CLIP_TEXT_ENABLED_PREF_NAME " ?");
-
-  static bool sIsBGClipKeywordIndexInitialized;
-  static int32_t sIndexOfTextInBGClipTable;
-  bool isBGClipTextEnabled =
-    Preferences::GetBool(BG_CLIP_TEXT_ENABLED_PREF_NAME, false);
-
-  if (!sIsBGClipKeywordIndexInitialized) {
-    // First run: find the position of "text" in kBackgroundClipKTable.
-    sIndexOfTextInBGClipTable =
-      nsCSSProps::FindIndexOfKeyword(eCSSKeyword_text,
-                                     nsCSSProps::kBackgroundClipKTable);
-
-    sIsBGClipKeywordIndexInitialized = true;
-  }
-
-  // OK -- now, stomp on or restore the "text" entry in kBackgroundClipKTable,
-  // depending on whether the pref is enabled vs. disabled.
-  if (sIndexOfTextInBGClipTable >= 0) {
-    nsCSSProps::kBackgroundClipKTable[sIndexOfTextInBGClipTable].mKeyword =
-      isBGClipTextEnabled ? eCSSKeyword_text : eCSSKeyword_UNKNOWN;
-  }
-}
-
 template<typename TestType>
 static bool
 HasMatchingAnimations(EffectSet* aEffects, TestType&& aTest)
 {
   for (KeyframeEffectReadOnly* effect : *aEffects) {
     if (aTest(*effect)) {
       return true;
     }
@@ -7811,18 +7777,16 @@ static const PrefCallbacks kPrefCallback
   { WEBKIT_PREFIXES_ENABLED_PREF_NAME,
     WebkitPrefixEnabledPrefChangeCallback },
   { TEXT_ALIGN_UNSAFE_ENABLED_PREF_NAME,
     TextAlignUnsafeEnabledPrefChangeCallback },
   { DISPLAY_FLOW_ROOT_ENABLED_PREF_NAME,
     DisplayFlowRootEnabledPrefChangeCallback },
   { FLOAT_LOGICAL_VALUES_ENABLED_PREF_NAME,
     FloatLogicalValuesEnabledPrefChangeCallback },
-  { BG_CLIP_TEXT_ENABLED_PREF_NAME,
-    BackgroundClipTextEnabledPrefChangeCallback },
 };
 
 /* static */
 void
 nsLayoutUtils::Initialize()
 {
   Preferences::AddUintVarCache(&sFontSizeInflationMaxRatio,
                                "font.size.inflation.maxRatio");
--- a/layout/reftests/backgrounds/reftest.list
+++ b/layout/reftests/backgrounds/reftest.list
@@ -173,16 +173,15 @@ fuzzy(80,500) fuzzy-if(skiaContent,100,9
 
 fuzzy-if(skiaContent,1,8) == background-multiple-with-border-radius.html background-multiple-with-border-radius-ref.html
 == background-repeat-large-area.html background-repeat-large-area-ref.html
 
 fuzzy(30,474) fuzzy-if(skiaContent,31,474) == background-tiling-zoom-1.html background-tiling-zoom-1-ref.html
 
 skip-if(!cocoaWidget) == background-repeat-resampling.html background-repeat-resampling-ref.html
 
-pref(layout.css.background-clip-text.enabled,true) fuzzy-if(winWidget,102,2032) fuzzy-if(skiaContent,102,2811) == background-clip-text-1a.html background-clip-text-1-ref.html
-pref(layout.css.background-clip-text.enabled,true) fuzzy-if(winWidget,102,2032) fuzzy-if(skiaContent,102,2811) == background-clip-text-1b.html background-clip-text-1-ref.html
-pref(layout.css.background-clip-text.enabled,true) fuzzy-if(winWidget,102,2032) fuzzy-if(skiaContent,102,2811) == background-clip-text-1c.html background-clip-text-1-ref.html
-pref(layout.css.background-clip-text.enabled,true) fuzzy-if(winWidget,102,2032) fuzzy-if(skiaContent,102,2811) == background-clip-text-1d.html background-clip-text-1-ref.html
-pref(layout.css.background-clip-text.enabled,true) fuzzy-if(winWidget,102,2032) fuzzy-if(skiaContent,102,2811) == background-clip-text-1e.html background-clip-text-1-ref.html
-pref(layout.css.background-clip-text.enabled,false) fails-if(stylo) != background-clip-text-1a.html background-clip-text-1-ref.html
+fuzzy-if(winWidget,102,2032) fuzzy-if(skiaContent,102,2811) == background-clip-text-1a.html background-clip-text-1-ref.html
+fuzzy-if(winWidget,102,2032) fuzzy-if(skiaContent,102,2811) == background-clip-text-1b.html background-clip-text-1-ref.html
+fuzzy-if(winWidget,102,2032) fuzzy-if(skiaContent,102,2811) == background-clip-text-1c.html background-clip-text-1-ref.html
+fuzzy-if(winWidget,102,2032) fuzzy-if(skiaContent,102,2811) == background-clip-text-1d.html background-clip-text-1-ref.html
+fuzzy-if(winWidget,102,2032) fuzzy-if(skiaContent,102,2811) == background-clip-text-1e.html background-clip-text-1-ref.html
 
-pref(layout.css.background-clip-text.enabled,true) == background-clip-text-2.html background-clip-text-2-ref.html
+== background-clip-text-2.html background-clip-text-2-ref.html
--- a/layout/reftests/w3c-css/submitted/will-change/reftest.list
+++ b/layout/reftests/w3c-css/submitted/will-change/reftest.list
@@ -5,15 +5,15 @@
 == will-change-stacking-context-mask-1.html green-square-100-by-100-ref.html
 == will-change-stacking-context-mix-blend-mode-1.html green-square-100-by-100-ref.html
 == will-change-stacking-context-opacity-1.html green-square-100-by-100-ref.html
 == will-change-stacking-context-perspective-1.html green-square-100-by-100-ref.html
 == will-change-stacking-context-position-1.html green-square-100-by-100-ref.html
 == will-change-stacking-context-transform-1.html green-square-100-by-100-ref.html
 == will-change-stacking-context-transform-style-1.html green-square-100-by-100-ref.html
 == will-change-stacking-context-z-index-1.html green-square-100-by-100-ref.html
-test-pref(layout.css.contain.enabled,true) fails-if(stylo) == will-change-fixpos-cb-contain-1.html green-square-100-by-100-offset-ref.html
+test-pref(layout.css.contain.enabled,true) == will-change-fixpos-cb-contain-1.html green-square-100-by-100-offset-ref.html
 == will-change-fixpos-cb-filter-1.html green-square-100-by-100-offset-ref.html
 == will-change-fixpos-cb-height-1.html green-square-100-by-100-offset-ref.html
 == will-change-fixpos-cb-perspective-1.html green-square-100-by-100-offset-ref.html
 == will-change-fixpos-cb-position-1.html green-square-100-by-100-offset-ref.html
 == will-change-fixpos-cb-transform-1.html green-square-100-by-100-offset-ref.html
 == will-change-fixpos-cb-transform-style-1.html green-square-100-by-100-offset-ref.html
--- a/layout/style/crashtests/crashtests.list
+++ b/layout/style/crashtests/crashtests.list
@@ -143,19 +143,19 @@ load 1247865-1.html
 asserts-if(stylo,0-1) load 1264396-1.html # bug 1324677
 # The following test relies on -webkit-text-fill-color being behind the
 # layout.css.prefixes.webkit pref
 pref(layout.css.prefixes.webkit,false) load 1265611-1.html
 load border-image-visited-link.html
 load font-face-truncated-src.html
 load large_border_image_width.html
 load long-url-list-stack-overflow.html
-pref(layout.css.background-clip-text.enabled,true) load 1264949.html
-pref(layout.css.background-clip-text.enabled,true) load 1270795.html
-pref(layout.css.background-clip-text.enabled,true) load 1275026.html
+load 1264949.html
+load 1270795.html
+load 1275026.html
 load 1278463-1.html
 pref(dom.animations-api.core.enabled,true) load 1277908-1.html # bug 1323652
 load 1277908-2.html
 load 1282076-1.html
 pref(dom.animations-api.core.enabled,true) load 1282076-2.html
 pref(dom.animations-api.core.enabled,true) load 1290994-1.html
 pref(dom.animations-api.core.enabled,true) load 1290994-2.html
 pref(dom.animations-api.core.enabled,true) load 1290994-3.html
--- a/layout/style/nsCSSProps.cpp
+++ b/layout/style/nsCSSProps.cpp
@@ -893,18 +893,16 @@ const KTableEntry nsCSSProps::kBackgroun
   { eCSSKeyword_content_box, StyleGeometryBox::ContentBox },
   { eCSSKeyword_UNKNOWN, -1 }
 };
 
 KTableEntry nsCSSProps::kBackgroundClipKTable[] = {
   { eCSSKeyword_border_box, StyleGeometryBox::BorderBox },
   { eCSSKeyword_padding_box, StyleGeometryBox::PaddingBox },
   { eCSSKeyword_content_box, StyleGeometryBox::ContentBox },
-  // The next entry is controlled by the layout.css.background-clip-text.enabled
-  // pref.
   { eCSSKeyword_text, StyleGeometryBox::Text },
   { eCSSKeyword_UNKNOWN, -1 }
 };
 
 const KTableEntry nsCSSProps::kMaskOriginKTable[] = {
   { eCSSKeyword_border_box, StyleGeometryBox::BorderBox },
   { eCSSKeyword_padding_box, StyleGeometryBox::PaddingBox },
   { eCSSKeyword_content_box, StyleGeometryBox::ContentBox },
--- a/layout/style/test/property_database.js
+++ b/layout/style/test/property_database.js
@@ -2214,16 +2214,18 @@ var gCSSProperties = {
         "top left / contain, bottom right / cover",
         /* test cases with clip+origin in the shorthand */
         "url(404.png) green padding-box",
         "url(404.png) border-box transparent",
         "content-box url(404.png) blue",
         "url(404.png) green padding-box padding-box",
         "url(404.png) green padding-box border-box",
         "content-box border-box url(404.png) blue",
+        "url(404.png) green padding-box text",
+        "content-box text url(404.png) blue"
     ],
     invalid_values: [
       /* mixes with keywords have to be in correct order */
       "50% left", "top 50%",
       /* no quirks mode colors */
       "radial-gradient(at 10% bottom, ffffff, black) scroll no-repeat",
       /* no quirks mode lengths */
       "linear-gradient(red -99, yellow, green, blue 120%)",
@@ -2266,17 +2268,17 @@ var gCSSProperties = {
     /*
      * When we rename this to 'background-clip', we also
      * need to rename the values to match the spec.
      */
     domProp: "backgroundClip",
     inherited: false,
     type: CSS_TYPE_LONGHAND,
     initial_values: [ "border-box" ],
-    other_values: [ "content-box", "padding-box", "border-box, padding-box", "padding-box, padding-box, padding-box", "border-box, border-box" ],
+    other_values: [ "content-box", "padding-box", "border-box, padding-box", "padding-box, padding-box, padding-box", "border-box, border-box", "text", "content-box, text", "text, border-box", "text, text" ],
     invalid_values: [ "margin-box", "border-box border-box", "fill-box", "stroke-box", "view-box", "no-clip" ]
   },
   "background-color": {
     domProp: "backgroundColor",
     inherited: false,
     type: CSS_TYPE_LONGHAND,
     initial_values: [ "transparent", "rgba(0, 0, 0, 0)" ],
     other_values: [ "green", "rgb(255, 0, 128)", "#fc2", "#96ed2a", "black", "rgba(255,255,0,3)", "hsl(240, 50%, 50%)", "rgb(50%, 50%, 50%)", "-moz-default-background-color", "rgb(100, 100.0, 100)", "rgba(255, 127, 15, 0)", "hsla(240, 97%, 50%, 0.0)", "rgba(255,255,255,-3.7)" ],
@@ -7841,40 +7843,16 @@ if (IsCSSPropertyPrefEnabled("layout.css
   gCSSProperties["clear"].other_values.push("inline-end");
 } else {
   gCSSProperties["float"].invalid_values.push("inline-start");
   gCSSProperties["float"].invalid_values.push("inline-end");
   gCSSProperties["clear"].invalid_values.push("inline-start");
   gCSSProperties["clear"].invalid_values.push("inline-end");
 }
 
-if (IsCSSPropertyPrefEnabled("layout.css.background-clip-text.enabled")) {
-  gCSSProperties["background-clip"].other_values.push(
-    "text",
-    "content-box, text",
-    "text, border-box",
-    "text, text"
-  );
-  gCSSProperties["background"].other_values.push(
-    "url(404.png) green padding-box text",
-    "content-box text url(404.png) blue"
-  );
-} else {
-  gCSSProperties["background-clip"].invalid_values.push(
-    "text",
-    "content-box, text",
-    "text, border-box",
-    "text, text"
-  );
-  gCSSProperties["background"].invalid_values.push(
-    "url(404.png) green padding-box text",
-    "content-box text url(404.png) blue"
-  );
-}
-
 if (IsCSSPropertyPrefEnabled("layout.css.display-flow-root.enabled")) {
   gCSSProperties["display"].other_values.push("flow-root");
 }
 
 // Copy aliased properties' fields from their alias targets.
 for (var prop in gCSSProperties) {
   var entry = gCSSProperties[prop];
   if (entry.alias_for) {
--- a/media/libopus/README_MOZILLA
+++ b/media/libopus/README_MOZILLA
@@ -3,9 +3,9 @@ IETF Opus audio codec reference implemen
 The source in this directory was copied from an opus
 repository checkout by running the ./update.sh script.
 Any changes made to this version of the source should
 be reflected in that script, e.g. by applying patch
 files after the copy step.
 
 The upstream repository is https://git.xiph.org/opus.git
 
-The git tag/revision used was v1.1.4.
+The git tag/revision used was v1.1.5.
--- a/media/libopus/moz.build
+++ b/media/libopus/moz.build
@@ -15,17 +15,17 @@ EXPORTS.opus += [
 ]
 
 # We allow warnings for third-party code that can be updated from upstream.
 ALLOW_COMPILER_WARNINGS = True
 
 FINAL_LIBRARY = 'gkmedias'
 
 DEFINES['OPUS_BUILD'] = True
-DEFINES['OPUS_VERSION'] = '"v1.1.4-mozilla"'
+DEFINES['OPUS_VERSION'] = '"v1.1.5-mozilla"'
 DEFINES['USE_ALLOCA'] = True
 
 # Don't export symbols
 DEFINES['OPUS_EXPORT'] = ''
 
 if CONFIG['CPU_ARCH'] == 'arm' and CONFIG['GNU_AS']:
     DEFINES['OPUS_ARM_ASM'] = True
     DEFINES['OPUS_ARM_EXTERNAL_ASM'] = True
--- a/media/libopus/src/opus_multistream_encoder.c
+++ b/media/libopus/src/opus_multistream_encoder.c
@@ -272,17 +272,17 @@ void surround_analysis(const CELTMode *c
       (*copy_channel_in)(x, 1, pcm, channels, c, len);
       celt_preemphasis(x, in+overlap, frame_size, 1, upsample, celt_mode->preemph, preemph_mem+c, 0);
 #ifndef FIXED_POINT
       {
          opus_val32 sum;
          sum = celt_inner_prod(in, in, frame_size+overlap, 0);
          /* This should filter out both NaNs and ridiculous signals that could
             cause NaNs further down. */
-         if (!(sum < 1e9f) || celt_isnan(sum))
+         if (!(sum < 1e18f) || celt_isnan(sum))
          {
             OPUS_CLEAR(in, frame_size+overlap);
             preemph_mem[c] = 0;
          }
       }
 #endif
       clt_mdct_forward(&celt_mode->mdct, in, freq, celt_mode->window,
             overlap, celt_mode->maxLM-LM, 1, arch);
--- a/media/libstagefright/binding/DecoderData.cpp
+++ b/media/libstagefright/binding/DecoderData.cpp
@@ -170,29 +170,16 @@ MP4VideoInfo::Update(const MetaData* aMe
   UpdateTrackInfo(*this, aMetaData, aMimeType);
   mDisplay.width = FindInt32(aMetaData, kKeyDisplayWidth);
   mDisplay.height = FindInt32(aMetaData, kKeyDisplayHeight);
   mImage.width = FindInt32(aMetaData, kKeyWidth);
   mImage.height = FindInt32(aMetaData, kKeyHeight);
   mRotation = VideoInfo::ToSupportedRotation(FindInt32(aMetaData, kKeyRotation));
 
   FindData(aMetaData, kKeyAVCC, mExtraData);
-  if (!mExtraData->Length()) {
-    if (FindData(aMetaData, kKeyESDS, mExtraData)) {
-      ESDS esds(mExtraData->Elements(), mExtraData->Length());
-
-      const void* data;
-      size_t size;
-      if (esds.getCodecSpecificInfo(&data, &size) == OK) {
-        const uint8_t* cdata = reinterpret_cast<const uint8_t*>(data);
-        mCodecSpecificConfig->AppendElements(cdata, size);
-      }
-    }
-  }
-
 }
 
 static void
 UpdateTrackProtectedInfo(mozilla::TrackInfo& aConfig,
                          const mp4parse_sinf_info& aSinf)
 {
   if (aSinf.is_encrypted != 0) {
     aConfig.mCrypto.mValid = true;
--- a/media/webrtc/signaling/gtest/jsep_session_unittest.cpp
+++ b/media/webrtc/signaling/gtest/jsep_session_unittest.cpp
@@ -921,22 +921,18 @@ protected:
     }
   }
 
   void CheckPairs(const JsepSession& session, const std::string& context)
   {
     auto pairs = session.GetNegotiatedTrackPairs();
 
     for (JsepTrackPair& pair : pairs) {
-      if (types.size() == 1) {
-        ASSERT_FALSE(pair.HasBundleLevel()) << context;
-      } else {
-        ASSERT_TRUE(pair.HasBundleLevel()) << context;
-        ASSERT_EQ(0U, pair.BundleLevel()) << context;
-      }
+      ASSERT_TRUE(pair.HasBundleLevel()) << context;
+      ASSERT_EQ(0U, pair.BundleLevel()) << context;
     }
   }
 
   void
   DisableMsid(std::string* sdp) const {
     size_t pos = sdp->find("a=msid-semantic");
     ASSERT_NE(std::string::npos, pos);
     (*sdp)[pos + 2] = 'X'; // garble, a=Xsid-semantic
--- a/media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
+++ b/media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
@@ -670,17 +670,17 @@ JsepSessionImpl::SetupBundle(Sdp* sdp) c
         // Set port to 0 for sections with bundle-only attribute. (mjf)
         sdp->GetMediaSection(i).SetPort(0);
       }
 
       mids.push_back(attrs.GetMid());
     }
   }
 
-  if (mids.size() > 1) {
+  if (mids.size() >= 1) {
     UniquePtr<SdpGroupAttributeList> groupAttr(new SdpGroupAttributeList);
     groupAttr->PushEntry(SdpGroupAttributeList::kBundle, mids);
     sdp->GetAttributeList().SetAttribute(groupAttr.release());
   }
 }
 
 nsresult
 JsepSessionImpl::GetRemoteIds(const Sdp& sdp,
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -140,16 +140,20 @@ GECKOVIEW_JARS = \
   constants.jar \
   gecko-R.jar \
   gecko-mozglue.jar \
   gecko-util.jar \
   gecko-view.jar \
   sync-thirdparty.jar \
   $(NULL)
 
+ifdef MOZ_ANDROID_HLS_SUPPORT
+GECKOVIEW_JARS += exoplayer2.jar
+endif
+
 ifdef MOZ_INSTALL_TRACKING
 GECKOVIEW_JARS += gecko-thirdparty-adjust_sdk.jar
 endif
 
 ifdef MOZ_ANDROID_MMA
 GECKOVIEW_JARS += gecko-thirdparty-leanplum_sdk.jar
 endif
 
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -1409,8 +1409,378 @@ gvjar.sources += ['generated/org/mozilla
     'media/ICodec.java',
     'media/ICodecCallbacks.java',
     'media/IMediaDrmBridge.java',
     'media/IMediaDrmBridgeCallbacks.java',
     'media/IMediaManager.java',
     'process/IChildProcess.java',
     'process/IProcessManager.java',
 ]]
+
+if CONFIG['MOZ_ANDROID_HLS_SUPPORT']:
+    gvjar.extra_jars += [
+        'exoplayer2.jar',
+    ]
+
+    exoplayer2_jar = add_java_jar('exoplayer2')
+
+    exoplayer2_jar.javac_flags += [
+        '-Xlint:all,-serial,-rawtypes,-unchecked,-fallthrough',
+    ]
+
+    exoplayer2_jar.extra_jars += [
+        CONFIG['ANDROID_SUPPORT_ANNOTATIONS_JAR_LIB'],
+    ]
+
+    exoplayer2_jar.sources += [geckoview_thirdparty_source_dir + 'java/com/google/android/exoplayer2/' + x for x in [
+        'audio/Ac3Util.java',
+        'audio/AudioCapabilities.java',
+        'audio/AudioCapabilitiesReceiver.java',
+        'audio/AudioDecoderException.java',
+        'audio/AudioProcessor.java',
+        'audio/AudioRendererEventListener.java',
+        'audio/AudioTrack.java',
+        'audio/ChannelMappingAudioProcessor.java',
+        'audio/DtsUtil.java',
+        'audio/MediaCodecAudioRenderer.java',
+        'audio/ResamplingAudioProcessor.java',
+        'audio/SimpleDecoderAudioRenderer.java',
+        'audio/Sonic.java',
+        'audio/SonicAudioProcessor.java',
+        'BaseRenderer.java',
+        'C.java',
+        'decoder/Buffer.java',
+        'decoder/CryptoInfo.java',
+        'decoder/Decoder.java',
+        'decoder/DecoderCounters.java',
+        'decoder/DecoderInputBuffer.java',
+        'decoder/OutputBuffer.java',
+        'decoder/SimpleDecoder.java',
+        'decoder/SimpleOutputBuffer.java',
+        'DefaultLoadControl.java',
+        'DefaultRenderersFactory.java',
+        'drm/DecryptionException.java',
+        'drm/DefaultDrmSessionManager.java',
+        'drm/DrmInitData.java',
+        'drm/DrmSession.java',
+        'drm/DrmSessionManager.java',
+        'drm/ExoMediaCrypto.java',
+        'drm/ExoMediaDrm.java',
+        'drm/FrameworkMediaCrypto.java',
+        'drm/FrameworkMediaDrm.java',
+        'drm/HttpMediaDrmCallback.java',
+        'drm/KeysExpiredException.java',
+        'drm/MediaDrmCallback.java',
+        'drm/OfflineLicenseHelper.java',
+        'drm/UnsupportedDrmException.java',
+        'drm/WidevineUtil.java',
+        'ExoPlaybackException.java',
+        'ExoPlayer.java',
+        'ExoPlayerFactory.java',
+        'ExoPlayerImpl.java',
+        'ExoPlayerImplInternal.java',
+        'ExoPlayerLibraryInfo.java',
+        'extractor/ChunkIndex.java',
+        'extractor/DefaultExtractorInput.java',
+        'extractor/DefaultExtractorsFactory.java',
+        'extractor/DefaultTrackOutput.java',
+        'extractor/DummyTrackOutput.java',
+        'extractor/Extractor.java',
+        'extractor/ExtractorInput.java',
+        'extractor/ExtractorOutput.java',
+        'extractor/ExtractorsFactory.java',
+        'extractor/flv/AudioTagPayloadReader.java',
+        'extractor/flv/FlvExtractor.java',
+        'extractor/flv/ScriptTagPayloadReader.java',
+        'extractor/flv/TagPayloadReader.java',
+        'extractor/flv/VideoTagPayloadReader.java',
+        'extractor/GaplessInfoHolder.java',
+        'extractor/mkv/DefaultEbmlReader.java',
+        'extractor/mkv/EbmlReader.java',
+        'extractor/mkv/EbmlReaderOutput.java',
+        'extractor/mkv/MatroskaExtractor.java',
+        'extractor/mkv/Sniffer.java',
+        'extractor/mkv/VarintReader.java',
+        'extractor/mp3/ConstantBitrateSeeker.java',
+        'extractor/mp3/Mp3Extractor.java',
+        'extractor/mp3/VbriSeeker.java',
+        'extractor/mp3/XingSeeker.java',
+        'extractor/mp4/Atom.java',
+        'extractor/mp4/AtomParsers.java',
+        'extractor/mp4/DefaultSampleValues.java',
+        'extractor/mp4/FixedSampleSizeRechunker.java',
+        'extractor/mp4/FragmentedMp4Extractor.java',
+        'extractor/mp4/MetadataUtil.java',
+        'extractor/mp4/Mp4Extractor.java',
+        'extractor/mp4/PsshAtomUtil.java',
+        'extractor/mp4/Sniffer.java',
+        'extractor/mp4/Track.java',
+        'extractor/mp4/TrackEncryptionBox.java',
+        'extractor/mp4/TrackFragment.java',
+        'extractor/mp4/TrackSampleTable.java',
+        'extractor/MpegAudioHeader.java',
+        'extractor/ogg/DefaultOggSeeker.java',
+        'extractor/ogg/FlacReader.java',
+        'extractor/ogg/OggExtractor.java',
+        'extractor/ogg/OggPacket.java',
+        'extractor/ogg/OggPageHeader.java',
+        'extractor/ogg/OggSeeker.java',
+        'extractor/ogg/OpusReader.java',
+        'extractor/ogg/StreamReader.java',
+        'extractor/ogg/VorbisBitArray.java',
+        'extractor/ogg/VorbisReader.java',
+        'extractor/ogg/VorbisUtil.java',
+        'extractor/PositionHolder.java',
+        'extractor/rawcc/RawCcExtractor.java',
+        'extractor/SeekMap.java',
+        'extractor/TrackOutput.java',
+        'extractor/ts/Ac3Extractor.java',
+        'extractor/ts/Ac3Reader.java',
+        'extractor/ts/AdtsExtractor.java',
+        'extractor/ts/AdtsReader.java',
+        'extractor/ts/DefaultTsPayloadReaderFactory.java',
+        'extractor/ts/DtsReader.java',
+        'extractor/ts/DvbSubtitleReader.java',
+        'extractor/ts/ElementaryStreamReader.java',
+        'extractor/ts/H262Reader.java',
+        'extractor/ts/H264Reader.java',
+        'extractor/ts/H265Reader.java',
+        'extractor/ts/Id3Reader.java',
+        'extractor/ts/MpegAudioReader.java',
+        'extractor/ts/NalUnitTargetBuffer.java',
+        'extractor/ts/PesReader.java',
+        'extractor/ts/PsExtractor.java',
+        'extractor/ts/SectionPayloadReader.java',
+        'extractor/ts/SectionReader.java',
+        'extractor/ts/SeiReader.java',
+        'extractor/ts/SpliceInfoSectionReader.java',
+        'extractor/ts/TsExtractor.java',
+        'extractor/ts/TsPayloadReader.java',
+        'extractor/wav/WavExtractor.java',
+        'extractor/wav/WavHeader.java',
+        'extractor/wav/WavHeaderReader.java',
+        'Format.java',
+        'FormatHolder.java',
+        'IllegalSeekPositionException.java',
+        'LoadControl.java',
+        'mediacodec/MediaCodecInfo.java',
+        'mediacodec/MediaCodecRenderer.java',
+        'mediacodec/MediaCodecSelector.java',
+        'mediacodec/MediaCodecUtil.java',
+        'metadata/emsg/EventMessage.java',
+        'metadata/emsg/EventMessageDecoder.java',
+        'metadata/id3/ApicFrame.java',
+        'metadata/id3/BinaryFrame.java',
+        'metadata/id3/ChapterFrame.java',
+        'metadata/id3/ChapterTocFrame.java',
+        'metadata/id3/CommentFrame.java',
+        'metadata/id3/GeobFrame.java',
+        'metadata/id3/Id3Decoder.java',
+        'metadata/id3/Id3Frame.java',
+        'metadata/id3/PrivFrame.java',
+        'metadata/id3/TextInformationFrame.java',
+        'metadata/id3/UrlLinkFrame.java',
+        'metadata/Metadata.java',
+        'metadata/MetadataDecoder.java',
+        'metadata/MetadataDecoderException.java',
+        'metadata/MetadataDecoderFactory.java',
+        'metadata/MetadataInputBuffer.java',
+        'metadata/MetadataRenderer.java',
+        'metadata/scte35/PrivateCommand.java',
+        'metadata/scte35/SpliceCommand.java',
+        'metadata/scte35/SpliceInfoDecoder.java',
+        'metadata/scte35/SpliceInsertCommand.java',
+        'metadata/scte35/SpliceNullCommand.java',
+        'metadata/scte35/SpliceScheduleCommand.java',
+        'metadata/scte35/TimeSignalCommand.java',
+        'ParserException.java',
+        'PlaybackParameters.java',
+        'Renderer.java',
+        'RendererCapabilities.java',
+        'RendererConfiguration.java',
+        'RenderersFactory.java',
+        'SimpleExoPlayer.java',
+        'source/AdaptiveMediaSourceEventListener.java',
+        'source/BehindLiveWindowException.java',
+        'source/chunk/BaseMediaChunk.java',
+        'source/chunk/BaseMediaChunkOutput.java',
+        'source/chunk/Chunk.java',
+        'source/chunk/ChunkedTrackBlacklistUtil.java',
+        'source/chunk/ChunkExtractorWrapper.java',
+        'source/chunk/ChunkHolder.java',
+        'source/chunk/ChunkSampleStream.java',
+        'source/chunk/ChunkSource.java',
+        'source/chunk/ContainerMediaChunk.java',
+        'source/chunk/DataChunk.java',
+        'source/chunk/InitializationChunk.java',
+        'source/chunk/MediaChunk.java',
+        'source/chunk/SingleSampleMediaChunk.java',
+        'source/ClippingMediaPeriod.java',
+        'source/ClippingMediaSource.java',
+        'source/CompositeSequenceableLoader.java',
+        'source/ConcatenatingMediaSource.java',
+        'source/EmptySampleStream.java',
+        'source/ExtractorMediaPeriod.java',
+        'source/ExtractorMediaSource.java',
+        'source/hls/Aes128DataSource.java',
+        'source/hls/DefaultHlsDataSourceFactory.java',
+        'source/hls/HlsChunkSource.java',
+        'source/hls/HlsDataSourceFactory.java',
+        'source/hls/HlsManifest.java',
+        'source/hls/HlsMediaChunk.java',
+        'source/hls/HlsMediaPeriod.java',
+        'source/hls/HlsMediaSource.java',
+        'source/hls/HlsSampleStream.java',
+        'source/hls/HlsSampleStreamWrapper.java',
+        'source/hls/playlist/HlsMasterPlaylist.java',
+        'source/hls/playlist/HlsMediaPlaylist.java',
+        'source/hls/playlist/HlsPlaylist.java',
+        'source/hls/playlist/HlsPlaylistParser.java',
+        'source/hls/playlist/HlsPlaylistTracker.java',
+        'source/hls/TimestampAdjusterProvider.java',
+        'source/hls/WebvttExtractor.java',
+        'source/LoopingMediaSource.java',
+        'source/MediaPeriod.java',
+        'source/MediaSource.java',
+        'source/MergingMediaPeriod.java',
+        'source/MergingMediaSource.java',
+        'source/SampleStream.java',
+        'source/SequenceableLoader.java',
+        'source/SinglePeriodTimeline.java',
+        'source/SingleSampleMediaPeriod.java',
+        'source/SingleSampleMediaSource.java',
+        'source/TrackGroup.java',
+        'source/TrackGroupArray.java',
+        'source/UnrecognizedInputFormatException.java',
+        'text/CaptionStyleCompat.java',
+        'text/cea/Cea608Decoder.java',
+        'text/cea/Cea708Cue.java',
+        'text/cea/Cea708Decoder.java',
+        'text/cea/CeaDecoder.java',
+        'text/cea/CeaOutputBuffer.java',
+        'text/cea/CeaSubtitle.java',
+        'text/cea/CeaUtil.java',
+        'text/Cue.java',
+        'text/dvb/DvbDecoder.java',
+        'text/dvb/DvbParser.java',
+        'text/dvb/DvbSubtitle.java',
+        'text/SimpleSubtitleDecoder.java',
+        'text/SimpleSubtitleOutputBuffer.java',
+        'text/subrip/SubripDecoder.java',
+        'text/subrip/SubripSubtitle.java',
+        'text/Subtitle.java',
+        'text/SubtitleDecoder.java',
+        'text/SubtitleDecoderException.java',
+        'text/SubtitleDecoderFactory.java',
+        'text/SubtitleInputBuffer.java',
+        'text/SubtitleOutputBuffer.java',
+        'text/TextRenderer.java',
+        'text/ttml/TtmlDecoder.java',
+        'text/ttml/TtmlNode.java',
+        'text/ttml/TtmlRegion.java',
+        'text/ttml/TtmlRenderUtil.java',
+        'text/ttml/TtmlStyle.java',
+        'text/ttml/TtmlSubtitle.java',
+        'text/tx3g/Tx3gDecoder.java',
+        'text/tx3g/Tx3gSubtitle.java',
+        'text/webvtt/CssParser.java',
+        'text/webvtt/Mp4WebvttDecoder.java',
+        'text/webvtt/Mp4WebvttSubtitle.java',
+        'text/webvtt/WebvttCssStyle.java',
+        'text/webvtt/WebvttCue.java',
+        'text/webvtt/WebvttCueParser.java',
+        'text/webvtt/WebvttDecoder.java',
+        'text/webvtt/WebvttParserUtil.java',
+        'text/webvtt/WebvttSubtitle.java',
+        'Timeline.java',
+        'trackselection/AdaptiveTrackSelection.java',
+        'trackselection/BaseTrackSelection.java',
+        'trackselection/DefaultTrackSelector.java',
+        'trackselection/FixedTrackSelection.java',
+        'trackselection/MappingTrackSelector.java',
+        'trackselection/RandomTrackSelection.java',
+        'trackselection/TrackSelection.java',
+        'trackselection/TrackSelectionArray.java',
+        'trackselection/TrackSelector.java',
+        'trackselection/TrackSelectorResult.java',
+        'upstream/Allocation.java',
+        'upstream/Allocator.java',
+        'upstream/AssetDataSource.java',
+        'upstream/BandwidthMeter.java',
+        'upstream/ByteArrayDataSink.java',
+        'upstream/ByteArrayDataSource.java',
+        'upstream/cache/Cache.java',
+        'upstream/cache/CacheDataSink.java',
+        'upstream/cache/CacheDataSinkFactory.java',
+        'upstream/cache/CacheDataSource.java',
+        'upstream/cache/CacheDataSourceFactory.java',
+        'upstream/cache/CachedContent.java',
+        'upstream/cache/CachedContentIndex.java',
+        'upstream/cache/CachedRegionTracker.java',
+        'upstream/cache/CacheEvictor.java',
+        'upstream/cache/CacheSpan.java',
+        'upstream/cache/CacheUtil.java',
+        'upstream/cache/LeastRecentlyUsedCacheEvictor.java',
+        'upstream/cache/NoOpCacheEvictor.java',
+        'upstream/cache/SimpleCache.java',
+        'upstream/cache/SimpleCacheSpan.java',
+        'upstream/ContentDataSource.java',
+        'upstream/crypto/AesCipherDataSink.java',
+        'upstream/crypto/AesCipherDataSource.java',
+        'upstream/crypto/AesFlushingCipher.java',
+        'upstream/crypto/CryptoUtil.java',
+        'upstream/DataSink.java',
+        'upstream/DataSource.java',
+        'upstream/DataSourceException.java',
+        'upstream/DataSourceInputStream.java',
+        'upstream/DataSpec.java',
+        'upstream/DefaultAllocator.java',
+        'upstream/DefaultBandwidthMeter.java',
+        'upstream/DefaultDataSource.java',
+        'upstream/DefaultDataSourceFactory.java',
+        'upstream/DefaultHttpDataSource.java',
+        'upstream/DefaultHttpDataSourceFactory.java',
+        'upstream/DummyDataSource.java',
+        'upstream/FileDataSource.java',
+        'upstream/FileDataSourceFactory.java',
+        'upstream/HttpDataSource.java',
+        'upstream/Loader.java',
+        'upstream/LoaderErrorThrower.java',
+        'upstream/ParsingLoadable.java',
+        'upstream/PriorityDataSource.java',
+        'upstream/PriorityDataSourceFactory.java',
+        'upstream/RawResourceDataSource.java',
+        'upstream/TeeDataSource.java',
+        'upstream/TransferListener.java',
+        'upstream/UdpDataSource.java',
+        'util/Assertions.java',
+        'util/AtomicFile.java',
+        'util/Clock.java',
+        'util/CodecSpecificDataUtil.java',
+        'util/ColorParser.java',
+        'util/ConditionVariable.java',
+        'util/FlacStreamInfo.java',
+        'util/LibraryLoader.java',
+        'util/LongArray.java',
+        'util/MediaClock.java',
+        'util/MimeTypes.java',
+        'util/NalUnitUtil.java',
+        'util/ParsableBitArray.java',
+        'util/ParsableByteArray.java',
+        'util/ParsableNalUnitBitArray.java',
+        'util/Predicate.java',
+        'util/PriorityTaskManager.java',
+        'util/ReusableBufferedOutputStream.java',
+        'util/SlidingPercentile.java',
+        'util/StandaloneMediaClock.java',
+        'util/SystemClock.java',
+        'util/TimestampAdjuster.java',
+        'util/TraceUtil.java',
+        'util/UriUtil.java',
+        'util/Util.java',
+        'util/XmlPullParserUtil.java',
+        'video/AvcConfig.java',
+        'video/ColorInfo.java',
+        'video/HevcConfig.java',
+        'video/MediaCodecVideoRenderer.java',
+        'video/VideoFrameReleaseTimeHelper.java',
+        'video/VideoRendererEventListener.java',
+    ]]
--- a/mobile/android/base/resources/layout/home_pager.xml
+++ b/mobile/android/base/resources/layout/home_pager.xml
@@ -15,12 +15,11 @@
 
     <org.mozilla.gecko.home.TabMenuStrip android:layout_width="match_parent"
                                          android:layout_height="@dimen/tabs_strip_height"
                                          android:background="@color/about_page_header_grey"
                                          android:layout_gravity="top"
                                          gecko:strip="@drawable/home_tab_menu_strip"
                                          gecko:activeTextColor="@color/placeholder_grey"
                                          gecko:inactiveTextColor="@color/tab_text_color"
-                                         gecko:tabsMarginLeft="@dimen/tab_strip_content_start"
-                                         gecko:titlebarFill="true" />
+                                         gecko:tabsMarginLeft="@dimen/tab_strip_content_start" />
 
 </org.mozilla.gecko.home.HomePager>
--- a/mobile/android/geckoview/build.gradle
+++ b/mobile/android/geckoview/build.gradle
@@ -68,16 +68,20 @@ android {
         abortOnError false
     }
 
     sourceSets {
         main {
             java {
                 srcDir "${topsrcdir}/mobile/android/geckoview/src/thirdparty/java"
 
+                if (!mozconfig.substs.MOZ_ANDROID_HLS_SUPPORT) {
+                    exclude 'com/google/android/exoplayer2/**'
+                }
+
                 // TODO: support WebRTC.
                 // if (mozconfig.substs.MOZ_WEBRTC) {
                 //     srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/audio_device/android/java/src"
                 //     srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_capture/android/java/src"
                 //     srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_render/android/java/src"
                 // }
 
                 // TODO: don't use AppConstants.
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/BaseRenderer.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MediaClock;
+import java.io.IOException;
+
+/**
+ * An abstract base class suitable for most {@link Renderer} implementations.
+ */
+public abstract class BaseRenderer implements Renderer, RendererCapabilities {
+
+  private final int trackType;
+
+  private RendererConfiguration configuration;
+  private int index;
+  private int state;
+  private SampleStream stream;
+  private long streamOffsetUs;
+  private boolean readEndOfStream;
+  private boolean streamIsFinal;
+
+  /**
+   * @param trackType The track type that the renderer handles. One of the {@link C}
+   * {@code TRACK_TYPE_*} constants.
+   */
+  public BaseRenderer(int trackType) {
+    this.trackType = trackType;
+    readEndOfStream = true;
+  }
+
+  @Override
+  public final int getTrackType() {
+    return trackType;
+  }
+
+  @Override
+  public final RendererCapabilities getCapabilities() {
+    return this;
+  }
+
+  @Override
+  public final void setIndex(int index) {
+    this.index = index;
+  }
+
+  @Override
+  public MediaClock getMediaClock() {
+    return null;
+  }
+
+  @Override
+  public final int getState() {
+    return state;
+  }
+
+  @Override
+  public final void enable(RendererConfiguration configuration, Format[] formats,
+      SampleStream stream, long positionUs, boolean joining, long offsetUs)
+      throws ExoPlaybackException {
+    Assertions.checkState(state == STATE_DISABLED);
+    this.configuration = configuration;
+    state = STATE_ENABLED;
+    onEnabled(joining);
+    replaceStream(formats, stream, offsetUs);
+    onPositionReset(positionUs, joining);
+  }
+
+  @Override
+  public final void start() throws ExoPlaybackException {
+    Assertions.checkState(state == STATE_ENABLED);
+    state = STATE_STARTED;
+    onStarted();
+  }
+
+  @Override
+  public final void replaceStream(Format[] formats, SampleStream stream, long offsetUs)
+      throws ExoPlaybackException {
+    Assertions.checkState(!streamIsFinal);
+    this.stream = stream;
+    readEndOfStream = false;
+    streamOffsetUs = offsetUs;
+    onStreamChanged(formats);
+  }
+
+  @Override
+  public final SampleStream getStream() {
+    return stream;
+  }
+
+  @Override
+  public final boolean hasReadStreamToEnd() {
+    return readEndOfStream;
+  }
+
+  @Override
+  public final void setCurrentStreamFinal() {
+    streamIsFinal = true;
+  }
+
+  @Override
+  public final boolean isCurrentStreamFinal() {
+    return streamIsFinal;
+  }
+
+  @Override
+  public final void maybeThrowStreamError() throws IOException {
+    stream.maybeThrowError();
+  }
+
+  @Override
+  public final void resetPosition(long positionUs) throws ExoPlaybackException {
+    streamIsFinal = false;
+    readEndOfStream = false;
+    onPositionReset(positionUs, false);
+  }
+
+  @Override
+  public final void stop() throws ExoPlaybackException {
+    Assertions.checkState(state == STATE_STARTED);
+    state = STATE_ENABLED;
+    onStopped();
+  }
+
+  @Override
+  public final void disable() {
+    Assertions.checkState(state == STATE_ENABLED);
+    state = STATE_DISABLED;
+    onDisabled();
+    stream = null;
+    streamIsFinal = false;
+  }
+
+  // RendererCapabilities implementation.
+
+  @Override
+  public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
+    return ADAPTIVE_NOT_SUPPORTED;
+  }
+
+  // ExoPlayerComponent implementation.
+
+  @Override
+  public void handleMessage(int what, Object object) throws ExoPlaybackException {
+    // Do nothing.
+  }
+
+  // Methods to be overridden by subclasses.
+
+  /**
+   * Called when the renderer is enabled.
+   * <p>
+   * The default implementation is a no-op.
+   *
+   * @param joining Whether this renderer is being enabled to join an ongoing playback.
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  protected void onEnabled(boolean joining) throws ExoPlaybackException {
+    // Do nothing.
+  }
+
+  /**
+   * Called when the renderer's stream has changed. This occurs when the renderer is enabled after
+   * {@link #onEnabled(boolean)} has been called, and also when the stream has been replaced whilst
+   * the renderer is enabled or started.
+   * <p>
+   * The default implementation is a no-op.
+   *
+   * @param formats The enabled formats.
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+    // Do nothing.
+  }
+
+  /**
+   * Called when the position is reset. This occurs when the renderer is enabled after
+   * {@link #onStreamChanged(Format[])} has been called, and also when a position discontinuity
+   * is encountered.
+   * <p>
+   * After a position reset, the renderer's {@link SampleStream} is guaranteed to provide samples
+   * starting from a key frame.
+   * <p>
+   * The default implementation is a no-op.
+   *
+   * @param positionUs The new playback position in microseconds.
+   * @param joining Whether this renderer is being enabled to join an ongoing playback.
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+    // Do nothing.
+  }
+
+  /**
+   * Called when the renderer is started.
+   * <p>
+   * The default implementation is a no-op.
+   *
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  protected void onStarted() throws ExoPlaybackException {
+    // Do nothing.
+  }
+
+  /**
+   * Called when the renderer is stopped.
+   * <p>
+   * The default implementation is a no-op.
+   *
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  protected void onStopped() throws ExoPlaybackException {
+    // Do nothing.
+  }
+
+  /**
+   * Called when the renderer is disabled.
+   * <p>
+   * The default implementation is a no-op.
+   */
+  protected void onDisabled() {
+    // Do nothing.
+  }
+
+  // Methods to be called by subclasses.
+
+  /**
+   * Returns the configuration set when the renderer was most recently enabled.
+   */
+  protected final RendererConfiguration getConfiguration() {
+    return configuration;
+  }
+
+  /**
+   * Returns the index of the renderer within the player.
+   */
+  protected final int getIndex() {
+    return index;
+  }
+
+  /**
+   * Reads from the enabled upstream source. If the upstream source has been read to the end then
+   * {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been
+   * called. {@link C#RESULT_NOTHING_READ} is returned otherwise.
+   *
+   * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
+   * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
+   *     end of the stream. If the end of the stream has been reached, the
+   *     {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
+   * @param formatRequired Whether the caller requires that the format of the stream be read even if
+   *     it's not changing. A sample will never be read if set to true, however it is still possible
+   *     for the end of stream or nothing to be read.
+   * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
+   *     {@link C#RESULT_BUFFER_READ}.
+   */
+  protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer,
+      boolean formatRequired) {
+    int result = stream.readData(formatHolder, buffer, formatRequired);
+    if (result == C.RESULT_BUFFER_READ) {
+      if (buffer.isEndOfStream()) {
+        readEndOfStream = true;
+        return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ;
+      }
+      buffer.timeUs += streamOffsetUs;
+    } else if (result == C.RESULT_FORMAT_READ) {
+      Format format = formatHolder.format;
+      if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
+        format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + streamOffsetUs);
+        formatHolder.format = format;
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Attempts to skip to the keyframe before the specified position, or to the end of the stream if
+   * {@code positionUs} is beyond it.
+   *
+   * @param positionUs The position in microseconds.
+   */
+  protected void skipSource(long positionUs) {
+    stream.skipData(positionUs - streamOffsetUs);
+  }
+
+  /**
+   * Returns whether the upstream source is ready.
+   *
+   * @return Whether the source is ready.
+   */
+  protected final boolean isSourceReady() {
+    return readEndOfStream ? streamIsFinal : stream.isReady();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/C.java
@@ -0,0 +1,664 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+import android.support.annotation.IntDef;
+import android.view.Surface;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.UUID;
+
+/**
+ * Defines constants used by the library.
+ */
+public final class C {
+
+  private C() {}
+
+  /**
+   * Special constant representing a time corresponding to the end of a source. Suitable for use in
+   * any time base.
+   */
+  public static final long TIME_END_OF_SOURCE = Long.MIN_VALUE;
+
+  /**
+   * Special constant representing an unset or unknown time or duration. Suitable for use in any
+   * time base.
+   */
+  public static final long TIME_UNSET = Long.MIN_VALUE + 1;
+
+  /**
+   * Represents an unset or unknown index.
+   */
+  public static final int INDEX_UNSET = -1;
+
+  /**
+   * Represents an unset or unknown position.
+   */
+  public static final int POSITION_UNSET = -1;
+
+  /**
+   * Represents an unset or unknown length.
+   */
+  public static final int LENGTH_UNSET = -1;
+
+  /**
+   * The number of microseconds in one second.
+   */
+  public static final long MICROS_PER_SECOND = 1000000L;
+
+  /**
+   * The number of nanoseconds in one second.
+   */
+  public static final long NANOS_PER_SECOND = 1000000000L;
+
+  /**
+   * The name of the UTF-8 charset.
+   */
+  public static final String UTF8_NAME = "UTF-8";
+
+  /**
+   * The name of the UTF-16 charset.
+   */
+  public static final String UTF16_NAME = "UTF-16";
+
+  /**
+   * * The name of the serif font family.
+   */
+  public static final String SERIF_NAME = "serif";
+
+  /**
+   * * The name of the sans-serif font family.
+   */
+  public static final String SANS_SERIF_NAME = "sans-serif";
+
+  /**
+   * Crypto modes for a codec.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({CRYPTO_MODE_UNENCRYPTED, CRYPTO_MODE_AES_CTR, CRYPTO_MODE_AES_CBC})
+  public @interface CryptoMode {}
+  /**
+   * @see MediaCodec#CRYPTO_MODE_UNENCRYPTED
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED;
+  /**
+   * @see MediaCodec#CRYPTO_MODE_AES_CTR
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR;
+  /**
+   * @see MediaCodec#CRYPTO_MODE_AES_CBC
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int CRYPTO_MODE_AES_CBC = 0x2;
+
+  /**
+   * Represents an unset {@link android.media.AudioTrack} session identifier. Equal to
+   * {@link AudioManager#AUDIO_SESSION_ID_GENERATE}.
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE;
+
+  /**
+   * Represents an audio encoding, or an invalid or unset value.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
+      ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_AC3, ENCODING_E_AC3, ENCODING_DTS,
+      ENCODING_DTS_HD})
+  public @interface Encoding {}
+
+  /**
+   * Represents a PCM audio encoding, or an invalid or unset value.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
+      ENCODING_PCM_24BIT, ENCODING_PCM_32BIT})
+  public @interface PcmEncoding {}
+  /**
+   * @see AudioFormat#ENCODING_INVALID
+   */
+  public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID;
+  /**
+   * @see AudioFormat#ENCODING_PCM_8BIT
+   */
+  public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT;
+  /**
+   * @see AudioFormat#ENCODING_PCM_16BIT
+   */
+  public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT;
+  /**
+   * PCM encoding with 24 bits per sample.
+   */
+  public static final int ENCODING_PCM_24BIT = 0x80000000;
+  /**
+   * PCM encoding with 32 bits per sample.
+   */
+  public static final int ENCODING_PCM_32BIT = 0x40000000;
+  /**
+   * @see AudioFormat#ENCODING_AC3
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
+  /**
+   * @see AudioFormat#ENCODING_E_AC3
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
+  /**
+   * @see AudioFormat#ENCODING_DTS
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS;
+  /**
+   * @see AudioFormat#ENCODING_DTS_HD
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD;
+
+  /**
+   * @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND
+   */
+  @SuppressWarnings({"InlinedApi", "deprecation"})
+  public static final int CHANNEL_OUT_7POINT1_SURROUND = Util.SDK_INT < 23
+      ? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
+
+  /**
+   * Stream types for an {@link android.media.AudioTrack}.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION, STREAM_TYPE_RING,
+      STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL})
+  public @interface StreamType {}
+  /**
+   * @see AudioManager#STREAM_ALARM
+   */
+  public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM;
+  /**
+   * @see AudioManager#STREAM_MUSIC
+   */
+  public static final int STREAM_TYPE_MUSIC = AudioManager.STREAM_MUSIC;
+  /**
+   * @see AudioManager#STREAM_NOTIFICATION
+   */
+  public static final int STREAM_TYPE_NOTIFICATION = AudioManager.STREAM_NOTIFICATION;
+  /**
+   * @see AudioManager#STREAM_RING
+   */
+  public static final int STREAM_TYPE_RING = AudioManager.STREAM_RING;
+  /**
+   * @see AudioManager#STREAM_SYSTEM
+   */
+  public static final int STREAM_TYPE_SYSTEM = AudioManager.STREAM_SYSTEM;
+  /**
+   * @see AudioManager#STREAM_VOICE_CALL
+   */
+  public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;
+  /**
+   * The default stream type used by audio renderers.
+   */
+  public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
+
+  /**
+   * Flags which can apply to a buffer containing a media sample.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef(flag = true, value = {BUFFER_FLAG_KEY_FRAME, BUFFER_FLAG_END_OF_STREAM,
+      BUFFER_FLAG_ENCRYPTED, BUFFER_FLAG_DECODE_ONLY})
+  public @interface BufferFlags {}
+  /**
+   * Indicates that a buffer holds a synchronization sample.
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int BUFFER_FLAG_KEY_FRAME = MediaCodec.BUFFER_FLAG_KEY_FRAME;
+  /**
+   * Flag for empty buffers that signal that the end of the stream was reached.
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
+  /**
+   * Indicates that a buffer is (at least partially) encrypted.
+   */
+  public static final int BUFFER_FLAG_ENCRYPTED = 0x40000000;
+  /**
+   * Indicates that a buffer should be decoded but not rendered.
+   */
+  public static final int BUFFER_FLAG_DECODE_ONLY = 0x80000000;
+
+  /**
+   * Video scaling modes for {@link MediaCodec}-based {@link Renderer}s.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef(value = {VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING})
+  public @interface VideoScalingMode {}
+  /**
+   * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT =
+      MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT;
+  /**
+   * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING =
+      MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING;
+  /**
+   * A default video scaling mode for {@link MediaCodec}-based {@link Renderer}s.
+   */
+  public static final int VIDEO_SCALING_MODE_DEFAULT = VIDEO_SCALING_MODE_SCALE_TO_FIT;
+
+  /**
+   * Track selection flags.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef(flag = true, value = {SELECTION_FLAG_DEFAULT, SELECTION_FLAG_FORCED,
+      SELECTION_FLAG_AUTOSELECT})
+  public @interface SelectionFlags {}
+  /**
+   * Indicates that the track should be selected if user preferences do not state otherwise.
+   */
+  public static final int SELECTION_FLAG_DEFAULT = 1;
+  /**
+   * Indicates that the track must be displayed. Only applies to text tracks.
+   */
+  public static final int SELECTION_FLAG_FORCED = 2;
+  /**
+   * Indicates that the player may choose to play the track in absence of an explicit user
+   * preference.
+   */
+  public static final int SELECTION_FLAG_AUTOSELECT = 4;
+
+  /**
+   * Represents a streaming or other media type.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_OTHER})
+  public @interface ContentType {}
+  /**
+   * Value returned by {@link Util#inferContentType(String)} for DASH manifests.
+   */
+  public static final int TYPE_DASH = 0;
+  /**
+   * Value returned by {@link Util#inferContentType(String)} for Smooth Streaming manifests.
+   */
+  public static final int TYPE_SS = 1;
+  /**
+   * Value returned by {@link Util#inferContentType(String)} for HLS manifests.
+   */
+  public static final int TYPE_HLS = 2;
+  /**
+   * Value returned by {@link Util#inferContentType(String)} for files other than DASH, HLS or
+   * Smooth Streaming manifests.
+   */
+  public static final int TYPE_OTHER = 3;
+
+  /**
+   * A return value for methods where the end of an input was encountered.
+   */
+  public static final int RESULT_END_OF_INPUT = -1;
+  /**
+   * A return value for methods where the length of parsed data exceeds the maximum length allowed.
+   */
+  public static final int RESULT_MAX_LENGTH_EXCEEDED = -2;
+  /**
+   * A return value for methods where nothing was read.
+   */
+  public static final int RESULT_NOTHING_READ = -3;
+  /**
+   * A return value for methods where a buffer was read.
+   */
+  public static final int RESULT_BUFFER_READ = -4;
+  /**
+   * A return value for methods where a format was read.
+   */
+  public static final int RESULT_FORMAT_READ = -5;
+
+  /**
+   * A data type constant for data of unknown or unspecified type.
+   */
+  public static final int DATA_TYPE_UNKNOWN = 0;
+  /**
+   * A data type constant for media, typically containing media samples.
+   */
+  public static final int DATA_TYPE_MEDIA = 1;
+  /**
+   * A data type constant for media, typically containing only initialization data.
+   */
+  public static final int DATA_TYPE_MEDIA_INITIALIZATION = 2;
+  /**
+   * A data type constant for drm or encryption data.
+   */
+  public static final int DATA_TYPE_DRM = 3;
+  /**
+   * A data type constant for a manifest file.
+   */
+  public static final int DATA_TYPE_MANIFEST = 4;
+  /**
+   * A data type constant for time synchronization data.
+   */
+  public static final int DATA_TYPE_TIME_SYNCHRONIZATION = 5;
+  /**
+   * Applications or extensions may define custom {@code DATA_TYPE_*} constants greater than or
+   * equal to this value.
+   */
+  public static final int DATA_TYPE_CUSTOM_BASE = 10000;
+
+  /**
+   * A type constant for tracks of unknown type.
+   */
+  public static final int TRACK_TYPE_UNKNOWN = -1;
+  /**
+   * A type constant for tracks of some default type, where the type itself is unknown.
+   */
+  public static final int TRACK_TYPE_DEFAULT = 0;
+  /**
+   * A type constant for audio tracks.
+   */
+  public static final int TRACK_TYPE_AUDIO = 1;
+  /**
+   * A type constant for video tracks.
+   */
+  public static final int TRACK_TYPE_VIDEO = 2;
+  /**
+   * A type constant for text tracks.
+   */
+  public static final int TRACK_TYPE_TEXT = 3;
+  /**
+   * A type constant for metadata tracks.
+   */
+  public static final int TRACK_TYPE_METADATA = 4;
+  /**
+   * Applications or extensions may define custom {@code TRACK_TYPE_*} constants greater than or
+   * equal to this value.
+   */
+  public static final int TRACK_TYPE_CUSTOM_BASE = 10000;
+
+  /**
+   * A selection reason constant for selections whose reasons are unknown or unspecified.
+   */
+  public static final int SELECTION_REASON_UNKNOWN = 0;
+  /**
+   * A selection reason constant for an initial track selection.
+   */
+  public static final int SELECTION_REASON_INITIAL = 1;
+  /**
+   * A selection reason constant for an manual (i.e. user initiated) track selection.
+   */
+  public static final int SELECTION_REASON_MANUAL = 2;
+  /**
+   * A selection reason constant for an adaptive track selection.
+   */
+  public static final int SELECTION_REASON_ADAPTIVE = 3;
+  /**
+   * A selection reason constant for a trick play track selection.
+   */
+  public static final int SELECTION_REASON_TRICK_PLAY = 4;
+  /**
+   * Applications or extensions may define custom {@code SELECTION_REASON_*} constants greater than
+   * or equal to this value.
+   */
+  public static final int SELECTION_REASON_CUSTOM_BASE = 10000;
+
+  /**
+   * A default size in bytes for an individual allocation that forms part of a larger buffer.
+   */
+  public static final int DEFAULT_BUFFER_SEGMENT_SIZE = 64 * 1024;
+
+  /**
+   * A default size in bytes for a video buffer.
+   */
+  public static final int DEFAULT_VIDEO_BUFFER_SIZE = 200 * DEFAULT_BUFFER_SEGMENT_SIZE;
+
+  /**
+   * A default size in bytes for an audio buffer.
+   */
+  public static final int DEFAULT_AUDIO_BUFFER_SIZE = 54 * DEFAULT_BUFFER_SEGMENT_SIZE;
+
+  /**
+   * A default size in bytes for a text buffer.
+   */
+  public static final int DEFAULT_TEXT_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE;
+
+  /**
+   * A default size in bytes for a metadata buffer.
+   */
+  public static final int DEFAULT_METADATA_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE;
+
+  /**
+   * A default size in bytes for a muxed buffer (e.g. containing video, audio and text).
+   */
+  public static final int DEFAULT_MUXED_BUFFER_SIZE = DEFAULT_VIDEO_BUFFER_SIZE
+      + DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE;
+
+  /**
+   * The Nil UUID as defined by
+   * <a href="https://tools.ietf.org/html/rfc4122#section-4.1.7">RFC4122</a>.
+   */
+  public static final UUID UUID_NIL = new UUID(0L, 0L);
+
+  /**
+   * UUID for the ClearKey DRM scheme.
+   * <p>
+   * ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up.
+   */
+  public static final UUID CLEARKEY_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL);
+
+  /**
+   * UUID for the Widevine DRM scheme.
+   * <p>
+   * Widevine is supported on Android devices running Android 4.3 (API Level 18) and up.
+   */
+  public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
+
+  /**
+   * UUID for the PlayReady DRM scheme.
+   * <p>
+   * PlayReady is supported on all AndroidTV devices. Note that most other Android devices do not
+   * provide PlayReady support.
+   */
+  public static final UUID PLAYREADY_UUID = new UUID(0x9A04F07998404286L, 0xAB92E65BE0885F95L);
+
+  /**
+   * The type of a message that can be passed to a video {@link Renderer} via
+   * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
+   * should be the target {@link Surface}, or null.
+   */
+  public static final int MSG_SET_SURFACE = 1;
+
+  /**
+   * A type of a message that can be passed to an audio {@link Renderer} via
+   * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
+   * should be a {@link Float} with 0 being silence and 1 being unity gain.
+   */
+  public static final int MSG_SET_VOLUME = 2;
+
+  /**
+   * A type of a message that can be passed to an audio {@link Renderer} via
+   * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
+   * should be one of the integer stream types in {@link C.StreamType}, and will specify the stream
+   * type of the underlying {@link android.media.AudioTrack}. See also
+   * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}. If the stream type
+   * is not set, audio renderers use {@link #STREAM_TYPE_DEFAULT}.
+   * <p>
+   * Note that when the stream type changes, the AudioTrack must be reinitialized, which can
+   * introduce a brief gap in audio output. Note also that tracks in the same audio session must
+   * share the same routing, so a new audio session id will be generated.
+   */
+  public static final int MSG_SET_STREAM_TYPE = 3;
+
+  /**
+   * The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer}
+   * via {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message
+   * object should be one of the integer scaling modes in {@link C.VideoScalingMode}.
+   * <p>
+   * Note that the scaling mode only applies if the {@link Surface} targeted by the renderer is
+   * owned by a {@link android.view.SurfaceView}.
+   */
+  public static final int MSG_SET_SCALING_MODE = 4;
+
+  /**
+   * Applications or extensions may define custom {@code MSG_*} constants greater than or equal to
+   * this value.
+   */
+  public static final int MSG_CUSTOM_BASE = 10000;
+
+  /**
+   * The stereo mode for 360/3D/VR videos.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({
+      Format.NO_VALUE,
+      STEREO_MODE_MONO,
+      STEREO_MODE_TOP_BOTTOM,
+      STEREO_MODE_LEFT_RIGHT,
+      STEREO_MODE_STEREO_MESH
+  })
+  public @interface StereoMode {}
+  /**
+   * Indicates Monoscopic stereo layout, used with 360/3D/VR videos.
+   */
+  public static final int STEREO_MODE_MONO = 0;
+  /**
+   * Indicates Top-Bottom stereo layout, used with 360/3D/VR videos.
+   */
+  public static final int STEREO_MODE_TOP_BOTTOM = 1;
+  /**
+   * Indicates Left-Right stereo layout, used with 360/3D/VR videos.
+   */
+  public static final int STEREO_MODE_LEFT_RIGHT = 2;
+  /**
+   * Indicates a stereo layout where the left and right eyes have separate meshes,
+   * used with 360/3D/VR videos.
+   */
+  public static final int STEREO_MODE_STEREO_MESH = 3;
+
+  /**
+   * Video colorspaces.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({Format.NO_VALUE, COLOR_SPACE_BT709, COLOR_SPACE_BT601, COLOR_SPACE_BT2020})
+  public @interface ColorSpace {}
+  /**
+   * @see MediaFormat#COLOR_STANDARD_BT709
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int COLOR_SPACE_BT709 = 0x01;
+  /**
+   * @see MediaFormat#COLOR_STANDARD_BT601_PAL
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int COLOR_SPACE_BT601 = 0x02;
+  /**
+   * @see MediaFormat#COLOR_STANDARD_BT2020
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int COLOR_SPACE_BT2020 = 0x06;
+
+  /**
+   * Video color transfer characteristics.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({Format.NO_VALUE, COLOR_TRANSFER_SDR, COLOR_TRANSFER_ST2084, COLOR_TRANSFER_HLG})
+  public @interface ColorTransfer {}
+  /**
+   * @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int COLOR_TRANSFER_SDR = 0x03;
+  /**
+   * @see MediaFormat#COLOR_TRANSFER_ST2084
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int COLOR_TRANSFER_ST2084 = 0x06;
+  /**
+   * @see MediaFormat#COLOR_TRANSFER_HLG
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int COLOR_TRANSFER_HLG = 0x07;
+
+  /**
+   * Video color range.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({Format.NO_VALUE, COLOR_RANGE_LIMITED, COLOR_RANGE_FULL})
+  public @interface ColorRange {}
+  /**
+   * @see MediaFormat#COLOR_RANGE_LIMITED
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int COLOR_RANGE_LIMITED = 0x02;
+  /**
+   * @see MediaFormat#COLOR_RANGE_FULL
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int COLOR_RANGE_FULL = 0x01;
+
+  /**
+   * Priority for media playback.
+   *
+   * <p>Larger values indicate higher priorities.
+   */
+  public static final int PRIORITY_PLAYBACK = 0;
+
+  /**
+   * Priority for media downloading.
+   *
+   * <p>Larger values indicate higher priorities.
+   */
+  public static final int PRIORITY_DOWNLOAD = PRIORITY_PLAYBACK - 1000;
+
+  /**
+   * Converts a time in microseconds to the corresponding time in milliseconds, preserving
+   * {@link #TIME_UNSET} values.
+   *
+   * @param timeUs The time in microseconds.
+   * @return The corresponding time in milliseconds.
+   */
+  public static long usToMs(long timeUs) {
+    return timeUs == TIME_UNSET ? TIME_UNSET : (timeUs / 1000);
+  }
+
+  /**
+   * Converts a time in milliseconds to the corresponding time in microseconds, preserving
+   * {@link #TIME_UNSET} values.
+   *
+   * @param timeMs The time in milliseconds.
+   * @return The corresponding time in microseconds.
+   */
+  public static long msToUs(long timeMs) {
+    return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000);
+  }
+
+  /**
+   * Returns a newly generated {@link android.media.AudioTrack} session identifier.
+   */
+  @TargetApi(21)
+  public static int generateAudioSessionIdV21(Context context) {
+    return ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE))
+        .generateAudioSessionId();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/DefaultLoadControl.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DefaultAllocator;
+import com.google.android.exoplayer2.util.PriorityTaskManager;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * The default {@link LoadControl} implementation.
+ */
+public final class DefaultLoadControl implements LoadControl {
+
+  /**
+   * The default minimum duration of media that the player will attempt to ensure is buffered at all
+   * times, in milliseconds.
+   */
+  public static final int DEFAULT_MIN_BUFFER_MS = 15000;
+
+  /**
+   * The default maximum duration of media that the player will attempt to buffer, in milliseconds.
+   */
+  public static final int DEFAULT_MAX_BUFFER_MS = 30000;
+
+  /**
+   * The default duration of media that must be buffered for playback to start or resume following a
+   * user action such as a seek, in milliseconds.
+   */
+  public static final int DEFAULT_BUFFER_FOR_PLAYBACK_MS = 2500;
+
+  /**
+   * The default duration of media that must be buffered for playback to resume after a rebuffer,
+   * in milliseconds. A rebuffer is defined to be caused by buffer depletion rather than a user
+   * action.
+   */
+  public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS  = 5000;
+
+  private static final int ABOVE_HIGH_WATERMARK = 0;
+  private static final int BETWEEN_WATERMARKS = 1;
+  private static final int BELOW_LOW_WATERMARK = 2;
+
+  private final DefaultAllocator allocator;
+
+  private final long minBufferUs;
+  private final long maxBufferUs;
+  private final long bufferForPlaybackUs;
+  private final long bufferForPlaybackAfterRebufferUs;
+  private final PriorityTaskManager priorityTaskManager;
+
+  private int targetBufferSize;
+  private boolean isBuffering;
+
+  /**
+   * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
+   */
+  public DefaultLoadControl() {
+    this(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE));
+  }
+
+  /**
+   * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
+   *
+   * @param allocator The {@link DefaultAllocator} used by the loader.
+   */
+  public DefaultLoadControl(DefaultAllocator allocator) {
+    this(allocator, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS,
+        DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
+  }
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param allocator The {@link DefaultAllocator} used by the loader.
+   * @param minBufferMs The minimum duration of media that the player will attempt to ensure is
+   *     buffered at all times, in milliseconds.
+   * @param maxBufferMs The maximum duration of media that the player will attempt buffer, in
+   *     milliseconds.
+   * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or
+   *     resume following a user action such as a seek, in milliseconds.
+   * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
+   *     playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
+   *     buffer depletion rather than a user action.
+   */
+  public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
+      long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) {
+    this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs,
+        null);
+  }
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param allocator The {@link DefaultAllocator} used by the loader.
+   * @param minBufferMs The minimum duration of media that the player will attempt to ensure is
+   *     buffered at all times, in milliseconds.
+   * @param maxBufferMs The maximum duration of media that the player will attempt buffer, in
+   *     milliseconds.
+   * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or
+   *     resume following a user action such as a seek, in milliseconds.
+   * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
+   *     playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
+   *     buffer depletion rather than a user action.
+   * @param priorityTaskManager If not null, registers itself as a task with priority
+   *     {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining
+   *     periods.
+   */
+  public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
+      long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs,
+      PriorityTaskManager priorityTaskManager) {
+    this.allocator = allocator;
+    minBufferUs = minBufferMs * 1000L;
+    maxBufferUs = maxBufferMs * 1000L;
+    bufferForPlaybackUs = bufferForPlaybackMs * 1000L;
+    bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L;
+    this.priorityTaskManager = priorityTaskManager;
+  }
+
+  @Override
+  public void onPrepared() {
+    reset(false);
+  }
+
+  @Override
+  public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
+      TrackSelectionArray trackSelections) {
+    targetBufferSize = 0;
+    for (int i = 0; i < renderers.length; i++) {
+      if (trackSelections.get(i) != null) {
+        targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType());
+      }
+    }
+    allocator.setTargetBufferSize(targetBufferSize);
+  }
+
+  @Override
+  public void onStopped() {
+    reset(true);
+  }
+
+  @Override
+  public void onReleased() {
+    reset(true);
+  }
+
+  @Override
+  public Allocator getAllocator() {
+    return allocator;
+  }
+
+  @Override
+  public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) {
+    long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs;
+    return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs;
+  }
+
+  @Override
+  public boolean shouldContinueLoading(long bufferedDurationUs) {
+    int bufferTimeState = getBufferTimeState(bufferedDurationUs);
+    boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
+    boolean wasBuffering = isBuffering;
+    isBuffering = bufferTimeState == BELOW_LOW_WATERMARK
+        || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached);
+    if (priorityTaskManager != null && isBuffering != wasBuffering) {
+      if (isBuffering) {
+        priorityTaskManager.add(C.PRIORITY_PLAYBACK);
+      } else {
+        priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
+      }
+    }
+    return isBuffering;
+  }
+
+  private int getBufferTimeState(long bufferedDurationUs) {
+    return bufferedDurationUs > maxBufferUs ? ABOVE_HIGH_WATERMARK
+        : (bufferedDurationUs < minBufferUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS);
+  }
+
+  private void reset(boolean resetAllocator) {
+    targetBufferSize = 0;
+    if (priorityTaskManager != null && isBuffering) {
+      priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
+    }
+    isBuffering = false;
+    if (resetAllocator) {
+      allocator.reset();
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.IntDef;
+import android.util.Log;
+import com.google.android.exoplayer2.audio.AudioCapabilities;
+import com.google.android.exoplayer2.audio.AudioProcessor;
+import com.google.android.exoplayer2.audio.AudioRendererEventListener;
+import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
+import com.google.android.exoplayer2.metadata.MetadataRenderer;
+import com.google.android.exoplayer2.text.TextRenderer;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
+import com.google.android.exoplayer2.video.VideoRendererEventListener;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+
+/**
+ * Default {@link RenderersFactory} implementation.
+ */
+public class DefaultRenderersFactory implements RenderersFactory {
+
+  /**
+   * The default maximum duration for which a video renderer can attempt to seamlessly join an
+   * ongoing playback.
+   */
+  public static final long DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS = 5000;
+
+  /**
+   * Modes for using extension renderers.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON,
+      EXTENSION_RENDERER_MODE_PREFER})
+  public @interface ExtensionRendererMode {}
+  /**
+   * Do not allow use of extension renderers.
+   */
+  public static final int EXTENSION_RENDERER_MODE_OFF = 0;
+  /**
+   * Allow use of extension renderers. Extension renderers are indexed after core renderers of the
+   * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore
+   * prefer to use a core renderer to an extension renderer in the case that both are able to play
+   * a given track.
+   */
+  public static final int EXTENSION_RENDERER_MODE_ON = 1;
+  /**
+   * Allow use of extension renderers. Extension renderers are indexed before core renderers of the
+   * same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore
+   * prefer to use an extension renderer to a core renderer in the case that both are able to play
+   * a given track.
+   */
+  public static final int EXTENSION_RENDERER_MODE_PREFER = 2;
+
+  private static final String TAG = "DefaultRenderersFactory";
+
+  protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;
+
+  private final Context context;
+  private final DrmSessionManager<FrameworkMediaCrypto> drmSessionManager;
+  private final @ExtensionRendererMode int extensionRendererMode;
+  private final long allowedVideoJoiningTimeMs;
+
+  /**
+   * @param context A {@link Context}.
+   */
+  public DefaultRenderersFactory(Context context) {
+    this(context, null);
+  }
+
+  /**
+   * @param context A {@link Context}.
+   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected
+   *     playbacks are not required.
+   */
+  public DefaultRenderersFactory(Context context,
+      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
+    this(context, drmSessionManager, EXTENSION_RENDERER_MODE_OFF);
+  }
+
+  /**
+   * @param context A {@link Context}.
+   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected
+   *     playbacks are not required..
+   * @param extensionRendererMode The extension renderer mode, which determines if and how
+   *     available extension renderers are used. Note that extensions must be included in the
+   *     application build for them to be considered available.
+   */
+  public DefaultRenderersFactory(Context context,
+      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      @ExtensionRendererMode int extensionRendererMode) {
+    this(context, drmSessionManager, extensionRendererMode,
+        DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
+  }
+
+  /**
+   * @param context A {@link Context}.
+   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if DRM protected
+   *     playbacks are not required..
+   * @param extensionRendererMode The extension renderer mode, which determines if and how
+   *     available extension renderers are used. Note that extensions must be included in the
+   *     application build for them to be considered available.
+   * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt
+   *     to seamlessly join an ongoing playback.
+   */
+  public DefaultRenderersFactory(Context context,
+      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) {
+    this.context = context;
+    this.drmSessionManager = drmSessionManager;
+    this.extensionRendererMode = extensionRendererMode;
+    this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs;
+  }
+
+  @Override
+  public Renderer[] createRenderers(Handler eventHandler,
+      VideoRendererEventListener videoRendererEventListener,
+      AudioRendererEventListener audioRendererEventListener,
+      TextRenderer.Output textRendererOutput, MetadataRenderer.Output metadataRendererOutput) {
+    ArrayList<Renderer> renderersList = new ArrayList<>();
+    buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs,
+        eventHandler, videoRendererEventListener, extensionRendererMode, renderersList);
+    buildAudioRenderers(context, drmSessionManager, buildAudioProcessors(),
+        eventHandler, audioRendererEventListener, extensionRendererMode, renderersList);
+    buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(),
+        extensionRendererMode, renderersList);
+    buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(),
+        extensionRendererMode, renderersList);
+    buildMiscellaneousRenderers(context, eventHandler, extensionRendererMode, renderersList);
+    return renderersList.toArray(new Renderer[renderersList.size()]);
+  }
+
+  /**
+   * Builds video renderers for use by the player.
+   *
+   * @param context The {@link Context} associated with the player.
+   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player
+   *     will not be used for DRM protected playbacks.
+   * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video
+   *     renderers can attempt to seamlessly join an ongoing playback.
+   * @param eventHandler A handler associated with the main thread's looper.
+   * @param eventListener An event listener.
+   * @param extensionRendererMode The extension renderer mode.
+   * @param out An array to which the built renderers should be appended.
+   */
+  protected void buildVideoRenderers(Context context,
+      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, long allowedVideoJoiningTimeMs,
+      Handler eventHandler, VideoRendererEventListener eventListener,
+      @ExtensionRendererMode int extensionRendererMode, ArrayList<Renderer> out) {
+    out.add(new MediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT,
+        allowedVideoJoiningTimeMs, drmSessionManager, false, eventHandler, eventListener,
+        MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
+
+    if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
+      return;
+    }
+    int extensionRendererIndex = out.size();
+    if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {
+      extensionRendererIndex--;
+    }
+
+    try {
+      Class<?> clazz =
+          Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer");
+      Constructor<?> constructor = clazz.getConstructor(boolean.class, long.class, Handler.class,
+          VideoRendererEventListener.class, int.class);
+      Renderer renderer = (Renderer) constructor.newInstance(true, allowedVideoJoiningTimeMs,
+          eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
+      out.add(extensionRendererIndex++, renderer);
+      Log.i(TAG, "Loaded LibvpxVideoRenderer.");
+    } catch (ClassNotFoundException e) {
+      // Expected if the app was built without the extension.
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * Builds audio renderers for use by the player.
+   *
+   * @param context The {@link Context} associated with the player.
+   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player
+   *     will not be used for DRM protected playbacks.
+   * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio
+   *     buffers before output. May be empty.
+   * @param eventHandler A handler to use when invoking event listeners and outputs.
+   * @param eventListener An event listener.
+   * @param extensionRendererMode The extension renderer mode.
+   * @param out An array to which the built renderers should be appended.
+   */
+  protected void buildAudioRenderers(Context context,
+      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      AudioProcessor[] audioProcessors, Handler eventHandler,
+      AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode,
+      ArrayList<Renderer> out) {
+    out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true,
+        eventHandler, eventListener, AudioCapabilities.getCapabilities(context), audioProcessors));
+
+    if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
+      return;
+    }
+    int extensionRendererIndex = out.size();
+    if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {
+      extensionRendererIndex--;
+    }
+
+    try {
+      Class<?> clazz =
+          Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer");
+      Constructor<?> constructor = clazz.getConstructor(Handler.class,
+          AudioRendererEventListener.class, AudioProcessor[].class);
+      Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener,
+          audioProcessors);
+      out.add(extensionRendererIndex++, renderer);
+      Log.i(TAG, "Loaded LibopusAudioRenderer.");
+    } catch (ClassNotFoundException e) {
+      // Expected if the app was built without the extension.
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+
+    try {
+      Class<?> clazz =
+          Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer");
+      Constructor<?> constructor = clazz.getConstructor(Handler.class,
+          AudioRendererEventListener.class, AudioProcessor[].class);
+      Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener,
+          audioProcessors);
+      out.add(extensionRendererIndex++, renderer);
+      Log.i(TAG, "Loaded LibflacAudioRenderer.");
+    } catch (ClassNotFoundException e) {
+      // Expected if the app was built without the extension.
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+
+    try {
+      Class<?> clazz =
+          Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer");
+      Constructor<?> constructor = clazz.getConstructor(Handler.class,
+          AudioRendererEventListener.class, AudioProcessor[].class);
+      Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener,
+          audioProcessors);
+      out.add(extensionRendererIndex++, renderer);
+      Log.i(TAG, "Loaded FfmpegAudioRenderer.");
+    } catch (ClassNotFoundException e) {
+      // Expected if the app was built without the extension.
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * Builds text renderers for use by the player.
+   *
+   * @param context The {@link Context} associated with the player.
+   * @param output An output for the renderers.
+   * @param outputLooper The looper associated with the thread on which the output should be
+   *     called.
+   * @param extensionRendererMode The extension renderer mode.
+   * @param out An array to which the built renderers should be appended.
+   */
+  protected void buildTextRenderers(Context context, TextRenderer.Output output,
+      Looper outputLooper, @ExtensionRendererMode int extensionRendererMode,
+      ArrayList<Renderer> out) {
+    out.add(new TextRenderer(output, outputLooper));
+  }
+
+  /**
+   * Builds metadata renderers for use by the player.
+   *
+   * @param context The {@link Context} associated with the player.
+   * @param output An output for the renderers.
+   * @param outputLooper The looper associated with the thread on which the output should be
+   *     called.
+   * @param extensionRendererMode The extension renderer mode.
+   * @param out An array to which the built renderers should be appended.
+   */
+  protected void buildMetadataRenderers(Context context, MetadataRenderer.Output output,
+      Looper outputLooper, @ExtensionRendererMode int extensionRendererMode,
+      ArrayList<Renderer> out) {
+    out.add(new MetadataRenderer(output, outputLooper));
+  }
+
+  /**
+   * Builds any miscellaneous renderers used by the player.
+   *
+   * @param context The {@link Context} associated with the player.
+   * @param eventHandler A handler to use when invoking event listeners and outputs.
+   * @param extensionRendererMode The extension renderer mode.
+   * @param out An array to which the built renderers should be appended.
+   */
+  protected void buildMiscellaneousRenderers(Context context, Handler eventHandler,
+      @ExtensionRendererMode int extensionRendererMode, ArrayList<Renderer> out) {
+    // Do nothing.
+  }
+
+  /**
+   * Builds an array of {@link AudioProcessor}s that will process PCM audio before output.
+   */
+  protected AudioProcessor[] buildAudioProcessors() {
+    return new AudioProcessor[0];
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/ExoPlaybackException.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Thrown when a non-recoverable playback failure occurs.
+ */
+public final class ExoPlaybackException extends Exception {
+
+  /**
+   * The type of source that produced the error.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED})
+  public @interface Type {}
+  /**
+   * The error occurred loading data from a {@link MediaSource}.
+   * <p>
+   * Call {@link #getSourceException()} to retrieve the underlying cause.
+   */
+  public static final int TYPE_SOURCE = 0;
+  /**
+   * The error occurred in a {@link Renderer}.
+   * <p>
+   * Call {@link #getRendererException()} to retrieve the underlying cause.
+   */
+  public static final int TYPE_RENDERER = 1;
+  /**
+   * The error was an unexpected {@link RuntimeException}.
+   * <p>
+   * Call {@link #getUnexpectedException()} to retrieve the underlying cause.
+   */
+  public static final int TYPE_UNEXPECTED = 2;
+
+  /**
+   * The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and
+   * {@link #TYPE_UNEXPECTED}.
+   */
+  @Type public final int type;
+
+  /**
+   * If {@link #type} is {@link #TYPE_RENDERER}, this is the index of the renderer.
+   */
+  public final int rendererIndex;
+
+  /**
+   * Creates an instance of type {@link #TYPE_RENDERER}.
+   *
+   * @param cause The cause of the failure.
+   * @param rendererIndex The index of the renderer in which the failure occurred.
+   * @return The created instance.
+   */
+  public static ExoPlaybackException createForRenderer(Exception cause, int rendererIndex) {
+    return new ExoPlaybackException(TYPE_RENDERER, null, cause, rendererIndex);
+  }
+
+  /**
+   * Creates an instance of type {@link #TYPE_SOURCE}.
+   *
+   * @param cause The cause of the failure.
+   * @return The created instance.
+   */
+  public static ExoPlaybackException createForSource(IOException cause) {
+    return new ExoPlaybackException(TYPE_SOURCE, null, cause, C.INDEX_UNSET);
+  }
+
+  /**
+   * Creates an instance of type {@link #TYPE_UNEXPECTED}.
+   *
+   * @param cause The cause of the failure.
+   * @return The created instance.
+   */
+  /* package */ static ExoPlaybackException createForUnexpected(RuntimeException cause) {
+    return new ExoPlaybackException(TYPE_UNEXPECTED, null, cause, C.INDEX_UNSET);
+  }
+
+  private ExoPlaybackException(@Type int type, String message, Throwable cause,
+      int rendererIndex) {
+    super(message, cause);
+    this.type = type;
+    this.rendererIndex = rendererIndex;
+  }
+
+  /**
+   * Retrieves the underlying error when {@link #type} is {@link #TYPE_SOURCE}.
+   *
+   * @throws IllegalStateException If {@link #type} is not {@link #TYPE_SOURCE}.
+   */
+  public IOException getSourceException() {
+    Assertions.checkState(type == TYPE_SOURCE);
+    return (IOException) getCause();
+  }
+
+  /**
+   * Retrieves the underlying error when {@link #type} is {@link #TYPE_RENDERER}.
+   *
+   * @throws IllegalStateException If {@link #type} is not {@link #TYPE_RENDERER}.
+   */
+  public Exception getRendererException() {
+    Assertions.checkState(type == TYPE_RENDERER);
+    return (Exception) getCause();
+  }
+
+  /**
+   * Retrieves the underlying error when {@link #type} is {@link #TYPE_UNEXPECTED}.
+   *
+   * @throws IllegalStateException If {@link #type} is not {@link #TYPE_UNEXPECTED}.
+   */
+  public RuntimeException getUnexpectedException() {
+    Assertions.checkState(type == TYPE_UNEXPECTED);
+    return (RuntimeException) getCause();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/ExoPlayer.java
@@ -0,0 +1,495 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
+import com.google.android.exoplayer2.metadata.MetadataRenderer;
+import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
+import com.google.android.exoplayer2.source.ExtractorMediaSource;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MergingMediaSource;
+import com.google.android.exoplayer2.source.SingleSampleMediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.text.TextRenderer;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
+
+/**
+ * An extensible media player exposing traditional high-level media player functionality, such as
+ * the ability to buffer media, play, pause and seek. Instances can be obtained from
+ * {@link ExoPlayerFactory}.
+ *
+ * <h3>Player composition</h3>
+ * <p>ExoPlayer is designed to make few assumptions about (and hence impose few restrictions on) the
+ * type of the media being played, how and where it is stored, and how it is rendered. Rather than
+ * implementing the loading and rendering of media directly, ExoPlayer implementations delegate this
+ * work to components that are injected when a player is created or when it's prepared for playback.
+ * Components common to all ExoPlayer implementations are:
+ * <ul>
+ *   <li>A <b>{@link MediaSource}</b> that defines the media to be played, loads the media, and from
+ *   which the loaded media can be read. A MediaSource is injected via {@link #prepare} at the start
+ *   of playback. The library modules provide default implementations for regular media files
+ *   ({@link ExtractorMediaSource}), DASH (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS
+ *   (HlsMediaSource), implementations for merging ({@link MergingMediaSource}) and concatenating
+ *   ({@link ConcatenatingMediaSource}) other MediaSources, and an implementation for loading single
+ *   samples ({@link SingleSampleMediaSource}) most often used for side-loaded subtitle and closed
+ *   caption files.</li>
+ *   <li><b>{@link Renderer}</b>s that render individual components of the media. The library
+ *   provides default implementations for common media types ({@link MediaCodecVideoRenderer},
+ *   {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A Renderer
+ *   consumes media of its corresponding type from the MediaSource being played. Renderers are
+ *   injected when the player is created.</li>
+ *   <li>A <b>{@link TrackSelector}</b> that selects tracks provided by the MediaSource to be
+ *   consumed by each of the available Renderers. The library provides a default implementation
+ *   ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected when
+ *   the player is created.</li>
+ *   <li>A <b>{@link LoadControl}</b> that controls when the MediaSource buffers more media, and how
+ *   much media is buffered. The library provides a default implementation
+ *   ({@link DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the
+ *   player is created.</li>
+ * </ul>
+ * <p>An ExoPlayer can be built using the default components provided by the library, but may also
+ * be built using custom implementations if non-standard behaviors are required. For example a
+ * custom LoadControl could be injected to change the player's buffering strategy, or a custom
+ * Renderer could be injected to use a video codec not supported natively by Android.
+ *
+ * <p>The concept of injecting components that implement pieces of player functionality is present
+ * throughout the library. The default component implementations listed above delegate work to
+ * further injected components. This allows many sub-components to be individually replaced with
+ * custom implementations. For example the default MediaSource implementations require one or more
+ * {@link DataSource} factories to be injected via their constructors. By providing a custom factory
+ * it's possible to load data from a non-standard source or through a different network stack.
+ *
+ * <h3>Threading model</h3>
+ * <p>The figure below shows ExoPlayer's threading model.</p>
+ * <p align="center">
+ *   <img src="doc-files/exoplayer-threading-model.svg" alt="ExoPlayer's threading model">
+ * </p>
+ *
+ * <ul>
+ * <li>It is recommended that ExoPlayer instances are created and accessed from a single application
+ * thread. The application's main thread is ideal. Accessing an instance from multiple threads is
+ * discouraged, however if an application does wish to do this then it may do so provided that it
+ * ensures accesses are synchronized.</li>
+ * <li>Registered listeners are called on the thread that created the ExoPlayer instance.</li>
+ * <li>An internal playback thread is responsible for playback. Injected player components such as
+ * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
+ * thread.</li>
+ * <li>When the application performs an operation on the player, for example a seek, a message is
+ * delivered to the internal playback thread via a message queue. The internal playback thread
+ * consumes messages from the queue and performs the corresponding operations. Similarly, when a
+ * playback event occurs on the internal playback thread, a message is delivered to the application
+ * thread via a second message queue. The application thread consumes messages from the queue,
+ * updating the application visible state and calling corresponding listener methods.</li>
+ * <li>Injected player components may use additional background threads. For example a MediaSource
+ * may use a background thread to load data. These are implementation specific.</li>
+ * </ul>
+ */
+public interface ExoPlayer {
+
+  /**
+   * Listener of changes in player state.
+   */
+  interface EventListener {
+
+    /**
+     * Called when the timeline and/or manifest has been refreshed.
+     * <p>
+     * Note that if the timeline has changed then a position discontinuity may also have occurred.
+     * For example the current period index may have changed as a result of periods being added or
+     * removed from the timeline. The will <em>not</em> be reported via a separate call to
+     * {@link #onPositionDiscontinuity()}.
+     *
+     * @param timeline The latest timeline. Never null, but may be empty.
+     * @param manifest The latest manifest. May be null.
+     */
+    void onTimelineChanged(Timeline timeline, Object manifest);
+
+    /**
+     * Called when the available or selected tracks change.
+     *
+     * @param trackGroups The available tracks. Never null, but may be of length zero.
+     * @param trackSelections The track selections for each {@link Renderer}. Never null and always
+     *     of length {@link #getRendererCount()}, but may contain null elements.
+     */
+    void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections);
+
+    /**
+     * Called when the player starts or stops loading the source.
+     *
+     * @param isLoading Whether the source is currently being loaded.
+     */
+    void onLoadingChanged(boolean isLoading);
+
+    /**
+     * Called when the value returned from either {@link #getPlayWhenReady()} or
+     * {@link #getPlaybackState()} changes.
+     *
+     * @param playWhenReady Whether playback will proceed when ready.
+     * @param playbackState One of the {@code STATE} constants defined in the {@link ExoPlayer}
+     *     interface.
+     */
+    void onPlayerStateChanged(boolean playWhenReady, int playbackState);
+
+    /**
+     * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE}
+     * immediately after this method is called. The player instance can still be used, and
+     * {@link #release()} must still be called on the player should it no longer be required.
+     *
+     * @param error The error.
+     */
+    void onPlayerError(ExoPlaybackException error);
+
+    /**
+     * Called when a position discontinuity occurs without a change to the timeline. A position
+     * discontinuity occurs when the current window or period index changes (as a result of playback
+     * transitioning from one period in the timeline to the next), or when the playback position
+     * jumps within the period currently being played (as a result of a seek being performed, or
+     * when the source introduces a discontinuity internally).
+     * <p>
+     * When a position discontinuity occurs as a result of a change to the timeline this method is
+     * <em>not</em> called. {@link #onTimelineChanged(Timeline, Object)} is called in this case.
+     */
+    void onPositionDiscontinuity();
+
+    /**
+     * Called when the current playback parameters change. The playback parameters may change due to
+     * a call to {@link ExoPlayer#setPlaybackParameters(PlaybackParameters)}, or the player itself
+     * may change them (for example, if audio playback switches to passthrough mode, where speed
+     * adjustment is no longer possible).
+     *
+     * @param playbackParameters The playback parameters.
+     */
+    void onPlaybackParametersChanged(PlaybackParameters playbackParameters);
+
+  }
+
+  /**
+   * A component of an {@link ExoPlayer} that can receive messages on the playback thread.
+   * <p>
+   * Messages can be delivered to a component via {@link #sendMessages} and
+   * {@link #blockingSendMessages}.
+   */
+  interface ExoPlayerComponent {
+
+    /**
+     * Handles a message delivered to the component. Called on the playback thread.
+     *
+     * @param messageType The message type.
+     * @param message The message.
+     * @throws ExoPlaybackException If an error occurred whilst handling the message.
+     */
+    void handleMessage(int messageType, Object message) throws ExoPlaybackException;
+
+  }
+
+  /**
+   * Defines a message and a target {@link ExoPlayerComponent} to receive it.
+   */
+  final class ExoPlayerMessage {
+
+    /**
+     * The target to receive the message.
+     */
+    public final ExoPlayerComponent target;
+    /**
+     * The type of the message.
+     */
+    public final int messageType;
+    /**
+     * The message.
+     */
+    public final Object message;
+
+    /**
+     * @param target The target of the message.
+     * @param messageType The message type.
+     * @param message The message.
+     */
+    public ExoPlayerMessage(ExoPlayerComponent target, int messageType, Object message) {
+      this.target = target;
+      this.messageType = messageType;
+      this.message = message;
+    }
+
+  }
+
+  /**
+   * The player does not have a source to play, so it is neither buffering nor ready to play.
+   */
+  int STATE_IDLE = 1;
+  /**
+   * The player not able to immediately play from the current position. The cause is
+   * {@link Renderer} specific, but this state typically occurs when more data needs to be
+   * loaded to be ready to play, or more data needs to be buffered for playback to resume.
+   */
+  int STATE_BUFFERING = 2;
+  /**
+   * The player is able to immediately play from the current position. The player will be playing if
+   * {@link #getPlayWhenReady()} returns true, and paused otherwise.
+   */
+  int STATE_READY = 3;
+  /**
+   * The player has finished playing the media.
+   */
+  int STATE_ENDED = 4;
+
+  /**
+   * Register a listener to receive events from the player. The listener's methods will be called on
+   * the thread that was used to construct the player.
+   *
+   * @param listener The listener to register.
+   */
+  void addListener(EventListener listener);
+
+  /**
+   * Unregister a listener. The listener will no longer receive events from the player.
+   *
+   * @param listener The listener to unregister.
+   */
+  void removeListener(EventListener listener);
+
+  /**
+   * Returns the current state of the player.
+   *
+   * @return One of the {@code STATE} constants defined in this interface.
+   */
+  int getPlaybackState();
+
+  /**
+   * Prepares the player to play the provided {@link MediaSource}. Equivalent to
+   * {@code prepare(mediaSource, true, true)}.
+   */
+  void prepare(MediaSource mediaSource);
+
+  /**
+   * Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback
+   * position the default position in the first {@link Timeline.Window}.
+   *
+   * @param mediaSource The {@link MediaSource} to play.
+   * @param resetPosition Whether the playback position should be reset to the default position in
+   *     the first {@link Timeline.Window}. If false, playback will start from the position defined
+   *     by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}.
+   * @param resetState Whether the timeline, manifest, tracks and track selections should be reset.
+   *     Should be true unless the player is being prepared to play the same media as it was playing
+   *     previously (e.g. if playback failed and is being retried).
+   */
+  void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState);
+
+  /**
+   * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
+   * <p>
+   * If the player is already in the ready state then this method can be used to pause and resume
+   * playback.
+   *
+   * @param playWhenReady Whether playback should proceed when ready.
+   */
+  void setPlayWhenReady(boolean playWhenReady);
+
+  /**
+   * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
+   *
+   * @return Whether playback will proceed when ready.
+   */
+  boolean getPlayWhenReady();
+
+  /**
+   * Whether the player is currently loading the source.
+   *
+   * @return Whether the player is currently loading the source.
+   */
+  boolean isLoading();
+
+  /**
+   * Seeks to the default position associated with the current window. The position can depend on
+   * the type of source passed to {@link #prepare(MediaSource)}. For live streams it will typically
+   * be the live edge of the window. For other streams it will typically be the start of the window.
+   */
+  void seekToDefaultPosition();
+
+  /**
+   * Seeks to the default position associated with the specified window. The position can depend on
+   * the type of source passed to {@link #prepare(MediaSource)}. For live streams it will typically
+   * be the live edge of the window. For other streams it will typically be the start of the window.
+   *
+   * @param windowIndex The index of the window whose associated default position should be seeked
+   *     to.
+   */
+  void seekToDefaultPosition(int windowIndex);
+
+  /**
+   * Seeks to a position specified in milliseconds in the current window.
+   *
+   * @param positionMs The seek position in the current window, or {@link C#TIME_UNSET} to seek to
+   *     the window's default position.
+   */
+  void seekTo(long positionMs);
+
+  /**
+   * Seeks to a position specified in milliseconds in the specified window.
+   *
+   * @param windowIndex The index of the window.
+   * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to
+   *     the window's default position.
+   */
+  void seekTo(int windowIndex, long positionMs);
+
+  /**
+   * Attempts to set the playback parameters. Passing {@code null} sets the parameters to the
+   * default, {@link PlaybackParameters#DEFAULT}, which means there is no speed or pitch adjustment.
+   * <p>
+   * Playback parameters changes may cause the player to buffer.
+   * {@link EventListener#onPlaybackParametersChanged(PlaybackParameters)} will be called whenever
+   * the currently active playback parameters change. When that listener is called, the parameters
+   * passed to it may not match {@code playbackParameters}. For example, the chosen speed or pitch
+   * may be out of range, in which case they are constrained to a set of permitted values. If it is
+   * not possible to change the playback parameters, the listener will not be invoked.
+   *
+   * @param playbackParameters The playback parameters, or {@code null} to use the defaults.
+   */
+  void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters);
+
+  /**
+   * Returns the currently active playback parameters.
+   *
+   * @see EventListener#onPlaybackParametersChanged(PlaybackParameters)
+   */
+  PlaybackParameters getPlaybackParameters();
+
+  /**
+   * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention
+   * is to pause playback.
+   * <p>
+   * Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The
+   * player instance can still be used, and {@link #release()} must still be called on the player if
+   * it's no longer required.
+   * <p>
+   * Calling this method does not reset the playback position.
+   */
+  void stop();
+
+  /**
+   * Releases the player. This method must be called when the player is no longer required. The
+   * player must not be used after calling this method.
+   */
+  void release();
+
+  /**
+   * Sends messages to their target components. The messages are delivered on the playback thread.
+   * If a component throws an {@link ExoPlaybackException} then it is propagated out of the player
+   * as an error.
+   *
+   * @param messages The messages to be sent.
+   */
+  void sendMessages(ExoPlayerMessage... messages);
+
+  /**
+   * Variant of {@link #sendMessages(ExoPlayerMessage...)} that blocks until after the messages have
+   * been delivered.
+   *
+   * @param messages The messages to be sent.
+   */
+  void blockingSendMessages(ExoPlayerMessage... messages);
+
+  /**
+   * Returns the number of renderers.
+   */
+  int getRendererCount();
+
+  /**
+   * Returns the track type that the renderer at a given index handles.
+   *
+   * @see Renderer#getTrackType()
+   * @param index The index of the renderer.
+   * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
+   */
+  int getRendererType(int index);
+
+  /**
+   * Returns the available track groups.
+   */
+  TrackGroupArray getCurrentTrackGroups();
+
+  /**
+   * Returns the current track selections for each renderer.
+   */
+  TrackSelectionArray getCurrentTrackSelections();
+
+  /**
+   * Returns the current manifest. The type depends on the {@link MediaSource} passed to
+   * {@link #prepare}. May be null.
+   */
+  Object getCurrentManifest();
+
+  /**
+   * Returns the current {@link Timeline}. Never null, but may be empty.
+   */
+  Timeline getCurrentTimeline();
+
+  /**
+   * Returns the index of the period currently being played.
+   */
+  int getCurrentPeriodIndex();
+
+  /**
+   * Returns the index of the window currently being played.
+   */
+  int getCurrentWindowIndex();
+
+  /**
+   * Returns the duration of the current window in milliseconds, or {@link C#TIME_UNSET} if the
+   * duration is not known.
+   */
+  long getDuration();
+
+  /**
+   * Returns the playback position in the current window, in milliseconds.
+   */
+  long getCurrentPosition();
+
+  /**
+   * Returns an estimate of the position in the current window up to which data is buffered, in
+   * milliseconds.
+   */
+  long getBufferedPosition();
+
+  /**
+   * Returns an estimate of the percentage in the current window up to which data is buffered, or 0
+   * if no estimate is available.
+   */
+  int getBufferedPercentage();
+
+  /**
+   * Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is
+   * empty.
+   *
+   * @see Timeline.Window#isDynamic
+   */
+  boolean isCurrentWindowDynamic();
+
+  /**
+   * Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is
+   * empty.
+   *
+   * @see Timeline.Window#isSeekable
+   */
+  boolean isCurrentWindowSeekable();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/ExoPlayerFactory.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.content.Context;
+import android.os.Looper;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+
+/**
+ * A factory for {@link ExoPlayer} instances.
+ */
+public final class ExoPlayerFactory {
+
+  private ExoPlayerFactory() {}
+
+  /**
+   * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+   * {@link Looper}.
+   *
+   * @param context A {@link Context}.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   * @param loadControl The {@link LoadControl} that will be used by the instance.
+   * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
+   */
+  @Deprecated
+  public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
+      LoadControl loadControl) {
+    RenderersFactory renderersFactory = new DefaultRenderersFactory(context);
+    return newSimpleInstance(renderersFactory, trackSelector, loadControl);
+  }
+
+  /**
+   * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+   * {@link Looper}. Available extension renderers are not used.
+   *
+   * @param context A {@link Context}.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   * @param loadControl The {@link LoadControl} that will be used by the instance.
+   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+   *     will not be used for DRM protected playbacks.
+   * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
+   */
+  @Deprecated
+  public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
+      LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
+    RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager);
+    return newSimpleInstance(renderersFactory, trackSelector, loadControl);
+  }
+
+  /**
+   * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+   * {@link Looper}.
+   *
+   * @param context A {@link Context}.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   * @param loadControl The {@link LoadControl} that will be used by the instance.
+   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+   *     will not be used for DRM protected playbacks.
+   * @param extensionRendererMode The extension renderer mode, which determines if and how available
+   *     extension renderers are used. Note that extensions must be included in the application
+   *     build for them to be considered available.
+   * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
+   */
+  @Deprecated
+  public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
+      LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) {
+    RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager,
+        extensionRendererMode);
+    return newSimpleInstance(renderersFactory, trackSelector, loadControl);
+  }
+
+  /**
+   * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+   * {@link Looper}.
+   *
+   * @param context A {@link Context}.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   * @param loadControl The {@link LoadControl} that will be used by the instance.
+   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+   *     will not be used for DRM protected playbacks.
+   * @param extensionRendererMode The extension renderer mode, which determines if and how available
+   *     extension renderers are used. Note that extensions must be included in the application
+   *     build for them to be considered available.
+   * @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to
+   *     seamlessly join an ongoing playback.
+   * @deprecated Use {@link #newSimpleInstance(RenderersFactory, TrackSelector, LoadControl)}.
+   */
+  @Deprecated
+  public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
+      LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode,
+      long allowedVideoJoiningTimeMs) {
+    RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager,
+        extensionRendererMode, allowedVideoJoiningTimeMs);
+    return newSimpleInstance(renderersFactory, trackSelector, loadControl);
+  }
+
+  /**
+   * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+   * {@link Looper}.
+   *
+   * @param context A {@link Context}.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   */
+  public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector) {
+    return newSimpleInstance(new DefaultRenderersFactory(context), trackSelector);
+  }
+
+  /**
+   * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+   * {@link Looper}.
+   *
+   * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   */
+  public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory,
+      TrackSelector trackSelector) {
+    return newSimpleInstance(renderersFactory, trackSelector, new DefaultLoadControl());
+  }
+
+  /**
+   * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+   * {@link Looper}.
+   *
+   * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   * @param loadControl The {@link LoadControl} that will be used by the instance.
+   */
+  public static SimpleExoPlayer newSimpleInstance(RenderersFactory renderersFactory,
+      TrackSelector trackSelector, LoadControl loadControl) {
+    return new SimpleExoPlayer(renderersFactory, trackSelector, loadControl);
+  }
+
+  /**
+   * Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated
+   * {@link Looper}.
+   *
+   * @param renderers The {@link Renderer}s that will be used by the instance.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   */
+  public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector) {
+    return newInstance(renderers, trackSelector, new DefaultLoadControl());
+  }
+
+  /**
+   * Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated
+   * {@link Looper}.
+   *
+   * @param renderers The {@link Renderer}s that will be used by the instance.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   * @param loadControl The {@link LoadControl} that will be used by the instance.
+   */
+  public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector,
+      LoadControl loadControl) {
+    return new ExoPlayerImpl(renderers, trackSelector, loadControl);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.support.annotation.Nullable;
+import android.util.Log;
+import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo;
+import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayerFactory}.
+ */
+/* package */ final class ExoPlayerImpl implements ExoPlayer {
+
+  private static final String TAG = "ExoPlayerImpl";
+
+  private final Renderer[] renderers;
+  private final TrackSelector trackSelector;
+  private final TrackSelectionArray emptyTrackSelections;
+  private final Handler eventHandler;
+  private final ExoPlayerImplInternal internalPlayer;
+  private final CopyOnWriteArraySet<EventListener> listeners;
+  private final Timeline.Window window;
+  private final Timeline.Period period;
+
+  private boolean tracksSelected;
+  private boolean playWhenReady;
+  private int playbackState;
+  private int pendingSeekAcks;
+  private int pendingPrepareAcks;
+  private boolean isLoading;
+  private Timeline timeline;
+  private Object manifest;
+  private TrackGroupArray trackGroups;
+  private TrackSelectionArray trackSelections;
+  private PlaybackParameters playbackParameters;
+
+  // Playback information when there is no pending seek/set source operation.
+  private PlaybackInfo playbackInfo;
+
+  // Playback information when there is a pending seek/set source operation.
+  private int maskingWindowIndex;
+  private int maskingPeriodIndex;
+  private long maskingWindowPositionMs;
+
+  /**
+   * Constructs an instance. Must be called from a thread that has an associated {@link Looper}.
+   *
+   * @param renderers The {@link Renderer}s that will be used by the instance.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   * @param loadControl The {@link LoadControl} that will be used by the instance.
+   */
+  @SuppressLint("HandlerLeak")
+  public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) {
+    Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION_SLASHY + " [" + Util.DEVICE_DEBUG_INFO + "]");
+    Assertions.checkState(renderers.length > 0);
+    this.renderers = Assertions.checkNotNull(renderers);
+    this.trackSelector = Assertions.checkNotNull(trackSelector);
+    this.playWhenReady = false;
+    this.playbackState = STATE_IDLE;
+    this.listeners = new CopyOnWriteArraySet<>();
+    emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]);
+    timeline = Timeline.EMPTY;
+    window = new Timeline.Window();
+    period = new Timeline.Period();
+    trackGroups = TrackGroupArray.EMPTY;
+    trackSelections = emptyTrackSelections;
+    playbackParameters = PlaybackParameters.DEFAULT;
+    eventHandler = new Handler() {
+      @Override
+      public void handleMessage(Message msg) {
+        ExoPlayerImpl.this.handleEvent(msg);
+      }
+    };
+    playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0);
+    internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady,
+        eventHandler, playbackInfo, this);
+  }
+
+  @Override
+  public void addListener(EventListener listener) {
+    listeners.add(listener);
+  }
+
+  @Override
+  public void removeListener(EventListener listener) {
+    listeners.remove(listener);
+  }
+
+  @Override
+  public int getPlaybackState() {
+    return playbackState;
+  }
+
+  @Override
+  public void prepare(MediaSource mediaSource) {
+    prepare(mediaSource, true, true);
+  }
+
+  @Override
+  public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
+    if (resetState) {
+      if (!timeline.isEmpty() || manifest != null) {
+        timeline = Timeline.EMPTY;
+        manifest = null;
+        for (EventListener listener : listeners) {
+          listener.onTimelineChanged(timeline, manifest);
+        }
+      }
+      if (tracksSelected) {
+        tracksSelected = false;
+        trackGroups = TrackGroupArray.EMPTY;
+        trackSelections = emptyTrackSelections;
+        trackSelector.onSelectionActivated(null);
+        for (EventListener listener : listeners) {
+          listener.onTracksChanged(trackGroups, trackSelections);
+        }
+      }
+    }
+    pendingPrepareAcks++;
+    internalPlayer.prepare(mediaSource, resetPosition);
+  }
+
+  @Override
+  public void setPlayWhenReady(boolean playWhenReady) {
+    if (this.playWhenReady != playWhenReady) {
+      this.playWhenReady = playWhenReady;
+      internalPlayer.setPlayWhenReady(playWhenReady);
+      for (EventListener listener : listeners) {
+        listener.onPlayerStateChanged(playWhenReady, playbackState);
+      }
+    }
+  }
+
+  @Override
+  public boolean getPlayWhenReady() {
+    return playWhenReady;
+  }
+
+  @Override
+  public boolean isLoading() {
+    return isLoading;
+  }
+
+  @Override
+  public void seekToDefaultPosition() {
+    seekToDefaultPosition(getCurrentWindowIndex());
+  }
+
+  @Override
+  public void seekToDefaultPosition(int windowIndex) {
+    seekTo(windowIndex, C.TIME_UNSET);
+  }
+
+  @Override
+  public void seekTo(long positionMs) {
+    seekTo(getCurrentWindowIndex(), positionMs);
+  }
+
+  @Override
+  public void seekTo(int windowIndex, long positionMs) {
+    if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) {
+      throw new IllegalSeekPositionException(timeline, windowIndex, positionMs);
+    }
+    pendingSeekAcks++;
+    maskingWindowIndex = windowIndex;
+    if (timeline.isEmpty()) {
+      maskingPeriodIndex = 0;
+    } else {
+      timeline.getWindow(windowIndex, window);
+      long resolvedPositionMs =
+          positionMs == C.TIME_UNSET ? window.getDefaultPositionUs() : positionMs;
+      int periodIndex = window.firstPeriodIndex;
+      long periodPositionUs = window.getPositionInFirstPeriodUs() + C.msToUs(resolvedPositionMs);
+      long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs();
+      while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs
+          && periodIndex < window.lastPeriodIndex) {
+        periodPositionUs -= periodDurationUs;
+        periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs();
+      }
+      maskingPeriodIndex = periodIndex;
+    }
+    if (positionMs == C.TIME_UNSET) {
+      maskingWindowPositionMs = 0;
+      internalPlayer.seekTo(timeline, windowIndex, C.TIME_UNSET);
+    } else {
+      maskingWindowPositionMs = positionMs;
+      internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs));
+      for (EventListener listener : listeners) {
+        listener.onPositionDiscontinuity();
+      }
+    }
+  }
+
+  @Override
+  public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) {
+    if (playbackParameters == null) {
+      playbackParameters = PlaybackParameters.DEFAULT;
+    }
+    internalPlayer.setPlaybackParameters(playbackParameters);
+  }
+
+  @Override
+  public PlaybackParameters getPlaybackParameters() {
+    return playbackParameters;
+  }
+
+  @Override
+  public void stop() {
+    internalPlayer.stop();
+  }
+
+  @Override
+  public void release() {
+    internalPlayer.release();
+    eventHandler.removeCallbacksAndMessages(null);
+  }
+
+  @Override
+  public void sendMessages(ExoPlayerMessage... messages) {
+    internalPlayer.sendMessages(messages);
+  }
+
+  @Override
+  public void blockingSendMessages(ExoPlayerMessage... messages) {
+    internalPlayer.blockingSendMessages(messages);
+  }
+
+  @Override
+  public int getCurrentPeriodIndex() {
+    if (timeline.isEmpty() || pendingSeekAcks > 0) {
+      return maskingPeriodIndex;
+    } else {
+      return playbackInfo.periodIndex;
+    }
+  }
+
+  @Override
+  public int getCurrentWindowIndex() {
+    if (timeline.isEmpty() || pendingSeekAcks > 0) {
+      return maskingWindowIndex;
+    } else {
+      return timeline.getPeriod(playbackInfo.periodIndex, period).windowIndex;
+    }
+  }
+
+  @Override
+  public long getDuration() {
+    if (timeline.isEmpty()) {
+      return C.TIME_UNSET;
+    }
+    return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
+  }
+
+  @Override
+  public long getCurrentPosition() {
+    if (timeline.isEmpty() || pendingSeekAcks > 0) {
+      return maskingWindowPositionMs;
+    } else {
+      timeline.getPeriod(playbackInfo.periodIndex, period);
+      return period.getPositionInWindowMs() + C.usToMs(playbackInfo.positionUs);
+    }
+  }
+
+  @Override
+  public long getBufferedPosition() {
+    // TODO - Implement this properly.
+    if (timeline.isEmpty() || pendingSeekAcks > 0) {
+      return maskingWindowPositionMs;
+    } else {
+      timeline.getPeriod(playbackInfo.periodIndex, period);
+      return period.getPositionInWindowMs() + C.usToMs(playbackInfo.bufferedPositionUs);
+    }
+  }
+
+  @Override
+  public int getBufferedPercentage() {
+    if (timeline.isEmpty()) {
+      return 0;
+    }
+    long bufferedPosition = getBufferedPosition();
+    long duration = getDuration();
+    return (bufferedPosition == C.TIME_UNSET || duration == C.TIME_UNSET) ? 0
+        : (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration);
+  }
+
+  @Override
+  public boolean isCurrentWindowDynamic() {
+    return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
+  }
+
+  @Override
+  public boolean isCurrentWindowSeekable() {
+    return !timeline.isEmpty() && timeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
+  }
+
+  @Override
+  public int getRendererCount() {
+    return renderers.length;
+  }
+
+  @Override
+  public int getRendererType(int index) {
+    return renderers[index].getTrackType();
+  }
+
+  @Override
+  public TrackGroupArray getCurrentTrackGroups() {
+    return trackGroups;
+  }
+
+  @Override
+  public TrackSelectionArray getCurrentTrackSelections() {
+    return trackSelections;
+  }
+
+  @Override
+  public Timeline getCurrentTimeline() {
+    return timeline;
+  }
+
+  @Override
+  public Object getCurrentManifest() {
+    return manifest;
+  }
+
+  // Not private so it can be called from an inner class without going through a thunk method.
+  /* package */ void handleEvent(Message msg) {
+    switch (msg.what) {
+      case ExoPlayerImplInternal.MSG_PREPARE_ACK: {
+        pendingPrepareAcks--;
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_STATE_CHANGED: {
+        playbackState = msg.arg1;
+        for (EventListener listener : listeners) {
+          listener.onPlayerStateChanged(playWhenReady, playbackState);
+        }
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_LOADING_CHANGED: {
+        isLoading = msg.arg1 != 0;
+        for (EventListener listener : listeners) {
+          listener.onLoadingChanged(isLoading);
+        }
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: {
+        if (pendingPrepareAcks == 0) {
+          TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj;
+          tracksSelected = true;
+          trackGroups = trackSelectorResult.groups;
+          trackSelections = trackSelectorResult.selections;
+          trackSelector.onSelectionActivated(trackSelectorResult.info);
+          for (EventListener listener : listeners) {
+            listener.onTracksChanged(trackGroups, trackSelections);
+          }
+        }
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_SEEK_ACK: {
+        if (--pendingSeekAcks == 0) {
+          playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj;
+          if (msg.arg1 != 0) {
+            for (EventListener listener : listeners) {
+              listener.onPositionDiscontinuity();
+            }
+          }
+        }
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: {
+        if (pendingSeekAcks == 0) {
+          playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj;
+          for (EventListener listener : listeners) {
+            listener.onPositionDiscontinuity();
+          }
+        }
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: {
+        SourceInfo sourceInfo = (SourceInfo) msg.obj;
+        pendingSeekAcks -= sourceInfo.seekAcks;
+        if (pendingPrepareAcks == 0) {
+          timeline = sourceInfo.timeline;
+          manifest = sourceInfo.manifest;
+          playbackInfo = sourceInfo.playbackInfo;
+          for (EventListener listener : listeners) {
+            listener.onTimelineChanged(timeline, manifest);
+          }
+        }
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: {
+        PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj;
+        if (!this.playbackParameters.equals(playbackParameters)) {
+          this.playbackParameters = playbackParameters;
+          for (EventListener listener : listeners) {
+            listener.onPlaybackParametersChanged(playbackParameters);
+          }
+        }
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_ERROR: {
+        ExoPlaybackException exception = (ExoPlaybackException) msg.obj;
+        for (EventListener listener : listeners) {
+          listener.onPlayerError(exception);
+        }
+        break;
+      }
+      default:
+        throw new IllegalStateException();
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -0,0 +1,1573 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MediaClock;
+import com.google.android.exoplayer2.util.StandaloneMediaClock;
+import com.google.android.exoplayer2.util.TraceUtil;
+import java.io.IOException;
+
+/**
+ * Implements the internal behavior of {@link ExoPlayerImpl}.
+ */
+/* package */ final class ExoPlayerImplInternal implements Handler.Callback,
+    MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener {
+
+  /**
+   * Playback position information which is read on the application's thread by
+   * {@link ExoPlayerImpl} and read/written internally on the player's thread.
+   */
+  public static final class PlaybackInfo {
+
+    public final int periodIndex;
+    public final long startPositionUs;
+
+    public volatile long positionUs;
+    public volatile long bufferedPositionUs;
+
+    public PlaybackInfo(int periodIndex, long startPositionUs) {
+      this.periodIndex = periodIndex;
+      this.startPositionUs = startPositionUs;
+      positionUs = startPositionUs;
+      bufferedPositionUs = startPositionUs;
+    }
+
+    public PlaybackInfo copyWithPeriodIndex(int periodIndex) {
+      PlaybackInfo playbackInfo = new PlaybackInfo(periodIndex, startPositionUs);
+      playbackInfo.positionUs = positionUs;
+      playbackInfo.bufferedPositionUs = bufferedPositionUs;
+      return playbackInfo;
+    }
+
+  }
+
+  public static final class SourceInfo {
+
+    public final Timeline timeline;
+    public final Object manifest;
+    public final PlaybackInfo playbackInfo;
+    public final int seekAcks;
+
+    public SourceInfo(Timeline timeline, Object manifest, PlaybackInfo playbackInfo, int seekAcks) {
+      this.timeline = timeline;
+      this.manifest = manifest;
+      this.playbackInfo = playbackInfo;
+      this.seekAcks = seekAcks;
+    }
+
+  }
+
+  private static final String TAG = "ExoPlayerImplInternal";
+
+  // External messages
+  public static final int MSG_PREPARE_ACK = 0;
+  public static final int MSG_STATE_CHANGED = 1;
+  public static final int MSG_LOADING_CHANGED = 2;
+  public static final int MSG_TRACKS_CHANGED = 3;
+  public static final int MSG_SEEK_ACK = 4;
+  public static final int MSG_POSITION_DISCONTINUITY = 5;
+  public static final int MSG_SOURCE_INFO_REFRESHED = 6;
+  public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 7;
+  public static final int MSG_ERROR = 8;
+
+  // Internal messages
+  private static final int MSG_PREPARE = 0;
+  private static final int MSG_SET_PLAY_WHEN_READY = 1;
+  private static final int MSG_DO_SOME_WORK = 2;
+  private static final int MSG_SEEK_TO = 3;
+  private static final int MSG_SET_PLAYBACK_PARAMETERS = 4;
+  private static final int MSG_STOP = 5;
+  private static final int MSG_RELEASE = 6;
+  private static final int MSG_REFRESH_SOURCE_INFO = 7;
+  private static final int MSG_PERIOD_PREPARED = 8;
+  private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9;
+  private static final int MSG_TRACK_SELECTION_INVALIDATED = 10;
+  private static final int MSG_CUSTOM = 11;
+
+  private static final int PREPARING_SOURCE_INTERVAL_MS = 10;
+  private static final int RENDERING_INTERVAL_MS = 10;
+  private static final int IDLE_INTERVAL_MS = 1000;
+
+  /**
+   * Limits the maximum number of periods to buffer ahead of the current playing period. The
+   * buffering policy normally prevents buffering too far ahead, but the policy could allow too many
+   * small periods to be buffered if the period count were not limited.
+   */
+  private static final int MAXIMUM_BUFFER_AHEAD_PERIODS = 100;
+
+  /**
+   * Offset added to all sample timestamps read by renderers to make them non-negative. This is
+   * provided for convenience of sources that may return negative timestamps due to prerolling
+   * samples from a keyframe before their first sample with timestamp zero, so it must be set to a
+   * value greater than or equal to the maximum key-frame interval in seekable periods.
+   */
+  private static final int RENDERER_TIMESTAMP_OFFSET_US = 60000000;
+
+  private final Renderer[] renderers;
+  private final RendererCapabilities[] rendererCapabilities;
+  private final TrackSelector trackSelector;
+  private final LoadControl loadControl;
+  private final StandaloneMediaClock standaloneMediaClock;
+  private final Handler handler;
+  private final HandlerThread internalPlaybackThread;
+  private final Handler eventHandler;
+  private final ExoPlayer player;
+  private final Timeline.Window window;
+  private final Timeline.Period period;
+
+  private PlaybackInfo playbackInfo;
+  private PlaybackParameters playbackParameters;
+  private Renderer rendererMediaClockSource;
+  private MediaClock rendererMediaClock;
+  private MediaSource mediaSource;
+  private Renderer[] enabledRenderers;
+  private boolean released;
+  private boolean playWhenReady;
+  private boolean rebuffering;
+  private boolean isLoading;
+  private int state;
+  private int customMessagesSent;
+  private int customMessagesProcessed;
+  private long elapsedRealtimeUs;
+
+  private int pendingInitialSeekCount;
+  private SeekPosition pendingSeekPosition;
+  private long rendererPositionUs;
+
+  private MediaPeriodHolder loadingPeriodHolder;
+  private MediaPeriodHolder readingPeriodHolder;
+  private MediaPeriodHolder playingPeriodHolder;
+
+  private Timeline timeline;
+
+  public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector,
+      LoadControl loadControl, boolean playWhenReady, Handler eventHandler,
+      PlaybackInfo playbackInfo, ExoPlayer player) {
+    this.renderers = renderers;
+    this.trackSelector = trackSelector;
+    this.loadControl = loadControl;
+    this.playWhenReady = playWhenReady;
+    this.eventHandler = eventHandler;
+    this.state = ExoPlayer.STATE_IDLE;
+    this.playbackInfo = playbackInfo;
+    this.player = player;
+
+    rendererCapabilities = new RendererCapabilities[renderers.length];
+    for (int i = 0; i < renderers.length; i++) {
+      renderers[i].setIndex(i);
+      rendererCapabilities[i] = renderers[i].getCapabilities();
+    }
+    standaloneMediaClock = new StandaloneMediaClock();
+    enabledRenderers = new Renderer[0];
+    window = new Timeline.Window();
+    period = new Timeline.Period();
+    trackSelector.init(this);
+    playbackParameters = PlaybackParameters.DEFAULT;
+
+    // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
+    // not normally change to this priority" is incorrect.
+    internalPlaybackThread = new HandlerThread("ExoPlayerImplInternal:Handler",
+        Process.THREAD_PRIORITY_AUDIO);
+    internalPlaybackThread.start();
+    handler = new Handler(internalPlaybackThread.getLooper(), this);
+  }
+
+  public void prepare(MediaSource mediaSource, boolean resetPosition) {
+    handler.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, 0, mediaSource)
+        .sendToTarget();
+  }
+
+  public void setPlayWhenReady(boolean playWhenReady) {
+    handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget();
+  }
+
+  public void seekTo(Timeline timeline, int windowIndex, long positionUs) {
+    handler.obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs))
+        .sendToTarget();
+  }
+
+  public void setPlaybackParameters(PlaybackParameters playbackParameters) {
+    handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget();
+  }
+
+  public void stop() {
+    handler.sendEmptyMessage(MSG_STOP);
+  }
+
+  public void sendMessages(ExoPlayerMessage... messages) {
+    if (released) {
+      Log.w(TAG, "Ignoring messages sent after release.");
+      return;
+    }
+    customMessagesSent++;
+    handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget();
+  }
+
+  public synchronized void blockingSendMessages(ExoPlayerMessage... messages) {
+    if (released) {
+      Log.w(TAG, "Ignoring messages sent after release.");
+      return;
+    }
+    int messageNumber = customMessagesSent++;
+    handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget();
+    while (customMessagesProcessed <= messageNumber) {
+      try {
+        wait();
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+      }
+    }
+  }
+
+  public synchronized void release() {
+    if (released) {
+      return;
+    }
+    handler.sendEmptyMessage(MSG_RELEASE);
+    while (!released) {
+      try {
+        wait();
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+      }
+    }
+    internalPlaybackThread.quit();
+  }
+
+  // MediaSource.Listener implementation.
+
+  @Override
+  public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+    handler.obtainMessage(MSG_REFRESH_SOURCE_INFO, Pair.create(timeline, manifest)).sendToTarget();
+  }
+
+  // MediaPeriod.Callback implementation.
+
+  @Override
+  public void onPrepared(MediaPeriod source) {
+    handler.obtainMessage(MSG_PERIOD_PREPARED, source).sendToTarget();
+  }
+
+  @Override
+  public void onContinueLoadingRequested(MediaPeriod source) {
+    handler.obtainMessage(MSG_SOURCE_CONTINUE_LOADING_REQUESTED, source).sendToTarget();
+  }
+
+  // TrackSelector.InvalidationListener implementation.
+
+  @Override
+  public void onTrackSelectionsInvalidated() {
+    handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED);
+  }
+
+  // Handler.Callback implementation.
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public boolean handleMessage(Message msg) {
+    try {
+      switch (msg.what) {
+        case MSG_PREPARE: {
+          prepareInternal((MediaSource) msg.obj, msg.arg1 != 0);
+          return true;
+        }
+        case MSG_SET_PLAY_WHEN_READY: {
+          setPlayWhenReadyInternal(msg.arg1 != 0);
+          return true;
+        }
+        case MSG_DO_SOME_WORK: {
+          doSomeWork();
+          return true;
+        }
+        case MSG_SEEK_TO: {
+          seekToInternal((SeekPosition) msg.obj);
+          return true;
+        }
+        case MSG_SET_PLAYBACK_PARAMETERS: {
+          setPlaybackParametersInternal((PlaybackParameters) msg.obj);
+          return true;
+        }
+        case MSG_STOP: {
+          stopInternal();
+          return true;
+        }
+        case MSG_RELEASE: {
+          releaseInternal();
+          return true;
+        }
+        case MSG_PERIOD_PREPARED: {
+          handlePeriodPrepared((MediaPeriod) msg.obj);
+          return true;
+        }
+        case MSG_REFRESH_SOURCE_INFO: {
+          handleSourceInfoRefreshed((Pair<Timeline, Object>) msg.obj);
+          return true;
+        }
+        case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: {
+          handleContinueLoadingRequested((MediaPeriod) msg.obj);
+          return true;
+        }
+        case MSG_TRACK_SELECTION_INVALIDATED: {
+          reselectTracksInternal();
+          return true;
+        }
+        case MSG_CUSTOM: {
+          sendMessagesInternal((ExoPlayerMessage[]) msg.obj);
+          return true;
+        }
+        default:
+          return false;
+      }
+    } catch (ExoPlaybackException e) {
+      Log.e(TAG, "Renderer error.", e);
+      eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
+      stopInternal();
+      return true;
+    } catch (IOException e) {
+      Log.e(TAG, "Source error.", e);
+      eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget();
+      stopInternal();
+      return true;
+    } catch (RuntimeException e) {
+      Log.e(TAG, "Internal runtime error.", e);
+      eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e))
+          .sendToTarget();
+      stopInternal();
+      return true;
+    }
+  }
+
+  // Private methods.
+
+  private void setState(int state) {
+    if (this.state != state) {
+      this.state = state;
+      eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget();
+    }
+  }
+
+  private void setIsLoading(boolean isLoading) {
+    if (this.isLoading != isLoading) {
+      this.isLoading = isLoading;
+      eventHandler.obtainMessage(MSG_LOADING_CHANGED, isLoading ? 1 : 0, 0).sendToTarget();
+    }
+  }
+
+  private void prepareInternal(MediaSource mediaSource, boolean resetPosition) {
+    eventHandler.sendEmptyMessage(MSG_PREPARE_ACK);
+    resetInternal(true);
+    loadControl.onPrepared();
+    if (resetPosition) {
+      playbackInfo = new PlaybackInfo(0, C.TIME_UNSET);
+    }
+    this.mediaSource = mediaSource;
+    mediaSource.prepareSource(player, true, this);
+    setState(ExoPlayer.STATE_BUFFERING);
+    handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+  }
+
+  private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
+    rebuffering = false;
+    this.playWhenReady = playWhenReady;
+    if (!playWhenReady) {
+      stopRenderers();
+      updatePlaybackPositions();
+    } else {
+      if (state == ExoPlayer.STATE_READY) {
+        startRenderers();
+        handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+      } else if (state == ExoPlayer.STATE_BUFFERING) {
+        handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+      }
+    }
+  }
+
+  private void startRenderers() throws ExoPlaybackException {
+    rebuffering = false;
+    standaloneMediaClock.start();
+    for (Renderer renderer : enabledRenderers) {
+      renderer.start();
+    }
+  }
+
+  private void stopRenderers() throws ExoPlaybackException {
+    standaloneMediaClock.stop();
+    for (Renderer renderer : enabledRenderers) {
+      ensureStopped(renderer);
+    }
+  }
+
+  private void updatePlaybackPositions() throws ExoPlaybackException {
+    if (playingPeriodHolder == null) {
+      return;
+    }
+
+    // Update the playback position.
+    long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity();
+    if (periodPositionUs != C.TIME_UNSET) {
+      resetRendererPosition(periodPositionUs);
+    } else {
+      if (rendererMediaClockSource != null && !rendererMediaClockSource.isEnded()) {
+        rendererPositionUs = rendererMediaClock.getPositionUs();
+        standaloneMediaClock.setPositionUs(rendererPositionUs);
+      } else {
+        rendererPositionUs = standaloneMediaClock.getPositionUs();
+      }
+      periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs);
+    }
+    playbackInfo.positionUs = periodPositionUs;
+    elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
+
+    // Update the buffered position.
+    long bufferedPositionUs = enabledRenderers.length == 0 ? C.TIME_END_OF_SOURCE
+        : playingPeriodHolder.mediaPeriod.getBufferedPositionUs();
+    playbackInfo.bufferedPositionUs = bufferedPositionUs == C.TIME_END_OF_SOURCE
+        ? timeline.getPeriod(playingPeriodHolder.index, period).getDurationUs()
+        : bufferedPositionUs;
+  }
+
+  private void doSomeWork() throws ExoPlaybackException, IOException {
+    long operationStartTimeMs = SystemClock.elapsedRealtime();
+    updatePeriods();
+    if (playingPeriodHolder == null) {
+      // We're still waiting for the first period to be prepared.
+      maybeThrowPeriodPrepareError();
+      scheduleNextWork(operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS);
+      return;
+    }
+
+    TraceUtil.beginSection("doSomeWork");
+
+    updatePlaybackPositions();
+    playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs);
+
+    boolean allRenderersEnded = true;
+    boolean allRenderersReadyOrEnded = true;
+    for (Renderer renderer : enabledRenderers) {
+      // TODO: Each renderer should return the maximum delay before which it wishes to be called
+      // again. The minimum of these values should then be used as the delay before the next
+      // invocation of this method.
+      renderer.render(rendererPositionUs, elapsedRealtimeUs);
+      allRenderersEnded = allRenderersEnded && renderer.isEnded();
+      // Determine whether the renderer is ready (or ended). If it's not, throw an error that's
+      // preventing the renderer from making progress, if such an error exists.
+      boolean rendererReadyOrEnded = renderer.isReady() || renderer.isEnded();
+      if (!rendererReadyOrEnded) {
+        renderer.maybeThrowStreamError();
+      }
+      allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded;
+    }
+
+    if (!allRenderersReadyOrEnded) {
+      maybeThrowPeriodPrepareError();
+    }
+
+    // The standalone media clock never changes playback parameters, so just check the renderer.
+    if (rendererMediaClock != null) {
+      PlaybackParameters playbackParameters = rendererMediaClock.getPlaybackParameters();
+      if (!playbackParameters.equals(this.playbackParameters)) {
+        // TODO: Make LoadControl, period transition position projection, adaptive track selection
+        // and potentially any time-related code in renderers take into account the playback speed.
+        this.playbackParameters = playbackParameters;
+        standaloneMediaClock.synchronize(rendererMediaClock);
+        eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters)
+            .sendToTarget();
+      }
+    }
+
+    long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.index, period)
+        .getDurationUs();
+    if (allRenderersEnded
+        && (playingPeriodDurationUs == C.TIME_UNSET
+        || playingPeriodDurationUs <= playbackInfo.positionUs)
+        && playingPeriodHolder.isLast) {
+      setState(ExoPlayer.STATE_ENDED);
+      stopRenderers();
+    } else if (state == ExoPlayer.STATE_BUFFERING) {
+      boolean isNewlyReady = enabledRenderers.length > 0
+          ? (allRenderersReadyOrEnded && haveSufficientBuffer(rebuffering))
+          : isTimelineReady(playingPeriodDurationUs);
+      if (isNewlyReady) {
+        setState(ExoPlayer.STATE_READY);
+        if (playWhenReady) {
+          startRenderers();
+        }
+      }
+    } else if (state == ExoPlayer.STATE_READY) {
+      boolean isStillReady = enabledRenderers.length > 0 ? allRenderersReadyOrEnded
+          : isTimelineReady(playingPeriodDurationUs);
+      if (!isStillReady) {
+        rebuffering = playWhenReady;
+        setState(ExoPlayer.STATE_BUFFERING);
+        stopRenderers();
+      }
+    }
+
+    if (state == ExoPlayer.STATE_BUFFERING) {
+      for (Renderer renderer : enabledRenderers) {
+        renderer.maybeThrowStreamError();
+      }
+    }
+
+    if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) {
+      scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS);
+    } else if (enabledRenderers.length != 0) {
+      scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
+    } else {
+      handler.removeMessages(MSG_DO_SOME_WORK);
+    }
+
+    TraceUtil.endSection();
+  }
+
+  private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) {
+    handler.removeMessages(MSG_DO_SOME_WORK);
+    long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs;
+    long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime();
+    if (nextOperationDelayMs <= 0) {
+      handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+    } else {
+      handler.sendEmptyMessageDelayed(MSG_DO_SOME_WORK, nextOperationDelayMs);
+    }
+  }
+
+  private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
+    if (timeline == null) {
+      pendingInitialSeekCount++;
+      pendingSeekPosition = seekPosition;
+      return;
+    }
+
+    Pair<Integer, Long> periodPosition = resolveSeekPosition(seekPosition);
+    if (periodPosition == null) {
+      // The seek position was valid for the timeline that it was performed into, but the
+      // timeline has changed and a suitable seek position could not be resolved in the new one.
+      playbackInfo = new PlaybackInfo(0, 0);
+      eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, playbackInfo).sendToTarget();
+      // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't
+      // ignored.
+      playbackInfo = new PlaybackInfo(0, C.TIME_UNSET);
+      setState(ExoPlayer.STATE_ENDED);
+      // Reset, but retain the source so that it can still be used should a seek occur.
+      resetInternal(false);
+      return;
+    }
+
+    boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET;
+    int periodIndex = periodPosition.first;
+    long periodPositionUs = periodPosition.second;
+
+    try {
+      if (periodIndex == playbackInfo.periodIndex
+          && ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000))) {
+        // Seek position equals the current position. Do nothing.
+        return;
+      }
+      long newPeriodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs);
+      seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs;
+      periodPositionUs = newPeriodPositionUs;
+    } finally {
+      playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs);
+      eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo)
+          .sendToTarget();
+    }
+  }
+
+  private long seekToPeriodPosition(int periodIndex, long periodPositionUs)
+      throws ExoPlaybackException {
+    stopRenderers();
+    rebuffering = false;
+    setState(ExoPlayer.STATE_BUFFERING);
+
+    MediaPeriodHolder newPlayingPeriodHolder = null;
+    if (playingPeriodHolder == null) {
+      // We're still waiting for the first period to be prepared.
+      if (loadingPeriodHolder != null) {
+        loadingPeriodHolder.release();
+      }
+    } else {
+      // Clear the timeline, but keep the requested period if it is already prepared.
+      MediaPeriodHolder periodHolder = playingPeriodHolder;
+      while (periodHolder != null) {
+        if (periodHolder.index == periodIndex && periodHolder.prepared) {
+          newPlayingPeriodHolder = periodHolder;
+        } else {
+          periodHolder.release();
+        }
+        periodHolder = periodHolder.next;
+      }
+    }
+
+    // Disable all the renderers if the period being played is changing, or if the renderers are
+    // reading from a period other than the one being played.
+    if (playingPeriodHolder != newPlayingPeriodHolder
+        || playingPeriodHolder != readingPeriodHolder) {
+      for (Renderer renderer : enabledRenderers) {
+        renderer.disable();
+      }
+      enabledRenderers = new Renderer[0];
+      rendererMediaClock = null;
+      rendererMediaClockSource = null;
+      playingPeriodHolder = null;
+    }
+
+    // Update the holders.
+    if (newPlayingPeriodHolder != null) {
+      newPlayingPeriodHolder.next = null;
+      loadingPeriodHolder = newPlayingPeriodHolder;
+      readingPeriodHolder = newPlayingPeriodHolder;
+      setPlayingPeriodHolder(newPlayingPeriodHolder);
+      if (playingPeriodHolder.hasEnabledTracks) {
+        periodPositionUs = playingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs);
+      }
+      resetRendererPosition(periodPositionUs);
+      maybeContinueLoading();
+    } else {
+      loadingPeriodHolder = null;
+      readingPeriodHolder = null;
+      playingPeriodHolder = null;
+      resetRendererPosition(periodPositionUs);
+    }
+
+    handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+    return periodPositionUs;
+  }
+
+  private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException {
+    rendererPositionUs = playingPeriodHolder == null
+        ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US
+        : playingPeriodHolder.toRendererTime(periodPositionUs);
+    standaloneMediaClock.setPositionUs(rendererPositionUs);
+    for (Renderer renderer : enabledRenderers) {
+      renderer.resetPosition(rendererPositionUs);
+    }
+  }
+
+  private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) {
+    playbackParameters = rendererMediaClock != null
+        ? rendererMediaClock.setPlaybackParameters(playbackParameters)
+        : standaloneMediaClock.setPlaybackParameters(playbackParameters);
+    this.playbackParameters = playbackParameters;
+    eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget();
+  }
+
+  private void stopInternal() {
+    resetInternal(true);
+    loadControl.onStopped();
+    setState(ExoPlayer.STATE_IDLE);
+  }
+
+  private void releaseInternal() {
+    resetInternal(true);
+    loadControl.onReleased();
+    setState(ExoPlayer.STATE_IDLE);
+    synchronized (this) {
+      released = true;
+      notifyAll();
+    }
+  }
+
+  private void resetInternal(boolean releaseMediaSource) {
+    handler.removeMessages(MSG_DO_SOME_WORK);
+    rebuffering = false;
+    standaloneMediaClock.stop();
+    rendererMediaClock = null;
+    rendererMediaClockSource = null;
+    rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US;
+    for (Renderer renderer : enabledRenderers) {
+      try {
+        ensureStopped(renderer);
+        renderer.disable();
+      } catch (ExoPlaybackException | RuntimeException e) {
+        // There's nothing we can do.
+        Log.e(TAG, "Stop failed.", e);
+      }
+    }
+    enabledRenderers = new Renderer[0];
+    releasePeriodHoldersFrom(playingPeriodHolder != null ? playingPeriodHolder
+        : loadingPeriodHolder);
+    loadingPeriodHolder = null;
+    readingPeriodHolder = null;
+    playingPeriodHolder = null;
+    setIsLoading(false);
+    if (releaseMediaSource) {
+      if (mediaSource != null) {
+        mediaSource.releaseSource();
+        mediaSource = null;
+      }
+      timeline = null;
+    }
+  }
+
+  private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException {
+    try {
+      for (ExoPlayerMessage message : messages) {
+        message.target.handleMessage(message.messageType, message.message);
+      }
+      if (mediaSource != null) {
+        // The message may have caused something to change that now requires us to do work.
+        handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+      }
+    } finally {
+      synchronized (this) {
+        customMessagesProcessed++;
+        notifyAll();
+      }
+    }
+  }
+
+  private void ensureStopped(Renderer renderer) throws ExoPlaybackException {
+    if (renderer.getState() == Renderer.STATE_STARTED) {
+      renderer.stop();
+    }
+  }
+
+  private void reselectTracksInternal() throws ExoPlaybackException {
+    if (playingPeriodHolder == null) {
+      // We don't have tracks yet, so we don't care.
+      return;
+    }
+    // Reselect tracks on each period in turn, until the selection changes.
+    MediaPeriodHolder periodHolder = playingPeriodHolder;
+    boolean selectionsChangedForReadPeriod = true;
+    while (true) {
+      if (periodHolder == null || !periodHolder.prepared) {
+        // The reselection did not change any prepared periods.
+        return;
+      }
+      if (periodHolder.selectTracks()) {
+        // Selected tracks have changed for this period.
+        break;
+      }
+      if (periodHolder == readingPeriodHolder) {
+        // The track reselection didn't affect any period that has been read.
+        selectionsChangedForReadPeriod = false;
+      }
+      periodHolder = periodHolder.next;
+    }
+
+    if (selectionsChangedForReadPeriod) {
+      // Update streams and rebuffer for the new selection, recreating all streams if reading ahead.
+      boolean recreateStreams = readingPeriodHolder != playingPeriodHolder;
+      releasePeriodHoldersFrom(playingPeriodHolder.next);
+      playingPeriodHolder.next = null;
+      loadingPeriodHolder = playingPeriodHolder;
+      readingPeriodHolder = playingPeriodHolder;
+
+      boolean[] streamResetFlags = new boolean[renderers.length];
+      long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection(
+          playbackInfo.positionUs, recreateStreams, streamResetFlags);
+      if (periodPositionUs != playbackInfo.positionUs) {
+        playbackInfo.positionUs = periodPositionUs;
+        resetRendererPosition(periodPositionUs);
+      }
+
+      int enabledRendererCount = 0;
+      boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
+      for (int i = 0; i < renderers.length; i++) {
+        Renderer renderer = renderers[i];
+        rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;
+        SampleStream sampleStream = playingPeriodHolder.sampleStreams[i];
+        if (sampleStream != null) {
+          enabledRendererCount++;
+        }
+        if (rendererWasEnabledFlags[i]) {
+          if (sampleStream != renderer.getStream()) {
+            // We need to disable the renderer.
+            if (renderer == rendererMediaClockSource) {
+              // The renderer is providing the media clock.
+              if (sampleStream == null) {
+                // The renderer won't be re-enabled. Sync standaloneMediaClock so that it can take
+                // over timing responsibilities.
+                standaloneMediaClock.synchronize(rendererMediaClock);
+              }
+              rendererMediaClock = null;
+              rendererMediaClockSource = null;
+            }
+            ensureStopped(renderer);
+            renderer.disable();
+          } else if (streamResetFlags[i]) {
+            // The renderer will continue to consume from its current stream, but needs to be reset.
+            renderer.resetPosition(rendererPositionUs);
+          }
+        }
+      }
+      eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult)
+          .sendToTarget();
+      enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
+    } else {
+      // Release and re-prepare/buffer periods after the one whose selection changed.
+      loadingPeriodHolder = periodHolder;
+      periodHolder = loadingPeriodHolder.next;
+      while (periodHolder != null) {
+        periodHolder.release();
+        periodHolder = periodHolder.next;
+      }
+      loadingPeriodHolder.next = null;
+      if (loadingPeriodHolder.prepared) {
+        long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.startPositionUs,
+            loadingPeriodHolder.toPeriodTime(rendererPositionUs));
+        loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false);
+      }
+    }
+    maybeContinueLoading();
+    updatePlaybackPositions();
+    handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+  }
+
+  private boolean isTimelineReady(long playingPeriodDurationUs) {
+    return playingPeriodDurationUs == C.TIME_UNSET
+        || playbackInfo.positionUs < playingPeriodDurationUs
+        || (playingPeriodHolder.next != null && playingPeriodHolder.next.prepared);
+  }
+
+  private boolean haveSufficientBuffer(boolean rebuffering) {
+    long loadingPeriodBufferedPositionUs = !loadingPeriodHolder.prepared
+        ? loadingPeriodHolder.startPositionUs
+        : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs();
+    if (loadingPeriodBufferedPositionUs == C.TIME_END_OF_SOURCE) {
+      if (loadingPeriodHolder.isLast) {
+        return true;
+      }
+      loadingPeriodBufferedPositionUs = timeline.getPeriod(loadingPeriodHolder.index, period)
+          .getDurationUs();
+    }
+    return loadControl.shouldStartPlayback(
+        loadingPeriodBufferedPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs),
+        rebuffering);
+  }
+
+  private void maybeThrowPeriodPrepareError() throws IOException {
+    if (loadingPeriodHolder != null && !loadingPeriodHolder.prepared
+        && (readingPeriodHolder == null || readingPeriodHolder.next == loadingPeriodHolder)) {
+      for (Renderer renderer : enabledRenderers) {
+        if (!renderer.hasReadStreamToEnd()) {
+          return;
+        }
+      }
+      loadingPeriodHolder.mediaPeriod.maybeThrowPrepareError();
+    }
+  }
+
+  private void handleSourceInfoRefreshed(Pair<Timeline, Object> timelineAndManifest)
+      throws ExoPlaybackException {
+    Timeline oldTimeline = timeline;
+    timeline = timelineAndManifest.first;
+    Object manifest = timelineAndManifest.second;
+
+    int processedInitialSeekCount = 0;
+    if (oldTimeline == null) {
+      if (pendingInitialSeekCount > 0) {
+        Pair<Integer, Long> periodPosition = resolveSeekPosition(pendingSeekPosition);
+        processedInitialSeekCount = pendingInitialSeekCount;
+        pendingInitialSeekCount = 0;
+        pendingSeekPosition = null;
+        if (periodPosition == null) {
+          // The seek position was valid for the timeline that it was performed into, but the
+          // timeline has changed and a suitable seek position could not be resolved in the new one.
+          handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount);
+          return;
+        }
+        playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second);
+      } else if (playbackInfo.startPositionUs == C.TIME_UNSET) {
+        if (timeline.isEmpty()) {
+          handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount);
+          return;
+        }
+        Pair<Integer, Long> defaultPosition = getPeriodPosition(0, C.TIME_UNSET);
+        playbackInfo = new PlaybackInfo(defaultPosition.first, defaultPosition.second);
+      }
+    }
+
+    MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder
+        : loadingPeriodHolder;
+    if (periodHolder == null) {
+      // We don't have any period holders, so we're done.
+      notifySourceInfoRefresh(manifest, processedInitialSeekCount);
+      return;
+    }
+
+    int periodIndex = timeline.getIndexOfPeriod(periodHolder.uid);
+    if (periodIndex == C.INDEX_UNSET) {
+      // We didn't find the current period in the new timeline. Attempt to resolve a subsequent
+      // period whose window we can restart from.
+      int newPeriodIndex = resolveSubsequentPeriod(periodHolder.index, oldTimeline, timeline);
+      if (newPeriodIndex == C.INDEX_UNSET) {
+        // We failed to resolve a suitable restart position.
+        handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount);
+        return;
+      }
+      // We resolved a subsequent period. Seek to the default position in the corresponding window.
+      Pair<Integer, Long> defaultPosition = getPeriodPosition(
+          timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET);
+      newPeriodIndex = defaultPosition.first;
+      long newPositionUs = defaultPosition.second;
+      timeline.getPeriod(newPeriodIndex, period, true);
+      // Clear the index of each holder that doesn't contain the default position. If a holder
+      // contains the default position then update its index so it can be re-used when seeking.
+      Object newPeriodUid = period.uid;
+      periodHolder.index = C.INDEX_UNSET;
+      while (periodHolder.next != null) {
+        periodHolder = periodHolder.next;
+        periodHolder.index = periodHolder.uid.equals(newPeriodUid) ? newPeriodIndex : C.INDEX_UNSET;
+      }
+      // Actually do the seek.
+      newPositionUs = seekToPeriodPosition(newPeriodIndex, newPositionUs);
+      playbackInfo = new PlaybackInfo(newPeriodIndex, newPositionUs);
+      notifySourceInfoRefresh(manifest, processedInitialSeekCount);
+      return;
+    }
+
+    // The current period is in the new timeline. Update the holder and playbackInfo.
+    timeline.getPeriod(periodIndex, period);
+    boolean isLastPeriod = periodIndex == timeline.getPeriodCount() - 1
+        && !timeline.getWindow(period.windowIndex, window).isDynamic;
+    periodHolder.setIndex(periodIndex, isLastPeriod);
+    boolean seenReadingPeriod = periodHolder == readingPeriodHolder;
+    if (periodIndex != playbackInfo.periodIndex) {
+      playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex);
+    }
+
+    // If there are subsequent holders, update the index for each of them. If we find a holder
+    // that's inconsistent with the new timeline then take appropriate action.
+    while (periodHolder.next != null) {
+      MediaPeriodHolder previousPeriodHolder = periodHolder;
+      periodHolder = periodHolder.next;
+      periodIndex++;
+      timeline.getPeriod(periodIndex, period, true);
+      isLastPeriod = periodIndex == timeline.getPeriodCount() - 1
+          && !timeline.getWindow(period.windowIndex, window).isDynamic;
+      if (periodHolder.uid.equals(period.uid)) {
+        // The holder is consistent with the new timeline. Update its index and continue.
+        periodHolder.setIndex(periodIndex, isLastPeriod);
+        seenReadingPeriod |= (periodHolder == readingPeriodHolder);
+      } else {
+        // The holder is inconsistent with the new timeline.
+        if (!seenReadingPeriod) {
+          // Renderers may have read from a period that's been removed. Seek back to the current
+          // position of the playing period to make sure none of the removed period is played.
+          periodIndex = playingPeriodHolder.index;
+          long newPositionUs = seekToPeriodPosition(periodIndex, playbackInfo.positionUs);
+          playbackInfo = new PlaybackInfo(periodIndex, newPositionUs);
+        } else {
+          // Update the loading period to be the last period that's still valid, and release all
+          // subsequent periods.
+          loadingPeriodHolder = previousPeriodHolder;
+          loadingPeriodHolder.next = null;
+          // Release the rest of the timeline.
+          releasePeriodHoldersFrom(periodHolder);
+        }
+        break;
+      }
+    }
+
+    notifySourceInfoRefresh(manifest, processedInitialSeekCount);
+  }
+
+  private void handleSourceInfoRefreshEndedPlayback(Object manifest,
+      int processedInitialSeekCount) {
+    // Set the playback position to (0,0) for notifying the eventHandler.
+    playbackInfo = new PlaybackInfo(0, 0);
+    notifySourceInfoRefresh(manifest, processedInitialSeekCount);
+    // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't ignored.
+    playbackInfo = new PlaybackInfo(0, C.TIME_UNSET);
+    setState(ExoPlayer.STATE_ENDED);
+    // Reset, but retain the source so that it can still be used should a seek occur.
+    resetInternal(false);
+  }
+
+  private void notifySourceInfoRefresh(Object manifest, int processedInitialSeekCount) {
+    eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED,
+        new SourceInfo(timeline, manifest, playbackInfo, processedInitialSeekCount)).sendToTarget();
+  }
+
+  /**
+   * Given a period index into an old timeline, finds the first subsequent period that also exists
+   * in a new timeline. The index of this period in the new timeline is returned.
+   *
+   * @param oldPeriodIndex The index of the period in the old timeline.
+   * @param oldTimeline The old timeline.
+   * @param newTimeline The new timeline.
+   * @return The index in the new timeline of the first subsequent period, or {@link C#INDEX_UNSET}
+   *     if no such period was found.
+   */
+  private int resolveSubsequentPeriod(int oldPeriodIndex, Timeline oldTimeline,
+      Timeline newTimeline) {
+    int newPeriodIndex = C.INDEX_UNSET;
+    while (newPeriodIndex == C.INDEX_UNSET && oldPeriodIndex < oldTimeline.getPeriodCount() - 1) {
+      newPeriodIndex = newTimeline.getIndexOfPeriod(
+          oldTimeline.getPeriod(++oldPeriodIndex, period, true).uid);
+    }
+    return newPeriodIndex;
+  }
+
+  /**
+   * Converts a {@link SeekPosition} into the corresponding (periodIndex, periodPositionUs) for the
+   * internal timeline.
+   *
+   * @param seekPosition The position to resolve.
+   * @return The resolved position, or null if resolution was not successful.
+   * @throws IllegalSeekPositionException If the window index of the seek position is outside the
+   *     bounds of the timeline.
+   */
+  private Pair<Integer, Long> resolveSeekPosition(SeekPosition seekPosition) {
+    Timeline seekTimeline = seekPosition.timeline;
+    if (seekTimeline.isEmpty()) {
+      // The application performed a blind seek without a non-empty timeline (most likely based on
+      // knowledge of what the future timeline will be). Use the internal timeline.
+      seekTimeline = timeline;
+    }
+    // Map the SeekPosition to a position in the corresponding timeline.
+    Pair<Integer, Long> periodPosition;
+    try {
+      periodPosition = getPeriodPosition(seekTimeline, seekPosition.windowIndex,
+          seekPosition.windowPositionUs);
+    } catch (IndexOutOfBoundsException e) {
+      // The window index of the seek position was outside the bounds of the timeline.
+      throw new IllegalSeekPositionException(timeline, seekPosition.windowIndex,
+          seekPosition.windowPositionUs);
+    }
+    if (timeline == seekTimeline) {
+      // Our internal timeline is the seek timeline, so the mapped position is correct.
+      return periodPosition;
+    }
+    // Attempt to find the mapped period in the internal timeline.
+    int periodIndex = timeline.getIndexOfPeriod(
+        seekTimeline.getPeriod(periodPosition.first, period, true).uid);
+    if (periodIndex != C.INDEX_UNSET) {
+      // We successfully located the period in the internal timeline.
+      return Pair.create(periodIndex, periodPosition.second);
+    }
+    // Try and find a subsequent period from the seek timeline in the internal timeline.
+    periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline);
+    if (periodIndex != C.INDEX_UNSET) {
+      // We found one. Map the SeekPosition onto the corresponding default position.
+      return getPeriodPosition(timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET);
+    }
+    // We didn't find one. Give up.
+    return null;
+  }
+
+  /**
+   * Calls {@link #getPeriodPosition(Timeline, int, long)} using the current timeline.
+   */
+  private Pair<Integer, Long> getPeriodPosition(int windowIndex, long windowPositionUs) {
+    return getPeriodPosition(timeline, windowIndex, windowPositionUs);
+  }
+
+  /**
+   * Calls {@link #getPeriodPosition(Timeline, int, long, long)} with a zero default position
+   * projection.
+   */
+  private Pair<Integer, Long> getPeriodPosition(Timeline timeline, int windowIndex,
+      long windowPositionUs) {
+    return getPeriodPosition(timeline, windowIndex, windowPositionUs, 0);
+  }
+
+  /**
+   * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs).
+   *
+   * @param timeline The timeline containing the window.
+   * @param windowIndex The window index.
+   * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default
+   *     start position.
+   * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the
+   *     duration into the future by which the window's position should be projected.
+   * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs}
+   *     is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's
+   *     position could not be projected by {@code defaultPositionProjectionUs}.
+   */
+  private Pair<Integer, Long> getPeriodPosition(Timeline timeline, int windowIndex,
+      long windowPositionUs, long defaultPositionProjectionUs) {
+    Assertions.checkIndex(windowIndex, 0, timeline.getWindowCount());
+    timeline.getWindow(windowIndex, window, false, defaultPositionProjectionUs);
+    if (windowPositionUs == C.TIME_UNSET) {
+      windowPositionUs = window.getDefaultPositionUs();
+      if (windowPositionUs == C.TIME_UNSET) {
+        return null;
+      }
+    }
+    int periodIndex = window.firstPeriodIndex;
+    long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs;
+    long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs();
+    while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs
+        && periodIndex < window.lastPeriodIndex) {
+      periodPositionUs -= periodDurationUs;
+      periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs();
+    }
+    return Pair.create(periodIndex, periodPositionUs);
+  }
+
+  private void updatePeriods() throws ExoPlaybackException, IOException {
+    if (timeline == null) {
+      // We're waiting to get information about periods.
+      mediaSource.maybeThrowSourceInfoRefreshError();
+      return;
+    }
+
+    // Update the loading period if required.
+    maybeUpdateLoadingPeriod();
+    if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) {
+      setIsLoading(false);
+    } else if (loadingPeriodHolder != null && loadingPeriodHolder.needsContinueLoading) {
+      maybeContinueLoading();
+    }
+
+    if (playingPeriodHolder == null) {
+      // We're waiting for the first period to be prepared.
+      return;
+    }
+
+    // Update the playing and reading periods.
+    while (playingPeriodHolder != readingPeriodHolder
+        && rendererPositionUs >= playingPeriodHolder.next.rendererPositionOffsetUs) {
+      // All enabled renderers' streams have been read to the end, and the playback position reached
+      // the end of the playing period, so advance playback to the next period.
+      playingPeriodHolder.release();
+      setPlayingPeriodHolder(playingPeriodHolder.next);
+      playbackInfo = new PlaybackInfo(playingPeriodHolder.index,
+          playingPeriodHolder.startPositionUs);
+      updatePlaybackPositions();
+      eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget();
+    }
+
+    if (readingPeriodHolder.isLast) {
+      for (int i = 0; i < renderers.length; i++) {
+        Renderer renderer = renderers[i];
+        SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
+        // Defer setting the stream as final until the renderer has actually consumed the whole
+        // stream in case of playlist changes that cause the stream to be no longer final.
+        if (sampleStream != null && renderer.getStream() == sampleStream
+            && renderer.hasReadStreamToEnd()) {
+          renderer.setCurrentStreamFinal();
+        }
+      }
+      return;
+    }
+
+    for (int i = 0; i < renderers.length; i++) {
+      Renderer renderer = renderers[i];
+      SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
+      if (renderer.getStream() != sampleStream
+          || (sampleStream != null && !renderer.hasReadStreamToEnd())) {
+        return;
+      }
+    }
+
+    if (readingPeriodHolder.next != null && readingPeriodHolder.next.prepared) {
+      TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
+      readingPeriodHolder = readingPeriodHolder.next;
+      TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
+
+      boolean initialDiscontinuity =
+          readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET;
+      for (int i = 0; i < renderers.length; i++) {
+        Renderer renderer = renderers[i];
+        TrackSelection oldSelection = oldTrackSelectorResult.selections.get(i);
+        if (oldSelection == null) {
+          // The renderer has no current stream and will be enabled when we play the next period.
+        } else if (initialDiscontinuity) {
+          // The new period starts with a discontinuity, so the renderer will play out all data then
+          // be disabled and re-enabled when it starts playing the next period.
+          renderer.setCurrentStreamFinal();
+        } else if (!renderer.isCurrentStreamFinal()) {
+          TrackSelection newSelection = newTrackSelectorResult.selections.get(i);
+          RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i];
+          RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i];
+          if (newSelection != null && newConfig.equals(oldConfig)) {
+            // Replace the renderer's SampleStream so the transition to playing the next period can
+            // be seamless.
+            Format[] formats = new Format[newSelection.length()];
+            for (int j = 0; j < formats.length; j++) {
+              formats[j] = newSelection.getFormat(j);
+            }
+            renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i],
+                readingPeriodHolder.getRendererOffset());
+          } else {
+            // The renderer will be disabled when transitioning to playing the next period, either
+            // because there's no new selection or because a configuration change is required. Mark
+            // the SampleStream as final to play out any remaining data.
+            renderer.setCurrentStreamFinal();
+          }
+        }
+      }
+    }
+  }
+
+  private void maybeUpdateLoadingPeriod() throws IOException {
+    int newLoadingPeriodIndex;
+    if (loadingPeriodHolder == null) {
+      newLoadingPeriodIndex = playbackInfo.periodIndex;
+    } else {
+      int loadingPeriodIndex = loadingPeriodHolder.index;
+      if (loadingPeriodHolder.isLast || !loadingPeriodHolder.isFullyBuffered()
+          || timeline.getPeriod(loadingPeriodIndex, period).getDurationUs() == C.TIME_UNSET) {
+        // Either the existing loading period is the last period, or we are not ready to advance to
+        // loading the next period because it hasn't been fully buffered or its duration is unknown.
+        return;
+      }
+      if (playingPeriodHolder != null
+          && loadingPeriodIndex - playingPeriodHolder.index == MAXIMUM_BUFFER_AHEAD_PERIODS) {
+        // We are already buffering the maximum number of periods ahead.
+        return;
+      }
+      newLoadingPeriodIndex = loadingPeriodHolder.index + 1;
+    }
+
+    if (newLoadingPeriodIndex >= timeline.getPeriodCount()) {
+      // The next period is not available yet.
+      mediaSource.maybeThrowSourceInfoRefreshError();
+      return;
+    }
+
+    long newLoadingPeriodStartPositionUs;
+    if (loadingPeriodHolder == null) {
+      newLoadingPeriodStartPositionUs = playbackInfo.positionUs;
+    } else {
+      int newLoadingWindowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex;
+      if (newLoadingPeriodIndex
+          != timeline.getWindow(newLoadingWindowIndex, window).firstPeriodIndex) {
+        // We're starting to buffer a new period in the current window. Always start from the
+        // beginning of the period.
+        newLoadingPeriodStartPositionUs = 0;
+      } else {
+        // We're starting to buffer a new window. When playback transitions to this window we'll
+        // want it to be from its default start position. The expected delay until playback
+        // transitions is equal the duration of media that's currently buffered (assuming no
+        // interruptions). Hence we project the default start position forward by the duration of
+        // the buffer, and start buffering from this point.
+        long defaultPositionProjectionUs = loadingPeriodHolder.getRendererOffset()
+            + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()
+            - rendererPositionUs;
+        Pair<Integer, Long> defaultPosition = getPeriodPosition(timeline, newLoadingWindowIndex,
+            C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs));
+        if (defaultPosition == null) {
+          return;
+        }
+
+        newLoadingPeriodIndex = defaultPosition.first;
+        newLoadingPeriodStartPositionUs = defaultPosition.second;
+      }
+    }
+
+    long rendererPositionOffsetUs = loadingPeriodHolder == null
+        ? newLoadingPeriodStartPositionUs + RENDERER_TIMESTAMP_OFFSET_US
+        : (loadingPeriodHolder.getRendererOffset()
+            + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs());
+    timeline.getPeriod(newLoadingPeriodIndex, period, true);
+    boolean isLastPeriod = newLoadingPeriodIndex == timeline.getPeriodCount() - 1
+        && !timeline.getWindow(period.windowIndex, window).isDynamic;
+    MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities,
+        rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid,
+        newLoadingPeriodIndex, isLastPeriod, newLoadingPeriodStartPositionUs);
+    if (loadingPeriodHolder != null) {
+      loadingPeriodHolder.next = newPeriodHolder;
+    }
+    loadingPeriodHolder = newPeriodHolder;
+    loadingPeriodHolder.mediaPeriod.prepare(this);
+    setIsLoading(true);
+  }
+
+  private void handlePeriodPrepared(MediaPeriod period) throws ExoPlaybackException {
+    if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) {
+      // Stale event.
+      return;
+    }
+    loadingPeriodHolder.handlePrepared();
+    if (playingPeriodHolder == null) {
+      // This is the first prepared period, so start playing it.
+      readingPeriodHolder = loadingPeriodHolder;
+      resetRendererPosition(readingPeriodHolder.startPositionUs);
+      setPlayingPeriodHolder(readingPeriodHolder);
+    }
+    maybeContinueLoading();
+  }
+
+  private void handleContinueLoadingRequested(MediaPeriod period) {
+    if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) {
+      // Stale event.
+      return;
+    }
+    maybeContinueLoading();
+  }
+
+  private void maybeContinueLoading() {
+    long nextLoadPositionUs = !loadingPeriodHolder.prepared ? 0
+        : loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs();
+    if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
+      setIsLoading(false);
+    } else {
+      long loadingPeriodPositionUs = loadingPeriodHolder.toPeriodTime(rendererPositionUs);
+      long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs;
+      boolean continueLoading = loadControl.shouldContinueLoading(bufferedDurationUs);
+      setIsLoading(continueLoading);
+      if (continueLoading) {
+        loadingPeriodHolder.needsContinueLoading = false;
+        loadingPeriodHolder.mediaPeriod.continueLoading(loadingPeriodPositionUs);
+      } else {
+        loadingPeriodHolder.needsContinueLoading = true;
+      }
+    }
+  }
+
+  private void releasePeriodHoldersFrom(MediaPeriodHolder periodHolder) {
+    while (periodHolder != null) {
+      periodHolder.release();
+      periodHolder = periodHolder.next;
+    }
+  }
+
+  private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) throws ExoPlaybackException {
+    if (playingPeriodHolder == periodHolder) {
+      return;
+    }
+
+    int enabledRendererCount = 0;
+    boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
+    for (int i = 0; i < renderers.length; i++) {
+      Renderer renderer = renderers[i];
+      rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;
+      TrackSelection newSelection = periodHolder.trackSelectorResult.selections.get(i);
+      if (newSelection != null) {
+        enabledRendererCount++;
+      }
+      if (rendererWasEnabledFlags[i] && (newSelection == null
+          || (renderer.isCurrentStreamFinal()
+          && renderer.getStream() == playingPeriodHolder.sampleStreams[i]))) {
+        // The renderer should be disabled before playing the next period, either because it's not
+        // needed to play the next period, or because we need to re-enable it as its current stream
+        // is final and it's not reading ahead.
+        if (renderer == rendererMediaClockSource) {
+          // Sync standaloneMediaClock so that it can take over timing responsibilities.
+          standaloneMediaClock.synchronize(rendererMediaClock);
+          rendererMediaClock = null;
+          rendererMediaClockSource = null;
+        }
+        ensureStopped(renderer);
+        renderer.disable();
+      }
+    }
+
+    playingPeriodHolder = periodHolder;
+    eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult).sendToTarget();
+    enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
+  }
+
+  private void enableRenderers(boolean[] rendererWasEnabledFlags, int enabledRendererCount)
+      throws ExoPlaybackException {
+    enabledRenderers = new Renderer[enabledRendererCount];
+    enabledRendererCount = 0;
+    for (int i = 0; i < renderers.length; i++) {
+      Renderer renderer = renderers[i];
+      TrackSelection newSelection = playingPeriodHolder.trackSelectorResult.selections.get(i);
+      if (newSelection != null) {
+        enabledRenderers[enabledRendererCount++] = renderer;
+        if (renderer.getState() == Renderer.STATE_DISABLED) {
+          RendererConfiguration rendererConfiguration =
+              playingPeriodHolder.trackSelectorResult.rendererConfigurations[i];
+          // The renderer needs enabling with its new track selection.
+          boolean playing = playWhenReady && state == ExoPlayer.STATE_READY;
+          // Consider as joining only if the renderer was previously disabled.
+          boolean joining = !rendererWasEnabledFlags[i] && playing;
+          // Build an array of formats contained by the selection.
+          Format[] formats = new Format[newSelection.length()];
+          for (int j = 0; j < formats.length; j++) {
+            formats[j] = newSelection.getFormat(j);
+          }
+          // Enable the renderer.
+          renderer.enable(rendererConfiguration, formats, playingPeriodHolder.sampleStreams[i],
+              rendererPositionUs, joining, playingPeriodHolder.getRendererOffset());
+          MediaClock mediaClock = renderer.getMediaClock();
+          if (mediaClock != null) {
+            if (rendererMediaClock != null) {
+              throw ExoPlaybackException.createForUnexpected(
+                  new IllegalStateException("Multiple renderer media clocks enabled."));
+            }
+            rendererMediaClock = mediaClock;
+            rendererMediaClockSource = renderer;
+            rendererMediaClock.setPlaybackParameters(playbackParameters);
+          }
+          // Start the renderer if playing.
+          if (playing) {
+            renderer.start();
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Holds a {@link MediaPeriod} with information required to play it as part of a timeline.
+   */
+  private static final class MediaPeriodHolder {
+
+    public final MediaPeriod mediaPeriod;
+    public final Object uid;
+    public final SampleStream[] sampleStreams;
+    public final boolean[] mayRetainStreamFlags;
+    public final long rendererPositionOffsetUs;
+
+    public int index;
+    public long startPositionUs;
+    public boolean isLast;
+    public boolean prepared;
+    public boolean hasEnabledTracks;
+    public MediaPeriodHolder next;
+    public boolean needsContinueLoading;
+    public TrackSelectorResult trackSelectorResult;
+
+    private final Renderer[] renderers;
+    private final RendererCapabilities[] rendererCapabilities;
+    private final TrackSelector trackSelector;
+    private final LoadControl loadControl;
+    private final MediaSource mediaSource;
+
+    private TrackSelectorResult periodTrackSelectorResult;
+
+    public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities,
+        long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl,
+        MediaSource mediaSource, Object periodUid, int periodIndex, boolean isLastPeriod,
+        long startPositionUs) {
+      this.renderers = renderers;
+      this.rendererCapabilities = rendererCapabilities;
+      this.rendererPositionOffsetUs = rendererPositionOffsetUs;
+      this.trackSelector = trackSelector;
+      this.loadControl = loadControl;
+      this.mediaSource = mediaSource;
+      this.uid = Assertions.checkNotNull(periodUid);
+      this.index = periodIndex;
+      this.isLast = isLastPeriod;
+      this.startPositionUs = startPositionUs;
+      sampleStreams = new SampleStream[renderers.length];
+      mayRetainStreamFlags = new boolean[renderers.length];
+      mediaPeriod = mediaSource.createPeriod(periodIndex, loadControl.getAllocator(),
+          startPositionUs);
+    }
+
+    public long toRendererTime(long periodTimeUs) {
+      return periodTimeUs + getRendererOffset();
+    }
+
+    public long toPeriodTime(long rendererTimeUs) {
+      return rendererTimeUs - getRendererOffset();
+    }
+
+    public long getRendererOffset() {
+      return rendererPositionOffsetUs - startPositionUs;
+    }
+
+    public void setIndex(int index, boolean isLast) {
+      this.index = index;
+      this.isLast = isLast;
+    }
+
+    public boolean isFullyBuffered() {
+      return prepared
+          && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE);
+    }
+
+    public void handlePrepared() throws ExoPlaybackException {
+      prepared = true;
+      selectTracks();
+      startPositionUs = updatePeriodTrackSelection(startPositionUs, false);
+    }
+
+    public boolean selectTracks() throws ExoPlaybackException {
+      TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities,
+          mediaPeriod.getTrackGroups());
+      if (selectorResult.isEquivalent(periodTrackSelectorResult)) {
+        return false;
+      }
+      trackSelectorResult = selectorResult;
+      return true;
+    }
+
+    public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams) {
+      return updatePeriodTrackSelection(positionUs, forceRecreateStreams,
+          new boolean[renderers.length]);
+    }
+
+    public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams,
+        boolean[] streamResetFlags) {
+      TrackSelectionArray trackSelections = trackSelectorResult.selections;
+      for (int i = 0; i < trackSelections.length; i++) {
+        mayRetainStreamFlags[i] = !forceRecreateStreams
+            && trackSelectorResult.isEquivalent(periodTrackSelectorResult, i);
+      }
+
+      // Disable streams on the period and get new streams for updated/newly-enabled tracks.
+      positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags,
+          sampleStreams, streamResetFlags, positionUs);
+      periodTrackSelectorResult = trackSelectorResult;
+
+      // Update whether we have enabled tracks and sanity check the expected streams are non-null.
+      hasEnabledTracks = false;
+      for (int i = 0; i < sampleStreams.length; i++) {
+        if (sampleStreams[i] != null) {
+          Assertions.checkState(trackSelections.get(i) != null);
+          hasEnabledTracks = true;
+        } else {
+          Assertions.checkState(trackSelections.get(i) == null);
+        }
+      }
+
+      // The track selection has changed.
+      loadControl.onTracksSelected(renderers, trackSelectorResult.groups, trackSelections);
+      return positionUs;
+    }
+
+    public void release() {
+      try {
+        mediaSource.releasePeriod(mediaPeriod);
+      } catch (RuntimeException e) {
+        // There's nothing we can do.
+        Log.e(TAG, "Period release failed.", e);
+      }
+    }
+
+  }
+
+  private static final class SeekPosition {
+
+    public final Timeline timeline;
+    public final int windowIndex;
+    public final long windowPositionUs;
+
+    public SeekPosition(Timeline timeline, int windowIndex, long windowPositionUs) {
+      this.timeline = timeline;
+      this.windowIndex = windowIndex;
+      this.windowPositionUs = windowPositionUs;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+/**
+ * Information about the ExoPlayer library.
+ */
+public interface ExoPlayerLibraryInfo {
+
+  /**
+   * The version of the library expressed as a string, for example "1.2.3".
+   */
+  // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
+  String VERSION = "2.4.0";
+
+  /**
+   * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
+   */
+  // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
+  String VERSION_SLASHY = "ExoPlayerLib/2.4.0";
+
+  /**
+   * The version of the library expressed as an integer, for example 1002003.
+   * <p>
+   * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
+   * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
+   * integer version 123045006 (123-045-006).
+   */
+  // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
+  int VERSION_INT = 2004000;
+
+  /**
+   * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
+   * checks enabled.
+   */
+  boolean ASSERTIONS_ENABLED = true;
+
+  /**
+   * Whether the library was compiled with {@link com.google.android.exoplayer2.util.TraceUtil}
+   * trace enabled.
+   */
+  boolean TRACE_ENABLED = true;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/Format.java
@@ -0,0 +1,723 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.MediaFormat;
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.ColorInfo;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Representation of a media format.
+ */
+public final class Format implements Parcelable {
+
+  /**
+   * A value for various fields to indicate that the field's value is unknown or not applicable.
+   */
+  public static final int NO_VALUE = -1;
+
+  /**
+   * A value for {@link #subsampleOffsetUs} to indicate that subsample timestamps are relative to
+   * the timestamps of their parent samples.
+   */
+  public static final long OFFSET_SAMPLE_RELATIVE = Long.MAX_VALUE;
+
+  /**
+   * An identifier for the format, or null if unknown or not applicable.
+   */
+  public final String id;
+  /**
+   * The average bandwidth in bits per second, or {@link #NO_VALUE} if unknown or not applicable.
+   */
+  public final int bitrate;
+  /**
+   * Codecs of the format as described in RFC 6381, or null if unknown or not applicable.
+   */
+  public final String codecs;
+  /**
+   * Metadata, or null if unknown or not applicable.
+   */
+  public final Metadata metadata;
+
+  // Container specific.
+
+  /**
+   * The mime type of the container, or null if unknown or not applicable.
+   */
+  public final String containerMimeType;
+
+  // Elementary stream specific.
+
+  /**
+   * The mime type of the elementary stream (i.e. the individual samples), or null if unknown or not
+   * applicable.
+   */
+  public final String sampleMimeType;
+  /**
+   * The maximum size of a buffer of data (typically one sample), or {@link #NO_VALUE} if unknown or
+   * not applicable.
+   */
+  public final int maxInputSize;
+  /**
+   * Initialization data that must be provided to the decoder. Will not be null, but may be empty
+   * if initialization data is not required.
+   */
+  public final List<byte[]> initializationData;
+  /**
+   * DRM initialization data if the stream is protected, or null otherwise.
+   */
+  public final DrmInitData drmInitData;
+
+  // Video specific.
+
+  /**
+   * The width of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable.
+   */
+  public final int width;
+  /**
+   * The height of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable.
+   */
+  public final int height;
+  /**
+   * The frame rate in frames per second, or {@link #NO_VALUE} if unknown or not applicable.
+   */
+  public final float frameRate;
+  /**
+   * The clockwise rotation that should be applied to the video for it to be rendered in the correct
+   * orientation, or {@link #NO_VALUE} if unknown or not applicable. Only 0, 90, 180 and 270 are
+   * supported.
+   */
+  public final int rotationDegrees;
+  /**
+   * The width to height ratio of pixels in the video, or {@link #NO_VALUE} if unknown or not
+   * applicable.
+   */
+  public final float pixelWidthHeightRatio;
+  /**
+   * The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo
+   * modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link
+   * C#STEREO_MODE_LEFT_RIGHT}, {@link C#STEREO_MODE_STEREO_MESH}.
+   */
+  @C.StereoMode
+  public final int stereoMode;
+  /**
+   * The projection data for 360/VR video, or null if not applicable.
+   */
+  public final byte[] projectionData;
+  /**
+   * The color metadata associated with the video, helps with accurate color reproduction.
+   */
+  public final ColorInfo colorInfo;
+
+  // Audio specific.
+
+  /**
+   * The number of audio channels, or {@link #NO_VALUE} if unknown or not applicable.
+   */
+  public final int channelCount;
+  /**
+   * The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable.
+   */
+  public final int sampleRate;
+  /**
+   * The encoding for PCM audio streams. If {@link #sampleMimeType} is {@link MimeTypes#AUDIO_RAW}
+   * then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT},
+   * {@link C#ENCODING_PCM_24BIT} and {@link C#ENCODING_PCM_32BIT}. Set to {@link #NO_VALUE} for
+   * other media types.
+   */
+  @C.PcmEncoding
+  public final int pcmEncoding;
+  /**
+   * The number of samples to trim from the start of the decoded audio stream.
+   */
+  public final int encoderDelay;
+  /**
+   * The number of samples to trim from the end of the decoded audio stream.
+   */
+  public final int encoderPadding;
+
+  // Text specific.
+
+  /**
+   * For samples that contain subsamples, this is an offset that should be added to subsample
+   * timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are
+   * relative to the timestamps of their parent samples.
+   */
+  public final long subsampleOffsetUs;
+
+  // Audio and text specific.
+
+  /**
+   * Track selection flags.
+   */
+  @C.SelectionFlags
+  public final int selectionFlags;
+
+  /**
+   * The language, or null if unknown or not applicable.
+   */
+  public final String language;
+
+  /**
+   * The Accessibility channel, or {@link #NO_VALUE} if not known or applicable.
+   */
+  public final int accessibilityChannel;
+
+  // Lazily initialized hashcode.
+  private int hashCode;
+
+  // Video.
+
+  public static Format createVideoContainerFormat(String id, String containerMimeType,
+      String sampleMimeType, String codecs, int bitrate, int width, int height,
+      float frameRate, List<byte[]> initializationData, @C.SelectionFlags int selectionFlags) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width,
+        height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, selectionFlags, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
+        initializationData, null, null);
+  }
+
+  public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, int maxInputSize, int width, int height, float frameRate,
+      List<byte[]> initializationData, DrmInitData drmInitData) {
+    return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width,
+        height, frameRate, initializationData, NO_VALUE, NO_VALUE, drmInitData);
+  }
+
+  public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, int maxInputSize, int width, int height, float frameRate,
+      List<byte[]> initializationData, int rotationDegrees, float pixelWidthHeightRatio,
+      DrmInitData drmInitData) {
+    return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width,
+        height, frameRate, initializationData, rotationDegrees, pixelWidthHeightRatio, null,
+        NO_VALUE, null, drmInitData);
+  }
+
+  public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, int maxInputSize, int width, int height, float frameRate,
+      List<byte[]> initializationData, int rotationDegrees, float pixelWidthHeightRatio,
+      byte[] projectionData, @C.StereoMode int stereoMode, ColorInfo colorInfo,
+      DrmInitData drmInitData) {
+    return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height,
+        frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
+        colorInfo, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE,
+        OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, null);
+  }
+
+  // Audio.
+
+  public static Format createAudioContainerFormat(String id, String containerMimeType,
+      String sampleMimeType, String codecs, int bitrate, int channelCount, int sampleRate,
+      List<byte[]> initializationData, @C.SelectionFlags int selectionFlags, String language) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate,
+        NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
+        initializationData, null, null);
+  }
+
+  public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, int maxInputSize, int channelCount, int sampleRate,
+      List<byte[]> initializationData, DrmInitData drmInitData,
+      @C.SelectionFlags int selectionFlags, String language) {
+    return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount,
+        sampleRate, NO_VALUE, initializationData, drmInitData, selectionFlags, language);
+  }
+
+  public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, int maxInputSize, int channelCount, int sampleRate,
+      @C.PcmEncoding int pcmEncoding, List<byte[]> initializationData, DrmInitData drmInitData,
+      @C.SelectionFlags int selectionFlags, String language) {
+    return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount,
+        sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, initializationData, drmInitData,
+        selectionFlags, language, null);
+  }
+
+  public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, int maxInputSize, int channelCount, int sampleRate,
+      @C.PcmEncoding int pcmEncoding, int encoderDelay, int encoderPadding,
+      List<byte[]> initializationData, DrmInitData drmInitData,
+      @C.SelectionFlags int selectionFlags, String language, Metadata metadata) {
+    return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, channelCount, sampleRate, pcmEncoding,
+        encoderDelay, encoderPadding, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
+        initializationData, drmInitData, metadata);
+  }
+
+  // Text.
+
+  public static Format createTextContainerFormat(String id, String containerMimeType,
+      String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
+      String language) {
+    return createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate,
+        selectionFlags, language, NO_VALUE);
+  }
+
+  public static Format createTextContainerFormat(String id, String containerMimeType,
+      String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
+      String language, int accessibilityChannel) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, selectionFlags, language, accessibilityChannel,
+        OFFSET_SAMPLE_RELATIVE, null, null, null);
+  }
+
+  public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) {
+    return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
+        NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.<byte[]>emptyList());
+  }
+
+  public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel,
+      DrmInitData drmInitData) {
+    return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
+        accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE, Collections.<byte[]>emptyList());
+  }
+
+  public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData,
+      long subsampleOffsetUs) {
+    return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
+        NO_VALUE, drmInitData, subsampleOffsetUs, Collections.<byte[]>emptyList());
+  }
+
+  public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, @C.SelectionFlags int selectionFlags, String language,
+      int accessibilityChannel, DrmInitData drmInitData, long subsampleOffsetUs,
+      List<byte[]> initializationData) {
+    return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
+        initializationData, drmInitData, null);
+  }
+
+  // Image.
+
+  public static Format createImageSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, List<byte[]> initializationData, String language, DrmInitData drmInitData) {
+    return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, 0, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData,
+        null);
+  }
+
+  // Generic.
+
+  public static Format createContainerFormat(String id, String containerMimeType,
+      String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
+      String language) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null,
+        null);
+  }
+
+  public static Format createSampleFormat(String id, String sampleMimeType,
+      long subsampleOffsetUs) {
+    return new Format(id, null, sampleMimeType, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, 0, null, NO_VALUE, subsampleOffsetUs, null, null, null);
+  }
+
+  public static Format createSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, DrmInitData drmInitData) {
+    return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null);
+  }
+
+  /* package */ Format(String id, String containerMimeType, String sampleMimeType, String codecs,
+      int bitrate, int maxInputSize, int width, int height, float frameRate, int rotationDegrees,
+      float pixelWidthHeightRatio, byte[] projectionData, @C.StereoMode int stereoMode,
+      ColorInfo colorInfo, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding,
+      int encoderDelay, int encoderPadding, @C.SelectionFlags int selectionFlags, String language,
+      int accessibilityChannel, long subsampleOffsetUs, List<byte[]> initializationData,
+      DrmInitData drmInitData, Metadata metadata) {
+    this.id = id;
+    this.containerMimeType = containerMimeType;
+    this.sampleMimeType = sampleMimeType;
+    this.codecs = codecs;
+    this.bitrate = bitrate;
+    this.maxInputSize = maxInputSize;
+    this.width = width;
+    this.height = height;
+    this.frameRate = frameRate;
+    this.rotationDegrees = rotationDegrees;
+    this.pixelWidthHeightRatio = pixelWidthHeightRatio;
+    this.projectionData = projectionData;
+    this.stereoMode = stereoMode;
+    this.colorInfo = colorInfo;
+    this.channelCount = channelCount;
+    this.sampleRate = sampleRate;
+    this.pcmEncoding = pcmEncoding;
+    this.encoderDelay = encoderDelay;
+    this.encoderPadding = encoderPadding;
+    this.selectionFlags = selectionFlags;
+    this.language = language;
+    this.accessibilityChannel = accessibilityChannel;
+    this.subsampleOffsetUs = subsampleOffsetUs;
+    this.initializationData = initializationData == null ? Collections.<byte[]>emptyList()
+        : initializationData;
+    this.drmInitData = drmInitData;
+    this.metadata = metadata;
+  }
+
+  @SuppressWarnings("ResourceType")
+  /* package */ Format(Parcel in) {
+    id = in.readString();
+    containerMimeType = in.readString();
+    sampleMimeType = in.readString();
+    codecs = in.readString();
+    bitrate = in.readInt();
+    maxInputSize = in.readInt();
+    width = in.readInt();
+    height = in.readInt();
+    frameRate = in.readFloat();
+    rotationDegrees = in.readInt();
+    pixelWidthHeightRatio = in.readFloat();
+    boolean hasProjectionData = in.readInt() != 0;
+    projectionData = hasProjectionData ? in.createByteArray() : null;
+    stereoMode = in.readInt();
+    colorInfo = in.readParcelable(ColorInfo.class.getClassLoader());
+    channelCount = in.readInt();
+    sampleRate = in.readInt();
+    pcmEncoding = in.readInt();
+    encoderDelay = in.readInt();
+    encoderPadding = in.readInt();
+    selectionFlags = in.readInt();
+    language = in.readString();
+    accessibilityChannel = in.readInt();
+    subsampleOffsetUs = in.readLong();
+    int initializationDataSize = in.readInt();
+    initializationData = new ArrayList<>(initializationDataSize);
+    for (int i = 0; i < initializationDataSize; i++) {
+      initializationData.add(in.createByteArray());
+    }
+    drmInitData = in.readParcelable(DrmInitData.class.getClassLoader());
+    metadata = in.readParcelable(Metadata.class.getClassLoader());
+  }
+
+  public Format copyWithMaxInputSize(int maxInputSize) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+        width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+        stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
+        encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
+        initializationData, drmInitData, metadata);
+  }
+
+  public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+        width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+        stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
+        encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
+        initializationData, drmInitData, metadata);
+  }
+
+  public Format copyWithContainerInfo(String id, String codecs, int bitrate, int width, int height,
+      @C.SelectionFlags int selectionFlags, String language) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+        width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+        stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
+        encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
+        initializationData, drmInitData, metadata);
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  public Format copyWithManifestFormatInfo(Format manifestFormat) {
+    if (this == manifestFormat) {
+      // No need to copy from ourselves.
+      return this;
+    }
+    String id = manifestFormat.id;
+    String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs;
+    int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate;
+    float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate;
+    @C.SelectionFlags int selectionFlags = this.selectionFlags |  manifestFormat.selectionFlags;
+    String language = this.language == null ? manifestFormat.language : this.language;
+    DrmInitData drmInitData = manifestFormat.drmInitData != null ? manifestFormat.drmInitData
+        : this.drmInitData;
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
+        height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
+        colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
+        selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
+        drmInitData, metadata);
+  }
+
+  public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+        width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+        stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
+        encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
+        initializationData, drmInitData, metadata);
+  }
+
+  public Format copyWithDrmInitData(DrmInitData drmInitData) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+        width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+        stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
+        encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
+        initializationData, drmInitData, metadata);
+  }
+
+  public Format copyWithMetadata(Metadata metadata) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+        width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+        stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay,
+        encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs,
+        initializationData, drmInitData, metadata);
+  }
+
+  /**
+   * Returns the number of pixels if this is a video format whose {@link #width} and {@link #height}
+   * are known, or {@link #NO_VALUE} otherwise
+   */
+  public int getPixelCount() {
+    return width == NO_VALUE || height == NO_VALUE ? NO_VALUE : (width * height);
+  }
+
+  /**
+   * Returns a {@link MediaFormat} representation of this format.
+   */
+  @SuppressLint("InlinedApi")
+  @TargetApi(16)
+  public final MediaFormat getFrameworkMediaFormatV16() {
+    MediaFormat format = new MediaFormat();
+    format.setString(MediaFormat.KEY_MIME, sampleMimeType);
+    maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language);
+    maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
+    maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width);
+    maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height);
+    maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate);
+    maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees);
+    maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount);
+    maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate);
+    maybeSetIntegerV16(format, "encoder-delay", encoderDelay);
+    maybeSetIntegerV16(format, "encoder-padding", encoderPadding);
+    for (int i = 0; i < initializationData.size(); i++) {
+      format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
+    }
+    maybeSetColorInfoV24(format, colorInfo);
+    return format;
+  }
+
+  @Override
+  public String toString() {
+    return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", "
+        + language + ", [" + width + ", " + height + ", " + frameRate + "]"
+        + ", [" + channelCount + ", " + sampleRate + "])";
+  }
+
+  @Override
+  public int hashCode() {
+    if (hashCode == 0) {
+      int result = 17;
+      result = 31 * result + (id == null ? 0 : id.hashCode());
+      result = 31 * result + (containerMimeType == null ? 0 : containerMimeType.hashCode());
+      result = 31 * result + (sampleMimeType == null ? 0 : sampleMimeType.hashCode());
+      result = 31 * result + (codecs == null ? 0 : codecs.hashCode());
+      result = 31 * result + bitrate;
+      result = 31 * result + width;
+      result = 31 * result + height;
+      result = 31 * result + channelCount;
+      result = 31 * result + sampleRate;
+      result = 31 * result + (language == null ? 0 : language.hashCode());
+      result = 31 * result + accessibilityChannel;
+      result = 31 * result + (drmInitData == null ? 0 : drmInitData.hashCode());
+      result = 31 * result + (metadata == null ? 0 : metadata.hashCode());
+      hashCode = result;
+    }
+    return hashCode;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null || getClass() != obj.getClass()) {
+      return false;
+    }
+    Format other = (Format) obj;
+    if (bitrate != other.bitrate || maxInputSize != other.maxInputSize
+        || width != other.width || height != other.height || frameRate != other.frameRate
+        || rotationDegrees != other.rotationDegrees
+        || pixelWidthHeightRatio != other.pixelWidthHeightRatio || stereoMode != other.stereoMode
+        || channelCount != other.channelCount || sampleRate != other.sampleRate
+        || pcmEncoding != other.pcmEncoding || encoderDelay != other.encoderDelay
+        || encoderPadding != other.encoderPadding || subsampleOffsetUs != other.subsampleOffsetUs
+        || selectionFlags != other.selectionFlags || !Util.areEqual(id, other.id)
+        || !Util.areEqual(language, other.language)
+        || accessibilityChannel != other.accessibilityChannel
+        || !Util.areEqual(containerMimeType, other.containerMimeType)
+        || !Util.areEqual(sampleMimeType, other.sampleMimeType)
+        || !Util.areEqual(codecs, other.codecs)
+        || !Util.areEqual(drmInitData, other.drmInitData)
+        || !Util.areEqual(metadata, other.metadata)
+        || !Util.areEqual(colorInfo, other.colorInfo)
+        || !Arrays.equals(projectionData, other.projectionData)
+        || initializationData.size() != other.initializationData.size()) {
+      return false;
+    }
+    for (int i = 0; i < initializationData.size(); i++) {
+      if (!Arrays.equals(initializationData.get(i), other.initializationData.get(i))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @TargetApi(24)
+  private static void maybeSetColorInfoV24(MediaFormat format, ColorInfo colorInfo) {
+    if (colorInfo == null) {
+      return;
+    }
+    maybeSetIntegerV16(format, "color-transfer", colorInfo.colorTransfer);
+    maybeSetIntegerV16(format, "color-standard", colorInfo.colorSpace);
+    maybeSetIntegerV16(format, "color-range", colorInfo.colorRange);
+    maybeSetByteBufferV16(format, "hdr-static-info", colorInfo.hdrStaticInfo);
+  }
+
+  @TargetApi(16)
+  private static void maybeSetStringV16(MediaFormat format, String key, String value) {
+    if (value != null) {
+      format.setString(key, value);
+    }
+  }
+
+  @TargetApi(16)
+  private static void maybeSetIntegerV16(MediaFormat format, String key, int value) {
+    if (value != NO_VALUE) {
+      format.setInteger(key, value);
+    }
+  }
+
+  @TargetApi(16)
+  private static void maybeSetFloatV16(MediaFormat format, String key, float value) {
+    if (value != NO_VALUE) {
+      format.setFloat(key, value);
+    }
+  }
+
+  @TargetApi(16)
+  private static void maybeSetByteBufferV16(MediaFormat format, String key, byte[] value) {
+    if (value != null) {
+      format.setByteBuffer(key, ByteBuffer.wrap(value));
+    }
+  }
+
+  // Utility methods
+
+  /**
+   * Returns a prettier {@link String} than {@link #toString()}, intended for logging.
+   */
+  public static String toLogString(Format format) {
+    if (format == null) {
+      return "null";
+    }
+    StringBuilder builder = new StringBuilder();
+    builder.append("id=").append(format.id).append(", mimeType=").append(format.sampleMimeType);
+    if (format.bitrate != Format.NO_VALUE) {
+      builder.append(", bitrate=").append(format.bitrate);
+    }
+    if (format.width != Format.NO_VALUE && format.height != Format.NO_VALUE) {
+      builder.append(", res=").append(format.width).append("x").append(format.height);
+    }
+    if (format.frameRate != Format.NO_VALUE) {
+      builder.append(", fps=").append(format.frameRate);
+    }
+    if (format.channelCount != Format.NO_VALUE) {
+      builder.append(", channels=").append(format.channelCount);
+    }
+    if (format.sampleRate != Format.NO_VALUE) {
+      builder.append(", sample_rate=").append(format.sampleRate);
+    }
+    if (format.language != null) {
+      builder.append(", language=").append(format.language);
+    }
+    return builder.toString();
+  }
+
+  // Parcelable implementation.
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+  @Override
+  public void writeToParcel(Parcel dest, int flags) {
+    dest.writeString(id);
+    dest.writeString(containerMimeType);
+    dest.writeString(sampleMimeType);
+    dest.writeString(codecs);
+    dest.writeInt(bitrate);
+    dest.writeInt(maxInputSize);
+    dest.writeInt(width);
+    dest.writeInt(height);
+    dest.writeFloat(frameRate);
+    dest.writeInt(rotationDegrees);
+    dest.writeFloat(pixelWidthHeightRatio);
+    dest.writeInt(projectionData != null ? 1 : 0);
+    if (projectionData != null) {
+      dest.writeByteArray(projectionData);
+    }
+    dest.writeInt(stereoMode);
+    dest.writeParcelable(colorInfo, flags);
+    dest.writeInt(channelCount);
+    dest.writeInt(sampleRate);
+    dest.writeInt(pcmEncoding);
+    dest.writeInt(encoderDelay);
+    dest.writeInt(encoderPadding);
+    dest.writeInt(selectionFlags);
+    dest.writeString(language);
+    dest.writeInt(accessibilityChannel);
+    dest.writeLong(subsampleOffsetUs);
+    int initializationDataSize = initializationData.size();
+    dest.writeInt(initializationDataSize);
+    for (int i = 0; i < initializationDataSize; i++) {
+      dest.writeByteArray(initializationData.get(i));
+    }
+    dest.writeParcelable(drmInitData, 0);
+    dest.writeParcelable(metadata, 0);
+  }
+
+  public static final Creator<Format> CREATOR = new Creator<Format>() {
+
+    @Override
+    public Format createFromParcel(Parcel in) {
+      return new Format(in);
+    }
+
+    @Override
+    public Format[] newArray(int size) {
+      return new Format[size];
+    }
+
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/FormatHolder.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+/**
+ * Holds a {@link Format}.
+ */
+public final class FormatHolder {
+
+  /**
+   * The held {@link Format}.
+   */
+  public Format format;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/IllegalSeekPositionException.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+/**
+ * Thrown when an attempt is made to seek to a position that does not exist in the player's
+ * {@link Timeline}.
+ */
+public final class IllegalSeekPositionException extends IllegalStateException {
+
+  /**
+   * The {@link Timeline} in which the seek was attempted.
+   */
+  public final Timeline timeline;
+  /**
+   * The index of the window being seeked to.
+   */
+  public final int windowIndex;
+  /**
+   * The seek position in the specified window.
+   */
+  public final long positionMs;
+
+  /**
+   * @param timeline The {@link Timeline} in which the seek was attempted.
+   * @param windowIndex The index of the window being seeked to.
+   * @param positionMs The seek position in the specified window.
+   */
+  public IllegalSeekPositionException(Timeline timeline, int windowIndex, long positionMs) {
+    this.timeline = timeline;
+    this.windowIndex = windowIndex;
+    this.positionMs = positionMs;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/LoadControl.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.upstream.Allocator;
+
+/**
+ * Controls buffering of media.
+ */
+public interface LoadControl {
+
+  /**
+   * Called by the player when prepared with a new source.
+   */
+  void onPrepared();
+
+  /**
+   * Called by the player when a track selection occurs.
+   *
+   * @param renderers The renderers.
+   * @param trackGroups The {@link TrackGroup}s from which the selection was made.
+   * @param trackSelections The track selections that were made.
+   */
+  void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
+      TrackSelectionArray trackSelections);
+
+  /**
+   * Called by the player when stopped.
+   */
+  void onStopped();
+
+  /**
+   * Called by the player when released.
+   */
+  void onReleased();
+
+  /**
+   * Returns the {@link Allocator} that should be used to obtain media buffer allocations.
+   */
+  Allocator getAllocator();
+
+  /**
+   * Called by the player to determine whether sufficient media is buffered for playback to be
+   * started or resumed.
+   *
+   * @param bufferedDurationUs The duration of media that's currently buffered.
+   * @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by
+   *     buffer depletion rather than a user action. Hence this parameter is false during initial
+   *     buffering and when buffering as a result of a seek operation.
+   * @return Whether playback should be allowed to start or resume.
+   */
+  boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering);
+
+  /**
+   * Called by the player to determine whether it should continue to load the source.
+   *
+   * @param bufferedDurationUs The duration of media that's currently buffered.
+   * @return Whether the loading should continue.
+   */
+  boolean shouldContinueLoading(long bufferedDurationUs);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/ParserException.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import java.io.IOException;
+
+/**
+ * Thrown when an error occurs parsing media data and metadata.
+ */
+public class ParserException extends IOException {
+
+  public ParserException() {
+    super();
+  }
+
+  /**
+   * @param message The detail message for the exception.
+   */
+  public ParserException(String message) {
+    super(message);
+  }
+
+  /**
+   * @param cause The cause for the exception.
+   */
+  public ParserException(Throwable cause) {
+    super(cause);
+  }
+
+  /**
+   * @param message The detail message for the exception.
+   * @param cause The cause for the exception.
+   */
+  public ParserException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/PlaybackParameters.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+/**
+ * The parameters that apply to playback.
+ */
+public final class PlaybackParameters {
+
+  /**
+   * The default playback parameters: real-time playback with no pitch modification.
+   */
+  public static final PlaybackParameters DEFAULT = new PlaybackParameters(1f, 1f);
+
+  /**
+   * The factor by which playback will be sped up.
+   */
+  public final float speed;
+
+  /**
+   * The factor by which the audio pitch will be scaled.
+   */
+  public final float pitch;
+
+  private final int scaledUsPerMs;
+
+  /**
+   * Creates new playback parameters.
+   *
+   * @param speed The factor by which playback will be sped up.
+   * @param pitch The factor by which the audio pitch will be scaled.
+   */
+  public PlaybackParameters(float speed, float pitch) {
+    this.speed = speed;
+    this.pitch = pitch;
+    scaledUsPerMs = Math.round(speed * 1000f);
+  }
+
+  /**
+   * Scales the millisecond duration {@code timeMs} by the playback speed, returning the result in
+   * microseconds.
+   *
+   * @param timeMs The time to scale, in milliseconds.
+   * @return The scaled time, in microseconds.
+   */
+  public long getSpeedAdjustedDurationUs(long timeMs) {
+    return timeMs * scaledUsPerMs;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null || getClass() != obj.getClass()) {
+      return false;
+    }
+    PlaybackParameters other = (PlaybackParameters) obj;
+    return this.speed == other.speed && this.pitch == other.pitch;
+  }
+  
+  @Override
+  public int hashCode() {
+    int result = 17;
+    result = 31 * result + Float.floatToRawIntBits(speed);
+    result = 31 * result + Float.floatToRawIntBits(pitch);
+    return result;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/Renderer.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.util.MediaClock;
+import java.io.IOException;
+
+/**
+ * Renders media read from a {@link SampleStream}.
+ * <p>
+ * Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is
+ * transitioned through various states as the overall playback state changes. The valid state
+ * transitions are shown below, annotated with the methods that are called during each transition.
+ * <p align="center">
+ *   <img src="doc-files/renderer-states.svg" alt="Renderer state transitions">
+ * </p>
+ */
+public interface Renderer extends ExoPlayerComponent {
+
+  /**
+   * The renderer is disabled.
+   */
+  int STATE_DISABLED = 0;
+  /**
+   * The renderer is enabled but not started. A renderer in this state is not actively rendering
+   * media, but will typically hold resources that it requires for rendering (e.g. media decoders).
+   */
+  int STATE_ENABLED = 1;
+  /**
+   * The renderer is started. Calls to {@link #render(long, long)} will cause media to be rendered.
+   */
+  int STATE_STARTED = 2;
+
+  /**
+   * Returns the track type that the {@link Renderer} handles. For example, a video renderer will
+   * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a
+   * text renderer will return {@link C#TRACK_TYPE_TEXT}, and so on.
+   *
+   * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
+   */
+  int getTrackType();
+
+  /**
+   * Returns the capabilities of the renderer.
+   *
+   * @return The capabilities of the renderer.
+   */
+  RendererCapabilities getCapabilities();
+
+  /**
+   * Sets the index of this renderer within the player.
+   *
+   * @param index The renderer index.
+   */
+  void setIndex(int index);
+
+  /**
+   * If the renderer advances its own playback position then this method returns a corresponding
+   * {@link MediaClock}. If provided, the player will use the returned {@link MediaClock} as its
+   * source of time during playback. A player may have at most one renderer that returns a
+   * {@link MediaClock} from this method.
+   *
+   * @return The {@link MediaClock} tracking the playback position of the renderer, or null.
+   */
+  MediaClock getMediaClock();
+
+  /**
+   * Returns the current state of the renderer.
+   *
+   * @return The current state (one of the {@code STATE_*} constants).
+   */
+  int getState();
+
+  /**
+   * Enables the renderer to consume from the specified {@link SampleStream}.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_DISABLED}.
+   *
+   * @param configuration The renderer configuration.
+   * @param formats The enabled formats.
+   * @param stream The {@link SampleStream} from which the renderer should consume.
+   * @param positionUs The player's current position.
+   * @param joining Whether this renderer is being enabled to join an ongoing playback.
+   * @param offsetUs The offset to be added to timestamps of buffers read from {@code stream}
+   *     before they are rendered.
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  void enable(RendererConfiguration configuration, Format[] formats, SampleStream stream,
+      long positionUs, boolean joining, long offsetUs) throws ExoPlaybackException;
+
+  /**
+   * Starts the renderer, meaning that calls to {@link #render(long, long)} will cause media to be
+   * rendered.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}.
+   *
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  void start() throws ExoPlaybackException;
+
+  /**
+   * Replaces the {@link SampleStream} from which samples will be consumed.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+   *
+   * @param formats The enabled formats.
+   * @param stream The {@link SampleStream} from which the renderer should consume.
+   * @param offsetUs The offset to be added to timestamps of buffers read from {@code stream} before
+   *     they are rendered.
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  void replaceStream(Format[] formats, SampleStream stream, long offsetUs)
+      throws ExoPlaybackException;
+
+  /**
+   * Returns the {@link SampleStream} being consumed, or null if the renderer is disabled.
+   */
+  SampleStream getStream();
+
+  /**
+   * Returns whether the renderer has read the current {@link SampleStream} to the end.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+   */
+  boolean hasReadStreamToEnd();
+
+  /**
+   * Signals to the renderer that the current {@link SampleStream} will be the final one supplied
+   * before it is next disabled or reset.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+   */
+  void setCurrentStreamFinal();
+
+  /**
+   * Returns whether the current {@link SampleStream} will be the final one supplied before the
+   * renderer is next disabled or reset.
+   */
+  boolean isCurrentStreamFinal();
+
+  /**
+   * Throws an error that's preventing the renderer from reading from its {@link SampleStream}. Does
+   * nothing if no such error exists.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+   *
+   * @throws IOException An error that's preventing the renderer from making progress or buffering
+   *     more data.
+   */
+  void maybeThrowStreamError() throws IOException;
+
+  /**
+   * Signals to the renderer that a position discontinuity has occurred.
+   * <p>
+   * After a position discontinuity, the renderer's {@link SampleStream} is guaranteed to provide
+   * samples starting from a key frame.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+   *
+   * @param positionUs The new playback position in microseconds.
+   * @throws ExoPlaybackException If an error occurs handling the reset.
+   */
+  void resetPosition(long positionUs) throws ExoPlaybackException;
+
+  /**
+   * Incrementally renders the {@link SampleStream}.
+   * <p>
+   * If the renderer is in the {@link #STATE_ENABLED} state then each call to this method will do
+   * work toward being ready to render the {@link SampleStream} when the renderer is started. It may
+   * also render the very start of the media, for example the first frame of a video stream. If the
+   * renderer is in the {@link #STATE_STARTED} state then calls to this method will render the
+   * {@link SampleStream} in sync with the specified media positions.
+   * <p>
+   * This method should return quickly, and should not block if the renderer is unable to make
+   * useful progress.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+   *
+   * @param positionUs The current media time in microseconds, measured at the start of the
+   *     current iteration of the rendering loop.
+   * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
+   *     measured at the start of the current iteration of the rendering loop.
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException;
+
+  /**
+   * Whether the renderer is able to immediately render media from the current position.
+   * <p>
+   * If the renderer is in the {@link #STATE_STARTED} state then returning true indicates that the
+   * renderer has everything that it needs to continue playback. Returning false indicates that
+   * the player should pause until the renderer is ready.
+   * <p>
+   * If the renderer is in the {@link #STATE_ENABLED} state then returning true indicates that the
+   * renderer is ready for playback to be started. Returning false indicates that it is not.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+   *
+   * @return Whether the renderer is ready to render media.
+   */
+  boolean isReady();
+
+  /**
+   * Whether the renderer is ready for the {@link ExoPlayer} instance to transition to
+   * {@link ExoPlayer#STATE_ENDED}. The player will make this transition as soon as {@code true} is
+   * returned by all of its {@link Renderer}s.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
+   *
+   * @return Whether the renderer is ready for the player to transition to the ended state.
+   */
+  boolean isEnded();
+
+  /**