mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-26 17:46:26 +03:00
489b11a959
Adds a new player method, preload. This asynchronous method creates a PreloadManager object, which will preload data for the given manifest, and which can be passed to the load method (in place of an asset URI) in order to apply that preloaded data. This will allow for lower load latency; if you can predict what asset will be loaded ahead of time (say, by preloading things the user is hovering their mouse over in a menu), you can load the manifest before the user presses the load button. Note that PreloadManagers are only meant to be used by the player instance that created them. Closes #880 Co-authored-by: Álvaro Velad Galván <ladvan91@hotmail.com>
799 lines
23 KiB
JavaScript
799 lines
23 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2023 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
goog.provide('shaka.media.PreloadManager');
|
|
|
|
goog.require('goog.asserts');
|
|
goog.require('shaka.log');
|
|
goog.require('shaka.util.Error');
|
|
goog.require('shaka.media.ManifestFilterer');
|
|
goog.require('shaka.media.ManifestParser');
|
|
goog.require('shaka.util.PublicPromise');
|
|
goog.require('shaka.media.PreferenceBasedCriteria');
|
|
goog.require('shaka.util.Stats');
|
|
goog.require('shaka.media.SegmentPrefetch');
|
|
goog.require('shaka.util.IDestroyable');
|
|
goog.require('shaka.net.NetworkingEngine');
|
|
goog.require('shaka.media.AdaptationSetCriteria');
|
|
goog.require('shaka.media.DrmEngine');
|
|
goog.require('shaka.media.RegionTimeline');
|
|
goog.require('shaka.media.QualityObserver');
|
|
goog.require('shaka.util.StreamUtils');
|
|
goog.require('shaka.media.StreamingEngine');
|
|
goog.require('shaka.media.SegmentPrefetch');
|
|
goog.require('shaka.util.ConfigUtils');
|
|
goog.require('shaka.util.FakeEvent');
|
|
goog.require('shaka.util.FakeEventTarget');
|
|
goog.require('shaka.util.ObjectUtils');
|
|
goog.require('shaka.util.PlayerConfiguration');
|
|
|
|
/**
|
|
* @implements {shaka.util.IDestroyable}
|
|
* @export
|
|
*/
|
|
shaka.media.PreloadManager = class extends shaka.util.FakeEventTarget {
|
|
/**
|
|
* @param {string} assetUri
|
|
* @param {?string} mimeType
|
|
* @param {number} startTimeOfLoad
|
|
* @param {?number} startTime
|
|
* @param {*} playerInterface
|
|
*/
|
|
constructor(assetUri, mimeType, startTimeOfLoad, startTime, playerInterface) {
|
|
super();
|
|
|
|
// Making the playerInterface a * and casting it to the right type allows
|
|
// for the PlayerInterface for this class to not be exported.
|
|
// Unfortunately, the constructor is exported by default.
|
|
const typedPlayerInterface =
|
|
/** @type {!shaka.media.PreloadManager.PlayerInterface} */ (
|
|
playerInterface);
|
|
|
|
/** @private {string} */
|
|
this.assetUri_ = assetUri;
|
|
|
|
/** @private {?string} */
|
|
this.mimeType_ = mimeType;
|
|
|
|
/** @private {!shaka.net.NetworkingEngine} */
|
|
this.networkingEngine_ = typedPlayerInterface.networkingEngine;
|
|
|
|
/** @private {?number} */
|
|
this.startTime_ = startTime;
|
|
|
|
/** @private {?shaka.media.AdaptationSetCriteria} */
|
|
this.currentAdaptationSetCriteria_ = null;
|
|
|
|
/** @private {number} */
|
|
this.startTimeOfLoad_ = startTimeOfLoad;
|
|
|
|
/** @private {number} */
|
|
this.startTimeOfDrm_ = 0;
|
|
|
|
/** @private {function():!shaka.media.DrmEngine} */
|
|
this.createDrmEngine_ = typedPlayerInterface.createDrmEngine;
|
|
|
|
/** @private {!shaka.media.ManifestFilterer} */
|
|
this.manifestFilterer_ = typedPlayerInterface.manifestFilterer;
|
|
|
|
/** @private {!shaka.extern.ManifestParser.PlayerInterface} */
|
|
this.manifestPlayerInterface_ =
|
|
typedPlayerInterface.manifestPlayerInterface;
|
|
|
|
/** @private {!shaka.extern.PlayerConfiguration} */
|
|
this.config_ = typedPlayerInterface.config;
|
|
|
|
/** @private {?shaka.extern.Manifest} */
|
|
this.manifest_ = null;
|
|
|
|
/** @private {?shaka.extern.ManifestParser.Factory} */
|
|
this.parserFactory_ = null;
|
|
|
|
/** @private {?shaka.extern.ManifestParser} */
|
|
this.parser_ = null;
|
|
|
|
/** @private {boolean} */
|
|
this.parserEntrusted_ = false;
|
|
|
|
/** @private {!shaka.media.RegionTimeline} */
|
|
this.regionTimeline_ = typedPlayerInterface.regionTimeline;
|
|
|
|
/** @private {boolean} */
|
|
this.regionTimelineEntrusted_ = false;
|
|
|
|
/** @private {?shaka.media.DrmEngine} */
|
|
this.drmEngine_ = null;
|
|
|
|
/** @private {boolean} */
|
|
this.drmEngineEntrusted_ = false;
|
|
|
|
/** @private {?shaka.extern.AbrManager.Factory} */
|
|
this.abrManagerFactory_ = null;
|
|
|
|
/** @private {shaka.extern.AbrManager} */
|
|
this.abrManager_ = null;
|
|
|
|
/** @private {boolean} */
|
|
this.abrManagerEntrusted_ = false;
|
|
|
|
/** @private {!Map.<number, shaka.media.SegmentPrefetch>} */
|
|
this.segmentPrefetchById_ = new Map();
|
|
|
|
/** @private {boolean} */
|
|
this.segmentPrefetchEntrusted_ = false;
|
|
|
|
/** @private {?shaka.media.QualityObserver} */
|
|
this.qualityObserver_ = typedPlayerInterface.qualityObserver;
|
|
|
|
/** @private {!shaka.util.Stats} */
|
|
this.stats_ = new shaka.util.Stats();
|
|
|
|
/** @private {!shaka.util.PublicPromise} */
|
|
this.manifestPromise_ = new shaka.util.PublicPromise();
|
|
|
|
/** @private {!shaka.util.PublicPromise} */
|
|
this.drmPromise_ = new shaka.util.PublicPromise();
|
|
|
|
/** @private {!shaka.util.PublicPromise} */
|
|
this.chooseInitialVariantPromise_ = new shaka.util.PublicPromise();
|
|
|
|
/** @private {!shaka.util.PublicPromise} */
|
|
this.destroyPromise_ = new shaka.util.PublicPromise();
|
|
|
|
// Silence promise failures, in case nobody is catching this.destroyPromise_
|
|
this.destroyPromise_.catch((error) => {});
|
|
|
|
/** @private {?shaka.util.FakeEventTarget} */
|
|
this.eventHandoffTarget_ = null;
|
|
|
|
/** @private {boolean} */
|
|
this.destroyed_ = false;
|
|
|
|
/** @private {boolean} */
|
|
this.allowPrefetch_ = typedPlayerInterface.allowPrefetch;
|
|
|
|
/** @private {?shaka.extern.Variant} */
|
|
this.prefetchedVariant_ = null;
|
|
|
|
/** @private {boolean} */
|
|
this.allowMakeAbrManager_ = typedPlayerInterface.allowMakeAbrManager;
|
|
|
|
/** @private {boolean} */
|
|
this.hasBeenAttached_ = false;
|
|
|
|
/** @private {?Array.<function()>} */
|
|
this.queuedOperations_ = [];
|
|
|
|
/** @private {?Array.<function()>} */
|
|
this.latePhaseQueuedOperations_ = [];
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} latePhase
|
|
* @param {function()} callback
|
|
*/
|
|
addQueuedOperation(latePhase, callback) {
|
|
const queue =
|
|
latePhase ? this.latePhaseQueuedOperations_ : this.queuedOperations_;
|
|
if (queue) {
|
|
queue.push(callback);
|
|
} else {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
/** Calls all late phase queued operations, and stops queueing them. */
|
|
stopQueuingLatePhaseQueuedOperations() {
|
|
if (this.latePhaseQueuedOperations_) {
|
|
for (const callback of this.latePhaseQueuedOperations_) {
|
|
callback();
|
|
}
|
|
}
|
|
this.latePhaseQueuedOperations_ = null;
|
|
}
|
|
|
|
/** @param {!shaka.util.FakeEventTarget} eventHandoffTarget */
|
|
setEventHandoffTarget(eventHandoffTarget) {
|
|
this.eventHandoffTarget_ = eventHandoffTarget;
|
|
this.hasBeenAttached_ = true;
|
|
// Also call all queued operations, and stop queuing them in the future.
|
|
if (this.queuedOperations_) {
|
|
for (const callback of this.queuedOperations_) {
|
|
callback();
|
|
}
|
|
}
|
|
this.queuedOperations_ = null;
|
|
}
|
|
|
|
/** @return {?number} */
|
|
getStartTime() {
|
|
return this.startTime_;
|
|
}
|
|
|
|
/** @return {number} */
|
|
getStartTimeOfLoad() {
|
|
return this.startTimeOfLoad_;
|
|
}
|
|
|
|
/** @return {number} */
|
|
getStartTimeOfDRM() {
|
|
return this.startTimeOfDrm_;
|
|
}
|
|
|
|
/** @return {?string} */
|
|
getMimeType() {
|
|
return this.mimeType_;
|
|
}
|
|
|
|
/** @return {string} */
|
|
getAssetUri() {
|
|
return this.assetUri_;
|
|
}
|
|
|
|
/** @return {?shaka.extern.Manifest} */
|
|
getManifest() {
|
|
return this.manifest_;
|
|
}
|
|
|
|
/** @return {?shaka.extern.ManifestParser.Factory} */
|
|
getParserFactory() {
|
|
return this.parserFactory_;
|
|
}
|
|
|
|
/** @return {?shaka.media.AdaptationSetCriteria} */
|
|
getCurrentAdaptationSetCriteria() {
|
|
return this.currentAdaptationSetCriteria_;
|
|
}
|
|
|
|
/** @return {?shaka.extern.AbrManager.Factory} */
|
|
getAbrManagerFactory() {
|
|
return this.abrManagerFactory_;
|
|
}
|
|
|
|
/**
|
|
* Gets the abr manager, if it exists. Also marks that the abr manager should
|
|
* not be stopped if this manager is destroyed.
|
|
* @return {?shaka.extern.AbrManager}
|
|
*/
|
|
receiveAbrManager() {
|
|
this.abrManagerEntrusted_ = true;
|
|
return this.abrManager_;
|
|
}
|
|
|
|
/**
|
|
* @return {?shaka.extern.AbrManager}
|
|
*/
|
|
getAbrManager() {
|
|
return this.abrManager_;
|
|
}
|
|
|
|
/**
|
|
* Gets the parser, if it exists. Also marks that the parser should not be
|
|
* stopped if this manager is destroyed.
|
|
* @return {?shaka.extern.ManifestParser}
|
|
*/
|
|
receiveParser() {
|
|
this.parserEntrusted_ = true;
|
|
return this.parser_;
|
|
}
|
|
|
|
/**
|
|
* @return {?shaka.extern.ManifestParser}
|
|
*/
|
|
getParser() {
|
|
return this.parser_;
|
|
}
|
|
|
|
/**
|
|
* Gets the region timeline, if it exists. Also marks that the timeline should
|
|
* not be released if this manager is destroyed.
|
|
* @return {?shaka.media.RegionTimeline}
|
|
*/
|
|
receiveRegionTimeline() {
|
|
this.regionTimelineEntrusted_ = true;
|
|
return this.regionTimeline_;
|
|
}
|
|
|
|
/**
|
|
* @return {?shaka.media.RegionTimeline}
|
|
*/
|
|
getRegionTimeline() {
|
|
return this.regionTimeline_;
|
|
}
|
|
|
|
/** @return {?shaka.media.QualityObserver} */
|
|
getQualityObserver() {
|
|
return this.qualityObserver_;
|
|
}
|
|
|
|
/** @return {!shaka.util.Stats} */
|
|
getStats() {
|
|
return this.stats_;
|
|
}
|
|
|
|
/** @return {!shaka.media.ManifestFilterer} */
|
|
getManifestFilterer() {
|
|
return this.manifestFilterer_;
|
|
}
|
|
|
|
/**
|
|
* Gets the drm engine, if it exists. Also marks that the drm engine should
|
|
* not be destroyed if this manager is destroyed.
|
|
* @return {?shaka.media.DrmEngine}
|
|
*/
|
|
receiveDrmEngine() {
|
|
this.drmEngineEntrusted_ = true;
|
|
return this.drmEngine_;
|
|
}
|
|
|
|
/**
|
|
* @return {?shaka.media.DrmEngine}
|
|
*/
|
|
getDrmEngine() {
|
|
return this.drmEngine_;
|
|
}
|
|
|
|
/**
|
|
* @return {?shaka.extern.Variant}
|
|
*/
|
|
getPrefetchedVariant() {
|
|
return this.prefetchedVariant_;
|
|
}
|
|
|
|
/**
|
|
* Gets the SegmentPrefetch objects for the initial stream ids. Also marks
|
|
* that those objects should not be aborted if this manager is destroyed.
|
|
* @return {!Map.<number, shaka.media.SegmentPrefetch>}
|
|
*/
|
|
receiveSegmentPrefetchesById() {
|
|
this.segmentPrefetchEntrusted_ = true;
|
|
return this.segmentPrefetchById_;
|
|
}
|
|
|
|
/** @return {!Promise} */
|
|
waitForManifest() {
|
|
return this.manifestPromise_;
|
|
}
|
|
|
|
/** @return {!Promise} */
|
|
waitForDrm() {
|
|
return this.drmPromise_;
|
|
}
|
|
|
|
/** @return {!Promise} */
|
|
waitForChooseInitialVariant() {
|
|
return this.chooseInitialVariantPromise_;
|
|
}
|
|
|
|
/**
|
|
* Starts the process of loading the asset.
|
|
* @return {!Promise}
|
|
*/
|
|
async start() {
|
|
// Force a context switch, to give the player a chance to hook up events
|
|
// immediately if desired.
|
|
await Promise.resolve();
|
|
|
|
// Perform the preloading process.
|
|
try {
|
|
await this.parseManifestInner_();
|
|
if (this.isDestroyed()) {
|
|
return;
|
|
}
|
|
this.manifestPromise_.resolve();
|
|
await this.initializeDrmInner_();
|
|
if (this.isDestroyed()) {
|
|
return;
|
|
}
|
|
this.drmPromise_.resolve();
|
|
await this.chooseInitialVariantInner_();
|
|
if (this.isDestroyed()) {
|
|
return;
|
|
}
|
|
this.chooseInitialVariantPromise_.resolve();
|
|
} catch (error) {
|
|
this.destroyPromise_.reject(error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
* @return {boolean}
|
|
* @override
|
|
*/
|
|
dispatchEvent(event) {
|
|
if (this.eventHandoffTarget_) {
|
|
return this.eventHandoffTarget_.dispatchEvent(event);
|
|
} else {
|
|
return super.dispatchEvent(event);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!shaka.util.Error} error
|
|
*/
|
|
onError(error) {
|
|
if (error.severity === shaka.util.Error.Severity.CRITICAL) {
|
|
// Cancel the loading process.
|
|
this.destroyPromise_.reject(error);
|
|
this.destroy();
|
|
}
|
|
|
|
const eventName = shaka.util.FakeEvent.EventName.Error;
|
|
const event = this.makeEvent_(eventName, (new Map()).set('detail', error));
|
|
this.dispatchEvent(event);
|
|
if (event.defaultPrevented) {
|
|
error.handled = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Makes a fires an event corresponding to entering a state of the loading
|
|
* process.
|
|
* @param {string} nodeName
|
|
* @private
|
|
*/
|
|
makeStateChangeEvent_(nodeName) {
|
|
this.dispatchEvent(new shaka.util.FakeEvent(
|
|
/* name= */ shaka.util.FakeEvent.EventName.OnStateChange,
|
|
/* data= */ (new Map()).set('state', nodeName)));
|
|
}
|
|
|
|
/**
|
|
* @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);
|
|
}
|
|
|
|
/**
|
|
* Pick and initialize a manifest parser, then have it download and parse the
|
|
* manifest.
|
|
*
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
async parseManifestInner_() {
|
|
this.makeStateChangeEvent_('manifest-parser');
|
|
|
|
if (!this.parser_) {
|
|
// Create the parser that we will use to parse the manifest.
|
|
this.parserFactory_ = shaka.media.ManifestParser.getFactory(
|
|
this.assetUri_, this.mimeType_);
|
|
goog.asserts.assert(this.parserFactory_, 'Must have manifest parser');
|
|
this.parser_ = this.parserFactory_();
|
|
|
|
this.parser_.configure(this.config_.manifest);
|
|
}
|
|
|
|
const startTime = Date.now() / 1000;
|
|
|
|
this.makeStateChangeEvent_('manifest');
|
|
|
|
if (!this.manifest_) {
|
|
this.manifest_ = await this.parser_.start(
|
|
this.assetUri_, this.manifestPlayerInterface_);
|
|
}
|
|
|
|
// This event is fired after the manifest is parsed, but before any
|
|
// filtering takes place.
|
|
const event =
|
|
this.makeEvent_(shaka.util.FakeEvent.EventName.ManifestParsed);
|
|
this.dispatchEvent(event);
|
|
|
|
// We require all manifests to have at least one variant.
|
|
if (this.manifest_.variants.length == 0) {
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MANIFEST,
|
|
shaka.util.Error.Code.NO_VARIANTS);
|
|
}
|
|
|
|
// Make sure that all variants are either: audio-only, video-only, or
|
|
// audio-video.
|
|
shaka.media.PreloadManager.filterForAVVariants_(this.manifest_);
|
|
|
|
const now = Date.now() / 1000;
|
|
const delta = now - startTime;
|
|
this.stats_.setManifestTime(delta);
|
|
}
|
|
|
|
/**
|
|
* Initializes the DRM engine.
|
|
*
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
async initializeDrmInner_() {
|
|
goog.asserts.assert(
|
|
this.manifest_, 'The manifest should already be parsed.');
|
|
|
|
this.makeStateChangeEvent_('drm-engine');
|
|
|
|
this.startTimeOfDrm_ = Date.now() / 1000;
|
|
|
|
this.drmEngine_ = this.createDrmEngine_();
|
|
this.manifestFilterer_.setDrmEngine(this.drmEngine_);
|
|
|
|
this.drmEngine_.configure(this.config_.drm);
|
|
|
|
const tracksChangedInitial = this.manifestFilterer_.applyRestrictions(
|
|
this.manifest_);
|
|
if (tracksChangedInitial) {
|
|
const event = this.makeEvent_(
|
|
shaka.util.FakeEvent.EventName.TracksChanged);
|
|
await Promise.resolve();
|
|
this.dispatchEvent(event);
|
|
}
|
|
|
|
const playableVariants = shaka.util.StreamUtils.getPlayableVariants(
|
|
this.manifest_.variants);
|
|
await this.drmEngine_.initForPlayback(
|
|
playableVariants,
|
|
this.manifest_.offlineSessionIds);
|
|
|
|
// Now that we have drm information, filter the manifest (again) so that
|
|
// we can ensure we only use variants with the selected key system.
|
|
// const tracksChanged =
|
|
const tracksChangedAfter = await this.manifestFilterer_.filterManifest(
|
|
this.manifest_);
|
|
if (tracksChangedAfter) {
|
|
const event = this.makeEvent_(
|
|
shaka.util.FakeEvent.EventName.TracksChanged);
|
|
await Promise.resolve();
|
|
this.dispatchEvent(event);
|
|
}
|
|
}
|
|
|
|
/** @param {!shaka.extern.PlayerConfiguration} config */
|
|
reconfigure(config) {
|
|
this.config_ = config;
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {*=} value
|
|
*/
|
|
configure(name, value) {
|
|
const config = shaka.util.ConfigUtils.convertToConfigObject(name, value);
|
|
shaka.util.PlayerConfiguration.mergeConfigObjects(this.config_, config);
|
|
}
|
|
|
|
/**
|
|
* Return a copy of the current configuration.
|
|
*
|
|
* @return {shaka.extern.PlayerConfiguration}
|
|
*/
|
|
getConfiguration() {
|
|
return shaka.util.ObjectUtils.cloneObject(this.config_);
|
|
}
|
|
|
|
/**
|
|
* Performs a final filtering of the manifest, and chooses the initial
|
|
* variant.
|
|
*
|
|
* @private
|
|
*/
|
|
chooseInitialVariantInner_() {
|
|
goog.asserts.assert(
|
|
this.manifest_, 'The manifest should already be parsed.');
|
|
|
|
// This step does not have any associated events, as it is only part of the
|
|
// "load" state in the old state graph.
|
|
|
|
// Copy preferred languages from the config again, in case the config was
|
|
// changed between construction and playback.
|
|
this.currentAdaptationSetCriteria_ =
|
|
new shaka.media.PreferenceBasedCriteria(
|
|
this.config_.preferredAudioLanguage,
|
|
this.config_.preferredVariantRole,
|
|
this.config_.preferredAudioChannelCount,
|
|
this.config_.preferredVideoHdrLevel,
|
|
this.config_.preferSpatialAudio,
|
|
this.config_.preferredVideoLayout,
|
|
this.config_.preferredAudioLabel,
|
|
this.config_.preferredVideoLabel,
|
|
this.config_.mediaSource.codecSwitchingStrategy,
|
|
this.config_.manifest.dash.enableAudioGroups);
|
|
|
|
// If the content is multi-codec and the browser can play more than one of
|
|
// them, choose codecs now before we initialize streaming.
|
|
shaka.util.StreamUtils.chooseCodecsAndFilterManifest(
|
|
this.manifest_,
|
|
this.config_.preferredVideoCodecs,
|
|
this.config_.preferredAudioCodecs,
|
|
this.config_.preferredDecodingAttributes);
|
|
|
|
// Make the ABR manager.
|
|
if (this.allowMakeAbrManager_) {
|
|
const abrFactory = this.config_.abrFactory;
|
|
this.abrManagerFactory_ = abrFactory;
|
|
this.abrManager_ = abrFactory();
|
|
this.abrManager_.configure(this.config_.abr);
|
|
}
|
|
|
|
if (this.allowPrefetch_) {
|
|
const isLive = this.manifest_.presentationTimeline.isLive();
|
|
// Prefetch segments for the predicted first variant.
|
|
// We start these here, but don't wait for them; it's okay to start the
|
|
// full load process while the segments are being prefetched.
|
|
const playableVariants = shaka.util.StreamUtils.getPlayableVariants(
|
|
this.manifest_.variants);
|
|
const adaptationSet = this.currentAdaptationSetCriteria_.create(
|
|
playableVariants);
|
|
// Guess what the first variant will be, based on a SimpleAbrManager.
|
|
this.abrManager_.configure(this.config_.abr);
|
|
this.abrManager_.setVariants(Array.from(adaptationSet.values()));
|
|
const variant =
|
|
this.abrManager_.chooseVariant(/* preferFastSwitching= */ true);
|
|
if (variant) {
|
|
this.prefetchedVariant_ = variant;
|
|
if (variant.video) {
|
|
this.makePrefetchForStream_(variant.video, isLive);
|
|
}
|
|
if (variant.audio) {
|
|
this.makePrefetchForStream_(variant.audio, isLive);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!shaka.extern.Stream} stream
|
|
* @param {boolean} isLive
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
async makePrefetchForStream_(stream, isLive) {
|
|
// Use the prefetch limit from the config if this is set, otherwise use 2.
|
|
const prefetchLimit = this.config_.streaming.segmentPrefetchLimit || 2;
|
|
const prefetch = new shaka.media.SegmentPrefetch(
|
|
prefetchLimit, stream, (reference, stream, streamDataCallback) => {
|
|
return shaka.media.StreamingEngine.dispatchFetch(
|
|
reference, stream, streamDataCallback || null,
|
|
this.config_.streaming.retryParameters, this.networkingEngine_);
|
|
});
|
|
this.segmentPrefetchById_.set(stream.id, prefetch);
|
|
|
|
// Start prefetching a bit.
|
|
await stream.createSegmentIndex();
|
|
const startTime = this.startTime_ || 0;
|
|
const prefetchSegmentIterator =
|
|
stream.segmentIndex.getIteratorForTime(startTime);
|
|
let prefetchSegment =
|
|
prefetchSegmentIterator ? prefetchSegmentIterator.current() : null;
|
|
if (!prefetchSegment) {
|
|
// If we can't get a segment at the desired spot, at least get a segment,
|
|
// so we can get the init segment.
|
|
prefetchSegment = stream.segmentIndex.get(0);
|
|
}
|
|
if (prefetchSegment) {
|
|
if (isLive) {
|
|
// Preload only the init segment for Live
|
|
if (prefetchSegment.initSegmentReference) {
|
|
prefetch.prefetchInitSegment(prefetchSegment.initSegmentReference);
|
|
}
|
|
} else {
|
|
// Preload a segment, too... either the first segment, or the segment
|
|
// that corresponds with this.startTime_, as appropriate.
|
|
// Note: this method also preload the init segment
|
|
prefetch.prefetchSegmentsByTime(prefetchSegment.startTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Waits for the loading to be finished (or to fail with an error).
|
|
* @return {!Promise}
|
|
* @export
|
|
*/
|
|
waitForFinish() {
|
|
return Promise.race([
|
|
this.chooseInitialVariantPromise_,
|
|
this.destroyPromise_,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Releases or stops all non-entrusted resources.
|
|
*
|
|
* @override
|
|
* @export
|
|
*/
|
|
async destroy() {
|
|
this.destroyed_ = true;
|
|
if (this.parser_ && !this.parserEntrusted_) {
|
|
await this.parser_.stop();
|
|
}
|
|
if (this.abrManager_ && !this.abrManagerEntrusted_) {
|
|
await this.abrManager_.stop();
|
|
}
|
|
if (this.regionTimeline_ && !this.regionTimelineEntrusted_) {
|
|
this.regionTimeline_.release();
|
|
}
|
|
if (this.drmEngine_ && !this.drmEngineEntrusted_) {
|
|
await this.drmEngine_.destroy();
|
|
}
|
|
if (this.segmentPrefetchById_.size > 0 && !this.segmentPrefetchEntrusted_) {
|
|
for (const segmentPrefetch of this.segmentPrefetchById_.values()) {
|
|
segmentPrefetch.clearAll();
|
|
}
|
|
}
|
|
// this.eventHandoffTarget_ is not unset, so that events and errors fired
|
|
// after the preload manager is destroyed will still be routed to the
|
|
// player, if it was once linked up.
|
|
}
|
|
|
|
/** @return {boolean} */
|
|
isDestroyed() {
|
|
return this.destroyed_;
|
|
}
|
|
|
|
/** @return {boolean} */
|
|
hasBeenAttached() {
|
|
return this.hasBeenAttached_;
|
|
}
|
|
|
|
/**
|
|
* Take a series of variants and ensure that they only contain one type of
|
|
* variant. The different options are:
|
|
* 1. Audio-Video
|
|
* 2. Audio-Only
|
|
* 3. Video-Only
|
|
*
|
|
* A manifest can only contain a single type because once we initialize media
|
|
* source to expect specific streams, it must always have content for those
|
|
* streams. If we were to start with audio+video and switch to an audio-only
|
|
* variant, media source would block waiting for video content.
|
|
*
|
|
* @param {shaka.extern.Manifest} manifest
|
|
* @private
|
|
*/
|
|
static filterForAVVariants_(manifest) {
|
|
const isAVVariant = (variant) => {
|
|
// Audio-video variants may include both streams separately or may be
|
|
// single multiplexed streams with multiple codecs.
|
|
return (variant.video && variant.audio) ||
|
|
(variant.video && variant.video.codecs.includes(','));
|
|
};
|
|
if (manifest.variants.some(isAVVariant)) {
|
|
shaka.log.debug('Found variant with audio and video content, ' +
|
|
'so filtering out audio-only content.');
|
|
manifest.variants = manifest.variants.filter(isAVVariant);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @typedef {{
|
|
* config: !shaka.extern.PlayerConfiguration,
|
|
* manifestPlayerInterface: !shaka.extern.ManifestParser.PlayerInterface,
|
|
* regionTimeline: !shaka.media.RegionTimeline,
|
|
* qualityObserver: ?shaka.media.QualityObserver,
|
|
* createDrmEngine: function():!shaka.media.DrmEngine,
|
|
* networkingEngine: !shaka.net.NetworkingEngine,
|
|
* manifestFilterer: !shaka.media.ManifestFilterer,
|
|
* allowPrefetch: boolean,
|
|
* allowMakeAbrManager: boolean
|
|
* }}
|
|
*
|
|
* @property {!shaka.extern.PlayerConfiguration} config
|
|
* @property {!shaka.extern.ManifestParser.PlayerInterface}
|
|
* manifestPlayerInterface
|
|
* @property {!shaka.media.RegionTimeline} regionTimeline
|
|
* @property {?shaka.media.QualityObserver} qualityObserver
|
|
* @property {function():!shaka.media.DrmEngine} createDrmEngine
|
|
* @property {!shaka.net.NetworkingEngine} networkingEngine
|
|
* @property {!shaka.media.ManifestFilterer} manifestFilterer
|
|
* @property {boolean} allowPrefetch
|
|
* @property {boolean} allowMakeAbrManager
|
|
*/
|
|
shaka.media.PreloadManager.PlayerInterface;
|