mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-17 16:26:39 +03:00
810f09ff1e
The period-flattening algorithm is run even on single-period content if the manifest is dynamic, because we don't know if another period will be added later. As a failsafe, we should create one output per input for single-period content, and we can ensure that no streams are unused by marking an input stream as the "best match" for an output if they share the same ID. That way, even if all other characteristics for two inputs are the same, the input-to-output mapping for single-period content will always be sane. That ID-based logic was missing for text streams. This change corrects that. Closes #2646 Change-Id: I28c6c63d92bcf710ae0072783911f9e66ed78228
1301 lines
44 KiB
JavaScript
1301 lines
44 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
goog.provide('shaka.util.PeriodCombiner');
|
|
|
|
goog.require('goog.asserts');
|
|
goog.require('shaka.media.DrmEngine');
|
|
goog.require('shaka.media.MetaSegmentIndex');
|
|
goog.require('shaka.media.SegmentIndex');
|
|
goog.require('shaka.util.Error');
|
|
goog.require('shaka.util.IReleasable');
|
|
goog.require('shaka.util.Iterables');
|
|
goog.require('shaka.util.LanguageUtils');
|
|
goog.require('shaka.util.ManifestParserUtils');
|
|
|
|
/**
|
|
* A utility to combine streams across periods.
|
|
*
|
|
* @implements {shaka.util.IReleasable}
|
|
* @final
|
|
*/
|
|
shaka.util.PeriodCombiner = class {
|
|
constructor() {
|
|
/** @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_ = [];
|
|
|
|
/**
|
|
* The IDs of the periods we have already used to generate streams.
|
|
* This helps us identify the periods which have been added when a live
|
|
* stream is updated.
|
|
*
|
|
* @private {!Set.<string>}
|
|
*/
|
|
this.usedPeriodIds_ = new Set();
|
|
}
|
|
|
|
/** @override */
|
|
release() {
|
|
const allStreams =
|
|
this.audioStreams_.concat(this.videoStreams_, this.textStreams_);
|
|
|
|
for (const stream of allStreams) {
|
|
if (stream.segmentIndex) {
|
|
stream.segmentIndex.release();
|
|
}
|
|
}
|
|
|
|
this.audioStreams_ = [];
|
|
this.videoStreams_ = [];
|
|
this.textStreams_ = [];
|
|
this.variants_ = [];
|
|
}
|
|
|
|
/** @return {!Array.<shaka.extern.Variant>} */
|
|
getVariants() {
|
|
return this.variants_;
|
|
}
|
|
|
|
/** @return {!Array.<shaka.extern.Stream>} */
|
|
getTextStreams() {
|
|
return this.textStreams_;
|
|
}
|
|
|
|
/**
|
|
* @param {!Array.<shaka.util.PeriodCombiner.Period>} periods
|
|
* @param {boolean} isDynamic
|
|
* @return {!Promise}
|
|
*/
|
|
async combinePeriods(periods, isDynamic) {
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
const Iterables = shaka.util.Iterables;
|
|
|
|
// Optimization: for single-period VOD, do nothing. This makes sure
|
|
// single-period DASH content will be 100% accurately represented in the
|
|
// output.
|
|
if (!isDynamic && periods.length == 1) {
|
|
const firstPeriod = periods[0];
|
|
this.audioStreams_ = firstPeriod.audioStreams;
|
|
this.videoStreams_ = firstPeriod.videoStreams;
|
|
this.textStreams_ = firstPeriod.textStreams;
|
|
} else {
|
|
// Find the first period we haven't seen before. Tag all the periods we
|
|
// see now as "used".
|
|
let firstNewPeriodIndex = -1;
|
|
for (const {i, item: period} of Iterables.enumerate(periods)) {
|
|
if (this.usedPeriodIds_.has(period.id)) {
|
|
// This isn't new.
|
|
} else {
|
|
// This one _is_ new.
|
|
this.usedPeriodIds_.add(period.id);
|
|
|
|
if (firstNewPeriodIndex == -1) {
|
|
// And it's the _first_ new one.
|
|
firstNewPeriodIndex = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (firstNewPeriodIndex == -1) {
|
|
// Nothing new? Nothing to do.
|
|
return;
|
|
}
|
|
|
|
const audioStreamsPerPeriod = periods.map(
|
|
(period) => period.audioStreams);
|
|
const videoStreamsPerPeriod = periods.map(
|
|
(period) => period.videoStreams);
|
|
const textStreamsPerPeriod = periods.map(
|
|
(period) => period.textStreams);
|
|
|
|
// It's okay to have a period with no text, but our algorithm fails on any
|
|
// period without matching streams. So we add dummy text streams to each
|
|
// period. Since we combine text streams by language, we might need a
|
|
// dummy even in periods with text streams already.
|
|
for (const textStreams of textStreamsPerPeriod) {
|
|
textStreams.push(shaka.util.PeriodCombiner.dummyTextStream_());
|
|
}
|
|
|
|
await shaka.util.PeriodCombiner.combine_(
|
|
this.audioStreams_,
|
|
audioStreamsPerPeriod,
|
|
firstNewPeriodIndex,
|
|
shaka.util.PeriodCombiner.cloneStream_,
|
|
shaka.util.PeriodCombiner.concatenateStreams_);
|
|
|
|
await shaka.util.PeriodCombiner.combine_(
|
|
this.videoStreams_,
|
|
videoStreamsPerPeriod,
|
|
firstNewPeriodIndex,
|
|
shaka.util.PeriodCombiner.cloneStream_,
|
|
shaka.util.PeriodCombiner.concatenateStreams_);
|
|
|
|
await shaka.util.PeriodCombiner.combine_(
|
|
this.textStreams_,
|
|
textStreamsPerPeriod,
|
|
firstNewPeriodIndex,
|
|
shaka.util.PeriodCombiner.cloneStream_,
|
|
shaka.util.PeriodCombiner.concatenateStreams_);
|
|
}
|
|
|
|
// 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_.concat(this.audioStreams_);
|
|
for (const stream of streams) {
|
|
const id = nextVariantId++;
|
|
variants.push({
|
|
id,
|
|
language: stream.language,
|
|
primary: stream.primary,
|
|
audio: stream.type == ContentType.AUDIO ? stream : null,
|
|
video: stream.type == ContentType.VIDEO ? stream : null,
|
|
bandwidth: stream.bandwidth || 0,
|
|
drmInfos: stream.drmInfos,
|
|
allowedByApplication: true,
|
|
allowedByKeySystem: true,
|
|
});
|
|
}
|
|
} else {
|
|
for (const audio of this.audioStreams_) {
|
|
for (const video of this.videoStreams_) {
|
|
const commonDrmInfos = shaka.media.DrmEngine.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;
|
|
}
|
|
|
|
const id = nextVariantId++;
|
|
variants.push({
|
|
id,
|
|
language: audio.language,
|
|
primary: audio.primary,
|
|
audio,
|
|
video,
|
|
bandwidth: (audio.bandwidth || 0) + (video.bandwidth || 0),
|
|
drmInfos: commonDrmInfos,
|
|
allowedByApplication: true,
|
|
allowedByKeySystem: true,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
this.variants_ = variants;
|
|
}
|
|
|
|
/**
|
|
* Stitch together DB streams across periods, taking a mix of stream types.
|
|
* The offline database does not separate these by type.
|
|
*
|
|
* Unlike the DASH case, this does not need to maintain any state for manifest
|
|
* updates.
|
|
*
|
|
* @param {!Array.<!Array.<shaka.extern.StreamDB>>} streamDbsPerPeriod
|
|
* @return {!Promise.<!Array.<shaka.extern.StreamDB>>}
|
|
*/
|
|
static async combineDbStreams(streamDbsPerPeriod) {
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
|
|
// Optimization: for single-period content, do nothing. This makes sure
|
|
// single-period DASH or any HLS content stored offline will be 100%
|
|
// accurately represented in the output.
|
|
if (streamDbsPerPeriod.length == 1) {
|
|
return streamDbsPerPeriod[0];
|
|
}
|
|
|
|
const audioStreamDbsPerPeriod = streamDbsPerPeriod.map(
|
|
(streams) => streams.filter((s) => s.type == ContentType.AUDIO));
|
|
const videoStreamDbsPerPeriod = streamDbsPerPeriod.map(
|
|
(streams) => streams.filter((s) => s.type == ContentType.VIDEO));
|
|
const textStreamDbsPerPeriod = streamDbsPerPeriod.map(
|
|
(streams) => streams.filter((s) => s.type == ContentType.TEXT));
|
|
|
|
// It's okay to have a period with no text, but our algorithm fails on any
|
|
// period without matching streams. So we add dummy text streams to each
|
|
// period. Since we combine text streams by language, we might need a
|
|
// dummy even in periods with text streams already.
|
|
for (const textStreams of textStreamDbsPerPeriod) {
|
|
textStreams.push(shaka.util.PeriodCombiner.dummyTextStreamDB_());
|
|
}
|
|
|
|
const combinedAudioStreamDbs = await shaka.util.PeriodCombiner.combine_(
|
|
/* outputStreams= */ [],
|
|
audioStreamDbsPerPeriod,
|
|
/* firstNewPeriodIndex= */ 0,
|
|
shaka.util.PeriodCombiner.cloneStreamDB_,
|
|
shaka.util.PeriodCombiner.concatenateStreamDBs_);
|
|
|
|
const combinedVideoStreamDbs = await shaka.util.PeriodCombiner.combine_(
|
|
/* outputStreams= */ [],
|
|
videoStreamDbsPerPeriod,
|
|
/* firstNewPeriodIndex= */ 0,
|
|
shaka.util.PeriodCombiner.cloneStreamDB_,
|
|
shaka.util.PeriodCombiner.concatenateStreamDBs_);
|
|
|
|
const combinedTextStreamDbs = await shaka.util.PeriodCombiner.combine_(
|
|
/* outputStreams= */ [],
|
|
textStreamDbsPerPeriod,
|
|
/* firstNewPeriodIndex= */ 0,
|
|
shaka.util.PeriodCombiner.cloneStreamDB_,
|
|
shaka.util.PeriodCombiner.concatenateStreamDBs_);
|
|
|
|
// Recreate variantIds from scratch in the output.
|
|
// HLS content is always single-period, so the early return at the top of
|
|
// this method would catch all HLS content. DASH content stored with v3.0
|
|
// will already be flattened before storage. Therefore the only content
|
|
// that reaches this point is multi-period DASH content stored before v3.0.
|
|
// Such content always had variants generated from all combinations of audio
|
|
// and video, so we can simply do that now without loss of correctness.
|
|
let nextVariantId = 0;
|
|
if (!combinedVideoStreamDbs.length || !combinedAudioStreamDbs.length) {
|
|
// For audio-only or video-only content, just give each stream its own
|
|
// variant ID.
|
|
const combinedStreamDbs =
|
|
combinedVideoStreamDbs.concat(combinedAudioStreamDbs);
|
|
for (const stream of combinedStreamDbs) {
|
|
stream.variantIds = [nextVariantId++];
|
|
}
|
|
} else {
|
|
for (const audio of combinedAudioStreamDbs) {
|
|
for (const video of combinedVideoStreamDbs) {
|
|
const id = nextVariantId++;
|
|
video.variantIds.push(id);
|
|
audio.variantIds.push(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
return combinedVideoStreamDbs
|
|
.concat(combinedAudioStreamDbs)
|
|
.concat(combinedTextStreamDbs);
|
|
}
|
|
|
|
/**
|
|
* Combine input Streams per period into flat output Streams.
|
|
* Templatized to handle both DASH Streams and offline StreamDBs.
|
|
*
|
|
* @param {!Array.<T>} outputStreams A list of existing output streams, to
|
|
* facilitate updates for live DASH content. Will be modified and returned.
|
|
* @param {!Array.<!Array.<T>>} streamsPerPeriod A list of lists of Streams
|
|
* from each period.
|
|
* @param {number} firstNewPeriodIndex An index into streamsPerPeriod which
|
|
* represents the first new period that hasn't been processed yet.
|
|
* @param {function(T):T} clone Make a clone of an input stream.
|
|
* @param {function(T, T)} concat Concatenate the second stream onto the end
|
|
* of the first.
|
|
*
|
|
* @return {!Promise.<!Array.<T>>} The same array passed to outputStreams,
|
|
* modified to include any newly-created streams.
|
|
*
|
|
* @template T
|
|
* Accepts either a StreamDB or Stream type.
|
|
*
|
|
* @private
|
|
*/
|
|
static async combine_(
|
|
outputStreams, streamsPerPeriod, firstNewPeriodIndex, clone, concat) {
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
const Iterables = shaka.util.Iterables;
|
|
|
|
const unusedStreamsPerPeriod = [];
|
|
for (const {i, item: streams} of Iterables.enumerate(streamsPerPeriod)) {
|
|
if (i >= firstNewPeriodIndex) {
|
|
// This periods streams are all new.
|
|
unusedStreamsPerPeriod.push(new Set(streams));
|
|
} else {
|
|
// This period's streams have all been used already.
|
|
unusedStreamsPerPeriod.push(new Set());
|
|
}
|
|
}
|
|
|
|
// First, extend all existing output Streams into the new periods.
|
|
for (const outputStream of outputStreams) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const ok = await shaka.util.PeriodCombiner.extendExistingOutputStream_(
|
|
outputStream, streamsPerPeriod, firstNewPeriodIndex, concat,
|
|
unusedStreamsPerPeriod);
|
|
if (!ok) {
|
|
// This output Stream was not properly extended to include streams from
|
|
// the new period. This is likely a bug in our algorithm, so throw an
|
|
// error.
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MANIFEST,
|
|
shaka.util.Error.Code.PERIOD_FLATTENING_FAILED);
|
|
}
|
|
|
|
// This output stream is now complete with content from all known
|
|
// periods.
|
|
} // for (const outputStream of outputStreams)
|
|
|
|
for (const unusedStreams of unusedStreamsPerPeriod) {
|
|
for (const stream of unusedStreams) {
|
|
// Create a new output stream which includes this input stream.
|
|
const outputStream =
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await shaka.util.PeriodCombiner.createNewOutputStream_(
|
|
stream, streamsPerPeriod, clone, concat,
|
|
unusedStreamsPerPeriod);
|
|
if (outputStream) {
|
|
outputStreams.push(outputStream);
|
|
} else {
|
|
// This is not a stream we can build output from, but it may become
|
|
// part of another output based on another period's stream.
|
|
}
|
|
} // for (const stream of unusedStreams)
|
|
} // for (const unusedStreams of unusedStreamsPerPeriod)
|
|
|
|
for (const unusedStreams of unusedStreamsPerPeriod) {
|
|
for (const stream of unusedStreams) {
|
|
if (stream.type == ContentType.TEXT && !stream.language) {
|
|
// This is one of our dummy text streams, so ignore it. We may not
|
|
// use them all, and that's fine.
|
|
continue;
|
|
}
|
|
|
|
// Any other unused stream is likely a bug in our algorithm, so throw
|
|
// an error.
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MANIFEST,
|
|
shaka.util.Error.Code.PERIOD_FLATTENING_FAILED);
|
|
}
|
|
}
|
|
|
|
return outputStreams;
|
|
}
|
|
|
|
/**
|
|
* @param {T} outputStream An existing output stream which needs to be
|
|
* extended into new periods.
|
|
* @param {!Array.<!Array.<T>>} streamsPerPeriod A list of lists of Streams
|
|
* from each period.
|
|
* @param {number} firstNewPeriodIndex An index into streamsPerPeriod which
|
|
* represents the first new period that hasn't been processed yet.
|
|
* @param {function(T, T)} concat Concatenate the second stream onto the end
|
|
* of the first.
|
|
* @param {!Array.<!Set.<T>>} unusedStreamsPerPeriod An array of sets of
|
|
* unused streams from each period.
|
|
*
|
|
* @return {!Promise.<boolean>}
|
|
*
|
|
* @template T
|
|
* Should only be called with a Stream type in practice, but has call sites
|
|
* from other templated functions that also accept a StreamDB.
|
|
*
|
|
* @private
|
|
*/
|
|
static async extendExistingOutputStream_(
|
|
outputStream, streamsPerPeriod, firstNewPeriodIndex, concat,
|
|
unusedStreamsPerPeriod) {
|
|
const matches = shaka.util.PeriodCombiner.findMatchesInAllPeriods_(
|
|
streamsPerPeriod, outputStream);
|
|
|
|
if (!matches) {
|
|
// We were unable to extend this output stream.
|
|
return false;
|
|
}
|
|
|
|
// This only exists where T == Stream, and this should only ever be called
|
|
// on Stream types. StreamDB should not have pre-existing output streams.
|
|
goog.asserts.assert(outputStream.createSegmentIndex,
|
|
'outputStream should be a Stream type!');
|
|
|
|
// We need to create all the per-period segment indexes and append them to
|
|
// the output's MetaSegmentIndex.
|
|
await Promise.all(matches.map((match) => match.createSegmentIndex()));
|
|
|
|
// Assure the compiler that matches didn't become null during the async
|
|
// operation above.
|
|
goog.asserts.assert(matches, 'Matches should be non-null');
|
|
|
|
shaka.util.PeriodCombiner.extendOutputStream_(
|
|
outputStream, matches, firstNewPeriodIndex, concat,
|
|
unusedStreamsPerPeriod);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Create a new output Stream based on a particular input Stream. Locates
|
|
* matching Streams in all other periods and combines them into an output
|
|
* Stream.
|
|
* Templatized to handle both DASH Streams and offline StreamDBs.
|
|
*
|
|
* @param {T} stream An input stream on which to base the output stream.
|
|
* @param {!Array.<!Array.<T>>} streamsPerPeriod A list of lists of Streams
|
|
* from each period.
|
|
* @param {function(T):T} clone Make a clone of an input stream.
|
|
* @param {function(T, T)} concat Concatenate the second stream onto the end
|
|
* of the first.
|
|
* @param {!Array.<!Set.<T>>} unusedStreamsPerPeriod An array of sets of
|
|
* unused streams from each period.
|
|
*
|
|
* @return {!Promise.<?T>} A newly-created output Stream, or null if matches
|
|
* could not be found.`
|
|
*
|
|
* @template T
|
|
* Accepts either a StreamDB or Stream type.
|
|
*
|
|
* @private
|
|
*/
|
|
static async createNewOutputStream_(
|
|
stream, streamsPerPeriod, clone, concat, unusedStreamsPerPeriod) {
|
|
// Start by cloning the stream without segments, key IDs, etc.
|
|
const outputStream = clone(stream);
|
|
|
|
// Find best-matching streams in all periods.
|
|
const matches = shaka.util.PeriodCombiner.findMatchesInAllPeriods_(
|
|
streamsPerPeriod, outputStream);
|
|
|
|
if (!matches) {
|
|
// This is not a stream we can build output from, but it may become part
|
|
// of another output based on another period's stream.
|
|
return null;
|
|
}
|
|
|
|
// This only exists where T == Stream.
|
|
if (outputStream.createSegmentIndex) {
|
|
// For T == Stream, we need to create all the per-period segment indexes
|
|
// in advance. concat() will add them to the output's MetaSegmentIndex.
|
|
await Promise.all(matches.map((match) => match.createSegmentIndex()));
|
|
}
|
|
|
|
// Assure the compiler that matches didn't become null during the async
|
|
// operation above.
|
|
goog.asserts.assert(matches, 'Matches should be non-null');
|
|
|
|
shaka.util.PeriodCombiner.extendOutputStream_(
|
|
outputStream, matches, /* firstNewPeriodIndex= */ 0, concat,
|
|
unusedStreamsPerPeriod);
|
|
|
|
return outputStream;
|
|
}
|
|
|
|
/**
|
|
* @param {T} outputStream An existing output stream which needs to be
|
|
* extended into new periods.
|
|
* @param {!Array.<T>} matches A list of matching Streams from each period.
|
|
* @param {number} firstNewPeriodIndex An index into streamsPerPeriod which
|
|
* represents the first new period that hasn't been processed yet.
|
|
* @param {function(T, T)} concat Concatenate the second stream onto the end
|
|
* of the first.
|
|
* @param {!Array.<!Set.<T>>} unusedStreamsPerPeriod An array of sets of
|
|
* unused streams from each period.
|
|
*
|
|
* @template T
|
|
* Accepts either a StreamDB or Stream type.
|
|
*
|
|
* @private
|
|
*/
|
|
static extendOutputStream_(
|
|
outputStream, matches, firstNewPeriodIndex, concat,
|
|
unusedStreamsPerPeriod) {
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
const LanguageUtils = shaka.util.LanguageUtils;
|
|
|
|
// Concatenate the new matches onto the stream, starting at the first new
|
|
// period.
|
|
const Iterables = shaka.util.Iterables;
|
|
for (const {i, item: match} of Iterables.enumerate(matches)) {
|
|
if (i >= firstNewPeriodIndex) {
|
|
concat(outputStream, match);
|
|
|
|
// We only consider an audio stream "used" if its language is related to
|
|
// the output language. There are scenarios where we want to generate
|
|
// separate tracks for each language, even when we are forced to connect
|
|
// unrelated languages across periods.
|
|
let used = true;
|
|
if (outputStream.type == ContentType.AUDIO) {
|
|
const relatedness = LanguageUtils.relatedness(
|
|
outputStream.language, match.language);
|
|
if (relatedness == 0) {
|
|
used = false;
|
|
}
|
|
}
|
|
|
|
if (used) {
|
|
unusedStreamsPerPeriod[i].delete(match);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clone a Stream to make an output Stream for combining others across
|
|
* periods.
|
|
*
|
|
* @param {shaka.extern.Stream} stream
|
|
* @return {shaka.extern.Stream}
|
|
* @private
|
|
*/
|
|
static cloneStream_(stream) {
|
|
const clone = /** @type {shaka.extern.Stream} */(Object.assign({}, stream));
|
|
|
|
// These are wiped out now and rebuilt later from the various per-period
|
|
// streams that match this output.
|
|
clone.originalId = null;
|
|
clone.createSegmentIndex = () => Promise.resolve();
|
|
clone.segmentIndex = new shaka.media.MetaSegmentIndex();
|
|
clone.emsgSchemeIdUris = [];
|
|
clone.keyIds = new Set();
|
|
clone.closedCaptions = null;
|
|
clone.trickModeVideo = null;
|
|
|
|
return clone;
|
|
}
|
|
|
|
/**
|
|
* Clone a StreamDB to make an output stream for combining others across
|
|
* periods.
|
|
*
|
|
* @param {shaka.extern.StreamDB} streamDb
|
|
* @return {shaka.extern.StreamDB}
|
|
* @private
|
|
*/
|
|
static cloneStreamDB_(streamDb) {
|
|
const clone = /** @type {shaka.extern.StreamDB} */(Object.assign(
|
|
{}, streamDb));
|
|
|
|
// These are wiped out now and rebuilt later from the various per-period
|
|
// streams that match this output.
|
|
clone.keyIds = new Set();
|
|
clone.segments = [];
|
|
clone.variantIds = [];
|
|
clone.closedCaptions = null;
|
|
|
|
return clone;
|
|
}
|
|
|
|
/**
|
|
* Combine the various fields of the input Stream into the output.
|
|
*
|
|
* @param {shaka.extern.Stream} output
|
|
* @param {shaka.extern.Stream} input
|
|
* @private
|
|
*/
|
|
static concatenateStreams_(output, input) {
|
|
// Combine arrays, keeping only the unique elements
|
|
const combineArrays = (a, b) => Array.from(new Set(a.concat(b)));
|
|
output.roles = combineArrays(output.roles, input.roles);
|
|
|
|
if (input.emsgSchemeIdUris) {
|
|
output.emsgSchemeIdUris = combineArrays(
|
|
output.emsgSchemeIdUris, input.emsgSchemeIdUris);
|
|
}
|
|
|
|
const combineSets = (a, b) => new Set([...a, ...b]);
|
|
output.keyIds = combineSets(output.keyIds, input.keyIds);
|
|
|
|
if (output.originalId == null) {
|
|
output.originalId = input.originalId;
|
|
} else {
|
|
output.originalId += ',' + (input.originalId || '');
|
|
}
|
|
|
|
const commonDrmInfos = shaka.media.DrmEngine.getCommonDrmInfos(
|
|
output.drmInfos, input.drmInfos);
|
|
if (input.drmInfos.length && output.drmInfos.length &&
|
|
!commonDrmInfos.length) {
|
|
throw new shaka.util.Error(
|
|
shaka.util.Error.Severity.CRITICAL,
|
|
shaka.util.Error.Category.MANIFEST,
|
|
shaka.util.Error.Code.INCONSISTENT_DRM_ACROSS_PERIODS);
|
|
}
|
|
output.drmInfos = commonDrmInfos;
|
|
|
|
// The output is encrypted if any input was encrypted.
|
|
output.encrypted = output.encrypted && input.encrypted;
|
|
|
|
// Take the max bandwidth, resolution, frame rate, sample rate, and channel
|
|
// count.
|
|
output.bandwidth = Math.max(output.bandwidth || 0, input.bandwidth || 0);
|
|
if (output.width) {
|
|
output.width = Math.max(output.width, input.width || 0);
|
|
}
|
|
if (output.height) {
|
|
output.height = Math.max(output.height, input.height || 0);
|
|
}
|
|
if (output.frameRate) {
|
|
output.frameRate = Math.max(output.frameRate, input.frameRate || 0);
|
|
}
|
|
if (output.audioSamplingRate) {
|
|
output.audioSamplingRate = Math.max(
|
|
output.audioSamplingRate, input.audioSamplingRate || 0);
|
|
}
|
|
if (output.channelsCount) {
|
|
output.channelsCount = Math.max(
|
|
output.channelsCount, input.channelsCount || 0);
|
|
}
|
|
|
|
// Combine the closed captions maps.
|
|
if (input.closedCaptions) {
|
|
if (!output.closedCaptions) {
|
|
output.closedCaptions = new Map();
|
|
}
|
|
for (const [key, value] of input.closedCaptions) {
|
|
output.closedCaptions.set(key, value);
|
|
}
|
|
}
|
|
|
|
// Satisfy the compiler about the type.
|
|
goog.asserts.assert(
|
|
output.segmentIndex instanceof shaka.media.MetaSegmentIndex,
|
|
'Output streams should have a MetaSegmentIndex!');
|
|
// Satisfy the compiler that the input index has been created.
|
|
goog.asserts.assert(
|
|
input.segmentIndex,
|
|
'Input segment index should have been created by now!');
|
|
|
|
output.segmentIndex.appendSegmentIndex(input.segmentIndex);
|
|
|
|
// Combine trick-play video streams, if present.
|
|
if (input.trickModeVideo) {
|
|
if (!output.trickModeVideo) {
|
|
// Create a fresh output stream for trick-mode playback.
|
|
output.trickModeVideo = shaka.util.PeriodCombiner.cloneStream_(
|
|
input.trickModeVideo);
|
|
// Start it with whatever non-trick-mode Streams are in the output so
|
|
// far.
|
|
output.trickModeVideo.segmentIndex = output.segmentIndex.clone();
|
|
}
|
|
|
|
// Concatenate the trick mode input onto the trick mode output.
|
|
shaka.util.PeriodCombiner.concatenateStreams_(
|
|
output.trickModeVideo, input.trickModeVideo);
|
|
} else if (output.trickModeVideo) {
|
|
// We have a trick mode output, but no input from this Period. Fill it in
|
|
// from the standard input Stream.
|
|
shaka.util.PeriodCombiner.concatenateStreams_(
|
|
output.trickModeVideo, input);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Combine the various fields of the input StreamDB into the output.
|
|
*
|
|
* @param {shaka.extern.StreamDB} output
|
|
* @param {shaka.extern.StreamDB} input
|
|
* @private
|
|
*/
|
|
static concatenateStreamDBs_(output, input) {
|
|
// Combine arrays, keeping only the unique elements
|
|
const combineArrays = (a, b) => Array.from(new Set(a.concat(b)));
|
|
output.roles = combineArrays(output.roles, input.roles);
|
|
|
|
const combineSets = (a, b) => new Set([...a, ...b]);
|
|
output.keyIds = combineSets(output.keyIds, input.keyIds);
|
|
|
|
// The output is encrypted if any input was encrypted.
|
|
output.encrypted = output.encrypted && input.encrypted;
|
|
|
|
// Concatenate segments without de-duping.
|
|
output.segments.push(...input.segments);
|
|
|
|
// Combine the closed captions maps.
|
|
if (input.closedCaptions) {
|
|
if (!output.closedCaptions) {
|
|
output.closedCaptions = new Map();
|
|
}
|
|
for (const [key, value] of input.closedCaptions) {
|
|
output.closedCaptions.set(key, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds streams in all periods which match the output stream.
|
|
*
|
|
* @param {!Array.<!Array.<T>>} streamsPerPeriod
|
|
* @param {T} outputStream
|
|
* @return {Array.<T>}
|
|
*
|
|
* @template T
|
|
* Accepts either a StreamDB or Stream type.
|
|
*
|
|
* @private
|
|
*/
|
|
static findMatchesInAllPeriods_(streamsPerPeriod, outputStream) {
|
|
const matches = [];
|
|
for (const streams of streamsPerPeriod) {
|
|
const match = shaka.util.PeriodCombiner.findBestMatchInPeriod_(
|
|
streams, outputStream);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
matches.push(match);
|
|
}
|
|
return matches;
|
|
}
|
|
|
|
/**
|
|
* Find the best match for the output stream.
|
|
*
|
|
* @param {!Array.<T>} streams
|
|
* @param {T} outputStream
|
|
* @return {?T} Returns null if no match can be found.
|
|
*
|
|
* @template T
|
|
* Accepts either a StreamDB or Stream type.
|
|
*
|
|
* @private
|
|
*/
|
|
static findBestMatchInPeriod_(streams, outputStream) {
|
|
const areCompatible = {
|
|
'audio': shaka.util.PeriodCombiner.areAVStreamsCompatible_,
|
|
'video': shaka.util.PeriodCombiner.areAVStreamsCompatible_,
|
|
'text': shaka.util.PeriodCombiner.areTextStreamsCompatible_,
|
|
}[outputStream.type];
|
|
|
|
const isBetterMatch = {
|
|
'audio': shaka.util.PeriodCombiner.isAudioStreamBetterMatch_,
|
|
'video': shaka.util.PeriodCombiner.isVideoStreamBetterMatch_,
|
|
'text': shaka.util.PeriodCombiner.isTextStreamBetterMatch_,
|
|
}[outputStream.type];
|
|
|
|
let best = null;
|
|
|
|
for (const stream of streams) {
|
|
if (!areCompatible(outputStream, stream)) {
|
|
continue;
|
|
}
|
|
|
|
if (!best || isBetterMatch(outputStream, best, stream)) {
|
|
best = stream;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
/**
|
|
* @param {T} outputStream An audio or video output stream
|
|
* @param {T} candidate A candidate stream to be combined with the output
|
|
* @return {boolean} True if the candidate could be combined with the
|
|
* output stream
|
|
*
|
|
* @template T
|
|
* Accepts either a StreamDB or Stream type.
|
|
*
|
|
* @private
|
|
*/
|
|
static areAVStreamsCompatible_(outputStream, candidate) {
|
|
const getCodecBase = (codecs) => shaka.util.MimeUtils.getCodecBase(codecs);
|
|
// Check MIME type and codecs, which should always be the same.
|
|
if (candidate.mimeType != outputStream.mimeType ||
|
|
getCodecBase(candidate.codecs) != getCodecBase(outputStream.codecs)) {
|
|
return false;
|
|
}
|
|
|
|
// This field is only available on Stream, not StreamDB.
|
|
if (outputStream.drmInfos) {
|
|
// Check for compatible DRM systems. Note that clear streams are
|
|
// implicitly compatible with any DRM and with each other.
|
|
if (!shaka.media.DrmEngine.areDrmCompatible(outputStream.drmInfos,
|
|
candidate.drmInfos)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {T} outputStream A text output stream
|
|
* @param {T} candidate A candidate stream to be combined with the output
|
|
* @return {boolean} True if the candidate could be combined with the
|
|
* output
|
|
*
|
|
* @template T
|
|
* Accepts either a StreamDB or Stream type.
|
|
*
|
|
* @private
|
|
*/
|
|
static areTextStreamsCompatible_(outputStream, candidate) {
|
|
const LanguageUtils = shaka.util.LanguageUtils;
|
|
|
|
// For text, we don't care about MIME type or codec. We can always switch
|
|
// between text types.
|
|
|
|
// The output stream should not be a dummy stream inserted to fill a period
|
|
// gap. So reject any candidate if the output has no language. This would
|
|
// cause findMatchesInAllPeriods_ to return null and this output stream to
|
|
// be skipped (meaning no output streams based on it).
|
|
if (!outputStream.language) {
|
|
return false;
|
|
}
|
|
|
|
// If the candidate is a dummy, then it is compatible, and we could use it
|
|
// if nothing else matches.
|
|
if (!candidate.language) {
|
|
return true;
|
|
}
|
|
|
|
const languageRelatedness = LanguageUtils.relatedness(
|
|
outputStream.language, candidate.language);
|
|
|
|
// We will strictly avoid combining text across languages or "kinds"
|
|
// (caption vs subtitle).
|
|
if (languageRelatedness == 0 ||
|
|
candidate.kind != outputStream.kind) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {T} outputStream An audio output stream
|
|
* @param {T} best The best match so far for this period
|
|
* @param {T} candidate A candidate stream which might be better
|
|
* @return {boolean} True if the candidate is a better match
|
|
*
|
|
* @template T
|
|
* Accepts either a StreamDB or Stream type.
|
|
*
|
|
* @private
|
|
*/
|
|
static isAudioStreamBetterMatch_(outputStream, best, candidate) {
|
|
const LanguageUtils = shaka.util.LanguageUtils;
|
|
const {BETTER, EQUAL, WORSE} = shaka.util.PeriodCombiner.BetterOrWorse_;
|
|
|
|
// If the output stream was based on the candidate stream, the candidate
|
|
// stream should be considered a better match. We can check this by
|
|
// comparing their ids.
|
|
if (outputStream.id == candidate.id) {
|
|
return true;
|
|
}
|
|
|
|
// Otherwise, compare the streams' characteristics to detecrmine the best
|
|
// match.
|
|
|
|
// The most important thing is language. In some cases, we will accept a
|
|
// different language across periods when we must.
|
|
const bestRelatedness = LanguageUtils.relatedness(
|
|
outputStream.language, best.language);
|
|
const candidateRelatedness = LanguageUtils.relatedness(
|
|
outputStream.language, candidate.language);
|
|
|
|
if (candidateRelatedness > bestRelatedness) {
|
|
return true;
|
|
}
|
|
if (candidateRelatedness < bestRelatedness) {
|
|
return false;
|
|
}
|
|
|
|
// If the language doesn't match, but the candidate is the "primary"
|
|
// language, then that should be preferred as a fallback.
|
|
if (!best.primary && candidate.primary) {
|
|
return true;
|
|
}
|
|
if (best.primary && !candidate.primary) {
|
|
return false;
|
|
}
|
|
|
|
// If language-based differences haven't decided this, look at roles. If
|
|
// the candidate has more roles in common with the output, upgrade to the
|
|
// candidate.
|
|
if (outputStream.roles.length) {
|
|
const bestRoleMatches =
|
|
best.roles.filter((role) => outputStream.roles.includes(role));
|
|
const candidateRoleMatches =
|
|
candidate.roles.filter((role) => outputStream.roles.includes(role));
|
|
if (candidateRoleMatches.length > bestRoleMatches.length) {
|
|
return true;
|
|
} else if (candidateRoleMatches.length < bestRoleMatches.length) {
|
|
return false;
|
|
} else {
|
|
// Both streams have the same role overlap with the outputStream
|
|
// If this is the case, choose the stream with the fewer roles overall.
|
|
// Streams that match best together tend to be streams with the same
|
|
// roles, e g stream1 with roles [r1, r2] is likely a better match
|
|
// for stream2 with roles [r1, r2] vs stream3 with roles
|
|
// [r1, r2, r3, r4].
|
|
// If we match stream1 with stream3 due to the same role overlap,
|
|
// stream2 is likely to be left unmatched and error out later.
|
|
// See https://github.com/google/shaka-player/issues/2542 for
|
|
// more details.
|
|
return candidate.roles.length < best.roles.length;
|
|
}
|
|
}
|
|
|
|
// If language-based and role-based features are equivalent, take the audio
|
|
// with the closes channel count to the output.
|
|
const channelsBetterOrWorse =
|
|
shaka.util.PeriodCombiner.compareClosestPreferLower_(
|
|
outputStream.channelsCount,
|
|
best.channelsCount,
|
|
candidate.channelsCount);
|
|
if (channelsBetterOrWorse == BETTER) {
|
|
return true;
|
|
} else if (channelsBetterOrWorse == WORSE) {
|
|
return false;
|
|
}
|
|
|
|
// If channels are equal, take the closest sample rate to the output.
|
|
const sampleRateBetterOrWorse =
|
|
shaka.util.PeriodCombiner.compareClosestPreferLower_(
|
|
outputStream.audioSamplingRate,
|
|
best.audioSamplingRate,
|
|
candidate.audioSamplingRate);
|
|
if (sampleRateBetterOrWorse == BETTER) {
|
|
return true;
|
|
} else if (sampleRateBetterOrWorse == WORSE) {
|
|
return false;
|
|
}
|
|
|
|
if (outputStream.bandwidth) {
|
|
// Take the audio with the closest bandwidth to the output.
|
|
const bandwidthBetterOrWorse =
|
|
shaka.util.PeriodCombiner.compareClosestPreferMinimalAbsDiff_(
|
|
outputStream.bandwidth,
|
|
best.bandwidth,
|
|
candidate.bandwidth);
|
|
if (bandwidthBetterOrWorse == BETTER) {
|
|
return true;
|
|
} else if (bandwidthBetterOrWorse == WORSE) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param {T} outputStream A video output stream
|
|
* @param {T} best The best match so far for this period
|
|
* @param {T} candidate A candidate stream which might be better
|
|
* @return {boolean} True if the candidate is a better match
|
|
*
|
|
* @template T
|
|
* Accepts either a StreamDB or Stream type.
|
|
*
|
|
* @private
|
|
*/
|
|
static isVideoStreamBetterMatch_(outputStream, best, candidate) {
|
|
const {BETTER, EQUAL, WORSE} = shaka.util.PeriodCombiner.BetterOrWorse_;
|
|
|
|
// If the output stream was based on the candidate stream, the candidate
|
|
// stream should be considered a better match. We can check this by
|
|
// comparing their ids.
|
|
if (outputStream.id == candidate.id) {
|
|
return true;
|
|
}
|
|
|
|
// Otherwise, compare the streams' characteristics to detecrmine the best
|
|
// match.
|
|
|
|
// Take the video with the closest resolution to the output.
|
|
const resolutionBetterOrWorse =
|
|
shaka.util.PeriodCombiner.compareClosestPreferLower_(
|
|
outputStream.width * outputStream.height,
|
|
best.width * best.height,
|
|
candidate.width * candidate.height);
|
|
if (resolutionBetterOrWorse == BETTER) {
|
|
return true;
|
|
} else if (resolutionBetterOrWorse == WORSE) {
|
|
return false;
|
|
}
|
|
|
|
// We may not know the frame rate for the content, in which case this gets
|
|
// skipped.
|
|
if (outputStream.frameRate) {
|
|
// Take the video with the closest frame rate to the output.
|
|
const frameRateBetterOrWorse =
|
|
shaka.util.PeriodCombiner.compareClosestPreferLower_(
|
|
outputStream.frameRate,
|
|
best.frameRate,
|
|
candidate.frameRate);
|
|
if (frameRateBetterOrWorse == BETTER) {
|
|
return true;
|
|
} else if (frameRateBetterOrWorse == WORSE) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
if (outputStream.bandwidth) {
|
|
// Take the video with the closest bandwidth to the output.
|
|
const bandwidthBetterOrWorse =
|
|
shaka.util.PeriodCombiner.compareClosestPreferMinimalAbsDiff_(
|
|
outputStream.bandwidth,
|
|
best.bandwidth,
|
|
candidate.bandwidth);
|
|
if (bandwidthBetterOrWorse == BETTER) {
|
|
return true;
|
|
} else if (bandwidthBetterOrWorse == WORSE) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param {T} outputStream A text output stream
|
|
* @param {T} best The best match so far for this period
|
|
* @param {T} candidate A candidate stream which might be better
|
|
* @return {boolean} True if the candidate is a better match
|
|
*
|
|
* @template T
|
|
* Accepts either a StreamDB or Stream type.
|
|
*
|
|
* @private
|
|
*/
|
|
static isTextStreamBetterMatch_(outputStream, best, candidate) {
|
|
const LanguageUtils = shaka.util.LanguageUtils;
|
|
|
|
// If the output stream was based on the candidate stream, the candidate
|
|
// stream should be considered a better match. We can check this by
|
|
// comparing their ids.
|
|
if (outputStream.id == candidate.id) {
|
|
return true;
|
|
}
|
|
|
|
// Otherwise, compare the streams' characteristics to detecrmine the best
|
|
// match.
|
|
|
|
// The most important thing is language. In some cases, we will accept a
|
|
// different language across periods when we must.
|
|
const bestRelatedness = LanguageUtils.relatedness(
|
|
outputStream.language, best.language);
|
|
const candidateRelatedness = LanguageUtils.relatedness(
|
|
outputStream.language, candidate.language);
|
|
|
|
if (candidateRelatedness > bestRelatedness) {
|
|
return true;
|
|
}
|
|
if (candidateRelatedness < bestRelatedness) {
|
|
return false;
|
|
}
|
|
|
|
// If the language doesn't match, but the candidate is the "primary"
|
|
// language, then that should be preferred as a fallback.
|
|
if (!best.primary && candidate.primary) {
|
|
return true;
|
|
}
|
|
if (best.primary && !candidate.primary) {
|
|
return false;
|
|
}
|
|
|
|
// If the candidate has more roles in common with the output, upgrade to the
|
|
// candidate.
|
|
if (outputStream.roles.length) {
|
|
const bestRoleMatches =
|
|
best.roles.filter((role) => outputStream.roles.includes(role));
|
|
const candidateRoleMatches =
|
|
candidate.roles.filter((role) => outputStream.roles.includes(role));
|
|
if (candidateRoleMatches.length > bestRoleMatches.length) {
|
|
return true;
|
|
}
|
|
if (candidateRoleMatches.length < bestRoleMatches.length) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// If the candidate has the same MIME type and codec, upgrade to the
|
|
// candidate. It's not required that text streams use the same format
|
|
// across periods, but it's a helpful signal. Some content in our demo app
|
|
// contains the same languages repeated with two different text formats in
|
|
// each period. This condition ensures that all text streams are used.
|
|
// Otherwise, we wind up with some one stream of each language left unused,
|
|
// triggering a failure.
|
|
if (candidate.mimeType == outputStream.mimeType &&
|
|
candidate.codecs == outputStream.codecs &&
|
|
(best.mimeType != outputStream.mimeType ||
|
|
best.codecs != outputStream.codecs)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Create a dummy text StreamDB to fill in periods with no text, to avoid
|
|
* failing the general flattening algorithm.
|
|
*
|
|
* @return {shaka.extern.StreamDB}
|
|
* @private
|
|
*/
|
|
static dummyTextStreamDB_() {
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
|
|
return {
|
|
id: 0,
|
|
originalId: '',
|
|
primary: false,
|
|
type: ContentType.TEXT,
|
|
mimeType: '',
|
|
codecs: '',
|
|
language: '',
|
|
label: null,
|
|
width: null,
|
|
height: null,
|
|
encrypted: false,
|
|
keyIds: new Set(),
|
|
segments: [],
|
|
variantIds: [],
|
|
roles: [],
|
|
channelsCount: null,
|
|
audioSamplingRate: null,
|
|
closedCaptions: null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a dummy text Stream to fill in periods with no text, to avoid
|
|
* failing the general flattening algorithm.
|
|
*
|
|
* @return {shaka.extern.Stream}
|
|
* @private
|
|
*/
|
|
static dummyTextStream_() {
|
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
|
|
return {
|
|
id: 0,
|
|
originalId: '',
|
|
createSegmentIndex: () => Promise.resolve(),
|
|
segmentIndex: new shaka.media.SegmentIndex([]),
|
|
mimeType: '',
|
|
codecs: '',
|
|
encrypted: false,
|
|
drmInfos: [],
|
|
keyIds: new Set(),
|
|
language: '',
|
|
label: null,
|
|
type: ContentType.TEXT,
|
|
primary: false,
|
|
trickModeVideo: null,
|
|
emsgSchemeIdUris: null,
|
|
roles: [],
|
|
channelsCount: null,
|
|
audioSamplingRate: null,
|
|
closedCaptions: null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Compare the best value so far with the candidate value and the output
|
|
* value. Decide if the candidate is better, equal, or worse than the best
|
|
* so far. Any value less than or equal to the output is preferred over a
|
|
* larger value, and closer to the output is better than farther.
|
|
*
|
|
* This provides us a generic way to choose things that should match as
|
|
* closely as possible, like resolution, frame rate, audio channels, or
|
|
* sample rate. If we have to go higher to make a match, we will. But if
|
|
* the user selects 480p, for example, we don't want to surprise them with
|
|
* 720p and waste bandwidth if there's another choice available to us.
|
|
*
|
|
* @param {number} outputValue
|
|
* @param {number} bestValue
|
|
* @param {number} candidateValue
|
|
* @return {shaka.util.PeriodCombiner.BetterOrWorse_}
|
|
* @private
|
|
*/
|
|
static compareClosestPreferLower_(outputValue, bestValue, candidateValue) {
|
|
const {BETTER, EQUAL, WORSE} = shaka.util.PeriodCombiner.BetterOrWorse_;
|
|
|
|
if (bestValue > outputValue) {
|
|
if (candidateValue <= outputValue) {
|
|
// Any smaller-or-equal-to-output value is preferable to a
|
|
// bigger-than-output value.
|
|
return BETTER;
|
|
}
|
|
|
|
// Both "best" and "candidate" are greater than the output. Take
|
|
// whichever is closer.
|
|
if (candidateValue - outputValue < bestValue - outputValue) {
|
|
return BETTER;
|
|
}
|
|
} else {
|
|
// The "best" so far is less than or equal to the output. If the
|
|
// candidate is bigger than the output, we don't want it.
|
|
if (candidateValue > outputValue) {
|
|
return WORSE;
|
|
}
|
|
|
|
// Both "best" and "candidate" are less than or equal to the output.
|
|
// Take whichever is closer.
|
|
if (outputValue - candidateValue < outputValue - bestValue) {
|
|
return BETTER;
|
|
}
|
|
}
|
|
|
|
return EQUAL;
|
|
}
|
|
|
|
/*
|
|
* @param {number} outputValue
|
|
* @param {number} bestValue
|
|
* @param {number} candidateValue
|
|
* @return {shaka.util.PeriodCombiner.BetterOrWorse_}
|
|
* @private
|
|
*/
|
|
static compareClosestPreferMinimalAbsDiff_(
|
|
outputValue, bestValue, candidateValue) {
|
|
const {BETTER, EQUAL, WORSE} = shaka.util.PeriodCombiner.BetterOrWorse_;
|
|
|
|
const absDiffBest = Math.abs(outputValue - bestValue);
|
|
const absDiffCandidate = Math.abs(outputValue - candidateValue);
|
|
if (absDiffCandidate < absDiffBest) {
|
|
return BETTER;
|
|
} else if (absDiffBest < absDiffCandidate) {
|
|
return WORSE;
|
|
}
|
|
|
|
return EQUAL;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @typedef {{
|
|
* id: string,
|
|
* audioStreams: !Array.<shaka.extern.Stream>,
|
|
* videoStreams: !Array.<shaka.extern.Stream>,
|
|
* textStreams: !Array.<shaka.extern.Stream>
|
|
* }}
|
|
*
|
|
* @description Contains the streams from one DASH period.
|
|
*
|
|
* @property {string} id
|
|
* The Period ID.
|
|
* @property {!Array.<shaka.extern.Stream>} audioStreams
|
|
* The audio streams from one Period.
|
|
* @property {!Array.<shaka.extern.Stream>} videoStreams
|
|
* The video streams from one Period.
|
|
* @property {!Array.<shaka.extern.Stream>} textStreams
|
|
* The text streams from one Period.
|
|
*/
|
|
shaka.util.PeriodCombiner.Period;
|
|
|
|
/**
|
|
* @enum {number}
|
|
* @private
|
|
*/
|
|
shaka.util.PeriodCombiner.BetterOrWorse_ = {
|
|
BETTER: 1,
|
|
EQUAL: 0,
|
|
WORSE: -1,
|
|
};
|