UNPKG

shaka-player

Version:
1,509 lines (1,303 loc) 246 kB
/*! @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.config.AutoShowText'); 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.MetaSegmentIndex'); goog.require('shaka.media.PlayRateController'); goog.require('shaka.media.Playhead'); goog.require('shaka.media.PlayheadObserverManager'); goog.require('shaka.media.PreferenceBasedCriteria'); goog.require('shaka.media.QualityObserver'); goog.require('shaka.media.RegionObserver'); goog.require('shaka.media.RegionTimeline'); goog.require('shaka.media.SegmentIndex'); goog.require('shaka.media.SegmentReference'); 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.TextEngine'); goog.require('shaka.text.UITextDisplayer'); goog.require('shaka.text.WebVttGenerator'); goog.require('shaka.util.AbortableOperation'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.CmcdManager'); 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.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.require('shaka.lcevc.Dec'); 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 an emsg box is found in a segment. * If the application calls preventDefault() on this event, further parsing * will not happen, and no 'metadata' event will be raised for ID3 payloads. * @property {string} type * 'emsg' * @property {shaka.extern.EmsgInfo} detail * An object which contains the content of the emsg box. * @exportDoc */ /** * @event shaka.Player.DownloadFailed * @description Fired when a download has failed, for any reason. * 'downloadfailed' * @property {!shaka.extern.Request} request * @property {?shaka.util.Error} error * @param {number} httpResponseCode * @param {boolean} aborted * @exportDoc */ /** * @event shaka.Player.DownloadHeadersReceived * @description Fired when the networking engine has received the headers for * a download, but before the body has been downloaded. * If the HTTP plugin being used does not track this information, this event * will default to being fired when the body is received, instead. * @property {!Object.<string, string>} headers * @property {!shaka.extern.Request} request * @property {!shaka.net.NetworkingEngine.RequestType} type * 'downloadheadersreceived' * @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.MediaQualityChangedEvent * @description Fired when the media quality changes at the playhead. * That may be caused by an adaptation change or a DASH period transition. * Separate events are emitted for audio and video contentTypes. * This is supported for only DASH streams at this time. * @property {string} type * 'mediaqualitychanged' * @property {shaka.extern.MediaQualityInfo} mediaQuality * Information about media quality at the playhead position. * @property {number} position * The playhead position. * @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' * @property {shaka.extern.Track} oldTrack * @property {shaka.extern.Track} newTrack * @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' * @property {shaka.extern.Track} oldTrack * @property {shaka.extern.Track} newTrack * @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.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.ManifestUpdatedEvent * @description Fired after the manifest has been updated (live streams). * @property {string} type * 'manifestupdated' * @property {boolean} isLive * True when the playlist is live. Useful to detect transition from live * to static playlist.. * @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.MetadataFrame} 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 */ /** * @event shaka.Player.SegmentAppended * @description Fired when a segment is appended to the media element. * @property {string} type * 'segmentappended' * @property {number} start * The start time of the segment. * @property {number} end * The end time of the segment. * @property {string} contentType * The content type of the segment. E.g. 'video', 'audio', or 'text'. * @exportDoc */ /** * @event shaka.Player.SessionDataEvent * @description Fired when the manifest parser find info about session data. * Specification: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 * @property {string} type * 'sessiondata' * @property {string} id * The id of the session data. * @property {string} uri * The uri with the session data info. * @property {string} language * The language of the session data. * @property {string} value * The value of the session data. * @exportDoc */ /** * @event shaka.Player.StallDetectedEvent * @description Fired when a stall in playback is detected by the StallDetector. * Not all stalls are caused by gaps in the buffered ranges. * @property {string} type * 'stalldetected' * @exportDoc */ /** * @event shaka.Player.GapJumpedEvent * @description Fired when the GapJumpingController jumps over a gap in the * buffered ranges. * @property {string} type * 'gapjumped' * @exportDoc */ /** * @event shaka.Player.KeyStatusChanged * @description Fired when the key status changed. * @property {string} type * 'keystatuschanged' * @exportDoc */ /** * @event shaka.Player.StateChanged * @description Fired when player state is changed. * @property {string} type * 'statechanged' * @property {string} newstate * The new state. * @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; /** * For listeners scoped to the lifetime of the Player instance. * @private {shaka.util.EventManager} */ this.globalEventManager_ = new shaka.util.EventManager(); /** * For listeners scoped to the lifetime of the media element attachment. * @private {shaka.util.EventManager} */ this.attachEventManager_ = new shaka.util.EventManager(); /** * For listeners scoped to the lifetime of the loaded content. * @private {shaka.util.EventManager} */ this.loadEventManager_ = 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.util.CmcdManager} */ this.cmcdManager_ = null; // This is the canvas element that will be used for rendering LCEVC // enhanced frames. /** @private {?HTMLCanvasElement} */ this.lcevcCanvas_ = null; // This is the LCEVC Decoder object to decode LCEVC. /** @private {?shaka.lcevc.Dec} */ this.lcevcDec_ = null; /** @private {shaka.media.QualityObserver} */ this.qualityObserver_ = 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 {boolean} */ this.fullyLoaded_ = false; /** @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, this.config_.preferredVideoHdrLevel, this.config_.mediaSource.codecSwitchingStrategy); /** @private {string} */ this.currentTextLanguage_ = this.config_.preferredTextLanguage; /** @private {string} */ this.currentTextRole_ = this.config_.preferredTextRole; /** @private {boolean} */ this.currentTextForced_ = this.config_.preferForcedSubs; /** @private {!Array.<function():(!Promise|undefined)>} */ this.cleanupOnUnload_ = []; /** * This playback start position will be used when * <code>updateStartTime()</code> has been called to provide an updated * start position during the media loading process. * * @private {?number} */ this.updatedStartTime_ = null; if (dependencyInjector) { dependencyInjector(this); } // Create the CMCD manager so client data can be attached to all requests this.cmcdManager_ = this.createCmcd_(); this.networkingEngine_ = this.createNetworkingEngine(); this.networkingEngine_.setForceHTTPS(this.config_.streaming.forceHTTPS); /** @private {shaka.extern.IAdManager} */ this.adManager_ = null; if (shaka.Player.adManagerFactory_) { this.adManager_ = shaka.Player.adManagerFactory_(); } // If the browser comes back online after being offline, then try to play // again. this.globalEventManager_.listen(window, 'online', () => { this.restoreDisabledVariants_(); 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.util.FakeEvent.EventName.OnStateChange, /* data= */ (new Map()).set('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.util.FakeEvent.EventName.OnStateIdle, /* data= */ (new Map()).set('state', node.name))); }, }; /** @private {shaka.routing.Walker} */ this.walker_ = new shaka.routing.Walker( this.detachNode_, shaka.Player.createEmptyPayload_(), walkerImplementation); /** @private {shaka.util.Timer} */ this.checkVariantsTimer_ = new shaka.util.Timer(() => this.checkVariants_()); // 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); } } /** * Create a shaka.lcevc.Dec object * @param {shaka.extern.LcevcConfiguration} config * @private */ createLcevcDec_(config) { if (this.lcevcDec_ == null) { this.lcevcDec_ = new shaka.lcevc.Dec( /** @type {HTMLVideoElement} */ (this.video_), this.lcevcCanvas_, config, ); if (this.mediaSourceEngine_) { this.mediaSourceEngine_.updateLcevcDec(this.lcevcDec_); } } } /** * Close a shaka.lcevc.Dec object if present and hide the canvas. * @private */ closeLcevcDec_() { if (this.lcevcDec_ != null) { this.lcevcDec_.hideCanvas(); this.lcevcDec_.release(); this.lcevcDec_ = null; } } /** * Setup shaka.lcevc.Dec object * @param {?shaka.extern.PlayerConfiguration} config * @private */ setupLcevc_(config) { if (config.lcevc.enabled) { const tracks = this.getVariantTracks(); if (tracks && tracks[0] && tracks[0].videoMimeType == shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_['ts']) { const edge = shaka.util.Platform.isEdge() || shaka.util.Platform.isLegacyEdge(); if (edge) { if (!config.mediaSource.forceTransmux) { // If forceTransmux is disabled for Microsoft Edge, LCEVC data // is stripped out in case of a MPEG-2 TS container. // Hence the warning for Microsoft Edge when playing content with // MPEG-2 TS container. shaka.log.alwaysWarn('LCEVC Warning: For MPEG-2 TS decoding '+ 'the config.mediaSource.forceTransmux must be enabled.'); } } } this.closeLcevcDec_(); this.createLcevcDec_(config.lcevc); } else { this.closeLcevcDec_(); } } /** * @param {!shaka.util.FakeEvent.EventName} name * @param {Map.<string, 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; } // If LCEVC Decoder exists close it. this.closeLcevcDec_(); // 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; const payload = shaka.Player.createEmptyPayload_(); payload.keepAdManager = false; // 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: payload, 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 managers to ensure handlers stop firing. if (this.globalEventManager_) { this.globalEventManager_.release(); this.globalEventManager_ = null; } if (this.attachEventManager_) { this.attachEventManager_.release(); this.attachEventManager_ = null; } if (this.loadEventManager_) { this.loadEventManager_.release(); this.loadEventManager_ = null; } this.abrManagerFactory_ = null; this.config_ = null; this.stats_ = null; this.videoContainer_ = null; this.cmcdManager_ = null; if (this.networkingEngine_) { await this.networkingEngine_.destroy(); this.networkingEngine_ = null; } if (this.abrManager_) { this.abrManager_.release(); this.abrManager_ = null; } // FakeEventTarget implements IReleasable super.release(); } /** * 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() { if (!window.Promise) { shaka.log.alwaysWarn('A Promise implementation or polyfill is required'); } // 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 IE if (shaka.util.Platform.isIE()) { return false; } // We do not support iOS 9, 10, 11 or 12, nor those same versions of // desktop Safari. const safariVersion = shaka.util.Platform.safariVersion(); if (safariVersion && safariVersion < 13) { 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 * @param {boolean=} promptsOkay * @return {!Promise.<shaka.extern.SupportType>} * @export */ static async probeSupport(promptsOkay=true) { goog.asserts.assert(shaka.Player.isBrowserSupported(), 'Must have basic support'); let drm = {}; if (promptsOkay) { 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); } /** * Calling <code>attachCanvas</code> will tell the player to set canvas * element for LCEVC decoding. * * @param {HTMLCanvasElement} canvas * @export */ attachCanvas(canvas) { this.lcevcCanvas_ = canvas; } /** * 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 * @param {boolean=} keepAdManager * @return {!Promise} * @export */ unload(initializeMediaSource = true, keepAdManager = false) { // Do not allow the player to be used after |destroy| is called. if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) { return Promise.reject(this.createAbortLoadError_()); } this.fullyLoaded_ = false; // If the platform does not support media source, we will never want to // initialize media source. if (!shaka.util.Platform.supportsMediaSource()) { initializeMediaSource = false; } // If LCEVC Decoder exists close it. this.closeLcevcDec_(); // 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_(); payload.keepAdManager = keepAdManager; 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); } /** * Provides a way to update the stream start position during the media loading * process. Can for example be called from the <code>manifestparsed</code> * event handler to update the start position based on information in the * manifest. * * @param {number} startTime * @export */ updateStartTime(startTime) { this.updatedStartTime_ = startTime; } /** * 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) { this.updatedStartTime_ = null; this.fullyLoaded_ = false; // 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.util.FakeEvent.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.util.FakeEvent.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; const MimeUtils = shaka.util.MimeUtils; // 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 = shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_[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. // Native HLS can be preferred on any platform via this flag: if (MimeUtils.isHlsType(mimeType) && this.config_.streaming.preferNativeHls) { return true; } // For Safari, we have an older flag which only applies to this one // browser: if (Platform.isApple()) { return this.config_.streaming.useNativeHlsOnSafari; } // In all other cases, we prefer MediaSource. return false; } // 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.attachEventManager_.listen(has.mediaElement, 'error', onError); } this.video_ = has.mediaElement; return Promise