mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-14 15:56:38 +03:00
2260aa9cf6
Related to https://github.com/shaka-project/shaka-player/issues/7602#issuecomment-2479518970 From 23009-1: The value of the minimum buffer time does not provide any instructions to the client on how long to buffer the media. The value however describes how much buffer a client should have under ideal network conditions. As such, MBT is not describing the burstiness or jitter in the network, it is describing the burstiness or jitter in the content encoding. Together with the BW value, it is a property of the content. Using the "leaky bucket" model, it is the size of the bucket that makes BW true, given the way the content is encoded
1161 lines
35 KiB
JavaScript
1161 lines
35 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
goog.provide('shaka.mss.MssParser');
|
|
|
|
goog.require('goog.asserts');
|
|
goog.require('shaka.Deprecate');
|
|
goog.require('shaka.abr.Ewma');
|
|
goog.require('shaka.log');
|
|
goog.require('shaka.media.InitSegmentReference');
|
|
goog.require('shaka.media.ManifestParser');
|
|
goog.require('shaka.media.PresentationTimeline');
|
|
goog.require('shaka.media.QualityObserver');
|
|
goog.require('shaka.media.SegmentIndex');
|
|
goog.require('shaka.media.SegmentReference');
|
|
goog.require('shaka.mss.ContentProtection');
|
|
goog.require('shaka.net.NetworkingEngine');
|
|
goog.require('shaka.util.Error');
|
|
goog.require('shaka.util.LanguageUtils');
|
|
goog.require('shaka.util.ManifestParserUtils');
|
|
goog.require('shaka.util.MimeUtils');
|
|
goog.require('shaka.util.Mp4Generator');
|
|
goog.require('shaka.util.OperationManager');
|
|
goog.require('shaka.util.PlayerConfiguration');
|
|
goog.require('shaka.util.Timer');
|
|
goog.require('shaka.util.TXml');
|
|
goog.require('shaka.util.XmlUtils');
|
|
|
|
|
|
/**
|
|
* Creates a new MSS parser.
|
|
*
|
|
* @implements {shaka.extern.ManifestParser}
|
|
* @export
|
|
*/
|
|
shaka.mss.MssParser = class {
|
|
/** Creates a new MSS parser. */
|
|
constructor() {
|
|
/** @private {?shaka.extern.ManifestConfiguration} */
|
|
this.config_ = null;
|
|
|
|
/** @private {?shaka.extern.ManifestParser.PlayerInterface} */
|
|
this.playerInterface_ = null;
|
|
|
|
/** @private {!Array.<string>} */
|
|
this.manifestUris_ = [];
|
|
|
|
/** @private {?shaka.extern.Manifest} */
|
|
this.manifest_ = null;
|
|
|
|
/** @private {number} */
|
|
this.globalId_ = 1;
|
|
|
|
/**
|
|
* The update period in seconds, or 0 for no updates.
|
|
* @private {number}
|
|
*/
|
|
this.updatePeriod_ = 0;
|
|
|
|
/** @private {?shaka.media.PresentationTimeline} */
|
|
this.presentationTimeline_ = null;
|
|
|
|
/**
|
|
* An ewma that tracks how long updates take.
|
|
* This is to mitigate issues caused by slow parsing on embedded devices.
|
|
* @private {!shaka.abr.Ewma}
|
|
*/
|
|
this.averageUpdateDuration_ = new shaka.abr.Ewma(5);
|
|
|
|
/** @private {shaka.util.Timer} */
|
|
this.updateTimer_ = new shaka.util.Timer(() => {
|
|
this.onUpdate_();
|
|
});
|
|
|
|
/** @private {!shaka.util.OperationManager} */
|
|
this.operationManager_ = new shaka.util.OperationManager();
|
|
|
|
/**
|
|
* @private {!Map.<number, !BufferSource>}
|
|
*/
|
|
this.initSegmentDataByStreamId_ = new Map();
|
|
|
|
/** @private {function():boolean} */
|
|
this.isPreloadFn_ = () => false;
|
|
}
|
|
|
|
/**
|
|
* @param {shaka.extern.ManifestConfiguration} config
|
|
* @param {(function():boolean)=} isPreloadFn
|
|
* @override
|
|
* @exportInterface
|
|
*/
|
|
configure(config, isPreloadFn) {
|
|
goog.asserts.assert(config.mss != null,
|
|
'MssManifestConfiguration should not be null!');
|
|
|
|
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.manifestUris_ = [uri];
|
|
this.playerInterface_ = playerInterface;
|
|
|
|
await this.requestManifest_();
|
|
|
|
// 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);
|
|
}
|
|
|
|
this.setUpdateTimer_();
|
|
|
|
goog.asserts.assert(this.manifest_, 'Manifest should be non-null!');
|
|
return this.manifest_;
|
|
}
|
|
|
|
/**
|
|
* Called when the update timer ticks.
|
|
*
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
async onUpdate_() {
|
|
goog.asserts.assert(this.updatePeriod_ >= 0,
|
|
'There should be an update period');
|
|
|
|
shaka.log.info('Updating manifest...');
|
|
|
|
try {
|
|
await this.requestManifest_();
|
|
} catch (error) {
|
|
goog.asserts.assert(error instanceof shaka.util.Error,
|
|
'Should only receive a Shaka error');
|
|
|
|
// Try updating again, but ensure we haven't been destroyed.
|
|
if (this.playerInterface_) {
|
|
// We will retry updating, so override the severity of the error.
|
|
error.severity = shaka.util.Error.Severity.RECOVERABLE;
|
|
this.playerInterface_.onError(error);
|
|
}
|
|
}
|
|
|
|
// Detect a call to stop()
|
|
if (!this.playerInterface_) {
|
|
return;
|
|
}
|
|
|
|
this.setUpdateTimer_();
|
|
}
|
|
|
|
/**
|
|
* Sets the update timer. Does nothing if the manifest is not live.
|
|
*
|
|
* @private
|
|
*/
|
|
setUpdateTimer_() {
|
|
if (this.updatePeriod_ <= 0) {
|
|
return;
|
|
}
|
|
|
|
const finalDelay = Math.max(
|
|
shaka.mss.MssParser.MIN_UPDATE_PERIOD_,
|
|
this.updatePeriod_,
|
|
this.averageUpdateDuration_.getEstimate());
|
|
|
|
// We do not run the timer as repeating because part of update is async and
|
|
// we need schedule the update after it finished.
|
|
this.updateTimer_.tickAfter(/* seconds= */ finalDelay);
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @exportInterface
|
|
*/
|
|
stop() {
|
|
this.playerInterface_ = null;
|
|
this.config_ = null;
|
|
this.manifestUris_ = [];
|
|
this.manifest_ = null;
|
|
|
|
if (this.updateTimer_ != null) {
|
|
this.updateTimer_.stop();
|
|
this.updateTimer_ = null;
|
|
}
|
|
|
|
this.initSegmentDataByStreamId_.clear();
|
|
|
|
return this.operationManager_.destroy();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @exportInterface
|
|
*/
|
|
async update() {
|
|
try {
|
|
await this.requestManifest_();
|
|
} catch (error) {
|
|
if (!this.playerInterface_ || !error) {
|
|
return;
|
|
}
|
|
goog.asserts.assert(error instanceof shaka.util.Error, 'Bad error type');
|
|
this.playerInterface_.onError(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @exportInterface
|
|
*/
|
|
onExpirationUpdated(sessionId, expiration) {
|
|
// No-op
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @exportInterface
|
|
*/
|
|
onInitialVariantChosen(variant) {
|
|
// No-op
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @exportInterface
|
|
*/
|
|
banLocation(uri) {
|
|
// No-op
|
|
}
|
|
|
|
/** @override */
|
|
setMediaElement(mediaElement) {
|
|
// No-op
|
|
}
|
|
|
|
/**
|
|
* Makes a network request for the manifest and parses the resulting data.
|
|
*
|
|
* @private
|
|
*/
|
|
async requestManifest_() {
|
|
const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
|
|
const type = shaka.net.NetworkingEngine.AdvancedRequestType.MSS;
|
|
const request = shaka.net.NetworkingEngine.makeRequest(
|
|
this.manifestUris_, this.config_.retryParameters);
|
|
const networkingEngine = this.playerInterface_.networkingEngine;
|
|
|
|
const startTime = Date.now();
|
|
const operation = networkingEngine.request(requestType, request, {
|
|
type,
|
|
isPreload: this.isPreloadFn_(),
|
|
});
|
|
this.operationManager_.manage(operation);
|
|
|
|
const response = await operation.promise;
|
|
|
|
// Detect calls to stop().
|
|
if (!this.playerInterface_) {
|
|
return;
|
|
}
|
|
|
|
// For redirections add the response uri to the first entry in the
|
|
// Manifest Uris array.
|
|
if (response.uri && !this.manifestUris_.includes(response.uri)) {
|
|
this.manifestUris_.unshift(response.uri);
|
|
}
|
|
|
|
// This may throw, but it will result in a failed promise.
|
|
this.parseManifest_(response.data, response.uri);
|
|
// Keep track of how long the longest manifest update took.
|
|
const endTime = Date.now();
|
|
const updateDuration = (endTime - startTime) / 1000.0;
|
|
this.averageUpdateDuration_.sample(1, updateDuration);
|
|
}
|
|
|
|
/**
|
|
* Parses the manifest XML. This also handles updates and will update the
|
|
* stored manifest.
|
|
*
|
|
* @param {BufferSource} data
|
|
* @param {string} finalManifestUri The final manifest URI, which may
|
|
* differ from this.manifestUri_ if there has been a redirect.
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
parseManifest_(data, finalManifestUri) {
|
|
let manifestData = data;
|
|
const manifestPreprocessor = this.config_.mss.manifestPreprocessor;
|
|
const defaultManifestPreprocessor =
|
|
shaka.util.PlayerConfiguration.defaultManifestPreprocessor;
|
|
if (manifestPreprocessor != defaultManifestPreprocessor) {
|
|
shaka.Deprecate.deprecateFeature(5,
|
|
'manifest.mss.manifestPreprocessor configuration',
|
|
'Please Use manifest.mss.manifestPreprocessorTXml instead.');
|
|
const mssElement =
|
|
shaka.util.XmlUtils.parseXml(manifestData, 'SmoothStreamingMedia');
|
|
if (!mssElement) {
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MANIFEST,
|
|
shaka.util.Error.Code.MSS_INVALID_XML,
|
|
finalManifestUri);
|
|
}
|
|
manifestPreprocessor(mssElement);
|
|
manifestData = shaka.util.XmlUtils.toArrayBuffer(mssElement);
|
|
}
|
|
const mss = shaka.util.TXml.parseXml(manifestData, 'SmoothStreamingMedia');
|
|
if (!mss) {
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MANIFEST,
|
|
shaka.util.Error.Code.MSS_INVALID_XML,
|
|
finalManifestUri);
|
|
}
|
|
const manifestPreprocessorTXml = this.config_.mss.manifestPreprocessorTXml;
|
|
const defaultManifestPreprocessorTXml =
|
|
shaka.util.PlayerConfiguration.defaultManifestPreprocessorTXml;
|
|
if (manifestPreprocessorTXml != defaultManifestPreprocessorTXml) {
|
|
manifestPreprocessorTXml(mss);
|
|
}
|
|
this.processManifest_(mss, finalManifestUri);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
|
|
/**
|
|
* Takes a formatted MSS and converts it into a manifest.
|
|
*
|
|
* @param {!shaka.extern.xml.Node} mss
|
|
* @param {string} finalManifestUri The final manifest URI, which may
|
|
* differ from this.manifestUri_ if there has been a redirect.
|
|
* @private
|
|
*/
|
|
processManifest_(mss, finalManifestUri) {
|
|
const TXml = shaka.util.TXml;
|
|
|
|
if (!this.presentationTimeline_) {
|
|
this.presentationTimeline_ = new shaka.media.PresentationTimeline(
|
|
/* presentationStartTime= */ null, /* delay= */ 0);
|
|
}
|
|
|
|
const isLive = TXml.parseAttr(mss, 'IsLive',
|
|
TXml.parseBoolean, /* defaultValue= */ false);
|
|
|
|
if (isLive) {
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MANIFEST,
|
|
shaka.util.Error.Code.MSS_LIVE_CONTENT_NOT_SUPPORTED);
|
|
}
|
|
|
|
this.presentationTimeline_.setStatic(!isLive);
|
|
|
|
const timescale = TXml.parseAttr(mss, 'TimeScale',
|
|
TXml.parseNonNegativeInt, shaka.mss.MssParser.DEFAULT_TIME_SCALE_);
|
|
goog.asserts.assert(timescale && timescale >= 0,
|
|
'Timescale must be defined!');
|
|
|
|
let dvrWindowLength = TXml.parseAttr(mss, 'DVRWindowLength',
|
|
TXml.parseNonNegativeInt);
|
|
// If the DVRWindowLength field is omitted for a live presentation or set
|
|
// to 0, the DVR window is effectively infinite
|
|
if (isLive && (dvrWindowLength === 0 || isNaN(dvrWindowLength))) {
|
|
dvrWindowLength = Infinity;
|
|
}
|
|
// Start-over
|
|
const canSeek = TXml.parseAttr(mss, 'CanSeek',
|
|
TXml.parseBoolean, /* defaultValue= */ false);
|
|
if (dvrWindowLength === 0 && canSeek) {
|
|
dvrWindowLength = Infinity;
|
|
}
|
|
|
|
let segmentAvailabilityDuration = null;
|
|
if (dvrWindowLength && dvrWindowLength > 0) {
|
|
segmentAvailabilityDuration = dvrWindowLength / timescale;
|
|
}
|
|
|
|
// If it's live, we check for an override.
|
|
if (isLive && !isNaN(this.config_.availabilityWindowOverride)) {
|
|
segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
|
|
}
|
|
|
|
// If it's null, that means segments are always available. This is always
|
|
// the case for VOD, and sometimes the case for live.
|
|
if (segmentAvailabilityDuration == null) {
|
|
segmentAvailabilityDuration = Infinity;
|
|
}
|
|
|
|
this.presentationTimeline_.setSegmentAvailabilityDuration(
|
|
segmentAvailabilityDuration);
|
|
|
|
// Duration in timescale units.
|
|
const duration = TXml.parseAttr(mss, 'Duration',
|
|
TXml.parseNonNegativeInt, Infinity);
|
|
goog.asserts.assert(duration && duration >= 0,
|
|
'Duration must be defined!');
|
|
|
|
if (!isLive) {
|
|
this.presentationTimeline_.setDuration(duration / timescale);
|
|
}
|
|
|
|
/** @type {!shaka.mss.MssParser.Context} */
|
|
const context = {
|
|
variants: [],
|
|
textStreams: [],
|
|
timescale: timescale,
|
|
duration: duration / timescale,
|
|
};
|
|
|
|
this.parseStreamIndexes_(mss, context);
|
|
|
|
// These steps are not done on manifest update.
|
|
if (!this.manifest_) {
|
|
this.manifest_ = {
|
|
presentationTimeline: this.presentationTimeline_,
|
|
variants: context.variants,
|
|
textStreams: context.textStreams,
|
|
imageStreams: [],
|
|
offlineSessionIds: [],
|
|
sequenceMode: this.config_.mss.sequenceMode,
|
|
ignoreManifestTimestampsInSegmentsMode: false,
|
|
type: shaka.media.ManifestParser.MSS,
|
|
serviceDescription: null,
|
|
nextUrl: null,
|
|
periodCount: 1,
|
|
gapCount: 0,
|
|
isLowLatency: false,
|
|
startTime: null,
|
|
};
|
|
|
|
// This is the first point where we have a meaningful presentation start
|
|
// time, and we need to tell PresentationTimeline that so that it can
|
|
// maintain consistency from here on.
|
|
this.presentationTimeline_.lockStartTime();
|
|
} else {
|
|
// Just update the variants and text streams.
|
|
this.manifest_.variants = context.variants;
|
|
this.manifest_.textStreams = context.textStreams;
|
|
|
|
// Re-filter the manifest. This will check any configured restrictions on
|
|
// new variants, and will pass any new init data to DrmEngine to ensure
|
|
// that key rotation works correctly.
|
|
this.playerInterface_.filter(this.manifest_);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!shaka.extern.xml.Node} mss
|
|
* @param {!shaka.mss.MssParser.Context} context
|
|
* @private
|
|
*/
|
|
parseStreamIndexes_(mss, context) {
|
|
const ContentProtection = shaka.mss.ContentProtection;
|
|
const TXml = shaka.util.TXml;
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
|
|
const protectionElems = TXml.findChildren(mss, 'Protection');
|
|
const drmInfos = ContentProtection.parseFromProtection(
|
|
protectionElems, this.config_.mss.keySystemsBySystemId);
|
|
|
|
const audioStreams = [];
|
|
const videoStreams = [];
|
|
const textStreams = [];
|
|
const streamIndexes = TXml.findChildren(mss, 'StreamIndex');
|
|
for (const streamIndex of streamIndexes) {
|
|
const qualityLevels = TXml.findChildren(streamIndex, 'QualityLevel');
|
|
const timeline = this.createTimeline_(
|
|
streamIndex, context.timescale, context.duration);
|
|
// For each QualityLevel node, create a stream element
|
|
for (const qualityLevel of qualityLevels) {
|
|
const stream = this.createStream_(
|
|
streamIndex, qualityLevel, timeline, drmInfos, context);
|
|
if (!stream) {
|
|
// Skip unsupported stream
|
|
continue;
|
|
}
|
|
if (stream.type == ContentType.AUDIO &&
|
|
!this.config_.disableAudio) {
|
|
audioStreams.push(stream);
|
|
} else if (stream.type == ContentType.VIDEO &&
|
|
!this.config_.disableVideo) {
|
|
videoStreams.push(stream);
|
|
} else if (stream.type == ContentType.TEXT &&
|
|
!this.config_.disableText) {
|
|
textStreams.push(stream);
|
|
}
|
|
}
|
|
}
|
|
|
|
const variants = [];
|
|
for (const audio of (audioStreams.length > 0 ? audioStreams : [null])) {
|
|
for (const video of (videoStreams.length > 0 ? videoStreams : [null])) {
|
|
variants.push(this.createVariant_(audio, video));
|
|
}
|
|
}
|
|
context.variants = variants;
|
|
context.textStreams = textStreams;
|
|
}
|
|
|
|
/**
|
|
* @param {!shaka.extern.xml.Node} streamIndex
|
|
* @param {!shaka.extern.xml.Node} qualityLevel
|
|
* @param {!Array.<shaka.mss.MssParser.TimeRange>} timeline
|
|
* @param {!Array.<shaka.extern.DrmInfo>} drmInfos
|
|
* @param {!shaka.mss.MssParser.Context} context
|
|
* @return {?shaka.extern.Stream}
|
|
* @private
|
|
*/
|
|
createStream_(streamIndex, qualityLevel, timeline, drmInfos, context) {
|
|
const TXml = shaka.util.TXml;
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
const MssParser = shaka.mss.MssParser;
|
|
|
|
const type = streamIndex.attributes['Type'];
|
|
const isValidType = type === 'audio' || type === 'video' ||
|
|
type === 'text';
|
|
if (!isValidType) {
|
|
shaka.log.alwaysWarn('Ignoring unrecognized type:', type);
|
|
return null;
|
|
}
|
|
|
|
const lang = streamIndex.attributes['Language'];
|
|
const id = this.globalId_++;
|
|
|
|
const bandwidth = TXml.parseAttr(
|
|
qualityLevel, 'Bitrate', TXml.parsePositiveInt);
|
|
const width = TXml.parseAttr(
|
|
qualityLevel, 'MaxWidth', TXml.parsePositiveInt);
|
|
const height = TXml.parseAttr(
|
|
qualityLevel, 'MaxHeight', TXml.parsePositiveInt);
|
|
const channelsCount = TXml.parseAttr(
|
|
qualityLevel, 'Channels', TXml.parsePositiveInt);
|
|
const audioSamplingRate = TXml.parseAttr(
|
|
qualityLevel, 'SamplingRate', TXml.parsePositiveInt);
|
|
|
|
let duration = context.duration;
|
|
if (timeline.length) {
|
|
const start = timeline[0].start;
|
|
const end = timeline[timeline.length - 1].end;
|
|
duration = end - start;
|
|
}
|
|
|
|
const presentationDuration = this.presentationTimeline_.getDuration();
|
|
this.presentationTimeline_.setDuration(
|
|
Math.min(duration, presentationDuration));
|
|
|
|
/** @type {!shaka.extern.Stream} */
|
|
const stream = {
|
|
id: id,
|
|
originalId: streamIndex.attributes['Name'] || String(id),
|
|
groupId: null,
|
|
createSegmentIndex: () => Promise.resolve(),
|
|
closeSegmentIndex: () => Promise.resolve(),
|
|
segmentIndex: null,
|
|
mimeType: '',
|
|
codecs: '',
|
|
frameRate: undefined,
|
|
pixelAspectRatio: undefined,
|
|
bandwidth: bandwidth || 0,
|
|
width: width || undefined,
|
|
height: height || undefined,
|
|
kind: '',
|
|
encrypted: drmInfos.length > 0,
|
|
drmInfos: drmInfos,
|
|
keyIds: new Set(),
|
|
language: shaka.util.LanguageUtils.normalize(lang || 'und'),
|
|
originalLanguage: lang,
|
|
label: '',
|
|
type: '',
|
|
primary: false,
|
|
trickModeVideo: null,
|
|
emsgSchemeIdUris: [],
|
|
roles: [],
|
|
forced: false,
|
|
channelsCount: channelsCount,
|
|
audioSamplingRate: audioSamplingRate,
|
|
spatialAudio: false,
|
|
closedCaptions: null,
|
|
hdr: undefined,
|
|
colorGamut: undefined,
|
|
videoLayout: undefined,
|
|
tilesLayout: undefined,
|
|
matchedStreams: [],
|
|
mssPrivateData: {
|
|
duration: duration,
|
|
timescale: context.timescale,
|
|
codecPrivateData: null,
|
|
},
|
|
accessibilityPurpose: null,
|
|
external: false,
|
|
fastSwitching: false,
|
|
fullMimeTypes: new Set(),
|
|
isAudioMuxedInVideo: false,
|
|
};
|
|
|
|
// This is specifically for text tracks.
|
|
const subType = streamIndex.attributes['Subtype'];
|
|
if (subType) {
|
|
const role = MssParser.ROLE_MAPPING_[subType];
|
|
if (role) {
|
|
stream.roles.push(role);
|
|
}
|
|
if (role === 'main') {
|
|
stream.primary = true;
|
|
}
|
|
}
|
|
|
|
let fourCCValue = qualityLevel.attributes['FourCC'];
|
|
|
|
// If FourCC not defined at QualityLevel level,
|
|
// then get it from StreamIndex level
|
|
if (fourCCValue === null || fourCCValue === '') {
|
|
fourCCValue = streamIndex.attributes['FourCC'];
|
|
}
|
|
|
|
// If still not defined (optional for audio stream,
|
|
// see https://msdn.microsoft.com/en-us/library/ff728116%28v=vs.95%29.aspx),
|
|
// then we consider the stream is an audio AAC stream
|
|
if (!fourCCValue) {
|
|
if (type === 'audio') {
|
|
fourCCValue = 'AAC';
|
|
} else if (type === 'video') {
|
|
shaka.log.alwaysWarn('FourCC is not defined whereas it is required ' +
|
|
'for a QualityLevel element for a StreamIndex of type "video"');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Check if codec is supported
|
|
if (!MssParser.SUPPORTED_CODECS_.includes(fourCCValue.toUpperCase())) {
|
|
shaka.log.alwaysWarn('Codec not supported:', fourCCValue);
|
|
return null;
|
|
}
|
|
|
|
const codecPrivateData = this.getCodecPrivateData_(
|
|
qualityLevel, type, fourCCValue, stream);
|
|
stream.mssPrivateData.codecPrivateData = codecPrivateData;
|
|
|
|
switch (type) {
|
|
case 'audio':
|
|
if (!codecPrivateData) {
|
|
shaka.log.alwaysWarn('Quality unsupported without CodecPrivateData',
|
|
type);
|
|
return null;
|
|
}
|
|
stream.type = ContentType.AUDIO;
|
|
// This mimetype is fake to allow the transmuxing.
|
|
stream.mimeType = 'mss/audio/mp4';
|
|
stream.codecs = this.getAACCodec_(
|
|
qualityLevel, fourCCValue, codecPrivateData);
|
|
break;
|
|
case 'video':
|
|
if (!codecPrivateData) {
|
|
shaka.log.alwaysWarn('Quality unsupported without CodecPrivateData',
|
|
type);
|
|
return null;
|
|
}
|
|
stream.type = ContentType.VIDEO;
|
|
// This mimetype is fake to allow the transmuxing.
|
|
stream.mimeType = 'mss/video/mp4';
|
|
stream.codecs = this.getH264Codec_(
|
|
qualityLevel, codecPrivateData);
|
|
break;
|
|
case 'text':
|
|
stream.type = ContentType.TEXT;
|
|
stream.mimeType = 'application/mp4';
|
|
if (fourCCValue === 'TTML' || fourCCValue === 'DFXP') {
|
|
stream.codecs = 'stpp';
|
|
}
|
|
break;
|
|
}
|
|
stream.fullMimeTypes.add(shaka.util.MimeUtils.getFullType(
|
|
stream.mimeType, stream.codecs));
|
|
|
|
// Lazy-Load the segment index to avoid create all init segment at the
|
|
// same time
|
|
stream.createSegmentIndex = () => {
|
|
if (stream.segmentIndex) {
|
|
return Promise.resolve();
|
|
}
|
|
let initSegmentData;
|
|
if (this.initSegmentDataByStreamId_.has(stream.id)) {
|
|
initSegmentData = this.initSegmentDataByStreamId_.get(stream.id);
|
|
} else {
|
|
let videoNalus = [];
|
|
if (stream.type == ContentType.VIDEO) {
|
|
const codecPrivateData = stream.mssPrivateData.codecPrivateData;
|
|
videoNalus = codecPrivateData.split('00000001').slice(1);
|
|
}
|
|
/** @type {shaka.util.Mp4Generator.StreamInfo} */
|
|
const streamInfo = {
|
|
id: stream.id,
|
|
type: stream.type,
|
|
codecs: stream.codecs,
|
|
encrypted: stream.encrypted,
|
|
timescale: stream.mssPrivateData.timescale,
|
|
duration: stream.mssPrivateData.duration,
|
|
videoNalus: videoNalus,
|
|
audioConfig: new Uint8Array([]),
|
|
videoConfig: new Uint8Array([]),
|
|
hSpacing: 0,
|
|
vSpacing: 0,
|
|
data: null, // Data is not necessary for init segement.
|
|
stream: stream,
|
|
};
|
|
const mp4Generator = new shaka.util.Mp4Generator([streamInfo]);
|
|
initSegmentData = mp4Generator.initSegment();
|
|
this.initSegmentDataByStreamId_.set(stream.id, initSegmentData);
|
|
}
|
|
const qualityInfo =
|
|
shaka.media.QualityObserver.createQualityInfo(stream);
|
|
const initSegmentRef = new shaka.media.InitSegmentReference(
|
|
() => [],
|
|
/* startByte= */ 0,
|
|
/* endByte= */ null,
|
|
qualityInfo,
|
|
stream.mssPrivateData.timescale,
|
|
initSegmentData);
|
|
|
|
const segments = this.createSegments_(initSegmentRef,
|
|
stream, streamIndex, timeline);
|
|
|
|
stream.segmentIndex = new shaka.media.SegmentIndex(segments);
|
|
return Promise.resolve();
|
|
};
|
|
stream.closeSegmentIndex = () => {
|
|
// If we have a segment index, release it.
|
|
if (stream.segmentIndex) {
|
|
stream.segmentIndex.release();
|
|
stream.segmentIndex = null;
|
|
}
|
|
};
|
|
|
|
return stream;
|
|
}
|
|
|
|
/**
|
|
* @param {!shaka.extern.xml.Node} qualityLevel
|
|
* @param {string} type
|
|
* @param {string} fourCCValue
|
|
* @param {!shaka.extern.Stream} stream
|
|
* @return {?string}
|
|
* @private
|
|
*/
|
|
getCodecPrivateData_(qualityLevel, type, fourCCValue, stream) {
|
|
const codecPrivateData = qualityLevel.attributes['CodecPrivateData'];
|
|
if (codecPrivateData) {
|
|
return codecPrivateData;
|
|
}
|
|
if (type !== 'audio') {
|
|
return null;
|
|
}
|
|
// For the audio we can reconstruct the CodecPrivateData
|
|
// By default stereo
|
|
const channels = stream.channelsCount || 2;
|
|
// By default 44,1kHz.
|
|
const samplingRate = stream.audioSamplingRate || 44100;
|
|
|
|
const samplingFrequencyIndex = {
|
|
96000: 0x0,
|
|
88200: 0x1,
|
|
64000: 0x2,
|
|
48000: 0x3,
|
|
44100: 0x4,
|
|
32000: 0x5,
|
|
24000: 0x6,
|
|
22050: 0x7,
|
|
16000: 0x8,
|
|
12000: 0x9,
|
|
11025: 0xA,
|
|
8000: 0xB,
|
|
7350: 0xC,
|
|
};
|
|
|
|
const indexFreq = samplingFrequencyIndex[samplingRate];
|
|
if (fourCCValue === 'AACH') {
|
|
// High Efficiency AAC Profile
|
|
const objectType = 0x05;
|
|
// 4 bytes :
|
|
// XXXXX XXXX XXXX XXXX
|
|
// 'ObjectType' 'Freq Index' 'Channels value' 'Extens Sampl Freq'
|
|
// XXXXX XXX XXXXXXX
|
|
// 'ObjectType' 'GAS' 'alignment = 0'
|
|
const data = new Uint8Array(4);
|
|
// In HE AAC Extension Sampling frequence
|
|
// equals to SamplingRate * 2
|
|
const extensionSamplingFrequencyIndex =
|
|
samplingFrequencyIndex[samplingRate * 2];
|
|
// Freq Index is present for 3 bits in the first byte, last bit is in
|
|
// the second
|
|
data[0] = (objectType << 3) | (indexFreq >> 1);
|
|
data[1] = (indexFreq << 7) | (channels << 3) |
|
|
(extensionSamplingFrequencyIndex >> 1);
|
|
// Origin object type equals to 2 => AAC Main Low Complexity
|
|
data[2] = (extensionSamplingFrequencyIndex << 7) | (0x02 << 2);
|
|
// Slignment bits
|
|
data[3] = 0x0;
|
|
// Put the 4 bytes in an 16 bits array
|
|
const arr16 = new Uint16Array(2);
|
|
arr16[0] = (data[0] << 8) + data[1];
|
|
arr16[1] = (data[2] << 8) + data[3];
|
|
// Convert decimal to hex value
|
|
return arr16[0].toString(16) + arr16[1].toString(16);
|
|
} else {
|
|
// AAC Main Low Complexity
|
|
const objectType = 0x02;
|
|
// 2 bytes:
|
|
// XXXXX XXXX XXXX XXX
|
|
// 'ObjectType' 'Freq Index' 'Channels value' 'GAS = 000'
|
|
const data = new Uint8Array(2);
|
|
// Freq Index is present for 3 bits in the first byte, last bit is in
|
|
// the second
|
|
data[0] = (objectType << 3) | (indexFreq >> 1);
|
|
data[1] = (indexFreq << 7) | (channels << 3);
|
|
// Put the 2 bytes in an 16 bits array
|
|
const arr16 = new Uint16Array(1);
|
|
arr16[0] = (data[0] << 8) + data[1];
|
|
// Convert decimal to hex value
|
|
return arr16[0].toString(16);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!shaka.extern.xml.Node} qualityLevel
|
|
* @param {string} fourCCValue
|
|
* @param {?string} codecPrivateData
|
|
* @return {string}
|
|
* @private
|
|
*/
|
|
getAACCodec_(qualityLevel, fourCCValue, codecPrivateData) {
|
|
let objectType = 0;
|
|
|
|
// Chrome problem, in implicit AAC HE definition, so when AACH is detected
|
|
// in FourCC set objectType to 5 => strange, it should be 2
|
|
if (fourCCValue === 'AACH') {
|
|
objectType = 0x05;
|
|
}
|
|
if (!codecPrivateData) {
|
|
// AAC Main Low Complexity => object Type = 2
|
|
objectType = 0x02;
|
|
if (fourCCValue === 'AACH') {
|
|
// High Efficiency AAC Profile = object Type = 5 SBR
|
|
objectType = 0x05;
|
|
}
|
|
} else if (objectType === 0) {
|
|
objectType = (parseInt(codecPrivateData.substr(0, 2), 16) & 0xF8) >> 3;
|
|
}
|
|
|
|
return 'mp4a.40.' + objectType;
|
|
}
|
|
|
|
/**
|
|
* @param {!shaka.extern.xml.Node} qualityLevel
|
|
* @param {?string} codecPrivateData
|
|
* @return {string}
|
|
* @private
|
|
*/
|
|
getH264Codec_(qualityLevel, codecPrivateData) {
|
|
// Extract from the CodecPrivateData field the hexadecimal representation
|
|
// of the following three bytes in the sequence parameter set NAL unit.
|
|
// => Find the SPS nal header
|
|
const nalHeader = /00000001[0-9]7/.exec(codecPrivateData);
|
|
if (!nalHeader.length) {
|
|
return '';
|
|
}
|
|
if (!codecPrivateData) {
|
|
return '';
|
|
}
|
|
// => Find the 6 characters after the SPS nalHeader (if it exists)
|
|
const avcoti = codecPrivateData.substr(
|
|
codecPrivateData.indexOf(nalHeader[0]) + 10, 6);
|
|
|
|
return 'avc1.' + avcoti;
|
|
}
|
|
|
|
/**
|
|
* @param {!shaka.media.InitSegmentReference} initSegmentRef
|
|
* @param {!shaka.extern.Stream} stream
|
|
* @param {!shaka.extern.xml.Node} streamIndex
|
|
* @param {!Array.<shaka.mss.MssParser.TimeRange>} timeline
|
|
* @return {!Array.<!shaka.media.SegmentReference>}
|
|
* @private
|
|
*/
|
|
createSegments_(initSegmentRef, stream, streamIndex, timeline) {
|
|
const ManifestParserUtils = shaka.util.ManifestParserUtils;
|
|
const url = streamIndex.attributes['Url'];
|
|
goog.asserts.assert(url, 'Missing URL for segments');
|
|
|
|
const mediaUrl = url.replace('{bitrate}', String(stream.bandwidth));
|
|
|
|
const segments = [];
|
|
for (const time of timeline) {
|
|
const getUris = () => {
|
|
return ManifestParserUtils.resolveUris(this.manifestUris_,
|
|
[mediaUrl.replace('{start time}', String(time.unscaledStart))]);
|
|
};
|
|
segments.push(new shaka.media.SegmentReference(
|
|
time.start,
|
|
time.end,
|
|
getUris,
|
|
/* startByte= */ 0,
|
|
/* endByte= */ null,
|
|
initSegmentRef,
|
|
/* timestampOffset= */ 0,
|
|
/* appendWindowStart= */ 0,
|
|
/* appendWindowEnd= */ stream.mssPrivateData.duration));
|
|
}
|
|
return segments;
|
|
}
|
|
|
|
/**
|
|
* Expands a streamIndex into an array-based timeline. The results are in
|
|
* seconds.
|
|
*
|
|
* @param {!shaka.extern.xml.Node} streamIndex
|
|
* @param {number} timescale
|
|
* @param {number} duration The duration in seconds.
|
|
* @return {!Array.<shaka.mss.MssParser.TimeRange>}
|
|
* @private
|
|
*/
|
|
createTimeline_(streamIndex, timescale, duration) {
|
|
goog.asserts.assert(
|
|
timescale > 0 && timescale < Infinity,
|
|
'timescale must be a positive, finite integer');
|
|
goog.asserts.assert(
|
|
duration > 0, 'duration must be a positive integer');
|
|
|
|
const TXml = shaka.util.TXml;
|
|
|
|
const timePoints = TXml.findChildren(streamIndex, 'c');
|
|
|
|
/** @type {!Array.<shaka.mss.MssParser.TimeRange>} */
|
|
const timeline = [];
|
|
let lastEndTime = 0;
|
|
|
|
for (let i = 0; i < timePoints.length; ++i) {
|
|
const timePoint = timePoints[i];
|
|
const next = timePoints[i + 1];
|
|
const t =
|
|
TXml.parseAttr(timePoint, 't', TXml.parseNonNegativeInt);
|
|
const d =
|
|
TXml.parseAttr(timePoint, 'd', TXml.parseNonNegativeInt);
|
|
const r = TXml.parseAttr(timePoint, 'r', TXml.parseInt);
|
|
|
|
if (!d) {
|
|
shaka.log.warning(
|
|
'"c" element must have a duration:',
|
|
'ignoring the remaining "c" elements.', timePoint);
|
|
return timeline;
|
|
}
|
|
|
|
let startTime = t != null ? t : lastEndTime;
|
|
|
|
let repeat = r || 0;
|
|
// Unlike in DASH, in MSS r does not start counting repetitions at 0 but
|
|
// at 1, to maintain the code equivalent to DASH if r exists we
|
|
// subtract 1.
|
|
if (repeat) {
|
|
repeat--;
|
|
}
|
|
if (repeat < 0) {
|
|
if (next) {
|
|
const nextStartTime =
|
|
TXml.parseAttr(next, 't', TXml.parseNonNegativeInt);
|
|
if (nextStartTime == null) {
|
|
shaka.log.warning(
|
|
'An "c" element cannot have a negative repeat',
|
|
'if the next "c" element does not have a valid start time:',
|
|
'ignoring the remaining "c" elements.', timePoint);
|
|
return timeline;
|
|
} else if (startTime >= nextStartTime) {
|
|
shaka.log.warning(
|
|
'An "c" element cannot have a negative repeatif its start ',
|
|
'time exceeds the next "c" element\'s start time:',
|
|
'ignoring the remaining "c" elements.', timePoint);
|
|
return timeline;
|
|
}
|
|
repeat = Math.ceil((nextStartTime - startTime) / d) - 1;
|
|
} else {
|
|
if (duration == Infinity) {
|
|
// The MSS spec. actually allows the last "c" element to have a
|
|
// negative repeat value even when it has an infinite
|
|
// duration. No one uses this feature and no one ever should,
|
|
// ever.
|
|
shaka.log.warning(
|
|
'The last "c" element cannot have a negative repeat',
|
|
'if the Period has an infinite duration:',
|
|
'ignoring the last "c" element.', timePoint);
|
|
return timeline;
|
|
} else if (startTime / timescale >= duration) {
|
|
shaka.log.warning(
|
|
'The last "c" element cannot have a negative repeat',
|
|
'if its start time exceeds the duration:',
|
|
'igoring the last "c" element.', timePoint);
|
|
return timeline;
|
|
}
|
|
repeat = Math.ceil((duration * timescale - startTime) / d) - 1;
|
|
}
|
|
}
|
|
|
|
for (let j = 0; j <= repeat; ++j) {
|
|
const endTime = startTime + d;
|
|
const item = {
|
|
start: startTime / timescale,
|
|
end: endTime / timescale,
|
|
unscaledStart: startTime,
|
|
};
|
|
timeline.push(item);
|
|
|
|
startTime = endTime;
|
|
lastEndTime = endTime;
|
|
}
|
|
}
|
|
|
|
return timeline;
|
|
}
|
|
|
|
/**
|
|
* @param {?shaka.extern.Stream} audioStream
|
|
* @param {?shaka.extern.Stream} videoStream
|
|
* @return {!shaka.extern.Variant}
|
|
* @private
|
|
*/
|
|
createVariant_(audioStream, videoStream) {
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
|
|
goog.asserts.assert(!audioStream ||
|
|
audioStream.type == ContentType.AUDIO, 'Audio parameter mismatch!');
|
|
goog.asserts.assert(!videoStream ||
|
|
videoStream.type == ContentType.VIDEO, 'Video parameter mismatch!');
|
|
|
|
let bandwidth = 0;
|
|
if (audioStream && audioStream.bandwidth && audioStream.bandwidth > 0) {
|
|
bandwidth += audioStream.bandwidth;
|
|
}
|
|
if (videoStream && videoStream.bandwidth && videoStream.bandwidth > 0) {
|
|
bandwidth += videoStream.bandwidth;
|
|
}
|
|
|
|
return {
|
|
id: this.globalId_++,
|
|
language: audioStream ? audioStream.language : 'und',
|
|
disabledUntilTime: 0,
|
|
primary: (!!audioStream && audioStream.primary) ||
|
|
(!!videoStream && videoStream.primary),
|
|
audio: audioStream,
|
|
video: videoStream,
|
|
bandwidth: bandwidth,
|
|
allowedByApplication: true,
|
|
allowedByKeySystem: true,
|
|
decodingInfos: [],
|
|
};
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Contains the minimum amount of time, in seconds, between manifest update
|
|
* requests.
|
|
*
|
|
* @private
|
|
* @const {number}
|
|
*/
|
|
shaka.mss.MssParser.MIN_UPDATE_PERIOD_ = 3;
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @const {number}
|
|
*/
|
|
shaka.mss.MssParser.DEFAULT_TIME_SCALE_ = 1e7;
|
|
|
|
|
|
/**
|
|
* MSS supported codecs.
|
|
*
|
|
* @private
|
|
* @const {!Array.<string>}
|
|
*/
|
|
shaka.mss.MssParser.SUPPORTED_CODECS_ = [
|
|
'AAC',
|
|
'AACL',
|
|
'AACH',
|
|
'AACP',
|
|
'AVC1',
|
|
'H264',
|
|
'TTML',
|
|
'DFXP',
|
|
];
|
|
|
|
|
|
/**
|
|
* MPEG-DASH Role and accessibility mapping for text tracks according to
|
|
* ETSI TS 103 285 v1.1.1 (section 7.1.2)
|
|
*
|
|
* @const {!Object.<string, string>}
|
|
* @private
|
|
*/
|
|
shaka.mss.MssParser.ROLE_MAPPING_ = {
|
|
'CAPT': 'main',
|
|
'SUBT': 'alternate',
|
|
'DESC': 'main',
|
|
};
|
|
|
|
|
|
/**
|
|
* @typedef {{
|
|
* variants: !Array.<shaka.extern.Variant>,
|
|
* textStreams: !Array.<shaka.extern.Stream>,
|
|
* timescale: number,
|
|
* duration: number
|
|
* }}
|
|
*
|
|
* @property {!Array.<shaka.extern.Variant>} variants
|
|
* The presentation's Variants.
|
|
* @property {!Array.<shaka.extern.Stream>} textStreams
|
|
* The presentation's text streams.
|
|
* @property {number} timescale
|
|
* The presentation's timescale.
|
|
* @property {number} duration
|
|
* The presentation's duration.
|
|
*/
|
|
shaka.mss.MssParser.Context;
|
|
|
|
|
|
/**
|
|
* @typedef {{
|
|
* start: number,
|
|
* unscaledStart: number,
|
|
* end: number
|
|
* }}
|
|
*
|
|
* @description
|
|
* Defines a time range of a media segment. Times are in seconds.
|
|
*
|
|
* @property {number} start
|
|
* The start time of the range.
|
|
* @property {number} unscaledStart
|
|
* The start time of the range in representation timescale units.
|
|
* @property {number} end
|
|
* The end time (exclusive) of the range.
|
|
*/
|
|
shaka.mss.MssParser.TimeRange;
|
|
|
|
shaka.media.ManifestParser.registerParserByMime(
|
|
'application/vnd.ms-sstr+xml', () => new shaka.mss.MssParser());
|