mirror of
https://github.com/zoriya/v10.git
synced 2026-08-05 05:37:21 +00:00
fix: textTrackList and optimize (#760)
This commit is contained in:
@@ -235,8 +235,8 @@ export interface MediaTextTrackState {
|
||||
thumbnailCues: MediaTextCue[];
|
||||
/** The `<track>` 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<TextTrackKind>[];
|
||||
/** Whether captions/subtitles are currently enabled. */
|
||||
subtitlesShowing: boolean;
|
||||
/** Toggle captions/subtitles visibility. Returns the new enabled value. */
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ function createMediaState(overrides: Partial<MediaTextTrackState> = {}): 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');
|
||||
});
|
||||
|
||||
@@ -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<PlayerTarget>()(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' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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<TextTrackKind>[] = [];
|
||||
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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<string | number, unknown>)[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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user