refactor(core): centralize feature state types (#448)

This commit is contained in:
rahim
2026-02-04 15:08:37 +11:00
committed by GitHub
parent 18eb9107bf
commit 05af832632
6 changed files with 114 additions and 36 deletions
+101
View File
@@ -34,3 +34,104 @@ export interface PlaybackState {
*/
pause(): void;
}
/** Indicates whether a feature can be programmatically controlled on this platform. */
export type FeatureAvailability = 'available' | 'unavailable' | 'unsupported';
export interface VolumeState {
/**
* Volume level from 0 (silent) to 1 (max).
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volume
*/
volume: number;
/**
* Whether audio is muted.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/muted
*/
muted: boolean;
/**
* Whether volume can be programmatically set on this platform.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volume
*/
volumeAvailability: FeatureAvailability;
/**
* Set volume (clamped 0-1). Returns the clamped value.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volume
*/
changeVolume(volume: number): number;
/**
* Toggle mute state. Returns the new muted value.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/muted
*/
toggleMute(): boolean;
}
export interface TimeState {
/**
* Current playback position in seconds.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/currentTime
*/
currentTime: number;
/**
* Total duration in seconds (0 if unknown).
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/duration
*/
duration: number;
/**
* Whether a seek operation is in progress.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeking
*/
seeking: boolean;
/**
* Seek to a time in seconds. Returns the actual position after seek.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/currentTime
*/
seek(time: number): Promise<number>;
}
export interface SourceState {
/**
* Current media source URL (null if none).
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/currentSrc
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/src
*/
source: string | null;
/**
* Whether enough data is loaded to begin playback.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
*/
canPlay: boolean;
/**
* Load a new media source. Returns the new source URL.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/src
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/load
*/
loadSource(src: string): string;
}
export interface BufferState {
/**
* Buffered time ranges as [start, end] tuples.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/buffered
*/
buffered: [number, number][];
/**
* Seekable time ranges as [start, end] tuples.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seekable
*/
seekable: [number, number][];
}
+1 -1
View File
@@ -9,7 +9,7 @@ export interface PlayerTarget {
container: MediaContainer | null;
}
export type FeatureAvailability = 'available' | 'unavailable' | 'unsupported';
export type { FeatureAvailability } from '../../core/media/state';
export type PlayerFeature<State> = Slice<PlayerTarget, State>;
@@ -1,14 +1,12 @@
import type { InferSliceState } from '@videojs/store';
import { listen, serializeTimeRanges } from '@videojs/utils/dom';
import type { BufferState } from '../../../core/media/state';
import { definePlayerFeature } from '../../feature';
export const bufferFeature = definePlayerFeature({
state: () => ({
/** Buffered time ranges as [start, end] tuples. */
buffered: [] as [number, number][],
/** Seekable time ranges as [start, end] tuples. */
seekable: [] as [number, number][],
state: (): BufferState => ({
buffered: [],
seekable: [],
}),
attach({ target, signal, set }) {
@@ -26,5 +24,3 @@ export const bufferFeature = definePlayerFeature({
listen(media, 'emptied', sync, { signal });
},
});
export type BufferState = InferSliceState<typeof bufferFeature>;
@@ -1,15 +1,12 @@
import type { InferSliceState } from '@videojs/store';
import { listen } from '@videojs/utils/dom';
import type { SourceState } from '../../../core/media/state';
import { definePlayerFeature } from '../../feature';
export const sourceFeature = definePlayerFeature({
state: ({ target, abort }) => ({
/** Current media source URL (null if none). */
source: null as string | null,
/** Whether enough data is loaded to begin playback. */
state: ({ target, abort }): SourceState => ({
source: null,
canPlay: false,
/** Load a new media source. Returns the new source URL. */
loadSource(src: string) {
abort(); // Cancel pending operations (e.g., seek)
@@ -38,5 +35,3 @@ export const sourceFeature = definePlayerFeature({
listen(media, 'emptied', sync, { signal });
},
});
export type SourceState = InferSliceState<typeof sourceFeature>;
+2 -8
View File
@@ -1,11 +1,11 @@
import type { InferSliceState } from '@videojs/store';
import { listen, onEvent } from '@videojs/utils/dom';
import { noop } from '@videojs/utils/function';
import type { TimeState } from '../../../core/media/state';
import { definePlayerFeature } from '../../feature';
import { hasMetadata } from '../../media/predicate';
export const timeFeature = definePlayerFeature({
state: ({ target, signal }) => {
state: ({ target, signal }): TimeState => {
let abort: AbortController | null = null;
const supersede = () => {
@@ -15,13 +15,9 @@ export const timeFeature = definePlayerFeature({
};
return {
/** Current playback position in seconds. */
currentTime: 0,
/** Total duration in seconds (0 if unknown). */
duration: 0,
/** Whether a seek operation is in progress. */
seeking: false,
/** Seek to a time in seconds. Returns the actual position after seek. */
async seek(time: number) {
const { media } = target(),
signal = supersede();
@@ -62,5 +58,3 @@ export const timeFeature = definePlayerFeature({
listen(media, 'emptied', sync, { signal });
},
});
export type TimeState = InferSliceState<typeof timeFeature>;
+3 -11
View File
@@ -1,26 +1,20 @@
import type { InferSliceState } from '@videojs/store';
import { listen } from '@videojs/utils/dom';
import type { FeatureAvailability, VolumeState } from '../../../core/media/state';
import { definePlayerFeature } from '../../feature';
import type { FeatureAvailability } from '../../media/types';
export const volumeFeature = definePlayerFeature({
state: ({ target }) => ({
/** Volume level from 0 (silent) to 1 (max). */
state: ({ target }): VolumeState => ({
volume: 1,
/** Whether audio is muted. */
muted: false,
/** Whether volume can be programmatically set on this platform. */
volumeAvailability: 'unavailable' as FeatureAvailability,
volumeAvailability: 'unavailable',
/** Set volume (clamped 0-1). Returns the clamped value. */
changeVolume(volume: number) {
const { media } = target();
media.volume = Math.max(0, Math.min(1, volume));
return media.volume;
},
/** Toggle mute state. Returns new muted value. */
toggleMute() {
const { media } = target();
media.muted = !media.muted;
@@ -40,8 +34,6 @@ export const volumeFeature = definePlayerFeature({
},
});
export type VolumeState = InferSliceState<typeof volumeFeature>;
/** Check if volume can be programmatically set (fails on iOS Safari). */
function canSetVolume(): FeatureAvailability {
const video = document.createElement('video');