text: /**
   * @license
   * Copyright 2016 Google Inc.
   *
   * 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.
   */
  
  goog.provide('shaka.Player');
  
  goog.require('goog.asserts');
  goog.require('shaka.Deprecate');
  goog.require('shaka.ads.AdManager');
  goog.require('shaka.log');
  goog.require('shaka.media.ActiveStreamMap');
  goog.require('shaka.media.AdaptationSetCriteria');
  goog.require('shaka.media.BufferingObserver');
  goog.require('shaka.media.DrmEngine');
  goog.require('shaka.media.ManifestParser');
  goog.require('shaka.media.MediaSourceEngine');
  goog.require('shaka.media.MuxJSClosedCaptionParser');
  goog.require('shaka.media.NoopCaptionParser');
  goog.require('shaka.media.PeriodObserver');
  goog.require('shaka.media.PlayRateController');
  goog.require('shaka.media.Playhead');
  goog.require('shaka.media.PlayheadObserverManager');
  goog.require('shaka.media.PreferenceBasedCriteria');
  goog.require('shaka.media.RegionObserver');
  goog.require('shaka.media.RegionTimeline');
  goog.require('shaka.media.SegmentIndex');
  goog.require('shaka.media.StreamingEngine');
  goog.require('shaka.net.NetworkingEngine');
  goog.require('shaka.routing.Walker');
  goog.require('shaka.text.SimpleTextDisplayer');
  goog.require('shaka.util.ArrayUtils');
  goog.require('shaka.util.Error');
  goog.require('shaka.util.EventManager');
  goog.require('shaka.util.FakeEvent');
  goog.require('shaka.util.FakeEventTarget');
  goog.require('shaka.util.IDestroyable');
  goog.require('shaka.util.LanguageUtils');
  goog.require('shaka.util.ManifestParserUtils');
  goog.require('shaka.util.MimeUtils');
  goog.require('shaka.util.MultiMap');
  goog.require('shaka.util.Periods');
  goog.require('shaka.util.Platform');
  goog.require('shaka.util.PlayerConfiguration');
  goog.require('shaka.util.Stats');
  goog.require('shaka.util.StreamUtils');
  goog.require('shaka.util.Timer');
  
  
  /**
   * @event shaka.Player.ErrorEvent
   * @description Fired when a playback error occurs.
   * @property {string} type
   *   'error'
   * @property {!shaka.util.Error} detail
   *   An object which contains details on the error.  The error's
   *   <code>category</code> and <code>code</code> properties will identify the
   *   specific error that occurred.  In an uncompiled build, you can also use the
   *   <code>message</code> and <code>stack</code> properties to debug.
   * @exportDoc
   */
  
  /**
   * @event shaka.Player.StateChangeEvent
   * @description Fired when the player changes load states.
   * @property {string} type
   *    'onstatechange'
   * @property {string} state
   *    The name of the state that the player just entered.
   * @exportDoc
   */
  
  /**
   * @event shaka.Player.StateIdleEvent
   * @description Fired when the player has stopped changing states and will
   *    remain idle until a new state change request (e.g. <code>load</code>,
   *    <code>attach</code>, etc.) is made.
   * @property {string} type
   *    'onstateidle'
   * @property {string} state
   *    The name of the state that the player stopped in.
   * @exportDoc
   */
  
  /**
   * @event shaka.Player.EmsgEvent
   * @description Fired when a non-typical emsg is found in a segment.
   * @property {string} type
   *   'emsg'
   * @property {shaka.extern.EmsgInfo} detail
   *   An object which contains the content of the emsg box.
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.DrmSessionUpdateEvent
   * @description Fired when the CDM has accepted the license response.
   * @property {string} type
   *   'drmsessionupdate'
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.TimelineRegionAddedEvent
   * @description Fired when a media timeline region is added.
   * @property {string} type
   *   'timelineregionadded'
   * @property {shaka.extern.TimelineRegionInfo} detail
   *   An object which contains a description of the region.
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.TimelineRegionEnterEvent
   * @description Fired when the playhead enters a timeline region.
   * @property {string} type
   *   'timelineregionenter'
   * @property {shaka.extern.TimelineRegionInfo} detail
   *   An object which contains a description of the region.
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.TimelineRegionExitEvent
   * @description Fired when the playhead exits a timeline region.
   * @property {string} type
   *   'timelineregionexit'
   * @property {shaka.extern.TimelineRegionInfo} detail
   *   An object which contains a description of the region.
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.BufferingEvent
   * @description Fired when the player's buffering state changes.
   * @property {string} type
   *   'buffering'
   * @property {boolean} buffering
   *   True when the Player enters the buffering state.
   *   False when the Player leaves the buffering state.
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.LoadingEvent
   * @description Fired when the player begins loading. The start of loading is
   *   defined as when the user has communicated intent to load content (i.e.
   *   <code>Player.load</code> has been called).
   * @property {string} type
   *   'loading'
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.UnloadingEvent
   * @description Fired when the player unloads or fails to load.
   *   Used by the Cast receiver to determine idle state.
   * @property {string} type
   *   'unloading'
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.TextTrackVisibilityEvent
   * @description Fired when text track visibility changes.
   * @property {string} type
   *   'texttrackvisibility'
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.TracksChangedEvent
   * @description Fired when the list of tracks changes.  For example, this will
   *   happen when changing periods or when track restrictions change.
   * @property {string} type
   *   'trackschanged'
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.AdaptationEvent
   * @description Fired when an automatic adaptation causes the active tracks
   *   to change.  Does not fire when the application calls
   *   <code>selectVariantTrack()</code>, <code>selectTextTrack()</code>,
   *   <code>selectAudioLanguage()</code>, or <code>selectTextLanguage()</code>.
   * @property {string} type
   *   'adaptation'
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.VariantChangedEvent
   * @description Fired when a call from the application caused a variant change.
   *  Can be triggered by calls to <code>selectVariantTrack()</code> or
   *  <code>selectAudioLanguage()</code>. Does not fire when an automatic
   *  adaptation causes a variant change.
   * @property {string} type
   *   'variantchanged'
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.TextChangedEvent
   * @description Fired when a call from the application caused a text stream
   *  change. Can be triggered by calls to <code>selectTextTrack()</code> or
   *  <code>selectTextLanguage()</code>.
   * @property {string} type
   *   'textchanged'
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.ExpirationUpdatedEvent
   * @description Fired when there is a change in the expiration times of an
   *   EME session.
   * @property {string} type
   *   'expirationupdated'
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.LargeGapEvent
   * @description Fired when the playhead enters a large gap.  If the
   *   <code>streaming.jumpLargeGaps</code> configuration is set, the default
   *   action of this event is to jump the gap; this can be prevented by calling
   *   <code>preventDefault()</code> on the event object.
   * @property {string} type
   *   'largegap'
   * @property {number} currentTime
   *   The current time of the playhead.
   * @property {number} gapSize
   *   The size of the gap, in seconds.
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.ManifestParsedEvent
   * @description Fired after the manifest has been parsed, but before anything
   *   else happens. The manifest may contain streams that will be filtered out,
   *   at this stage of the loading process.
   * @property {string} type
   *   'manifestparsed'
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.StreamingEvent
   * @description Fired after the manifest has been parsed and track information
   *   is available, but before streams have been chosen and before any segments
   *   have been fetched.  You may use this event to configure the player based on
   *   information found in the manifest.
   * @property {string} type
   *   'streaming'
   * @exportDoc
   */
  
  
  /**
   * @event shaka.Player.AbrStatusChangedEvent
   * @description Fired when the state of abr has been changed.
   *    (Enabled or disabled).
   * @property {string} type
   *   'abrstatuschanged'
   * @property {boolean} newStatus
   *  The new status of the application. True for 'is enabled' and
   *  false otherwise.
   * @exportDoc
   */
  
  
  /**
   * @summary The main player object for Shaka Player.
   *
   * @implements {shaka.util.IDestroyable}
   * @export
   */
  shaka.Player = class extends shaka.util.FakeEventTarget {
    /**
     * @param {HTMLMediaElement=} mediaElement
     *    When provided, the player will attach to <code>mediaElement</code>,
     *    similar to calling <code>attach</code>. When not provided, the player
     *    will remain detached.
     * @param {function(shaka.Player)=} dependencyInjector Optional callback
     *   which is called to inject mocks into the Player.  Used for testing.
     */
    constructor(mediaElement, dependencyInjector) {
      super();
  
      /** @private {shaka.Player.LoadMode} */
      this.loadMode_ = shaka.Player.LoadMode.NOT_LOADED;
  
      /** @private {HTMLMediaElement} */
      this.video_ = null;
  
      /**
       * Since we may not always have a text displayer created (e.g. before |load|
       * is called), we need to track what text visibility SHOULD be so that we
       * can ensure that when we create the text displayer. When we create our
       * text displayer, we will use this to show (or not show) text as per the
       * user's requests.
       *
       * @private {boolean}
       */
      this.isTextVisible_ = false;
  
      /** @private {shaka.util.EventManager} */
      this.eventManager_ = new shaka.util.EventManager();
  
      /** @private {shaka.net.NetworkingEngine} */
      this.networkingEngine_ = null;
  
      /** @private {shaka.media.DrmEngine} */
      this.drmEngine_ = null;
  
      /** @private {shaka.media.MediaSourceEngine} */
      this.mediaSourceEngine_ = null;
  
      /** @private {shaka.media.Playhead} */
      this.playhead_ = null;
  
      /**
       * The playhead observers are used to monitor the position of the playhead
       * and some other source of data (e.g. buffered content), and raise events.
       *
       * @private {shaka.media.PlayheadObserverManager}
       */
      this.playheadObservers_ = null;
  
      /**
       * This is our control over the playback rate of the media element. This
       * provides the missing functionality that we need to provide trick play,
       * for example a negative playback rate.
       *
       * @private {shaka.media.PlayRateController}
       */
      this.playRateController_ = null;
  
      // We use the buffering observer and timer to track when we move from having
      // enough buffered content to not enough. They only exist when content has
      // been loaded and are not re-used between loads.
      /** @private {shaka.util.Timer} */
      this.bufferPoller_ = null;
  
      /** @private {shaka.media.BufferingObserver} */
      this.bufferObserver_ = null;
  
      /** @private {shaka.media.RegionTimeline} */
      this.regionTimeline_ = null;
  
      /** @private {shaka.media.StreamingEngine} */
      this.streamingEngine_ = null;
  
      /** @private {shaka.extern.ManifestParser} */
      this.parser_ = null;
  
      /** @private {?shaka.extern.Manifest} */
      this.manifest_ = null;
  
      /** @private {?string} */
      this.assetUri_ = null;
  
      /** @private {shaka.extern.AbrManager} */
      this.abrManager_ = null;
  
      /**
       * The factory that was used to create the abrManager_ instance.
       * @private {?shaka.extern.AbrManager.Factory}
       */
      this.abrManagerFactory_ = null;
  
      /**
       * Contains an ID for use with creating streams.  The manifest parser should
       * start with small IDs, so this starts with a large one.
       * @private {number}
       */
      this.nextExternalStreamId_ = 1e9;
  
      /** @private {!Set.<shaka.extern.Stream>} */
      this.loadingTextStreams_ = new Set();
  
      /** @private {boolean} */
      this.switchingPeriods_ = true;
  
      /** @private {?shaka.extern.Variant} */
      this.deferredVariant_ = null;
  
      /** @private {boolean} */
      this.deferredVariantClearBuffer_ = false;
  
      /** @private {number} */
      this.deferredVariantClearBufferSafeMargin_ = 0;
  
      /** @private {?shaka.extern.Stream} */
      this.deferredTextStream_ = null;
  
      /**
       * A mapping of which streams are/were active in each period. Used when the
       * current period (the one containing playhead) differs from the active
       * period (the one being streamed in by streaming engine).
       *
       * @private {!shaka.media.ActiveStreamMap}
       */
      this.activeStreams_ = new shaka.media.ActiveStreamMap();
  
      /** @private {?shaka.extern.PlayerConfiguration} */
      this.config_ = this.defaultConfig_();
  
      /**
       * The TextDisplayerFactory that was last used to make a text displayer.
       * Stored so that we can tell if a new type of text displayer is desired.
       * @private {?shaka.extern.TextDisplayer.Factory}
       */
      this.lastTextFactory_;
  
      /** @private {{width: number, height: number}} */
      this.maxHwRes_ = {width: Infinity, height: Infinity};
  
      /** @private {shaka.util.Stats} */
      this.stats_ = null;
  
      /** @private {!shaka.media.AdaptationSetCriteria} */
      this.currentAdaptationSetCriteria_ =
          new shaka.media.PreferenceBasedCriteria(
              this.config_.preferredAudioLanguage,
              this.config_.preferredVariantRole,
              this.config_.preferredAudioChannelCount);
  
      /** @private {string} */
      this.currentTextLanguage_ = this.config_.preferredTextLanguage;
  
      /** @private {string} */
      this.currentTextRole_ = this.config_.preferredTextRole;
  
      if (dependencyInjector) {
        dependencyInjector(this);
      }
  
      this.networkingEngine_ = this.createNetworkingEngine();
  
      /** @private {shaka.ads.AdManager} */
      this.adManager_ = new shaka.ads.AdManager();
  
      // If the browser comes back online after being offline, then try to play
      // again.
      this.eventManager_.listen(window, 'online', () => {
        this.retryStreaming();
      });
  
      /** @private {shaka.routing.Node} */
      this.detachNode_ = {name: 'detach'};
      /** @private {shaka.routing.Node} */
      this.attachNode_ = {name: 'attach'};
      /** @private {shaka.routing.Node} */
      this.unloadNode_ = {name: 'unload'};
      /** @private {shaka.routing.Node} */
      this.parserNode_ = {name: 'manifest-parser'};
      /** @private {shaka.routing.Node} */
      this.manifestNode_ = {name: 'manifest'};
      /** @private {shaka.routing.Node} */
      this.mediaSourceNode_ = {name: 'media-source'};
      /** @private {shaka.routing.Node} */
      this.drmNode_ = {name: 'drm-engine'};
      /** @private {shaka.routing.Node} */
      this.loadNode_ = {name: 'load'};
      /** @private {shaka.routing.Node} */
      this.srcEqualsDrmNode_ = {name: 'src-equals-drm-engine'};
      /** @private {shaka.routing.Node} */
      this.srcEqualsNode_ = {name: 'src-equals'};
  
      const AbortableOperation = shaka.util.AbortableOperation;
  
      const actions = new Map();
      actions.set(this.attachNode_, (has, wants) => {
        return AbortableOperation.notAbortable(this.onAttach_(has, wants));
      });
      actions.set(this.detachNode_, (has, wants) => {
        return AbortableOperation.notAbortable(this.onDetach_(has, wants));
      });
      actions.set(this.unloadNode_, (has, wants) => {
        return AbortableOperation.notAbortable(this.onUnload_(has, wants));
      });
      actions.set(this.mediaSourceNode_, (has, wants) => {
        const p = this.onInitializeMediaSourceEngine_(has, wants);
        return AbortableOperation.notAbortable(p);
      });
      actions.set(this.parserNode_, (has, wants) => {
        const p = this.onInitializeParser_(has, wants);
        return AbortableOperation.notAbortable(p);
      });
      actions.set(this.manifestNode_, (has, wants) => {
        // This action is actually abortable, so unlike the other callbacks, this
        // one will return an abortable operation.
        return this.onParseManifest_(has, wants);
      });
      actions.set(this.drmNode_, (has, wants) => {
        const p = this.onInitializeDrm_(has, wants);
        return AbortableOperation.notAbortable(p);
      });
      actions.set(this.loadNode_, (has, wants) => {
        return AbortableOperation.notAbortable(this.onLoad_(has, wants));
      });
  
      actions.set(this.srcEqualsDrmNode_, (has, wants) => {
        const p = this.onInitializeSrcEqualsDrm_(has, wants);
        return AbortableOperation.notAbortable(p);
      });
      actions.set(this.srcEqualsNode_, (has, wants) => {
        return this.onSrcEquals_(has, wants);
      });
  
      /** @private {shaka.routing.Walker.Implementation} */
      const walkerImplementation = {
        getNext: (at, has, goingTo, wants) => {
          return this.getNextStep_(at, has, goingTo, wants);
        },
        enterNode: (node, has, wants) => {
          this.dispatchEvent(new shaka.util.FakeEvent(
              /* name= */ 'onstatechange',
              /* data= */ {'state': node.name}));
  
          const action = actions.get(node);
          return action(has, wants);
        },
        handleError: async (has, error) => {
          shaka.log.warning('The walker saw an error:');
          if (error instanceof shaka.util.Error) {
            shaka.log.warning('Error Code:', error.code);
          } else {
            shaka.log.warning('Error Message:', error.message);
            shaka.log.warning('Error Stack:', error.stack);
          }
  
          // Regardless of what state we were in, if there is an error, we unload.
          // This ensures that any initialized system will be torn-down and we
          // will go back to a safe foundation. We assume that the media element
          // is always safe to use after an error.
          await this.onUnload_(has, shaka.Player.createEmptyPayload_());
  
          // There are only two nodes that come before we start loading content,
          // attach and detach. If we have a media element, it means we were
          // attached to the element, and we can safely return to the attach state
          // (we assume that the video element is always re-usable). We favor
          // returning to the attach node since it means that the app won't need
          // to re-attach if it saw an error.
          return has.mediaElement ? this.attachNode_ : this.detachNode_;
        },
        onIdle: (node) => {
          this.dispatchEvent(new shaka.util.FakeEvent(
              /* name= */ 'onstateidle',
              /* data= */ {'state': node.name}));
        },
      };
  
      /** @private {shaka.routing.Walker} */
      this.walker_ = new shaka.routing.Walker(
          this.detachNode_,
          shaka.Player.createEmptyPayload_(),
          walkerImplementation);
  
      // Even though |attach| will start in later interpreter cycles, it should be
      // the LAST thing we do in the constructor because conceptually it relies on
      // player having been initialized.
      if (mediaElement) {
        this.attach(mediaElement, /* initializeMediaSource= */ true);
      }
    }
  
    /**
     * After destruction, a Player object cannot be used again.
     *
     * @override
     * @export
     */
    async destroy() {
      // Make sure we only execute the destroy logic once.
      if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
        return;
      }
  
      // Mark as "dead". This should stop external-facing calls from changing our
      // internal state any more. This will stop calls to |attach|, |detach|, etc.
      // from interrupting our final move to the detached state.
      this.loadMode_ = shaka.Player.LoadMode.DESTROYED;
  
      // Because we have set |loadMode_| to |DESTROYED| we can't call |detach|. We
      // must talk to |this.walker_| directly.
      const events = this.walker_.startNewRoute((currentPayload) => {
        return {
          node: this.detachNode_,
          payload: shaka.Player.createEmptyPayload_(),
          interruptible: false,
        };
      });
  
      // Wait until the detach has finished so that we don't interrupt it by
      // calling |destroy| on |this.walker_|. To avoid failing here, we always
      // resolve the promise.
      await new Promise((resolve) => {
        events.onStart = () => {
          shaka.log.info('Preparing to destroy walker...');
        };
        events.onEnd = () => {
          resolve();
        };
        events.onCancel = () => {
          goog.asserts.assert(false,
              'Our final detach call should never be cancelled.');
          resolve();
        };
        events.onError = () => {
          goog.asserts.assert(false,
              'Our final detach call should never see an error');
          resolve();
        };
        events.onSkip = () => {
          goog.asserts.assert(false,
              'Our final detach call should never be skipped');
          resolve();
        };
      });
      await this.walker_.destroy();
  
      // Tear-down the event manager to ensure messages stop moving around.
      if (this.eventManager_) {
        this.eventManager_.release();
        this.eventManager_ = null;
      }
  
      this.abrManagerFactory_ = null;
      this.abrManager_ = null;
      this.config_ = null;
  
      if (this.networkingEngine_) {
        await this.networkingEngine_.destroy();
        this.networkingEngine_ = null;
      }
    }
  
    /**
     * Registers a plugin callback that will be called with
     * <code>support()</code>.  The callback will return the value that will be
     * stored in the return value from <code>support()</code>.
     *
     * @param {string} name
     * @param {function():*} callback
     * @export
     */
    static registerSupportPlugin(name, callback) {
      shaka.Player.supportPlugins_[name] = callback;
    }
  
    /**
     * Return whether the browser provides basic support.  If this returns false,
     * Shaka Player cannot be used at all.  In this case, do not construct a
     * Player instance and do not use the library.
     *
     * @return {boolean}
     * @export
     */
    static isBrowserSupported() {
      // Basic features needed for the library to be usable.
      const basicSupport = !!window.Promise && !!window.Uint8Array &&
                           // eslint-disable-next-line no-restricted-syntax
                           !!Array.prototype.forEach;
      if (!basicSupport) {
        return false;
      }
  
      // DRM support is not strictly necessary, but the APIs at least need to be
      // there.  Our no-op DRM polyfill should handle that.
      // TODO(#1017): Consider making even DrmEngine optional.
      const drmSupport = shaka.media.DrmEngine.isBrowserSupported();
      if (!drmSupport) {
        return false;
      }
  
      // If we have MediaSource (MSE) support, we should be able to use Shaka.
      if (shaka.util.Platform.supportsMediaSource()) {
        return true;
      }
  
      // If we don't have MSE, we _may_ be able to use Shaka.  Look for native HLS
      // support, and call this platform usable if we have it.
      return shaka.util.Platform.supportsMediaType('application/x-mpegurl');
    }
  
    /**
     * Probes the browser to determine what features are supported.  This makes a
     * number of requests to EME/MSE/etc which may result in user prompts.  This
     * should only be used for diagnostics.
     *
     * <p>
     * NOTE: This may show a request to the user for permission.
     *
     * @see https://bit.ly/2ywccmH
     * @return {!Promise.<shaka.extern.SupportType>}
     * @export
     */
    static async probeSupport() {
      goog.asserts.assert(shaka.Player.isBrowserSupported(),
          'Must have basic support');
      const drm = await shaka.media.DrmEngine.probeSupport();
      const manifest = shaka.media.ManifestParser.probeSupport();
      const media = shaka.media.MediaSourceEngine.probeSupport();
      const ret = {
        manifest: manifest,
        media: media,
        drm: drm,
      };
  
      const plugins = shaka.Player.supportPlugins_;
      for (const name in plugins) {
        ret[name] = plugins[name]();
      }
  
      return ret;
    }
  
    /**
     * Tell the player to use <code>mediaElement</code> for all <code>load</code>
     * requests until <code>detach</code> or <code>destroy</code> are called.
     *
     * <p>
     * Calling <code>attach</code> with <code>initializedMediaSource=true</code>
     * will tell the player to take the initial load step and initialize media
     * source.
     *
     * <p>
     * Calls to <code>attach</code> will interrupt any in-progress calls to
     * <code>load</code> but cannot interrupt calls to <code>attach</code>,
     * <code>detach</code>, or <code>unload</code>.
     *
     * @param {!HTMLMediaElement} mediaElement
     * @param {boolean=} initializeMediaSource
     * @return {!Promise}
     * @export
     */
    attach(mediaElement, initializeMediaSource = true) {
      // Do not allow the player to be used after |destroy| is called.
      if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
        return Promise.reject(this.createAbortLoadError_());
      }
  
      const payload = shaka.Player.createEmptyPayload_();
      payload.mediaElement = mediaElement;
  
      // If the platform does not support media source, we will never want to
      // initialize media source.
      if (!shaka.util.Platform.supportsMediaSource()) {
        initializeMediaSource = false;
      }
  
      const destination = initializeMediaSource ?
                          this.mediaSourceNode_ :
                          this.attachNode_;
  
      // Do not allow this route to be interrupted because calls after this attach
      // call will depend on the media element being attached.
      const events = this.walker_.startNewRoute((currentPayload) => {
        return {
          node: destination,
          payload: payload,
          interruptible: false,
        };
      });
  
      // List to the events that can occur with our request.
      events.onStart = () => shaka.log.info('Starting attach...');
      return this.wrapWalkerListenersWithPromise_(events);
    }
  
    /**
     * Tell the player to stop using its current media element. If the player is:
     * <ul>
     *  <li>detached, this will do nothing,
     *  <li>attached, this will release the media element,
     *  <li>loading, this will abort loading, unload, and release the media
     *      element,
     *  <li>playing content, this will stop playback, unload, and release the
     *      media element.
     * </ul>
     *
     * <p>
     * Calls to <code>detach</code> will interrupt any in-progress calls to
     * <code>load</code> but cannot interrupt calls to <code>attach</code>,
     * <code>detach</code>, or <code>unload</code>.
     *
     * @return {!Promise}
     * @export
     */
    detach() {
      // Do not allow the player to be used after |destroy| is called.
      if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
        return Promise.reject(this.createAbortLoadError_());
      }
  
      // Tell the walker to go "detached", but do not allow it to be interrupted.
      // If it could be interrupted it means that our media element could fall out
      // of sync.
      const events = this.walker_.startNewRoute((currentPayload) => {
        return {
          node: this.detachNode_,
          payload: shaka.Player.createEmptyPayload_(),
          interruptible: false,
        };
      });
  
      events.onStart = () => shaka.log.info('Starting detach...');
      return this.wrapWalkerListenersWithPromise_(events);
    }
  
    /**
     * Tell the player to either return to:
     * <ul>
     *   <li>detached (when it does not have a media element),
     *   <li>attached (when it has a media element and
     *     <code>initializedMediaSource=false</code>)
     *   <li>media source initialized (when it has a media element and
     *     <code>initializedMediaSource=true</code>)
     * </ul>
     *
     * <p>
     * Calls to <code>unload</code> will interrupt any in-progress calls to
     * <code>load</code> but cannot interrupt calls to <code>attach</code>,
     * <code>detach</code>, or <code>unload</code>.
     *
     * @param {boolean=} initializeMediaSource
     * @return {!Promise}
     * @export
     */
    unload(initializeMediaSource = true) {
      // Do not allow the player to be used after |destroy| is called.
      if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
        return Promise.reject(this.createAbortLoadError_());
      }
  
      // If the platform does not support media source, we will never want to
      // initialize media source.
      if (!shaka.util.Platform.supportsMediaSource()) {
        initializeMediaSource = false;
      }
  
      // Since we are going either to attached or detached (through unloaded), we
      // can't allow it to be interrupted or else we could lose track of what
      // media element we are suppose to use.
      //
      // Using the current payload, we can determine which node we want to go to.
      // If we have a media element, we want to go back to attached. If we have no
      // media element, we want to go back to detached.
      const payload = shaka.Player.createEmptyPayload_();
  
      const events = this.walker_.startNewRoute((currentPayload) => {
        // When someone calls |unload| we can either be before attached or
        // detached (there is nothing stopping someone from calling |detach| when
        // we are already detached).
        //
        // If we are attached to the correct element, we can tear down the
        // previous playback components and go to the attached media source node
        // depending on whether or not the caller wants to pre-init media source.
        //
        // If we don't have a media element, we assume that we are already at the
        // detached node - but only the walker knows that. To ensure we are
        // actually there, we tell the walker to go to detach. While this is
        // technically unnecessary, it ensures that we are in the state we want
        // to be in and ready for the next request.
        let destination = null;
  
        if (currentPayload.mediaElement && initializeMediaSource) {
          destination = this.mediaSourceNode_;
        } else if (currentPayload.mediaElement) {
          destination = this.attachNode_;
        } else {
          destination = this.detachNode_;
        }
  
        goog.asserts.assert(destination, 'We should have picked a destination.');
  
        // Copy over the media element because we want to keep using the same
        // element - the other values don't matter.
        payload.mediaElement = currentPayload.mediaElement;
  
        return {
          node: destination,
          payload: payload,
          interruptible: false,
        };
      });
  
      events.onStart = () => shaka.log.info('Starting unload...');
      return this.wrapWalkerListenersWithPromise_(events);
    }
  
    /**
     * Tell the player to load the content at <code>assetUri</code> and start
     * playback at <code>startTime</code>. Before calling <code>load</code>,
     * a call to <code>attach</code> must have succeeded.
     *
     * <p>
     * Calls to <code>load</code> will interrupt any in-progress calls to
     * <code>load</code> but cannot interrupt calls to <code>attach</code>,
     * <code>detach</code>, or <code>unload</code>.
     *
     * @param {string} assetUri
     * @param {?number=} startTime
     *    When <code>startTime</code> is <code>null</code> or
     *    <code>undefined</code>, playback will start at the default start time (0
     *    for VOD and liveEdge for LIVE).
     * @param {string|shaka.extern.ManifestParser.Factory=} mimeType
     * @return {!Promise}
     * @export
     */
    load(assetUri, startTime, mimeType) {
      // Do not allow the player to be used after |destroy| is called.
      if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
        return Promise.reject(this.createAbortLoadError_());
      }
  
      // We dispatch the loading event when someone calls |load| because we want
      // to surface the user intent.
      this.dispatchEvent(new shaka.util.FakeEvent('loading'));
  
      // Right away we know what the asset uri and start-of-load time are. We will
      // fill-in the rest of the information later.
      const payload = shaka.Player.createEmptyPayload_();
      payload.uri = assetUri;
      payload.startTimeOfLoad = Date.now() / 1000;
  
      if (mimeType && typeof mimeType != 'string') {
        shaka.Deprecate.deprecateFeature(
            2, 6,
            'Loading with a manifest parser factory',
            'Please register a manifest parser and for the mime-type.');
        const Factory =
        /** @type {shaka.extern.ManifestParser.Factory} */ (mimeType);
        payload.factory = () => new Factory();
      }
  
      if (mimeType && typeof mimeType == 'string') {
        payload.mimeType = /** @type {string} */ (mimeType);
      }
  
      // Because we allow |startTime| to be optional, it means that it will be
      // |undefined| when not provided. This means that we need to re-map
      // |undefined| to |null| while preserving |0| as a meaningful value.
      if (startTime !== undefined) {
        payload.startTime = startTime;
      }
  
      // TODO: Refactor to determine whether it's a manifest or not, and whether
      // or not we can play it.  Then we could return a better error than
      // UNABLE_TO_GUESS_MANIFEST_TYPE for WebM in Safari.
      const useSrcEquals = this.shouldUseSrcEquals_(payload);
      const destination = useSrcEquals ? this.srcEqualsNode_ : this.loadNode_;
  
      // Allow this request to be interrupted, this will allow other requests to
      // cancel a load and quickly start a new load.
      const events = this.walker_.startNewRoute((currentPayload) => {
        if (currentPayload.mediaElement == null) {
          // Because we return null, this "new route" will not be used.
          return null;
        }
  
        // Keep using whatever media element we have right now.
        payload.mediaElement = currentPayload.mediaElement;
  
        return {
          node: destination,
          payload: payload,
          interruptible: true,
        };
      });
  
      // Load's request is a little different, so we can't use our normal
      // listeners-to-promise method. It is the only request where we may skip the
      // request, so we need to set the on skip callback to reject with a specific
      // error.
      events.onStart =
          () => shaka.log.info('Starting load of ' + assetUri + '...');
      return new Promise((resolve, reject) => {
        events.onSkip = () => reject(new shaka.util.Error(
            shaka.util.Error.Severity.CRITICAL,
            shaka.util.Error.Category.PLAYER,
            shaka.util.Error.Code.NO_VIDEO_ELEMENT));
  
        events.onEnd = () => resolve();
        events.onCancel = () => reject(this.createAbortLoadError_());
        events.onError = (e) => reject(e);
      });
    }
  
    /**
     * Check if src= should be used to load the asset at |uri|. Assume that media
     * source is the default option, and that src= is for special cases.
     *
     * @param {shaka.routing.Payload} payload
     * @return {boolean}
     *    |true| if the content should be loaded with src=, |false| if the content
     *    should be loaded with MediaSource.
     * @private
     */
    shouldUseSrcEquals_(payload) {
      const Platform = shaka.util.Platform;
  
      // If an explicit ManifestParser factory has been given, we can't do src=.
      if (payload.factory) {
        return false;
      }
  
      // If we are using a platform that does not support media source, we will
      // fall back to src= to handle all playback.
      if (!Platform.supportsMediaSource()) {
        return true;
      }
  
      // The most accurate way to tell the player how to load the content is via
      // MIME type.  We can fall back to features of the URI if needed.
      let mimeType = payload.mimeType;
      const uri = payload.uri || '';
  
      // If we don't have a MIME type, try to guess based on the file extension.
      // TODO: Too generic to belong to ManifestParser now.  Refactor.
      if (!mimeType) {
        // Try using the uri extension.
        const extension = shaka.media.ManifestParser.getExtension(uri);
        mimeType = {
          'mp4': 'video/mp4',
          'm4v': 'video/mp4',
          'm4a': 'audio/mp4',
          'webm': 'video/webm',
          'ts': 'video/mp2t',
          'm3u8': 'application/x-mpegurl',
          'mp3': 'audio/mpeg',
          'aac': 'audio/aac',
          'flac': 'audio/flac',
        }[extension];
      }
  
      // TODO: The load graph system has a design limitation that requires routing
      // destination to be chosen synchronously.  This means we can only make the
      // right choice about src= consistently if we have a well-known file
      // extension or API-provided MIME type.  Detection of MIME type from a HEAD
      // request (as is done for manifest types) can't be done yet.
  
      if (mimeType) {
        // If we have a MIME type, check if the browser can play it natively.
        // This will cover both single files and native HLS.
        const canPlayNatively = Platform.supportsMediaType(mimeType);
  
        // If we can't play natively, then src= isn't an option.
        if (!canPlayNatively) {
          return false;
        }
  
        const canPlayMediaSource =
            shaka.media.ManifestParser.isSupported(uri, mimeType);
  
        // If MediaSource isn't an option, the native option is our only chance.
        if (!canPlayMediaSource) {
          return true;
        }
  
        // If we land here, both are feasible.
        goog.asserts.assert(canPlayNatively && canPlayMediaSource,
            'Both native and MSE playback should be possible!');
  
        // We would prefer MediaSource in some cases, and src= in others.  For
        // example, Android has native HLS, but we'd prefer our own MediaSource
        // version there.  For Safari, the choice is governed by the
        // useNativeHlsOnSafari setting of the streaming config.
        return Platform.isApple() && this.config_.streaming.useNativeHlsOnSafari;
      }
  
      // Unless there are good reasons to use src= (single-file playback or native
      // HLS), we prefer MediaSource.  So the final return value for choosing src=
      // is false.
      return false;
    }
  
    /**
     * This should only be called by the load graph when it is time to attach to
     * a media element. The only times this may be called are when we are being
     * asked to re-attach to the current media element, or attach to a new media
     * element while not attached to a media element.
     *
     * This method assumes that it is safe for it to execute, the load-graph is
     * responsible for ensuring all assumptions are true.
     *
     * Attaching to a media element is defined as:
     *  - Registering error listeners to the media element.
     *  - Caching the video element for use outside of the load graph.
     *
     * @param {shaka.routing.Payload} has
     * @param {shaka.routing.Payload} wants
     * @return {!Promise}
     * @private
     */
    onAttach_(has, wants) {
      // If we don't have a media element yet, it means we are entering
      // "attach" from another node.
      //
      // If we have a media element, it should match |wants.mediaElement|
      // because it means we are going from "attach" to "attach".
      //
      // These constraints should be maintained and guaranteed by the routing
      // logic in |getNextStep_|.
      goog.asserts.assert(
          has.mediaElement == null || has.mediaElement == wants.mediaElement,
          'The routing logic failed. MediaElement requirement failed.');
  
      if (has.mediaElement == null) {
        has.mediaElement = wants.mediaElement;
  
        const onError = (error) => this.onVideoError_(error);
        this.eventManager_.listen(has.mediaElement, 'error', onError);
      }
  
      this.video_ = has.mediaElement;
  
      return Promise.resolve();
    }
  
    /**
     * This should only be called by the load graph when it is time to detach from
     * a media element. The only times this may be called are when we are being
     * asked to detach from the current media element, or detach when we are
     * already detached.
     *
     * This method assumes that it is safe for it to execute, the load-graph is
     * responsible for ensuring all assumptions are true.
     *
     * Detaching from a media element is defined as:
     *  - Removing error listeners from the media element.
     *  - Dropping the cached reference to the video element.
     *
     * @param {shaka.routing.Payload} has
     * @param {shaka.routing.Payload} wants
     * @return {!Promise}
     * @private
     */
    onDetach_(has, wants) {
      // If we are going from "detached" to "detached" we wouldn't have
      // a media element to detach from.
      if (has.mediaElement) {
        this.eventManager_.unlisten(has.mediaElement, 'error');
        has.mediaElement = null;
      }
  
      // Clear our cached copy of the media element.
      this.video_ = null;
  
      return Promise.resolve();
    }
  
    /**
     * This should only be called by the load graph when it is time to unload all
     * currently initialized playback components. Unlike the other load actions,
     * this action is built to be more general. We need to do this because we
     * don't know what state the player will be in before unloading (including
     * after an error occurred in the middle of a transition).
     *
     * This method assumes that any component could be |null| and should be safe
     * to call from any point in the load graph.
     *
     * @param {shaka.routing.Payload} has
     * @param {shaka.routing.Payload} wants
     * @return {!Promise}
     * @private
     */
    async onUnload_(has, wants) {
      // Set the load mode to unload right away so that all the public methods
      // will stop using the internal components. We need to make sure that we
      // are not overriding the destroyed state because we will unload when we are
      // destroying the player.
      if (this.loadMode_ != shaka.Player.LoadMode.DESTROYED) {
        this.loadMode_ = shaka.Player.LoadMode.NOT_LOADED;
      }
  
      this.dispatchEvent(new shaka.util.FakeEvent('unloading'));
  
      // Remove everything that has to do with loading content from our payload
      // since we are releasing everything that depended on it.
      has.factory = null;
      has.mimeType = null;
      has.startTime = null;
      has.uri = null;
  
      // In most cases we should have a media element. The one exception would
      // be if there was an error and we, by chance, did not have a media element.
      if (has.mediaElement) {
        this.eventManager_.unlisten(has.mediaElement, 'loadeddata');
        this.eventManager_.unlisten(has.mediaElement, 'playing');
        this.eventManager_.unlisten(has.mediaElement, 'pause');
        this.eventManager_.unlisten(has.mediaElement, 'ended');
        this.eventManager_.unlisten(has.mediaElement, 'ratechange');
      }
  
      // Some observers use some playback components, shutting down the observers
      // first ensures that they don't try to use the playback components
      // mid-destroy.
      if (this.playheadObservers_) {
        this.playheadObservers_.release();
        this.playheadObservers_ = null;
      }
  
      if (this.bufferPoller_) {
        this.bufferPoller_.stop();
        this.bufferPoller_ = null;
      }
  
      // Stop the parser early. Since it is at the start of the pipeline, it
      // should be start early to avoid is pushing new data downstream.
      if (this.parser_) {
        await this.parser_.stop();
        this.parser_ = null;
      }
  
      // Abr Manager will tell streaming engine what to do, so we need to stop
      // it before we destroy streaming engine. Unlike with the other components,
      // we do not release the instance, we will reuse it in later loads.
      if (this.abrManager_) {
        await this.abrManager_.stop();
      }
  
      // Streaming engine will push new data to media source engine, so we need
      // to shut it down before destroy media source engine.
      if (this.streamingEngine_) {
        await this.streamingEngine_.destroy();
        this.streamingEngine_ = null;
      }
  
      // Playhead is used by StreamingEngine, so we can't destroy this until after
      // StreamingEngine has stopped.
      if (this.playhead_) {
        this.playhead_.release();
        this.playhead_ = null;
      }
  
      // Media source engine holds onto the media element, and in order to detach
      // the media keys (with drm engine), we need to break the connection between
      // media source engine and the media element.
      if (this.mediaSourceEngine_) {
        await this.mediaSourceEngine_.destroy();
        this.mediaSourceEngine_ = null;
      }
  
      // In order to unload a media element, we need to remove the src attribute
      // and then load again. When we destroy media source engine, this will be
      // done for us, but for src=, we need to do it here.
      //
      // DrmEngine requires this to be done before we destroy DrmEngine itself.
      if (has.mediaElement && has.mediaElement.src) {
        // TODO: Investigate this more.  Only reproduces on Firefox 69.
        // Introduce a delay before detaching the video source.  We are seeing
        // spurious Promise rejections involving an AbortError in our tests
        // otherwise.
        await new Promise(
            (resolve) => new shaka.util.Timer(resolve).tickAfter(0.1));
  
        has.mediaElement.removeAttribute('src');
        has.mediaElement.load();
      }
  
      if (this.drmEngine_) {
        await this.drmEngine_.destroy();
        this.drmEngine_ = null;
      }
  
      this.activeStreams_.clear();
      this.assetUri_ = null;
      this.bufferObserver_ = null;
      this.loadingTextStreams_.clear();
      this.manifest_ = null;
      this.stats_ = null;
      this.lastTextFactory_ = null;
      this.switchingPeriods_ = true;
  
      // Make sure that the app knows of the new buffering state.
      this.updateBufferState_();
    }
  
    /**
     * This should only be called by the load graph when it is time to initialize
     * media source engine. The only time this may be called is when we are
     * attached to the same media element as in the request.
     *
     * This method assumes that it is safe for it to execute. The load-graph is
     * responsible for ensuring all assumptions are true.
     *
     * @param {shaka.routing.Payload} has
     * @param {shaka.routing.Payload} wants
     *
     * @return {!Promise}
     * @private
     */
    async onInitializeMediaSourceEngine_(has, wants) {
      goog.asserts.assert(
          shaka.util.Platform.supportsMediaSource(),
          'We should not be initializing media source on a platform that does ' +
              'not support media source.');
      goog.asserts.assert(
          has.mediaElement,
          'We should have a media element when initializing media source.');
      goog.asserts.assert(
          has.mediaElement == wants.mediaElement,
          '|has| and |wants| should have the same media element when ' +
              'initializing media source.');
  
      goog.asserts.assert(
          this.mediaSourceEngine_ == null,
          'We should not have a media source engine yet.');
  
      const closedCaptionsParser =
          shaka.media.MuxJSClosedCaptionParser.isSupported() ?
          new shaka.media.MuxJSClosedCaptionParser() :
          new shaka.media.NoopCaptionParser();
  
      // When changing text visibility we need to update both the text displayer
      // and streaming engine because we don't always stream text. To ensure that
      // text displayer and streaming engine are always in sync, wait until they
      // are both initialized before setting the initial value.
      const TextDisplayerFactory = this.config_.textDisplayFactory;
      const textDisplayer = new TextDisplayerFactory();
      this.lastTextFactory_ = TextDisplayerFactory;
  
      const mediaSourceEngine = this.createMediaSourceEngine(
          has.mediaElement, closedCaptionsParser, textDisplayer);
  
      // Wait for media source engine to finish opening. This promise should
      // NEVER be rejected as per the media source engine implementation.
      await mediaSourceEngine.open();
  
      // Wait until it is ready to actually store the reference.
      this.mediaSourceEngine_ = mediaSourceEngine;
    }
  
    /**
     * Create the parser for the asset located at |wants.uri|. This should only be
     * called as part of the load graph.
     *
     * This method assumes that it is safe for it to execute, the load-graph is
     * responsible for ensuring all assumptions are true.
     *
     * @param {shaka.routing.Payload} has
     * @param {shaka.routing.Payload} wants
     * @return {!Promise}
     * @private
     */
    async onInitializeParser_(has, wants) {
      goog.asserts.assert(
          has.mediaElement,
          'We should have a media element when initializing the parser.');
      goog.asserts.assert(
          has.mediaElement == wants.mediaElement,
          '|has| and |wants| should have the same media element when ' +
              'initializing the parser.');
  
      goog.asserts.assert(
          this.networkingEngine_,
          'Need networking engine when initializing the parser.');
      goog.asserts.assert(
          this.config_,
          'Need player config when initializing the parser.');
  
      // We are going to "lock-in" the factory, mime type, and uri since they are
      // what we are going to use to create our parser and parse the manifest.
      has.factory = wants.factory;
      has.mimeType = wants.mimeType;
      has.uri = wants.uri;
  
      goog.asserts.assert(
          has.uri,
          'We should have an asset uri when initializing the parsing.');
  
      // Store references to things we asserted so that we don't need to reassert
      // them again later.
      const assetUri = has.uri;
      const networkingEngine = this.networkingEngine_;
  
      // Save the uri so that it can be used outside of the load-graph.
      this.assetUri_ = assetUri;
  
      // Create the parser that we will use to parse the manifest.
      if (has.factory) {
        this.parser_ = has.factory();
      } else {
        this.parser_ = await shaka.media.ManifestParser.create(
            assetUri,
            networkingEngine,
            this.config_.manifest.retryParameters,
            has.mimeType);
      }
  
      this.parser_.configure(this.config_.manifest);
    }
  
    /**
     * Parse the manifest at |has.uri| using the parser that should have already
     * been created. This should only be called as part of the load graph.
     *
     * This method assumes that it is safe for it to execute, the load-graph is
     * responsible for ensuring all assumptions are true.
     *
     * @param {shaka.routing.Payload} has
     * @param {shaka.routing.Payload} wants
     * @return {!shaka.util.AbortableOperation}
     * @private
     */
    onParseManifest_(has, wants) {
      goog.asserts.assert(
          has.factory == wants.factory,
          '|has| and |wants| should have the same factory when parsing.');
      goog.asserts.assert(
          has.mimeType == wants.mimeType,
          '|has| and |wants| should have the same mime type when parsing.');
      goog.asserts.assert(
          has.uri == wants.uri,
          '|has| and |wants| should have the same uri when parsing.');
  
      goog.asserts.assert(
          has.uri,
          '|has| should have a valid uri when parsing.');
      goog.asserts.assert(
          has.uri == this.assetUri_,
          '|has.uri| should match the cached asset uri.');
  
      goog.asserts.assert(
          this.networkingEngine_,
          'Need networking engine to parse manifest.');
      goog.asserts.assert(
          this.config_,
          'Need player config to parse manifest.');
  
      goog.asserts.assert(
          this.parser_,
          '|this.parser_| should have been set in an earlier step.');
  
      // Store references to things we asserted so that we don't need to reassert
      // them again later.
      const assetUri = has.uri;
      const networkingEngine = this.networkingEngine_;
  
      // This will be needed by the parser once it starts parsing, so we will
      // initialize it now even through it appears a little out-of-place.
      this.regionTimeline_ = new shaka.media.RegionTimeline();
      this.regionTimeline_.setListeners(/* onRegionAdded */ (region) => {
        this.onRegionEvent_('timelineregionadded', region);
      });
  
      const playerInterface = {
        networkingEngine: networkingEngine,
        filterNewPeriod: (period) => this.filterNewPeriod_(period),
        filterAllPeriods: (periods) => this.filterAllPeriods_(periods),
  
        // Called when the parser finds a timeline region. This can be called
        // before we start playback or during playback (live/in-progress
        // manifest).
        onTimelineRegionAdded: (region) => this.regionTimeline_.addRegion(region),
  
        onEvent: (event) => this.dispatchEvent(event),
        onError: (error) => this.onError_(error),
      };
  
      return new shaka.util.AbortableOperation(
          /* promise= */ (async () => {
            this.manifest_ = await this.parser_.start(assetUri, playerInterface);
  
            // This event is fired after the manifest is parsed, but before any
            // filtering takes place.
            this.dispatchEvent(new shaka.util.FakeEvent('manifestparsed'));
  
            // We require all manifests to have already one period.
            if (this.manifest_.periods.length == 0) {
              throw new shaka.util.Error(
                  shaka.util.Error.Severity.CRITICAL,
                  shaka.util.Error.Category.MANIFEST,
                  shaka.util.Error.Code.NO_PERIODS);
            }
  
            // Make sure that all periods are either: audio-only, video-only, or
            // audio-video.
            shaka.Player.filterForAVVariants_(this.manifest_.periods);
          })(),
          /* onAbort= */ () => {
            shaka.log.info('Aborting parser step...');
            return this.parser_.stop();
          });
    }
  
    /**
     * This should only be called by the load graph when it is time to initialize
     * drmEngine. The only time this may be called is when we are attached a
     * media element and have parsed a manifest.
     *
     * The load-graph is responsible for ensuring all assumptions made by this
     * method are valid before executing it.
     *
     * @param {shaka.routing.Payload} has
     * @param {shaka.routing.Payload} wants
     * @return {!Promise}
     * @private
     */
    async onInitializeDrm_(has, wants) {
      goog.asserts.assert(
          has.factory == wants.factory,
          'The load graph should have ensured the factories matched.');
      goog.asserts.assert(
          has.mimeType == wants.mimeType,
          'The load graph should have ensured the mime types matched.');
      goog.asserts.assert(
          has.uri == wants.uri,
          'The load graph should have ensured the uris matched');
  
      goog.asserts.assert(
          this.networkingEngine_,
          '|onInitializeDrm_| should never be called after |destroy|');
      goog.asserts.assert(
          this.config_,
          '|onInitializeDrm_| should never be called after |destroy|');
      goog.asserts.assert(
          this.manifest_,
          '|this.manifest_| should have been set in an earlier step.');
  
      this.drmEngine_ = this.createDrmEngine({
        netEngine: this.networkingEngine_,
        onError: (e) => {
          this.onError_(e);
        },
        onKeyStatus: (map) => {
          this.onKeyStatus_(map);
        },
        onExpirationUpdated: (id, expiration) => {
          this.onExpirationUpdated_(id, expiration);
        },
        onEvent: (e) => {
          this.dispatchEvent(e);
        },
      });
  
      this.drmEngine_.configure(this.config_.drm);
  
      await this.drmEngine_.initForPlayback(
          shaka.util.Periods.getAllVariantsFrom(this.manifest_.periods),
          this.manifest_.offlineSessionIds);
  
      // Now that we have drm information, filter the manifest (again) so that we
      // can ensure we only use variants with the selected key system.
      this.filterAllPeriods_(this.manifest_.periods);
    }
  
    /**
     * This should only be called by the load graph when it is time to load all
     * playback components needed for playback. The only times this may be called
     * is when we are attached to the same media element as in the request.
     *
     * This method assumes that it is safe for it to execute, the load-graph is
     * responsible for ensuring all assumptions are true.
     *
     * Loading is defined as:
     *  - Attaching all playback-related listeners to the media element
     *  - Initializing playback and observers
     *  - Initializing ABR Manager
     *  - Initializing Streaming Engine
     *  - Starting playback at |wants.startTime|
     *
     * @param {shaka.routing.Payload} has
     * @param {shaka.routing.Payload} wants
     * @private
     */
    async onLoad_(has, wants) {
      goog.asserts.assert(
          has.factory == wants.factory,
          '|has| and |wants| should have the same factory when loading.');
      goog.asserts.assert(
          has.mimeType == wants.mimeType,
          '|has| and |wants| should have the same mime type when loading.');
      goog.asserts.assert(
          has.uri == wants.uri,
          '|has| and |wants| should have the same uri when loading.');
  
      goog.asserts.assert(
          has.mediaElement,
          'We should have a media element when loading.');
      goog.asserts.assert(
          wants.startTimeOfLoad != null,
          '|wants| should tell us when the load was originally requested');
  
      // Since we are about to start playback, we will lock in the start time as
      // something we are now depending on.
      has.startTime = wants.startTime;
  
      // Store a reference to values in |has| after asserting so that closure will
      // know that they will still be non-null between calls to await.
      const mediaElement = has.mediaElement;
      const assetUri = has.uri;
  
      // Save the uri so that it can be used outside of the load-graph.
      this.assetUri_ = assetUri;
  
      // Stats are for a single playback/load session. Stats must be initialized
      // before we allow calls to |updateStateHistory|.
      this.stats_ = new shaka.util.Stats();
  
      const updateStateHistory = () => this.updateStateHistory_();
      const onRateChange = () => this.onRateChange_();
      this.eventManager_.listen(mediaElement, 'playing', updateStateHistory);
      this.eventManager_.listen(mediaElement, 'pause', updateStateHistory);
      this.eventManager_.listen(mediaElement, 'ended', updateStateHistory);
      this.eventManager_.listen(mediaElement, 'ratechange', onRateChange);
  
      const AbrManagerFactory = this.config_.abrFactory;
      if (!this.abrManager_ || this.abrManagerFactory_ != AbrManagerFactory) {
        this.abrManagerFactory_ = AbrManagerFactory;
        this.abrManager_ = new AbrManagerFactory();
        this.abrManager_.configure(this.config_.abr);
      }
  
      // TODO: When a manifest update adds a new period, that period's closed
      // captions should also be turned into text streams. This should be called
      // for each new period as well.
      this.createTextStreamsForClosedCaptions_(this.manifest_.periods);
  
      // Copy preferred languages from the config again, in case the config was
      // changed between construction and playback.
      this.currentAdaptationSetCriteria_ =
          new shaka.media.PreferenceBasedCriteria(
              this.config_.preferredAudioLanguage,
              this.config_.preferredVariantRole,
              this.config_.preferredAudioChannelCount);
  
      this.currentTextLanguage_ = this.config_.preferredTextLanguage;
  
      shaka.Player.applyPlayRange_(this.manifest_.presentationTimeline,
          this.config_.playRangeStart,
          this.config_.playRangeEnd);
  
      await this.drmEngine_.attach(mediaElement);
  
      this.abrManager_.init((variant, clearBuffer, safeMargin) => {
        return this.switch_(variant, clearBuffer, safeMargin);
      });
  
      this.playhead_ = this.createPlayhead(has.startTime);
      this.playheadObservers_ = this.createPlayheadObserversForMSE_();
  
      this.playRateController_ = new shaka.media.PlayRateController({
        getRate: () => has.mediaElement.playbackRate,
        setRate: (rate) => { has.mediaElement.playbackRate = rate; },
        movePlayhead: (delta) => { has.mediaElement.currentTime += delta; },
      });
  
      // We need to start the buffer management code near the end because it will
      // set the initial buffering state and that depends on other components
      // being initialized.
      const rebufferThreshold = Math.max(
          this.manifest_.minBufferTime, this.config_.streaming.rebufferingGoal);
      this.startBufferManagement_(rebufferThreshold);
  
      this.streamingEngine_ = this.createStreamingEngine();
      this.streamingEngine_.configure(this.config_.streaming);
  
      // If the content is multi-codec and the browser can play more than one of
      // them, choose codecs now before we initialize streaming.
      this.chooseCodecsAndFilterManifest_();
  
      // Set the load mode to "loaded with media source" as late as possible so
      // that public methods won't try to access internal components until
      // they're all initialized. We MUST switch to loaded before calling
      // "streaming" so that they can access internal information.
      this.loadMode_ = shaka.Player.LoadMode.MEDIA_SOURCE;
  
      // The event must be fired after we filter by restrictions but before the
      // active stream is picked to allow those listening for the "streaming"
      // event to make changes before streaming starts.
      this.dispatchEvent(new shaka.util.FakeEvent('streaming'));
  
      // Start streaming content. This will start the flow of content down to
      // media source, including picking the initial streams to play.
      await this.streamingEngine_.start();
  
      // We MUST wait until after we create streaming engine to adjust the start
      // time because we rely on the active audio and video streams, which are
      // selected in |StreamingEngine.init|.
      if (this.config_.streaming.startAtSegmentBoundary) {
        const startTime = this.playhead_.getTime();
        const adjustedTime = this.adjustStartTime_(startTime);
  
        this.playhead_.setStartTime(adjustedTime);
      }
  
      // Re-filter the manifest after streams have been chosen.
      for (const period of this.manifest_.periods) {
        this.filterNewPeriod_(period);
      }
      // Dispatch a 'trackschanged' event now that all initial filtering is done.
      this.onTracksChanged_();
      // Since the first streams just became active, send an adaptation event.
      this.onAdaptation_();
  
      // Now that we've filtered out variants that aren't compatible with the
      // active one, update abr manager with filtered variants for the current
      // period.
      /** @type {shaka.extern.Period} */
      const currentPeriod =
          this.getPresentationPeriod_() || this.manifest_.periods[0];
      const hasPrimary = currentPeriod.variants.some((v) => v.primary);
  
      if (!this.config_.preferredAudioLanguage && !hasPrimary) {
        shaka.log.warning('No preferred audio language set.  We will choose an ' +
                          'arbitrary language initially');
      }
  
      this.chooseVariant_(currentPeriod.variants);
  
      // Wait for the 'loadeddata' event to measure load() latency.
      this.eventManager_.listenOnce(mediaElement, 'loadeddata', () => {
        const now = Date.now() / 1000;
        const delta = now - wants.startTimeOfLoad;
  
        this.stats_.setLoadLatency(delta);
      });
    }
  
    /**
     * This should only be called by the load graph when it is time to initialize
     * drmEngine for src= playbacks.
     *
     * The load-graph is responsible for ensuring all assumptions made by this
     * method are valid before executing it.
     *
     * @param {shaka.routing.Payload} has
     * @param {shaka.routing.Payload} wants
     * @return {!Promise}
     * @private
     */
    async onInitializeSrcEqualsDrm_(has, wants) {
      const ContentType = shaka.util.ManifestParserUtils.ContentType;
  
      goog.asserts.assert(
          this.networkingEngine_,
          '|onInitializeSrcEqualsDrm_| should never be called after |destroy|');
      goog.asserts.assert(
          this.config_,
          '|onInitializeSrcEqualsDrm_| should never be called after |destroy|');
  
      this.drmEngine_ = this.createDrmEngine({
        netEngine: this.networkingEngine_,
        onError: (e) => {
          this.onError_(e);
        },
        onKeyStatus: (map) => {
          this.onKeyStatus_(map);
        },
        onExpirationUpdated: (id, expiration) => {
          this.onExpirationUpdated_(id, expiration);
        },
        onEvent: (e) => {
          this.dispatchEvent(e);
        },
      });
  
      this.drmEngine_.configure(this.config_.drm);
  
      // TODO: Instead of feeding DrmEngine with Variants, we should refactor
      // DrmEngine so that it takes a minimal config derived from Variants.  In
      // cases like this one or in removal of stored content, the details are
      // largely unimportant.  We should have a saner way to initialize DrmEngine.
      // That would also insulate DrmEngine from manifest changes in the future.
      // For now, that is time-consuming and this synthetic Variant is easy, so
      // I'm putting it off.  Since this is only expected to be used for native
      // HLS in Safari, this should be safe. -JCP
      /** @type {shaka.extern.Variant} */
      const variant = {
        id: 0,
        language: 'und',
        primary: false,
        audio: null,
        video: {
          id: 0,
          originalId: null,
          createSegmentIndex: () => Promise.resolve(),
          segmentIndex: null,
          mimeType: 'video/mp4',
          codecs: '',
          encrypted: true,
          keyId: null,
          language: 'und',
          label: null,
          type: ContentType.VIDEO,
          primary: false,
          trickModeVideo: null,
          emsgSchemeIdUris: null,
          roles: [],
          channelsCount: null,
          closedCaptions: null,
        },
        bandwidth: 100,
        drmInfos: [],  // Filled in by DrmEngine config.
        allowedByApplication: true,
        allowedByKeySystem: true,
      };
  
      await this.drmEngine_.initForPlayback(
          [variant], /* offlineSessionIds= */ []);
      await this.drmEngine_.attach(has.mediaElement);
    }
  
    /**
     * This should only be called by the load graph when it is time to set-up the
     * media element to play content using src=. The only times this may be called
     * is when we are attached to the same media element as in the request.
     *
     * This method assumes that it is safe for it to execute, the load-graph is
     * responsible for ensuring all assumptions are true.
     *
     * @param {shaka.routing.Payload} has
     * @param {shaka.routing.Payload} wants
     * @return {!shaka.util.AbortableOperation}
     *
     * @private
     */
    onSrcEquals_(has, wants) {
      goog.asserts.assert(
          has.mediaElement,
          'We should have a media element when loading.');
      goog.asserts.assert(
          wants.uri,
          '|has| should have a valid uri when loading.');
      goog.asserts.assert(
          wants.startTimeOfLoad,
          '|wants| should tell us when the load was originally requested');
      goog.asserts.assert(
          this.video_ == has.mediaElement,
          'The video element should match our media element');
  
      // Lock-in the values that we are using so that the routing logic knows what
      // we have.
      has.uri = wants.uri;
      has.startTime = wants.startTime;
  
      // Save the uri so that it can be used outside of the load-graph.
      this.assetUri_ = has.uri;
  
      // Stats are for a single playback/load session.
      this.stats_ = new shaka.util.Stats();
  
      this.playhead_ = new shaka.media.SrcEqualsPlayhead(has.mediaElement);
  
      if (has.startTime != null) {
        this.playhead_.setStartTime(has.startTime);
      }
  
      this.playRateController_ = new shaka.media.PlayRateController({
        getRate: () => has.mediaElement.playbackRate,
        setRate: (rate) => { has.mediaElement.playbackRate = rate; },
        movePlayhead: (delta) => { has.mediaElement.currentTime += delta; },
      });
  
      // We need to start the buffer management code near the end because it will
      // set the initial buffering state and that depends on other components
      // being initialized.
      const rebufferThreshold = this.config_.streaming.rebufferingGoal;
      this.startBufferManagement_(rebufferThreshold);
  
      // Add all media element listeners.
      const updateStateHistory = () => this.updateStateHistory_();
      this.eventManager_.listen(has.mediaElement, 'playing', updateStateHistory);
      this.eventManager_.listen(has.mediaElement, 'pause', updateStateHistory);
      this.eventManager_.listen(has.mediaElement, 'ended', updateStateHistory);
  
      // Wait for the 'loadeddata' event to measure load() latency.
      this.eventManager_.listenOnce(has.mediaElement, 'loadeddata', () => {
        const now = Date.now() / 1000;
        const delta = now - wants.startTimeOfLoad;
  
        this.stats_.setLoadLatency(delta);
      });
  
      // The audio tracks are only available on Safari at the moment, but this
      // drives the tracks API for Safari's native HLS. So when they change,
      // fire the corresponding Shaka Player event.
      if (this.video_.audioTracks) {
        this.eventManager_.listen(
            this.video_.audioTracks, 'addtrack', () => this.onTracksChanged_());
        this.eventManager_.listen(
            this.video_.audioTracks, 'removetrack',
            () => this.onTracksChanged_());
        this.eventManager_.listen(
            this.video_.audioTracks, 'change', () => this.onTracksChanged_());
      }
      if (this.video_.textTracks) {
        // This is a real EventTarget, but the compiler doesn't know that.
        // TODO: File a bug or send a PR to the compiler externs to fix this.
        const textTracks = /** @type {EventTarget} */(this.video_.textTracks);
        this.eventManager_.listen(
            textTracks, 'addtrack', () => this.onTracksChanged_());
        this.eventManager_.listen(
            textTracks, 'removetrack', () => this.onTracksChanged_());
        this.eventManager_.listen(
            textTracks, 'change', () => this.onTracksChanged_());
      }
  
      // By setting |src| we are done "loading" with src=. We don't need to set
      // the current time because |playhead| will do that for us.
      has.mediaElement.src = has.uri;
  
      // Set the load mode last so that we know that all our components are
      // initialized.
      this.loadMode_ = shaka.Player.LoadMode.SRC_EQUALS;
  
      // The event doesn't mean as much for src= playback, since we don't control
      // streaming.  But we should fire it in this path anyway since some
      // applications may be expecting it as a life-cycle event.
      this.dispatchEvent(new shaka.util.FakeEvent('streaming'));
  
      // This is fully loaded when we have loaded the first frame.
      const fullyLoaded = new shaka.util.PublicPromise();
      if (this.video_.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
        // Already done!
        fullyLoaded.resolve();
      } else if (this.video_.error) {
        // Already failed!
        fullyLoaded.reject(this.videoErrorToShakaError_());
      } else {
        // Wait for success or failure.
        this.eventManager_.listenOnce(this.video_, 'loadeddata', () => {
          fullyLoaded.resolve();
        });
        this.eventManager_.listenOnce(this.video_, 'error', () => {
          fullyLoaded.reject(this.videoErrorToShakaError_());
        });
      }
      return new shaka.util.AbortableOperation(fullyLoaded, /* onAbort= */ () => {
        const abortedError = new shaka.util.Error(
            shaka.util.Error.Severity.CRITICAL,
            shaka.util.Error.Category.PLAYER,
            shaka.util.Error.Code.OPERATION_ABORTED);
        fullyLoaded.reject(abortedError);
        return Promise.resolve();  // Abort complete.
      });
    }
  
    /**
     * Take a series of periods and ensure that they only contain one type of
     * variant. The different options are:
     *  1. Audio-Video
     *  2. Audio-Only
     *  3. Video-Only
     *
     * A manifest can only contain a single type because once we initialize media
     * source to expect specific streams, it must always have content for those
     * streams. If we were to start period 1 with audio+video but period 2 only
     * had audio, media source would block waiting for video content.
     *
     * @param {!Array.<shaka.extern.Period>} periods
     * @private
     */
    static filterForAVVariants_(periods) {
      const isAVVariant = (variant) => {
        // Audio-video variants may include both streams separately or may be
        // single multiplexed streams with multiple codecs.
        return (variant.video && variant.audio) ||
               (variant.video && variant.video.codecs.includes(','));
      };
      const hasAVVariant = periods.some((period) => {
        return period.variants.some(isAVVariant);
      });
      if (hasAVVariant) {
        shaka.log.debug('Found variant with audio and video content, ' +
            'so filtering out audio-only content in all periods.');
        for (const period of periods) {
          period.variants = period.variants.filter(isAVVariant);
        }
      }
    }
  
    /**
     * In case of multiple usable codecs, choose one based on lowest average
     * bandwidth and filter out the rest.
     * @private
     */
    chooseCodecsAndFilterManifest_() {
      const MimeUtils = shaka.util.MimeUtils;
  
      // Collect a list of variants for all periods.
      /** @type {!Array.<shaka.extern.Variant>} */
      let variants = this.manifest_.periods.reduce(
          (variants, period) => variants.concat(period.variants), []);
  
      // To start, consider a subset of variants based on audio channel
      // preferences.
      // For some content (#1013), surround-sound variants will use a different
      // codec than stereo variants, so it is important to choose codecs **after**
      // considering the audio channel config.
      variants = shaka.util.StreamUtils.filterVariantsByAudioChannelCount(
          variants, this.config_.preferredAudioChannelCount);
  
      function variantCodecs(variant) {
        // Only consider the base of the codec string.  For example, these should
        // both be considered the same codec: avc1.42c01e, avc1.4d401f
        let baseVideoCodec = '';
        if (variant.video) {
          baseVideoCodec = MimeUtils.getCodecBase(variant.video.codecs);
        }
  
        let baseAudioCodec = '';
        if (variant.audio) {
          baseAudioCodec = MimeUtils.getCodecBase(variant.audio.codecs);
        }
  
        return baseVideoCodec + '-' + baseAudioCodec;
      }
  
      // Now organize variants into buckets by codecs.
      /** @type {!shaka.util.MultiMap.<shaka.extern.Variant>} */
      const variantsByCodecs = new shaka.util.MultiMap();
      for (const variant of variants) {
        const group = variantCodecs(variant);
        variantsByCodecs.push(group, variant);
      }
  
      // Compute the average bandwidth for each group of variants.
      // Choose the lowest-bandwidth codecs.
      let bestCodecs = null;
      let lowestAverageBandwidth = Infinity;
      variantsByCodecs.forEach((codecs, variants) => {
        let sum = 0;
        let num = 0;
        for (const variant of variants) {
          sum += variant.bandwidth || 0;
          ++num;
        }
        const averageBandwidth = sum / num;
        shaka.log.debug('codecs', codecs, 'avg bandwidth', averageBandwidth);
  
        if (averageBandwidth < lowestAverageBandwidth) {
          bestCodecs = codecs;
          lowestAverageBandwidth = averageBandwidth;
        }
      });
      goog.asserts.assert(bestCodecs != null, 'Should have chosen codecs!');
      goog.asserts.assert(!isNaN(lowestAverageBandwidth),
          'Bandwidth should be a number!');
  
      // Filter out any variants that don't match, forcing AbrManager to choose
      // from the most efficient variants possible.
      for (const period of this.manifest_.periods) {
        period.variants = period.variants.filter((variant) => {
          const codecs = variantCodecs(variant);
          if (codecs == bestCodecs) {
            return true;
          }
  
          shaka.log.debug('Dropping Variant (better codec available)', variant);
          return false;
        });
      }
    }
  
    /**
     * Create a new DrmEngine instance. This may be replaced by tests to create
     * fake instances. Configuration and initialization will be handled after
     * |createDrmEngine|.
     *
     * @param {shaka.media.DrmEngine.PlayerInterface} playerInterface
     * @return {!shaka.media.DrmEngine}
     */
    createDrmEngine(playerInterface) {
      return new shaka.media.DrmEngine(playerInterface);
    }
  
    /**
     * Creates a new instance of NetworkingEngine.  This can be replaced by tests
     * to create fake instances instead.
     *
     * @return {!shaka.net.NetworkingEngine}
     */
    createNetworkingEngine() {
      /** @type {function(number, number)} */
      const onProgressUpdated_ = (deltaTimeMs, bytesDownloaded) => {
        // In some situations, such as during offline storage, the abr manager
        // might not yet exist. Therefore, we need to check if abr manager has
        // been initialized before using it.
        if (this.abrManager_) {
          this.abrManager_.segmentDownloaded(deltaTimeMs, bytesDownloaded);
        }
      };
  
      return new shaka.net.NetworkingEngine(onProgressUpdated_);
    }
  
    /**
     * Creates a new instance of Playhead.  This can be replaced by tests to
     * create fake instances instead.
     *
     * @param {?number} startTime
     * @return {!shaka.media.Playhead}
     */
    createPlayhead(startTime) {
      goog.asserts.assert(this.manifest_, 'Must have manifest');
      goog.asserts.assert(this.video_, 'Must have video');
      return new shaka.media.MediaSourcePlayhead(
          this.video_,
          this.manifest_,
          this.config_.streaming,
          startTime,
          () => this.onSeek_(),
          (event) => this.dispatchEvent(event));
    }
  
    /**
     * Create the observers for MSE playback. These observers are responsible for
     * notifying the app and player of specific events during MSE playback.
     *
     * @return {!shaka.media.PlayheadObserverManager}
     * @private
     */
    createPlayheadObserversForMSE_() {
      goog.asserts.assert(this.manifest_, 'Must have manifest');
      goog.asserts.assert(this.regionTimeline_, 'Must have region timeline');
      goog.asserts.assert(this.video_, 'Must have video element');
  
      // Create the period observer. This will allow us to notify the app when we
      // transition between periods.
      const periodObserver = new shaka.media.PeriodObserver(this.manifest_);
      periodObserver.setListeners((period) => this.onChangePeriod_());
  
      // Create the region observer. This will allow us to notify the app when we
      // move in and out of timeline regions.
      const regionObserver = new shaka.media.RegionObserver(this.regionTimeline_);
      const onEnterRegion = (region, seeking) => {
        this.onRegionEvent_('timelineregionenter', region);
      };
      const onExitRegion = (region, seeking) => {
        this.onRegionEvent_('timelineregionexit', region);
      };
      const onSkipRegion = (region, seeking) => {
        // If we are seeking, we don't want to surface the enter/exit events since
        // they didn't play through them.
        if (!seeking) {
          this.onRegionEvent_('timelineregionenter', region);
          this.onRegionEvent_('timelineregionexit', region);
        }
      };
      regionObserver.setListeners(onEnterRegion, onExitRegion, onSkipRegion);
  
      // Now that we have all our observers, create a manager for them.
      const manager = new shaka.media.PlayheadObserverManager(this.video_);
      manager.manage(periodObserver);
      manager.manage(regionObserver);
  
      return manager;
    }
  
    /**
     * Initialize and start the buffering system (observer and timer) so that we
     * can monitor our buffer lead during playback.
     *
     * @param {number} rebufferingGoal
     * @private
     */
    startBufferManagement_(rebufferingGoal) {
      goog.asserts.assert(
          !this.bufferObserver_,
          'No buffering observer should exist before initialization.');
  
      goog.asserts.assert(
          !this.bufferPoller_,
          'No buffer timer should exist before initialization.');
  
      // Give dummy values, will be updated below.
      this.bufferObserver_ = new shaka.media.BufferingObserver(1, 2);
  
      // Force us back to a buffering state. This ensure everything is starting in
      // the same state.
      this.bufferObserver_.setState(shaka.media.BufferingObserver.State.STARVING);
      this.updateBufferingSettings_(rebufferingGoal);
      this.updateBufferState_();
  
      // TODO: We should take some time to look into the effects of our
      //       quarter-second refresh practice. We often use a quarter-second
      //       but we have no documentation about why.
      this.bufferPoller_ = new shaka.util.Timer(() => {
        this.pollBufferState_();
      }).tickEvery(/* seconds= */ 0.25);
    }
  
    /**
     * Updates the buffering thresholds based on the new rebuffering goal.
     *
     * @param {number} rebufferingGoal
     * @private
     */
    updateBufferingSettings_(rebufferingGoal) {
      // The threshold to transition back to satisfied when starving.
      const starvingThreshold = rebufferingGoal;
      // The threshold to transition into starving when satisfied.
      // We use a "typical" threshold, unless the rebufferingGoal is unusually
      // low.
      // Then we force the value down to half the rebufferingGoal, since
      // starvingThreshold must be strictly larger than satisfiedThreshold for the
      // logic in BufferingObserver to work correctly.
      const satisfiedThreshold = Math.min(
          shaka.Player.TYPICAL_BUFFERING_THRESHOLD_, rebufferingGoal / 2);
  
      this.bufferObserver_.setThresholds(starvingThreshold, satisfiedThreshold);
    }
  
    /**
     * This method is called periodically to check what the buffering observer
     * says so that we can update the rest of the buffering behaviours.
     *
     * @private
     */
    pollBufferState_() {
      goog.asserts.assert(
          this.video_,
          'Need a media element to update the buffering observer');
  
      goog.asserts.assert(
          this.bufferObserver_,
          'Need a buffering observer to update');
  
      let bufferedToEnd;
      switch (this.loadMode_) {
        case shaka.Player.LoadMode.SRC_EQUALS:
          bufferedToEnd = this.isBufferedToEndSrc_();
          break;
        case shaka.Player.LoadMode.MEDIA_SOURCE:
          bufferedToEnd = this.isBufferedToEndMS_();
          break;
        default:
          bufferedToEnd = false;
          break;
      }
  
      const bufferLead = shaka.media.TimeRangesUtils.bufferedAheadOf(
          this.video_.buffered,
          this.video_.currentTime);
  
      const stateChanged = this.bufferObserver_.update(bufferLead, bufferedToEnd);
  
      // If the state changed, we need to surface the event.
      if (stateChanged) {
        this.updateBufferState_();
      }
    }
  
    /**
     * Create a new media source engine. This will ONLY be replaced by tests as a
     * way to inject fake media source engine instances.
     *
     * @param {!HTMLMediaElement} mediaElement
     * @param {!shaka.media.IClosedCaptionParser} closedCaptionsParser
     * @param {!shaka.extern.TextDisplayer} textDisplayer
     *
     * @return {!shaka.media.MediaSourceEngine}
     */
    createMediaSourceEngine(mediaElement, closedCaptionsParser, textDisplayer) {
      return new shaka.media.MediaSourceEngine(
          mediaElement, closedCaptionsParser, textDisplayer);
    }
  
    /**
     * Creates a new instance of StreamingEngine.  This can be replaced by tests
     * to create fake instances instead.
     *
     * @return {!shaka.media.StreamingEngine}
     */
    createStreamingEngine() {
      goog.asserts.assert(
          this.playhead_ && this.abrManager_ && this.mediaSourceEngine_ &&
          this.manifest_,
          'Must not be destroyed');
  
      /** @type {shaka.media.StreamingEngine.PlayerInterface} */
      const playerInterface = {
        getPresentationTime: () => this.playhead_.getTime(),
        getBandwidthEstimate: () => this.abrManager_.getBandwidthEstimate(),
        mediaSourceEngine: this.mediaSourceEngine_,
        netEngine: this.networkingEngine_,
        onChooseStreams: (period) => this.onChooseStreams_(period),
        onCanSwitch: () => this.canSwitch_(),
        onError: (error) => this.onError_(error),
        onEvent: (event) => this.dispatchEvent(event),
        onManifestUpdate: () => this.onManifestUpdate_(),
        onSegmentAppended: () => this.onSegmentAppended_(),
      };
  
      return new shaka.media.StreamingEngine(this.manifest_, playerInterface);
    }
  
    /**
     * Changes configuration settings on the Player.  This checks the names of
     * keys and the types of values to avoid coding errors.  If there are errors,
     * this logs them to the console and returns false.  Correct fields are still
     * applied even if there are other errors.  You can pass an explicit
     * <code>undefined</code> value to restore the default value.  This has two
     * modes of operation:
     *
     * <p>
     * First, this can be passed a single "plain" object.  This object should
     * follow the {@link shaka.extern.PlayerConfiguration} object.  Not all fields
     * need to be set; unset fields retain their old values.
     *
     * <p>
     * Second, this can be passed two arguments.  The first is the name of the key
     * to set.  This should be a '.' separated path to the key.  For example,
     * <code>'streaming.alwaysStreamText'</code>.  The second argument is the
     * value to set.
     *
     * @param {string|!Object} config This should either be a field name or an
     *   object.
     * @param {*=} value In the second mode, this is the value to set.
     * @return {boolean} True if the passed config object was valid, false if
     *   there were invalid entries.
     * @export
     */
    configure(config, value) {
      goog.asserts.assert(this.config_, 'Config must not be null!');
      goog.asserts.assert(typeof(config) == 'object' || arguments.length == 2,
          'String configs should have values!');
  
      // ('fieldName', value) format
      if (arguments.length == 2 && typeof(config) == 'string') {
        config = shaka.util.ConfigUtils.convertToConfigObject(config, value);
      }
  
      goog.asserts.assert(typeof(config) == 'object', 'Should be an object!');
  
      const ret = shaka.util.PlayerConfiguration.mergeConfigObjects(
          this.config_, config, this.defaultConfig_());
  
      this.applyConfig_();
      return ret;
    }
  
    /**
     * Apply config changes.
     * @private
     */
    applyConfig_() {
      if (this.parser_) {
        this.parser_.configure(this.config_.manifest);
      }
      if (this.drmEngine_) {
        this.drmEngine_.configure(this.config_.drm);
      }
      if (this.streamingEngine_) {
        this.streamingEngine_.configure(this.config_.streaming);
  
        // Need to apply the restrictions to every period.
        try {
          // this.filterNewPeriod_() may throw.
          for (const period of this.manifest_.periods) {
            this.filterNewPeriod_(period);
          }
        } catch (error) {
          this.onError_(error);
        }
  
        // If the stream we are playing is restricted, we need to switch.
        const activeAudio = this.streamingEngine_.getBufferingAudio();
        const activeVideo = this.streamingEngine_.getBufferingVideo();
        /** @type {shaka.extern.Period} */
        const period = this.getPresentationPeriod_();
        const activeVariant = shaka.util.StreamUtils.getVariantByStreams(
            activeAudio, activeVideo, period.variants);
        if (this.abrManager_ && activeVariant &&
            activeVariant.allowedByApplication &&
            activeVariant.allowedByKeySystem) {
          // Update AbrManager variants to match these new settings.
          this.chooseVariant_(period.variants);
        } else {
          shaka.log.debug('Choosing new streams after changing configuration');
          this.chooseStreamsAndSwitch_(period);
        }
      }
      if (this.mediaSourceEngine_) {
        const TextDisplayerFactory = this.config_.textDisplayFactory;
        if (this.lastTextFactory_ != TextDisplayerFactory) {
          const displayer = new TextDisplayerFactory();
          this.mediaSourceEngine_.setTextDisplayer(displayer);
          this.lastTextFactory_ = TextDisplayerFactory;
  
          if (this.streamingEngine_) {
            // Reload the text stream, so the cues will load again.
            this.streamingEngine_.reloadTextStream();
          }
        }
      }
      if (this.abrManager_) {
        this.abrManager_.configure(this.config_.abr);
        // Simply enable/disable ABR with each call, since multiple calls to these
        // methods have no effect.
        if (this.config_.abr.enabled && !this.switchingPeriods_) {
          this.abrManager_.enable();
        } else {
          this.abrManager_.disable();
        }
  
        this.onAbrStatusChanged_();
      }
      if (this.bufferObserver_) {
        let rebufferThreshold = this.config_.streaming.rebufferingGoal;
        if (this.manifest_) {
          rebufferThreshold =
              Math.max(rebufferThreshold, this.manifest_.minBufferTime);
        }
        this.updateBufferingSettings_(rebufferThreshold);
      }
    }
  
    /**
     * Return a copy of the current configuration.  Modifications of the returned
     * value will not affect the Player's active configuration.  You must call
     * <code>player.configure()</code> to make changes.
     *
     * @return {shaka.extern.PlayerConfiguration}
     * @export
     */
    getConfiguration() {
      goog.asserts.assert(this.config_, 'Config must not be null!');
  
      const ret = this.defaultConfig_();
      shaka.util.PlayerConfiguration.mergeConfigObjects(
          ret, this.config_, this.defaultConfig_());
      return ret;
    }
  
    /**
     * Return a reference to the current configuration. Modifications to the
     * returned value will affect the Player's active configuration. This method
     * is not exported as sharing configuration with external objects is not
     * supported.
     *
     * @return {shaka.extern.PlayerConfiguration}
     */
    getSharedConfiguration() {
      goog.asserts.assert(
          this.config_, 'Cannot call getSharedConfiguration after call destroy!');
      return this.config_;
    }
  
    /**
     * Reset configuration to default.
     * @export
     */
    resetConfiguration() {
      goog.asserts.assert(this.config_, 'Cannot be destroyed');
      // Remove the old keys so we remove open-ended dictionaries like drm.servers
      // but keeps the same object reference.
      for (const key in this.config_) {
        delete this.config_[key];
      }
  
      shaka.util.PlayerConfiguration.mergeConfigObjects(
          this.config_, this.defaultConfig_(), this.defaultConfig_());
      this.applyConfig_();
    }
  
    /**
     * Get the current load mode.
     *
     * @return {shaka.Player.LoadMode}
     * @export
     */
    getLoadMode() {
      return this.loadMode_;
    }
  
    /**
     * Get the media element that the player is currently using to play loaded
     * content. If the player has not loaded content, this will return
     * <code>null</code>.
     *
     * @return {HTMLMediaElement}
     * @export
     */
    getMediaElement() {
      return this.video_;
    }
  
    /**
     * @return {shaka.net.NetworkingEngine} A reference to the Player's networking
     *     engine.  Applications may use this to make requests through Shaka's
     *     networking plugins.
     * @export
     */
    getNetworkingEngine() {
      return this.networkingEngine_;
    }
  
    /**
     * Get the uri to the asset that the player has loaded. If the player has not
     * loaded content, this will return <code>null</code>.
     *
     * @return {?string}
     * @export
     */
    getAssetUri() {
      return this.assetUri_;
    }
  
    /**
     * Get the uri to the asset that the player has loaded. If the player has not
     * loaded content, this will return <code>null</code>.
     *
     * @deprecated Use <code>getAssetUri</code> instead.
     * @return {?string}
     * @export
     */
    getManifestUri() {
      shaka.Deprecate.deprecateFeature(
          2, 6, 'getManifestUri', 'Please use "getAssetUri" instead.');
  
      return this.getAssetUri();
    }
  
    /**
      * Returns a shaka.ads.AdManager instance, responsible for Dynamic
      * Ad Insertion functionality.
      *
      * @return {shaka.ads.AdManager}
      * @export
      */
    getAdManager() {
      // This is a bit redundant, but if we just have "return this.adManager_",
      // the compiler does some black magic on it and always returns undefined.
      return this.adManager_ ? this.adManager_ : null;
    }
  
    /**
     * Get if the player is playing live content. If the player has not loaded
     * content, this will return <code>false</code>.
     *
     * @return {boolean}
     * @export
     */
    isLive() {
      if (this.manifest_) {
        return this.manifest_.presentationTimeline.isLive();
      }
  
      // For native HLS, the duration for live streams seems to be Infinity.
      if (this.video_ && this.video_.src) {
        return this.video_.duration == Infinity;
      }
  
      return false;
    }
  
    /**
     * Get if the player is playing in-progress content. If the player has not
     * loaded content, this will return <code>false</code>.
     *
     * @return {boolean}
     * @export
     */
    isInProgress() {
      return this.manifest_ ?
             this.manifest_.presentationTimeline.isInProgress() :
             false;
    }
  
    /**
     * Check if the manifest contains only audio-only content. If the player has
     * not loaded content, this will return <code>false</code>.
     *
     * <p>
     * The player does not support content that contain more than one type of
     * variants (i.e. mixing audio-only, video-only, audio-video). Content will be
     * filtered to only contain one type of variant.
     *
     * @return {boolean}
     * @export
     */
    isAudioOnly() {
      if (this.manifest_) {
        const periods = this.manifest_.periods;
        if (!periods.length) {
          return false;
        }
  
        const variants = this.manifest_.periods[0].variants;
        if (!variants.length) {
          return false;
        }
  
        // Note that if there are some audio-only variants and some audio-video
        // variants, the audio-only variants are removed during filtering.
        // Therefore if the first variant has no video, that's sufficient to say
        // it is audio-only content.
        return !variants[0].video;
      } else if (this.video_ && this.video_.src) {
        // If we have video track info, use that.  It will be the least
        // error-prone way with native HLS.  In contrast, videoHeight might be
        // unset until the first frame is loaded.  Since isAudioOnly is queried
        // by the UI on the 'trackschanged' event, the videoTracks info should be
        // up-to-date.
        if (this.video_.videoTracks) {
          return this.video_.videoTracks.length == 0;
        }
  
        // We cast to the more specific HTMLVideoElement to access videoHeight.
        // This might be an audio element, though, in which case videoHeight will
        // be undefined at runtime.  For audio elements, this will always return
        // true.
        const video = /** @type {HTMLVideoElement} */(this.video_);
        return video.videoHeight == 0;
      } else {
        return false;
      }
    }
  
    /**
     * Get the range of time (in seconds) that seeking is allowed. If the player
     * has not loaded content, this will return a range from 0 to 0.
     *
     * @return {{start: number, end: number}}
     * @export
     */
    seekRange() {
      if (this.manifest_) {
        const timeline = this.manifest_.presentationTimeline;
  
        return {
          'start': timeline.getSeekRangeStart(),
          'end': timeline.getSeekRangeEnd(),
        };
      }
  
      // If we have loaded content with src=, we ask the video element for its
      // seekable range.  This covers both plain mp4s and native HLS playbacks.
      if (this.video_ && this.video_.src) {
        const seekable = this.video_.seekable;
        if (seekable.length) {
          return {
            'start': seekable.start(0),
            'end': seekable.end(seekable.length - 1),
          };
        }
      }
  
      return {'start': 0, 'end': 0};
    }
  
    /**
     * Get the key system currently used by EME. If EME is not being used, this
     * will return an empty string. If the player has not loaded content, this
     * will return an empty string.
     *
     * @return {string}
     * @export
     */
    keySystem() {
      return shaka.media.DrmEngine.keySystem(this.drmInfo());
    }
  
    /**
     * Get the drm info used to initialize EME. If EME is not being used, this
     * will return <code>null</code>. If the player is idle or has not initialized
     * EME yet, this will return <code>null</code>.
     *
     * @return {?shaka.extern.DrmInfo}
     * @export
     */
    drmInfo() {
      return this.drmEngine_ ? this.drmEngine_.getDrmInfo() : null;
    }
  
    /**
     * Get the next known expiration time for any EME session. If the session
     * never expires, this will return <code>Infinity</code>. If there are no EME
     * sessions, this will return <code>Infinity</code>. If the player has not
     * loaded content, this will return <code>Infinity</code>.
     *
     * @return {number}
     * @export
     */
    getExpiration() {
      return this.drmEngine_ ? this.drmEngine_.getExpiration() : Infinity;
    }
  
    /**
     * Check if the player is currently in a buffering state (has too little
     * content to play smoothly). If the player has not loaded content, this will
     * return <code>false</code>.
     *
     * @return {boolean}
     * @export
     */
    isBuffering() {
      const State = shaka.media.BufferingObserver.State;
      return this.bufferObserver_ ?
             this.bufferObserver_.getState() == State.STARVING :
             false;
    }
  
    /**
     * Get the playback rate of what is playing right now. If we are using trick
     * play, this will return the trick play rate. If no content is playing, this
     * will return 0. If content is buffering, this will return 0.
     *
     * <p>
     * If the player has not loaded content, this will return a playback rate of
     * 0.
     *
     * @return {number}
     * @export
     */
    getPlaybackRate() {
      return this.playRateController_ ?
             this.playRateController_.getActiveRate() :
             0;
    }
  
    /**
     * Enable trick play to skip through content without playing by repeatedly
     * seeking. For example, a rate of 2.5 would result in 2.5 seconds of content
     * being skipped every second. A negative rate will result in moving
     * backwards.
     *
     * <p>
     * If the player has not loaded content or is still loading content this will
     * be a no-op. Wait until <code>load</code> has completed before calling.
     *
     * <p>
     * Trick play will be canceled automatically if the playhead hits the
     * beginning or end of the seekable range for the content.
     *
     * @param {number} rate
     * @export
     */
    trickPlay(rate) {
      // A playbackRate of 0 is used internally when we are in a buffering state,
      // and doesn't make sense for trick play.  If you set a rate of 0 for trick
      // play, we will reject it and issue a warning.  If it happens during a
      // test, we will fail the test through this assertion.
      goog.asserts.assert(rate != 0, 'Should never set a trick play rate of 0!');
      if (rate == 0) {
        shaka.log.alwaysWarn('A trick play rate of 0 is unsupported!');
        return;
      }
  
      if (this.video_.paused) {
        // Our fast forward is implemented with playbackRate and needs the video
        // to be playing (to not be paused) to take immediate effect.
        // If the video is paused, "unpause" it.
        this.video_.play();
      }
      this.playRateController_.set(rate);
  
      if (this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) {
        this.streamingEngine_.setTrickPlay(Math.abs(rate) > 1);
      }
    }
  
    /**
     * Cancel trick-play. If the player has not loaded content or is still loading
     * content this will be a no-op.
     *
     * @export
     */
    cancelTrickPlay() {
      if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
        this.playRateController_.set(1);
      }
  
      if (this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) {
        this.playRateController_.set(1);
        this.streamingEngine_.setTrickPlay(false);
      }
    }
  
    /**
     * Return a list of variant tracks that can be switched to in the current
     * period. If there are multiple periods, you must seek to the period in order
     * to get variants from that period.
     *
     * <p>
     * If the player has not loaded content, this will return an empty list.
     *
     * @return {!Array.<shaka.extern.Track>}
     * @export
     */
    getVariantTracks() {
      if (this.manifest_ && this.playhead_) {
        const currentVariant = this.getPresentationVariant_();
  
        const tracks = [];
  
        // Convert each variant to a track.
        for (const variant of this.getSelectableVariants_()) {
          const track = shaka.util.StreamUtils.variantToTrack(variant);
          track.active = variant == currentVariant;
  
          tracks.push(track);
        }
  
        return tracks;
      } else if (this.video_ && this.video_.audioTracks) {
        // Safari's native HLS always shows a single element in videoTracks.
        // You can't use that API to change resolutions.  But we can use
        // audioTracks to generate a variant list that is usable for changing
        // languages.
        const audioTracks = Array.from(this.video_.audioTracks);
        return audioTracks.map((audio) =>
          shaka.util.StreamUtils.html5AudioTrackToTrack(audio));
      } else {
        return [];
      }
    }
  
    /**
     * Return a list of text tracks that can be switched to in the current period.
     * If there are multiple periods, you must seek to a period in order to get
     * text tracks from that period.
     *
     * <p>
     * If the player has not loaded content, this will return an empty list.
     *
     * @return {!Array.<shaka.extern.Track>}
     * @export
     */
    getTextTracks() {
      if (this.manifest_ && this.playhead_) {
        const currentText = this.getPresentationText_();
        const tracks = [];
  
        // Convert all selectable text streams to tracks.
        for (const text of this.getSelectableText_()) {
          const track = shaka.util.StreamUtils.textStreamToTrack(text);
          track.active = text == currentText;
  
          tracks.push(track);
        }
  
        return tracks;
      } else if (this.video_ && this.video_.src && this.video_.textTracks) {
        const textTracks = Array.from(this.video_.textTracks);
        const StreamUtils = shaka.util.StreamUtils;
        return textTracks.map((text) => StreamUtils.html5TextTrackToTrack(text));
      } else {
        return [];
      }
    }
  
    /**
     * Select a specific text track from the current period. <code>track</code>
     * should come from a call to <code>getTextTracks</code>. If the track is not
     * found in the current period, this will be a no-op. If the player has not
     * loaded content, this will be a no-op.
     *
     * <p>
     * Note that <code>AdaptationEvents</code> are not fired for manual track
     * selections.
     *
     * @param {shaka.extern.Track} track
     * @export
     */
    selectTextTrack(track) {
      if (this.manifest_ && this.streamingEngine_) {
        /** @type {shaka.extern.Period} */
        const period = this.getPresentationPeriod_();
        const stream = period.textStreams.find((stream) => stream.id == track.id);
  
        if (!stream) {
          shaka.log.error('No stream with id', track.id);
          return;
        }
  
        // Add entries to the history.
        this.addTextStreamToSwitchHistory_(
            period, stream, /* fromAdaptation= */ false);
  
        this.switchTextStream_(stream);
  
        // Workaround for https://github.com/google/shaka-player/issues/1299
        // When track is selected, back-propagate the language to
        // currentTextLanguage_.
        this.currentTextLanguage_ = stream.language;
      } else if (this.video_ && this.video_.src && this.video_.textTracks) {
        const textTracks = Array.from(this.video_.textTracks);
        for (const textTrack of textTracks) {
          if (shaka.util.StreamUtils.html5TrackId(textTrack) == track.id) {
            // Leave the track in 'hidden' if it's selected but not showing.
            textTrack.mode = this.isTextVisible_ ? 'showing' : 'hidden';
          } else {
            // Safari allows multiple text tracks to have mode == 'showing', so be
            // explicit in resetting the others.
            textTrack.mode = 'disabled';
          }
        }
        this.onTextChanged_();
      }
    }
  
    /**
     * Find the CEA 608/708 text stream embedded in video, and switch to it.
     *
     * @deprecated Use <code>selectTextTrack</code> instead.
     * @export
     */
    selectEmbeddedTextTrack() {
      shaka.Deprecate.deprecateFeature(
          2, 6,
          'selectEmbeddedTextTrack',
          [
            'If closed captions are signaled in the manifest, a text stream will',
            'be created to represent them. Please use SelectTextTrack.',
          ].join(' '));
  
      const tracks = this.getTextTracks().filter((track) => {
        return track.mimeType == shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE;
      });
  
      if (tracks.length > 0) {
        this.selectTextTrack(tracks[0]);
      } else {
        shaka.log.warning('Unable to find the text track embedded in the video.');
      }
    }
  
    /**
     * @return {boolean} True if we are using any embedded text tracks present.
     *
     * @deprecated Use <code>getTextTracks</code> and the <code>active</code>
     *   property instead.
     * @export
     */
    usingEmbeddedTextTrack() {
      shaka.Deprecate.deprecateFeature(
          2, 6,
          'usingEmbeddedTextTrack',
          [
            'If closed captions are signaled in the manifest, a text stream will',
            'be created to represent them. There should be no reason to know if',
            'the player is playing embedded text.',
          ].join(' '));
  
      const activeTrack = this.getTextTracks().filter((track) => {
        return track.active;
      })[0];
  
      if (activeTrack) {
        return activeTrack.mimeType ==
               shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE;
      }
  
      return false;
    }
  
    /**
     * Select a specific variant track to play from the current period.
     * <code>track</code> should come from a call to
     * <code>getVariantTracks</code>. If <code>track</code> cannot be found in the
     * current variant, this will be a no-op. If the player has not loaded
     * content, this will be a no-op.
     *
     * <p>
     * Changing variants will take effect once the currently buffered content has
     * been played. To force the change to happen sooner, use
     * <code>clearBuffer</code> with <code>safeMargin</code>. Setting
     * <code>clearBuffer</code> to <code>true</code> will clear all buffered
     * content after <code>safeMargin</code>, allowing the new variant to start
     * playing sooner.
     *
     * <p>
     * Note that <code>AdaptationEvents</code> are not fired for manual track
     * selections.
     *
     * @param {shaka.extern.Track} track
     * @param {boolean=} clearBuffer
     * @param {number=} safeMargin Optional amount of buffer (in seconds) to
     *   retain when clearing the buffer. Useful for switching variant quickly
     *   without causing a buffering event. Defaults to 0 if not provided. Ignored
     *   if clearBuffer is false. Can cause hiccups on some browsers if chosen too
     *   small, e.g. The amount of two segments is a fair minimum to consider as
     *   safeMargin value.
     * @export
     */
    selectVariantTrack(track, clearBuffer, safeMargin = 0) {
      if (this.manifest_ && this.streamingEngine_) {
        /** @type {shaka.extern.Period} */
        const period = this.getPresentationPeriod_();
  
        if (this.config_.abr.enabled) {
          shaka.log.alwaysWarn('Changing tracks while abr manager is enabled ' +
                               'will likely result in the selected track ' +
                               'being overriden. Consider disabling abr before ' +
                               'calling selectVariantTrack().');
        }
  
        const variant = period.variants.find((variant) => variant.id == track.id);
        if (!variant) {
          shaka.log.error('No variant with id', track.id);
          return;
        }
  
        // Double check that the track is allowed to be played. The track list
        // should only contain playable variants, but if restrictions change and
        // |selectVariantTrack| is called before the track list is updated, we
        // could get a now-restricted variant.
        if (!shaka.util.StreamUtils.isPlayable(variant)) {
          shaka.log.error('Unable to switch to restricted track', track.id);
          return;
        }
  
        // Add entries to the history.
        this.addVariantToSwitchHistory_(period, variant,
            /* fromAdaptation= */ false);
        this.switchVariant_(variant, clearBuffer, safeMargin);
  
        // Workaround for https://github.com/google/shaka-player/issues/1299
        // When track is selected, back-propagate the language to
        // currentAudioLanguage_.
        this.currentAdaptationSetCriteria_ = new shaka.media.ExampleBasedCriteria(
            variant);
  
        // Update AbrManager variants to match these new settings.
        this.chooseVariant_(period.variants);
      } else if (this.video_ && this.video_.audioTracks) {
        // Safari's native HLS won't let you choose an explicit variant, though
        // you can choose audio languages this way.
        const audioTracks = Array.from(this.video_.audioTracks);
        for (const audioTrack of audioTracks) {
          if (shaka.util.StreamUtils.html5TrackId(audioTrack) == track.id) {
            // This will reset the "enabled" of other tracks to false.
            audioTrack.enabled = true;
          }
        }
        this.onVariantChanged_();
      }
    }
  
    /**
     * Return a list of audio language-role combinations available for the current
     * period. If the player has not loaded any content, this will return an empty
     * list.
     *
     * @return {!Array.<shaka.extern.LanguageRole>}
     * @export
     */
    getAudioLanguagesAndRoles() {
      return shaka.Player.getLanguageAndRolesFrom_(this.getVariantTracks());
    }
  
    /**
     * Return a list of text language-role combinations available for the current
     * period. If the player has not loaded any content, this will be return an
     * empty list.
     *
     * @return {!Array.<shaka.extern.LanguageRole>}
     * @export
     */
    getTextLanguagesAndRoles() {
      return shaka.Player.getLanguageAndRolesFrom_(this.getTextTracks());
    }
  
    /**
     * Return a list of audio languages available for the current period. If the
     * player has not loaded any content, this will return an empty list.
     *
     * @return {!Array.<string>}
     * @export
     */
    getAudioLanguages() {
      return Array.from(shaka.Player.getLanguagesFrom_(this.getVariantTracks()));
    }
  
    /**
     * Return a list of text languages available for the current period. If the
     * player has not loaded any content, this will return an empty list.
     *
     * @return {!Array.<string>}
     * @export
     */
    getTextLanguages() {
      return Array.from(shaka.Player.getLanguagesFrom_(this.getTextTracks()));
    }
  
    /**
     * Sets the current audio language and current variant role to the selected
     * language and role, and chooses a new variant if need be. If the player has
     * not loaded any content, this will be a no-op.
     *
     * @param {string} language
     * @param {string=} role
     * @export
     */
    selectAudioLanguage(language, role) {
      if (this.manifest_ && this.playhead_) {
        /** @type {shaka.extern.Period} */
        const period = this.getPresentationPeriod_();
  
        this.currentAdaptationSetCriteria_ =
            new shaka.media.PreferenceBasedCriteria(language, role || '', 0);
  
        // TODO: Refactor to only change audio and not affect text.
        this.chooseStreamsAndSwitch_(period);
      } else if (this.video_ && this.video_.audioTracks) {
        const audioTracks = Array.from(this.video_.audioTracks);
        for (const audioTrack of audioTracks) {
          if (audioTrack.language == language) {
            // This will reset the "enabled" of other tracks to false.
            audioTrack.enabled = true;
          }
        }
        this.onVariantChanged_();
      }
    }
  
    /**
     * Sets the current text language and current text role to the selected
     * language and role, and chooses a new variant if need be. If the player has
     * not loaded any content, this will be a no-op.
     *
     * @param {string} language
     * @param {string=} role
     * @export
     */
    selectTextLanguage(language, role) {
      if (this.manifest_ && this.playhead_) {
        /** @type {shaka.extern.Period} */
        const period = this.getPresentationPeriod_();
  
        this.currentTextLanguage_ = language;
        this.currentTextRole_ = role || '';
  
        const chosenText = this.chooseTextStream_(period.textStreams);
        if (chosenText) {
          this.addTextStreamToSwitchHistory_(
              period, chosenText, /* fromAdaptation= */ false);
          if (this.shouldStreamText_()) {
            this.switchTextStream_(chosenText);
          }
        }
      } else {
        const tracks = this.getTextTracks().filter((t) => t.language == language);
        const track = tracks[0];
        if (track) {
          this.selectTextTrack(track);
        }
      }
    }
  
    /**
     * Check if the text displayer is enabled.
     *
     * @return {boolean}
     * @export
     */
    isTextTrackVisible() {
      const expected = this.isTextVisible_;
  
      if (this.manifest_) {
        // Make sure our values are still in-sync.
        const actual = this.mediaSourceEngine_.getTextDisplayer().isTextVisible();
        goog.asserts.assert(
            actual == expected, 'text visibility has fallen out of sync');
  
        // Always return the actual value so that the app has the most accurate
        // information (in the case that the values come out of sync in prod).
        return actual;
      } else if (this.video_ && this.video_.src && this.video_.textTracks) {
        const textTracks = Array.from(this.video_.textTracks);
        return textTracks.some((t) => t.mode == 'showing');
      }
  
      return expected;
    }
  
    /**
     * Enable or disable the text displayer.  If the player is in an unloaded
     * state, the request will be applied next time content is loaded.
     *
     * @param {boolean} isVisible
     * @return {!Promise}
     * @export
     */
    async setTextTrackVisibility(isVisible) {
      const oldVisibilty = this.isTextVisible_;
      const newVisibility = isVisible;
  
      if (oldVisibilty == newVisibility) {
        return;
      }
  
      this.isTextVisible_ = newVisibility;
  
      // Hold of on setting the text visibility until we have all the components
      // we need. This ensures that they stay in-sync.
      if (this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) {
        this.mediaSourceEngine_.getTextDisplayer().setTextVisibility(isVisible);
  
        // When the user wants to see captions, we stream captions. When the user
        // doesn't want to see captions, we don't stream captions. This is to
        // avoid bandwidth consumption by an unused resource. The app developer
        // can override this and configure us to always stream captions.
        if (!this.config_.streaming.alwaysStreamText) {
          if (isVisible) {
            // Find the text stream that best matches the user's preferences.
            /** @type {shaka.extern.Period} */
            const period = this.getPresentationPeriod_();
            const streams = shaka.util.StreamUtils.filterStreamsByLanguageAndRole(
                period.textStreams, this.currentTextLanguage_,
                this.currentTextRole_);
  
            // It is possible that there are no streams to play.
            if (streams.length > 0) {
              await this.streamingEngine_.loadNewTextStream(streams[0]);
            }
          } else {
            this.streamingEngine_.unloadTextStream();
          }
        }
      } else if (this.video_ && this.video_.src && this.video_.textTracks) {
        const textTracks = Array.from(this.video_.textTracks);
  
        // Find the active track by looking for one which is not disabled.  This
        // is the only way to identify the track which is currently displayed.
        // Set it to 'showing' or 'hidden' based on isVisible.
        for (const textTrack of textTracks) {
          if (textTrack.mode != 'disabled') {
            textTrack.mode = isVisible ? 'showing' : 'hidden';
          }
        }
      }
  
      // We need to fire the event after we have updated everything so that
      // everything will be in a stable state when the app responds to the
      // event.
      this.onTextTrackVisibility_();
    }
  
    /**
     * Get the current playhead position as a date. This should only be called
     * when the player has loaded a live stream. If the player has not loaded a
     * live stream, this will return <code>null</code>.
     *
     * @return {Date}
     * @export
     */
    getPlayheadTimeAsDate() {
      if (!this.isLive()) {
        shaka.log.warning('getPlayheadTimeAsDate is for live streams!');
        return null;
      }
  
      if (this.manifest_) {
        const timeline = this.manifest_.presentationTimeline;
        const startTime = timeline.getPresentationStartTime();
        const presentationTime = this.video_.currentTime;
        return new Date(/* ms= */ (startTime + presentationTime) * 1000);
      } else if (this.video_ && this.video_.getStartDate) {
        // Apple's native HLS gives us getStartDate(), which is only available if
        // EXT-X-PROGRAM-DATETIME is in the playlist.
        const startDate = this.video_.getStartDate();
        if (isNaN(startDate.getTime())) {
          shaka.log.warning(
              'EXT-X-PROGRAM-DATETIME required to get playhead time as Date!');
          return null;
        }
        return new Date(startDate.getTime() + (this.video_.currentTime * 1000));
      } else {
        shaka.log.warning('No way to get playhead time as Date!');
        return null;
      }
    }
  
    /**
     * Get the presentation start time as a date. This should only be called when
     * the player has loaded a live stream. If the player has not loaded a live
     * stream, this will return <code>null</code>.
     *
     * @return {Date}
     * @export
     */
    getPresentationStartTimeAsDate() {
      if (!this.isLive()) {
        shaka.log.warning('getPresentationStartTimeAsDate is for live streams!');
        return null;
      }
  
      if (this.manifest_) {
        const timeline = this.manifest_.presentationTimeline;
        const startTime = timeline.getPresentationStartTime();
        return new Date(/* ms= */ startTime * 1000);
      } else if (this.video_ && this.video_.getStartDate) {
        // Apple's native HLS gives us getStartDate(), which is only available if
        // EXT-X-PROGRAM-DATETIME is in the playlist.
        const startDate = this.video_.getStartDate();
        if (isNaN(startDate.getTime())) {
          shaka.log.warning(
              'EXT-X-PROGRAM-DATETIME required to get presentation start time ' +
              'as Date!');
          return null;
        }
        return startDate;
      } else {
        shaka.log.warning('No way to get presentation start time as Date!');
        return null;
      }
    }
  
    /**
     * Get information about what the player has buffered. If the player has not
     * loaded content or is currently loading content, the buffered content will
     * be empty.
     *
     * @return {shaka.extern.BufferedInfo}
     * @export
     */
    getBufferedInfo() {
      const info = {
        total: [],
        audio: [],
        video: [],
        text: [],
      };
  
      if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
        const TimeRangesUtils = shaka.media.TimeRangesUtils;
        info.total = TimeRangesUtils.getBufferedInfo(this.video_.buffered);
      }
  
      if (this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) {
        this.mediaSourceEngine_.getBufferedInfo(info);
      }
  
      return info;
    }
  
    /**
     * Get statistics for the current playback session. If the player is not
     * playing content, this will return an empty stats object.
     *
     * @return {shaka.extern.Stats}
     * @export
     */
    getStats() {
      // If the Player is not in a fully-loaded state, then return an empty stats
      // blob so that this call will never fail.
      const loaded = this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE ||
                     this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS;
      if (!loaded) {
        return shaka.util.Stats.getEmptyBlob();
      }
  
      this.updateStateHistory_();
  
      goog.asserts.assert(this.video_, 'If we have stats, we should have video_');
      const element = /** @type {!HTMLVideoElement} */ (this.video_);
  
      if (element.getVideoPlaybackQuality) {
        const info = element.getVideoPlaybackQuality();
  
        this.stats_.setDroppedFrames(
            Number(info.droppedVideoFrames),
            Number(info.totalVideoFrames));
      }
  
      if (this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) {
        // Event through we are loaded, it is still possible that we don't have a
        // presentation variant yet because we set the load mode before we select
        // the first variant to stream.
        const variant = this.getPresentationVariant_();
  
        if (variant) {
          this.stats_.setVariantBandwidth(variant.bandwidth);
        }
  
        if (variant && variant.video) {
          this.stats_.setResolution(
              /* width= */ variant.video.width || NaN,
              /* height= */ variant.video.height || NaN);
        }
  
        const estimate = this.abrManager_.getBandwidthEstimate();
        this.stats_.setBandwidthEstimate(estimate);
      }
  
      return this.stats_.getBlob();
    }
  
    /**
     * Adds the given text track to the current Period.  <code>load()</code> must
     * resolve before calling.  The current Period or the presentation must have a
     * duration.
     * This returns a Promise that will resolve with the track that was created,
     * when that track can be switched to.
     *
     * @param {string} uri
     * @param {string} language
     * @param {string} kind
     * @param {string} mime
     * @param {string=} codec
     * @param {string=} label
     * @return {!Promise.<shaka.extern.Track>}
     * @export
     */
    async addTextTrack(uri, language, kind, mime, codec, label) {
      // TODO: Add an actual error for this.
      if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
        shaka.log.error('Cannot add text when loaded with src=');
        throw new Error('State error!');
      }
      if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) {
        shaka.log.error(
            'Must call load() and wait for it to resolve before adding text ' +
            'tracks.');
        throw new Error('State error!');
      }
  
      /** @type {shaka.extern.Period} */
      const period = this.getPresentationPeriod_();
  
      const ContentType = shaka.util.ManifestParserUtils.ContentType;
  
      // Get the Period duration.
      /** @type {number} */
      const periodIndex = this.manifest_.periods.indexOf(period);
      /** @type {number} */
      const nextPeriodIndex = periodIndex + 1;
      /** @type {number} */
      const nextPeriodStart = nextPeriodIndex >= this.manifest_.periods.length ?
                              this.manifest_.presentationTimeline.getDuration() :
                              this.manifest_.periods[nextPeriodIndex].startTime;
      /** @type {number} */
      const periodDuration = nextPeriodStart - period.startTime;
      if (periodDuration == Infinity) {
        throw new shaka.util.Error(
            shaka.util.Error.Severity.RECOVERABLE,
            shaka.util.Error.Category.MANIFEST,
            shaka.util.Error.Code.CANNOT_ADD_EXTERNAL_TEXT_TO_LIVE_STREAM);
      }
  
      /** @type {shaka.extern.Stream} */
      const stream = {
        id: this.nextExternalStreamId_++,
        originalId: null,
        createSegmentIndex: () => Promise.resolve(),
        segmentIndex: shaka.media.SegmentIndex.forSingleSegment(
            /* duration */ periodDuration,
            /* uris */ [uri]),
        mimeType: mime,
        codecs: codec || '',
        kind: kind,
        encrypted: false,
        keyId: null,
        language: language,
        label: label || null,
        type: ContentType.TEXT,
        primary: false,
        trickModeVideo: null,
        emsgSchemeIdUris: null,
        roles: [],
        channelsCount: null,
        closedCaptions: null,
      };
  
      // Add the stream to the loading list to ensure it isn't switched to while
      // it is initializing.
      this.loadingTextStreams_.add(stream);
      period.textStreams.push(stream);
  
      await this.streamingEngine_.loadNewTextStream(stream);
      goog.asserts.assert(period, 'The period should still be non-null here.');
  
      const activeText = this.streamingEngine_.getBufferingText();
      if (activeText) {
        // If this was the first text stream, StreamingEngine will start streaming
        // it in loadNewTextStream.  To reflect this, update the active stream.
        this.activeStreams_.useText(period, activeText);
      }
  
      // Remove the stream from the loading list.
      this.loadingTextStreams_.delete(stream);
  
      shaka.log.debug('Choosing new streams after adding a text stream');
      this.chooseStreamsAndSwitch_(period);
      this.onTracksChanged_();
  
      return shaka.util.StreamUtils.textStreamToTrack(stream);
    }
  
    /**
     * Set the maximum resolution that the platform's hardware can handle.
     * This will be called automatically by <code>shaka.cast.CastReceiver</code>
     * to enforce limitations of the Chromecast hardware.
     *
     * @param {number} width
     * @param {number} height
     * @export
     */
    setMaxHardwareResolution(width, height) {
      this.maxHwRes_.width = width;
      this.maxHwRes_.height = height;
    }
  
    /**
     * Retry streaming after a streaming failure has occurred. When the player has
     * not loaded content or is loading content, this will be a no-op and will
     * return <code>false</code>.
     *
     * <p>
     * If the player has loaded content, and streaming has not seen an error, this
     * will return <code>false</code>.
     *
     * <p>
     * If the player has loaded content, and streaming seen an error, but the
     * could not resume streaming, this will return <code>false</code>.
     *
     * @return {boolean}
     * @export
     */
    retryStreaming() {
      return this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE ?
             this.streamingEngine_.retry() :
             false;
    }
  
    /**
     * Get the manifest that the player has loaded. If the player has not loaded
     * any content, this will return <code>null</code>.
     *
     * @return {?shaka.extern.Manifest}
     * @export
     */
    getManifest() {
      return this.manifest_;
    }
  
    /**
     * Get the type of manifest parser that the player is using. If the player has
     * not loaded any content, this will return <code>null</code>.
     *
     * @return {?shaka.extern.ManifestParser.Factory}
     * @export
     */
    getManifestParserFactory() {
      return this.parser_ ? this.parser_.constructor : null;
    }
  
    /**
     * @param {shaka.extern.Period} period
     * @param {shaka.extern.Variant} variant
     * @param {boolean} fromAdaptation
     * @private
     */
    addVariantToSwitchHistory_(period, variant, fromAdaptation) {
      this.activeStreams_.useVariant(period, variant);
      const switchHistory = this.stats_.getSwitchHistory();
      switchHistory.updateCurrentVariant(variant, fromAdaptation);
    }
  
    /**
     * @param {shaka.extern.Period} period
     * @param {shaka.extern.Stream} textStream
     * @param {boolean} fromAdaptation
     * @private
     */
    addTextStreamToSwitchHistory_(period, textStream, fromAdaptation) {
      this.activeStreams_.useText(period, textStream);
      const switchHistory = this.stats_.getSwitchHistory();
      switchHistory.updateCurrentText(textStream, fromAdaptation);
    }
  
    /**
     * @return {shaka.extern.PlayerConfiguration}
     * @private
     */
    defaultConfig_() {
      const config = shaka.util.PlayerConfiguration.createDefault();
  
      config.streaming.failureCallback = (error) => {
        this.defaultStreamingFailureCallback_(error);
      };
  
      // Because this.video_ may not be set when the config is built, the default
      // TextDisplay factory must capture a reference to "this" as "self" to use
      // at the time we call the factory.  Bind can't be used here because we
      // call the factory with "new", effectively removing any binding to "this".
      const self = this;
      // eslint-disable-next-line no-restricted-syntax
      config.textDisplayFactory = function() {
        return new shaka.text.SimpleTextDisplayer(self.video_);
      };
  
      return config;
    }
  
    /**
     * @param {!shaka.util.Error} error
     * @private
     */
    defaultStreamingFailureCallback_(error) {
      const retryErrorCodes = [
        shaka.util.Error.Code.BAD_HTTP_STATUS,
        shaka.util.Error.Code.HTTP_ERROR,
        shaka.util.Error.Code.TIMEOUT,
      ];
  
      if (this.isLive() && retryErrorCodes.includes(error.code)) {
        error.severity = shaka.util.Error.Severity.RECOVERABLE;
  
        shaka.log.warning('Live streaming error.  Retrying automatically...');
        this.retryStreaming();
      }
    }
  
    /**
     * For CEA closed captions embedded in the video streams, create dummy text
     * stream.
     * @param {!Array.<!shaka.extern.Period>} periods
     * @private
     */
    createTextStreamsForClosedCaptions_(periods) {
      const ContentType = shaka.util.ManifestParserUtils.ContentType;
      const TextStreamKind = shaka.util.ManifestParserUtils.TextStreamKind;
  
      for (const period of periods) {
        // A map of the closed captions id and the new dummy text stream.
        const closedCaptionsMap = new Map();
        for (const variant of period.variants) {
          if (variant.video && variant.video.closedCaptions) {
            const video = variant.video;
            for (const id of video.closedCaptions.keys()) {
              if (!closedCaptionsMap.has(id)) {
                const textStream = {
                  id: this.nextExternalStreamId_++,  // A globally unique ID.
                  originalId: id, // The CC ID string, like 'CC1', 'CC3', etc.
                  createSegmentIndex: () => Promise.resolve(),
                  segmentIndex: null,
                  mimeType: shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE,
                  codecs: '',
                  kind: TextStreamKind.CLOSED_CAPTION,
                  encrypted: false,
                  keyId: null,
                  language: video.closedCaptions.get(id),
                  label: null,
                  type: ContentType.TEXT,
                  primary: false,
                  trickModeVideo: null,
                  emsgSchemeIdUris: null,
                  roles: video.roles,
                  channelsCount: null,
                  closedCaptions: null,
                };
                closedCaptionsMap.set(id, textStream);
              }
            }
          }
        }
        for (const textStream of closedCaptionsMap.values()) {
          period.textStreams.push(textStream);
        }
      }
    }
  
    /**
     * Filters a list of periods.
     * @param {!Array.<!shaka.extern.Period>} periods
     * @private
     */
    filterAllPeriods_(periods) {
      goog.asserts.assert(this.video_, 'Must not be destroyed');
      const ArrayUtils = shaka.util.ArrayUtils;
      const StreamUtils = shaka.util.StreamUtils;
  
      /** @type {?shaka.extern.Stream} */
      const activeAudio = this.streamingEngine_ ?
                          this.streamingEngine_.getBufferingAudio() :
                          null;
      /** @type {?shaka.extern.Stream} */
      const activeVideo = this.streamingEngine_ ?
                          this.streamingEngine_.getBufferingVideo() :
                          null;
  
      for (const period of periods) {
        StreamUtils.filterNewPeriod(
            this.drmEngine_, activeAudio, activeVideo, period);
      }
  
      const validPeriodsCount = ArrayUtils.count(periods, (period) => {
        return period.variants.some(StreamUtils.isPlayable);
      });
  
      // If none of the periods are playable, throw
      // CONTENT_UNSUPPORTED_BY_BROWSER.
      if (validPeriodsCount == 0) {
        throw new shaka.util.Error(
            shaka.util.Error.Severity.CRITICAL,
            shaka.util.Error.Category.MANIFEST,
            shaka.util.Error.Code.CONTENT_UNSUPPORTED_BY_BROWSER);
      }
  
      // If only some of the periods are playable, throw UNPLAYABLE_PERIOD.
      if (validPeriodsCount < periods.length) {
        throw new shaka.util.Error(
            shaka.util.Error.Severity.CRITICAL,
            shaka.util.Error.Category.MANIFEST,
            shaka.util.Error.Code.UNPLAYABLE_PERIOD);
      }
  
      for (const period of periods) {
        const tracksChanged = shaka.util.StreamUtils.applyRestrictions(
            period.variants, this.config_.restrictions, this.maxHwRes_);
        if (tracksChanged && this.streamingEngine_ &&
            this.getPresentationPeriod_() == period) {
          this.onTracksChanged_();
        }
  
        this.checkRestrictedVariants_(period.variants);
      }
    }
  
    /**
     * Filters a new period.
     * @param {shaka.extern.Period} period
     * @private
     */
    filterNewPeriod_(period) {
      goog.asserts.assert(this.video_, 'Must not be destroyed');
      const StreamUtils = shaka.util.StreamUtils;
  
      /** @type {?shaka.extern.Stream} */
      const activeAudio = this.streamingEngine_ ?
                          this.streamingEngine_.getBufferingAudio() :
                          null;
      /** @type {?shaka.extern.Stream} */
      const activeVideo = this.streamingEngine_ ?
                          this.streamingEngine_.getBufferingVideo() :
                          null;
  
      StreamUtils.filterNewPeriod(
          this.drmEngine_, activeAudio, activeVideo, period);
  
      /** @type {!Array.<shaka.extern.Variant>} */
      const variants = period.variants;
  
      // Check for playable variants before restrictions, so that we can give a
      // special error when there were tracks but they were all filtered.
      const hasPlayableVariant = variants.some(StreamUtils.isPlayable);
      if (!hasPlayableVariant) {
        throw new shaka.util.Error(
            shaka.util.Error.Severity.CRITICAL,
            shaka.util.Error.Category.MANIFEST,
            shaka.util.Error.Code.UNPLAYABLE_PERIOD);
      }
  
      this.checkRestrictedVariants_(period.variants);
  
      const tracksChanged = shaka.util.StreamUtils.applyRestrictions(
          variants, this.config_.restrictions, this.maxHwRes_);
  
      // Trigger the track change event if the restrictions now prevent use from
      // using a variant that we previously thought we could use.
      if (tracksChanged && this.streamingEngine_ &&
          this.getPresentationPeriod_() == period) {
        this.onTracksChanged_();
      }
  
      // For new Periods, we may need to create new sessions for any new init
      // data.
      const curDrmInfo = this.drmEngine_ ? this.drmEngine_.getDrmInfo() : null;
      if (curDrmInfo) {
        for (const variant of variants) {
          for (const drmInfo of variant.drmInfos) {
            // Ignore any data for different key systems.
            if (drmInfo.keySystem == curDrmInfo.keySystem) {
              for (const initData of (drmInfo.initData || [])) {
                this.drmEngine_.newInitData(
                    initData.initDataType, initData.initData);
              }
            }
          }
        }
      }
    }
  
    /**
     * Switches to the given variant, deferring if needed.
     * @param {shaka.extern.Variant} variant
     * @param {boolean=} clearBuffer
     * @param {number=} safeMargin
     * @private
     */
    switchVariant_(variant, clearBuffer = false, safeMargin = 0) {
      if (this.switchingPeriods_) {
        // Store this action for later.
        this.deferredVariant_ = variant;
        this.deferredVariantClearBuffer_ = clearBuffer;
        this.deferredVariantClearBufferSafeMargin_ = safeMargin;
      } else {
        // Act now.
        this.streamingEngine_.switchVariant(variant, clearBuffer, safeMargin);
        // Dispatch a 'variantchanged' event
        this.onVariantChanged_();
      }
    }
  
    /**
     * Switches to the given text stream, deferring if needed.
     * @param {shaka.extern.Stream} textStream
     * @private
     */
    switchTextStream_(textStream) {
      if (this.switchingPeriods_) {
        // Store this action for later.
        this.deferredTextStream_ = textStream;
      } else {
        // Act now.
        this.streamingEngine_.switchTextStream(textStream);
        this.onTextChanged_();
      }
    }
  
    /**
     * Verifies that the active streams according to the player match those in
     * StreamingEngine.
     * @private
     */
    assertCorrectActiveStreams_() {
      if (!this.streamingEngine_ || !this.manifest_ || !goog.DEBUG) {
        return;
      }
  
      const activePeriod = this.streamingEngine_.getBufferingPeriod();
      /** @type {shaka.extern.Period} */
      const currentPeriod = this.getPresentationPeriod_();
      if (activePeriod == null || activePeriod != currentPeriod) {
        return;
      }
  
      const activeAudio = this.streamingEngine_.getBufferingAudio();
      const activeVideo = this.streamingEngine_.getBufferingVideo();
      const activeText = this.streamingEngine_.getBufferingText();
  
      // If we have deferred variants/text we want to compare against those rather
      // than what we are actually streaming.
      const expectedAudio = this.deferredVariant_ ?
                            this.deferredVariant_.audio :
                            activeAudio;
  
      const expectedVideo = this.deferredVariant_ ?
                            this.deferredVariant_.video :
                            activeVideo;
  
      const expectedText = this.deferredTextStream_ || activeText;
  
      const actualVariant = this.activeStreams_.getVariant(currentPeriod);
      const actualText = this.activeStreams_.getText(currentPeriod);
  
      goog.asserts.assert(
          actualVariant.audio == expectedAudio,
          'Inconsistent active audio stream');
      goog.asserts.assert(
          actualVariant.video == expectedVideo,
          'Inconsistent active video stream');
  
      // Because we always set a text stream to be active in the active stream
      // map, regardless of whether or not we are actually streaming text, it is
      // possible for these to be out of line.
      goog.asserts.assert(
          expectedText == null || actualText == expectedText,
          'Inconsistent active text stream');
    }
  
    /**
     * @param {number} time
     * @return {number}
     * @private
     */
    adjustStartTime_(time) {
      /** @type {?shaka.extern.Stream} */
      const activeAudio = this.streamingEngine_.getBufferingAudio();
      /** @type {?shaka.extern.Stream} */
      const activeVideo = this.streamingEngine_.getBufferingVideo();
      /** @type {shaka.extern.Period} */
      const period = this.getPresentationPeriod_();
  
      // This method is called after StreamingEngine.init resolves, which means
      // that all the active streams have had createSegmentIndex called.
      function getAdjustedTime(stream, time) {
        if (!stream) {
          return null;
        }
        const idx = stream.segmentIndex.find(time - period.startTime);
        if (idx == null) {
          return null;
        }
        const ref = stream.segmentIndex.get(idx);
        if (!ref) {
          return null;
        }
        const refTime = ref.startTime + period.startTime;
        goog.asserts.assert(refTime <= time, 'Segment should start before time');
        return refTime;
      }
  
      const audioStartTime = getAdjustedTime(activeAudio, time);
      const videoStartTime = getAdjustedTime(activeVideo, time);
  
      // If we have both video and audio times, pick the larger one.  If we picked
      // the smaller one, that one will download an entire segment to buffer the
      // difference.
      if (videoStartTime != null && audioStartTime != null) {
        return Math.max(videoStartTime, audioStartTime);
      } else if (videoStartTime != null) {
        return videoStartTime;
      } else if (audioStartTime != null) {
        return audioStartTime;
      } else {
        return time;
      }
    }
  
    /**
     * Update the buffering state to be either "we are buffering" or "we are not
     * buffering", firing events to the app as needed.
     *
     * @private
     */
    updateBufferState_() {
      const isBuffering = this.isBuffering();
  
      // Make sure we have all the components we need before we consider ourselves
      // as being loaded.
      // TODO: Make the check for "loaded" simpler.
      const loaded = this.stats_ && this.bufferObserver_ && this.playhead_;
  
      if (loaded) {
        this.playRateController_.setBuffering(isBuffering);
        this.updateStateHistory_();
      }
  
      // Surface the buffering event so that the app knows if/when we are
      // buffering.
      const event =
          new shaka.util.FakeEvent('buffering', {'buffering': isBuffering});
      this.dispatchEvent(event);
    }
  
    /**
     * Callback from PlayheadObserver.
     * @private
     */
    onChangePeriod_() {
      this.onTracksChanged_();
    }
  
    /**
     * A callback for when the playback rate changes. We need to watch the
     * playback rate so that if the playback rate on the media element changes
     * (that was not caused by our play rate controller) we can notify the
     * controller so that it can stay in-sync with the change.
     *
     * @private
     */
    onRateChange_() {
      /** @type {number} */
      const newRate = this.video_.playbackRate;
  
      // On Edge, when someone seeks using the native controls, it will set the
      // playback rate to zero until they finish seeking, after which it will
      // return the playback rate.
      //
      // If the playback rate changes while seeking, Edge will cache the playback
      // rate and use it after seeking.
      //
      // https://github.com/google/shaka-player/issues/951
      if (newRate == 0) {
        return;
      }
  
      // The playback rate has changed. This could be us or someone else.
      // If this was us, setting the rate again will be a no-op.
      this.playRateController_.set(newRate);
    }
  
    /**
     * Try updating the state history. If the player has not finished
     * initializing, this will be a no-op.
     *
     * @private
     */
    updateStateHistory_() {
      // If we have not finish initializing, this will be a no-op.
      if (!this.stats_) {
        return;
      }
      if (!this.bufferObserver_) {
        return;
      }
  
      const State = shaka.media.BufferingObserver.State;
  
      const history = this.stats_.getStateHistory();
  
      if (this.bufferObserver_.getState() == State.STARVING) {
        history.update('buffering');
      } else if (this.video_.paused) {
        history.update('paused');
      } else if (this.video_.ended) {
        history.update('ended');
      } else {
        history.update('playing');
      }
    }
  
    /**
     * Callback from Playhead.
     *
     * @private
     */
    onSeek_() {
      if (this.playheadObservers_) {
        this.playheadObservers_.notifyOfSeek();
      }
      if (this.streamingEngine_) {
        this.streamingEngine_.seeked();
      }
      if (this.bufferObserver_) {
        // If we seek into an unbuffered range, we should fire a 'buffering' event
        // immediately.  If StreamingEngine can buffer fast enough, we may not
        // update our buffering tracking otherwise.
        this.pollBufferState_();
      }
    }
  
    /**
     * Chooses a variant from all possible variants while taking into account
     * restrictions, preferences, and ABR.
     *
     * On error, this dispatches an error event and returns null.
     *
     * @param {!Array.<shaka.extern.Variant>} allVariants
     * @return {?shaka.extern.Variant}
     * @private
     */
    chooseVariant_(allVariants) {
      goog.asserts.assert(this.config_, 'Must not be destroyed');
  
      try {
        // |variants| are the filtered variants, use |period.variants| so we know
        // why they we restricted.
        this.checkRestrictedVariants_(allVariants);
      } catch (e) {
        this.onError_(e);
        return null;
      }
  
      goog.asserts.assert(
          allVariants.length, 'Should have thrown for no Variants.');
  
      const playableVariants = allVariants.filter((variant) => {
        return shaka.util.StreamUtils.isPlayable(variant);
      });
  
      // Update the abr manager with newly filtered variants.
      const adaptationSet = this.currentAdaptationSetCriteria_.create(
          playableVariants);
      this.abrManager_.setVariants(Array.from(adaptationSet.values()));
      return this.abrManager_.chooseVariant();
    }
  
    /**
     * Choose a text stream from all possible text streams while taking into
     * account user preference.
     *
     * @param {!Array.<shaka.extern.Stream>} textStreams
     * @return {?shaka.extern.Stream}
     * @private
     */
    chooseTextStream_(textStreams) {
      const subset = shaka.util.StreamUtils.filterStreamsByLanguageAndRole(
          textStreams,
          this.currentTextLanguage_,
          this.currentTextRole_);
  
      return subset[0] || null;
    }
  
    /**
     * Chooses streams from the given Period and switches to them.
     * Called after a config change, a new text stream, a key status event, or an
     * explicit language change.
     *
     * @param {!shaka.extern.Period} period
     * @private
     */
    chooseStreamsAndSwitch_(period) {
      goog.asserts.assert(this.config_, 'Must not be destroyed');
  
      // Because we're running this after a config change (manual language
      // change), a new text stream, or a key status event, and because switching
      // to an active stream is a no-op, it is always okay to clear the buffer
      // here.
      const chosenVariant = this.chooseVariant_(period.variants);
      if (chosenVariant) {
        this.addVariantToSwitchHistory_(
            period, chosenVariant, /* fromAdaptation= */ true);
        this.switchVariant_(chosenVariant, /* clearBuffers */ true);
      }
  
      // Only switch text if we should be streaming text right now.
      const chosenText = this.chooseTextStream_(period.textStreams);
      if (chosenText && this.shouldStreamText_()) {
        this.addTextStreamToSwitchHistory_(
            period, chosenText, /* fromAdaptation= */ true);
        this.switchTextStream_(chosenText);
      }
  
      // Send an adaptation event so that the UI can show the new language/tracks.
      this.onAdaptation_();
    }
  
    /**
     * Callback from StreamingEngine, invoked when a period starts. This method
     * must always "succeed" so it may not throw an error. Any errors must be
     * routed to |onError|.
     *
     * @param {!shaka.extern.Period} period
     * @return {shaka.media.StreamingEngine.ChosenStreams}
     *    An object containing the chosen variant and text stream.
     * @private
     */
    onChooseStreams_(period) {
      shaka.log.debug('onChooseStreams_', period);
  
      goog.asserts.assert(this.config_, 'Must not be destroyed');
  
      try {
        shaka.log.v2('onChooseStreams_, choosing variant from ', period.variants);
        shaka.log.v2('onChooseStreams_, choosing text from ', period.textStreams);
  
        const chosen = this.chooseStreams_(period);
  
        shaka.log.v2('onChooseStreams_, chose variant ', chosen.variant);
        shaka.log.v2('onChooseStreams_, chose text ', chosen.text);
  
        return chosen;
      } catch (e) {
        this.onError_(e);
        return {variant: null, text: null};
      }
    }
  
    /**
     * This is the internal logic for |onChooseStreams_|. This separation is done
     * to allow this implementation to throw errors without consequence.
     *
     * @param {shaka.extern.Period} period
     *    The period that we are selecting streams from.
     * @return {shaka.media.StreamingEngine.ChosenStreams}
     *    An object containing the chosen variant and text stream.
     * @private
     */
    chooseStreams_(period) {
      // We are switching Periods, so the AbrManager will be disabled.  But if we
      // want to abr.enabled, we do not want to call AbrManager.enable before
      // canSwitch_ is called.
      this.switchingPeriods_ = true;
      this.abrManager_.disable();
      this.onAbrStatusChanged_();
  
      shaka.log.debug('Choosing new streams after period changed');
  
      let chosenVariant = this.chooseVariant_(period.variants);
      let chosenText = this.chooseTextStream_(period.textStreams);
  
      // Ignore deferred variant or text streams only if we are starting a new
      // period.  In this case, any deferred switches were from an older period,
      // so they do not apply.  We can still have deferred switches from the
      // current period in the case of an early call to select*Track while we are
      // setting up the first period.  This can happen with the 'streaming' event.
      if (this.deferredVariant_) {
        if (period.variants.includes(this.deferredVariant_)) {
          chosenVariant = this.deferredVariant_;
        }
        this.deferredVariant_ = null;
      }
  
      if (this.deferredTextStream_) {
        if (period.textStreams.includes(this.deferredTextStream_)) {
          chosenText = this.deferredTextStream_;
        }
        this.deferredTextStream_ = null;
      }
  
      if (chosenVariant) {
        this.addVariantToSwitchHistory_(
            period, chosenVariant, /* fromAdaptation= */ true);
      }
  
      if (chosenText) {
        this.addTextStreamToSwitchHistory_(
            period, chosenText, /* fromAdaptation= */ true);
      }
  
      // Check if we should show text (based on difference between audio and text
      // languages). Only check this during startup so we don't "pop-up" captions
      // mid playback.
      const startingUp = !this.streamingEngine_.getBufferingPeriod();
      const chosenAudio = chosenVariant ? chosenVariant.audio : null;
      if (startingUp && chosenText) {
        if (chosenAudio && this.shouldShowText_(chosenAudio, chosenText)) {
          this.isTextVisible_ = true;
        }
        if (this.isTextVisible_) {
          // If the cached value says to show text, then update the text displayer
          // since it defaults to not shown.  Note that returning the |chosenText|
          // below will make StreamingEngine stream the text.
          this.mediaSourceEngine_.getTextDisplayer().setTextVisibility(true);
          goog.asserts.assert(this.shouldStreamText_(),
              'Should be streaming text');
        }
        this.onTextTrackVisibility_();
      }
  
      // Don't fire a tracks-changed event since we aren't inside the new Period
      // yet.
      // Don't initialize with a text stream unless we should be streaming text.
      if (this.shouldStreamText_()) {
        return {variant: chosenVariant, text: chosenText};
      } else {
        return {variant: chosenVariant, text: null};
      }
    }
  
    /**
     * Check if we should show text on screen automatically.
     *
     * The text should automatically be shown if the text is language-compatible
     * with the user's text language preference, but not compatible with the
     * audio.
     *
     * For example:
     *   preferred | chosen | chosen |
     *   text      | text   | audio  | show
     *   -----------------------------------
     *   en-CA     | en     | jp     | true
     *   en        | en-US  | fr     | true
     *   fr-CA     | en-US  | jp     | false
     *   en-CA     | en-US  | en-US  | false
     *
     * @param {shaka.extern.Stream} audioStream
     * @param {shaka.extern.Stream} textStream
     * @return {boolean}
     * @private
     */
    shouldShowText_(audioStream, textStream) {
      const LanguageUtils = shaka.util.LanguageUtils;
  
      /** @type {string} */
      const preferredTextLocale =
          LanguageUtils.normalize(this.config_.preferredTextLanguage);
      /** @type {string} */
      const audioLocale = LanguageUtils.normalize(audioStream.language);
      /** @type {string} */
      const textLocale = LanguageUtils.normalize(textStream.language);
  
      return (
        LanguageUtils.areLanguageCompatible(textLocale, preferredTextLocale) &&
        !LanguageUtils.areLanguageCompatible(audioLocale, textLocale));
    }
  
    /**
     * Callback from StreamingEngine, invoked when the period is set up.
     *
     * @private
     */
    canSwitch_() {
      shaka.log.debug('canSwitch_');
      goog.asserts.assert(this.config_, 'Must not be destroyed');
  
      this.switchingPeriods_ = false;
  
      if (this.config_.abr.enabled) {
        this.abrManager_.enable();
        this.onAbrStatusChanged_();
      }
  
      // If we still have deferred switches, switch now.
      if (this.deferredVariant_) {
        this.streamingEngine_.switchVariant(
            this.deferredVariant_, this.deferredVariantClearBuffer_,
            this.deferredVariantClearBufferSafeMargin_);
        this.onVariantChanged_();
        this.deferredVariant_ = null;
      }
      if (this.deferredTextStream_) {
        this.streamingEngine_.switchTextStream(this.deferredTextStream_);
        this.onTextChanged_();
        this.deferredTextStream_ = null;
      }
    }
  
    /**
     * Callback from StreamingEngine.
     *
     * @private
     */
    onManifestUpdate_() {
      if (this.parser_ && this.parser_.update) {
        this.parser_.update();
      }
    }
  
    /**
     * Callback from StreamingEngine.
     *
     * @private
     */
    onSegmentAppended_() {
      // When we append a segment to media source (via streaming engine) we are
      // changing what data we have buffered, so notify the playhead of the
      // change.
      if (this.playhead_) {
        this.playhead_.notifyOfBufferingChange();
      }
    }
  
    /**
     * Callback from AbrManager.
     *
     * @param {shaka.extern.Variant} variant
     * @param {boolean=} clearBuffer
     * @param {number=} safeMargin Optional amount of buffer (in seconds) to
     *   retain when clearing the buffer.
     *   Defaults to 0 if not provided. Ignored if clearBuffer is false.
     * @private
     */
    switch_(variant, clearBuffer = false, safeMargin = 0) {
      shaka.log.debug('switch_');
      goog.asserts.assert(this.config_.abr.enabled,
          'AbrManager should not call switch while disabled!');
      goog.asserts.assert(!this.switchingPeriods_,
          'AbrManager should not call switch while transitioning between ' +
          'Periods!');
      goog.asserts.assert(this.manifest_, 'We need a manifest to switch ' +
                                          'variants.');
  
      const period = this.findPeriodWithVariant_(variant);
      goog.asserts.assert(period, 'A period should contain the variant.');
  
      this.addVariantToSwitchHistory_(period, variant, /* fromAdaptation */ true);
  
      if (!this.streamingEngine_) {
        // There's no way to change it.
        return;
      }
  
      this.streamingEngine_.switchVariant(variant, clearBuffer, safeMargin);
      this.onAdaptation_();
    }
  
    /**
     * Dispatches an 'adaptation' event.
     * @private
     */
    onAdaptation_() {
      // Delay the 'adaptation' event so that StreamingEngine has time to absorb
      // the changes before the user tries to query it.
      this.delayDispatchEvent_(new shaka.util.FakeEvent('adaptation'));
    }
  
    /**
     * Dispatches a 'trackschanged' event.
     * @private
     */
    onTracksChanged_() {
      // Delay the 'trackschanged' event so StreamingEngine has time to absorb the
      // changes before the user tries to query it.
      this.delayDispatchEvent_(new shaka.util.FakeEvent('trackschanged'));
    }
  
    /**
     * Dispatches a 'variantchanged' event.
     * @private
     */
    onVariantChanged_() {
      // Delay the 'variantchanged' event so StreamingEngine has time to absorb
      // the changes before the user tries to query it.
      this.delayDispatchEvent_(new shaka.util.FakeEvent('variantchanged'));
    }
  
    /**
     * Dispatches a 'textchanged' event.
     * @private
     */
    onTextChanged_() {
      // Delay the 'textchanged' event so StreamingEngine time to absorb the
      // changes before the user tries to query it.
      this.delayDispatchEvent_(new shaka.util.FakeEvent('textchanged'));
    }
  
    /** @private */
    onTextTrackVisibility_() {
      this.delayDispatchEvent_(new shaka.util.FakeEvent('texttrackvisibility'));
    }
  
    /** @private */
    onAbrStatusChanged_() {
      this.delayDispatchEvent_(new shaka.util.FakeEvent('abrstatuschanged', {
        newStatus: this.config_.abr.enabled,
      }));
    }
  
    /**
     * @param {!shaka.util.Error} error
     * @private
     */
    onError_(error) {
      goog.asserts.assert(error instanceof shaka.util.Error, 'Wrong error type!');
  
      // Errors dispatched after |destroy| is called are not meaningful and should
      // be safe to ignore.
      if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
        return;
      }
  
      const event = new shaka.util.FakeEvent('error', {'detail': error});
      this.dispatchEvent(event);
      if (event.defaultPrevented) {
        error.handled = true;
      }
    }
  
    /**
     * When we fire region events, we need to copy the information out of the
     * region to break the connection with the player's internal data. We do the
     * copy here because this is the transition point between the player and the
     * app.
     *
     * @param {string} eventName
     * @param {shaka.extern.TimelineRegionInfo} region
     *
     * @private
     */
    onRegionEvent_(eventName, region) {
      // Always make a copy to avoid exposing our internal data to the app.
      const clone = {
        schemeIdUri: region.schemeIdUri,
        value: region.value,
        startTime: region.startTime,
        endTime: region.endTime,
        id: region.id,
        eventElement: region.eventElement,
      };
  
      this.dispatchEvent(new shaka.util.FakeEvent(eventName, {detail: clone}));
    }
  
    /**
     * Turn the media element's error object into a Shaka Player error object.
     *
     * @return {shaka.util.Error}
     * @private
     */
    videoErrorToShakaError_() {
      goog.asserts.assert(this.video_.error,
          'Video error expected, but missing!');
      if (!this.video_.error) {
        return null;
      }
  
      const code = this.video_.error.code;
      if (code == 1 /* MEDIA_ERR_ABORTED */) {
        // Ignore this error code, which should only occur when navigating away or
        // deliberately stopping playback of HTTP content.
        return null;
      }
  
      // Extra error information from MS Edge and IE11:
      let extended = this.video_.error.msExtendedCode;
      if (extended) {
        // Convert to unsigned:
        if (extended < 0) {
          extended += Math.pow(2, 32);
        }
        // Format as hex:
        extended = extended.toString(16);
      }
  
      // Extra error information from Chrome:
      const message = this.video_.error.message;
  
      return new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.MEDIA,
          shaka.util.Error.Code.VIDEO_ERROR,
          code, extended, message);
    }
  
    /**
     * @param {!Event} event
     * @private
     */
    onVideoError_(event) {
      const error = this.videoErrorToShakaError_();
      if (!error) {
        return;
      }
      this.onError_(error);
    }
  
    /**
     * @param {!Object.<string, string>} keyStatusMap A map of hex key IDs to
     *   statuses.
     * @private
     */
    onKeyStatus_(keyStatusMap) {
      if (!this.streamingEngine_) {
        // We can't use this info to manage restrictions in src= mode, so ignore
        // it.
        return;
      }
  
      const restrictedStatuses = shaka.Player.restrictedStatuses_;
  
      /** @type {shaka.extern.Period} */
      const currentPeriod = this.getPresentationPeriod_();
      let tracksChanged = false;
  
      const keyIds = Object.keys(keyStatusMap);
      if (keyIds.length == 0) {
        shaka.log.warning(
            'Got a key status event without any key statuses, so we don\'t ' +
            'know the real key statuses. If we don\'t have all the keys, ' +
            'you\'ll need to set restrictions so we don\'t select those tracks.');
      }
  
      // If EME is using a synthetic key ID, the only key ID is '00' (a single 0
      // byte).  In this case, it is only used to report global success/failure.
      // See note about old platforms in: https://bit.ly/2tpez5Z
      const isGlobalStatus = keyIds.length == 1 && keyIds[0] == '00';
  
      if (isGlobalStatus) {
        shaka.log.warning(
            'Got a synthetic key status event, so we don\'t know the real key ' +
            'statuses. If we don\'t have all the keys, you\'ll need to set ' +
            'restrictions so we don\'t select those tracks.');
      }
  
      // Only filter tracks for keys if we have some key statuses to look at.
      if (keyIds.length) {
        for (const period of this.manifest_.periods) {
          for (const variant of period.variants) {
            const streams = shaka.util.StreamUtils.getVariantStreams(variant);
  
            for (const stream of streams) {
              const originalAllowed = variant.allowedByKeySystem;
  
              // Only update if we have a key ID for the stream.
              // If the key isn't present, then we don't have that key and the
              // track should be restricted.
              if (stream.keyId) {
                const keyStatus =
                    keyStatusMap[isGlobalStatus ? '00' : stream.keyId];
                variant.allowedByKeySystem =
                    !!keyStatus && !restrictedStatuses.includes(keyStatus);
              }
  
              if (originalAllowed != variant.allowedByKeySystem) {
                tracksChanged = true;
              }
            }  // for (const stream of streams)
          }  // for (const variant of period.variants)
        }  // for (const period of this.manifest_.periods)
      }  // if (keyIds.length)
  
      // TODO: Get StreamingEngine to track variants and create
      // getBufferingVariant()
      const activeAudio = this.streamingEngine_.getBufferingAudio();
      const activeVideo = this.streamingEngine_.getBufferingVideo();
      const activeVariant = shaka.util.StreamUtils.getVariantByStreams(
          activeAudio, activeVideo, currentPeriod.variants);
  
      if (activeVariant && !activeVariant.allowedByKeySystem) {
        shaka.log.debug('Choosing new streams after key status changed');
        this.chooseStreamsAndSwitch_(currentPeriod);
      }
  
      if (tracksChanged) {
        this.onTracksChanged_();
        this.chooseVariant_(currentPeriod.variants);
      }
    }
  
    /**
     * Callback from DrmEngine
     * @param {string} keyId
     * @param {number} expiration
     * @private
     */
    onExpirationUpdated_(keyId, expiration) {
      if (this.parser_ && this.parser_.onExpirationUpdated) {
        this.parser_.onExpirationUpdated(keyId, expiration);
      }
  
      const event = new shaka.util.FakeEvent('expirationupdated');
      this.dispatchEvent(event);
    }
  
    /**
     * @return {boolean} true if we should stream text right now.
     * @private
     */
    shouldStreamText_() {
      return this.config_.streaming.alwaysStreamText || this.isTextTrackVisible();
    }
  
    /**
     * Applies playRangeStart and playRangeEnd to the given timeline. This will
     * only affect non-live content.
     *
     * @param {shaka.media.PresentationTimeline} timeline
     * @param {number} playRangeStart
     * @param {number} playRangeEnd
     *
     * @private
     */
    static applyPlayRange_(timeline, playRangeStart, playRangeEnd) {
      if (playRangeStart > 0) {
        if (timeline.isLive()) {
          shaka.log.warning(
              '|playRangeStart| has been configured for live content. ' +
              'Ignoring the setting.');
        } else {
          timeline.setUserSeekStart(playRangeStart);
        }
      }
  
      // If the playback has been configured to end before the end of the
      // presentation, update the duration unless it's live content.
      const fullDuration = timeline.getDuration();
      if (playRangeEnd < fullDuration) {
        if (timeline.isLive()) {
          shaka.log.warning(
              '|playRangeEnd| has been configured for live content. ' +
              'Ignoring the setting.');
        } else {
          timeline.setDuration(playRangeEnd);
        }
      }
    }
  
    /**
     * Checks the given variants and if they are all restricted, throw an
     * appropriate exception.
     *
     * @param {!Array.<shaka.extern.Variant>} variants
     * @private
     */
    checkRestrictedVariants_(variants) {
      const restrictedStatuses = shaka.Player.restrictedStatuses_;
      const keyStatusMap =
          this.drmEngine_ ? this.drmEngine_.getKeyStatuses() : {};
      const keyIds = Object.keys(keyStatusMap);
      const isGlobalStatus = keyIds.length && keyIds[0] == '00';
  
      let hasPlayable = false;
      let hasAppRestrict = false;
      const missingKeys = [];
      const badKeyStatuses = [];
  
      for (const variant of variants) {
        // TODO: Combine with onKeyStatus_.
        const streams = [];
        if (variant.audio) {
          streams.push(variant.audio);
        }
        if (variant.video) {
          streams.push(variant.video);
        }
  
        for (const stream of streams) {
          if (stream.keyId) {
            const keyStatus = keyStatusMap[isGlobalStatus ? '00' : stream.keyId];
            if (!keyStatus) {
              if (!missingKeys.includes(stream.keyId)) {
                missingKeys.push(stream.keyId);
              }
            } else if (restrictedStatuses.includes(keyStatus)) {
              if (!badKeyStatuses.includes(keyStatus)) {
                badKeyStatuses.push(keyStatus);
              }
            }
          }
        }
  
        if (!variant.allowedByApplication) {
          hasAppRestrict = true;
        } else if (variant.allowedByKeySystem) {
          hasPlayable = true;
        }
      }
  
      if (!hasPlayable) {
        /** @type {shaka.extern.RestrictionInfo} */
        const data = {
          hasAppRestrictions: hasAppRestrict,
          missingKeys: missingKeys,
          restrictedKeyStatuses: badKeyStatuses,
        };
        throw new shaka.util.Error(
            shaka.util.Error.Severity.CRITICAL,
            shaka.util.Error.Category.MANIFEST,
            shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET,
            data);
      }
    }
  
    /**
     * Fire an event, but wait a little bit so that the immediate execution can
     * complete before the event is handled.
     *
     * @param {!shaka.util.FakeEvent} event
     * @private
     */
    async delayDispatchEvent_(event) {
      // Wait until the next interpreter cycle.
      await Promise.resolve();
  
      // Only dispatch the event if we are still alive.
      if (this.loadMode_ != shaka.Player.LoadMode.DESTROYED) {
        this.dispatchEvent(event);
      }
    }
  
    /**
     * Get the normalized languages for a group of tracks.
     *
     * @param {!Array.<?shaka.extern.Track>} tracks
     * @return {!Set.<string>}
     * @private
     */
    static getLanguagesFrom_(tracks) {
      const languages = new Set();
  
      for (const track of tracks) {
        if (track.language) {
          languages.add(shaka.util.LanguageUtils.normalize(track.language));
        } else {
          languages.add('und');
        }
      }
  
      return languages;
    }
  
    /**
     * Get all permutations of normalized languages and role for a group of
     * tracks.
     *
     * @param {!Array.<?shaka.extern.Track>} tracks
     * @return {!Array.<shaka.extern.LanguageRole>}
     * @private
     */
    static getLanguageAndRolesFrom_(tracks) {
      /** @type {!Map.<string, !Set>} */
      const languageToRoles = new Map();
  
      for (const track of tracks) {
        let language = 'und';
        let roles = [];
  
        if (track.language) {
          language = shaka.util.LanguageUtils.normalize(track.language);
        }
  
        if (track.type == 'variant') {
          roles = track.audioRoles;
        } else {
          roles = track.roles;
        }
  
        if (!roles || !roles.length) {
          // We must have an empty role so that we will still get a language-role
          // entry from our Map.
          roles = [''];
        }
  
        if (!languageToRoles.has(language)) {
          languageToRoles.set(language, new Set());
        }
  
        for (const role of roles) {
          languageToRoles.get(language).add(role);
        }
      }
  
      // Flatten our map to an array of language-role pairs.
      const pairings = [];
      languageToRoles.forEach((roles, language) => {
        for (const role of roles) {
          pairings.push({
            language: language,
            role: role,
          });
        }
      });
      return pairings;
    }
  
    /**
     * Get the variants that the user can select. The variants will be based on
     * the period that the playhead is in and what variants are playable.
     *
     * @return {!Array.<shaka.extern.Variant>}
     * @private
     */
    getSelectableVariants_() {
      // Use the period that is currently playing, allowing the change to affect
      // the "now".
      /** @type {shaka.extern.Period} */
      const currentPeriod = this.getPresentationPeriod_();
  
      // If we have been called before we load content or after we have unloaded
      // content, then we should return no variants.
      if (currentPeriod == null) {
        return [];
      }
  
      this.assertCorrectActiveStreams_();
  
      return currentPeriod.variants.filter((variant) => {
        return shaka.util.StreamUtils.isPlayable(variant);
      });
    }
  
    /**
     * Get the text streams that the user can select. The streams will be based on
     * the period that the playhead is in and what streams have finished loading.
     *
     * @return {!Array.<shaka.extern.Stream>}
     * @private
     */
    getSelectableText_() {
      // Use the period that is currently playing, allowing the change to affect
      // the "now".
      /** @type {shaka.extern.Period} */
      const currentPeriod = this.getPresentationPeriod_();
  
      // If we have been called before we load content or after we have unloaded
      // content, then we should return no streams.
      if (currentPeriod == null) {
        return [];
      }
  
      this.assertCorrectActiveStreams_();
  
      // Don't show return streams that are still loading.
      return currentPeriod.textStreams.filter((stream) => {
        return !this.loadingTextStreams_.has(stream);
      });
    }
  
    /**
     * Get the period that is on the screen. This will return |null| if nothing
     * is loaded.
     *
     * @return {shaka.extern.Period}
     * @private
     */
    getPresentationPeriod_() {
      goog.asserts.assert(this.manifest_ && this.playhead_,
          'Only ask for the presentation period when loaded with media source.');
  
      const presentationTime = this.playhead_.getTime();
  
      let lastPeriod = null;
  
      // Periods are ordered by |startTime|. If we always keep the last period
      // that started before our presentation time, it means we will have the
      // best guess at which period we are presenting.
      for (const period of this.manifest_.periods) {
        if (period.startTime <= presentationTime) {
          lastPeriod = period;
        }
      }
  
      goog.asserts.assert(lastPeriod, 'Should have found a period.');
      return lastPeriod;
    }
  
    /**
     * Get the variant that we are currently presenting to the user. If we are not
     * showing anything, then we will return |null|.
     *
     * @return {?shaka.extern.Variant}
     * @private
     */
    getPresentationVariant_() {
      /** @type {shaka.extern.Period} */
      const currentPeriod = this.getPresentationPeriod_();
      return this.activeStreams_.getVariant(currentPeriod);
    }
  
    /**
     * Get the text stream that we are either currently presenting to the user or
     * will be presenting will captions are enabled. If we have no text to
     * display, this will return |null|.
     *
     * @return {?shaka.extern.Stream}
     * @private
     */
    getPresentationText_() {
      /** @type {shaka.extern.Period} */
      const currentPeriod = this.getPresentationPeriod_();
  
      // Can't have a text stream when there is no period.
      if (currentPeriod == null) {
        return null;
      }
  
      // This is a workaround for the demo page to be able to display the list of
      // text tracks. If no text track is currently active, pick the one that's
      // going to be streamed when captions are enabled and mark it as active.
      if (!this.activeStreams_.getText(currentPeriod)) {
        const textStreams = shaka.util.StreamUtils.filterStreamsByLanguageAndRole(
            currentPeriod.textStreams,
            this.currentTextLanguage_,
            this.currentTextRole_);
  
        if (textStreams.length) {
          this.activeStreams_.useText(currentPeriod, textStreams[0]);
        }
      }
  
      return this.activeStreams_.getText(currentPeriod);
    }
  
    /**
     * Assuming the player is playing content with media source, check if the
     * player has buffered enough content to make it to the end of the
     * presentation.
     *
     * @return {boolean}
     * @private
     */
    isBufferedToEndMS_() {
      goog.asserts.assert(
          this.video_,
          'We need a video element to get buffering information');
      goog.asserts.assert(
          this.mediaSourceEngine_,
          'We need a media source engine to get buffering information');
      goog.asserts.assert(
          this.manifest_,
          'We need a manifest to get buffering information');
  
      // This is a strong guarantee that we are buffered to the end, because it
      // means the playhead is already at that end.
      if (this.video_.ended) {
        return true;
      }
  
      // This means that MediaSource has buffered the final segment in all
      // SourceBuffers and is no longer accepting additional segments.
      if (this.mediaSourceEngine_.ended()) {
        return true;
      }
  
      // Live streams are "buffered to the end" when they have buffered to the
      // live edge or beyond (into the region covered by the presentation delay).
      if (this.manifest_.presentationTimeline.isLive()) {
        const liveEdge =
            this.manifest_.presentationTimeline.getSegmentAvailabilityEnd();
        const bufferEnd =
            shaka.media.TimeRangesUtils.bufferEnd(this.video_.buffered);
  
        if (bufferEnd >= liveEdge) {
          return true;
        }
      }
  
      return false;
    }
  
    /**
     * Assuming the player is playing content with src=, check if the player has
     * buffered enough content to make it to the end of the presentation.
     *
     * @return {boolean}
     * @private
     */
    isBufferedToEndSrc_() {
      goog.asserts.assert(
          this.video_,
          'We need a video element to get buffering information');
  
      // This is a strong guarantee that we are buffered to the end, because it
      // means the playhead is already at that end.
      if (this.video_.ended) {
        return true;
      }
  
      // If we have buffered to the duration of the content, it means we will have
      // enough content to buffer to the end of the presentation.
      const bufferEnd =
          shaka.media.TimeRangesUtils.bufferEnd(this.video_.buffered);
  
      // Because Safari's native HLS reports slightly inaccurate values for
      // bufferEnd here, we use a fudge factor.  Without this, we can end up in a
      // buffering state at the end of the stream.  See issue #2117.
      // TODO: Try to remove the fudge here once we no longer manage buffering
      // state above the browser with playbackRate=0.
      const fudge = 1;  // 1000 ms
      return bufferEnd >= this.video_.duration - fudge;
    }
  
    /**
     * Find the period in |this.manifest_| that contains |variant|. If no period
     * contains |variant| this will return |null|.
     *
     * @param {shaka.extern.Variant} variant
     * @return {?shaka.extern.Period}
     * @private
     */
    findPeriodWithVariant_(variant) {
      for (const period of this.manifest_.periods) {
        if (period.variants.includes(variant)) {
          return period;
        }
      }
  
      return null;
    }
  
    /**
     * Create an error for when we purposely interrupt a load operation.
     *
     * @return {!shaka.util.Error}
     * @private
     */
    createAbortLoadError_() {
      return new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.PLAYER,
          shaka.util.Error.Code.LOAD_INTERRUPTED);
    }
  
    /**
     * Key
     * ----------------------
     * D   : Detach Node
     * A   : Attach Node
     * MS  : Media Source Node
     * P   : Manifest Parser Node
     * M   : Manifest Node
     * DRM : Drm Engine Node
     * L   : Load Node
     * U   : Unloading Node
     * SRC : Src Equals Node
     *
     * Graph Topology
     * ----------------------
     *
     *        [SRC]-----+
     *         ^        |
     *         |        v
     * [D]<-->[A]<-----[U]
     *         |        ^
     *         v        |
     *        [MS]------+
     *         |        |
     *         v        |
     *        [P]-------+
     *         |        |
     *         v        |
     *        [M]-------+
     *         |        |
     *         v        |
     *        [DRM]-----+
     *         |        |
     *         v        |
     *        [L]-------+
     *
     * @param {!shaka.routing.Node} currentlyAt
     * @param {shaka.routing.Payload} currentlyWith
     * @param {!shaka.routing.Node} wantsToBeAt
     * @param {shaka.routing.Payload} wantsToHave
     * @return {?shaka.routing.Node}
     * @private
     */
    getNextStep_(currentlyAt, currentlyWith, wantsToBeAt, wantsToHave) {
      let next = null;
  
      // Detach is very simple, either stay in detach (because |detach| was called
      // while in detached) or go somewhere that requires us to attach to an
      // element.
      if (currentlyAt == this.detachNode_) {
        next = wantsToBeAt == this.detachNode_ ?
               this.detachNode_ :
               this.attachNode_;
      }
  
      if (currentlyAt == this.attachNode_) {
        next = this.getNextAfterAttach_(wantsToBeAt, currentlyWith, wantsToHave);
      }
  
      if (currentlyAt == this.mediaSourceNode_) {
        next = this.getNextAfterMediaSource_(
            wantsToBeAt, currentlyWith, wantsToHave);
      }
  
      if (currentlyAt == this.parserNode_) {
        next = this.getNextMatchingAllDependencies_(
            /* destination= */ this.loadNode_,
            /* next= */ this.manifestNode_,
            /* reset= */ this.unloadNode_,
            /* goingTo= */ wantsToBeAt,
            /* has= */ currentlyWith,
            /* wants= */ wantsToHave);
      }
  
      if (currentlyAt == this.manifestNode_) {
        next = this.getNextMatchingAllDependencies_(
            /* destination= */ this.loadNode_,
            /* next= */ this.drmNode_,
            /* reset= */ this.unloadNode_,
            /* goingTo= */ wantsToBeAt,
            /* has= */ currentlyWith,
            /* wants= */ wantsToHave);
      }
  
      // For DRM, we have two options "load" or "unload". If all our constraints
      // are met, we can go to "load". If anything is off, we must go back to
      // "unload" to reset.
      if (currentlyAt == this.drmNode_) {
        next = this.getNextMatchingAllDependencies_(
            /* destination= */ this.loadNode_,
            /* next= */ this.loadNode_,
            /* reset= */ this.unloadNode_,
            /* goingTo= */ wantsToBeAt,
            /* has= */ currentlyWith,
            /* wants= */ wantsToHave);
      }
  
      // For DRM w/ src= playback, we only care about destination and media
      // element.
      if (currentlyAt == this.srcEqualsDrmNode_) {
        if (wantsToBeAt == this.srcEqualsNode_ &&
            currentlyWith.mediaElement == wantsToHave.mediaElement) {
          next = this.srcEqualsNode_;
        } else {
          next = this.unloadNode_;
        }
      }
  
      // After we load content, always go through unload because we can't safely
      // use components after we have started playback.
      if (currentlyAt == this.loadNode_ || currentlyAt == this.srcEqualsNode_) {
        next = this.unloadNode_;
      }
  
      if (currentlyAt == this.unloadNode_) {
        next = this.getNextAfterUnload_(wantsToBeAt, currentlyWith, wantsToHave);
      }
  
      goog.asserts.assert(next, 'Missing next step!');
      return next;
    }
  
    /**
     * @param {!shaka.routing.Node} goingTo
     * @param {shaka.routing.Payload} has
     * @param {shaka.routing.Payload} wants
     * @return {?shaka.routing.Node}
     * @private
     */
    getNextAfterAttach_(goingTo, has, wants) {
      // Attach and detach are the only two nodes that we can directly go
      // back-and-forth between.
      if (goingTo == this.detachNode_) {
        return this.detachNode_;
      }
  
      // If we are going anywhere other than detach, then we need the media
      // element to match, if they don't match, we need to go through detach
      // first.
      if (has.mediaElement != wants.mediaElement) {
        return this.detachNode_;
      }
  
      // If we are already in attached, and someone calls |attach| again (to the
      // same video element), we can handle the redundant request by re-entering
      // our current state.
      if (goingTo == this.attachNode_) {
        return this.attachNode_;
      }
  
      // The next step from attached to loaded is through media source.
      if (goingTo == this.mediaSourceNode_ || goingTo == this.loadNode_) {
        return this.mediaSourceNode_;
      }
  
      // If we are going to src=, then we should set up DRM first.  This will
      // support cases like FairPlay HLS on Safari.
      if (goingTo == this.srcEqualsNode_) {
        return this.srcEqualsDrmNode_;
      }
  
      // We are missing a rule, the null will get caught by a common check in
      // the routing system.
      return null;
    }
  
    /**
     * @param {!shaka.routing.Node} goingTo
     * @param {shaka.routing.Payload} has
     * @param {shaka.routing.Payload} wants
     * @return {?shaka.routing.Node}
     * @private
     */
    getNextAfterMediaSource_(goingTo, has, wants) {
      // We can only go to parse manifest or unload. If we want to go to load and
      // we have the right media element, we can go to parse manifest. If we
      // don't, no matter where we want to go, we must go through unload.
      if (goingTo == this.loadNode_ && has.mediaElement == wants.mediaElement) {
        return this.parserNode_;
      }
  
      // Right now the unload node is responsible for tearing down all playback
      // components (including media source). So since we have created media
      // source, we need to unload since our dependencies are not compatible.
      //
      // TODO: We are structured this way to maintain a historic structure. Going
      //       forward, there is no reason to restrict ourselves to this. Going
      //       forward we should explore breaking apart |onUnload| and develop
      //       more meaningful terminology around tearing down playback resources.
      return this.unloadNode_;
    }
  
    /**
     * After unload there are only two options, attached or detached. This choice
     * is based on whether or not we have a media element. If we have a media
     * element, then we go to attach. If we don't have a media element, we go to
     * detach.
     *
     * @param {!shaka.routing.Node} goingTo
     * @param {shaka.routing.Payload} has
     * @param {shaka.routing.Payload} wants
     * @return {?shaka.routing.Node}
     * @private
     */
    getNextAfterUnload_(goingTo, has, wants) {
      // If we don't want a media element, detach.
      // If we have the wrong media element, detach.
      // Otherwise it means we want to attach to a media element and it is safe to
      // do so.
      return !wants.mediaElement || has.mediaElement != wants.mediaElement ?
             this.detachNode_ :
             this.attachNode_;
    }
  
    /**
     * A general method used to handle routing when we can either than one step
     * toward our destination (while all our dependencies match) or go to a node
     * that will reset us so we can try again.
     *
     * @param {!shaka.routing.Node} destinationNode
     *   What |goingTo| must be for us to step toward |nextNode|. Otherwise we
     *   will go to |resetNode|.
     * @param {!shaka.routing.Node} nextNode
     *   The node we will go to next if |goingTo == destinationNode| and all
     *   dependencies match.
     * @param {!shaka.routing.Node} resetNode
     *   The node we will go to next if |goingTo != destinationNode| or any
     *   dependency does not match.
     * @param {!shaka.routing.Node} goingTo
     *   The node that the walker is trying to go to.
     * @param {shaka.routing.Payload} has
     *   The payload that the walker currently has.
     * @param {shaka.routing.Payload} wants
     *   The payload that the walker wants to have when iy gets to |goingTo|.
     * @return {shaka.routing.Node}
     * @private
     */
    getNextMatchingAllDependencies_(destinationNode, nextNode, resetNode, goingTo,
        has, wants) {
      if (goingTo == destinationNode &&
          has.mediaElement == wants.mediaElement &&
          has.uri == wants.uri &&
          has.mimeType == wants.mimeType &&
          has.factory == wants.factory) {
        return nextNode;
      }
  
      return resetNode;
    }
  
    /**
     * @return {shaka.routing.Payload}
     * @private
     */
    static createEmptyPayload_() {
      return {
        factory: null,
        mediaElement: null,
        mimeType: null,
        startTime: null,
        startTimeOfLoad: null,
        uri: null,
      };
    }
  
    /**
     * Using a promise, wrap the listeners returned by |Walker.startNewRoute|.
     * This will work for most usages in |Player| but should not be used for
     * special cases.
     *
     * This will connect |onCancel|, |onEnd|, |onError|, and |onSkip| with
     * |resolve| and |reject| but will leave |onStart| unset.
     *
     * @param {shaka.routing.Walker.Listeners} listeners
     * @return {!Promise}
     * @private
     */
    wrapWalkerListenersWithPromise_(listeners) {
      return new Promise((resolve, reject) => {
        listeners.onCancel = () => reject(this.createAbortLoadError_());
        listeners.onEnd = () => resolve();
        listeners.onError = (e) => reject(e);
        listeners.onSkip = () => reject(this.createAbortLoadError_());
      });
    }
  };
  
  /**
   * In order to know what method of loading the player used for some content, we
   * have this enum. It lets us know if content has not been loaded, loaded with
   * media source, or loaded with src equals.
   *
   * This enum has a low resolution, because it is only meant to express the
   * outer limits of the various states that the player is in. For example, when
   * someone calls a public method on player, it should not matter if they have
   * initialized drm engine, it should only matter if they finished loading
   * content.
   *
   * @enum {number}
   * @export
   */
  shaka.Player.LoadMode = {
    'DESTROYED': 0,
    'NOT_LOADED': 1,
    'MEDIA_SOURCE': 2,
    'SRC_EQUALS': 3,
  };
  
  /**
   * The typical buffering threshold.  When we have less than this buffered (in
   * seconds), we enter a buffering state.  This specific value is based on manual
   * testing and evaluation across a variety of platforms.
   *
   * To make the buffering logic work in all cases, this "typical" threshold will
   * be overridden if the rebufferingGoal configuration is too low.
   *
   * @const {number}
   * @private
   */
  shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
  
  /**
   * @define {string} A version number taken from git at compile time.
   * @export
   */
  shaka.Player.version = 'v2.5.6-master-uncompiled';
  
  // Initialize the deprecation system using the version string we just set
  // on the player.
  shaka.Deprecate.init(shaka.Player.version);
  
  
  /**
   * These are the EME key statuses that represent restricted playback.
   * 'usable', 'released', 'output-downscaled', 'status-pending' are statuses
   * of the usable keys.  'expired' status is being handled separately in
   * DrmEngine.
   *
   * @const {!Array.<string>}
   * @private
   */
  shaka.Player.restrictedStatuses_ = ['output-restricted', 'internal-error'];
  
  
  /** @private {!Object.<string, function():*>} */
  shaka.Player.supportPlugins_ = {};
  