diff --git a/doc/api/Player_Events.md b/doc/api/Player_Events.md index 7503c990cc9..07db1e91807 100644 --- a/doc/api/Player_Events.md +++ b/doc/api/Player_Events.md @@ -217,6 +217,12 @@ The array emitted contains object describing each available text track: - `closedCaption` (`Boolean`): Whether the track is specially adapted for the hard of hearing or not. +- `forced` (`Boolean`): If `true` this text track is meant to be displayed by + default if no other text track is selected. + + It is often used to clarify dialogue, alternate languages, texted graphics or + location and person identification. + - `active` (`Boolean`): Whether the track is the one currently active or not. @@ -259,9 +265,18 @@ The payload is an object describing the new track, with the following properties: - `id` (`Number|string`): The id used to identify the track. + - `language` (`string`): The language the text track is in. + - `closedCaption` (`Boolean`): Whether the track is specially adapted for the hard of hearing or not. + +- `forced` (`Boolean`): If `true` this text track is meant to be displayed by + default if no other text track is selected. + + It is often used to clarify dialogue, alternate languages, texted graphics or + location and person identification. + - `label` (`string|undefined`): A human readable label that may be displayed in the user interface providing a choice between text tracks. diff --git a/doc/api/Track_Selection/getAvailableTextTracks.md b/doc/api/Track_Selection/getAvailableTextTracks.md index c83b5e1c4d5..46019df3953 100644 --- a/doc/api/Track_Selection/getAvailableTextTracks.md +++ b/doc/api/Track_Selection/getAvailableTextTracks.md @@ -21,6 +21,12 @@ Each of the objects in the returned array have the following properties: - `closedCaption` (`Boolean`): Whether the track is specially adapted for the hard of hearing or not. +- `forced` (`Boolean`): If `true` this text track is meant to be displayed by + default if no other text track is selected. + + It is often used to clarify dialogue, alternate languages, texted graphics or + location and person identification. + - `label` (`string|undefined`): A human readable label that may be displayed in the user interface providing a choice between text tracks. diff --git a/doc/api/Track_Selection/getPreferredTextTracks.md b/doc/api/Track_Selection/getPreferredTextTracks.md index cfbd38b6f5c..14be57caebe 100644 --- a/doc/api/Track_Selection/getPreferredTextTracks.md +++ b/doc/api/Track_Selection/getPreferredTextTracks.md @@ -12,8 +12,10 @@ it was called: { language: "fra", // {string} The wanted language // (ISO 639-1, ISO 639-2 or ISO 639-3 language code) - closedCaption: false // {Boolean} Whether the text track should be a closed - // caption for the hard of hearing + closedCaption: false, // {Boolean} Whether the text track should be a closed + // caption for the hard of hearing + forced: false // {Boolean|undefined} If `true` this text track is meant to be + // displayed by default if no other text track is selected. } ``` diff --git a/doc/api/Track_Selection/getTextTrack.md b/doc/api/Track_Selection/getTextTrack.md index c86de6e12bb..9ba3211f733 100644 --- a/doc/api/Track_Selection/getTextTrack.md +++ b/doc/api/Track_Selection/getTextTrack.md @@ -31,6 +31,12 @@ return an object with the following properties: - `closedCaption` (`Boolean`): Whether the track is specially adapted for the hard of hearing or not. +- `forced` (`Boolean`): If `true` this text track is meant to be displayed by + default if no other text track is selected. + + It is often used to clarify dialogue, alternate languages, texted graphics or + location and person identification. + `undefined` if no text content has been loaded yet or if its information is unknown. diff --git a/doc/api/Track_Selection/setPreferredTextTracks.md b/doc/api/Track_Selection/setPreferredTextTracks.md index 635eb5d19e6..ebce65ab525 100644 --- a/doc/api/Track_Selection/setPreferredTextTracks.md +++ b/doc/api/Track_Selection/setPreferredTextTracks.md @@ -13,14 +13,22 @@ apply to every future loaded content in the current RxPlayer instance. The first argument should be set as an array of objects, each object describing constraints a text track should respect. +Here are the list of properties that can be set on each of those objects: + + - **language** (`string`): The wanted language (preferably as an ISO 639-1, + ISO 639-2 or ISO 639-3 language code) + + - **closedCaption** (`boolean`): Whether the text track should be a closed + caption for the hard of hearing + + - **forced** (`boolean|undefined`): If `true` the text track should be a + "forced subtitle", which are default text tracks used when no other text + track is selected. + Here is all the properties that should be set in a single object of that array. ```js { - language: "fra", // {string} The wanted language - // (ISO 639-1, ISO 639-2 or ISO 639-3 language code) - closedCaption: false // {Boolean} Whether the text track should be a closed - // caption for the hard of hearing } ``` diff --git a/src/core/api/tracks_management/track_choice_manager.ts b/src/core/api/tracks_management/track_choice_manager.ts index 51f3c1ab7a4..504dfd814e7 100644 --- a/src/core/api/tracks_management/track_choice_manager.ts +++ b/src/core/api/tracks_management/track_choice_manager.ts @@ -84,6 +84,7 @@ type INormalizedPreferredTextTrack = null | /** Text track preference when it is not set to `null`. */ interface INormalizedPreferredTextTrackObject { normalized : string; + forced : boolean | undefined; closedCaption : boolean; } @@ -116,6 +117,7 @@ function normalizeTextTracks( return tracks.map(t => t === null ? t : { normalized: normalizeLanguage(t.language), + forced: t.forced, closedCaption: t.closedCaption }); } @@ -384,8 +386,10 @@ export default class TrackChoiceManager { // Find the optimal text Adaptation const preferredTextTracks = this._preferredTextTracks; const normalizedPref = normalizeTextTracks(preferredTextTracks); - const optimalAdaptation = findFirstOptimalTextAdaptation(textAdaptations, - normalizedPref); + const optimalAdaptation = findFirstOptimalTextAdaptation( + textAdaptations, + normalizedPref, + this._audioChoiceMemory.get(period)); this._textChoiceMemory.set(period, optimalAdaptation); textInfos.adaptation$.next(optimalAdaptation); } else { @@ -645,13 +649,17 @@ export default class TrackChoiceManager { return null; } - return { + const formatted : ITextTrack = { language: takeFirstSet(chosenTextAdaptation.language, ""), normalized: takeFirstSet(chosenTextAdaptation.normalizedLanguage, ""), closedCaption: chosenTextAdaptation.isClosedCaption === true, id: chosenTextAdaptation.id, label: chosenTextAdaptation.label, }; + if (chosenTextAdaptation.isForcedSubtitles !== undefined) { + formatted.forced = chosenTextAdaptation.isForcedSubtitles; + } + return formatted; } /** @@ -768,15 +776,21 @@ export default class TrackChoiceManager { null; return textInfos.adaptations - .map((adaptation) => ({ - language: takeFirstSet(adaptation.language, ""), - normalized: takeFirstSet(adaptation.normalizedLanguage, ""), - closedCaption: adaptation.isClosedCaption === true, - id: adaptation.id, - active: currentId === null ? false : - currentId === adaptation.id, - label: adaptation.label, - })); + .map((adaptation) => { + const formatted : IAvailableTextTrack = { + language: takeFirstSet(adaptation.language, ""), + normalized: takeFirstSet(adaptation.normalizedLanguage, ""), + closedCaption: adaptation.isClosedCaption === true, + id: adaptation.id, + active: currentId === null ? false : + currentId === adaptation.id, + label: adaptation.label, + }; + if (adaptation.isForcedSubtitles !== undefined) { + formatted.forced = adaptation.isForcedSubtitles; + } + return formatted; + }); } /** @@ -956,8 +970,10 @@ export default class TrackChoiceManager { return; } - const optimalAdaptation = findFirstOptimalTextAdaptation(textAdaptations, - normalizedPref); + const optimalAdaptation = findFirstOptimalTextAdaptation( + textAdaptations, + normalizedPref, + this._audioChoiceMemory.get(period)); this._textChoiceMemory.set(period, optimalAdaptation); textItem.adaptation$.next(optimalAdaptation); @@ -1162,7 +1178,9 @@ function createTextPreferenceMatcher( return takeFirstSet(textAdaptation.normalizedLanguage, "") === preferredTextTrack.normalized && (preferredTextTrack.closedCaption ? textAdaptation.isClosedCaption === true : - textAdaptation.isClosedCaption !== true); + textAdaptation.isClosedCaption !== true) && + (preferredTextTrack.forced === true ? textAdaptation.isForcedSubtitles === true : + textAdaptation.isForcedSubtitles !== true); }; } @@ -1173,12 +1191,14 @@ function createTextPreferenceMatcher( * `null` if the most optimal text adaptation is no text adaptation. * @param {Array.} textAdaptations * @param {Array.} preferredTextTracks + * @param {Object|null|undefined} chosenAudioAdaptation * @returns {Adaptation|null} */ function findFirstOptimalTextAdaptation( textAdaptations : Adaptation[], - preferredTextTracks : INormalizedPreferredTextTrack[] -) : Adaptation|null { + preferredTextTracks : INormalizedPreferredTextTrack[], + chosenAudioAdaptation : Adaptation | null | undefined +) : Adaptation | null { if (textAdaptations.length === 0) { return null; } @@ -1198,6 +1218,19 @@ function findFirstOptimalTextAdaptation( } } + const forcedSubtitles = textAdaptations.filter((ad) => ad.isForcedSubtitles === true); + if (forcedSubtitles.length > 0) { + if (chosenAudioAdaptation !== null && chosenAudioAdaptation !== undefined) { + const sameLanguage = arrayFind(forcedSubtitles, (f) => + f.normalizedLanguage === chosenAudioAdaptation.normalizedLanguage); + if (sameLanguage !== undefined) { + return sameLanguage; + } + } + return arrayFind(forcedSubtitles, (f) => f.normalizedLanguage === undefined) ?? + null; + } + // no optimal adaptation return null; } diff --git a/src/manifest/adaptation.ts b/src/manifest/adaptation.ts index 9ac56657770..fd94d36c7b7 100644 --- a/src/manifest/adaptation.ts +++ b/src/manifest/adaptation.ts @@ -56,6 +56,14 @@ export default class Adaptation { /** Whether this Adaptation contains closed captions for the hard-of-hearing. */ public isClosedCaption? : boolean; + /** + * If `true` this Adaptation are subtitles Meant for display when no other text + * Adaptation is selected. It is used to clarify dialogue, alternate + * languages, texted graphics or location/person IDs that are not otherwise + * covered in the dubbed/localized audio Adaptation. + */ + public isForcedSubtitles? : boolean; + /** If true this Adaptation contains sign interpretation. */ public isSignInterpreted? : boolean; @@ -120,6 +128,9 @@ export default class Adaptation { if (parsedAdaptation.isDub !== undefined) { this.isDub = parsedAdaptation.isDub; } + if (parsedAdaptation.forcedSubtitles !== undefined) { + this.isForcedSubtitles = parsedAdaptation.forcedSubtitles; + } if (parsedAdaptation.isSignInterpreted !== undefined) { this.isSignInterpreted = parsedAdaptation.isSignInterpreted; } diff --git a/src/parsers/manifest/dash/common/parse_adaptation_sets.ts b/src/parsers/manifest/dash/common/parse_adaptation_sets.ts index 6f15c7d497e..29af2c504c3 100644 --- a/src/parsers/manifest/dash/common/parse_adaptation_sets.ts +++ b/src/parsers/manifest/dash/common/parse_adaptation_sets.ts @@ -107,21 +107,36 @@ function isVisuallyImpaired( /** * Detect if the accessibility given defines an adaptation for the hard of * hearing. - * Based on DVB Document A168 (DVB-DASH). - * @param {Object} accessibility + * Based on DVB Document A168 (DVB-DASH) and DASH specification. + * @param {Array.} accessibilities + * @param {Array.} roles * @returns {Boolean} */ -function isHardOfHearing( - accessibility : { schemeIdUri? : string | undefined; - value? : string | undefined; } | - undefined +function isCaptionning( + accessibilities : Array<{ schemeIdUri? : string | undefined; + value? : string | undefined; }> | + undefined, + roles : Array<{ schemeIdUri? : string | undefined; + value? : string | undefined; }> | + undefined ) : boolean { - if (accessibility === undefined) { - return false; + if (accessibilities !== undefined) { + const hasDvbClosedCaptionSignaling = accessibilities.some(accessibility => + (accessibility.schemeIdUri === "urn:tva:metadata:cs:AudioPurposeCS:2007" && + accessibility.value === "2")); + if (hasDvbClosedCaptionSignaling) { + return true; + } } - - return (accessibility.schemeIdUri === "urn:tva:metadata:cs:AudioPurposeCS:2007" && - accessibility.value === "2"); + if (roles !== undefined) { + const hasDashCaptionSinaling = roles.some(role => + (role.schemeIdUri === "urn:mpeg:dash:role:2011" && + role.value === "caption")); + if (hasDashCaptionSinaling) { + return true; + } + } + return false; } /** @@ -147,14 +162,13 @@ function hasSignLanguageInterpretation( /** * Contruct Adaptation ID from the information we have. * @param {Object} adaptation - * @param {Array.} representations - * @param {Array.} representations * @param {Object} infos * @returns {string} */ function getAdaptationID( adaptation : IAdaptationSetIntermediateRepresentation, infos : { isClosedCaption : boolean | undefined; + isForcedSubtitle : boolean | undefined; isAudioDescription : boolean | undefined; isSignInterpreted : boolean | undefined; isTrickModeTrack: boolean; @@ -165,6 +179,7 @@ function getAdaptationID( } const { isClosedCaption, + isForcedSubtitle, isAudioDescription, isSignInterpreted, isTrickModeTrack, @@ -177,6 +192,9 @@ function getAdaptationID( if (isClosedCaption === true) { idString += "-cc"; } + if (isForcedSubtitle === true) { + idString += "-cc"; + } if (isAudioDescription === true) { idString += "-ad"; } @@ -365,8 +383,17 @@ export default function parseAdaptationSets( let isClosedCaption; if (type !== "text") { isClosedCaption = false; - } else if (accessibilities !== undefined) { - isClosedCaption = accessibilities.some(isHardOfHearing); + } else { + isClosedCaption = isCaptionning(accessibilities, roles); + } + + let isForcedSubtitle; + if (type === "text" && + roles !== undefined && + roles.some((role) => role.value === "forced-subtitle" || + role.value === "forced_subtitle")) + { + isForcedSubtitle = true; } let isAudioDescription; @@ -385,6 +412,7 @@ export default function parseAdaptationSets( let adaptationID = getAdaptationID(adaptation, { isAudioDescription, + isForcedSubtitle, isClosedCaption, isSignInterpreted, isTrickModeTrack, @@ -421,6 +449,9 @@ export default function parseAdaptationSets( if (isDub === true) { parsedAdaptationSet.isDub = true; } + if (isForcedSubtitle !== undefined) { + parsedAdaptationSet.forcedSubtitles = isForcedSubtitle; + } if (isSignInterpreted === true) { parsedAdaptationSet.isSignInterpreted = true; } diff --git a/src/parsers/manifest/types.ts b/src/parsers/manifest/types.ts index 076e4941673..d6cf68bc853 100644 --- a/src/parsers/manifest/types.ts +++ b/src/parsers/manifest/types.ts @@ -193,6 +193,13 @@ export interface IParsedAdaptation { * a video track). */ closedCaption? : boolean | undefined; + /** + * If `true` this Adaptation are subtitles Meant for display when no other text + * Adaptation is selected. It is used to clarify dialogue, alternate + * languages, texted graphics or location/person IDs that are not otherwise + * covered in the dubbed/localized audio Adaptation. + */ + forcedSubtitles? : boolean; /** * If true this Adaptation is in a dub: it was recorded in another language * than the original(s) one(s). diff --git a/src/public_types.ts b/src/public_types.ts index e64ae9bd7f1..a8b9f7b6589 100644 --- a/src/public_types.ts +++ b/src/public_types.ts @@ -636,6 +636,7 @@ export type IAudioTrackPreference = null | /** Single preference for a text track Adaptation. */ export type ITextTrackPreference = null | { language : string; + forced? : boolean | undefined; closedCaption : boolean; }; /** Single preference for a video track Adaptation. */ @@ -763,6 +764,7 @@ export interface IAudioTrack { language : string; export interface ITextTrack { language : string; normalized : string; closedCaption : boolean; + forced? : boolean | undefined; label? : string | undefined; id : number|string; } diff --git a/tests/contents/DASH_static_SegmentTimeline/forced-subtitles.js b/tests/contents/DASH_static_SegmentTimeline/forced-subtitles.js new file mode 100644 index 00000000000..f5eecb24d40 --- /dev/null +++ b/tests/contents/DASH_static_SegmentTimeline/forced-subtitles.js @@ -0,0 +1,23 @@ +const BASE_URL = "http://" + + /* eslint-disable no-undef */ + __TEST_CONTENT_SERVER__.URL + ":" + + __TEST_CONTENT_SERVER__.PORT + + /* eslint-enable no-undef */ + "/DASH_static_SegmentTimeline/media/"; +export default { + url: BASE_URL + "forced-subtitles.mpd", + transport: "dash", + isDynamic: false, + isLive: false, + minimumPosition: 0, + maximumPosition: 101.568367, + duration: 101.568367, + availabilityStartTime: 0, + + /** + * We don't care about that for now. As this content is only tested for track + * preferences. + * TODO still add it to our list of commonly tested contents? + */ + periods: [], +}; diff --git a/tests/contents/DASH_static_SegmentTimeline/index.js b/tests/contents/DASH_static_SegmentTimeline/index.js index f581990211a..c9c0ccb3d38 100644 --- a/tests/contents/DASH_static_SegmentTimeline/index.js +++ b/tests/contents/DASH_static_SegmentTimeline/index.js @@ -1,19 +1,20 @@ import manifestInfos from "./infos.js"; -import trickModeInfos from "./trickmode.js"; import discontinuityInfos from "./discontinuity.js"; +import forcedSubtitles from "./forced-subtitles.js"; import multiAdaptationSetsInfos from "./multi-AdaptationSets.js"; import multiPeriodDifferentChoicesInfos from "./multi_period_different_choices"; import multiPeriodSameChoicesInfos from "./multi_period_same_choices"; import notStartingAt0ManifestInfos from "./not_starting_at_0.js"; -import streamEventsInfos from "./event-stream"; import segmentTemplateInheritanceASRep from "./segment_template_inheritance_as_rep"; import segmentTemplateInheritancePeriodAS from "./segment_template_inheritance_period_as"; import segmentTimelineEndNumber from "./segment_timeline_end_number"; +import streamEventsInfos from "./event-stream"; +import trickModeInfos from "./trickmode.js"; export { manifestInfos, - trickModeInfos, discontinuityInfos, + forcedSubtitles, multiAdaptationSetsInfos, multiPeriodDifferentChoicesInfos, multiPeriodSameChoicesInfos, @@ -22,4 +23,5 @@ export { segmentTemplateInheritancePeriodAS, segmentTimelineEndNumber, streamEventsInfos, + trickModeInfos, }; diff --git a/tests/contents/DASH_static_SegmentTimeline/media/forced-subtitles.mpd b/tests/contents/DASH_static_SegmentTimeline/media/forced-subtitles.mpd new file mode 100644 index 00000000000..8373f1b7a5b --- /dev/null +++ b/tests/contents/DASH_static_SegmentTimeline/media/forced-subtitles.mpd @@ -0,0 +1,133 @@ + + + + + dash/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/contents/DASH_static_SegmentTimeline/urls.js b/tests/contents/DASH_static_SegmentTimeline/urls.js index b65753d7899..3278dd43821 100644 --- a/tests/contents/DASH_static_SegmentTimeline/urls.js +++ b/tests/contents/DASH_static_SegmentTimeline/urls.js @@ -91,6 +91,11 @@ module.exports = [ path: path.join(__dirname, "media/multi-AdaptationSets.mpd"), contentType: "application/dash+xml", }, + { + url: BASE_URL + "forced-subtitles.mpd", + path: path.join(__dirname, "media/forced-subtitles.mpd"), + contentType: "application/dash+xml", + }, { url: BASE_URL + "event-streams.mpd", path: path.join(__dirname, "media/event-streams.mpd"), diff --git a/tests/integration/scenarios/dash_forced-subtitles.js b/tests/integration/scenarios/dash_forced-subtitles.js new file mode 100644 index 00000000000..e162ce5e466 --- /dev/null +++ b/tests/integration/scenarios/dash_forced-subtitles.js @@ -0,0 +1,148 @@ +import { expect } from "chai"; +import RxPlayer from "../../../src"; +import { + forcedSubtitles, +} from "../../contents/DASH_static_SegmentTimeline"; +import XHRMock from "../../utils/request_mock"; +import { + waitForLoadedStateAfterLoadVideo, +} from "../../utils/waitForPlayerState"; + +describe("DASH forced-subtitles content (SegmentTimeline)", function () { + let player; + let xhrMock; + + async function loadContent() { + player.loadVideo({ url: forcedSubtitles.url, + transport: forcedSubtitles.transport }); + await waitForLoadedStateAfterLoadVideo(player); + } + + function checkNoTextTrack() { + const currentTextTrack = player.getTextTrack(); + expect(currentTextTrack).to.equal(null); + } + + function checkAudioTrack(language, normalizedLanguage, isAudioDescription) { + const currentAudioTrack = player.getAudioTrack(); + expect(currentAudioTrack).to.not.equal(null); + expect(currentAudioTrack.language).to.equal(language); + expect(currentAudioTrack.normalized).to.equal(normalizedLanguage); + expect(currentAudioTrack.audioDescription).to.equal(isAudioDescription); + } + + function checkTextTrack(language, normalizedLanguage, props) { + const currentTextTrack = player.getTextTrack(); + expect(currentTextTrack).to.not.equal(null); + expect(currentTextTrack.language).to.equal(language); + expect(currentTextTrack.normalized).to.equal(normalizedLanguage); + expect(currentTextTrack.closedCaption).to.equal( + props.closedCaption, + `"closedCaption" not set to "${props.closedCaption}" but ` + + `to "${currentTextTrack.closedCaption}"`); + expect(currentTextTrack.forced).to.equal( + props.forced, + `"forced" not set to "${props.forced}" but ` + + `to "${currentTextTrack.forced}"`); + } + + beforeEach(() => { + player = new RxPlayer(); + player.setWantedBufferAhead(5); // We don't really care + xhrMock = new XHRMock(); + }); + + afterEach(() => { + player.dispose(); + xhrMock.restore(); + }); + + it("should set the forced text track associated to the current audio track", async function () { + player.dispose(); + player = new RxPlayer({ + preferredAudioTracks: [{ + language: "fr", + audioDescription: false, + }], + }); + + await loadContent(); + checkAudioTrack("fr", "fra", false); + checkTextTrack("fr", "fra", { closedCaption: false, forced: true }); + + player.setPreferredAudioTracks([{ language: "de", audioDescription: false }]); + await loadContent(); + checkAudioTrack("de", "deu", false); + checkTextTrack("de", "deu", { closedCaption: false, forced: true }); + }); + + it("should set the forced text track associated to no language if none is linked to the audio track", async function () { + player.dispose(); + player = new RxPlayer({ + preferredAudioTracks: [{ + language: "en", + audioDescription: false, + }], + }); + + await loadContent(); + checkAudioTrack("en", "eng", false); + checkTextTrack("", "", { + closedCaption: false, + forced: true, + }); + }); + + it("should still prefer preferences over forced subtitles", async function () { + player.dispose(); + player = new RxPlayer({ + preferredAudioTracks: [{ + language: "fr", + audioDescription: false, + }], + preferredTextTracks: [{ + language: "fr", + closedCaption: false, + }], + }); + + await loadContent(); + checkAudioTrack("fr", "fra", false); + checkTextTrack("fr", "fra", { closedCaption: false, forced: undefined }); + + player.setPreferredTextTracks([{ language: "fr", closedCaption: true }]); + await loadContent(); + checkAudioTrack("fr", "fra", false); + checkTextTrack("fr", "fra", { closedCaption: true, forced: undefined }); + + player.setPreferredAudioTracks([{ language: "de", audioDescription: undefined }]); + await loadContent(); + checkAudioTrack("de", "deu", false); + checkTextTrack("fr", "fra", { closedCaption: true, forced: undefined }); + + player.setPreferredTextTracks([null]); + await loadContent(); + checkNoTextTrack(); + }); + + it("should fallback to forced subtitles if no preference match", async function () { + player.dispose(); + player = new RxPlayer({ + preferredAudioTracks: [{ + language: "fr", + audioDescription: false, + }], + preferredTextTracks: [{ + language: "swa", + closedCaption: false, + }, { + language: "de", + closedCaption: true, + }], + }); + + await loadContent(); + checkAudioTrack("fr", "fra", false); + checkTextTrack("fr", "fra", { closedCaption: false, forced: true }); + }); +});