diff --git a/packages/core/src/core/media/state.ts b/packages/core/src/core/media/state.ts index 85a8b6a3..2f5bfbbe 100644 --- a/packages/core/src/core/media/state.ts +++ b/packages/core/src/core/media/state.ts @@ -235,8 +235,8 @@ export interface MediaTextTrackState { thumbnailCues: MediaTextCue[]; /** The `` element's `src` for resolving relative cue text URLs. */ thumbnailTrackSrc: string | null; - /** Caption/subtitle tracks that can be selected or toggled. */ - subtitlesList: MediaTextTrack<'subtitles' | 'captions'>[]; + /** All text tracks available on the media element. */ + textTrackList: MediaTextTrack[]; /** Whether captions/subtitles are currently enabled. */ subtitlesShowing: boolean; /** Toggle captions/subtitles visibility. Returns the new enabled value. */ diff --git a/packages/core/src/core/ui/captions-button/captions-button-core.ts b/packages/core/src/core/ui/captions-button/captions-button-core.ts index d957ef2b..707b9396 100644 --- a/packages/core/src/core/ui/captions-button/captions-button-core.ts +++ b/packages/core/src/core/ui/captions-button/captions-button-core.ts @@ -60,7 +60,9 @@ export class CaptionsButtonCore { const media = this.#media!; return { subtitlesShowing: media.subtitlesShowing, - availability: media.subtitlesList.length > 0 ? 'available' : 'unavailable', + availability: media.textTrackList.some((t) => t.kind === 'captions' || t.kind === 'subtitles') + ? 'available' + : 'unavailable', }; } diff --git a/packages/core/src/core/ui/captions-button/tests/captions-button-core.test.ts b/packages/core/src/core/ui/captions-button/tests/captions-button-core.test.ts index 54c6996f..9fc72f82 100644 --- a/packages/core/src/core/ui/captions-button/tests/captions-button-core.test.ts +++ b/packages/core/src/core/ui/captions-button/tests/captions-button-core.test.ts @@ -9,7 +9,7 @@ function createMediaState(overrides: Partial = {}): MediaTe chaptersCues: [], thumbnailCues: [], thumbnailTrackSrc: null, - subtitlesList: [], + textTrackList: [], subtitlesShowing: false, toggleSubtitles: vi.fn(() => true), ...overrides, @@ -30,7 +30,7 @@ describe('CaptionsButtonCore', () => { const core = new CaptionsButtonCore(); const media = createMediaState({ subtitlesShowing: true, - subtitlesList: [{ kind: 'subtitles', label: 'English', language: 'en', mode: 'showing' }], + textTrackList: [{ kind: 'subtitles', label: 'English', language: 'en', mode: 'showing' }], }); core.setMedia(media); const state = core.getState(); @@ -42,7 +42,10 @@ describe('CaptionsButtonCore', () => { const core = new CaptionsButtonCore(); core.setMedia( createMediaState({ - subtitlesList: [{ kind: 'subtitles', label: 'English', language: 'en', mode: 'disabled' }], + textTrackList: [ + { kind: 'subtitles', label: 'English', language: 'en', mode: 'disabled' }, + { kind: 'metadata', label: 'thumbnails', language: '', mode: 'hidden' }, + ], }) ); @@ -51,7 +54,9 @@ describe('CaptionsButtonCore', () => { it('returns unavailable when no subtitles', () => { const core = new CaptionsButtonCore(); - core.setMedia(createMediaState({ subtitlesList: [] })); + core.setMedia( + createMediaState({ textTrackList: [{ kind: 'metadata', label: 'thumbnails', language: '', mode: 'hidden' }] }) + ); expect(core.getState().availability).toBe('unavailable'); }); diff --git a/packages/core/src/dom/store/features/tests/text-track.test.ts b/packages/core/src/dom/store/features/tests/text-track.test.ts index 326960fc..a042dc6b 100644 --- a/packages/core/src/dom/store/features/tests/text-track.test.ts +++ b/packages/core/src/dom/store/features/tests/text-track.test.ts @@ -43,7 +43,7 @@ describe('textTrackFeature', () => { expect(store.state.chaptersCues).toEqual([]); expect(store.state.thumbnailCues).toEqual([]); expect(store.state.thumbnailTrackSrc).toBeNull(); - expect(store.state.subtitlesList).toEqual([]); + expect(store.state.textTrackList).toEqual([]); expect(store.state.subtitlesShowing).toBe(false); }); }); @@ -149,18 +149,20 @@ describe('textTrackFeature', () => { expect(store.state.subtitlesShowing).toBe(true); }); - it('exposes subtitlesList from captions/subtitles tracks', () => { + it('exposes textTrackList for all track kinds', () => { const video = createVideo(); const subtitlesTrack = { kind: 'subtitles', mode: 'showing', label: 'English', language: 'en' } as TextTrack; const captionsTrack = { kind: 'captions', mode: 'disabled', label: 'CC', language: 'en' } as TextTrack; - mockTextTracks(video, [subtitlesTrack, captionsTrack, createMockTrack('metadata', 'showing')]); + const metadataTrack = createMockTrack('metadata', 'showing'); + mockTextTracks(video, [subtitlesTrack, captionsTrack, metadataTrack]); const store = createStore()(textTrackFeature); store.attach({ media: video, container: null }); - expect(store.state.subtitlesList).toEqual([ + expect(store.state.textTrackList).toEqual([ { kind: 'subtitles', label: 'English', language: 'en', mode: 'showing' }, { kind: 'captions', label: 'CC', language: 'en', mode: 'disabled' }, + { kind: 'metadata', label: '', language: '', mode: 'showing' }, ]); }); diff --git a/packages/core/src/dom/store/features/text-track.ts b/packages/core/src/dom/store/features/text-track.ts index c6d8a732..1a0a5312 100644 --- a/packages/core/src/dom/store/features/text-track.ts +++ b/packages/core/src/dom/store/features/text-track.ts @@ -1,4 +1,4 @@ -import { findTrackElement, getSubtitlesTracks, listen } from '@videojs/utils/dom'; +import { findTrackElement, getTextTrackList, listen } from '@videojs/utils/dom'; import type { MediaTextCue, MediaTextTrack, MediaTextTrackState } from '../../../core/media/state'; import { definePlayerFeature } from '../../feature'; @@ -9,10 +9,13 @@ export const textTrackFeature = definePlayerFeature({ chaptersCues: [], thumbnailCues: [], thumbnailTrackSrc: null, - subtitlesList: [], + textTrackList: [], subtitlesShowing: false, toggleSubtitles(forceShow?: boolean) { - const subtitlesTracks = getSubtitlesTracks(target().media); + const subtitlesTracks = getTextTrackList( + target().media, + (track) => track.kind === 'subtitles' || track.kind === 'captions' + ); if (!subtitlesTracks.length) return false; const showing = subtitlesTracks.some((track: TextTrack) => track.mode === 'showing'); @@ -37,25 +40,23 @@ export const textTrackFeature = definePlayerFeature({ let chaptersTrack: TextTrack | null = null; let thumbnailTrack: TextTrack | null = null; - const subtitlesList: MediaTextTrack<'subtitles' | 'captions'>[] = []; + const textTrackList: MediaTextTrack[] = []; let subtitlesShowing = false; for (let i = 0; i < media.textTracks.length; i++) { const track = media.textTracks[i]!; if (!chaptersTrack && track.kind === 'chapters') chaptersTrack = track; if (!thumbnailTrack && track.kind === 'metadata' && track.label === 'thumbnails') thumbnailTrack = track; - if (track.kind === 'captions' || track.kind === 'subtitles') { - const showing = track.mode === 'showing'; - subtitlesList.push({ - kind: track.kind, - label: track.label, - language: track.language, - mode: track.mode, - }); - if (showing) { - subtitlesShowing = true; - } + textTrackList.push({ + kind: track.kind, + label: track.label, + language: track.language, + mode: track.mode, + }); + + if ((track.kind === 'captions' || track.kind === 'subtitles') && track.mode === 'showing') { + subtitlesShowing = true; } } @@ -83,7 +84,7 @@ export const textTrackFeature = definePlayerFeature({ } } - set({ chaptersCues, thumbnailCues, thumbnailTrackSrc, subtitlesList, subtitlesShowing }); + set({ chaptersCues, thumbnailCues, thumbnailTrackSrc, textTrackList, subtitlesShowing }); } sync(); diff --git a/packages/utils/src/dom/index.ts b/packages/utils/src/dom/index.ts index e6817346..4841958f 100644 --- a/packages/utils/src/dom/index.ts +++ b/packages/utils/src/dom/index.ts @@ -14,6 +14,6 @@ export { supportsIdleCallback, supportsPopoverAPI, } from './supports'; -export { findTrackElement, getSubtitlesTracks, getTextTracksList } from './text-track'; +export { findTrackElement, getTextTrackList } from './text-track'; export { serializeTimeRanges } from './time-ranges'; export type { CustomElement, CustomElementCallbacks } from './types'; diff --git a/packages/utils/src/dom/text-track.ts b/packages/utils/src/dom/text-track.ts index 9b92e88e..f05dfa01 100644 --- a/packages/utils/src/dom/text-track.ts +++ b/packages/utils/src/dom/text-track.ts @@ -6,45 +6,11 @@ export function findTrackElement(media: HTMLMediaElement, track: TextTrack): HTM return null; } -export function getSubtitlesTracks(media: HTMLMediaElement): TextTrack[] { - return getTextTracksList(media, isSubtitleTrack).sort(sortByTextTrackKind); -} - -export function getTextTracksList( - media: HTMLMediaElement, - filterPredOrObj: ((textTrack: TextTrack) => boolean) | TextTrack = alwaysTrue -): TextTrack[] { +export function getTextTrackList(media: HTMLMediaElement, filterPred: (textTrack: TextTrack) => boolean): TextTrack[] { if (!media?.textTracks) return []; - - const filterPred = typeof filterPredOrObj === 'function' ? filterPredOrObj : textTrackObjAsPred(filterPredOrObj); - - return (Array.from(media.textTracks) as TextTrack[]).filter(filterPred); -} - -export function textTrackObjAsPred(filterObj: any): (textTrack: TextTrack) => boolean { - const preds = Object.entries(filterObj).map(([key, value]) => { - // Translate each key/value pair into a single predicate - return isMatchingPropOf(key, value); - }); - - // Return a predicate function that takes the array of single key/value pair predicates and asserts that *every* pred in the array is true of the (TextTrack-like) object - return (textTrack) => preds.every((pred) => pred(textTrack)); -} - -export function isMatchingPropOf(key: string | number, value: any): (candidate: TextTrack) => boolean { - return function matchProp(candidate): boolean { - return (candidate as unknown as Record)[key] === value; - }; -} - -function isSubtitleTrack(textTrack: TextTrack): boolean { - return ['subtitles', 'captions'].includes(textTrack.kind); + return (Array.from(media.textTracks) as TextTrack[]).filter(filterPred).sort(sortByTextTrackKind); } function sortByTextTrackKind(a: TextTrack, b: TextTrack): number { return a.kind >= b.kind ? 1 : -1; } - -function alwaysTrue(): boolean { - return true; -}