diff --git a/packages/react-native-video/src/core/VideoPlayer.web.ts b/packages/react-native-video/src/core/VideoPlayer.web.ts index 15c07ddb..63f3a946 100644 --- a/packages/react-native-video/src/core/VideoPlayer.web.ts +++ b/packages/react-native-video/src/core/VideoPlayer.web.ts @@ -12,7 +12,8 @@ import type { VideoPlayerSourceBase } from "./types/VideoPlayerSourceBase"; import type { VideoPlayerStatus } from "./types/VideoPlayerStatus"; import { VideoPlayerEvents } from "./events/VideoPlayerEvents"; import { MediaSessionHandler } from "./web/MediaSession"; -import { WebEventEmitter, type VideoStore } from "./web/WebEventEmitter"; +import { WebEventEmitter } from "./web/WebEventEmitter"; +import type { VideoStore } from "./web/VideoStore"; class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { private video: HTMLVideoElement; @@ -90,40 +91,34 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { }; } - // Store when available, video element fallback. - // Store may provide more accurate values for HLS/DASH/DRM content. + /** Store when available, video element fallback. */ + private get media(): VideoStore | HTMLVideoElement { + return this._store ?? this.video; + } get status(): VideoPlayerStatus { - const s = this._store; - if (s) { - if (s.error) return "error"; - if (s.canPlay) return "readyToPlay"; - if (s.waiting) return "loading"; - if (s.source) return "loading"; - return "idle"; - } - if (this.video.error) return "error"; + if (this.media.error) return "error"; if (this.video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) return "readyToPlay"; if (this.video.readyState > HTMLMediaElement.HAVE_NOTHING) return "loading"; if (this.video.src) return "loading"; return "idle"; } - get duration(): number { return this._store?.duration || this.video.duration || NaN; } - get volume(): number { return this._store?.volume ?? this.video.volume; } + get duration(): number { return this.media.duration || NaN; } + get volume(): number { return this.media.volume; } set volume(v: number) { const clamped = Math.max(0, Math.min(1, v)); if (this._store) { this._store.setVolume(clamped); } else { this.video.volume = clamped; } } - get currentTime(): number { return this._store?.currentTime ?? this.video.currentTime; } + get currentTime(): number { return this.media.currentTime; } set currentTime(v: number) { if (this._store) { this._store.seek(v); } else { this.video.currentTime = v; } } - get muted(): boolean { return this._store?.muted ?? this.video.muted; } + get muted(): boolean { return this.media.muted; } set muted(v: boolean) { this.video.muted = v; } get loop(): boolean { return this.video.loop; } set loop(v: boolean) { this.video.loop = v; } - get rate(): number { return this._store?.playbackRate ?? this.video.playbackRate; } + get rate(): number { return this.media.playbackRate; } set rate(v: number) { if (this._store) { this._store.setPlaybackRate(v); } else { this.video.playbackRate = v; } } @@ -137,9 +132,7 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { get playWhenInactive(): boolean { return true; } set playWhenInactive(_: boolean) {} - get isPlaying(): boolean { - return this._store ? !this._store.paused : !this.video.paused && !this.video.ended; - } + get isPlaying(): boolean { return !this.media.paused; } get showNotificationControls(): boolean { return this.mediaSession?.enabled ?? false; @@ -159,16 +152,11 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase { } release(): void { this.__destroy(); } - play(): void { - if (this._store) { this._store.play()?.catch(() => {}); } else { this.video.play()?.catch(() => {}); } - } - - pause(): void { - if (this._store) { this._store.pause(); } else { this.video.pause(); } - } + play(): void { this.media.play()?.catch(() => {}); } + pause(): void { this.media.pause(); } seekBy(time: number): void { - const now = this._store?.currentTime ?? this.video.currentTime; + const now = this.media.currentTime; if (this._store) { this._store.seek(now + time); } else { this.video.currentTime = now + time; } } diff --git a/packages/react-native-video/src/core/video-view/VideoView.web.tsx b/packages/react-native-video/src/core/video-view/VideoView.web.tsx index dab645b2..b0762a08 100644 --- a/packages/react-native-video/src/core/video-view/VideoView.web.tsx +++ b/packages/react-native-video/src/core/video-view/VideoView.web.tsx @@ -14,7 +14,7 @@ import type { VideoViewProps, VideoViewRef } from "./VideoViewProps"; import { createPlayer, videoFeatures, usePlayerContext, useMediaAttach } from "@videojs/react"; import { VideoSkin } from "@videojs/react/video"; import "@videojs/react/video/skin.css"; -import type { VideoStore } from "../web/WebEventEmitter"; +import type { VideoStore } from "../web/VideoStore"; const Player = createPlayer({ features: videoFeatures }); diff --git a/packages/react-native-video/src/core/web/MediaSession.ts b/packages/react-native-video/src/core/web/MediaSession.ts index d77c9074..7786e2bc 100644 --- a/packages/react-native-video/src/core/web/MediaSession.ts +++ b/packages/react-native-video/src/core/web/MediaSession.ts @@ -1,24 +1,11 @@ import type { CustomVideoMetadata } from "../types/VideoConfig"; +import type { MediaSessionStore } from "./VideoStore"; function getMediaSession(): MediaSession | undefined { if (typeof window === "undefined") return undefined; return window.navigator?.mediaSession; } -/** - * video.js store interface — the subset MediaSession needs. - */ -interface MediaSessionStore { - readonly paused: boolean; - readonly currentTime: number; - readonly duration: number; - readonly playbackRate: number; - play(): Promise; - pause(): void; - seek(time: number): Promise; - subscribe(callback: () => void): () => void; -} - export class MediaSessionHandler { enabled: boolean = false; diff --git a/packages/react-native-video/src/core/web/VideoStore.ts b/packages/react-native-video/src/core/web/VideoStore.ts new file mode 100644 index 00000000..d8716ec9 --- /dev/null +++ b/packages/react-native-video/src/core/web/VideoStore.ts @@ -0,0 +1,51 @@ +/** + * video.js store interface. + * Represents the reactive store created by @videojs/react's createPlayer. + * Used as the primary data source when VideoView is mounted; + * HTMLVideoElement is the fallback when store is not available. + */ +export interface VideoStore { + // State + readonly paused: boolean; + readonly ended: boolean; + readonly waiting: boolean; + readonly seeking: boolean; + readonly canPlay: boolean; + readonly currentTime: number; + readonly duration: number; + readonly volume: number; + readonly muted: boolean; + readonly playbackRate: number; + readonly source: string | null; + readonly buffered: [number, number][]; + readonly error: { code: number; message: string } | null; + readonly textTrackList: Array<{ kind: string; label: string; language: string; mode: string }>; + readonly destroyed: boolean; + readonly target: unknown; + readonly pipAvailability: string; + + // Actions + play(): Promise; + pause(): void; + seek(time: number): Promise; + setVolume(volume: number): number; + setPlaybackRate(rate: number): void; + loadSource(src: string): string; + requestFullscreen(): Promise; + exitFullscreen(): Promise; + requestPictureInPicture(): Promise; + exitPictureInPicture(): Promise; + + // Lifecycle + subscribe(callback: () => void): () => void; + attach(target: { media: HTMLVideoElement; container: HTMLElement | null }): () => void; + destroy(): void; +} + +/** + * Subset of VideoStore used by MediaSessionHandler. + */ +export type MediaSessionStore = Pick< + VideoStore, + "paused" | "currentTime" | "duration" | "playbackRate" | "play" | "pause" | "seek" | "subscribe" +>; diff --git a/packages/react-native-video/src/core/web/WebEventEmitter.ts b/packages/react-native-video/src/core/web/WebEventEmitter.ts index d7fed337..758d98e8 100644 --- a/packages/react-native-video/src/core/web/WebEventEmitter.ts +++ b/packages/react-native-video/src/core/web/WebEventEmitter.ts @@ -22,41 +22,7 @@ import type { VideoPlayerEventEmitterBase, } from "../types/EventEmitter"; -/** - * video.js store interface — optional, used for enhanced buffering info when available. - */ -export interface VideoStore { - readonly paused: boolean; - readonly ended: boolean; - readonly waiting: boolean; - readonly seeking: boolean; - readonly canPlay: boolean; - readonly currentTime: number; - readonly duration: number; - readonly volume: number; - readonly muted: boolean; - readonly playbackRate: number; - readonly source: string | null; - readonly buffered: [number, number][]; - readonly error: { code: number; message: string } | null; - readonly textTrackList: Array<{ kind: string; label: string; language: string; mode: string }>; - readonly destroyed: boolean; - readonly target: unknown; - subscribe(callback: () => void): () => void; - attach(target: { media: HTMLVideoElement; container: HTMLElement | null }): () => void; - destroy(): void; - loadSource(src: string): string; - play(): Promise; - pause(): void; - seek(time: number): Promise; - setVolume(volume: number): number; - setPlaybackRate(rate: number): void; - requestFullscreen(): Promise; - exitFullscreen(): Promise; - requestPictureInPicture(): Promise; - exitPictureInPicture(): Promise; - readonly pipAvailability: string; -} +import type { VideoStore } from "./VideoStore"; /** * WebEventEmitter bridges HTML5 media events to our event system.