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