From 447e9f9c7aa972c4710cb97961d7669d383bdd7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Fri, 8 Dec 2023 12:42:52 +0100 Subject: [PATCH 1/7] reduce spare collections --- lib/util/periods.js | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/util/periods.js b/lib/util/periods.js index 71ae09c724..f15843542c 100644 --- a/lib/util/periods.js +++ b/lib/util/periods.js @@ -826,6 +826,8 @@ shaka.util.PeriodCombiner = class { } }; + // Clone roles array so this output stream can own it. + clone.roles = clone.roles.slice(); clone.segmentIndex = null; clone.emsgSchemeIdUris = []; clone.keyIds = new Set(); @@ -871,7 +873,17 @@ shaka.util.PeriodCombiner = class { // the line. // Combine arrays, keeping only the unique elements - const combineArrays = (a, b) => Array.from(new Set(a.concat(b))); + const combineArrays = (output, input) => { + if (!output) { + output = []; + } + for (const item of input) { + if (!output.includes(item)) { + output.push(item); + } + } + return output; + }; output.roles = combineArrays(output.roles, input.roles); if (input.emsgSchemeIdUris) { @@ -879,8 +891,9 @@ shaka.util.PeriodCombiner = class { output.emsgSchemeIdUris, input.emsgSchemeIdUris); } - const combineSets = (a, b) => new Set([...a, ...b]); - output.keyIds = combineSets(output.keyIds, input.keyIds); + for (const keyId of input.keyIds) { + output.keyIds.add(keyId); + } if (output.originalId == null) { output.originalId = input.originalId; @@ -952,11 +965,22 @@ shaka.util.PeriodCombiner = class { */ static concatenateStreamDBs_(output, input) { // Combine arrays, keeping only the unique elements - const combineArrays = (a, b) => Array.from(new Set(a.concat(b))); + const combineArrays = (output, input) => { + if (!output) { + output = []; + } + for (const item of input) { + if (!output.includes(item)) { + output.push(item); + } + } + return output; + }; output.roles = combineArrays(output.roles, input.roles); - const combineSets = (a, b) => new Set([...a, ...b]); - output.keyIds = combineSets(output.keyIds, input.keyIds); + for (const keyId of input.keyIds) { + output.keyIds.add(keyId); + } // The output is encrypted if any input was encrypted. output.encrypted = output.encrypted && input.encrypted; From ff85e6eb20f73fdfb15db82765f9f21d2bca5b18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Fri, 8 Dec 2023 12:58:03 +0100 Subject: [PATCH 2/7] check for dummy output stream quickly --- lib/util/periods.js | 47 ++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/lib/util/periods.js b/lib/util/periods.js index f15843542c..c4ed0d9fca 100644 --- a/lib/util/periods.js +++ b/lib/util/periods.js @@ -506,8 +506,6 @@ shaka.util.PeriodCombiner = class { */ async combine_( outputStreams, streamsPerPeriod, firstNewPeriodIndex, clone, concat) { - const ContentType = shaka.util.ManifestParserUtils.ContentType; - const unusedStreamsPerPeriod = []; for (let i = 0; i < streamsPerPeriod.length; i++) { if (i >= firstNewPeriodIndex) { @@ -556,10 +554,7 @@ shaka.util.PeriodCombiner = class { for (const unusedStreams of unusedStreamsPerPeriod) { for (const stream of unusedStreams) { - const isDummyText = stream.type == ContentType.TEXT && !stream.language; - const isDummyImage = stream.type == ContentType.IMAGE && - !stream.tilesLayout; - if (isDummyText || isDummyImage) { + if (shaka.util.PeriodCombiner.isDummy_(stream)) { // This is one of our dummy streams, so ignore it. We may not use // them all, and that's fine. continue; @@ -707,6 +702,11 @@ shaka.util.PeriodCombiner = class { */ createNewOutputStream_( stream, streamsPerPeriod, clone, concat, unusedStreamsPerPeriod) { + // Check do we want to create output stream from dummy stream + // and if so, return quickly. + if (shaka.util.PeriodCombiner.isDummy_(stream)) { + return null; + } // Start by cloning the stream without segments, key IDs, etc. const outputStream = clone(stream); @@ -1152,14 +1152,6 @@ shaka.util.PeriodCombiner = class { // 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) { @@ -1194,14 +1186,6 @@ shaka.util.PeriodCombiner = class { // For image, we don't care about MIME type. We can always switch // between image 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 tilesLayout. This - // would cause findMatchesInAllPeriods_ to return null and this output - // stream to be skipped (meaning no output streams based on it). - if (!outputStream.tilesLayout) { - return false; - } - return true; } @@ -1709,6 +1693,25 @@ shaka.util.PeriodCombiner = class { return EQUAL; } + + /** + * @param {T} stream + * @return {boolean} + * @template T + * Accepts either a StreamDB or Stream type. + * @private + */ + static isDummy_(stream) { + const ContentType = shaka.util.ManifestParserUtils.ContentType; + switch (stream.type) { + case ContentType.TEXT: + return !stream.language; + case ContentType.IMAGE: + return !stream.tilesLayout; + default: + return false; + } + } }; /** From 3ce95152d0acbc8d505622cd9983868cfeb54a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Fri, 8 Dec 2023 13:01:22 +0100 Subject: [PATCH 3/7] remove conditional logic when creating unusedStreams array --- lib/util/periods.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/util/periods.js b/lib/util/periods.js index c4ed0d9fca..5ff9eb972c 100644 --- a/lib/util/periods.js +++ b/lib/util/periods.js @@ -507,14 +507,13 @@ shaka.util.PeriodCombiner = class { async combine_( outputStreams, streamsPerPeriod, firstNewPeriodIndex, clone, concat) { const unusedStreamsPerPeriod = []; - for (let i = 0; i < streamsPerPeriod.length; i++) { - if (i >= firstNewPeriodIndex) { - // This periods streams are all new. - unusedStreamsPerPeriod.push(new Set(streamsPerPeriod[i])); - } else { - // This period's streams have all been used already. - unusedStreamsPerPeriod.push(new Set()); - } + for (let i = 0; i < firstNewPeriodIndex; i++) { + // This period's streams have all been used already. + unusedStreamsPerPeriod.push(new Set()); + } + for (let i = firstNewPeriodIndex; i < streamsPerPeriod.length; i++) { + // This periods streams are all new. + unusedStreamsPerPeriod.push(new Set(streamsPerPeriod[i])); } // First, extend all existing output Streams into the new periods. From abc7a42ec45db2e7265958eff57dbe5aa210f31e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Fri, 8 Dec 2023 13:07:14 +0100 Subject: [PATCH 4/7] move dummy streams creation to getStreamsPerPeriod_ --- lib/util/periods.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/util/periods.js b/lib/util/periods.js index 5ff9eb972c..9ccdd50348 100644 --- a/lib/util/periods.js +++ b/lib/util/periods.js @@ -118,6 +118,8 @@ shaka.util.PeriodCombiner = class { * @private */ getStreamsPerPeriod_(periods) { + const ContentType = shaka.util.ManifestParserUtils.ContentType; + const audioStreamsPerPeriod = []; const videoStreamsPerPeriod = []; const textStreamsPerPeriod = []; @@ -128,6 +130,16 @@ shaka.util.PeriodCombiner = class { videoStreamsPerPeriod.push(period.videoStreams); textStreamsPerPeriod.push(period.textStreams); imageStreamsPerPeriod.push(period.imageStreams); + + // It's okay to have a period with no text or images, but our algorithm + // fails on any period without matching streams. So we add dummy streams + // to each period. Since we combine text streams by language and image + // streams by resolution, we might need a dummy even in periods with these + // streams already. + period.textStreams.push(shaka.util.PeriodCombiner.dummyStream_( + ContentType.TEXT)); + period.imageStreams.push(shaka.util.PeriodCombiner.dummyStream_( + ContentType.IMAGE)); } return { audioStreamsPerPeriod, @@ -189,20 +201,6 @@ shaka.util.PeriodCombiner = class { imageStreamsPerPeriod, } = this.getStreamsPerPeriod_(periods); - // It's okay to have a period with no text or images, but our algorithm - // fails on any period without matching streams. So we add dummy streams - // to each period. Since we combine text streams by language and image - // streams by resolution, we might need a dummy even in periods with these - // streams already. - for (const textStreams of textStreamsPerPeriod) { - textStreams.push(shaka.util.PeriodCombiner.dummyStream_( - ContentType.TEXT)); - } - for (const imageStreams of imageStreamsPerPeriod) { - imageStreams.push(shaka.util.PeriodCombiner.dummyStream_( - ContentType.IMAGE)); - } - await Promise.all([ this.combine_( this.audioStreams_, From 4378742efb2241dceff20e77f37d403aeef67a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Fri, 8 Dec 2023 13:52:59 +0100 Subject: [PATCH 5/7] do not create matchedStreams array in DASH parser --- lib/dash/dash_parser.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 1acdcd4fdf..af5063a055 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -1610,7 +1610,6 @@ shaka.dash.DashParser = class { hdr, videoLayout: undefined, tilesLayout, - matchedStreams: [], accessibilityPurpose, external: false, fastSwitching: false, From 7448d3c90eca54abc64b660cf4cefdb2deb38557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Fri, 8 Dec 2023 14:29:57 +0100 Subject: [PATCH 6/7] use cache for faster matches lookup --- lib/util/periods.js | 368 +++++++++++++++++++------------------- test/util/periods_unit.js | 16 +- 2 files changed, 192 insertions(+), 192 deletions(-) diff --git a/lib/util/periods.js b/lib/util/periods.js index 9ccdd50348..4b0e63befb 100644 --- a/lib/util/periods.js +++ b/lib/util/periods.js @@ -11,12 +11,10 @@ goog.require('shaka.log'); goog.require('shaka.media.DrmEngine'); goog.require('shaka.media.MetaSegmentIndex'); goog.require('shaka.media.SegmentIndex'); -goog.require('shaka.util.ArrayUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.IReleasable'); goog.require('shaka.util.LanguageUtils'); goog.require('shaka.util.ManifestParserUtils'); -goog.require('shaka.util.MapUtils'); goog.require('shaka.util.MimeUtils'); /** @@ -107,18 +105,20 @@ shaka.util.PeriodCombiner = class { } /** - * Returns an object that contains arrays of streams by type - * @param {!Array.} periods - * @return {{ - * audioStreamsPerPeriod: !Array.>, - * videoStreamsPerPeriod: !Array.>, - * textStreamsPerPeriod: !Array.>, - * imageStreamsPerPeriod: !Array.> - * }} + * Returns an object that contains arrays of streams by type + * @param {!Array} periods + * @param {boolean} addDummy + * @return {{ + * audioStreamsPerPeriod: !Array>, + * videoStreamsPerPeriod: !Array>, + * textStreamsPerPeriod: !Array>, + * imageStreamsPerPeriod: !Array> + * }} * @private - */ - getStreamsPerPeriod_(periods) { + */ + getStreamsPerPeriod_(periods, addDummy) { const ContentType = shaka.util.ManifestParserUtils.ContentType; + const PeriodCombiner = shaka.util.PeriodCombiner; const audioStreamsPerPeriod = []; const videoStreamsPerPeriod = []; @@ -126,20 +126,31 @@ shaka.util.PeriodCombiner = class { const imageStreamsPerPeriod = []; for (const period of periods) { - audioStreamsPerPeriod.push(period.audioStreams); - videoStreamsPerPeriod.push(period.videoStreams); - textStreamsPerPeriod.push(period.textStreams); - imageStreamsPerPeriod.push(period.imageStreams); + const audioMap = new Map(period.audioStreams.map((s) => + [PeriodCombiner.generateAudioKey_(s), s])); + const videoMap = new Map(period.videoStreams.map((s) => + [PeriodCombiner.generateVideoKey_(s), s])); + const textMap = new Map(period.textStreams.map((s) => + [PeriodCombiner.generateTextKey_(s), s])); + const imageMap = new Map(period.imageStreams.map((s) => + [PeriodCombiner.generateImageKey_(s), s])); // It's okay to have a period with no text or images, but our algorithm // fails on any period without matching streams. So we add dummy streams // to each period. Since we combine text streams by language and image // streams by resolution, we might need a dummy even in periods with these // streams already. - period.textStreams.push(shaka.util.PeriodCombiner.dummyStream_( - ContentType.TEXT)); - period.imageStreams.push(shaka.util.PeriodCombiner.dummyStream_( - ContentType.IMAGE)); + if (addDummy) { + const dummyText = PeriodCombiner.dummyStream_(ContentType.TEXT); + textMap.set(PeriodCombiner.generateTextKey_(dummyText), dummyText); + const dummyImage = PeriodCombiner.dummyStream_(ContentType.IMAGE); + imageMap.set(PeriodCombiner.generateImageKey_(dummyImage), dummyImage); + } + + audioStreamsPerPeriod.push(audioMap); + videoStreamsPerPeriod.push(videoMap); + textStreamsPerPeriod.push(textMap); + imageStreamsPerPeriod.push(imageMap); } return { audioStreamsPerPeriod, @@ -159,17 +170,22 @@ shaka.util.PeriodCombiner = class { async combinePeriods(periods, isDynamic) { const ContentType = shaka.util.ManifestParserUtils.ContentType; - 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 // output. if (!isDynamic && periods.length == 1) { - const firstPeriod = periods[0]; - this.audioStreams_ = firstPeriod.audioStreams; - this.videoStreams_ = firstPeriod.videoStreams; - this.textStreams_ = firstPeriod.textStreams; - this.imageStreams_ = firstPeriod.imageStreams; + // We need to filter out duplicates, so call getStreamsPerPeriod() + // so it will do that by usage of Map. + const { + audioStreamsPerPeriod, + videoStreamsPerPeriod, + textStreamsPerPeriod, + imageStreamsPerPeriod, + } = this.getStreamsPerPeriod_(periods, /* addDummy= */ false); + this.audioStreams_ = Array.from(audioStreamsPerPeriod[0].values()); + this.videoStreams_ = Array.from(videoStreamsPerPeriod[0].values()); + this.textStreams_ = Array.from(textStreamsPerPeriod[0].values()); + this.imageStreams_ = Array.from(imageStreamsPerPeriod[0].values()); } else { // Find the first period we haven't seen before. Tag all the periods we // see now as "used". @@ -199,7 +215,7 @@ shaka.util.PeriodCombiner = class { videoStreamsPerPeriod, textStreamsPerPeriod, imageStreamsPerPeriod, - } = this.getStreamsPerPeriod_(periods); + } = this.getStreamsPerPeriod_(periods, /* addDummy= */ true); await Promise.all([ this.combine_( @@ -288,92 +304,6 @@ shaka.util.PeriodCombiner = class { this.variants_ = variants; } - /** - * @param {!Array} periods - * @private - */ - static filterOutDuplicates_(periods) { - const PeriodCombiner = shaka.util.PeriodCombiner; - const ArrayUtils = shaka.util.ArrayUtils; - const MapUtils = shaka.util.MapUtils; - - for (const period of periods) { - // 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.fastSwitching == v2.fastSwitching && - 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; - }); - // 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.fastSwitching == a2.fastSwitching && - 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} streams - * @param {function( - * shaka.extern.Stream, shaka.extern.Stream): boolean} isDuplicate - * @return {!Array} - * @private - */ - static filterOutStreamDuplicates_(streams, isDuplicate) { - const filteredStreams = []; - for (const s1 of streams) { - const duplicate = filteredStreams.some((s2) => isDuplicate(s1, s2)); - if (!duplicate) { - filteredStreams.push(s1); - } - } - return filteredStreams; - } /** * Stitch together DB streams across periods, taking a mix of stream types. @@ -387,6 +317,7 @@ shaka.util.PeriodCombiner = class { */ static async combineDbStreams(streamDbsPerPeriod) { const ContentType = shaka.util.ManifestParserUtils.ContentType; + const PeriodCombiner = shaka.util.PeriodCombiner; // Optimization: for single-period content, do nothing. This makes sure // single-period DASH or any HLS content stored offline will be 100% @@ -396,13 +327,21 @@ shaka.util.PeriodCombiner = class { } const audioStreamDbsPerPeriod = streamDbsPerPeriod.map( - (streams) => streams.filter((s) => s.type == ContentType.AUDIO)); + (streams) => new Map(streams + .filter((s) => s.type === ContentType.AUDIO) + .map((s) => [PeriodCombiner.generateAudioKey_(s), s]))); const videoStreamDbsPerPeriod = streamDbsPerPeriod.map( - (streams) => streams.filter((s) => s.type == ContentType.VIDEO)); + (streams) => new Map(streams + .filter((s) => s.type === ContentType.VIDEO) + .map((s) => [PeriodCombiner.generateVideoKey_(s), s]))); const textStreamDbsPerPeriod = streamDbsPerPeriod.map( - (streams) => streams.filter((s) => s.type == ContentType.TEXT)); + (streams) => new Map(streams + .filter((s) => s.type === ContentType.TEXT) + .map((s) => [PeriodCombiner.generateTextKey_(s), s]))); const imageStreamDbsPerPeriod = streamDbsPerPeriod.map( - (streams) => streams.filter((s) => s.type == ContentType.IMAGE)); + (streams) => new Map(streams + .filter((s) => s.type === ContentType.IMAGE) + .map((s) => [PeriodCombiner.generateImageKey_(s), s]))); // It's okay to have a period with no text or images, but our algorithm // fails on any period without matching streams. So we add dummy streams to @@ -410,12 +349,12 @@ shaka.util.PeriodCombiner = class { // by resolution, we might need a dummy even in periods with these streams // already. for (const textStreams of textStreamDbsPerPeriod) { - textStreams.push(shaka.util.PeriodCombiner.dummyStreamDB_( - ContentType.TEXT)); + const dummy = PeriodCombiner.dummyStreamDB_(ContentType.TEXT); + textStreams.set(PeriodCombiner.generateTextKey_(dummy), dummy); } for (const imageStreams of imageStreamDbsPerPeriod) { - imageStreams.push(shaka.util.PeriodCombiner.dummyStreamDB_( - ContentType.IMAGE)); + const dummy = PeriodCombiner.dummyStreamDB_(ContentType.IMAGE); + imageStreams.set(PeriodCombiner.generateImageKey_(dummy), dummy); } const periodCombiner = new shaka.util.PeriodCombiner(); @@ -486,7 +425,7 @@ shaka.util.PeriodCombiner = class { * * @param {!Array.} outputStreams A list of existing output streams, to * facilitate updates for live DASH content. Will be modified and returned. - * @param {!Array.>} streamsPerPeriod A list of lists of Streams + * @param {!Array>} streamsPerPeriod A list of maps of Streams * from each period. * @param {number} firstNewPeriodIndex An index into streamsPerPeriod which * represents the first new period that hasn't been processed yet. @@ -511,7 +450,7 @@ shaka.util.PeriodCombiner = class { } for (let i = firstNewPeriodIndex; i < streamsPerPeriod.length; i++) { // This periods streams are all new. - unusedStreamsPerPeriod.push(new Set(streamsPerPeriod[i])); + unusedStreamsPerPeriod.push(new Set(streamsPerPeriod[i].values())); } // First, extend all existing output Streams into the new periods. @@ -582,7 +521,7 @@ shaka.util.PeriodCombiner = class { /** * @param {T} outputStream An existing output stream which needs to be * extended into new periods. - * @param {!Array.>} streamsPerPeriod A list of lists of Streams + * @param {!Array>} streamsPerPeriod A list of maps of Streams * from each period. * @param {number} firstNewPeriodIndex An index into streamsPerPeriod which * represents the first new period that hasn't been processed yet. @@ -681,7 +620,7 @@ shaka.util.PeriodCombiner = class { * Templatized to handle both DASH Streams and offline StreamDBs. * * @param {T} stream An input stream on which to base the output stream. - * @param {!Array.>} streamsPerPeriod A list of lists of Streams + * @param {!Array>} streamsPerPeriod A list of maps 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 @@ -999,7 +938,7 @@ shaka.util.PeriodCombiner = class { /** * Finds streams in all periods which match the output stream. * - * @param {!Array.>} streamsPerPeriod + * @param {!Array>} streamsPerPeriod * @param {T} outputStream * * @template T @@ -1022,7 +961,7 @@ shaka.util.PeriodCombiner = class { /** * Find the best match for the output stream. * - * @param {!Array.} streams + * @param {!Map} streams * @param {T} outputStream * @return {?T} Returns null if no match can be found. * @@ -1032,29 +971,42 @@ shaka.util.PeriodCombiner = class { * @private */ findBestMatchInPeriod_(streams, outputStream) { - const areCompatible = { - 'audio': (os, s) => this.areAVStreamsCompatible_(os, s), - 'video': (os, s) => this.areAVStreamsCompatible_(os, s), - 'text': shaka.util.PeriodCombiner.areTextStreamsCompatible_, - 'image': shaka.util.PeriodCombiner.areImageStreamsCompatible_, - }[outputStream.type]; - - const isBetterMatch = { - 'audio': shaka.util.PeriodCombiner.isAudioStreamBetterMatch_, - 'video': shaka.util.PeriodCombiner.isVideoStreamBetterMatch_, - 'text': shaka.util.PeriodCombiner.isTextStreamBetterMatch_, - 'image': shaka.util.PeriodCombiner.isImageStreamBetterMatch_, + const getKey = { + 'audio': shaka.util.PeriodCombiner.generateAudioKey_, + 'video': shaka.util.PeriodCombiner.generateVideoKey_, + 'text': shaka.util.PeriodCombiner.generateTextKey_, + 'image': shaka.util.PeriodCombiner.generateImageKey_, }[outputStream.type]; let best = null; + const key = getKey(outputStream); + if (streams.has(key)) { + // We've found exact match by hashing. + best = streams.get(key); + } else { + // We haven't found exact match, try to find the best one via + // linear search. + const areCompatible = { + 'audio': (os, s) => this.areAVStreamsCompatible_(os, s), + 'video': (os, s) => this.areAVStreamsCompatible_(os, s), + 'text': shaka.util.PeriodCombiner.areTextStreamsCompatible_, + 'image': shaka.util.PeriodCombiner.areImageStreamsCompatible_, + }[outputStream.type]; + const isBetterMatch = { + 'audio': shaka.util.PeriodCombiner.isAudioStreamBetterMatch_, + 'video': shaka.util.PeriodCombiner.isVideoStreamBetterMatch_, + 'text': shaka.util.PeriodCombiner.isTextStreamBetterMatch_, + 'image': shaka.util.PeriodCombiner.isImageStreamBetterMatch_, + }[outputStream.type]; + + for (const stream of streams.values()) { + if (!areCompatible(outputStream, stream)) { + continue; + } - for (const stream of streams) { - if (!areCompatible(outputStream, stream)) { - continue; - } - - if (!best || isBetterMatch(outputStream, best, stream)) { - best = stream; + if (!best || isBetterMatch(outputStream, best, stream)) { + best = stream; + } } } @@ -1201,13 +1153,6 @@ shaka.util.PeriodCombiner = class { 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; - } - // An exact match is better than a non-exact match. const bestIsExact = shaka.util.PeriodCombiner.areAVStreamsExactMatch_( outputStream, best); @@ -1220,9 +1165,6 @@ shaka.util.PeriodCombiner = class { return true; } - // Otherwise, compare the streams' characteristics to determine 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( @@ -1336,13 +1278,6 @@ shaka.util.PeriodCombiner = class { 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; - } - // An exact match is better than a non-exact match. const bestIsExact = shaka.util.PeriodCombiner.areAVStreamsExactMatch_( outputStream, best); @@ -1355,9 +1290,6 @@ shaka.util.PeriodCombiner = class { return true; } - // Otherwise, compare the streams' characteristics to determine the best - // match. - // Take the video with the closest resolution to the output. const resolutionBetterOrWorse = shaka.util.PeriodCombiner.compareClosestPreferLower( @@ -1419,16 +1351,6 @@ shaka.util.PeriodCombiner = class { 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 determine 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( @@ -1505,13 +1427,6 @@ shaka.util.PeriodCombiner = class { static isImageStreamBetterMatch_(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; - } - // Take the image with the closest resolution to the output. const resolutionBetterOrWorse = shaka.util.PeriodCombiner.compareClosestPreferLower( @@ -1709,6 +1624,91 @@ shaka.util.PeriodCombiner = class { return false; } } + + /** + * @param {T} v + * @return {string} + * @template T + * Accepts either a StreamDB or Stream type. + * @private + */ + static generateVideoKey_(v) { + return shaka.util.PeriodCombiner.generateKey_([ + v.fastSwitching, + v.width, + v.frameRate, + v.codecs, + v.mimeType, + v.label, + v.roles, + v.closedCaptions ? Array.from(v.closedCaptions.entries()) : null, + v.bandwidth, + ]); + } + + /** + * @param {T} a + * @return {string} + * @template T + * Accepts either a StreamDB or Stream type. + * @private + */ + static generateAudioKey_(a) { + return shaka.util.PeriodCombiner.generateKey_([ + a.fastSwitching, + a.channelsCount, + a.language, + a.bandwidth, + a.label, + a.codecs, + a.mimeType, + a.roles, + a.audioSamplingRate, + a.primary, + ]); + } + + /** + * @param {T} t + * @return {string} + * @template T + * Accepts either a StreamDB or Stream type. + * @private + */ + static generateTextKey_(t) { + return shaka.util.PeriodCombiner.generateKey_([ + t.language, + t.label, + t.codecs, + t.mimeType, + t.bandwidth, + t.roles, + ]); + } + + /** + * @param {T} i + * @return {string} + * @template T + * Accepts either a StreamDB or Stream type. + * @private + */ + static generateImageKey_(i) { + return shaka.util.PeriodCombiner.generateKey_([ + i.width, + i.codecs, + i.mimeType, + ]); + } + + /** + * @param {!Array<*>} values + * @return {string} + * @private + */ + static generateKey_(values) { + return JSON.stringify(values); + } }; /** diff --git a/test/util/periods_unit.js b/test/util/periods_unit.js index c18eec3eb4..07ef999bed 100644 --- a/test/util/periods_unit.js +++ b/test/util/periods_unit.js @@ -572,34 +572,34 @@ describe('PeriodCombiner', () => { const variants = combiner.getVariants(); expect(variants.length).toBe(8); - // v3 should've been filtered out + // v1 should've been filtered out const videoIds = variants.map((v) => v.video.originalId); for (const id of videoIds) { - expect(id).not.toBe('v3'); + expect(id).not.toBe('v1'); } - // a2 should've been filtered out + // a1 should've been filtered out const audioIds = variants.map((v) => v.audio.originalId); for (const id of audioIds) { - expect(id).not.toBe('a2'); + expect(id).not.toBe('a1'); } const textStreams = combiner.getTextStreams(); expect(textStreams.length).toBe(3); - // t3 should've been filtered out + // t1 should've been filtered out const textIds = textStreams.map((t) => t.originalId); for (const id of textIds) { - expect(id).not.toBe('t3'); + expect(id).not.toBe('t1'); } const imageStreams = combiner.getImageStreams(); expect(imageStreams.length).toBe(2); - // i3 should've been filtered out + // i1 should've been filtered out const imageIds = imageStreams.map((i) => i.originalId); for (const id of imageIds) { - expect(id).not.toBe('i3'); + expect(id).not.toBe('i1'); } }); From 9aba0bf070c607d49c6ae87bef3683daf1db3349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Fri, 8 Dec 2023 14:43:18 +0100 Subject: [PATCH 7/7] clone roles array in StreamDB --- lib/util/periods.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/util/periods.js b/lib/util/periods.js index 4b0e63befb..9c5bb40889 100644 --- a/lib/util/periods.js +++ b/lib/util/periods.js @@ -785,6 +785,8 @@ shaka.util.PeriodCombiner = class { const clone = /** @type {shaka.extern.StreamDB} */(Object.assign( {}, streamDb)); + // Clone roles array so this output stream can own it. + clone.roles = clone.roles.slice(); // These are wiped out now and rebuilt later from the various per-period // streams that match this output. clone.keyIds = new Set();