mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(packages): add audio tracks menu (#1714)
This commit is contained in:
@@ -6,6 +6,8 @@ export * from './ui/airplay-button/airplay-button-core';
|
||||
export * from './ui/airplay-button/airplay-button-data-attrs';
|
||||
export * from './ui/alert-dialog/alert-dialog-core';
|
||||
export * from './ui/alert-dialog/alert-dialog-data-attrs';
|
||||
export * from './ui/audio-track-radio-group/audio-track-radio-group-core';
|
||||
export * from './ui/audio-track-radio-group/audio-track-radio-group-data-attrs';
|
||||
export * from './ui/buffering-indicator/buffering-indicator-core';
|
||||
export * from './ui/buffering-indicator/buffering-indicator-data-attrs';
|
||||
export * from './ui/captions-button/captions-button-core';
|
||||
|
||||
@@ -2,6 +2,7 @@ import { isFunction, isObject, isUndefined } from '@videojs/utils/predicate';
|
||||
|
||||
import { EMPTY_REMOTE, EMPTY_TEXT_TRACKS, EMPTY_TIME_RANGES } from './constants';
|
||||
import type {
|
||||
MediaAudioTrackCapability,
|
||||
MediaBufferCapability,
|
||||
MediaErrorCapability,
|
||||
MediaLiveCapability,
|
||||
@@ -85,6 +86,12 @@ export function isMediaVideoRenditionCapable(value: unknown): value is MediaVide
|
||||
return !isUndefined(media.videoRenditions);
|
||||
}
|
||||
|
||||
export function isMediaAudioTrackCapable(value: unknown): value is MediaAudioTrackCapability {
|
||||
if (!isObject(value)) return false;
|
||||
const media = value as Record<string, unknown>;
|
||||
return !isUndefined(media.audioTracks);
|
||||
}
|
||||
|
||||
export function isMediaVideoDimensionsCapable(value: unknown): value is MediaVideoDimensionsCapability {
|
||||
if (!isObject(value)) return false;
|
||||
const media = value as Record<string, unknown>;
|
||||
|
||||
@@ -249,6 +249,21 @@ export interface MediaQualityState {
|
||||
selectVideoRendition(value: string): void;
|
||||
}
|
||||
|
||||
export interface MediaAudioTrack {
|
||||
id?: string;
|
||||
kind?: string;
|
||||
label: string;
|
||||
language: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface MediaAudioTrackState {
|
||||
/** Audio tracks available for manual track selection. */
|
||||
audioTrackList: MediaAudioTrack[];
|
||||
/** Select an audio track by menu value. */
|
||||
selectAudioTrack(value: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A text cue.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { createState } from '@videojs/store';
|
||||
import { defaults } from '@videojs/utils/object';
|
||||
import { isFunction } from '@videojs/utils/predicate';
|
||||
import type { NonNullableObject } from '@videojs/utils/types';
|
||||
|
||||
import type { MediaAudioTrack, MediaAudioTrackState } from '../../media/state';
|
||||
import type { ButtonState } from '../types';
|
||||
|
||||
export interface AudioTrackRadioGroupProps {
|
||||
/** Custom label for the options group. */
|
||||
label?: string | ((state: AudioTrackRadioGroupState) => string) | undefined;
|
||||
/** Custom formatter for visible track labels. */
|
||||
formatTrack?: ((track: MediaAudioTrack) => string) | undefined;
|
||||
/** Whether audio track selection is disabled. */
|
||||
disabled?: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface AudioTrackRadioGroupTrack {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface AudioTrackRadioGroupState extends ButtonState {
|
||||
tracks: readonly AudioTrackRadioGroupTrack[];
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
availability: 'available' | 'unavailable';
|
||||
}
|
||||
|
||||
function formatTrackLabel(track: MediaAudioTrack): string {
|
||||
if (track.label) return track.label;
|
||||
if (track.language) return track.language;
|
||||
if (track.kind) return track.kind;
|
||||
return 'Audio';
|
||||
}
|
||||
|
||||
function getTrackValue(track: MediaAudioTrack, index: number): string {
|
||||
return track.id || String(index);
|
||||
}
|
||||
|
||||
export class AudioTrackRadioGroupCore {
|
||||
static readonly defaultProps: NonNullableObject<AudioTrackRadioGroupProps> = {
|
||||
label: '',
|
||||
formatTrack: formatTrackLabel,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
readonly state = createState<AudioTrackRadioGroupState>({
|
||||
tracks: [],
|
||||
value: '',
|
||||
disabled: false,
|
||||
availability: 'unavailable',
|
||||
label: '',
|
||||
});
|
||||
|
||||
#props = { ...AudioTrackRadioGroupCore.defaultProps };
|
||||
#media: MediaAudioTrackState | null = null;
|
||||
|
||||
constructor(props?: AudioTrackRadioGroupProps) {
|
||||
if (props) this.setProps(props);
|
||||
}
|
||||
|
||||
setProps(props: AudioTrackRadioGroupProps): void {
|
||||
this.#props = defaults(props, AudioTrackRadioGroupCore.defaultProps);
|
||||
}
|
||||
|
||||
getLabel(state: AudioTrackRadioGroupState): string {
|
||||
const { label } = this.#props;
|
||||
|
||||
if (isFunction(label)) {
|
||||
const customLabel = label(state);
|
||||
if (customLabel) return customLabel;
|
||||
} else if (label) {
|
||||
return label;
|
||||
}
|
||||
|
||||
return 'Audio';
|
||||
}
|
||||
|
||||
getTrackLabel(track: MediaAudioTrack): string {
|
||||
return this.#props.formatTrack(track);
|
||||
}
|
||||
|
||||
getAttrs(state: AudioTrackRadioGroupState) {
|
||||
return {
|
||||
'aria-label': this.getLabel(state),
|
||||
'aria-disabled': state.disabled ? 'true' : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
setMedia(media: MediaAudioTrackState): void {
|
||||
this.#media = media;
|
||||
}
|
||||
|
||||
getState(): AudioTrackRadioGroupState {
|
||||
const media = this.#media!;
|
||||
const enabledIndex = media.audioTrackList.findIndex((track) => track.enabled);
|
||||
const tracks = media.audioTrackList.map((track, index) => ({
|
||||
value: getTrackValue(track, index),
|
||||
label: this.getTrackLabel(track),
|
||||
}));
|
||||
const availability: AudioTrackRadioGroupState['availability'] = tracks.length > 1 ? 'available' : 'unavailable';
|
||||
|
||||
this.state.patch({
|
||||
tracks,
|
||||
value: enabledIndex === -1 ? '' : getTrackValue(media.audioTrackList[enabledIndex]!, enabledIndex),
|
||||
disabled: this.#props.disabled || availability === 'unavailable',
|
||||
availability,
|
||||
});
|
||||
this.state.patch({ label: this.getLabel(this.state.current) });
|
||||
|
||||
return this.state.current;
|
||||
}
|
||||
|
||||
select(media: MediaAudioTrackState, value: string): void {
|
||||
if (this.#props.disabled) return;
|
||||
|
||||
const hasValue = media.audioTrackList.some((track, index) => getTrackValue(track, index) === value);
|
||||
if (!hasValue) return;
|
||||
|
||||
media.selectAudioTrack(value);
|
||||
}
|
||||
|
||||
selectValue(media: MediaAudioTrackState, value: string): void {
|
||||
this.select(media, value);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace AudioTrackRadioGroupCore {
|
||||
export type Props = AudioTrackRadioGroupProps;
|
||||
export type State = AudioTrackRadioGroupState;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import type { StateAttrMap } from '../types';
|
||||
import type { AudioTrackRadioGroupState } from './audio-track-radio-group-core';
|
||||
|
||||
export const AudioTrackRadioGroupDataAttrs = {
|
||||
/** Current audio track value. */
|
||||
value: 'data-audio-track',
|
||||
/** Present when audio track selection is disabled. */
|
||||
disabled: 'data-disabled',
|
||||
/** Indicates audio track availability (`available` or `unavailable`). */
|
||||
availability: 'data-availability',
|
||||
} as const satisfies StateAttrMap<AudioTrackRadioGroupState>;
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { MediaAudioTrackState } from '../../../media/state';
|
||||
import type { AudioTrackRadioGroupState } from '../audio-track-radio-group-core';
|
||||
import { AudioTrackRadioGroupCore } from '../audio-track-radio-group-core';
|
||||
|
||||
function createMediaState(overrides: Partial<MediaAudioTrackState> = {}): MediaAudioTrackState {
|
||||
return {
|
||||
audioTrackList: [
|
||||
{ id: '0', kind: 'main', label: 'English', language: 'en', enabled: true },
|
||||
{ id: '1', kind: 'alternative', label: 'Spanish', language: 'es', enabled: false },
|
||||
],
|
||||
selectAudioTrack: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createState(overrides: Partial<AudioTrackRadioGroupState> = {}): AudioTrackRadioGroupState {
|
||||
return {
|
||||
tracks: [
|
||||
{ value: '0', label: 'English' },
|
||||
{ value: '1', label: 'Spanish' },
|
||||
],
|
||||
value: '0',
|
||||
disabled: false,
|
||||
availability: 'available',
|
||||
label: '',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('AudioTrackRadioGroupCore', () => {
|
||||
describe('getState', () => {
|
||||
it('projects audio tracks', () => {
|
||||
const core = new AudioTrackRadioGroupCore();
|
||||
const media = createMediaState();
|
||||
core.setMedia(media);
|
||||
|
||||
const state = core.getState();
|
||||
|
||||
expect(state.tracks).toEqual([
|
||||
{ value: '0', label: 'English' },
|
||||
{ value: '1', label: 'Spanish' },
|
||||
]);
|
||||
expect(state.value).toBe('0');
|
||||
});
|
||||
|
||||
it('falls back to language, kind, then Audio labels', () => {
|
||||
const core = new AudioTrackRadioGroupCore();
|
||||
const media = createMediaState({
|
||||
audioTrackList: [
|
||||
{ id: '0', kind: 'main', label: '', language: 'en', enabled: true },
|
||||
{ id: '1', kind: 'commentary', label: '', language: '', enabled: false },
|
||||
{ id: '2', label: '', language: '', enabled: false },
|
||||
],
|
||||
});
|
||||
core.setMedia(media);
|
||||
|
||||
expect(core.getState().tracks).toEqual([
|
||||
{ value: '0', label: 'en' },
|
||||
{ value: '1', label: 'commentary' },
|
||||
{ value: '2', label: 'Audio' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses index values when ids are missing', () => {
|
||||
const core = new AudioTrackRadioGroupCore();
|
||||
const media = createMediaState({
|
||||
audioTrackList: [
|
||||
{ label: 'English', language: 'en', enabled: false },
|
||||
{ label: 'Spanish', language: 'es', enabled: true },
|
||||
],
|
||||
});
|
||||
core.setMedia(media);
|
||||
|
||||
expect(core.getState().tracks.map((track) => track.value)).toEqual(['0', '1']);
|
||||
expect(core.getState().value).toBe('1');
|
||||
});
|
||||
|
||||
it('marks availability unavailable with one track', () => {
|
||||
const core = new AudioTrackRadioGroupCore();
|
||||
core.setMedia(
|
||||
createMediaState({ audioTrackList: [{ id: '0', label: 'English', language: 'en', enabled: true }] })
|
||||
);
|
||||
|
||||
expect(core.getState().availability).toBe('unavailable');
|
||||
expect(core.getState().disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLabel', () => {
|
||||
it('returns the default label', () => {
|
||||
const core = new AudioTrackRadioGroupCore();
|
||||
expect(core.getLabel(createState())).toBe('Audio');
|
||||
});
|
||||
|
||||
it('returns a custom string label', () => {
|
||||
const core = new AudioTrackRadioGroupCore({ label: 'Audio tracks' });
|
||||
expect(core.getLabel(createState())).toBe('Audio tracks');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTrackLabel', () => {
|
||||
it('uses a custom formatter', () => {
|
||||
const core = new AudioTrackRadioGroupCore({
|
||||
formatTrack: (track) => `${track.language}: ${track.label}`,
|
||||
});
|
||||
|
||||
expect(core.getTrackLabel({ label: 'English', language: 'en', enabled: false })).toBe('en: English');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectValue', () => {
|
||||
it('selects a known track', () => {
|
||||
const core = new AudioTrackRadioGroupCore();
|
||||
const media = createMediaState();
|
||||
|
||||
core.selectValue(media, '1');
|
||||
|
||||
expect(media.selectAudioTrack).toHaveBeenCalledWith('1');
|
||||
});
|
||||
|
||||
it('does nothing for an unknown track', () => {
|
||||
const core = new AudioTrackRadioGroupCore();
|
||||
const media = createMediaState();
|
||||
|
||||
core.selectValue(media, '3');
|
||||
|
||||
expect(media.selectAudioTrack).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does nothing when disabled', () => {
|
||||
const core = new AudioTrackRadioGroupCore({ disabled: true });
|
||||
const media = createMediaState();
|
||||
|
||||
core.selectValue(media, '1');
|
||||
|
||||
expect(media.selectAudioTrack).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AnySlice, Slice, Store, UnionSliceState } from '@videojs/store';
|
||||
import type {
|
||||
MediaAudioTrackState,
|
||||
MediaBufferState,
|
||||
MediaControlsState,
|
||||
MediaErrorState,
|
||||
@@ -44,6 +45,7 @@ export type VideoFeatures = [
|
||||
PlayerFeature<MediaPlaybackState>,
|
||||
PlayerFeature<MediaPlaybackRateState>,
|
||||
PlayerFeature<MediaQualityState>,
|
||||
PlayerFeature<MediaAudioTrackState>,
|
||||
PlayerFeature<MediaVolumeState>,
|
||||
PlayerFeature<MediaTimeState>,
|
||||
PlayerFeature<MediaSourceState>,
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { listen } from '@videojs/utils/dom';
|
||||
import { isMediaAudioTrackCapable } from '../../../core/media/predicate';
|
||||
import type { MediaAudioTrack, MediaAudioTrackState } from '../../../core/media/state';
|
||||
import type { AudioTrackLike, AudioTrackListLike } from '../../../core/media/types';
|
||||
import { definePlayerFeature } from '../../feature';
|
||||
|
||||
function getTrackValue(track: AudioTrackLike, index: number): string {
|
||||
return track.id || String(index);
|
||||
}
|
||||
|
||||
function toMediaTrack(track: AudioTrackLike): MediaAudioTrack {
|
||||
return {
|
||||
...(track.id !== undefined && { id: track.id }),
|
||||
...(track.kind !== undefined && { kind: track.kind }),
|
||||
label: track.label,
|
||||
language: track.language,
|
||||
enabled: track.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
export const audioTrackFeature = definePlayerFeature({
|
||||
name: 'audioTrack',
|
||||
state: ({ target }): MediaAudioTrackState => ({
|
||||
audioTrackList: [],
|
||||
selectAudioTrack(value: string) {
|
||||
const { media } = target();
|
||||
if (!isMediaAudioTrackCapable(media)) return;
|
||||
|
||||
const tracks = [...media.audioTracks];
|
||||
const track = tracks.find((candidate, index) => getTrackValue(candidate, index) === value);
|
||||
if (!track) return;
|
||||
|
||||
for (const candidate of tracks) {
|
||||
candidate.enabled = candidate === track;
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
attach({ target, signal, set }) {
|
||||
const { media } = target;
|
||||
let audioTracks: AudioTrackListLike | null = null;
|
||||
let cleanup: AbortController | null = null;
|
||||
|
||||
const getAudioTracks = () => (isMediaAudioTrackCapable(media) ? media.audioTracks : null);
|
||||
const sync = (list = getAudioTracks()) => {
|
||||
set({ audioTrackList: list ? [...list].map(toMediaTrack) : [] });
|
||||
};
|
||||
|
||||
const bind = () => {
|
||||
const nextAudioTracks = getAudioTracks();
|
||||
|
||||
if (nextAudioTracks === audioTracks) {
|
||||
sync(nextAudioTracks);
|
||||
return;
|
||||
}
|
||||
|
||||
cleanup?.abort();
|
||||
cleanup = new AbortController();
|
||||
audioTracks = nextAudioTracks;
|
||||
|
||||
if (audioTracks) {
|
||||
listen(audioTracks, 'addtrack', () => sync(audioTracks), { signal: cleanup.signal });
|
||||
listen(audioTracks, 'removetrack', () => sync(audioTracks), { signal: cleanup.signal });
|
||||
listen(audioTracks, 'change', () => sync(audioTracks), { signal: cleanup.signal });
|
||||
}
|
||||
|
||||
sync(audioTracks);
|
||||
};
|
||||
|
||||
bind();
|
||||
|
||||
listen(media, 'loadstart', bind, { signal });
|
||||
signal.addEventListener('abort', () => cleanup?.abort(), { once: true });
|
||||
},
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { audioTrackFeature } from './audio-track';
|
||||
import { bufferFeature } from './buffer';
|
||||
import { controlsFeature } from './controls';
|
||||
import { fullscreenFeature } from './fullscreen';
|
||||
@@ -18,6 +19,7 @@ export { audioFeatures, backgroundFeatures, videoFeatures } from './presets';
|
||||
|
||||
// Short aliases
|
||||
export {
|
||||
audioTrackFeature as audioTrack,
|
||||
bufferFeature as buffer,
|
||||
controlsFeature as controls,
|
||||
fullscreenFeature as fullscreen,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './audio-track';
|
||||
export * from './buffer';
|
||||
export * from './controls';
|
||||
export * from './error';
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
LiveVideoFeatures,
|
||||
VideoFeatures,
|
||||
} from '../../media/types';
|
||||
import { audioTrackFeature } from './audio-track';
|
||||
import { bufferFeature } from './buffer';
|
||||
import { controlsFeature } from './controls';
|
||||
import { errorFeature } from './error';
|
||||
@@ -24,6 +25,7 @@ export const videoFeatures: VideoFeatures = [
|
||||
playbackFeature,
|
||||
playbackRateFeature,
|
||||
qualityFeature,
|
||||
audioTrackFeature,
|
||||
volumeFeature,
|
||||
timeFeature,
|
||||
sourceFeature,
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { createStore } from '@videojs/store';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { AudioTrackLike } from '../../../../core/media/types';
|
||||
import type { PlayerTarget } from '../../../media/types';
|
||||
import { audioTrackFeature } from '../audio-track';
|
||||
|
||||
class TestAudioTrackList extends EventTarget {
|
||||
tracks: AudioTrackLike[];
|
||||
|
||||
constructor(tracks: AudioTrackLike[]) {
|
||||
super();
|
||||
this.tracks = tracks;
|
||||
}
|
||||
|
||||
[Symbol.iterator](): Iterator<AudioTrackLike> {
|
||||
return this.tracks.values();
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this.tracks.length;
|
||||
}
|
||||
}
|
||||
|
||||
class TestMedia extends EventTarget {
|
||||
audioTracks: TestAudioTrackList | undefined = undefined;
|
||||
|
||||
constructor(tracks?: AudioTrackLike[]) {
|
||||
super();
|
||||
if (tracks) this.audioTracks = new TestAudioTrackList(tracks);
|
||||
}
|
||||
|
||||
async play() {}
|
||||
}
|
||||
|
||||
function createTrack(overrides: Partial<AudioTrackLike>): AudioTrackLike {
|
||||
return {
|
||||
id: undefined,
|
||||
kind: undefined,
|
||||
label: '',
|
||||
language: '',
|
||||
enabled: false,
|
||||
addRendition: () => ({ id: undefined, bitrate: undefined, codec: undefined, selected: false }),
|
||||
removeRendition: () => {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createMedia(tracks: AudioTrackLike[]): PlayerTarget['media'] {
|
||||
return new TestMedia(tracks) as unknown as PlayerTarget['media'];
|
||||
}
|
||||
|
||||
describe('audioTrackFeature', () => {
|
||||
it('syncs audio tracks on attach', () => {
|
||||
const media = createMedia([
|
||||
createTrack({ id: '0', kind: 'main', label: 'English', language: 'en', enabled: true }),
|
||||
createTrack({ id: '1', kind: 'alternative', label: 'Spanish', language: 'es' }),
|
||||
]);
|
||||
const store = createStore<PlayerTarget>()(audioTrackFeature);
|
||||
|
||||
store.attach({ media, container: null });
|
||||
|
||||
expect(store.state.audioTrackList).toEqual([
|
||||
{ id: '0', kind: 'main', label: 'English', language: 'en', enabled: true },
|
||||
{ id: '1', kind: 'alternative', label: 'Spanish', language: 'es', enabled: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it('syncs audio tracks after loadstart', () => {
|
||||
const media = new TestMedia() as unknown as PlayerTarget['media'];
|
||||
const store = createStore<PlayerTarget>()(audioTrackFeature);
|
||||
|
||||
store.attach({ media, container: null });
|
||||
|
||||
expect(store.state.audioTrackList).toEqual([]);
|
||||
|
||||
const list = new TestAudioTrackList([createTrack({ id: '0', label: 'English', enabled: true })]);
|
||||
(media as unknown as TestMedia).audioTracks = list;
|
||||
media.dispatchEvent(new Event('loadstart'));
|
||||
|
||||
expect(store.state.audioTrackList).toEqual([{ id: '0', label: 'English', language: '', enabled: true }]);
|
||||
|
||||
list.tracks.push(createTrack({ id: '1', label: 'Spanish' }));
|
||||
list.dispatchEvent(new Event('addtrack'));
|
||||
|
||||
expect(store.state.audioTrackList).toEqual([
|
||||
{ id: '0', label: 'English', language: '', enabled: true },
|
||||
{ id: '1', label: 'Spanish', language: '', enabled: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it('resyncs on audio track change', () => {
|
||||
const media = createMedia([createTrack({ id: '0', label: 'English' }), createTrack({ id: '1', label: 'Spanish' })]);
|
||||
const store = createStore<PlayerTarget>()(audioTrackFeature);
|
||||
store.attach({ media, container: null });
|
||||
|
||||
(media as any).audioTracks.tracks[1].enabled = true;
|
||||
(media as any).audioTracks.dispatchEvent(new Event('change'));
|
||||
|
||||
expect(store.state.audioTrackList[1]?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('selects an audio track exclusively', () => {
|
||||
const media = createMedia([
|
||||
createTrack({ id: '0', label: 'English', enabled: true }),
|
||||
createTrack({ id: '1', label: 'Spanish' }),
|
||||
]);
|
||||
const store = createStore<PlayerTarget>()(audioTrackFeature);
|
||||
store.attach({ media, container: null });
|
||||
|
||||
store.state.selectAudioTrack('1');
|
||||
|
||||
expect([...((media as any).audioTracks as TestAudioTrackList)].map((track) => track.enabled)).toEqual([
|
||||
false,
|
||||
true,
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores unknown audio track values', () => {
|
||||
const media = createMedia([createTrack({ id: '0', label: 'English', enabled: true })]);
|
||||
const store = createStore<PlayerTarget>()(audioTrackFeature);
|
||||
store.attach({ media, container: null });
|
||||
|
||||
store.state.selectAudioTrack('missing');
|
||||
|
||||
expect([...((media as any).audioTracks as TestAudioTrackList)].map((track) => track.enabled)).toEqual([true]);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createSelector } from '@videojs/store';
|
||||
|
||||
import { audioTrackFeature } from './features/audio-track';
|
||||
import { bufferFeature } from './features/buffer';
|
||||
import { controlsFeature } from './features/controls';
|
||||
import { errorFeature } from './features/error';
|
||||
@@ -16,6 +17,8 @@ import { textTrackFeature } from './features/text-track';
|
||||
import { timeFeature } from './features/time';
|
||||
import { volumeFeature } from './features/volume';
|
||||
|
||||
/** Select the audio track state (audioTrackList, selectAudioTrack). */
|
||||
export const selectAudioTrack = createSelector(audioTrackFeature);
|
||||
/** Select the buffer state (buffered ranges, percent buffered). */
|
||||
export const selectBuffer = createSelector(bufferFeature);
|
||||
/** Select the controls state (controls visible, user-active). */
|
||||
|
||||
Reference in New Issue
Block a user