From 05af832632ebdaa4a0ee24caa0b3a7fa23598f2e Mon Sep 17 00:00:00 2001 From: rahim Date: Wed, 4 Feb 2026 15:08:37 +1100 Subject: [PATCH] refactor(core): centralize feature state types (#448) --- packages/core/src/core/media/state.ts | 101 ++++++++++++++++++ packages/core/src/dom/media/types.ts | 2 +- .../core/src/dom/store/features/buffer.ts | 12 +-- .../core/src/dom/store/features/source.ts | 11 +- packages/core/src/dom/store/features/time.ts | 10 +- .../core/src/dom/store/features/volume.ts | 14 +-- 6 files changed, 114 insertions(+), 36 deletions(-) diff --git a/packages/core/src/core/media/state.ts b/packages/core/src/core/media/state.ts index a4ad1b6c..4cf0798a 100644 --- a/packages/core/src/core/media/state.ts +++ b/packages/core/src/core/media/state.ts @@ -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; +} + +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][]; +} diff --git a/packages/core/src/dom/media/types.ts b/packages/core/src/dom/media/types.ts index 5b1179ba..7e3077f4 100644 --- a/packages/core/src/dom/media/types.ts +++ b/packages/core/src/dom/media/types.ts @@ -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 = Slice; diff --git a/packages/core/src/dom/store/features/buffer.ts b/packages/core/src/dom/store/features/buffer.ts index 2c765132..94f8ed1e 100644 --- a/packages/core/src/dom/store/features/buffer.ts +++ b/packages/core/src/dom/store/features/buffer.ts @@ -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; diff --git a/packages/core/src/dom/store/features/source.ts b/packages/core/src/dom/store/features/source.ts index ff81757f..22de198a 100644 --- a/packages/core/src/dom/store/features/source.ts +++ b/packages/core/src/dom/store/features/source.ts @@ -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; diff --git a/packages/core/src/dom/store/features/time.ts b/packages/core/src/dom/store/features/time.ts index 47cc99dd..2d951ac2 100644 --- a/packages/core/src/dom/store/features/time.ts +++ b/packages/core/src/dom/store/features/time.ts @@ -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; diff --git a/packages/core/src/dom/store/features/volume.ts b/packages/core/src/dom/store/features/volume.ts index 450234cd..bf6e99f5 100644 --- a/packages/core/src/dom/store/features/volume.ts +++ b/packages/core/src/dom/store/features/volume.ts @@ -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; - /** Check if volume can be programmatically set (fails on iOS Safari). */ function canSetVolume(): FeatureAvailability { const video = document.createElement('video');