mirror of
https://github.com/zoriya/react-native-video.git
synced 2026-05-24 23:37:00 +00:00
refactor: extract WebMediaProxy for unified store/video access
This commit is contained in:
@@ -15,6 +15,7 @@ import type { VideoPlayerStatus } from './types/VideoPlayerStatus';
|
||||
import { VideoPlayerEvents } from './events/VideoPlayerEvents';
|
||||
import { MediaSessionHandler } from './web/MediaSession';
|
||||
import { WebEventEmitter } from './web/WebEventEmitter';
|
||||
import { WebMediaProxy } from './web/WebMediaProxy';
|
||||
import type { VideoStore } from './web/VideoStore';
|
||||
|
||||
function setExternalSubtitles(
|
||||
@@ -84,22 +85,10 @@ function selectTrack(
|
||||
}
|
||||
|
||||
class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer {
|
||||
private video: HTMLVideoElement;
|
||||
private _storeRef: WeakRef<VideoStore> | null = null;
|
||||
private _media: WebMediaProxy;
|
||||
private mediaSession: MediaSessionHandler | null = null;
|
||||
private _source: NativeVideoConfig | undefined;
|
||||
|
||||
/** Returns store if alive, null if destroyed or disconnected. */
|
||||
private get _store(): VideoStore | null {
|
||||
const store = this._storeRef?.deref() ?? null;
|
||||
return store?.destroyed ? null : store;
|
||||
}
|
||||
|
||||
/** Store when available, video element fallback. */
|
||||
private get media(): VideoStore | HTMLVideoElement {
|
||||
return this._store ?? this.video;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a detached <video> element that works immediately.
|
||||
* VideoView later mounts it into the DOM and connects the video.js store.
|
||||
@@ -114,8 +103,9 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer {
|
||||
const video = document.createElement('video');
|
||||
video.playsInline = true;
|
||||
|
||||
super(new WebEventEmitter(null, () => video));
|
||||
this.video = video;
|
||||
const media = new WebMediaProxy(video);
|
||||
super(new WebEventEmitter(media));
|
||||
this._media = media;
|
||||
|
||||
(this.eventEmitter as WebEventEmitter).addOnErrorListener((error) => {
|
||||
this.triggerJSEvent('onError', error);
|
||||
@@ -128,8 +118,7 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer {
|
||||
|
||||
/** @internal */
|
||||
__setStore(store: VideoStore | null) {
|
||||
this._storeRef = store ? new WeakRef(store) : null;
|
||||
(this.eventEmitter as WebEventEmitter).setStore(store);
|
||||
this._media.setStore(store);
|
||||
if (store) {
|
||||
this.mediaSession = new MediaSessionHandler(store);
|
||||
} else {
|
||||
@@ -143,12 +132,12 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer {
|
||||
this.mediaSession?.disable();
|
||||
(this.eventEmitter as WebEventEmitter).destroy();
|
||||
this.clearAllEvents();
|
||||
this._storeRef = null;
|
||||
this._media.setStore(null);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
__getMedia(): HTMLVideoElement {
|
||||
return this.video;
|
||||
return this._media.video;
|
||||
}
|
||||
|
||||
// --- Playback state (read from store or video element) ---
|
||||
@@ -159,9 +148,9 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer {
|
||||
config: this._source ?? { uri: '' },
|
||||
getAssetInformationAsync: async () => ({
|
||||
bitrate: NaN,
|
||||
width: this.video.videoWidth,
|
||||
height: this.video.videoHeight,
|
||||
duration: this.video.duration || NaN,
|
||||
width: this._media.video.videoWidth,
|
||||
height: this._media.video.videoHeight,
|
||||
duration: this._media.duration || NaN,
|
||||
fileSize: -1n,
|
||||
isHDR: false,
|
||||
isLive: false,
|
||||
@@ -171,61 +160,57 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer {
|
||||
}
|
||||
|
||||
get status(): VideoPlayerStatus {
|
||||
if (this.media.error) return 'error';
|
||||
if (this.video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA)
|
||||
const video = this._media.video;
|
||||
if (this._media.error) return 'error';
|
||||
if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA)
|
||||
return 'readyToPlay';
|
||||
if (this.video.readyState > HTMLMediaElement.HAVE_NOTHING) return 'loading';
|
||||
if (this.video.src) return 'loading';
|
||||
if (video.readyState > HTMLMediaElement.HAVE_NOTHING) return 'loading';
|
||||
if (video.src) return 'loading';
|
||||
return 'idle';
|
||||
}
|
||||
|
||||
get duration(): number {
|
||||
return this.media.duration || NaN;
|
||||
return this._media.duration || NaN;
|
||||
}
|
||||
get currentTime(): number {
|
||||
return this.media.currentTime;
|
||||
return this._media.currentTime;
|
||||
}
|
||||
get volume(): number {
|
||||
return this.media.volume;
|
||||
return this._media.volume;
|
||||
}
|
||||
get muted(): boolean {
|
||||
return this.media.muted;
|
||||
return this._media.muted;
|
||||
}
|
||||
get loop(): boolean {
|
||||
return this.video.loop;
|
||||
return this._media.video.loop;
|
||||
}
|
||||
get rate(): number {
|
||||
return this.media.playbackRate;
|
||||
return this._media.playbackRate;
|
||||
}
|
||||
get isPlaying(): boolean {
|
||||
return !this.media.paused;
|
||||
return !this._media.paused;
|
||||
}
|
||||
|
||||
// --- Playback state (write through store or video element) ---
|
||||
|
||||
set volume(v: number) {
|
||||
const clamped = Math.max(0, Math.min(1, v));
|
||||
this._store
|
||||
? this._store.setVolume(clamped)
|
||||
: (this.video.volume = clamped);
|
||||
this._media.setVolume(Math.max(0, Math.min(1, v)));
|
||||
}
|
||||
|
||||
set currentTime(v: number) {
|
||||
this._store ? this._store.seek(v) : (this.video.currentTime = v);
|
||||
this._media.seek(v);
|
||||
}
|
||||
|
||||
// video.js store has toggleMuted() but no direct setter
|
||||
set muted(v: boolean) {
|
||||
this.video.muted = v;
|
||||
this._media.video.muted = v;
|
||||
}
|
||||
set loop(v: boolean) {
|
||||
this.video.loop = v;
|
||||
this._media.video.loop = v;
|
||||
}
|
||||
|
||||
set rate(v: number) {
|
||||
this._store
|
||||
? this._store.setPlaybackRate(v)
|
||||
: (this.video.playbackRate = v);
|
||||
this._media.setPlaybackRate(v);
|
||||
}
|
||||
|
||||
// --- Unsupported on web (no-op) ---
|
||||
@@ -268,26 +253,26 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer {
|
||||
async initialize(): Promise<void> {}
|
||||
|
||||
async preload(): Promise<void> {
|
||||
this.video.preload = 'auto';
|
||||
this.video.load();
|
||||
this._media.video.preload = 'auto';
|
||||
this._media.video.load();
|
||||
}
|
||||
|
||||
release(): void {
|
||||
this.__destroy();
|
||||
}
|
||||
play(): void {
|
||||
this.media.play()?.catch(() => {});
|
||||
this._media.play()?.catch(() => {});
|
||||
}
|
||||
pause(): void {
|
||||
this.media.pause();
|
||||
this._media.pause();
|
||||
}
|
||||
|
||||
seekTo(time: number): void {
|
||||
this._store ? this._store.seek(time) : (this.video.currentTime = time);
|
||||
this._media.seek(time);
|
||||
}
|
||||
|
||||
seekBy(time: number): void {
|
||||
this.seekTo(this.media.currentTime + time);
|
||||
this.seekTo(this._media.currentTime + time);
|
||||
}
|
||||
|
||||
// --- Source management ---
|
||||
@@ -299,9 +284,11 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer {
|
||||
| NoAutocomplete<VideoPlayerSourceBase>
|
||||
| null
|
||||
): Promise<void> {
|
||||
const video = this._media.video;
|
||||
|
||||
if (!source) {
|
||||
this.video.removeAttribute('src');
|
||||
this.video.load();
|
||||
video.removeAttribute('src');
|
||||
video.load();
|
||||
this._source = undefined;
|
||||
return;
|
||||
}
|
||||
@@ -318,15 +305,13 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer {
|
||||
}
|
||||
|
||||
this._source = source as NativeVideoConfig;
|
||||
this._store
|
||||
? this._store.loadSource(source.uri)
|
||||
: (this.video.src = source.uri);
|
||||
this._media.loadSource(source.uri);
|
||||
|
||||
if (this.mediaSession?.enabled) {
|
||||
this.mediaSession.updateMediaSession(source.metadata);
|
||||
}
|
||||
|
||||
setExternalSubtitles(this.video, source.externalSubtitles);
|
||||
setExternalSubtitles(video, source.externalSubtitles);
|
||||
|
||||
if (source.initializeOnCreation) await this.preload();
|
||||
}
|
||||
@@ -334,10 +319,10 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer {
|
||||
// --- Tracks ---
|
||||
|
||||
getAvailableTextTracks(): TextTrack[] {
|
||||
return getTracks(this.video, 'textTracks');
|
||||
return getTracks(this._media.video, 'textTracks');
|
||||
}
|
||||
selectTextTrack(t: TextTrack | null): void {
|
||||
selectTrack(this.video, 'textTracks', t?.id ?? null);
|
||||
selectTrack(this._media.video, 'textTracks', t?.id ?? null);
|
||||
}
|
||||
get selectedTrack(): TextTrack | undefined {
|
||||
return this.getAvailableTextTracks().find((x) => x.selected);
|
||||
@@ -345,20 +330,20 @@ class VideoPlayer extends VideoPlayerEvents implements WebVideoPlayer {
|
||||
|
||||
// Audio/video tracks: web-only, ~16% browser support (Safari only, flags in Chrome/Firefox)
|
||||
getAvailableAudioTracks(): AudioTrack[] {
|
||||
return getTracks(this.video, 'audioTracks');
|
||||
return getTracks(this._media.video, 'audioTracks');
|
||||
}
|
||||
selectAudioTrack(t: AudioTrack | null): void {
|
||||
selectTrack(this.video, 'audioTracks', t?.id ?? null);
|
||||
selectTrack(this._media.video, 'audioTracks', t?.id ?? null);
|
||||
}
|
||||
get selectedAudioTrack(): AudioTrack | undefined {
|
||||
return this.getAvailableAudioTracks().find((x) => x.selected);
|
||||
}
|
||||
|
||||
getAvailableVideoTracks(): VideoTrack[] {
|
||||
return getTracks(this.video, 'videoTracks');
|
||||
return getTracks(this._media.video, 'videoTracks');
|
||||
}
|
||||
selectVideoTrack(t: VideoTrack | null): void {
|
||||
selectTrack(this.video, 'videoTracks', t?.id ?? null);
|
||||
selectTrack(this._media.video, 'videoTracks', t?.id ?? null);
|
||||
}
|
||||
get selectedVideoTrack(): VideoTrack | undefined {
|
||||
return this.getAvailableVideoTracks().find((x) => x.selected);
|
||||
|
||||
@@ -19,46 +19,29 @@ import type {
|
||||
VideoPlayerEventEmitterBase,
|
||||
} from '../types/EventEmitter';
|
||||
|
||||
import type { VideoStore } from './VideoStore';
|
||||
import type { WebMediaProxy } from './WebMediaProxy';
|
||||
|
||||
/**
|
||||
* WebEventEmitter bridges HTML5 media events to our event system.
|
||||
* Works with or without a video.js store — video element events are the primary source.
|
||||
* When a video.js store is connected, it provides enhanced buffering info.
|
||||
* Reads state from WebMediaProxy (store when available, video element fallback).
|
||||
*/
|
||||
export class WebEventEmitter implements VideoPlayerEventEmitterBase {
|
||||
private _listeners: Map<string, Set<(...args: any[]) => void>> = new Map();
|
||||
private _mediaCleanup: (() => void) | null = null;
|
||||
private _storeUnsubscribe: (() => void) | null = null;
|
||||
private _isBuffering = false;
|
||||
|
||||
constructor(
|
||||
store: VideoStore | null,
|
||||
private getMedia: () => HTMLVideoElement | null
|
||||
) {
|
||||
// Attach to video element immediately if available
|
||||
constructor(private _media: WebMediaProxy) {
|
||||
this._attachMediaListeners();
|
||||
if (store) this.setStore(store);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect or disconnect the video.js store (optional enhancement).
|
||||
*/
|
||||
setStore(_store: VideoStore | null) {
|
||||
this._storeUnsubscribe?.();
|
||||
this._storeUnsubscribe = null;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._storeUnsubscribe?.();
|
||||
this._storeUnsubscribe = null;
|
||||
this._mediaCleanup?.();
|
||||
this._mediaCleanup = null;
|
||||
}
|
||||
|
||||
private _attachMediaListeners() {
|
||||
const video = this.getMedia();
|
||||
if (!video) return;
|
||||
const video = this._media.video;
|
||||
const media = this._media;
|
||||
|
||||
const on = (event: string, handler: () => void) => {
|
||||
video.addEventListener(event, handler);
|
||||
@@ -70,7 +53,7 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase {
|
||||
cleanups.push(
|
||||
on('play', () => {
|
||||
this._emit('onPlaybackStateChange', {
|
||||
isPlaying: true,
|
||||
isPlaying: !media.paused,
|
||||
isBuffering: this._isBuffering,
|
||||
});
|
||||
})
|
||||
@@ -79,7 +62,7 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase {
|
||||
cleanups.push(
|
||||
on('pause', () => {
|
||||
this._emit('onPlaybackStateChange', {
|
||||
isPlaying: false,
|
||||
isPlaying: !media.paused,
|
||||
isBuffering: this._isBuffering,
|
||||
});
|
||||
})
|
||||
@@ -103,22 +86,19 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase {
|
||||
|
||||
cleanups.push(
|
||||
on('timeupdate', () => {
|
||||
const buffered = video.buffered;
|
||||
const lastBuffered =
|
||||
buffered.length > 0 ? buffered.end(buffered.length - 1) : 0;
|
||||
this._emit('onProgress', {
|
||||
currentTime: video.currentTime,
|
||||
bufferDuration: lastBuffered,
|
||||
currentTime: media.currentTime,
|
||||
bufferDuration: media.bufferEnd,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
cleanups.push(
|
||||
on('durationchange', () => {
|
||||
if (video.duration > 0) {
|
||||
if (media.duration > 0) {
|
||||
this._emit('onLoad', {
|
||||
currentTime: video.currentTime,
|
||||
duration: video.duration,
|
||||
currentTime: media.currentTime,
|
||||
duration: media.duration,
|
||||
width: video.videoWidth,
|
||||
height: video.videoHeight,
|
||||
orientation: 'unknown',
|
||||
@@ -136,7 +116,7 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase {
|
||||
|
||||
cleanups.push(
|
||||
on('ratechange', () => {
|
||||
this._emit('onPlaybackRateChange', video.playbackRate);
|
||||
this._emit('onPlaybackRateChange', media.playbackRate);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -148,15 +128,15 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase {
|
||||
|
||||
cleanups.push(
|
||||
on('seeked', () => {
|
||||
this._emit('onSeek', video.currentTime);
|
||||
this._emit('onSeek', media.currentTime);
|
||||
})
|
||||
);
|
||||
|
||||
cleanups.push(
|
||||
on('volumechange', () => {
|
||||
this._emit('onVolumeChange', {
|
||||
volume: video.volume,
|
||||
muted: video.muted,
|
||||
volume: media.volume,
|
||||
muted: media.muted,
|
||||
});
|
||||
})
|
||||
);
|
||||
@@ -172,7 +152,7 @@ export class WebEventEmitter implements VideoPlayerEventEmitterBase {
|
||||
externalSubtitles: [],
|
||||
},
|
||||
getAssetInformationAsync: async () => ({
|
||||
duration: video.duration || NaN,
|
||||
duration: media.duration || NaN,
|
||||
width: video.videoWidth,
|
||||
height: video.videoHeight,
|
||||
orientation: 'unknown',
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { VideoStore } from './VideoStore';
|
||||
|
||||
/**
|
||||
* Unified read/write access to video state.
|
||||
* Reads from video.js store when available, falls back to HTMLVideoElement.
|
||||
* Shared by VideoPlayer and WebEventEmitter.
|
||||
*/
|
||||
export class WebMediaProxy {
|
||||
private _storeRef: WeakRef<VideoStore> | null = null;
|
||||
|
||||
constructor(readonly video: HTMLVideoElement) {}
|
||||
|
||||
get store(): VideoStore | null {
|
||||
const store = this._storeRef?.deref() ?? null;
|
||||
return store?.destroyed ? null : store;
|
||||
}
|
||||
|
||||
setStore(store: VideoStore | null) {
|
||||
this._storeRef = store ? new WeakRef(store) : null;
|
||||
}
|
||||
|
||||
// --- Read (store preferred, video fallback) ---
|
||||
|
||||
get paused() {
|
||||
return (this.store ?? this.video).paused;
|
||||
}
|
||||
get currentTime() {
|
||||
return (this.store ?? this.video).currentTime;
|
||||
}
|
||||
get duration() {
|
||||
return (this.store ?? this.video).duration;
|
||||
}
|
||||
get volume() {
|
||||
return (this.store ?? this.video).volume;
|
||||
}
|
||||
get muted() {
|
||||
return (this.store ?? this.video).muted;
|
||||
}
|
||||
get playbackRate() {
|
||||
return (this.store ?? this.video).playbackRate;
|
||||
}
|
||||
get error() {
|
||||
return (this.store ?? this.video).error;
|
||||
}
|
||||
|
||||
get bufferEnd(): number {
|
||||
const store = this.store;
|
||||
if (store) {
|
||||
const ranges = store.buffered;
|
||||
return ranges.length > 0 ? ranges[ranges.length - 1]![1] : 0;
|
||||
}
|
||||
const ranges = this.video.buffered;
|
||||
return ranges.length > 0 ? ranges.end(ranges.length - 1) : 0;
|
||||
}
|
||||
|
||||
// --- Write (route to store or video) ---
|
||||
|
||||
play() {
|
||||
return (this.store ?? this.video).play();
|
||||
}
|
||||
|
||||
pause() {
|
||||
(this.store ?? this.video).pause();
|
||||
}
|
||||
|
||||
seek(time: number) {
|
||||
this.store ? this.store.seek(time) : (this.video.currentTime = time);
|
||||
}
|
||||
|
||||
setVolume(v: number) {
|
||||
this.store ? this.store.setVolume(v) : (this.video.volume = v);
|
||||
}
|
||||
|
||||
setPlaybackRate(v: number) {
|
||||
this.store ? this.store.setPlaybackRate(v) : (this.video.playbackRate = v);
|
||||
}
|
||||
|
||||
loadSource(src: string) {
|
||||
this.store ? this.store.loadSource(src) : (this.video.src = src);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user