diff --git a/src/parsers/manifest/dash/common/parse_adaptation_sets.ts b/src/parsers/manifest/dash/common/parse_adaptation_sets.ts index 12116b8e7c..817e2ee7a1 100644 --- a/src/parsers/manifest/dash/common/parse_adaptation_sets.ts +++ b/src/parsers/manifest/dash/common/parse_adaptation_sets.ts @@ -15,14 +15,18 @@ */ import log from "../../../../log"; -import { Period } from "../../../../manifest"; +import { + IAdaptationType, + Period, + SUPPORTED_ADAPTATIONS_TYPE, +} from "../../../../manifest"; import arrayFind from "../../../../utils/array_find"; +import arrayFindIndex from "../../../../utils/array_find_index"; import arrayIncludes from "../../../../utils/array_includes"; import isNonEmptyString from "../../../../utils/is_non_empty_string"; import { IParsedAdaptation, IParsedAdaptations, - IParsedAdaptationType, } from "../../types"; import { IAdaptationSetIntermediateRepresentation, @@ -235,30 +239,31 @@ export default function parseAdaptationSets( adaptationsIR : IAdaptationSetIntermediateRepresentation[], context : IAdaptationSetContext ): IParsedAdaptations { - const parsedAdaptations : IParsedAdaptations = {}; + const parsedAdaptations : Record< + IAdaptationType, + Array<[ IParsedAdaptation, + IAdaptationSetOrderingData ]> + > = { video: [], + audio: [], + text: [], + image: [] }; const trickModeAdaptations: Array<{ adaptation: IParsedAdaptation; trickModeAttachedAdaptationIds: string[]; }> = []; const adaptationSwitchingInfos : IAdaptationSwitchingInfos = {}; + const parsedAdaptationsIDs : string[] = []; /** - * Index of the last parsed AdaptationSet with a Role set as "main" in - * `parsedAdaptations` for a given type. - * Not defined for a type with no main Adaptation inside. - * This is used to put main AdaptationSet first in the resulting array of - * Adaptation while still preserving the MPD order among them. + * Index of the last parsed Video AdaptationSet with a Role set as "main" in + * `parsedAdaptations.video`. + * `-1` if not yet encountered. + * Used as we merge all main video AdaptationSet due to a comprehension of the + * DASH-IF IOP. */ - const lastMainAdaptationIndex : Partial> = {}; - - // first sort AdaptationSets by absolute priority. - adaptationsIR.sort((a, b) => { - /* As of DASH-IF 4.3, `1` is the default value. */ - const priority1 = a.attributes.selectionPriority ?? 1; - const priority2 = b.attributes.selectionPriority ?? 1; - return priority2 - priority1; - }); + let lastMainVideoAdapIdx = -1; - for (const adaptation of adaptationsIR) { + for (let adaptationIdx = 0; adaptationIdx < adaptationsIR.length; adaptationIdx++) { + const adaptation = adaptationsIR[adaptationIdx]; const adaptationChildren = adaptation.children; const { essentialProperties, roles } = adaptationChildren; @@ -291,6 +296,7 @@ export default function parseAdaptationSets( continue; } + const priority = adaptation.attributes.selectionPriority ?? 1; const originalID = adaptation.attributes.id; let newID : string; const adaptationSetSwitchingIDs = getAdaptationSetSwitchingIDs(adaptation); @@ -334,14 +340,11 @@ export default function parseAdaptationSets( if (type === "video" && isMainAdaptation && - parsedAdaptations.video !== undefined && - parsedAdaptations.video.length > 0 && - lastMainAdaptationIndex.video !== undefined && + lastMainVideoAdapIdx >= 0 && + parsedAdaptations.video.length > lastMainVideoAdapIdx && !isTrickModeTrack) { - // Add to the already existing main video adaptation - // TODO remove that ugly custom logic? - const videoMainAdaptation = parsedAdaptations.video[lastMainAdaptationIndex.video]; + const videoMainAdaptation = parsedAdaptations.video[lastMainVideoAdapIdx][0]; reprCtxt.unsafelyBaseOnPreviousAdaptation = context .unsafelyBaseOnPreviousPeriod?.getAdaptation(videoMainAdaptation.id) ?? null; const representations = parseRepresentations(representationsIR, @@ -422,65 +425,56 @@ export default function parseAdaptationSets( parsedAdaptationSet.isSignInterpreted = true; } - const adaptationsOfTheSameType = parsedAdaptations[type]; if (trickModeAttachedAdaptationIds !== undefined) { trickModeAdaptations.push({ adaptation: parsedAdaptationSet, trickModeAttachedAdaptationIds }); - } else if (adaptationsOfTheSameType === undefined) { - parsedAdaptations[type] = [parsedAdaptationSet]; - if (isMainAdaptation) { - lastMainAdaptationIndex[type] = 0; - } } else { - let mergedInto : IParsedAdaptation|null = null; // look if we have to merge this into another Adaptation + let mergedIntoIdx = -1; for (const id of adaptationSetSwitchingIDs) { const switchingInfos = adaptationSwitchingInfos[id]; - if (switchingInfos != null && + if (switchingInfos !== undefined && switchingInfos.newID !== newID && arrayIncludes(switchingInfos.adaptationSetSwitchingIDs, originalID)) { - const adaptationToMergeInto = arrayFind(adaptationsOfTheSameType, - (a) => a.id === id); - if (adaptationToMergeInto != null && - adaptationToMergeInto.audioDescription === + mergedIntoIdx = arrayFindIndex(parsedAdaptations[type], + (a) => a[0].id === id); + const mergedInto = parsedAdaptations[type][mergedIntoIdx]; + if (mergedInto !== undefined && + mergedInto[0].audioDescription === parsedAdaptationSet.audioDescription && - adaptationToMergeInto.closedCaption === + mergedInto[0].closedCaption === parsedAdaptationSet.closedCaption && - adaptationToMergeInto.language === parsedAdaptationSet.language) + mergedInto[0].language === parsedAdaptationSet.language) { log.info("DASH Parser: merging \"switchable\" AdaptationSets", originalID, id); - adaptationToMergeInto.representations - .push(...parsedAdaptationSet.representations); - mergedInto = adaptationToMergeInto; + mergedInto[0].representations.push(...parsedAdaptationSet.representations); + if (type === "video" && + isMainAdaptation && + !mergedInto[1].isMainAdaptation) + { + lastMainVideoAdapIdx = Math.max(lastMainVideoAdapIdx, mergedIntoIdx); + } + mergedInto[1] = { + priority: Math.max(priority, mergedInto[1].priority), + isMainAdaptation: isMainAdaptation || + mergedInto[1].isMainAdaptation, + indexInMpd: Math.min(adaptationIdx, mergedInto[1].indexInMpd), + }; } } } - if (isMainAdaptation) { - const oldLastMainIdx = lastMainAdaptationIndex[type]; - const newLastMainIdx = oldLastMainIdx === undefined ? 0 : - oldLastMainIdx + 1; - if (mergedInto === null) { - // put "main" Adaptation after all other Main Adaptations - adaptationsOfTheSameType.splice(newLastMainIdx, 0, parsedAdaptationSet); - lastMainAdaptationIndex[type] = newLastMainIdx; - } else { - const indexOf = adaptationsOfTheSameType.indexOf(mergedInto); - if (indexOf < 0) { // Weird, not found - adaptationsOfTheSameType.splice(newLastMainIdx, 0, parsedAdaptationSet); - lastMainAdaptationIndex[type] = newLastMainIdx; - } else if (oldLastMainIdx === undefined || indexOf > oldLastMainIdx) { - // Found but was not main - adaptationsOfTheSameType.splice(indexOf, 1); - adaptationsOfTheSameType.splice(newLastMainIdx, 0, mergedInto); - lastMainAdaptationIndex[type] = newLastMainIdx; - } + if (mergedIntoIdx < 0) { + parsedAdaptations[type].push([ parsedAdaptationSet, + { priority, + isMainAdaptation, + indexInMpd: adaptationIdx }]); + if (type === "video" && isMainAdaptation) { + lastMainVideoAdapIdx = parsedAdaptations.video.length - 1; } - } else if (mergedInto === null) { - adaptationsOfTheSameType.push(parsedAdaptationSet); } } } @@ -490,8 +484,59 @@ export default function parseAdaptationSets( adaptationSetSwitchingIDs }; } } - attachTrickModeTrack(parsedAdaptations, trickModeAdaptations); - return parsedAdaptations; + + const adaptationsPerType = SUPPORTED_ADAPTATIONS_TYPE + .reduce((acc : IParsedAdaptations, adaptationType : IAdaptationType) => { + const adaptationsParsedForType = parsedAdaptations[adaptationType]; + if (adaptationsParsedForType.length > 0) { + adaptationsParsedForType.sort(compareAdaptations); + acc[adaptationType] = adaptationsParsedForType + .map(([parsedAdaptation]) => parsedAdaptation); + } + return acc; + }, {}); + parsedAdaptations.video.sort(compareAdaptations); + attachTrickModeTrack(adaptationsPerType, trickModeAdaptations); + return adaptationsPerType; +} + +/** Metadata allowing to order AdaptationSets between one another. */ +interface IAdaptationSetOrderingData { + /** + * If `true`, this AdaptationSet is considered as a "main" one (e.g. it had a + * Role set to "main"). + */ + isMainAdaptation : boolean; + /** + * Set to the `selectionPriority` attribute of the corresponding AdaptationSet + * or to `1` by default. + */ + priority : number; + /** Index of this AdaptationSet in the original MPD, starting from `0`. */ + indexInMpd : number; +} + +/** + * Compare groups of parsed AdaptationSet, alongside some ordering metadata, + * allowing to easily sort them through JavaScript's `Array.prototype.sort` + * method. + * @param {Array.} a + * @param {Array.} b + * @returns {number} + */ +function compareAdaptations( + a : [IParsedAdaptation, IAdaptationSetOrderingData], + b : [IParsedAdaptation, IAdaptationSetOrderingData] +) : number { + const priorityDiff = b[1].priority - a[1].priority; + if (priorityDiff !== 0) { + return priorityDiff; + } + if (a[1].isMainAdaptation !== b[1].isMainAdaptation) { + return a[1].isMainAdaptation ? -1 : + 1; + } + return a[1].indexInMpd - b[1].indexInMpd; } /** Context needed when calling `parseAdaptationSets`. */ diff --git a/tests/contents/DASH_static_SegmentTimeline/media/multi-AdaptationSets.mpd b/tests/contents/DASH_static_SegmentTimeline/media/multi-AdaptationSets.mpd index d0a21d5540..c968e9f4fb 100644 --- a/tests/contents/DASH_static_SegmentTimeline/media/multi-AdaptationSets.mpd +++ b/tests/contents/DASH_static_SegmentTimeline/media/multi-AdaptationSets.mpd @@ -380,17 +380,18 @@ - + + @@ -401,17 +402,17 @@ - + - @@ -485,6 +486,27 @@ + + + + + + + + + + + + + - - + - + - + - + - + + - - diff --git a/tests/contents/DASH_static_SegmentTimeline/media/multi_period_same_choices.mpd b/tests/contents/DASH_static_SegmentTimeline/media/multi_period_same_choices.mpd index eee248607d..e39d5181ea 100644 --- a/tests/contents/DASH_static_SegmentTimeline/media/multi_period_same_choices.mpd +++ b/tests/contents/DASH_static_SegmentTimeline/media/multi_period_same_choices.mpd @@ -253,7 +253,7 @@ - + - @@ -567,46 +567,47 @@ - + + - + - + - - + - + + - diff --git a/tests/integration/scenarios/dash_multi-track.js b/tests/integration/scenarios/dash_multi-track.js index f26740dfae..9a1409359e 100644 --- a/tests/integration/scenarios/dash_multi-track.js +++ b/tests/integration/scenarios/dash_multi-track.js @@ -116,7 +116,6 @@ describe("DASH multi-track content (SegmentTimeline)", function () { function checkVideoTrack(codecRules, isSignInterpreted) { const currentVideoTrack = player.getVideoTrack(); - expect(currentVideoTrack).to.not.equal(null); if (isSignInterpreted === undefined) { expect(Object.prototype.hasOwnProperty.call(currentVideoTrack, @@ -169,11 +168,12 @@ describe("DASH multi-track content (SegmentTimeline)", function () { // TODO AUDIO codec checkAudioTrack("de", "deu", false); checkNoTextTrack(); - checkVideoTrack({ all: false, test: /avc1\.640028/ }, undefined); + checkVideoTrack({ all: true, test: /avc1\.42C014/ }, true); await goToSecondPeriod(); - checkAudioTrack("be", "bel", false); + checkAudioTrack("fr", "fra", true); checkNoTextTrack(); + checkVideoTrack({ all: false, test: /avc1\.42C014/ }, undefined); checkVideoTrack({ all: false, test: /avc1\.640028/ }, undefined); }); @@ -326,7 +326,7 @@ describe("DASH multi-track content (SegmentTimeline)", function () { // TODO AUDIO codec checkAudioTrack("de", "deu", false); checkTextTrack("de", "deu", false); - checkVideoTrack({ all: false, test: /avc1\.640028/ }, undefined); + checkVideoTrack({ all: false, test: /avc1\.640028/ }, true); await goToSecondPeriod(); checkAudioTrack("de", "deu", false); @@ -356,17 +356,18 @@ describe("DASH multi-track content (SegmentTimeline)", function () { // TODO AUDIO codec checkAudioTrack("de", "deu", false); checkNoTextTrack(); - checkVideoTrack({ all: false, test: /avc1\.640028/ }, undefined); + checkVideoTrack({ all: true, test: /avc1\.42C014/ }, true); await goToSecondPeriod(); - checkAudioTrack("be", "bel", false); + checkAudioTrack("fr", "fra", true); checkNoTextTrack(); + checkVideoTrack({ all: false, test: /avc1\.42C014/ }, undefined); checkVideoTrack({ all: false, test: /avc1\.640028/ }, undefined); }); it("should not update the current tracks for non-applied preferences", async () => { await loadContent(); - player.setPreferredAudioTracks([ { language: "fr", + player.setPreferredAudioTracks([ { language: "be", audioDescription: true } ]); player.setPreferredVideoTracks([ { codec: { all: false, test: /avc1\.640028/}, @@ -376,10 +377,10 @@ describe("DASH multi-track content (SegmentTimeline)", function () { await sleep(100); checkAudioTrack("de", "deu", false); checkNoTextTrack(); - checkVideoTrack({ all: false, test: /avc1\.640028/ }, undefined); + checkVideoTrack({ all: true, test: /avc1\.42C014/ }, true); await goToSecondPeriod(); - checkAudioTrack("fr", "fra", true); + checkAudioTrack("be", "bel", true); checkTextTrack("de", "deu", false); checkVideoTrack({ all: false, test: /avc1\.640028/ }, true); }); @@ -389,7 +390,7 @@ describe("DASH multi-track content (SegmentTimeline)", function () { await sleep(100); checkAudioTrack("de", "deu", false); checkNoTextTrack(); - checkVideoTrack({ all: false, test: /avc1\.640028/ }, undefined); + checkVideoTrack({ all: true, test: /avc1\.42C014/ }, true); await goToSecondPeriod(); player.setPreferredAudioTracks([ { language: "fr", @@ -403,7 +404,7 @@ describe("DASH multi-track content (SegmentTimeline)", function () { await goToFirstPeriod(); checkAudioTrack("de", "deu", false); checkNoTextTrack(); - checkVideoTrack({ all: false, test: /avc1\.640028/ }, undefined); + checkVideoTrack({ all: true, test: /avc1\.42C014/ }, true); }); it("should update the current tracks for applied preferences", async () => { @@ -431,7 +432,7 @@ describe("DASH multi-track content (SegmentTimeline)", function () { await sleep(100); checkAudioTrack("de", "deu", false); checkNoTextTrack(); - checkVideoTrack({ all: false, test: /avc1\.640028/ }, undefined); + checkVideoTrack({ all: true, test: /avc1\.42C014/ }, true); await goToSecondPeriod(); player.setPreferredAudioTracks([ { language: "fr", @@ -458,7 +459,7 @@ describe("DASH multi-track content (SegmentTimeline)", function () { checkNoVideoTrack(); await goToSecondPeriod(); - checkAudioTrack("be", "bel", false); + checkAudioTrack("fr", "fra", true); checkNoTextTrack(); checkNoVideoTrack(); }); @@ -487,7 +488,7 @@ describe("DASH multi-track content (SegmentTimeline)", function () { checkNoVideoTrack(); await goToSecondPeriod(); - checkAudioTrack("be", "bel", false); + checkAudioTrack("fr", "fra", true); checkNoTextTrack(); checkNoVideoTrack(); }); @@ -499,7 +500,7 @@ describe("DASH multi-track content (SegmentTimeline)", function () { // TODO AUDIO codec checkAudioTrack("de", "deu", false); checkNoTextTrack(); - checkVideoTrack({ all: false, test: /avc1\.640028/ }, undefined); + checkVideoTrack({ all: true, test: /avc1\.42C014/ }, true); setAudioTrack("fr", true); setTextTrack("de", false); @@ -511,8 +512,9 @@ describe("DASH multi-track content (SegmentTimeline)", function () { checkVideoTrack({ all: false, test: /avc1\.640028/ }, true); await goToSecondPeriod(); - checkAudioTrack("be", "bel", false); + checkAudioTrack("fr", "fra", true); checkNoTextTrack(); + checkVideoTrack({ all: false, test: /avc1\.42C014/ }, undefined); checkVideoTrack({ all: false, test: /avc1\.640028/ }, undefined); }); @@ -522,22 +524,22 @@ describe("DASH multi-track content (SegmentTimeline)", function () { // TODO AUDIO codec checkAudioTrack("de", "deu", false); checkNoTextTrack(); - checkVideoTrack({ all: false, test: /avc1\.640028/ }, undefined); + checkVideoTrack({ all: true, test: /avc1\.42C014/ }, true); setAudioTrack("fr", true); setTextTrack("de", false); - setVideoTrack({ all: false, test: /avc1\.640028/ }, true); + setVideoTrack({ all: true, test: /avc1\.42C014/ }, undefined); await sleep(50); checkAudioTrack("fr", "fra", true); checkTextTrack("de", "deu", false); - checkVideoTrack({ all: false, test: /avc1\.640028/ }, true); + checkVideoTrack({ all: false, test: /avc1\.42C014/ }, undefined); await goToSecondPeriod(); await goToFirstPeriod(); checkAudioTrack("fr", "fra", true); checkTextTrack("de", "deu", false); - checkVideoTrack({ all: false, test: /avc1\.640028/ }, true); + checkVideoTrack({ all: false, test: /avc1\.42C014/ }, undefined); }); it("preferences should be persisted if already set for a given Period", async function() {