feat(core): add error feature (#713)

This commit is contained in:
rahim
2026-03-04 23:36:26 -08:00
committed by GitHub
parent ae754eec8f
commit 879d55d1d2
9 changed files with 63 additions and 0 deletions
+26
View File
@@ -206,6 +206,32 @@ export interface MediaTextTrackState {
thumbnailTrackSrc: string | null;
}
export interface MediaError {
/**
* The error code (mirrors MediaError.code constants).
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code
*/
code: number;
/**
* A human-readable error message.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/MediaError/message
*/
message: string;
}
export interface MediaErrorState {
/**
* The current media error, or null if none.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error
*/
error: MediaError | null;
/** Dismiss the current error by clearing it. */
dismissError(): void;
}
export interface MediaPictureInPictureState {
/**
* Whether picture-in-picture mode is currently active.
+2
View File
@@ -2,6 +2,7 @@ import type { AnySlice, Slice, Store, UnionSliceState } from '@videojs/store';
import type {
MediaBufferState,
MediaControlsState,
MediaErrorState,
MediaFullscreenState,
MediaPictureInPictureState,
MediaPlaybackRateState,
@@ -55,6 +56,7 @@ export type VideoFeatures = [
PlayerFeature<MediaPictureInPictureState>,
PlayerFeature<MediaControlsState>,
PlayerFeature<MediaTextTrackState>,
PlayerFeature<MediaErrorState>,
];
export type AudioFeatures = [
@@ -0,0 +1,25 @@
import { listen } from '@videojs/utils/dom';
import type { MediaErrorState } from '../../../core/media/state';
import { definePlayerFeature } from '../../feature';
export const errorFeature = definePlayerFeature({
name: 'error',
state: ({ set }): MediaErrorState => ({
error: null,
dismissError() {
set({ error: null });
},
}),
attach({ target, signal, set }) {
const { media } = target;
const syncError = () => set({ error: media.error });
listen(media, 'error', syncError, { signal });
// Reset error state when a new source is loaded.
listen(media, 'emptied', () => set({ error: null }), { signal });
},
});
@@ -1,5 +1,6 @@
export * from './buffer';
export * from './controls';
export * from './error';
export * as features from './feature.parts';
export * from './fullscreen';
export * from './pip';
@@ -1,6 +1,7 @@
import type { AudioFeatures, BackgroundFeatures, VideoFeatures } from '../../media/types';
import { bufferFeature } from './buffer';
import { controlsFeature } from './controls';
import { errorFeature } from './error';
import { fullscreenFeature } from './fullscreen';
import { pipFeature } from './pip';
import { playbackFeature } from './playback';
@@ -21,6 +22,7 @@ export const videoFeatures: VideoFeatures = [
pipFeature,
controlsFeature,
textTrackFeature,
errorFeature,
];
export const audioFeatures: AudioFeatures = [
+3
View File
@@ -2,6 +2,7 @@ import { createSelector } from '@videojs/store';
import { bufferFeature } from './features/buffer';
import { controlsFeature } from './features/controls';
import { errorFeature } from './features/error';
import { fullscreenFeature } from './features/fullscreen';
import { pipFeature } from './features/pip';
import { playbackFeature } from './features/playback';
@@ -15,6 +16,8 @@ import { volumeFeature } from './features/volume';
export const selectBuffer = createSelector(bufferFeature);
/** Select the controls state (controls visible, user-active). */
export const selectControls = createSelector(controlsFeature);
/** Select the error state (error, dismissed, dismissError). */
export const selectError = createSelector(errorFeature);
/** Select the fullscreen state (fullscreen active, availability). */
export const selectFullscreen = createSelector(fullscreenFeature);
/** Select the PiP state (picture-in-picture active, availability). */
+1
View File
@@ -7,6 +7,7 @@ import type { AnySlice, InferSliceState, StateContext } from './slice';
const stateContext: StateContext<unknown> = {
target: throwNoTargetError,
signals: new AbortControllerRegistry(),
set: throwNoTargetError,
};
/**
+2
View File
@@ -40,6 +40,8 @@ export interface StateContext<Target> {
* (e.g., loading a new source cancels pending seeks).
*/
signals: AbortControllerRegistry;
/** Patch the slice state. Safe to use inside action closures (not during `state()` init). */
set: (partial: Record<string, unknown>) => void;
}
// ----------------------------------------
+1
View File
@@ -38,6 +38,7 @@ export function createStore<Target = unknown>(): <State>(
return target!;
},
signals,
set: (partial) => state.patch(partial as Partial<State>),
} satisfies StateContext<Target>);
state = createState(initialState);