Skip to content

Commit

Permalink
Merge pull request #1187 from canalplus/feat/forced-subtitles
Browse files Browse the repository at this point in the history
DASH: implement forced-subtitles
  • Loading branch information
peaBerberian authored Dec 8, 2022
2 parents 466b5e3 + 6a2fd4d commit 516cccd
Show file tree
Hide file tree
Showing 15 changed files with 473 additions and 41 deletions.
15 changes: 15 additions & 0 deletions doc/api/Player_Events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down
6 changes: 6 additions & 0 deletions doc/api/Track_Selection/getAvailableTextTracks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 4 additions & 2 deletions doc/api/Track_Selection/getPreferredTextTracks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
```

Expand Down
6 changes: 6 additions & 0 deletions doc/api/Track_Selection/getTextTrack.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
16 changes: 12 additions & 4 deletions doc/api/Track_Selection/setPreferredTextTracks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```

Expand Down
67 changes: 50 additions & 17 deletions src/core/api/tracks_management/track_choice_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -116,6 +117,7 @@ function normalizeTextTracks(
return tracks.map(t => t === null ?
t :
{ normalized: normalizeLanguage(t.language),
forced: t.forced,
closedCaption: t.closedCaption });
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -645,13 +649,17 @@ export default class TrackChoiceManager {
return null;
}

return {
const formatted : ITextTrack = {
language: takeFirstSet<string>(chosenTextAdaptation.language, ""),
normalized: takeFirstSet<string>(chosenTextAdaptation.normalizedLanguage, ""),
closedCaption: chosenTextAdaptation.isClosedCaption === true,
id: chosenTextAdaptation.id,
label: chosenTextAdaptation.label,
};
if (chosenTextAdaptation.isForcedSubtitles !== undefined) {
formatted.forced = chosenTextAdaptation.isForcedSubtitles;
}
return formatted;
}

/**
Expand Down Expand Up @@ -768,15 +776,21 @@ export default class TrackChoiceManager {
null;

return textInfos.adaptations
.map((adaptation) => ({
language: takeFirstSet<string>(adaptation.language, ""),
normalized: takeFirstSet<string>(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<string>(adaptation.language, ""),
normalized: takeFirstSet<string>(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;
});
}

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1162,7 +1178,9 @@ function createTextPreferenceMatcher(
return takeFirstSet<string>(textAdaptation.normalizedLanguage,
"") === preferredTextTrack.normalized &&
(preferredTextTrack.closedCaption ? textAdaptation.isClosedCaption === true :
textAdaptation.isClosedCaption !== true);
textAdaptation.isClosedCaption !== true) &&
(preferredTextTrack.forced === true ? textAdaptation.isForcedSubtitles === true :
textAdaptation.isForcedSubtitles !== true);
};
}

Expand All @@ -1173,12 +1191,14 @@ function createTextPreferenceMatcher(
* `null` if the most optimal text adaptation is no text adaptation.
* @param {Array.<Object>} textAdaptations
* @param {Array.<Object|null>} 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;
}
Expand All @@ -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;
}
Expand Down
11 changes: 11 additions & 0 deletions src/manifest/adaptation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down
61 changes: 46 additions & 15 deletions src/parsers/manifest/dash/common/parse_adaptation_sets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.<Object>} accessibilities
* @param {Array.<Object>} 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;
}

/**
Expand All @@ -147,14 +162,13 @@ function hasSignLanguageInterpretation(
/**
* Contruct Adaptation ID from the information we have.
* @param {Object} adaptation
* @param {Array.<Object>} representations
* @param {Array.<Object>} 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;
Expand All @@ -165,6 +179,7 @@ function getAdaptationID(
}

const { isClosedCaption,
isForcedSubtitle,
isAudioDescription,
isSignInterpreted,
isTrickModeTrack,
Expand All @@ -177,6 +192,9 @@ function getAdaptationID(
if (isClosedCaption === true) {
idString += "-cc";
}
if (isForcedSubtitle === true) {
idString += "-cc";
}
if (isAudioDescription === true) {
idString += "-ad";
}
Expand Down Expand Up @@ -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;
Expand All @@ -385,6 +412,7 @@ export default function parseAdaptationSets(

let adaptationID = getAdaptationID(adaptation,
{ isAudioDescription,
isForcedSubtitle,
isClosedCaption,
isSignInterpreted,
isTrickModeTrack,
Expand Down Expand Up @@ -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;
}
Expand Down
7 changes: 7 additions & 0 deletions src/parsers/manifest/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Loading

0 comments on commit 516cccd

Please sign in to comment.