mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-27 17:56:23 +03:00
ecfccbb367
Allow evict the buffer more aggressive in MSF streams Do not create a prefetch because for MSF it is not necessary because the data is always in the segments. Set MSF streams as low latency.
842 lines
26 KiB
JavaScript
842 lines
26 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
|
|
goog.provide('shaka.msf.MSFParser');
|
|
|
|
goog.require('goog.asserts');
|
|
goog.require('shaka.log');
|
|
goog.require('shaka.drm.DrmUtils');
|
|
goog.require('shaka.media.InitSegmentReference');
|
|
goog.require('shaka.media.ManifestParser');
|
|
goog.require('shaka.media.PresentationTimeline');
|
|
goog.require('shaka.media.SegmentIndex');
|
|
goog.require('shaka.media.SegmentReference');
|
|
goog.require('shaka.media.SegmentUtils');
|
|
goog.require('shaka.msf.MSFTransport');
|
|
goog.require('shaka.msf.Utils');
|
|
goog.require('shaka.net.NetworkingEngine');
|
|
goog.require('shaka.util.Error');
|
|
goog.require('shaka.util.Functional');
|
|
goog.require('shaka.util.LanguageUtils');
|
|
goog.require('shaka.util.ManifestParserUtils');
|
|
goog.require('shaka.util.MimeUtils');
|
|
goog.require('shaka.util.PublicPromise');
|
|
goog.require('shaka.util.StringUtils');
|
|
goog.require('shaka.util.Uint8ArrayUtils');
|
|
|
|
|
|
/**
|
|
* MOQT Streaming Format.
|
|
*
|
|
* @see https://datatracker.ietf.org/doc/draft-ietf-moq-transport/
|
|
* @see https://datatracker.ietf.org/doc/draft-ietf-moq-msf/
|
|
* @see https://datatracker.ietf.org/doc/draft-ietf-moq-cmsf/
|
|
*
|
|
* @implements {shaka.extern.ManifestParser}
|
|
* @export
|
|
*/
|
|
shaka.msf.MSFParser = class {
|
|
constructor() {
|
|
/** @private {?shaka.extern.ManifestParser.PlayerInterface} */
|
|
this.playerInterface_ = null;
|
|
|
|
/** @private {?shaka.extern.ManifestConfiguration} */
|
|
this.config_ = null;
|
|
|
|
/** @private {function():boolean} */
|
|
this.isPreloadFn_ = () => false;
|
|
|
|
/** @private {?shaka.extern.Manifest} */
|
|
this.manifest_ = null;
|
|
|
|
/** @private {?shaka.msf.MSFTransport} */
|
|
this.msfTransport_ = null;
|
|
|
|
/** @private {?shaka.msf.MSFConnection } */
|
|
this.connection_ = null;
|
|
|
|
/** @private {!Array<Array<string>>} */
|
|
this.publishNamespaces_ = [];
|
|
|
|
/** @private {?function()} */
|
|
this.unregisterPublishNamespaceCallback_ = null;
|
|
|
|
/** @private {?function()} */
|
|
this.unregisterCatalogCallback_ = null;
|
|
|
|
/** @private {!shaka.util.PublicPromise} */
|
|
this.catalogPromise_ = new shaka.util.PublicPromise();
|
|
|
|
/** @private {number} */
|
|
this.globalId_ = 1;
|
|
|
|
/** @private {?shaka.media.PresentationTimeline} */
|
|
this.presentationTimeline_ = null;
|
|
|
|
/** @private {!Array<!shaka.extern.Variant>} */
|
|
this.variants_ = [];
|
|
|
|
/** @private {!Array<!shaka.extern.Stream>} */
|
|
this.audioStreams_ = [];
|
|
|
|
/** @private {!Array<!shaka.extern.Stream>} */
|
|
this.videoStreams_ = [];
|
|
|
|
/** @private {!Array<!shaka.extern.Stream>} */
|
|
this.textStreams_ = [];
|
|
|
|
/** @private {!Map<string, function()>} */
|
|
this.unregisterTracksCallback_ = new Map();
|
|
|
|
/** @private {boolean} */
|
|
this.isFirstVideoSegment_ = true;
|
|
|
|
/**
|
|
* Tracks the first segment start time for each content type.
|
|
* Used to delay locking the presentation timeline until all expected
|
|
* stream types have received their first segment, preventing premature
|
|
* eviction of segments from streams with earlier timestamps.
|
|
* @private {!Map<string, number>}
|
|
*/
|
|
this.firstSegmentStartTimes_ = new Map();
|
|
}
|
|
|
|
/**
|
|
* @param {shaka.extern.ManifestConfiguration} config
|
|
* @param {(function():boolean)=} isPreloadFn
|
|
* @override
|
|
* @exportInterface
|
|
*/
|
|
configure(config, isPreloadFn) {
|
|
this.config_ = config;
|
|
if (isPreloadFn) {
|
|
this.isPreloadFn_ = isPreloadFn;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @exportInterface
|
|
*/
|
|
async start(uri, playerInterface) {
|
|
goog.asserts.assert(this.config_, 'Must call configure() before start()!');
|
|
this.playerInterface_ = playerInterface;
|
|
|
|
this.msfTransport_ = new shaka.msf.MSFTransport();
|
|
|
|
/** @type {?Uint8Array} */
|
|
let fingerprint = null;
|
|
if (this.config_.msf.fingerprintUri) {
|
|
const requestType = shaka.net.NetworkingEngine.RequestType.FINGERPRINT;
|
|
const request = shaka.net.NetworkingEngine.makeRequest(
|
|
[this.config_.msf.fingerprintUri], this.config_.retryParameters);
|
|
const response = await this.playerInterface_.networkingEngine.request(
|
|
requestType, request).promise;
|
|
|
|
// Make sure that the parser has not been destroyed.
|
|
if (!this.playerInterface_) {
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.PLAYER,
|
|
shaka.util.Error.Code.OPERATION_ABORTED);
|
|
}
|
|
|
|
const hexString = shaka.util.StringUtils.fromUTF8(response.data).trim();
|
|
|
|
const hexBytes = new Uint8Array(hexString.length / 2);
|
|
for (let i = 0; i < hexBytes.length; i += 1) {
|
|
hexBytes[i] = parseInt(hexString.slice(2 * i, 2 * i + 2), 16);
|
|
}
|
|
fingerprint = hexBytes;
|
|
}
|
|
|
|
try {
|
|
this.connection_ = await this.msfTransport_.connect(uri, fingerprint);
|
|
} catch (error) {
|
|
if (error instanceof shaka.util.Error) {
|
|
throw error;
|
|
}
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MANIFEST,
|
|
shaka.util.Error.Code.WEBTRANSPORT_INITIALIZATION_FAILED,
|
|
error);
|
|
}
|
|
|
|
if (this.config_.msf.namespaces.length) {
|
|
// Subscribe to the catalog in this namespace
|
|
this.subscribeToCatalog_(this.config_.msf.namespaces);
|
|
} else {
|
|
// Listen for announcements
|
|
// Catalog subscription will happen after announcement is received
|
|
this.listenForAnnouncements_();
|
|
}
|
|
|
|
this.presentationTimeline_ = new shaka.media.PresentationTimeline(
|
|
/* presentationStartTime= */ 0, /* delay= */ 0);
|
|
this.presentationTimeline_.setStatic(true);
|
|
|
|
this.manifest_ = {
|
|
presentationTimeline: this.presentationTimeline_,
|
|
variants: [],
|
|
textStreams: [],
|
|
imageStreams: [],
|
|
chapterStreams: [],
|
|
offlineSessionIds: [],
|
|
sequenceMode: false,
|
|
ignoreManifestTimestampsInSegmentsMode: false,
|
|
type: shaka.media.ManifestParser.MSF,
|
|
serviceDescription: null,
|
|
nextUrl: null,
|
|
periodCount: 1,
|
|
gapCount: 0,
|
|
isLowLatency: false,
|
|
startTime: null,
|
|
};
|
|
|
|
let catalog;
|
|
try {
|
|
catalog = await shaka.util.Functional.promiseWithTimeout(
|
|
/* seconds= */ 10, this.catalogPromise_);
|
|
} catch (e) {
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MANIFEST,
|
|
shaka.util.Error.Code.CATALOG_TIMEOUT);
|
|
}
|
|
|
|
await this.processCatalog_(catalog);
|
|
|
|
if (!this.presentationTimeline_.isLive()) {
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MANIFEST,
|
|
shaka.util.Error.Code.MSF_VOD_CONTENT_NOT_SUPPORTED);
|
|
}
|
|
|
|
this.createVariants_();
|
|
|
|
this.manifest_.isLowLatency = this.presentationTimeline_.isDynamic();
|
|
this.manifest_.variants = this.variants_;
|
|
this.manifest_.textStreams = this.textStreams_;
|
|
|
|
this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
|
|
|
|
return this.manifest_;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @exportInterface
|
|
*/
|
|
async stop() {
|
|
this.unregisterCatalogCallback_?.();
|
|
this.unregisterCatalogCallback_ = null;
|
|
this.unregisterPublishNamespaceCallback_?.();
|
|
this.unregisterPublishNamespaceCallback_ = null;
|
|
this.unregisterTracksCallback_.forEach((callback, key) => {
|
|
callback();
|
|
});
|
|
this.unregisterTracksCallback_.clear();
|
|
if (this.connection_) {
|
|
// Try to close the connection if it's not already closed
|
|
try {
|
|
// WebTransport requires some time to close connections, so we set
|
|
// 1 second here, but this is based on experimental testing only.
|
|
await shaka.util.Functional.delay(/* seconds= */ 1);
|
|
await this.connection_.close();
|
|
this.connection_ = null;
|
|
} catch (error) {
|
|
// Ignore error
|
|
}
|
|
}
|
|
this.msfTransport_?.release();
|
|
this.msfTransport_ = null;
|
|
this.playerInterface_ = null;
|
|
this.config_ = null;
|
|
this.manifest_ = null;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @exportInterface
|
|
*/
|
|
update() {}
|
|
|
|
/**
|
|
* @override
|
|
* @exportInterface
|
|
*/
|
|
onExpirationUpdated(sessionId, expiration) {
|
|
// No-op
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @exportInterface
|
|
*/
|
|
onInitialVariantChosen(variant) {
|
|
// No-op
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @exportInterface
|
|
*/
|
|
banLocation(uri) {
|
|
// No-op
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @exportInterface
|
|
*/
|
|
setMediaElement(mediaElement) {
|
|
// No-op
|
|
}
|
|
|
|
/**
|
|
* Listen for announcements from the server
|
|
* @private
|
|
*/
|
|
listenForAnnouncements_() {
|
|
try {
|
|
shaka.log.debug('Listening for announcements...');
|
|
|
|
// Subscribe to announcements
|
|
const unregister =
|
|
this.msfTransport_.registerPublishNamespaceCallback((namespace) => {
|
|
shaka.log.debug(`Received publish namespace callback with namespace:
|
|
${namespace.join('/')}`);
|
|
|
|
const namespaceStr = namespace.join('/');
|
|
const isAlreadyProcessed = this.publishNamespaces_.some((ns) =>
|
|
ns.join('/') === namespaceStr);
|
|
if (isAlreadyProcessed) {
|
|
shaka.log.debug(`Already processed namespace: ${namespaceStr}`);
|
|
return;
|
|
}
|
|
|
|
// Store the namespace
|
|
this.publishNamespaces_.push(namespace);
|
|
|
|
// Subscribe to the catalog in this namespace
|
|
this.subscribeToCatalog_(namespace);
|
|
});
|
|
|
|
// Save the unregister function
|
|
this.unregisterPublishNamespaceCallback_ = unregister;
|
|
|
|
// Log that we've registered the callback
|
|
shaka.log.debug('Announcement listener registered successfully');
|
|
} catch (error) {
|
|
shaka.log.error(`Error listening for announcements:
|
|
${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Subscribe to the catalog in the given namespace
|
|
*
|
|
* @param {Array<string>} namespace
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
async subscribeToCatalog_(namespace) {
|
|
try {
|
|
const namespaceStr = namespace.join('/');
|
|
shaka.log.debug(`Subscribing to catalog in namespace: ${namespaceStr}`);
|
|
|
|
// Subscribe to the "catalog" track in the given namespace
|
|
const trackAlias = await this.msfTransport_.subscribeTrack(
|
|
namespaceStr, 'catalog', (obj) => {
|
|
try {
|
|
const text = shaka.util.StringUtils.fromUTF8(obj['data']);
|
|
const catalog = /** @type {msfCatalog.Catalog} */(
|
|
JSON.parse(text));
|
|
this.catalogPromise_.resolve(catalog);
|
|
} catch (e) {
|
|
shaka.log.error(`Failed to decode catalog data:
|
|
${e instanceof Error ? e.message : String(e)}`);
|
|
}
|
|
});
|
|
|
|
// Create an unregister function that uses the track alias
|
|
const unregisterFunc = () => {
|
|
// Don't try to unsubscribe if we're already disconnecting
|
|
if (!this.playerInterface_) {
|
|
shaka.log.debug('Skipping catalog unsubscribe during disconnect');
|
|
return;
|
|
}
|
|
shaka.log.debug(`Unsubscribing from catalog track with alias
|
|
${trackAlias}`);
|
|
this.msfTransport_.unsubscribeTrack(trackAlias).catch((err) => {
|
|
shaka.log.error(`Failed to unsubscribe from catalog: ${err}`);
|
|
});
|
|
};
|
|
|
|
if (this.unregisterCatalogCallback_) {
|
|
shaka.log.debug('Unregistering previous catalog callback');
|
|
this.unregisterCatalogCallback_();
|
|
}
|
|
this.unregisterCatalogCallback_ = unregisterFunc;
|
|
shaka.log.debug(`Successfully subscribed to catalog in namespace:
|
|
${namespaceStr} with track alias: ${trackAlias}`);
|
|
} catch (error) {
|
|
shaka.log.error(`Error subscribing to catalog:
|
|
${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {msfCatalog.Track} track
|
|
* @param {string} trackKey
|
|
* @param {shaka.msf.Utils.ObjectCallback} callback
|
|
* @return {!Promise}
|
|
*/
|
|
async subscribeToTrack(track, trackKey, callback) {
|
|
try {
|
|
// Fall back to the session namespace when the catalog track object
|
|
// does not include an explicit namespace field (the namespace is
|
|
// typically established at the transport level via PUBLISH_NAMESPACE).
|
|
let namespace = track.namespace;
|
|
if (!namespace) {
|
|
if (this.config_.msf.namespaces.length) {
|
|
namespace = this.config_.msf.namespaces.join('/');
|
|
} else if (this.publishNamespaces_.length) {
|
|
namespace = this.publishNamespaces_[0].join('/');
|
|
} else {
|
|
namespace = '';
|
|
}
|
|
}
|
|
const trackName = track.name;
|
|
|
|
shaka.log.debug(`Subscribing to track: ${trackKey}`);
|
|
|
|
const trackAlias = await this.msfTransport_.subscribeTrack(
|
|
namespace, trackName, (obj) => {
|
|
shaka.log.debug(
|
|
`Received object for track ${trackKey} with ${obj}`);
|
|
callback(obj);
|
|
});
|
|
|
|
// Create an unregister function that uses the track alias
|
|
const unregisterFunc = () => {
|
|
// Don't try to unsubscribe if we're already disconnecting
|
|
if (!this.playerInterface_) {
|
|
shaka.log.debug('Skipping catalog unsubscribe during disconnect');
|
|
return;
|
|
}
|
|
shaka.log.debug(`Unsubscribing from catalog track with alias
|
|
${trackAlias}`);
|
|
this.msfTransport_.unsubscribeTrack(trackAlias).catch((err) => {
|
|
shaka.log.error(`Failed to unsubscribe from catalog: ${err}`);
|
|
});
|
|
};
|
|
|
|
// Store the subscription
|
|
this.unregisterTracksCallback_.set(trackKey, unregisterFunc);
|
|
shaka.log.debug(
|
|
`Subscribed to track ${trackKey} with alias ${trackAlias}`);
|
|
} catch (error) {
|
|
shaka.log.debug(`Error subscribing to track:
|
|
${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {msfCatalog.Catalog} catalog
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
processCatalog_(catalog) {
|
|
shaka.log.info('MSF Catalog:', catalog);
|
|
const promises = [];
|
|
let isLive = true;
|
|
let duration = Infinity;
|
|
let targetLatency = 0;
|
|
for (const track of catalog.tracks) {
|
|
if ('isLive' in track) {
|
|
isLive = track.isLive;
|
|
}
|
|
if (track.targetLatency) {
|
|
targetLatency = Math.max(targetLatency, track.targetLatency);
|
|
}
|
|
if (track.trackDuration) {
|
|
duration = Math.min(duration, track.trackDuration);
|
|
}
|
|
promises.push(this.processTrack_(track));
|
|
}
|
|
if (targetLatency) {
|
|
/** @type {shaka.extern.ServiceDescription} */
|
|
const serviceDescription = {
|
|
maxLatency: null,
|
|
maxPlaybackRate: null,
|
|
minLatency: null,
|
|
minPlaybackRate: null,
|
|
targetLatency: targetLatency / 1000,
|
|
};
|
|
this.manifest_.serviceDescription = serviceDescription;
|
|
}
|
|
if (isLive) {
|
|
this.presentationTimeline_.setStatic(false);
|
|
}
|
|
this.presentationTimeline_.setDuration(duration);
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
/**
|
|
* @param {msfCatalog.Track} track
|
|
* @private
|
|
*/
|
|
processTrack_(track) {
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
const ManifestParserUtils = shaka.util.ManifestParserUtils;
|
|
|
|
let initData = new Uint8Array([]);
|
|
if (track.initData) {
|
|
initData = shaka.util.Uint8ArrayUtils.fromBase64(track.initData);
|
|
}
|
|
|
|
/** @type {?shaka.media.SegmentUtils.BasicInfo} */
|
|
let basicInfo = null;
|
|
const validPackagings = [
|
|
'cmaf',
|
|
'chunk-per-object',
|
|
];
|
|
if (validPackagings.includes(track.packaging)) {
|
|
basicInfo = shaka.media.SegmentUtils.getBasicInfoFromMp4(
|
|
initData, initData, /* disableText= */ false);
|
|
}
|
|
|
|
if (!basicInfo) {
|
|
shaka.log.info('Skipping incompatible track', track);
|
|
return;
|
|
}
|
|
|
|
const timescale = basicInfo.timescale || track.timescale;
|
|
if (!timescale) {
|
|
shaka.log.info(
|
|
'Skipping incompatible track due missing timescale', track);
|
|
return;
|
|
}
|
|
|
|
const mimeType = basicInfo.mimeType || track.mimeType || '';
|
|
const codecs = basicInfo.codecs;
|
|
let language = track.lang;
|
|
if (!track.lang || track.lang == 'und') {
|
|
language = basicInfo.language || track.lang;
|
|
}
|
|
const frameRate = Number(basicInfo.frameRate) || track.framerate;
|
|
const width = Number(basicInfo.width) || track.width;
|
|
const height = Number(basicInfo.height) || track.height;
|
|
let channelsCount = basicInfo.channelCount;
|
|
if (!channelsCount && track.channelConfig) {
|
|
channelsCount = parseInt(track.channelConfig, 10);
|
|
}
|
|
const audioSamplingRate = basicInfo.sampleRate || track.samplerate || null;
|
|
const closedCaptions = basicInfo.closedCaptions;
|
|
const hdr = basicInfo.videoRange || undefined;
|
|
const colorGamut = basicInfo.colorGamut || undefined;
|
|
|
|
let type = ContentType.TEXT;
|
|
for (const format of ManifestParserUtils.VIDEO_CODEC_REGEXPS) {
|
|
if (format.test(codecs.trim())) {
|
|
type = ContentType.VIDEO;
|
|
}
|
|
}
|
|
if (type == ContentType.TEXT) {
|
|
for (const format of ManifestParserUtils.AUDIO_CODEC_REGEXPS) {
|
|
if (format.test(codecs.trim())) {
|
|
type = ContentType.AUDIO;
|
|
}
|
|
}
|
|
}
|
|
|
|
let kind = undefined;
|
|
let accessibilityPurpose = null;
|
|
|
|
const roles = [];
|
|
if (track.role) {
|
|
roles.push(track.role);
|
|
switch (track.role) {
|
|
case 'audiodescription':
|
|
if (type == ContentType.AUDIO) {
|
|
accessibilityPurpose =
|
|
shaka.media.ManifestParser.AccessibilityPurpose.VISUALLY_IMPAIRED;
|
|
}
|
|
break;
|
|
case 'caption':
|
|
if (type == ContentType.TEXT) {
|
|
kind = ManifestParserUtils.TextStreamKind.CLOSED_CAPTION;
|
|
}
|
|
break;
|
|
case 'subtitle':
|
|
if (type == ContentType.TEXT) {
|
|
kind = ManifestParserUtils.TextStreamKind.SUBTITLE;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/** @type {shaka.extern.Stream} */
|
|
const stream = {
|
|
id: this.globalId_++,
|
|
originalId: track.name,
|
|
groupId: null,
|
|
createSegmentIndex: () => Promise.resolve(),
|
|
segmentIndex: null,
|
|
mimeType,
|
|
codecs,
|
|
supplementalCodecs: '',
|
|
kind,
|
|
encrypted: false,
|
|
drmInfos: basicInfo.drmInfos,
|
|
keyIds: new Set(),
|
|
language: shaka.util.LanguageUtils.normalize(language || 'und'),
|
|
originalLanguage: language || null,
|
|
label: track.label || null,
|
|
type,
|
|
primary: false,
|
|
trickModeVideo: null,
|
|
dependencyStream: null,
|
|
emsgSchemeIdUris: null,
|
|
frameRate,
|
|
pixelAspectRatio: undefined,
|
|
width,
|
|
height,
|
|
bandwidth: track.bitrate,
|
|
roles,
|
|
forced: false,
|
|
channelsCount,
|
|
audioSamplingRate,
|
|
spatialAudio: false,
|
|
closedCaptions,
|
|
hdr,
|
|
colorGamut,
|
|
videoLayout: undefined,
|
|
tilesLayout: undefined,
|
|
accessibilityPurpose,
|
|
external: false,
|
|
fastSwitching: false,
|
|
fullMimeTypes: new Set([shaka.util.MimeUtils.getFullType(
|
|
mimeType, codecs)]),
|
|
isAudioMuxedInVideo: false,
|
|
baseOriginalId: null,
|
|
};
|
|
|
|
const trackKey = `${track.namespace || ''}/${track.name}`;
|
|
|
|
const initSegmentReference = new shaka.media.InitSegmentReference(
|
|
() => [],
|
|
/* startBytes= */ 0,
|
|
/* endBytes= */ null,
|
|
/* mediaQuality= */ null,
|
|
timescale,
|
|
initData);
|
|
|
|
stream.createSegmentIndex = () => {
|
|
/** @type {?shaka.util.PublicPromise} */
|
|
let promise = new shaka.util.PublicPromise();
|
|
this.subscribeToTrack(track, trackKey, (obj) => {
|
|
if (!stream.segmentIndex || !obj.data.byteLength) {
|
|
return;
|
|
}
|
|
const SegmentUtils = shaka.media.SegmentUtils;
|
|
|
|
goog.asserts.assert(typeof timescale == 'number',
|
|
'Timescale should be a number!');
|
|
const {startTime, duration} =
|
|
SegmentUtils.getStartTimeAndDurationFromMp4(obj.data, timescale);
|
|
|
|
const reference = new shaka.media.SegmentReference(
|
|
/* startTime= */ startTime,
|
|
/* endTime= */ startTime + duration,
|
|
/* getUris= */ () => [],
|
|
/* startByte= */ 0,
|
|
/* endByte= */ null,
|
|
/* initSegmentReference= */ initSegmentReference,
|
|
/* presentationTimeOffset= */ 0,
|
|
/* appendWindowStart= */ 0,
|
|
/* appendWindowEnd= */ Infinity);
|
|
|
|
reference.setSegmentData(obj.data);
|
|
|
|
// Track the first segment start time per content type.
|
|
// We must not lock the presentation timeline until all expected
|
|
// stream types have received at least one segment, otherwise
|
|
// streams with earlier timestamps get their segments evicted
|
|
// immediately by mergeAndEvict (e.g. video starts 4s before audio).
|
|
if (!this.firstSegmentStartTimes_.has(type)) {
|
|
this.firstSegmentStartTimes_.set(type, startTime);
|
|
}
|
|
|
|
const timelineLocked = this.presentationTimeline_.isStartTimeLocked();
|
|
|
|
if (timelineLocked) {
|
|
const evictTime = Math.min(reference.startTime - 2,
|
|
this.presentationTimeline_.getSegmentAvailabilityStart());
|
|
stream.segmentIndex.mergeAndEvict([reference], evictTime);
|
|
} else {
|
|
stream.segmentIndex.merge([reference]);
|
|
}
|
|
|
|
this.presentationTimeline_.notifySegments([reference]);
|
|
|
|
if (!timelineLocked) {
|
|
// Only lock once we have first segments from all expected types.
|
|
const hasAudio = this.audioStreams_.length > 0;
|
|
const hasVideo = this.videoStreams_.length > 0;
|
|
const gotAudio = this.firstSegmentStartTimes_.has(
|
|
ContentType.AUDIO);
|
|
const gotVideo = this.firstSegmentStartTimes_.has(
|
|
ContentType.VIDEO);
|
|
const allReady = (!hasAudio || gotAudio) && (!hasVideo || gotVideo);
|
|
|
|
if (allReady) {
|
|
// Set availability duration wide enough to cover the gap
|
|
// between the earliest and latest stream start times, so
|
|
// segments from streams with earlier timestamps aren't evicted.
|
|
const starts = [...this.firstSegmentStartTimes_.values()];
|
|
const gap = Math.max(...starts) - Math.min(...starts);
|
|
this.presentationTimeline_.lockStartTime();
|
|
this.presentationTimeline_.setSegmentAvailabilityDuration(
|
|
gap + duration);
|
|
}
|
|
}
|
|
|
|
if (!this.config_.disableText &&
|
|
this.isFirstVideoSegment_ && type == ContentType.VIDEO) {
|
|
this.isFirstVideoSegment_ = false;
|
|
const videoInfo = shaka.media.SegmentUtils.getBasicInfoFromMp4(
|
|
initData, obj.data, /* disableText= */ false);
|
|
stream.closedCaptions = videoInfo.closedCaptions;
|
|
if (this.manifest_) {
|
|
this.playerInterface_.makeTextStreamsForClosedCaptions(
|
|
this.manifest_);
|
|
}
|
|
}
|
|
|
|
promise?.resolve();
|
|
promise = null;
|
|
});
|
|
stream.segmentIndex = new shaka.media.SegmentIndex([]);
|
|
// goog.asserts.assert(promise != null, 'promise must not be null!');
|
|
return /** @type {!Promise} */(promise);
|
|
};
|
|
|
|
stream.closeSegmentIndex = () => {
|
|
if (this.unregisterTracksCallback_.has(trackKey)) {
|
|
this.unregisterTracksCallback_.get(trackKey)();
|
|
this.unregisterTracksCallback_.delete(trackKey);
|
|
}
|
|
// If we have a segment index, release it.
|
|
stream.segmentIndex?.release();
|
|
stream.segmentIndex = null;
|
|
};
|
|
|
|
switch (type) {
|
|
case ContentType.AUDIO:
|
|
if (!this.config_.disableAudio) {
|
|
this.audioStreams_.push(stream);
|
|
}
|
|
break;
|
|
case ContentType.VIDEO:
|
|
if (!this.config_.disableVideo) {
|
|
this.videoStreams_.push(stream);
|
|
}
|
|
break;
|
|
case ContentType.TEXT:
|
|
if (!this.config_.disableText) {
|
|
this.textStreams_.push(stream);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
createVariants_() {
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
|
|
// Create variants for all audio/video combinations.
|
|
let nextVariantId = 0;
|
|
const variants = [];
|
|
if (!this.videoStreams_.length || !this.audioStreams_.length) {
|
|
// For audio-only or video-only content, just give each stream its own
|
|
// variant.
|
|
const streams = this.videoStreams_.length ? this.videoStreams_ :
|
|
this.audioStreams_;
|
|
for (const stream of streams) {
|
|
const id = nextVariantId++;
|
|
let bandwidth = stream.bandwidth || 0;
|
|
if (stream.dependencyStream) {
|
|
bandwidth += stream.dependencyStream.bandwidth || 0;
|
|
}
|
|
variants.push({
|
|
id,
|
|
language: stream.language,
|
|
disabledUntilTime: 0,
|
|
primary: stream.primary,
|
|
audio: stream.type == ContentType.AUDIO ? stream : null,
|
|
video: stream.type == ContentType.VIDEO ? stream : null,
|
|
bandwidth,
|
|
drmInfos: stream.drmInfos,
|
|
allowedByApplication: true,
|
|
allowedByKeySystem: true,
|
|
decodingInfos: [],
|
|
});
|
|
}
|
|
} else {
|
|
for (const audio of this.audioStreams_) {
|
|
for (const video of this.videoStreams_) {
|
|
const commonDrmInfos = shaka.drm.DrmUtils.getCommonDrmInfos(
|
|
audio.drmInfos, video.drmInfos);
|
|
|
|
if (audio.drmInfos.length && video.drmInfos.length &&
|
|
!commonDrmInfos.length) {
|
|
shaka.log.warning(
|
|
'Incompatible DRM in audio & video, skipping variant creation.',
|
|
audio, video);
|
|
continue;
|
|
}
|
|
|
|
let bandwidth = (audio.bandwidth || 0) + (video.bandwidth || 0);
|
|
if (audio.dependencyStream) {
|
|
bandwidth += audio.dependencyStream.bandwidth || 0;
|
|
}
|
|
if (video.dependencyStream) {
|
|
bandwidth += video.dependencyStream.bandwidth || 0;
|
|
}
|
|
|
|
const id = nextVariantId++;
|
|
variants.push({
|
|
id,
|
|
language: audio.language,
|
|
disabledUntilTime: 0,
|
|
primary: audio.primary,
|
|
audio,
|
|
video,
|
|
bandwidth,
|
|
drmInfos: commonDrmInfos,
|
|
allowedByApplication: true,
|
|
allowedByKeySystem: true,
|
|
decodingInfos: [],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
this.variants_ = variants;
|
|
}
|
|
};
|
|
|
|
|
|
shaka.media.ManifestParser.registerParserByMime(
|
|
'application/msf', () => new shaka.msf.MSFParser());
|