From 4a0b616f002c979da93dbcc4a6c327a11741e875 Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Wed, 4 Oct 2023 18:34:20 -0700 Subject: [PATCH 1/3] Use subtitle track name and language to find and match subtitle/captions TextTrack rather than subtitleTrack index Resolves #4571 --- src/controller/audio-track-controller.ts | 7 ++ src/controller/subtitle-track-controller.ts | 111 +++++++++++-------- src/controller/timeline-controller.ts | 47 +++++--- src/utils/texttrack-utils.ts | 17 +++ tests/unit/controller/timeline-controller.js | 28 ++++- 5 files changed, 143 insertions(+), 67 deletions(-) diff --git a/src/controller/audio-track-controller.ts b/src/controller/audio-track-controller.ts index 4f8b894ce2c..fdea6a382a8 100644 --- a/src/controller/audio-track-controller.ts +++ b/src/controller/audio-track-controller.ts @@ -277,6 +277,13 @@ class AudioTrackController extends BasePlaylistController { ) { return track.id; } + if ( + mediaAttributesIdentical(currentTrack.attrs, track.attrs, [ + 'LANGUAGE', + ]) + ) { + return track.id; + } } } return -1; diff --git a/src/controller/subtitle-track-controller.ts b/src/controller/subtitle-track-controller.ts index ae9bd57e785..c31a83f85d2 100644 --- a/src/controller/subtitle-track-controller.ts +++ b/src/controller/subtitle-track-controller.ts @@ -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'; @@ -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(); } } @@ -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) @@ -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; } @@ -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; + } } } @@ -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}`); @@ -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 }); @@ -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; diff --git a/src/controller/timeline-controller.ts b/src/controller/timeline-controller.ts index 4055b22bdb0..d5d0bc420d0 100644 --- a/src/controller/timeline-controller.ts +++ b/src/controller/timeline-controller.ts @@ -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, @@ -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; } } @@ -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) { @@ -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; } @@ -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) => { @@ -744,13 +762,14 @@ export class TimelineController implements ComponentAPI { } function canReuseVttTextTrack( - inUseTrack: (TextTrack & { textTrack1?; textTrack2? }) | null, - manifestTrack: MediaPlaylist, + inUseTrack: TextTrack | null, + manifestTrack: Pick, ): 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) ); } diff --git a/src/utils/texttrack-utils.ts b/src/utils/texttrack-utils.ts index a07e1d1512b..b1a164ed29a 100644 --- a/src/utils/texttrack-utils.ts +++ b/src/utils/texttrack-utils.ts @@ -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; +} diff --git a/tests/unit/controller/timeline-controller.js b/tests/unit/controller/timeline-controller.js index d4ebd45ed6d..b6fcda03113 100644 --- a/tests/unit/controller/timeline-controller.js +++ b/tests/unit/controller/timeline-controller.js @@ -62,8 +62,18 @@ 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' }, + }, ], }); @@ -78,8 +88,18 @@ 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' }, + }, ], }); From 6562c19079abad041c19852480436906c759e753 Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Fri, 6 Oct 2023 09:50:27 -0700 Subject: [PATCH 2/3] Fix regression in dev where AUDIO_TRACKS_UPDATED is re-dispatched with zero tracks Allow selection of audioTrack on AUDIO_TRACKS_UPDATED (overriding default selection - Resolves #5831) Cleanup selection of subtitleTrack on SUBTITLE_TRACKS_UPDATED (overriding default selection) --- src/controller/audio-track-controller.ts | 94 ++++++++++----------- src/controller/subtitle-track-controller.ts | 29 ++++--- src/utils/media-option-attributes.ts | 2 +- 3 files changed, 63 insertions(+), 62 deletions(-) diff --git a/src/controller/audio-track-controller.ts b/src/controller/audio-track-controller.ts index fdea6a382a8..5a8f6a44a26 100644 --- a/src/controller/audio-track-controller.ts +++ b/src/controller/audio-track-controller.ts @@ -58,10 +58,10 @@ class AudioTrackController extends BasePlaylistController { protected onManifestLoading(): void { this.tracks = []; - this.groupIds = null; this.tracksInGroup = []; - this.trackId = -1; + this.groupIds = null; this.currentTrack = null; + this.trackId = -1; this.selectDefaultTrack = true; } @@ -113,40 +113,48 @@ class AudioTrackController extends BasePlaylistController { private switchLevel(levelIndex: number) { const levelInfo = this.hls.levels[levelIndex]; - if (!levelInfo) { return; } - const audioGroups = levelInfo.audioGroups || null; const currentGroups = this.groupIds; + const currentTrack = this.currentTrack; if ( !audioGroups || currentGroups?.length !== audioGroups?.length || audioGroups?.some((groupId) => currentGroups?.indexOf(groupId) === -1) ) { this.groupIds = audioGroups; + this.trackId = -1; + this.currentTrack = null; const audioTracks = this.tracks.filter( (track): boolean => !audioGroups || audioGroups.indexOf(track.groupId) !== -1, ); - - // Disable selectDefaultTrack if there are no default tracks - if ( - this.selectDefaultTrack && - !audioTracks.some((track) => track.default) - ) { - this.selectDefaultTrack = false; - } - if (!audioTracks.length) { - this.trackId = -1; - this.currentTrack = null; + if (audioTracks.length) { + // Disable selectDefaultTrack if there are no default tracks + if ( + this.selectDefaultTrack && + !audioTracks.some((track) => track.default) + ) { + this.selectDefaultTrack = false; + } + // track.id should match hls.audioTracks index + audioTracks.forEach((track, i) => { + track.id = i; + }); + } else if (!currentTrack && !this.tracksInGroup.length) { + // Do not dispatch AUDIO_TRACKS_UPDATED when there were and are no tracks + return; } - audioTracks.forEach((track, i) => { - track.id = i; - }); + this.tracksInGroup = audioTracks; + let trackId = this.findTrackId(currentTrack); + if (trackId === -1 && currentTrack) { + trackId = this.findTrackId(null); + } + const audioTracksUpdated: AudioTracksUpdatedData = { audioTracks }; this.log( `Updating audio tracks, ${ @@ -155,8 +163,25 @@ class AudioTrackController extends BasePlaylistController { ); this.hls.trigger(Events.AUDIO_TRACKS_UPDATED, audioTracksUpdated); - this.selectInitialTrack(); - } else if (this.shouldReloadPlaylist(this.currentTrack)) { + const selectedTrackId = this.trackId; + if (trackId !== -1 && selectedTrackId === -1) { + this.setAudioTrack(trackId); + } else if (audioTracks.length && selectedTrackId === -1) { + const error = new Error( + `No audio track selected for current audio group-ID(s): ${this.groupIds?.join( + ',', + )} track count: ${audioTracks.length}`, + ); + this.warn(error.message); + + this.hls.trigger(Events.ERROR, { + type: ErrorTypes.MEDIA_ERROR, + details: ErrorDetails.AUDIO_TRACK_LOAD_ERROR, + fatal: true, + error, + }); + } + } else if (this.shouldReloadPlaylist(currentTrack)) { // Retry playlist loading if no playlist is or has been loaded yet this.setAudioTrack(this.trackId); } @@ -228,35 +253,6 @@ class AudioTrackController extends BasePlaylistController { this.loadPlaylist(hlsUrlParameters); } - private selectInitialTrack(): void { - const audioTracks = this.tracksInGroup; - if (!audioTracks.length) { - return; - } - let trackId = this.findTrackId(this.currentTrack); - if (trackId === -1) { - trackId = this.findTrackId(null); - } - - if (trackId !== -1) { - this.setAudioTrack(trackId); - } else { - const error = new Error( - `No track found for running audio group-ID(s): ${this.groupIds?.join( - ',', - )} track count: ${audioTracks.length}`, - ); - this.warn(error.message); - - this.hls.trigger(Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.AUDIO_TRACK_LOAD_ERROR, - fatal: true, - error, - }); - } - } - private findTrackId(currentTrack: MediaPlaylist | null): number { const audioTracks = this.tracksInGroup; for (let i = 0; i < audioTracks.length; i++) { diff --git a/src/controller/subtitle-track-controller.ts b/src/controller/subtitle-track-controller.ts index c31a83f85d2..c6f5f2f12da 100644 --- a/src/controller/subtitle-track-controller.ts +++ b/src/controller/subtitle-track-controller.ts @@ -204,7 +204,7 @@ class SubtitleTrackController extends BasePlaylistController { private switchLevel(levelIndex: number) { const levelInfo = this.hls.levels[levelIndex]; - if (!levelInfo?.textGroupIds) { + if (!levelInfo) { return; } const subtitleGroups = levelInfo.subtitleGroups || null; @@ -216,22 +216,27 @@ class SubtitleTrackController extends BasePlaylistController { subtitleGroups?.some((groupId) => currentGroups?.indexOf(groupId) === -1) ) { this.groupIds = subtitleGroups; + this.trackId = -1; + this.currentTrack = null; const subtitleTracks = this.tracks.filter( (track): boolean => !subtitleGroups || subtitleGroups.indexOf(track.groupId) !== -1, ); - if (!subtitleTracks.length) { - this.trackId = -1; - this.currentTrack = null; + if (subtitleTracks.length) { + // track.id should match hls.audioTracks index + subtitleTracks.forEach((track, i) => { + track.id = i; + }); + } else if (!currentTrack && !this.tracksInGroup.length) { + // Do not dispatch SUBTITLE_TRACKS_UPDATED when there were and are no tracks + return; } - subtitleTracks.forEach((track, i) => { - track.id = i; - }); + this.tracksInGroup = subtitleTracks; - let initialTrackId = this.findTrackId(currentTrack); - if (initialTrackId === -1) { - initialTrackId = this.findTrackId(null); + let trackId = this.findTrackId(currentTrack); + if (trackId === -1 && currentTrack) { + trackId = this.findTrackId(null); } const subtitleTracksUpdated: SubtitleTracksUpdatedData = { @@ -244,8 +249,8 @@ class SubtitleTrackController extends BasePlaylistController { ); this.hls.trigger(Events.SUBTITLE_TRACKS_UPDATED, subtitleTracksUpdated); - if (initialTrackId !== -1) { - this.setSubtitleTrack(initialTrackId); + if (trackId !== -1 && this.trackId === -1) { + this.setSubtitleTrack(trackId); } } else if (this.shouldReloadPlaylist(currentTrack)) { // Retry playlist loading if no playlist is or has been loaded yet diff --git a/src/utils/media-option-attributes.ts b/src/utils/media-option-attributes.ts index 25c24eb7efa..2c31307b7a8 100644 --- a/src/utils/media-option-attributes.ts +++ b/src/utils/media-option-attributes.ts @@ -35,12 +35,12 @@ export function mediaAttributesIdentical( return !( customAttributes || [ 'LANGUAGE', - 'ASSOC-LANGUAGE', 'NAME', 'CHARACTERISTICS', 'AUTOSELECT', 'DEFAULT', 'FORCED', + 'ASSOC-LANGUAGE', ] ).some( (subtitleAttribute) => From 230a7d8e3840f166036f2c1071c0a83ce6268fda Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Fri, 6 Oct 2023 11:54:47 -0700 Subject: [PATCH 3/3] Fix TextTrack use with ASSOC-LANGUAGE Match the initial default/forced/autoselect choice made when playing HLS in Safari --- src/controller/audio-track-controller.ts | 45 ++++++++--------- src/controller/subtitle-track-controller.ts | 38 ++++++++++---- src/controller/timeline-controller.ts | 56 ++++++++++++--------- src/utils/media-option-attributes.ts | 12 +++++ 4 files changed, 95 insertions(+), 56 deletions(-) diff --git a/src/controller/audio-track-controller.ts b/src/controller/audio-track-controller.ts index 5a8f6a44a26..e365169e570 100644 --- a/src/controller/audio-track-controller.ts +++ b/src/controller/audio-track-controller.ts @@ -257,29 +257,28 @@ class AudioTrackController extends BasePlaylistController { const audioTracks = this.tracksInGroup; for (let i = 0; i < audioTracks.length; i++) { const track = audioTracks[i]; - if (!this.selectDefaultTrack || track.default) { - if ( - !currentTrack || - mediaAttributesIdentical(currentTrack.attrs, track.attrs) - ) { - return track.id; - } - if ( - mediaAttributesIdentical(currentTrack.attrs, track.attrs, [ - 'LANGUAGE', - 'ASSOC-LANGUAGE', - 'CHARACTERISTICS', - ]) - ) { - return track.id; - } - if ( - mediaAttributesIdentical(currentTrack.attrs, track.attrs, [ - 'LANGUAGE', - ]) - ) { - return track.id; - } + if (this.selectDefaultTrack && !track.default) { + continue; + } + if ( + !currentTrack || + mediaAttributesIdentical(currentTrack.attrs, track.attrs) + ) { + return track.id; + } + if ( + mediaAttributesIdentical(currentTrack.attrs, track.attrs, [ + 'LANGUAGE', + 'ASSOC-LANGUAGE', + 'CHARACTERISTICS', + ]) + ) { + return track.id; + } + if ( + mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE']) + ) { + return track.id; } } return -1; diff --git a/src/controller/subtitle-track-controller.ts b/src/controller/subtitle-track-controller.ts index c6f5f2f12da..3436ee9d4b7 100644 --- a/src/controller/subtitle-track-controller.ts +++ b/src/controller/subtitle-track-controller.ts @@ -5,7 +5,10 @@ import { filterSubtitleTracks, } from '../utils/texttrack-utils'; import { PlaylistContextType } from '../types/loader'; -import { mediaAttributesIdentical } from '../utils/media-option-attributes'; +import { + mediaAttributesIdentical, + subtitleTrackMatchesTextTrack, +} from '../utils/media-option-attributes'; import type Hls from '../hls'; import type { MediaPlaylist } from '../types/media-playlist'; import type { HlsUrlParameters } from '../types/level'; @@ -224,6 +227,15 @@ class SubtitleTrackController extends BasePlaylistController { !subtitleGroups || subtitleGroups.indexOf(track.groupId) !== -1, ); if (subtitleTracks.length) { + // Disable selectDefaultTrack if there are no default tracks + if ( + this.selectDefaultTrack && + !subtitleTracks.some( + (track) => track.default || track.forced || track.autoselect, + ) + ) { + this.selectDefaultTrack = false; + } // track.id should match hls.audioTracks index subtitleTracks.forEach((track, i) => { track.id = i; @@ -260,8 +272,18 @@ class SubtitleTrackController extends BasePlaylistController { private findTrackId(currentTrack: MediaPlaylist | null): number { const tracks = this.tracksInGroup; + const selectDefault = this.selectDefaultTrack; for (let i = 0; i < tracks.length; i++) { const track = tracks[i]; + if ( + (selectDefault && + !track.default && + !track.forced && + !track.autoselect) || + (!selectDefault && !currentTrack) + ) { + continue; + } if ( !currentTrack || mediaAttributesIdentical(currentTrack.attrs, track.attrs) @@ -291,10 +313,7 @@ class SubtitleTrackController extends BasePlaylistController { 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 - ) { + if (subtitleTrackMatchesTextTrack(track, textTrack)) { return i; } } @@ -376,13 +395,12 @@ class SubtitleTrackController extends BasePlaylistController { const currentTrack = this.currentTrack; let nextTrack; if (currentTrack) { - const { name, lang } = currentTrack; - nextTrack = textTracks.filter( - (track) => track.label === name && track.language === lang, + nextTrack = textTracks.filter((textTrack) => + subtitleTrackMatchesTextTrack(currentTrack, textTrack), )[0]; if (!nextTrack) { this.warn( - `Unable to find subtitle TextTrack with name "${name}" and language "${lang}"`, + `Unable to find subtitle TextTrack with name "${currentTrack.name}" and language "${currentTrack.lang}"`, ); } } @@ -424,6 +442,7 @@ class SubtitleTrackController extends BasePlaylistController { // stopping live reloading timer if any this.clearTimer(); + this.selectDefaultTrack = false; const lastTrack = this.currentTrack; const track: MediaPlaylist | null = tracks[newId] || null; this.trackId = newId; @@ -480,7 +499,6 @@ class SubtitleTrackController extends BasePlaylistController { // Find internal track index for TextTrack const trackId = this.findTrackForTextTrack(textTrack); if (this.subtitleTrack !== trackId) { - this.selectDefaultTrack = false; this.setSubtitleTrack(trackId); } } diff --git a/src/controller/timeline-controller.ts b/src/controller/timeline-controller.ts index d5d0bc420d0..4fe852d8a77 100644 --- a/src/controller/timeline-controller.ts +++ b/src/controller/timeline-controller.ts @@ -9,7 +9,10 @@ import { removeCuesInRange, filterSubtitleTracks, } from '../utils/texttrack-utils'; -import { subtitleOptionsIdentical } from '../utils/media-option-attributes'; +import { + subtitleOptionsIdentical, + subtitleTrackMatchesTextTrack, +} 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'; @@ -213,7 +216,13 @@ export class TimelineController implements ComponentAPI { if (media) { for (let i = 0; i < media.textTracks.length; i++) { const textTrack = media.textTracks[i]; - if (canReuseVttTextTrack(textTrack, { name: label, lang: language })) { + if ( + canReuseVttTextTrack(textTrack, { + name: label, + lang: language, + attrs: {} as any, + }) + ) { return textTrack; } } @@ -380,8 +389,7 @@ export class TimelineController implements ComponentAPI { if (textTrack) { clearCurrentCues(textTrack); } else { - const textTrackKind = - this._captionsOrSubtitlesFromCharacteristics(track); + const textTrackKind = captionsOrSubtitlesFromCharacteristics(track); textTrack = this.createTextTrack( textTrackKind, track.name, @@ -425,21 +433,6 @@ export class TimelineController implements ComponentAPI { } } - private _captionsOrSubtitlesFromCharacteristics( - track: MediaPlaylist, - ): TextTrackKind { - if (track.characteristics) { - if ( - /transcribes-spoken-dialog/gi.test(track.characteristics) && - /describes-music-and-sound/gi.test(track.characteristics) - ) { - return 'captions'; - } - } - - return 'subtitles'; - } - private onManifestLoaded( event: Events.MANIFEST_LOADED, data: ManifestLoadedData, @@ -761,15 +754,32 @@ export class TimelineController implements ComponentAPI { } } +function captionsOrSubtitlesFromCharacteristics( + track: Pick, +): TextTrackKind { + if (track.characteristics) { + if ( + /transcribes-spoken-dialog/gi.test(track.characteristics) && + /describes-music-and-sound/gi.test(track.characteristics) + ) { + return 'captions'; + } + } + + return 'subtitles'; +} + function canReuseVttTextTrack( inUseTrack: TextTrack | null, - manifestTrack: Pick, + manifestTrack: Pick< + MediaPlaylist, + 'name' | 'lang' | 'attrs' | 'characteristics' + >, ): boolean { return ( !!inUseTrack && - (inUseTrack.kind === 'subtitles' || inUseTrack.kind === 'captions') && - inUseTrack.label === manifestTrack.name && - inUseTrack.language.substring(0, 2) === manifestTrack.lang?.substring(0, 2) + inUseTrack.kind === captionsOrSubtitlesFromCharacteristics(manifestTrack) && + subtitleTrackMatchesTextTrack(manifestTrack, inUseTrack) ); } diff --git a/src/utils/media-option-attributes.ts b/src/utils/media-option-attributes.ts index 2c31307b7a8..ff298f16015 100644 --- a/src/utils/media-option-attributes.ts +++ b/src/utils/media-option-attributes.ts @@ -47,3 +47,15 @@ export function mediaAttributesIdentical( attrs1[subtitleAttribute] !== attrs2[subtitleAttribute], ); } + +export function subtitleTrackMatchesTextTrack( + subtitleTrack: Pick, + textTrack: TextTrack, +) { + return ( + textTrack.label.toLowerCase() === subtitleTrack.name.toLowerCase() && + (!textTrack.language || + textTrack.language.toLowerCase() === + (subtitleTrack.lang || '').toLowerCase()) + ); +}