mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-16 16:16:40 +03:00
80139b0ba7
Regression of https://github.com/shaka-project/shaka-player/commit/90710023466dcad4d1d14a2869ae50e8da4e93ee Fixes https://github.com/shaka-project/shaka-player/issues/6710
819 lines
24 KiB
JavaScript
819 lines
24 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_;
|
|
}
|
|
|
|
/**
|
|
* @param {?shaka.extern.AbrManager} abrManager
|
|
* @param {?shaka.extern.AbrManager.Factory} abrFactory
|
|
*/
|
|
attachAbrManager(abrManager, abrFactory) {
|
|
this.abrManager_ = abrManager;
|
|
this.abrManagerFactory_ = abrFactory;
|
|
}
|
|
|
|
/**
|
|
* @param {?shaka.media.AdaptationSetCriteria} adaptationSetCriteria
|
|
*/
|
|
attachAdaptationSetCriteria(adaptationSetCriteria) {
|
|
this.currentAdaptationSetCriteria_ = adaptationSetCriteria;
|
|
}
|
|
|
|
/**
|
|
* @param {!shaka.extern.Manifest} manifest
|
|
* @param {!shaka.extern.ManifestParser} parser
|
|
* @param {!shaka.extern.ManifestParser.Factory} parserFactory
|
|
*/
|
|
attachManifest(manifest, parser, parserFactory) {
|
|
this.manifest_ = manifest;
|
|
this.parser_ = parser;
|
|
this.parserFactory_ = parserFactory;
|
|
}
|
|
|
|
/**
|
|
* 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 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.
|
|
|
|
if (!this.currentAdaptationSetCriteria_) {
|
|
// 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);
|
|
}
|
|
|
|
// 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;
|