Skip to content

Commit

Permalink
Use subtitle track name and language to find and match subtitle/capti…
Browse files Browse the repository at this point in the history
…ons TextTrack rather than subtitleTrack index

Resolves #4571
  • Loading branch information
robwalch committed Oct 5, 2023
1 parent 3ecc666 commit e48894c
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 67 deletions.
7 changes: 7 additions & 0 deletions src/controller/audio-track-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,13 @@ class AudioTrackController extends BasePlaylistController {
) {
return track.id;
}
if (
mediaAttributesIdentical(currentTrack.attrs, track.attrs, [
'LANGUAGE',
])
) {
return track.id;
}
}
}
return -1;
Expand Down
111 changes: 62 additions & 49 deletions src/controller/subtitle-track-controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import BasePlaylistController from './base-playlist-controller';
import { Events } from '../events';
import { clearCurrentCues } from '../utils/texttrack-utils';
import {
clearCurrentCues,
filterSubtitleTracks,
} from '../utils/texttrack-utils';
import { PlaylistContextType } from '../types/loader';
import { mediaAttributesIdentical } from '../utils/media-option-attributes';
import type Hls from '../hls';
Expand Down Expand Up @@ -52,7 +55,7 @@ class SubtitleTrackController extends BasePlaylistController {
public set subtitleDisplay(value: boolean) {
this._subtitleDisplay = value;
if (this.trackId > -1) {
this.toggleTrackModes(this.trackId);
this.toggleTrackModes();
}
}

Expand Down Expand Up @@ -251,9 +254,9 @@ class SubtitleTrackController extends BasePlaylistController {
}

private findTrackId(currentTrack: MediaPlaylist | null): number {
const textTracks = this.tracksInGroup;
for (let i = 0; i < textTracks.length; i++) {
const track = textTracks[i];
const tracks = this.tracksInGroup;
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i];
if (
!currentTrack ||
mediaAttributesIdentical(currentTrack.attrs, track.attrs)
Expand All @@ -269,6 +272,27 @@ class SubtitleTrackController extends BasePlaylistController {
) {
return track.id;
}
if (
mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE'])
) {
return track.id;
}
}
return -1;
}

private findTrackForTextTrack(textTrack: TextTrack | null): number {
if (textTrack) {
const tracks = this.tracksInGroup;
for (let i = 0; i < tracks.length; i++) {
const track = tracks[i];
if (
textTrack.label === track.name &&
textTrack.language === track.lang
) {
return i;
}
}
}
return -1;
}
Expand Down Expand Up @@ -337,31 +361,36 @@ class SubtitleTrackController extends BasePlaylistController {
* This operates on the DOM textTracks.
* A value of -1 will disable all subtitle tracks.
*/
private toggleTrackModes(newId: number): void {
const { media, trackId } = this;
private toggleTrackModes(): void {
const { media } = this;
if (!media) {
return;
}

const textTracks = filterSubtitleTracks(media.textTracks);
const groupIds = this.groupIds;
const groupTracks = textTracks.filter(
(track) => !groupIds || groupIds.indexOf((track as any).groupId) !== -1,
);
if (newId === -1) {
[].slice.call(textTracks).forEach((track) => {
track.mode = 'disabled';
});
} else {
const oldTrack = groupTracks[trackId];
if (oldTrack) {
oldTrack.mode = 'disabled';
const currentTrack = this.currentTrack;
let nextTrack;
if (currentTrack) {
const { name, lang } = currentTrack;
nextTrack = textTracks.filter(
(track) => track.label === name && track.language === lang,
)[0];
if (!nextTrack) {
this.warn(
`Unable to find subtitle TextTrack with name "${name}" and language "${lang}"`,
);
}
}

const nextTrack = groupTracks[newId];
[].slice.call(textTracks).forEach((track) => {
if (track.mode !== 'disabled' && track !== nextTrack) {
track.mode = 'disabled';
}
});
if (nextTrack) {
nextTrack.mode = this.subtitleDisplay ? 'showing' : 'hidden';
const mode = this.subtitleDisplay ? 'showing' : 'hidden';
if (nextTrack.mode !== mode) {
nextTrack.mode = mode;
}
}
}

Expand All @@ -381,10 +410,6 @@ class SubtitleTrackController extends BasePlaylistController {
return;
}

if (this.trackId !== newId) {
this.toggleTrackModes(newId);
}

// exit if track id as already set or invalid
if (newId < -1 || newId >= tracks.length) {
this.warn(`Invalid subtitle track id: ${newId}`);
Expand All @@ -398,6 +423,7 @@ class SubtitleTrackController extends BasePlaylistController {
const track: MediaPlaylist | null = tracks[newId] || null;
this.trackId = newId;
this.currentTrack = track;
this.toggleTrackModes();
if (!track) {
// switch to -1
this.hls.trigger(Events.SUBTITLE_TRACK_SWITCH, { id: newId });
Expand Down Expand Up @@ -434,38 +460,25 @@ class SubtitleTrackController extends BasePlaylistController {
return;
}

let trackId: number = -1;
let textTrack: TextTrack | null = null;
const tracks = filterSubtitleTracks(this.media.textTracks);
for (let id = 0; id < tracks.length; id++) {
if (tracks[id].mode === 'hidden') {
for (let i = 0; i < tracks.length; i++) {
if (tracks[i].mode === 'hidden') {
// Do not break in case there is a following track with showing.
trackId = id;
} else if (tracks[id].mode === 'showing') {
trackId = id;
textTrack = tracks[i];
} else if (tracks[i].mode === 'showing') {
textTrack = tracks[i];
break;
}
}

// Setting current subtitleTrack will invoke code.
// Find internal track index for TextTrack
const trackId = this.findTrackForTextTrack(textTrack);
if (this.subtitleTrack !== trackId) {
this.subtitleTrack = trackId;
}
}
}

function filterSubtitleTracks(textTrackList: TextTrackList): TextTrack[] {
const tracks: TextTrack[] = [];
for (let i = 0; i < textTrackList.length; i++) {
const track = textTrackList[i];
// Edge adds a track without a label; we don't want to use it
if (
(track.kind === 'subtitles' || track.kind === 'captions') &&
track.label
) {
tracks.push(textTrackList[i]);
this.selectDefaultTrack = false;
this.setSubtitleTrack(trackId);
}
}
return tracks;
}

export default SubtitleTrackController;
47 changes: 33 additions & 14 deletions src/controller/timeline-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import {
clearCurrentCues,
addCueToTrack,
removeCuesInRange,
filterSubtitleTracks,
} from '../utils/texttrack-utils';
import { subtitleOptionsIdentical } from '../utils/media-option-attributes';
import { parseIMSC1, IMSC1_CODEC } from '../utils/imsc1-ttml-parser';
import { appendUint8Array } from '../utils/mp4-tools';
import { PlaylistLevelType } from '../types/loader';
import { Fragment } from '../loader/fragment';
import {
import type {
FragParsingUserdataData,
FragLoadedData,
FragDecryptedData,
Expand Down Expand Up @@ -207,12 +208,12 @@ export class TimelineController implements ComponentAPI {
}
}

private getExistingTrack(trackName: string): TextTrack | null {
private getExistingTrack(label: string, language: string): TextTrack | null {
const { media } = this;
if (media) {
for (let i = 0; i < media.textTracks.length; i++) {
const textTrack = media.textTracks[i];
if (textTrack[trackName]) {
if (canReuseVttTextTrack(textTrack, { name: label, lang: language })) {
return textTrack;
}
}
Expand All @@ -235,7 +236,7 @@ export class TimelineController implements ComponentAPI {
const { captionsProperties, captionsTracks, media } = this;
const { label, languageCode } = captionsProperties[trackName];
// Enable reuse of existing text track.
const existingTrack = this.getExistingTrack(trackName);
const existingTrack = this.getExistingTrack(label, languageCode);
if (!existingTrack) {
const textTrack = this.createTextTrack('captions', label, languageCode);
if (textTrack) {
Expand Down Expand Up @@ -352,21 +353,26 @@ export class TimelineController implements ComponentAPI {
this.tracks = tracks;

if (this.config.renderTextTracksNatively) {
const inUseTracks = this.media ? this.media.textTracks : null;
const media = this.media;
const inUseTracks: (TextTrack | null)[] | null = media
? filterSubtitleTracks(media.textTracks)
: null;

this.tracks.forEach((track, index) => {
// Reuse tracks with the same label and lang, but do not reuse 608/708 tracks
let textTrack: TextTrack | undefined;
if (inUseTracks && index < inUseTracks.length) {
if (inUseTracks) {
let inUseTrack: TextTrack | null = null;

for (let i = 0; i < inUseTracks.length; i++) {
if (canReuseVttTextTrack(inUseTracks[i], track)) {
if (
inUseTracks[i] &&
canReuseVttTextTrack(inUseTracks[i], track)
) {
inUseTrack = inUseTracks[i];
inUseTracks[i] = null;
break;
}
}

// Reuse tracks with the same label, but do not reuse 608/708 tracks
if (inUseTrack) {
textTrack = inUseTrack;
}
Expand All @@ -386,10 +392,22 @@ export class TimelineController implements ComponentAPI {
}
}
if (textTrack) {
(textTrack as any).groupId = track.groupId;
this.textTracks.push(textTrack);
}
});
// Warn when video element has captions or subtitle TextTracks carried over from another source
if (inUseTracks?.length) {
const unusedTextTracks = inUseTracks
.filter((t) => t !== null)
.map((t) => (t as TextTrack).label);
if (unusedTextTracks.length) {
logger.warn(
`Media element contains unused subtitle tracks: ${unusedTextTracks.join(
', ',
)}. Replace media element for each source to clear TextTracks and captions menu.`,
);
}
}
} else if (this.tracks.length) {
// Create a list of tracks for the provider to consume
const tracksList = this.tracks.map((track) => {
Expand Down Expand Up @@ -744,13 +762,14 @@ export class TimelineController implements ComponentAPI {
}

function canReuseVttTextTrack(
inUseTrack: (TextTrack & { textTrack1?; textTrack2? }) | null,
manifestTrack: MediaPlaylist,
inUseTrack: TextTrack | null,
manifestTrack: Pick<MediaPlaylist, 'name' | 'lang'>,
): boolean {
return (
!!inUseTrack &&
(inUseTrack.kind === 'subtitles' || inUseTrack.kind === 'captions') &&
inUseTrack.label === manifestTrack.name &&
!(inUseTrack.textTrack1 || inUseTrack.textTrack2)
inUseTrack.language.substring(0, 2) === manifestTrack.lang?.substring(0, 2)
);
}

Expand Down
17 changes: 17 additions & 0 deletions src/utils/texttrack-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,20 @@ export function getCuesInRange(
}
return cuesFound;
}

export function filterSubtitleTracks(
textTrackList: TextTrackList,
): TextTrack[] {
const tracks: TextTrack[] = [];
for (let i = 0; i < textTrackList.length; i++) {
const track = textTrackList[i];
// Edge adds a track without a label; we don't want to use it
if (
(track.kind === 'subtitles' || track.kind === 'captions') &&
track.label
) {
tracks.push(textTrackList[i]);
}
}
return tracks;
}
8 changes: 4 additions & 4 deletions tests/unit/controller/timeline-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ describe('TimelineController', function () {

timelineController.onSubtitleTracksUpdated(Events.MANIFEST_LOADED, {
subtitleTracks: [
{ id: 0, name: 'en', attrs: { LANGUAGE: 'en', NAME: 'en' } },
{ id: 1, name: 'ru', attrs: { LANGUAGE: 'ru', NAME: 'ru' } },
{ id: 0, name: 'en', lang: 'en', attrs: { LANGUAGE: 'en', NAME: 'en' } },
{ id: 1, name: 'ru', lang: 'ru', attrs: { LANGUAGE: 'ru', NAME: 'ru' } },
],
});

Expand All @@ -78,8 +78,8 @@ describe('TimelineController', function () {

timelineController.onSubtitleTracksUpdated(Events.MANIFEST_LOADED, {
subtitleTracks: [
{ id: 0, name: 'ru', attrs: { LANGUAGE: 'ru', NAME: 'ru' } },
{ id: 1, name: 'en', attrs: { LANGUAGE: 'en', NAME: 'en' } },
{ id: 0, name: 'ru', lang: 'ru', attrs: { LANGUAGE: 'ru', NAME: 'ru' } },
{ id: 1, name: 'en', lang: 'en', attrs: { LANGUAGE: 'en', NAME: 'en' } },
],
});

Expand Down

0 comments on commit e48894c

Please sign in to comment.