shaka-player
Version:
DASH/EME video player library
1,630 lines (1,383 loc) • 303 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.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