react-native-video-provider 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AuVideo.podspec +28 -0
- package/LICENSE +20 -0
- package/README.md +183 -0
- package/android/build.gradle +59 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/auvideo/AuVideoModule.kt +259 -0
- package/android/src/main/java/com/auvideo/AuVideoPackage.kt +35 -0
- package/android/src/main/java/com/auvideo/AuVideoSurfaceView.kt +40 -0
- package/android/src/main/java/com/auvideo/AuVideoSurfaceViewManager.kt +38 -0
- package/android/src/main/java/com/auvideo/PlayerCore.kt +366 -0
- package/android/src/main/java/com/auvideo/SurfaceRegistry.kt +26 -0
- package/android/src/main/res/layout/au_video_player_view.xml +10 -0
- package/ios/AuVideo.h +10 -0
- package/ios/AuVideo.mm +232 -0
- package/ios/AuVideoOrientation.swift +59 -0
- package/ios/AuVideoPlayerCore.swift +415 -0
- package/ios/AuVideoSurfaceRegistry.swift +30 -0
- package/ios/AuVideoSurfaceView.h +15 -0
- package/ios/AuVideoSurfaceView.mm +63 -0
- package/lib/module/AuVideoSurfaceNativeComponent.ts +8 -0
- package/lib/module/NativeAuVideo.js +5 -0
- package/lib/module/NativeAuVideo.js.map +1 -0
- package/lib/module/components/FloatingPlayer.js +119 -0
- package/lib/module/components/FloatingPlayer.js.map +1 -0
- package/lib/module/components/FullscreenPlayer.js +59 -0
- package/lib/module/components/FullscreenPlayer.js.map +1 -0
- package/lib/module/components/GestureOverlay.js +56 -0
- package/lib/module/components/GestureOverlay.js.map +1 -0
- package/lib/module/components/MiniPlayer.js +89 -0
- package/lib/module/components/MiniPlayer.js.map +1 -0
- package/lib/module/components/VideoControls.js +191 -0
- package/lib/module/components/VideoControls.js.map +1 -0
- package/lib/module/components/VideoPlayer.js +65 -0
- package/lib/module/components/VideoPlayer.js.map +1 -0
- package/lib/module/components/VideoSurface.js +35 -0
- package/lib/module/components/VideoSurface.js.map +1 -0
- package/lib/module/core/VideoManager.js +436 -0
- package/lib/module/core/VideoManager.js.map +1 -0
- package/lib/module/hooks/useFullscreen.js +19 -0
- package/lib/module/hooks/useFullscreen.js.map +1 -0
- package/lib/module/hooks/usePiP.js +17 -0
- package/lib/module/hooks/usePiP.js.map +1 -0
- package/lib/module/hooks/usePlayback.js +21 -0
- package/lib/module/hooks/usePlayback.js.map +1 -0
- package/lib/module/hooks/useVideo.js +17 -0
- package/lib/module/hooks/useVideo.js.map +1 -0
- package/lib/module/hooks/useVideoEvents.js +26 -0
- package/lib/module/hooks/useVideoEvents.js.map +1 -0
- package/lib/module/index.js +19 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/provider/VideoContext.js +14 -0
- package/lib/module/provider/VideoContext.js.map +1 -0
- package/lib/module/provider/VideoProvider.js +36 -0
- package/lib/module/provider/VideoProvider.js.map +1 -0
- package/lib/module/state/createVideoStore.js +33 -0
- package/lib/module/state/createVideoStore.js.map +1 -0
- package/lib/module/types/events.js +4 -0
- package/lib/module/types/events.js.map +1 -0
- package/lib/module/types/index.js +2 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/types/video.js +2 -0
- package/lib/module/types/video.js.map +1 -0
- package/lib/module/utils/Emitter.js +39 -0
- package/lib/module/utils/Emitter.js.map +1 -0
- package/lib/module/utils/formatTime.js +15 -0
- package/lib/module/utils/formatTime.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/AuVideoSurfaceNativeComponent.d.ts +8 -0
- package/lib/typescript/src/AuVideoSurfaceNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/NativeAuVideo.d.ts +85 -0
- package/lib/typescript/src/NativeAuVideo.d.ts.map +1 -0
- package/lib/typescript/src/components/FloatingPlayer.d.ts +12 -0
- package/lib/typescript/src/components/FloatingPlayer.d.ts.map +1 -0
- package/lib/typescript/src/components/FullscreenPlayer.d.ts +12 -0
- package/lib/typescript/src/components/FullscreenPlayer.d.ts.map +1 -0
- package/lib/typescript/src/components/GestureOverlay.d.ts +14 -0
- package/lib/typescript/src/components/GestureOverlay.d.ts.map +1 -0
- package/lib/typescript/src/components/MiniPlayer.d.ts +16 -0
- package/lib/typescript/src/components/MiniPlayer.d.ts.map +1 -0
- package/lib/typescript/src/components/VideoControls.d.ts +17 -0
- package/lib/typescript/src/components/VideoControls.d.ts.map +1 -0
- package/lib/typescript/src/components/VideoPlayer.d.ts +27 -0
- package/lib/typescript/src/components/VideoPlayer.d.ts.map +1 -0
- package/lib/typescript/src/components/VideoSurface.d.ts +18 -0
- package/lib/typescript/src/components/VideoSurface.d.ts.map +1 -0
- package/lib/typescript/src/core/VideoManager.d.ts +88 -0
- package/lib/typescript/src/core/VideoManager.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useFullscreen.d.ts +7 -0
- package/lib/typescript/src/hooks/useFullscreen.d.ts.map +1 -0
- package/lib/typescript/src/hooks/usePiP.d.ts +6 -0
- package/lib/typescript/src/hooks/usePiP.d.ts.map +1 -0
- package/lib/typescript/src/hooks/usePlayback.d.ts +13 -0
- package/lib/typescript/src/hooks/usePlayback.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useVideo.d.ts +13 -0
- package/lib/typescript/src/hooks/useVideo.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useVideoEvents.d.ts +14 -0
- package/lib/typescript/src/hooks/useVideoEvents.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +18 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/provider/VideoContext.d.ts +8 -0
- package/lib/typescript/src/provider/VideoContext.d.ts.map +1 -0
- package/lib/typescript/src/provider/VideoProvider.d.ts +18 -0
- package/lib/typescript/src/provider/VideoProvider.d.ts.map +1 -0
- package/lib/typescript/src/state/createVideoStore.d.ts +6 -0
- package/lib/typescript/src/state/createVideoStore.d.ts.map +1 -0
- package/lib/typescript/src/types/events.d.ts +46 -0
- package/lib/typescript/src/types/events.d.ts.map +1 -0
- package/lib/typescript/src/types/index.d.ts +3 -0
- package/lib/typescript/src/types/index.d.ts.map +1 -0
- package/lib/typescript/src/types/video.d.ts +70 -0
- package/lib/typescript/src/types/video.d.ts.map +1 -0
- package/lib/typescript/src/utils/Emitter.d.ts +15 -0
- package/lib/typescript/src/utils/Emitter.d.ts.map +1 -0
- package/lib/typescript/src/utils/formatTime.d.ts +2 -0
- package/lib/typescript/src/utils/formatTime.d.ts.map +1 -0
- package/package.json +154 -0
- package/src/AuVideoSurfaceNativeComponent.ts +8 -0
- package/src/NativeAuVideo.ts +106 -0
- package/src/components/FloatingPlayer.tsx +130 -0
- package/src/components/FullscreenPlayer.tsx +64 -0
- package/src/components/GestureOverlay.tsx +76 -0
- package/src/components/MiniPlayer.tsx +93 -0
- package/src/components/VideoControls.tsx +213 -0
- package/src/components/VideoPlayer.tsx +77 -0
- package/src/components/VideoSurface.tsx +39 -0
- package/src/core/VideoManager.ts +441 -0
- package/src/hooks/useFullscreen.ts +14 -0
- package/src/hooks/usePiP.ts +13 -0
- package/src/hooks/usePlayback.ts +26 -0
- package/src/hooks/useVideo.ts +16 -0
- package/src/hooks/useVideoEvents.ts +33 -0
- package/src/index.ts +54 -0
- package/src/provider/VideoContext.ts +12 -0
- package/src/provider/VideoProvider.tsx +41 -0
- package/src/state/createVideoStore.ts +33 -0
- package/src/types/events.ts +22 -0
- package/src/types/index.ts +11 -0
- package/src/types/video.ts +85 -0
- package/src/utils/Emitter.ts +51 -0
- package/src/utils/formatTime.ts +12 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import type { EventSubscription } from 'react-native';
|
|
2
|
+
import NativeAuVideo, { type NativeVideoSource } from '../NativeAuVideo';
|
|
3
|
+
import {
|
|
4
|
+
createVideoStore,
|
|
5
|
+
initialVideoState,
|
|
6
|
+
type VideoStore,
|
|
7
|
+
} from '../state/createVideoStore';
|
|
8
|
+
import type { VideoEventMap, VideoEventName } from '../types/events';
|
|
9
|
+
import type {
|
|
10
|
+
PlaybackStatus,
|
|
11
|
+
PlayerMode,
|
|
12
|
+
ResizeMode,
|
|
13
|
+
SetSourceOptions,
|
|
14
|
+
VideoProviderConfig,
|
|
15
|
+
VideoSource,
|
|
16
|
+
VideoState,
|
|
17
|
+
} from '../types/video';
|
|
18
|
+
import { Emitter, type Listener, type Subscription } from '../utils/Emitter';
|
|
19
|
+
|
|
20
|
+
/** Surface id used by the built-in fullscreen host. */
|
|
21
|
+
export const FULLSCREEN_SURFACE_ID = '__au_fullscreen__';
|
|
22
|
+
/** Surface id used by the built-in floating host. */
|
|
23
|
+
export const FLOATING_SURFACE_ID = '__au_floating__';
|
|
24
|
+
|
|
25
|
+
const RESERVED_SURFACES = new Set([FULLSCREEN_SURFACE_ID, FLOATING_SURFACE_ID]);
|
|
26
|
+
|
|
27
|
+
function toNativeSource(source: VideoSource): NativeVideoSource {
|
|
28
|
+
return {
|
|
29
|
+
id: source.id,
|
|
30
|
+
uri: source.uri,
|
|
31
|
+
headers: source.headers,
|
|
32
|
+
title: source.title,
|
|
33
|
+
artist: source.artist,
|
|
34
|
+
artworkUri: source.artworkUri,
|
|
35
|
+
startPosition: source.startPosition,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The single owner of the native playback engine on the JS side.
|
|
41
|
+
*
|
|
42
|
+
* React components never talk to the native module directly — they issue
|
|
43
|
+
* commands here and subscribe to `store` for state. The manager exists for
|
|
44
|
+
* the whole app lifetime; unmounting React trees never destroys it.
|
|
45
|
+
*/
|
|
46
|
+
export class VideoManager {
|
|
47
|
+
private static instance: VideoManager | null = null;
|
|
48
|
+
|
|
49
|
+
static get shared(): VideoManager {
|
|
50
|
+
if (!VideoManager.instance) {
|
|
51
|
+
VideoManager.instance = new VideoManager();
|
|
52
|
+
}
|
|
53
|
+
return VideoManager.instance;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
readonly store: VideoStore = createVideoStore();
|
|
57
|
+
|
|
58
|
+
private events = new Emitter<VideoEventMap>();
|
|
59
|
+
private nativeSubscriptions: EventSubscription[] = [];
|
|
60
|
+
private initialized = false;
|
|
61
|
+
private config: Required<VideoProviderConfig> = {
|
|
62
|
+
fullscreenHost: true,
|
|
63
|
+
floatingHost: true,
|
|
64
|
+
pauseOnDetach: false,
|
|
65
|
+
};
|
|
66
|
+
/** Last non-reserved surface, restored after fullscreen/floating exits. */
|
|
67
|
+
private lastInlineSurfaceId: string | null = null;
|
|
68
|
+
|
|
69
|
+
private constructor() {}
|
|
70
|
+
|
|
71
|
+
get providerConfig(): Required<VideoProviderConfig> {
|
|
72
|
+
return this.config;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Idempotent. Called by VideoProvider on mount ("mount silently"):
|
|
77
|
+
* creates the native player once and wires native events.
|
|
78
|
+
*/
|
|
79
|
+
init(config?: VideoProviderConfig): void {
|
|
80
|
+
this.config = { ...this.config, ...config };
|
|
81
|
+
if (this.initialized) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.initialized = true;
|
|
85
|
+
NativeAuVideo.nativeInit();
|
|
86
|
+
this.subscribeNative();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------- events
|
|
90
|
+
|
|
91
|
+
addListener<K extends VideoEventName>(
|
|
92
|
+
event: K,
|
|
93
|
+
listener: Listener<VideoEventMap[K]>
|
|
94
|
+
): Subscription {
|
|
95
|
+
return this.events.addListener(event, listener);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private subscribeNative(): void {
|
|
99
|
+
const subs = this.nativeSubscriptions;
|
|
100
|
+
|
|
101
|
+
subs.push(
|
|
102
|
+
NativeAuVideo.onStatusChange((e) => {
|
|
103
|
+
this.applyStatus(e.status as PlaybackStatus);
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
subs.push(
|
|
107
|
+
NativeAuVideo.onLoad((e) => {
|
|
108
|
+
this.set({
|
|
109
|
+
duration: e.duration,
|
|
110
|
+
videoWidth: e.width,
|
|
111
|
+
videoHeight: e.height,
|
|
112
|
+
loading: false,
|
|
113
|
+
});
|
|
114
|
+
this.events.emit('onLoad', e);
|
|
115
|
+
this.events.emit('onReady', { videoId: e.videoId });
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
subs.push(
|
|
119
|
+
NativeAuVideo.onProgress((e) => {
|
|
120
|
+
this.set({
|
|
121
|
+
position: e.position,
|
|
122
|
+
duration: e.duration,
|
|
123
|
+
buffered: e.buffered,
|
|
124
|
+
});
|
|
125
|
+
this.events.emit('onProgress', e);
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
subs.push(
|
|
129
|
+
NativeAuVideo.onSeek((e) => {
|
|
130
|
+
this.set({ position: e.position });
|
|
131
|
+
this.events.emit('onSeek', e);
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
subs.push(
|
|
135
|
+
NativeAuVideo.onEnd(() => {
|
|
136
|
+
this.applyStatus('ended');
|
|
137
|
+
this.events.emit('onEnd', undefined);
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
subs.push(
|
|
141
|
+
NativeAuVideo.onError((e) => {
|
|
142
|
+
this.set({ error: e, status: 'error', playing: false, loading: false });
|
|
143
|
+
this.events.emit('onError', e);
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
subs.push(
|
|
147
|
+
NativeAuVideo.onAttach((e) => {
|
|
148
|
+
this.events.emit('onAttach', e);
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
subs.push(
|
|
152
|
+
NativeAuVideo.onDetach((e) => {
|
|
153
|
+
this.events.emit('onDetach', e);
|
|
154
|
+
if (this.config.pauseOnDetach) {
|
|
155
|
+
this.pause();
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
);
|
|
159
|
+
subs.push(
|
|
160
|
+
NativeAuVideo.onPipChange((e) => {
|
|
161
|
+
this.set({ pip: e.active });
|
|
162
|
+
this.setMode(e.active ? 'pip' : this.deriveMode({ pip: false }));
|
|
163
|
+
this.events.emit('onPipChanged', e);
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private applyStatus(status: PlaybackStatus): void {
|
|
169
|
+
const prev = this.store.getState();
|
|
170
|
+
this.set({
|
|
171
|
+
status,
|
|
172
|
+
playing: status === 'playing',
|
|
173
|
+
paused: status === 'paused',
|
|
174
|
+
buffering: status === 'buffering',
|
|
175
|
+
loading: status === 'loading',
|
|
176
|
+
error: status === 'error' ? prev.error : null,
|
|
177
|
+
});
|
|
178
|
+
if (status === 'playing' && prev.status !== 'playing') {
|
|
179
|
+
this.events.emit('onPlay', undefined);
|
|
180
|
+
}
|
|
181
|
+
if (status === 'paused' && prev.status !== 'paused') {
|
|
182
|
+
this.events.emit('onPause', undefined);
|
|
183
|
+
}
|
|
184
|
+
if ((status === 'buffering') !== (prev.status === 'buffering')) {
|
|
185
|
+
this.events.emit('onBuffer', { buffering: status === 'buffering' });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// -------------------------------------------------------------- commands
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Load a video. Same-video handoff: if `source.id` equals the current
|
|
193
|
+
* video's id the engine is untouched — position, buffer and play state
|
|
194
|
+
* survive. Pass `surfaceId` to attach in the same call.
|
|
195
|
+
*/
|
|
196
|
+
setSource(source: VideoSource, options?: SetSourceOptions): void {
|
|
197
|
+
const autoplay = options?.autoplay ?? true;
|
|
198
|
+
const current = this.store.getState().currentVideo;
|
|
199
|
+
const sameVideo = current?.id === source.id;
|
|
200
|
+
|
|
201
|
+
if (!sameVideo) {
|
|
202
|
+
this.set({
|
|
203
|
+
currentVideo: source,
|
|
204
|
+
status: 'loading',
|
|
205
|
+
loading: true,
|
|
206
|
+
position: source.startPosition ?? 0,
|
|
207
|
+
duration: 0,
|
|
208
|
+
buffered: 0,
|
|
209
|
+
error: null,
|
|
210
|
+
});
|
|
211
|
+
NativeAuVideo.setSource(toNativeSource(source), autoplay);
|
|
212
|
+
this.events.emit('onVideoChanged', { video: source });
|
|
213
|
+
} else if (autoplay && !this.store.getState().playing) {
|
|
214
|
+
this.play();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (options?.surfaceId) {
|
|
218
|
+
this.attach(options.surfaceId);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Warm a source without rendering or touching current playback. */
|
|
223
|
+
preload(source: VideoSource): void {
|
|
224
|
+
NativeAuVideo.preload(toNativeSource(source));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
play(): void {
|
|
228
|
+
NativeAuVideo.play();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
pause(): void {
|
|
232
|
+
NativeAuVideo.pause();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
resume(): void {
|
|
236
|
+
NativeAuVideo.play();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
toggle(): void {
|
|
240
|
+
if (this.store.getState().playing) {
|
|
241
|
+
this.pause();
|
|
242
|
+
} else {
|
|
243
|
+
this.play();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
stop(): void {
|
|
248
|
+
NativeAuVideo.stop();
|
|
249
|
+
this.set({ position: 0, playing: false, status: 'idle' });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** @param position seconds */
|
|
253
|
+
seek(position: number): void {
|
|
254
|
+
const duration = this.store.getState().duration;
|
|
255
|
+
const clamped = Math.max(
|
|
256
|
+
0,
|
|
257
|
+
duration > 0 ? Math.min(position, duration) : position
|
|
258
|
+
);
|
|
259
|
+
this.set({ position: clamped });
|
|
260
|
+
NativeAuVideo.seekTo(clamped);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
seekBy(offset: number): void {
|
|
264
|
+
this.seek(this.store.getState().position + offset);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
setRate(rate: number): void {
|
|
268
|
+
this.set({ rate });
|
|
269
|
+
NativeAuVideo.setRate(rate);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
setVolume(volume: number): void {
|
|
273
|
+
const clamped = Math.max(0, Math.min(volume, 1));
|
|
274
|
+
this.set({ volume: clamped });
|
|
275
|
+
NativeAuVideo.setVolume(clamped);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
mute(): void {
|
|
279
|
+
this.set({ muted: true });
|
|
280
|
+
NativeAuVideo.setMuted(true);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
unmute(): void {
|
|
284
|
+
this.set({ muted: false });
|
|
285
|
+
NativeAuVideo.setMuted(false);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
setRepeat(repeat: boolean): void {
|
|
289
|
+
this.set({ repeat });
|
|
290
|
+
NativeAuVideo.setRepeat(repeat);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
setResizeMode(mode: ResizeMode): void {
|
|
294
|
+
this.set({ resizeMode: mode });
|
|
295
|
+
NativeAuVideo.setResizeMode(mode);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async getPosition(): Promise<number> {
|
|
299
|
+
return NativeAuVideo.getPosition();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// -------------------------------------------------------------- surfaces
|
|
303
|
+
|
|
304
|
+
/** Re-parent the player into the surface registered under `surfaceId`. */
|
|
305
|
+
attach(surfaceId: string): void {
|
|
306
|
+
if (!RESERVED_SURFACES.has(surfaceId)) {
|
|
307
|
+
this.lastInlineSurfaceId = surfaceId;
|
|
308
|
+
}
|
|
309
|
+
this.set({ surfaceId });
|
|
310
|
+
this.setMode(this.deriveMode({}));
|
|
311
|
+
NativeAuVideo.attach(surfaceId);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Detach from any surface. Playback continues (audio) unless configured otherwise. */
|
|
315
|
+
detach(): void {
|
|
316
|
+
this.set({ surfaceId: null });
|
|
317
|
+
this.setMode('hidden');
|
|
318
|
+
NativeAuVideo.detach();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Called by VideoSurface on unmount. Only clears JS state when the
|
|
323
|
+
* unmounting surface is the active one; the native registry has already
|
|
324
|
+
* dropped its weak reference.
|
|
325
|
+
*/
|
|
326
|
+
handleSurfaceUnmount(surfaceId: string): void {
|
|
327
|
+
const state = this.store.getState();
|
|
328
|
+
if (state.surfaceId === surfaceId) {
|
|
329
|
+
this.set({ surfaceId: null });
|
|
330
|
+
if (this.config.pauseOnDetach) {
|
|
331
|
+
this.pause();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (this.lastInlineSurfaceId === surfaceId) {
|
|
335
|
+
this.lastInlineSurfaceId = null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ------------------------------------------------------------ modes
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Show the built-in fullscreen host and unlock device rotation.
|
|
343
|
+
* The host mounts a surface and attaches automatically.
|
|
344
|
+
*/
|
|
345
|
+
enterFullscreen(): void {
|
|
346
|
+
if (this.store.getState().fullscreen) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
this.set({ fullscreen: true, floating: false });
|
|
350
|
+
this.setMode('fullscreen');
|
|
351
|
+
NativeAuVideo.enterFullscreen();
|
|
352
|
+
this.events.emit('onEnterFullscreen', undefined);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Restore the previous orientation lock and re-attach the previous surface. */
|
|
356
|
+
exitFullscreen(): void {
|
|
357
|
+
if (!this.store.getState().fullscreen) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
NativeAuVideo.exitFullscreen();
|
|
361
|
+
this.set({ fullscreen: false });
|
|
362
|
+
this.events.emit('onExitFullscreen', undefined);
|
|
363
|
+
this.restoreInlineSurface();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
toggleFullscreen(): void {
|
|
367
|
+
if (this.store.getState().fullscreen) {
|
|
368
|
+
this.exitFullscreen();
|
|
369
|
+
} else {
|
|
370
|
+
this.enterFullscreen();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
showFloating(): void {
|
|
375
|
+
if (this.store.getState().floating) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
this.set({ floating: true, fullscreen: false });
|
|
379
|
+
this.setMode('floating');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
hideFloating(): void {
|
|
383
|
+
if (!this.store.getState().floating) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
this.set({ floating: false });
|
|
387
|
+
this.restoreInlineSurface();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async enterPiP(): Promise<boolean> {
|
|
391
|
+
return NativeAuVideo.enterPip();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
exitPiP(): void {
|
|
395
|
+
NativeAuVideo.exitPip();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private restoreInlineSurface(): void {
|
|
399
|
+
if (this.lastInlineSurfaceId) {
|
|
400
|
+
this.attach(this.lastInlineSurfaceId);
|
|
401
|
+
} else {
|
|
402
|
+
this.set({ surfaceId: null });
|
|
403
|
+
this.setMode('hidden');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private deriveMode(override: Partial<Pick<VideoState, 'pip'>>): PlayerMode {
|
|
408
|
+
const s = { ...this.store.getState(), ...override };
|
|
409
|
+
if (s.pip) return 'pip';
|
|
410
|
+
if (s.fullscreen) return 'fullscreen';
|
|
411
|
+
if (s.floating) return 'floating';
|
|
412
|
+
if (s.surfaceId) return 'inline';
|
|
413
|
+
return 'hidden';
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private setMode(mode: PlayerMode): void {
|
|
417
|
+
if (this.store.getState().mode !== mode) {
|
|
418
|
+
this.set({ mode });
|
|
419
|
+
this.events.emit('onModeChanged', { mode });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ------------------------------------------------------------ lifecycle
|
|
424
|
+
|
|
425
|
+
/** Tear the native player down entirely. Rarely needed. */
|
|
426
|
+
destroy(): void {
|
|
427
|
+
for (const sub of this.nativeSubscriptions) {
|
|
428
|
+
sub.remove();
|
|
429
|
+
}
|
|
430
|
+
this.nativeSubscriptions = [];
|
|
431
|
+
this.events.removeAll();
|
|
432
|
+
this.initialized = false;
|
|
433
|
+
this.lastInlineSurfaceId = null;
|
|
434
|
+
NativeAuVideo.releasePlayer();
|
|
435
|
+
this.store.setState({ ...initialVideoState }, true);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private set(partial: Partial<VideoState>): void {
|
|
439
|
+
this.store.setState(partial);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useVideoManager } from '../provider/VideoContext';
|
|
3
|
+
import { usePlayback } from './usePlayback';
|
|
4
|
+
|
|
5
|
+
export function useFullscreen() {
|
|
6
|
+
const manager = useVideoManager();
|
|
7
|
+
const isFullscreen = usePlayback((s) => s.fullscreen);
|
|
8
|
+
|
|
9
|
+
const enter = useCallback(() => manager.enterFullscreen(), [manager]);
|
|
10
|
+
const exit = useCallback(() => manager.exitFullscreen(), [manager]);
|
|
11
|
+
const toggle = useCallback(() => manager.toggleFullscreen(), [manager]);
|
|
12
|
+
|
|
13
|
+
return { isFullscreen, enter, exit, toggle };
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useVideoManager } from '../provider/VideoContext';
|
|
3
|
+
import { usePlayback } from './usePlayback';
|
|
4
|
+
|
|
5
|
+
export function usePiP() {
|
|
6
|
+
const manager = useVideoManager();
|
|
7
|
+
const isActive = usePlayback((s) => s.pip);
|
|
8
|
+
|
|
9
|
+
const enter = useCallback(() => manager.enterPiP(), [manager]);
|
|
10
|
+
const exit = useCallback(() => manager.exitPiP(), [manager]);
|
|
11
|
+
|
|
12
|
+
return { isActive, enter, exit };
|
|
13
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useStore } from 'zustand';
|
|
2
|
+
import { useVideoManager } from '../provider/VideoContext';
|
|
3
|
+
import type { VideoState } from '../types/video';
|
|
4
|
+
|
|
5
|
+
const identity = (s: VideoState): VideoState => s;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Subscribe to playback state. Pass a selector so your component only
|
|
9
|
+
* re-renders for the fields it displays:
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const position = usePlayback((s) => s.position);
|
|
13
|
+
* const { playing, duration } = usePlayback((s) => ({ ... }));
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function usePlayback(): VideoState;
|
|
17
|
+
export function usePlayback<T>(selector: (state: VideoState) => T): T;
|
|
18
|
+
export function usePlayback<T>(
|
|
19
|
+
selector?: (state: VideoState) => T
|
|
20
|
+
): T | VideoState {
|
|
21
|
+
const manager = useVideoManager();
|
|
22
|
+
return useStore(
|
|
23
|
+
manager.store,
|
|
24
|
+
(selector ?? identity) as (state: VideoState) => T | VideoState
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useVideoManager } from '../provider/VideoContext';
|
|
2
|
+
import type { VideoManager } from '../core/VideoManager';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Command API for the singleton player:
|
|
6
|
+
* `play, pause, resume, toggle, stop, seek, seekBy, setSource, preload,
|
|
7
|
+
* attach, detach, enterFullscreen, exitFullscreen, showFloating,
|
|
8
|
+
* hideFloating, enterPiP, exitPiP, setRate, setVolume, mute, unmute,
|
|
9
|
+
* setRepeat, setResizeMode, addListener, destroy` — plus `store` for
|
|
10
|
+
* imperative reads (`useVideo().store.getState()`).
|
|
11
|
+
*
|
|
12
|
+
* Commands never cause re-renders; subscribe with `usePlayback()` for state.
|
|
13
|
+
*/
|
|
14
|
+
export function useVideo(): VideoManager {
|
|
15
|
+
return useVideoManager();
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useVideoManager } from '../provider/VideoContext';
|
|
3
|
+
import type { VideoEventMap, VideoEventName } from '../types/events';
|
|
4
|
+
|
|
5
|
+
export type VideoEventHandlers = {
|
|
6
|
+
[K in VideoEventName]?: (payload: VideoEventMap[K]) => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Subscribe to player events for the lifetime of the component.
|
|
11
|
+
* Handlers are kept in a ref, so inline functions are fine.
|
|
12
|
+
*
|
|
13
|
+
* ```tsx
|
|
14
|
+
* useVideoEvents({ onEnd: () => next(), onError: (e) => report(e) });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function useVideoEvents(handlers: VideoEventHandlers): void {
|
|
18
|
+
const manager = useVideoManager();
|
|
19
|
+
const ref = useRef(handlers);
|
|
20
|
+
ref.current = handlers;
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const names = Object.keys(ref.current) as VideoEventName[];
|
|
24
|
+
const subs = names.map((name) =>
|
|
25
|
+
manager.addListener(name, (payload) => {
|
|
26
|
+
const handler = ref.current[name] as
|
|
27
|
+
((p: VideoEventMap[typeof name]) => void) | undefined;
|
|
28
|
+
handler?.(payload);
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
return () => subs.forEach((s) => s.remove());
|
|
32
|
+
}, [manager]);
|
|
33
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export {
|
|
2
|
+
VideoProvider,
|
|
3
|
+
type VideoProviderProps,
|
|
4
|
+
} from './provider/VideoProvider';
|
|
5
|
+
export { useVideoManager } from './provider/VideoContext';
|
|
6
|
+
export {
|
|
7
|
+
VideoManager,
|
|
8
|
+
FULLSCREEN_SURFACE_ID,
|
|
9
|
+
FLOATING_SURFACE_ID,
|
|
10
|
+
} from './core/VideoManager';
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
VideoSurface,
|
|
14
|
+
type VideoSurfaceProps,
|
|
15
|
+
} from './components/VideoSurface';
|
|
16
|
+
export { VideoPlayer, type VideoPlayerProps } from './components/VideoPlayer';
|
|
17
|
+
export { FullscreenPlayer } from './components/FullscreenPlayer';
|
|
18
|
+
export {
|
|
19
|
+
FloatingPlayer,
|
|
20
|
+
type FloatingPlayerProps,
|
|
21
|
+
} from './components/FloatingPlayer';
|
|
22
|
+
export { MiniPlayer, type MiniPlayerProps } from './components/MiniPlayer';
|
|
23
|
+
export {
|
|
24
|
+
VideoControls,
|
|
25
|
+
type VideoControlsProps,
|
|
26
|
+
} from './components/VideoControls';
|
|
27
|
+
export {
|
|
28
|
+
GestureOverlay,
|
|
29
|
+
type GestureOverlayProps,
|
|
30
|
+
} from './components/GestureOverlay';
|
|
31
|
+
|
|
32
|
+
export { useVideo } from './hooks/useVideo';
|
|
33
|
+
export { usePlayback } from './hooks/usePlayback';
|
|
34
|
+
export { useFullscreen } from './hooks/useFullscreen';
|
|
35
|
+
export { usePiP } from './hooks/usePiP';
|
|
36
|
+
export {
|
|
37
|
+
useVideoEvents,
|
|
38
|
+
type VideoEventHandlers,
|
|
39
|
+
} from './hooks/useVideoEvents';
|
|
40
|
+
|
|
41
|
+
export { formatTime } from './utils/formatTime';
|
|
42
|
+
|
|
43
|
+
export type {
|
|
44
|
+
VideoSource,
|
|
45
|
+
ResizeMode,
|
|
46
|
+
PlayerMode,
|
|
47
|
+
PlaybackStatus,
|
|
48
|
+
VideoError,
|
|
49
|
+
VideoState,
|
|
50
|
+
SetSourceOptions,
|
|
51
|
+
VideoProviderConfig,
|
|
52
|
+
VideoEventMap,
|
|
53
|
+
VideoEventName,
|
|
54
|
+
} from './types';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { VideoManager } from '../core/VideoManager';
|
|
3
|
+
|
|
4
|
+
export const VideoContext = createContext<VideoManager | null>(null);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the app-wide VideoManager. Falls back to the shared singleton so
|
|
8
|
+
* the API also works outside the provider (e.g. headless/background code).
|
|
9
|
+
*/
|
|
10
|
+
export function useVideoManager(): VideoManager {
|
|
11
|
+
return useContext(VideoContext) ?? VideoManager.shared;
|
|
12
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useEffect, useMemo, type PropsWithChildren } from 'react';
|
|
2
|
+
import { VideoManager } from '../core/VideoManager';
|
|
3
|
+
import type { VideoProviderConfig } from '../types/video';
|
|
4
|
+
import { VideoContext } from './VideoContext';
|
|
5
|
+
import { FullscreenPlayer } from '../components/FullscreenPlayer';
|
|
6
|
+
import { FloatingPlayer } from '../components/FloatingPlayer';
|
|
7
|
+
|
|
8
|
+
export interface VideoProviderProps extends PropsWithChildren {
|
|
9
|
+
config?: VideoProviderConfig;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wrap the app once. Creates the singleton native player silently and
|
|
14
|
+
* mounts the fullscreen/floating hosts above the app so `enterFullscreen()`
|
|
15
|
+
* and `showFloating()` work from any screen.
|
|
16
|
+
*
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <VideoProvider>
|
|
19
|
+
* <Navigation />
|
|
20
|
+
* </VideoProvider>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function VideoProvider({ children, config }: VideoProviderProps) {
|
|
24
|
+
const manager = useMemo(() => VideoManager.shared, []);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
manager.init(config);
|
|
28
|
+
// Intentionally no destroy on unmount: the engine outlives React trees
|
|
29
|
+
// (and survives Fast Refresh). Call manager.destroy() explicitly to tear down.
|
|
30
|
+
}, [manager, config]);
|
|
31
|
+
|
|
32
|
+
const cfg = manager.providerConfig;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<VideoContext.Provider value={manager}>
|
|
36
|
+
{children}
|
|
37
|
+
{cfg.floatingHost !== false ? <FloatingPlayer /> : null}
|
|
38
|
+
{cfg.fullscreenHost !== false ? <FullscreenPlayer /> : null}
|
|
39
|
+
</VideoContext.Provider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createStore, type StoreApi } from 'zustand/vanilla';
|
|
2
|
+
import type { VideoState } from '../types/video';
|
|
3
|
+
|
|
4
|
+
export const initialVideoState: VideoState = {
|
|
5
|
+
currentVideo: null,
|
|
6
|
+
status: 'idle',
|
|
7
|
+
playing: false,
|
|
8
|
+
paused: false,
|
|
9
|
+
buffering: false,
|
|
10
|
+
loading: false,
|
|
11
|
+
position: 0,
|
|
12
|
+
duration: 0,
|
|
13
|
+
buffered: 0,
|
|
14
|
+
rate: 1,
|
|
15
|
+
volume: 1,
|
|
16
|
+
muted: false,
|
|
17
|
+
repeat: false,
|
|
18
|
+
resizeMode: 'contain',
|
|
19
|
+
fullscreen: false,
|
|
20
|
+
pip: false,
|
|
21
|
+
floating: false,
|
|
22
|
+
mode: 'hidden',
|
|
23
|
+
surfaceId: null,
|
|
24
|
+
videoWidth: 0,
|
|
25
|
+
videoHeight: 0,
|
|
26
|
+
error: null,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type VideoStore = StoreApi<VideoState>;
|
|
30
|
+
|
|
31
|
+
export function createVideoStore(): VideoStore {
|
|
32
|
+
return createStore<VideoState>()(() => ({ ...initialVideoState }));
|
|
33
|
+
}
|