shaka-player
Version:
DASH/EME video player library
1,509 lines (1,303 loc) • 246 kB
JavaScript
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.Player');
goog.require('goog.asserts');
goog.require('shaka.Deprecate');
goog.require('shaka.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