UNPKG

shaka-player

Version:
1,630 lines (1,383 loc) 303 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.config.CrossBoundaryStrategy'); goog.require('shaka.Deprecate'); goog.require('shaka.drm.DrmEngine'); goog.require('shaka.drm.DrmUtils'); goog.require('shaka.log'); goog.require('shaka.media.AdaptationSetCriteria'); goog.require('shaka.media.BufferingObserver'); goog.require('shaka.media.ManifestFilterer'); 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.PreloadManager'); goog.require('shaka.media.QualityObserver'); goog.require('shaka.media.RegionObserver'); goog.require('shaka.media.RegionTimeline'); goog.require('shaka.media.SegmentIndex'); goog.require('shaka.media.SegmentPrefetch'); 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.net.NetworkingUtils'); goog.require('shaka.text.Cue'); goog.require('shaka.text.SimpleTextDisplayer'); goog.require('shaka.text.StubTextDisplayer'); goog.require('shaka.text.TextEngine'); goog.require('shaka.text.Utils'); goog.require('shaka.text.UITextDisplayer'); goog.require('shaka.text.WebVttGenerator'); goog.require('shaka.util.ArrayUtils'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.CmcdManager'); goog.require('shaka.util.CmsdManager'); goog.require('shaka.util.ConfigUtils'); goog.require('shaka.util.Dom'); 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.MapUtils'); goog.require('shaka.util.MediaReadyState'); goog.require('shaka.util.MimeUtils'); goog.require('shaka.util.Mutex'); goog.require('shaka.util.NumberUtils'); 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'); /** * @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.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.DownloadCompleted * @description Fired when a download has completed. * @property {string} type * 'downloadcompleted' * @property {!shaka.extern.Request} request * @property {!shaka.extern.Response} response * @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 * @property {number} httpResponseCode * @property {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. * @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.MediaSourceRecoveredEvent * @description Fired when MediaSource has been successfully recovered * after occurrence of video error. * @property {string} type * 'mediasourcerecovered' * @exportDoc */ /** * @event shaka.Player.AudioTrackChangedEvent * @description Fired when the audio track changes at the playhead. * That may be caused by a user requesting to chang audio tracks. * @property {string} type * 'audiotrackchanged' * @property {shaka.extern.MediaQualityInfo} mediaQuality * Information about media quality at the playhead position. * @property {number} position * The playhead position. * @exportDoc */ /** * @event shaka.Player.BoundaryCrossedEvent * @description Fired when the player's crossed a boundary and reset * the MediaSource successfully. * @property {string} type * 'boundarycrossed' * @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. * An app may want to look at <code>getStats()</code> or * <code>getVariantTracks()</code> to see what happened. * @property {string} type * 'texttrackvisibility' * @exportDoc */ /** * @event shaka.Player.AudioTracksChangedEvent * @description Fired when the list of audio tracks changes. * An app may want to look at <code>getAudioTracks()</code> to see what * happened. * @property {string} type * 'audiotrackschanged' * @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. * An app may want to look at <code>getVariantTracks()</code> to see what * happened. * @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. * An app may want to look at <code>getStats()</code> or * <code>getVariantTracks()</code> to see what happened. * @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>. * An app may want to look at <code>getStats()</code> or * <code>getTextTracks()</code> to see what happened. * @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 'com.apple.quicktime.HLS' * @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'. * @property {boolean} isMuxed * Indicates if the segment is muxed (audio + video). * @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. * An app may want to look at <code>getStats()</code> to see what happened. * @property {string} type * 'stalldetected' * @exportDoc */ /** * @event shaka.Player.GapJumpedEvent * @description Fired when the GapJumpingController jumps over a gap in the * buffered ranges. * An app may want to look at <code>getStats()</code> to see what happened. * @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 */ /** * @event shaka.Player.Started * @description Fires when the content starts playing. * Only for VoD. * @property {string} type * 'started' * @exportDoc */ /** * @event shaka.Player.FirstQuartile * @description Fires when the content playhead crosses first quartile. * Only for VoD. * @property {string} type * 'firstquartile' * @exportDoc */ /** * @event shaka.Player.Midpoint * @description Fires when the content playhead crosses midpoint. * Only for VoD. * @property {string} type * 'midpoint' * @exportDoc */ /** * @event shaka.Player.ThirdQuartile * @description Fires when the content playhead crosses third quartile. * Only for VoD. * @property {string} type * 'thirdquartile' * @exportDoc */ /** * @event shaka.Player.Complete * @description Fires when the content completes playing. * Only for VoD. * @property {string} type * 'complete' * @exportDoc */ /** * @event shaka.Player.SpatialVideoInfoEvent * @description Fired when the video has spatial video info. If a previous * event was fired, this include the new info. * @property {string} type * 'spatialvideoinfo' * @property {shaka.extern.SpatialVideoInfo} detail * An object which contains the content of the emsg box. * @exportDoc */ /** * @event shaka.Player.NoSpatialVideoInfoEvent * @description Fired when the video no longer has spatial video information. * For it to be fired, the shaka.Player.SpatialVideoInfoEvent event must * have been previously fired. * @property {string} type * 'nospatialvideoinfo' * @exportDoc */ /** * @event shaka.Player.ProducerReferenceTimeEvent * @description Fired when the content includes ProducerReferenceTime (PRFT) * info. * @property {string} type * 'prft' * @property {shaka.extern.ProducerReferenceTime} detail * An object which contains the content of the PRFT box. * @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 {HTMLElement=} videoContainer * The videoContainer to construct UITextDisplayer * @param {function(shaka.Player)=} dependencyInjector Optional callback * which is called to inject mocks into the Player. Used for testing. */ constructor(mediaElement, videoContainer = null, dependencyInjector) { super(); /** @private {shaka.Player.LoadMode} */ this.loadMode_ = shaka.Player.LoadMode.NOT_LOADED; /** @private {HTMLMediaElement} */ this.video_ = null; /** @private {HTMLElement} */ this.videoContainer_ = videoContainer; /** * 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(); /** * For listeners scoped to the lifetime of the loaded content. * @private {shaka.util.EventManager} */ this.trickPlayEventManager_ = new shaka.util.EventManager(); /** * For listeners scoped to the lifetime of the ad manager. * @private {shaka.util.EventManager} */ this.adManagerEventManager_ = new shaka.util.EventManager(); /** @private {shaka.net.NetworkingEngine} */ this.networkingEngine_ = null; /** @private {shaka.drm.DrmEngine} */ this.drmEngine_ = null; /** @private {shaka.media.MediaSourceEngine} */ this.mediaSourceEngine_ = null; /** @private {shaka.media.Playhead} */ this.playhead_ = null; /** * Incremented whenever a top-level operation (load, attach, etc) is * performed. * Used to determine if a load operation has been interrupted. * @private {number} */ this.operationId_ = 0; /** @private {!shaka.util.Mutex} */ this.mutex_ = new shaka.util.Mutex(); /** * 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< * shaka.extern.TimelineRegionInfo>} */ this.regionTimeline_ = null; /** * @private {shaka.media.RegionTimeline< * shaka.extern.MetadataTimelineRegionInfo>} */ this.metadataRegionTimeline_ = null; /** * @private {shaka.media.RegionTimeline< * shaka.extern.EmsgTimelineRegionInfo>} */ this.emsgRegionTimeline_ = null; /** @private {shaka.util.CmcdManager} */ this.cmcdManager_ = null; /** @private {shaka.util.CmsdManager} */ this.cmsdManager_ = 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 {?string} */ this.mimeType_ = null; /** @private {?number} */ this.startTime_ = 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 {!Array<shaka.extern.Stream>} */ this.externalSrcEqualsThumbnailsStreams_ = []; /** @private {number} */ this.completionPercent_ = -1; /** @private {?shaka.extern.PlayerConfiguration} */ this.config_ = this.defaultConfig_(); /** @private {!Object} */ this.lowLatencyConfig_ = shaka.util.PlayerConfiguration.createDefaultForLL(); /** @private {?number} */ this.currentTargetLatency_ = null; /** @private {number} */ this.rebufferingCount_ = -1; /** @private {?number} */ this.targetLatencyReached_ = null; /** * 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 {shaka.extern.Resolution} */ this.maxHwRes_ = {width: Infinity, height: Infinity}; /** @private {!shaka.media.ManifestFilterer} */ this.manifestFilterer_ = new shaka.media.ManifestFilterer( this.config_, this.maxHwRes_, null); /** @private {!Array<shaka.media.PreloadManager>} */ this.createdPreloadManagers_ = []; /** @private {shaka.util.Stats} */ this.stats_ = null; /** @private {!shaka.media.AdaptationSetCriteria} */ this.currentAdaptationSetCriteria_ = this.config_.adaptationSetCriteriaFactory(); this.currentAdaptationSetCriteria_.configure({ language: this.config_.preferredAudioLanguage, role: this.config_.preferredVariantRole, channelCount: this.config_.preferredAudioChannelCount, hdrLevel: this.config_.preferredVideoHdrLevel, spatialAudio: this.config_.preferSpatialAudio, videoLayout: this.config_.preferredVideoLayout, audioLabel: this.config_.preferredAudioLabel, videoLabel: this.config_.preferredVideoLabel, codecSwitchingStrategy: this.config_.mediaSource.codecSwitchingStrategy, audioCodec: '', }); /** @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_ = []; if (dependencyInjector) { dependencyInjector(this); } // Create the CMCD manager so client data can be attached to all requests this.cmcdManager_ = this.createCmcd_(); this.cmsdManager_ = this.createCmsd_(); this.networkingEngine_ = this.createNetworkingEngine(); this.networkingEngine_.setForceHTTP(this.config_.streaming.forceHTTP); this.networkingEngine_.setForceHTTPS(this.config_.streaming.forceHTTPS); this.networkingEngine_.setMinBytesForProgressEvents( this.config_.streaming.minBytesForProgressEvents); /** @private {shaka.extern.IAdManager} */ this.adManager_ = null; /** @private {?shaka.media.PreloadManager} */ this.preloadDueAdManager_ = null; /** @private {HTMLMediaElement} */ this.preloadDueAdManagerVideo_ = null; /** @private {boolean} */ this.preloadDueAdManagerVideoEnded_ = false; /** @private {shaka.util.Timer} */ this.preloadDueAdManagerTimer_ = new shaka.util.Timer(async () => { if (this.preloadDueAdManager_) { goog.asserts.assert(this.preloadDueAdManagerVideo_, 'Must have video'); await this.attach( this.preloadDueAdManagerVideo_, /* initializeMediaSource= */ true); await this.load(this.preloadDueAdManager_); if (!this.preloadDueAdManagerVideoEnded_) { this.preloadDueAdManagerVideo_.play(); } else { this.preloadDueAdManagerVideo_.pause(); } this.preloadDueAdManager_ = null; this.preloadDueAdManagerVideoEnded_ = false; } }); if (shaka.Player.adManagerFactory_) { this.adManager_ = shaka.Player.adManagerFactory_(); this.adManager_.configure(this.config_.ads); // Note: we don't use shaka.ads.Utils.AD_CONTENT_PAUSE_REQUESTED to // avoid add a optional module in the player. this.adManagerEventManager_.listen( this.adManager_, 'ad-content-pause-requested', async (e) => { this.preloadDueAdManagerTimer_.stop(); if (!this.preloadDueAdManager_) { this.preloadDueAdManagerVideo_ = this.video_; this.preloadDueAdManagerVideoEnded_ = this.isEnded(); const saveLivePosition = /** @type {boolean} */( e['saveLivePosition']) || false; this.preloadDueAdManager_ = await this.detachAndSavePreload( /* keepAdManager= */ true, saveLivePosition); } }); // Note: we don't use shaka.ads.Utils.AD_CONTENT_RESUME_REQUESTED to // avoid add a optional module in the player. this.adManagerEventManager_.listen( this.adManager_, 'ad-content-resume-requested', (e) => { const offset = /** @type {number} */(e['offset']) || 0; if (this.preloadDueAdManager_) { this.preloadDueAdManager_.setOffsetToStartTime(offset); } this.preloadDueAdManagerTimer_.tickAfter(0.1); }); // Note: we don't use shaka.ads.Utils.AD_CONTENT_ATTACH_REQUESTED to // avoid add a optional module in the player. this.adManagerEventManager_.listen( this.adManager_, 'ad-content-attach-requested', async (e) => { if (!this.video_ && this.preloadDueAdManagerVideo_) { goog.asserts.assert(this.preloadDueAdManagerVideo_, 'Must have video'); await this.attach(this.preloadDueAdManagerVideo_, /* initializeMediaSource= */ true); } }); } // 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.util.Timer} */ this.checkVariantsTimer_ = new shaka.util.Timer(() => this.checkVariants_()); /** @private {?shaka.media.PreloadManager} */ this.preloadNextUrl_ = null; // 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) { shaka.Deprecate.deprecateFeature(5, 'Player w/ mediaElement', 'Please migrate from initializing Player with a mediaElement; ' + 'use the attach method instead.'); this.attach(mediaElement, /* initializeMediaSource= */ true); } /** @private {?shaka.extern.TextDisplayer} */ this.textDisplayer_ = null; } /** * Create a shaka.lcevc.Dec object * @param {shaka.extern.LcevcConfiguration} config * @param {boolean} isDualTrack * @private */ createLcevcDec_(config, isDualTrack) { if (this.lcevcDec_ == null) { this.lcevcDec_ = new shaka.lcevc.Dec( /** @type {HTMLVideoElement} */ (this.video_), this.lcevcCanvas_, config, isDualTrack, ); 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 * @param {boolean} isDualTrack * @private */ setupLcevc_(config, isDualTrack) { if (isDualTrack || config.lcevc.enabled) { this.closeLcevcDec_(); this.createLcevcDec_(config.lcevc, isDualTrack); } else { this.closeLcevcDec_(); } } /** * @param {!shaka.util.FakeEvent.EventName} name * @param {Map<string, Object>=} data * @return {!shaka.util.FakeEvent} * @private */ static 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_(); const detachPromise = this.detach(); // 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; await detachPromise; // A PreloadManager can only be used with the Player instance that created // it, so all PreloadManagers this Player has created are now useless. // Destroy any remaining managers now, to help prevent memory leaks. await this.destroyAllPreloads(); // 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; } if (this.trickPlayEventManager_) { this.trickPlayEventManager_.release(); this.trickPlayEventManager_ = null; } if (this.adManagerEventManager_) { this.adManagerEventManager_.release(); this.adManagerEventManager_ = null; } this.abrManagerFactory_ = null; this.config_ = null; this.stats_ = null; this.videoContainer_ = null; this.cmcdManager_ = null; this.cmsdManager_ = 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_.set(name, callback); } /** * Set a factory to create an ad manager during player construction time. * This method needs to be called before 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; } // 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.drm.DrmEngine.probeSupport(); } const manifest = shaka.media.ManifestParser.probeSupport(); const media = shaka.media.MediaSourceEngine.probeSupport(); const hardwareResolution = await shaka.util.Platform.detectMaxHardwareResolution(); /** @type {shaka.extern.SupportType} */ const ret = { manifest, media, drm, hardwareResolution, }; const plugins = shaka.Player.supportPlugins_; plugins.forEach((value, key) => { ret[key] = value(); }); return ret; } /** * Makes a fires an event corresponding to entering a state of the loading * process. * @param {string} nodeName * @private */ makeStateChangeEvent_(nodeName) { this.dispatchEvent(shaka.Player.makeEvent_( /* name= */ shaka.util.FakeEvent.EventName.OnStateChange, /* data= */ (new Map()).set('state', nodeName))); } /** * Attaches the player to a media element. * If the player was already attached to a media element, first detaches from * that media element. * * @param {!HTMLMediaElement} mediaElement * @param {boolean=} initializeMediaSource * @return {!Promise} * @export */ async attach(mediaElement, initializeMediaSource = true) { // Do not allow the player to be used after |destroy| is called. if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) { throw this.createAbortLoadError_(); } const noop = this.video_ && this.video_ == mediaElement; if (this.video_ && this.video_ != mediaElement) { await this.detach(); } if (await this.atomicOperationAcquireMutex_('attach')) { return; } try { if (!noop) { this.makeStateChangeEvent_('attach'); const onError = (error) => this.onVideoError_(error); this.attachEventManager_.listen(mediaElement, 'error', onError); this.video_ = mediaElement; if (this.cmcdManager_) { this.cmcdManager_.setMediaElement(mediaElement); } } // Only initialize media source if the platform supports it. if (initializeMediaSource && shaka.util.Platform.supportsMediaSource() && !this.mediaSourceEngine_) { await this.initializeMediaSourceEngineInner_(); } } catch (error) { await this.detach(); throw error; } finally { this.mutex_.release(); } } /** * Calling <code>attachCanvas</code> will tell the player to set canvas * element for LCEVC decoding. * * @param {HTMLCanvasElement} canvas * @export */ attachCanvas(canvas) { this.lcevcCanvas_ = canvas; } /** * Detach the player from the current media element. Leaves the player in a * state where it cannot play media, until it has been attached to something * else. * * @param {boolean=} keepAdManager * * @return {!Promise} * @export */ async detach(keepAdManager = false) { // Do not allow the player to be used after |destroy| is called. if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) { throw this.createAbortLoadError_(); } await this.unload(/* initializeMediaSource= */ false, keepAdManager); if (await this.atomicOperationAcquireMutex_('detach')) { return; } try { // If we were going from "detached" to "detached" we wouldn't have // a media element to detach from. if (this.video_) { this.attachEventManager_.removeAll(); this.video_ = null; } this.makeStateChangeEvent_('detach'); if (this.adManager_ && !keepAdManager) { // The ad manager is specific to the video, so detach it too. this.adManager_.release(); } } finally { this.mutex_.release(); } } /** * Tries to acquire the mutex, and then returns if the operation should end * early due to someone else starting a mutex-acquiring operation. * Meant for operations that can't be interrupted midway through (e.g. * everything but load). * @param {string} mutexIdentifier * @return {!Promise<boolean>} endEarly If false, the calling context will * need to release the mutex. * @private */ async atomicOperationAcquireMutex_(mutexIdentifier) { const operationId = ++this.operationId_; await this.mutex_.acquire(mutexIdentifier); if (operationId != this.operationId_) { this.mutex_.release(); return true; } return false; } /** * Unloads the currently playing stream, if any. * * @param {boolean=} initializeMediaSource * @param {boolean=} keepAdManager * @return {!Promise} * @export */ async unload(initializeMediaSource = true, keepAdManager = false) { // 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; } if (await this.atomicOperationAcquireMutex_('unload')) { return; } try { this.fullyLoaded_ = false; this.makeStateChangeEvent_('unload'); // If LCEVC Decoder exists close it. this.closeLcevcDec_(); // 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( shaka.Player.makeEvent_(shaka.util.FakeEvent.EventName.Unloading)); // Release the region timeline, which is created when parsing the // manifest. if (this.regionTimeline_) { this.regionTimeline_.release(); this.regionTimeline_ = null; } if (this.metadataRegionTimeline_) { this.metadataRegionTimeline_.release(); this.metadataRegionTimeline_ = null; } if (this.emsgRegionTimeline_) { this.emsgRegionTimeline_.release(); this.emsgRegionTimeline_ = 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 (this.video_) { this.loadEventManager_.removeAll(); this.trickPlayEventManager_.removeAll(); } // Stop the variant checker timer this.checkVariantsTimer_.stop(); // 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; } // EME v0.1b requires the media element to clear the MediaKeys if (shaka.drm.DrmUtils.isMediaKeysPolyfilled('webkit') && this.drmEngine_) { await this.drmEngine_.destroy(); this.drmEngine_ = 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_ && !keepAdManager) { this.adManager_.onAssetUnload(); } if (this.preloadDueAdManager_ && !keepAdManager) { this.preloadDueAdManager_.destroy(); this.preloadDueAdManager_ = null; } if (!keepAdManager) { this.preloadDueAdManagerTimer_.stop(); } if (this.cmcdManager_) { this.cmcdManager_.reset(); } if (this.cmsdManager_) { this.cmsdManager_.reset(); } if (this.textDisplayer_) { await this.textDisplayer_.destroy(); this.textDisplayer_ = null; } if (this.video_) { // Remove all track nodes shaka.util.Dom.removeAllChildren(this.video_); // 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 (this.video_.src) { this.video_.removeAttribute('src'); this.video_.load(); } } if (this.drmEngine_) { await this.drmEngine_.destroy(); this.drmEngine_ = null; } if (this.preloadNextUrl_ && this.assetUri_ != this.preloadNextUrl_.getAssetUri()) { if (!this.preloadNextUrl_.isDestroyed()) { this.preloadNextUrl_.destroy(); } this.preloadNextUrl_ = null; } this.assetUri_ = null; this.mimeType_ = 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(); } } } // On some devices, cached MediaKeySystemAccess objects may corrupt // after several playbacks, and they are not able anymore to properly // create MediaKeys objects. To prevent it, clear the cache after // each playback. if (this.config_ && this.config_.streaming.clearDecodingCache) { shaka.util.StreamUtils.clearDecodingConfigCache(); shaka.drm.DrmUtils.clearMediaKeySystemAccessMap(); } this.manifest_ = null; this.stats_ = new shaka.util.Stats(); // Replace with a clean object. this.lastTextFactory_ = null; this.targetLatencyReached_ = null; this.currentTargetLatency_ = null; this.rebufferingCount_ = -1; this.externalSrcEqualsThumbnailsStreams_ = []; this.completionPercent_ = -1; if (this.networkingEngine_) { this.networkingEngine_.clearCommonAccessTokenMap(); } // Make sure that the app knows of the new buffering state. this.updateBufferState_(); } finally { this.mutex_.release(); } if (initializeMediaSource && shaka.util.Platform.supportsMediaSource() && !this.mediaSourceEngine_ && this.video_) { await this.initializeMediaSourceEngineInner_(); } } /** * 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.startTime_ = startTime; } /** * Loads a new stream. * If another stream was already playing, first unloads that stream. * * @param {string|shaka.media.PreloadManager} assetUriOrPreloader * @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 */ async load(assetUriOrPreloader, startTime = null, mimeType) { // Do not allow the player to be used after |destroy| is called. if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) { throw this.createAbortLoadError_(); } /** @type {?shaka.media.PreloadManager} */ let preloadManager = null; let assetUri = ''; if (assetUriOrPreloader instanceof shaka.media.PreloadManager) { if (assetUriOrPreloader.isD