merge mozilla-inbound to mozilla-central. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Wed, 24 May 2017 11:25:03 +0200
changeset 360366 291a11111bdd05c5cd55dd552da4b1285ceba9b2
parent 360320 ffaa07672466b06cd748b07a34cf95377afdde41 (diff)
parent 360365 735c20e6a9b7e923c92cc0286643d46268928663 (current diff)
child 360367 744e9a4706a08b8e5b6484355e9531706539e351
child 360386 d93182f36b3c134a3b1c6718f09eb87c2913e364
child 360444 7fc3bfbb3e59ab0e065cc6160b4af9def63b40e6
child 361316 e4db9580e41b486ee1dbd603bd2e011003f5fb1f
push id43290
push userarchaeopteryx@coole-files.de
push dateWed, 24 May 2017 09:26:23 +0000
treeherderautoland@744e9a4706a0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge, merge
milestone55.0a1
first release with
nightly linux32
291a11111bdd / 55.0a1 / 20170524100215 / files
nightly linux64
291a11111bdd / 55.0a1 / 20170524100215 / files
nightly mac
291a11111bdd / 55.0a1 / 20170524030204 / files
nightly win32
291a11111bdd / 55.0a1 / 20170524030204 / files
nightly win64
291a11111bdd / 55.0a1 / 20170524030204 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge mozilla-inbound to mozilla-central. r=merge a=merge MozReview-Commit-ID: HGZE1dfSDNK
modules/libpref/init/all.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();
+
+  /**
+   * Stops the renderer, transitioning it to the {@link #STATE_ENABLED} state.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_STARTED}.
+   *
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  void stop() throws ExoPlaybackException;
+
+  /**
+   * Disable the renderer, transitioning it to the {@link #STATE_DISABLED} state.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}.
+   */
+  void disable();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/thirdparty/java/com/google/android/exoplayer2/RendererCapabilities.java
@@ -0,0 +1,140 @@
+/*
+ * 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.util.MimeTypes;
+
+/**
+ * Defines the capabilities of a {@link Renderer}.
+ */
+public interface RendererCapabilities {
+
+  /**
+   * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
+   * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES},
+   * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}.
+   */
+  int FORMAT_SUPPORT_MASK = 0b11;
+  /**
+   * The {@link Renderer} is capable of rendering the format.
+   */
+  int FORMAT_HANDLED = 0b11;
+  /**
+   * The {@link Renderer} is capable of rendering formats with the same mime type, but the
+   * properties of the format exceed the renderer's capability.
+   * <p>
+   * Example: The {@link Renderer} is capable of rendering H264 and the format's mime type is
+   * {@link MimeTypes#VIDEO_H264}, but the format's resolution exceeds the maximum limit supported
+   * by the underlying H264 decoder.
+   */
+  int FORMAT_EXCEEDS_CAPABILITIES = 0b10;
+  /**
+   * The {@link Renderer} is a general purpose renderer for formats of the same top-level type,
+   * but is not capable of rendering the format or any other format with the same mime type because
+   * the sub-type is not supported.
+   * <p>
+   * Example: The {@link Renderer} is a general purpose audio renderer and the format's
+   * mime type matches audio/[subtype], but there does not exist a suitable decoder for [subtype].
+   */
+  int FORMAT_UNSUPPORTED_SUBTYPE = 0b01;
+  /**
+   * The {@link Renderer} is not capable of rendering the format, either because it does not
+   * support the format's top-level type, or because it's a specialized renderer for a different
+   * mime type.
+   * <p>
+   * Example: The {@link Renderer} is a general purpose video renderer, but the format has an
+   * audio mime type.
+   */
+  int FORMAT_UNSUPPORTED_TYPE = 0b00;
+
+  /**
+   * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
+   * {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}.
+   */
+  int ADAPTIVE_SUPPORT_MASK = 0b1100;
+  /**
+   * The {@link Renderer} can seamlessly adapt between formats.
+   */
+  int ADAPTIVE_SEAMLESS = 0b1000;
+  /**
+   * The {@link Renderer} can adapt between formats, but may suffer a brief discontinuity
+   * (~50-100ms) when adaptation occurs.
+   */
+  int ADAPTIVE_NOT_SEAMLESS = 0b0100;
+  /**
+   * The {@link Renderer} does not support adaptation between formats.
+   */
+  int ADAPTIVE_NOT_SUPPORTED = 0b0000;
+
+  /**
+   * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
+   * {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}.
+   */
+  int TUNNELING_SUPPORT_MASK = 0b10000;
+  /**
+   * The {@link Renderer} supports tunneled output.
+   */
+  int TUNNELING_SUPPORTED = 0b10000;
+  /**
+   * The {@link Renderer} does not support tunneled output.
+   */
+  int TUNNELING_NOT_SUPPORTED = 0b00000;
+
+  /**
+   * 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.
+   *
+   * @see Renderer#getTrackType()
+   * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
+   */
+  int getTrackType();
+
+  /**
+   * Returns the extent to which the {@link Renderer} supports a given format. The returned value is
+   * the bitwise OR of three properties:
+   * <ul>
+   * <li>The level of support for the format itself. One of {@link #FORMAT_HANDLED},
+   * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_SUBTYPE} and
+   * {@link #FORMAT_UNSUPPORTED_TYPE}.</li>
+   * <li>The level of support for adapting from the format to another format of the same mime type.
+   * One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and
+   * {@link #ADAPTIVE_NOT_SUPPORTED}.</li>
+   * <li>The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and
+   * {@link #TUNNELING_NOT_SUPPORTED}.</li>
+   * </ul>
+   *