Skip to content

Commit

Permalink
perf(DASH): PeriodCombiner optimisations (#5837)
Browse files Browse the repository at this point in the history
- reduce number of iterations during filtering out stream duplicates
- reduce number of iterations when extending output stream
- for audio or video only content, reuse existing stream array instead
of copying it
  • Loading branch information
tykus160 authored Oct 31, 2023
1 parent 577d141 commit ade93b0
Showing 1 changed file with 87 additions and 144 deletions.
231 changes: 87 additions & 144 deletions lib/util/periods.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,7 @@ shaka.util.PeriodCombiner = class {
async combinePeriods(periods, isDynamic) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;

shaka.util.PeriodCombiner.filterOutAudioStreamDuplicates_(periods);
shaka.util.PeriodCombiner.filterOutVideoStreamDuplicates_(periods);
shaka.util.PeriodCombiner.filterOutTextStreamDuplicates_(periods);
shaka.util.PeriodCombiner.filterOutImageStreamDuplicates_(periods);
shaka.util.PeriodCombiner.filterOutDuplicates_(periods);

// Optimization: for single-period VOD, do nothing. This makes sure
// single-period DASH content will be 100% accurately represented in the
Expand Down Expand Up @@ -237,7 +234,8 @@ shaka.util.PeriodCombiner = class {
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_);
const streams = this.videoStreams_.length ? this.videoStreams_ :
this.audioStreams_;
for (const stream of streams) {
const id = nextVariantId++;
variants.push({
Expand Down Expand Up @@ -290,141 +288,88 @@ shaka.util.PeriodCombiner = class {
}

/**
* @param {!Array.<shaka.extern.Period>} periods
* @private
*/
static filterOutAudioStreamDuplicates_(periods) {
const ArrayUtils = shaka.util.ArrayUtils;
// Two audio streams are considered to be duplicates of
// one another if their ids are different, but all the other
// information is the same.
for (const period of periods) {
const filteredAudios = [];
for (const a1 of period.audioStreams) {
let duplicate = false;
for (const a2 of filteredAudios) {
if (a1.id != a2.id &&
a1.channelsCount == a2.channelsCount &&
a1.language == a2.language &&
a1.bandwidth == a2.bandwidth &&
a1.label == a2.label &&
a1.codecs == a2.codecs &&
a1.mimeType == a2.mimeType &&
ArrayUtils.hasSameElements(a1.roles, a2.roles) &&
a1.audioSamplingRate == a2.audioSamplingRate &&
a1.primary == a2.primary) {
duplicate = true;
}
}

if (!duplicate) {
filteredAudios.push(a1);
}
}

period.audioStreams = filteredAudios;
}
}

/**
* @param {!Array.<shaka.extern.Period>} periods
* @param {!Array<shaka.extern.Period>} periods
* @private
*/
static filterOutTextStreamDuplicates_(periods) {
const ArrayUtils = shaka.util.ArrayUtils;
// Two text streams are considered to be duplicates of
// one another if their ids are different, but all the other
// information is the same.
for (const period of periods) {
const filteredTexts = [];
for (const t1 of period.textStreams) {
let duplicate = false;
for (const t2 of filteredTexts) {
if (t1.id != t2.id &&
t1.language == t2.language &&
t1.label == t2.label &&
t1.codecs == t2.codecs &&
t1.mimeType == t2.mimeType &&
t1.bandwidth == t2.bandwidth &&
ArrayUtils.hasSameElements(t1.roles, t2.roles)) {
duplicate = true;
}
}

if (!duplicate) {
filteredTexts.push(t1);
}
}

period.textStreams = filteredTexts;
}
}

/**
* @param {!Array.<shaka.extern.Period>} periods
* @private
*/
static filterOutVideoStreamDuplicates_(periods) {
static filterOutDuplicates_(periods) {
const PeriodCombiner = shaka.util.PeriodCombiner;
const ArrayUtils = shaka.util.ArrayUtils;
const MapUtils = shaka.util.MapUtils;
// Two video streams are considered to be duplicates of
// one another if their ids are different, but all the other
// information is the same.

for (const period of periods) {
const filteredVideos = [];
for (const v1 of period.videoStreams) {
let duplicate = false;
for (const v2 of filteredVideos) {
if (v1.id != v2.id &&
v1.width == v2.width &&
v1.frameRate == v2.frameRate &&
v1.codecs == v2.codecs &&
v1.mimeType == v2.mimeType &&
v1.label == v2.label &&
// Two video streams are considered to be duplicates of
// one another if their ids are different, but all the other
// information is the same.
period.videoStreams = PeriodCombiner.filterOutStreamDuplicates_(
period.videoStreams, (v1, v2) => {
return v1.id !== v2.id &&
v1.width === v2.width &&
v1.frameRate === v2.frameRate &&
v1.codecs === v2.codecs &&
v1.mimeType === v2.mimeType &&
v1.label === v2.label &&
ArrayUtils.hasSameElements(v1.roles, v2.roles) &&
MapUtils.hasSameElements(v1.closedCaptions, v2.closedCaptions) &&
v1.bandwidth == v2.bandwidth) {
duplicate = true;
}
}

if (!duplicate) {
filteredVideos.push(v1);
}
}

period.videoStreams = filteredVideos;
v1.bandwidth === v2.bandwidth;
});
// Two audio streams are considered to be duplicates of
// one another if their ids are different, but all the other
// information is the same.
period.audioStreams = PeriodCombiner.filterOutStreamDuplicates_(
period.audioStreams, (a1, a2) => {
return a1.id !== a2.id &&
a1.channelsCount === a2.channelsCount &&
a1.language === a2.language &&
a1.bandwidth === a2.bandwidth &&
a1.label === a2.label &&
a1.codecs === a2.codecs &&
a1.mimeType === a2.mimeType &&
ArrayUtils.hasSameElements(a1.roles, a2.roles) &&
a1.audioSamplingRate === a2.audioSamplingRate &&
a1.primary === a2.primary;
});
// Two text streams are considered to be duplicates of
// one another if their ids are different, but all the other
// information is the same.
period.textStreams = PeriodCombiner.filterOutStreamDuplicates_(
period.textStreams, (t1, t2) => {
return t1.id !== t2.id &&
t1.language === t2.language &&
t1.label === t2.label &&
t1.codecs === t2.codecs &&
t1.mimeType === t2.mimeType &&
t1.bandwidth === t2.bandwidth &&
ArrayUtils.hasSameElements(t1.roles, t2.roles);
});
// Two image streams are considered to be duplicates of
// one another if their ids are different, but all the other
// information is the same.
period.imageStreams = PeriodCombiner.filterOutStreamDuplicates_(
period.imageStreams, (i1, i2) => {
return i1.id !== i2.id &&
i1.width === i2.width &&
i1.codecs === i2.codecs &&
i1.mimeType === i2.mimeType;
});
}
}

/**
* @param {!Array.<shaka.extern.Period>} periods
* @param {!Array<shaka.extern.Stream>} streams
* @param {function(
* shaka.extern.Stream, shaka.extern.Stream): boolean} isDuplicate
* @return {!Array<shaka.extern.Stream>}
* @private
*/
static filterOutImageStreamDuplicates_(periods) {
// Two image streams are considered to be duplicates of
// one another if their ids are different, but all the other
// information is the same.
for (const period of periods) {
const filteredImages = [];
for (const i1 of period.imageStreams) {
let duplicate = false;
for (const i2 of filteredImages) {
if (i1.id != i2.id &&
i1.width == i2.width &&
i1.codecs == i2.codecs &&
i1.mimeType == i2.mimeType) {
duplicate = true;
}
}

if (!duplicate) {
filteredImages.push(i1);
}
static filterOutStreamDuplicates_(streams, isDuplicate) {
const filteredStreams = [];
for (const s1 of streams) {
const duplicate = filteredStreams.some((s2) => isDuplicate(s1, s2));
if (!duplicate) {
filteredStreams.push(s1);
}

period.imageStreams = filteredImages;
}
return filteredStreams;
}

/**
Expand Down Expand Up @@ -812,27 +757,25 @@ shaka.util.PeriodCombiner = class {

// Concatenate the new matches onto the stream, starting at the first new
// period.
for (let i = 0; i < matches.length; i++) {
if (i >= firstNewPeriodIndex) {
const match = matches[i];
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;
}
for (let i = firstNewPeriodIndex; i < matches.length; i++) {
const match = matches[i];
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);
}
if (used) {
unusedStreamsPerPeriod[i].delete(match);
}
}
}
Expand Down

0 comments on commit ade93b0

Please sign in to comment.