shaka-player
Version: 
DASH/EME video player library
1,437 lines (1,251 loc) • 182 kB
JavaScript
/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
goog.provide('shaka.Player');
goog.require('goog.asserts');
goog.require('shaka.Deprecate');
goog.require('shaka.log');
goog.require('shaka.media.AdaptationSetCriteria');
goog.require('shaka.media.BufferingObserver');
goog.require('shaka.media.DrmEngine');
goog.require('shaka.media.ExampleBasedCriteria');
goog.require('shaka.media.ManifestParser');
goog.require('shaka.media.MediaSourceEngine');
goog.require('shaka.media.MediaSourcePlayhead');
goog.require('shaka.media.MuxJSClosedCaptionParser');
goog.require('shaka.media.NoopCaptionParser');
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.SrcEqualsPlayhead');
goog.require('shaka.media.StreamingEngine');
goog.require('shaka.media.TimeRangesUtils');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.routing.Walker');
goog.require('shaka.text.SimpleTextDisplayer');
goog.require('shaka.text.UITextDisplayer');
goog.require('shaka.util.AbortableOperation');
goog.require('shaka.util.ConfigUtils');
goog.require('shaka.util.Error');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.FakeEventTarget');
goog.require('shaka.util.Functional');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.LanguageUtils');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.MediaReadyState');
goog.require('shaka.util.MimeUtils');
goog.require('shaka.util.ObjectUtils');
goog.require('shaka.util.Platform');
goog.require('shaka.util.PlayerConfiguration');
goog.require('shaka.util.PublicPromise');
goog.require('shaka.util.Stats');
goog.require('shaka.util.StreamUtils');
goog.require('shaka.util.Timer');
goog.requireType('shaka.media.IClosedCaptionParser');
goog.requireType('shaka.media.PresentationTimeline');
goog.requireType('shaka.routing.Node');
goog.requireType('shaka.routing.Payload');
/**
 * @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.LoadedEvent
 * @description Fired when the player ends the load.
 * @property {string} type
 *   'loaded'
 * @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 new tracks are added/removed 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.MetadataEvent
 * @description Triggers after metadata associated with the stream is found.
 *   Usually they are metadata of type ID3.
 * @property {string} type
 *   'metadata'
 * @property {number} startTime
 *   The time that describes the beginning of the range of the metadata to
 *   which the cue applies.
 * @property {?number} endTime
 *   The time that describes the end of the range of the metadata to which
 *   the cue applies.
 * @property {string} metadataType
 *   Type of metadata. Eg: org.id3 or org.mp4ra
 * @property {shaka.extern.ID3Metadata} payload
 *   The metadata itself
 * @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
 */
/**
 * @event shaka.Player.RateChangeEvent
 * @description Fired when the video's playback rate changes.
 *    This allows the PlayRateController to update it's internal rate field,
 *    before the UI updates playback button with the newest playback rate.
 * @property {string} type
 *    'ratechange'
 * @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;
    /** @private {HTMLElement} */
    this.videoContainer_ = 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.ManifestParser.Factory} */
    this.parserFactory_ = 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 {?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;
    /** @private {!Array.<function():(!Promise|undefined)>} */
    this.cleanupOnUnload_ = [];
    if (dependencyInjector) {
      dependencyInjector(this);
    }
    this.networkingEngine_ = this.createNetworkingEngine();
    /** @private {shaka.extern.IAdManager} */
    this.adManager_ = null;
    if (shaka.Player.adManagerFactory_) {
      this.adManager_ =
          shaka.util.Functional.callFactory(shaka.Player.adManagerFactory_);
    }
    // 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(this.makeEvent_(
            /* name= */ shaka.Player.EventName.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(this.makeEvent_(
            /* name= */ shaka.Player.EventName.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);
    }
  }
  /**
   * @param {!shaka.Player.EventName} name
   * @param {Object=} data
   * @return {!shaka.util.FakeEvent}
   * @private
   */
  makeEvent_(name, data) {
    return new shaka.util.FakeEvent(name, data);
  }
  /**
   * 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;
    this.stats_ = null;
    this.videoContainer_ = 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;
  }
  /**
   * Set a factory to create an ad manager during player construction time.
   * This method needs to be called bafore instantiating the Player class.
   *
   * @param {!shaka.extern.IAdManager.Factory} factory
   * @export
   */
  static setAdManagerFactory(factory) {
    shaka.Player.adManagerFactory_ = factory;
  }
  /**
   * 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;
    }
    // We do not support iOS 9, 10, or 11, nor those same versions of desktop
    // Safari.
    const safariVersion = shaka.util.Platform.safariVersion();
    if (safariVersion && safariVersion < 12) {
      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=} 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(this.makeEvent_(shaka.Player.EventName.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) {
      payload.mimeType = 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,
      };
    });
    // Stats are for a single playback/load session. Stats must be initialized
    // before we allow calls to |updateStateHistory|.
    this.stats_ = new shaka.util.Stats();
    // 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();
        // We dispatch the loaded event when the load promise is resolved
        this.dispatchEvent(this.makeEvent_(shaka.Player.EventName.Loaded));
      };
      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 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',
        'weba': 'audio/webm',
        'mkv': 'video/webm', // Chromium browsers supports it.
        'ts': 'video/mp2t',
        'ogv': 'video/ogg',
        'ogg': 'audio/ogg',
        'mpg': 'video/mpeg',
        'mpeg': 'video/mpeg',
        'm3u8': 'application/x-mpegurl',
        'mp3': 'audio/mpeg',
        'aac': 'audio/aac',
        'flac': 'audio/flac',
        'wav': 'audio/wav',
      }[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 mediaElement = payload.mediaElement || Platform.anyMediaElement();
      const canPlayNatively = mediaElement.canPlayType(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;
    }
    // Run any general cleanup tasks now.  This should be here at the top, right
    // after setting loadMode_, so that internal components still exist as they
    // did when the cleanup tasks were registered in the array.
    const cleanupTasks = this.cleanupOnUnload_.map((cb) => cb());
    this.cleanupOnUnload_ = [];
    await Promise.all(cleanupTasks);
    // Dispatch the unloading event.
    this.dispatchEvent(this.makeEvent_(shaka.Player.EventName.Unloading));
    // Remove everything that has to do with loading content from our payload
    // since we are releasing everything that depended on it.
    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, 'loadedmetadata');
      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;
      this.parserFactory_ = 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;
    }
    if (this.playRateController_) {
      this.playRateController_.release();
      this.playRateController_ = 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;
    }
    if (this.adManager_) {
      this.adManager_.onAssetUnload();
    }
    // 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.assetUri_ = null;
    this.bufferObserver_ = null;
    if (this.manifest_) {
      for (const variant of this.manifest_.variants) {
        for (const stream of [variant.audio, variant.video]) {
          if (stream && stream.segmentIndex) {
            stream.segmentIndex.release();
          }
        }
      }
      for (const stream of this.manifest_.textStreams) {
        if (stream.segmentIndex) {
          stream.segmentIndex.release();
        }
      }
    }
    this.manifest_ = null;
    this.stats_ = new shaka.util.Stats(); // Replace with a clean stats object.
    this.lastTextFactory_ = null;
    // 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 =
        shaka.util.Functional.callFactory(textDisplayerFactory);
    th