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,76 @@
|
|
|
1
|
+
import { useCallback, useRef, type PropsWithChildren } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
type GestureResponderEvent,
|
|
6
|
+
type LayoutChangeEvent,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
|
|
9
|
+
export interface GestureOverlayProps extends PropsWithChildren {
|
|
10
|
+
onSingleTap?: () => void;
|
|
11
|
+
onDoubleTapLeft?: () => void;
|
|
12
|
+
onDoubleTapRight?: () => void;
|
|
13
|
+
onLongPress?: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DOUBLE_TAP_MS = 280;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tap layer for player chrome: single tap (toggle controls), double tap
|
|
20
|
+
* left/right (seek back/forward), long press. Pinch/brightness/volume
|
|
21
|
+
* gestures are on the roadmap (need native cooperation).
|
|
22
|
+
*/
|
|
23
|
+
export function GestureOverlay({
|
|
24
|
+
onSingleTap,
|
|
25
|
+
onDoubleTapLeft,
|
|
26
|
+
onDoubleTapRight,
|
|
27
|
+
onLongPress,
|
|
28
|
+
children,
|
|
29
|
+
}: GestureOverlayProps) {
|
|
30
|
+
const width = useRef(0);
|
|
31
|
+
const lastTap = useRef(0);
|
|
32
|
+
const singleTapTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
33
|
+
|
|
34
|
+
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
|
35
|
+
width.current = e.nativeEvent.layout.width;
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const handlePress = useCallback(
|
|
39
|
+
(e: GestureResponderEvent) => {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const x = e.nativeEvent.locationX;
|
|
42
|
+
|
|
43
|
+
if (now - lastTap.current < DOUBLE_TAP_MS) {
|
|
44
|
+
lastTap.current = 0;
|
|
45
|
+
if (singleTapTimer.current) {
|
|
46
|
+
clearTimeout(singleTapTimer.current);
|
|
47
|
+
singleTapTimer.current = null;
|
|
48
|
+
}
|
|
49
|
+
if (x < width.current / 2) {
|
|
50
|
+
onDoubleTapLeft?.();
|
|
51
|
+
} else {
|
|
52
|
+
onDoubleTapRight?.();
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
lastTap.current = now;
|
|
58
|
+
singleTapTimer.current = setTimeout(() => {
|
|
59
|
+
singleTapTimer.current = null;
|
|
60
|
+
onSingleTap?.();
|
|
61
|
+
}, DOUBLE_TAP_MS);
|
|
62
|
+
},
|
|
63
|
+
[onSingleTap, onDoubleTapLeft, onDoubleTapRight]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Pressable
|
|
68
|
+
style={StyleSheet.absoluteFill}
|
|
69
|
+
onLayout={onLayout}
|
|
70
|
+
onPress={handlePress}
|
|
71
|
+
onLongPress={onLongPress}
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
</Pressable>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Pressable,
|
|
3
|
+
StyleSheet,
|
|
4
|
+
Text,
|
|
5
|
+
View,
|
|
6
|
+
type ViewProps,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
import { usePlayback } from '../hooks/usePlayback';
|
|
9
|
+
import { useVideoManager } from '../provider/VideoContext';
|
|
10
|
+
import { VideoSurface } from './VideoSurface';
|
|
11
|
+
|
|
12
|
+
export interface MiniPlayerProps extends ViewProps {
|
|
13
|
+
/** Surface id. Default "__au_mini__". */
|
|
14
|
+
surfaceId?: string;
|
|
15
|
+
/** Tapping the bar (e.g. navigate back to the detail screen). */
|
|
16
|
+
onPress?: () => void;
|
|
17
|
+
/** Called by the ✕ button; button hidden when omitted. */
|
|
18
|
+
onClose?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Docked mini player bar (Spotify style): thumbnail surface, title,
|
|
23
|
+
* play/pause and close. Attach it with `attach(surfaceId)` — typically when
|
|
24
|
+
* the user leaves the screen that owned playback.
|
|
25
|
+
*/
|
|
26
|
+
export function MiniPlayer({
|
|
27
|
+
surfaceId = '__au_mini__',
|
|
28
|
+
onPress,
|
|
29
|
+
onClose,
|
|
30
|
+
style,
|
|
31
|
+
...rest
|
|
32
|
+
}: MiniPlayerProps) {
|
|
33
|
+
const manager = useVideoManager();
|
|
34
|
+
const playing = usePlayback((s) => s.playing);
|
|
35
|
+
const title = usePlayback((s) => s.currentVideo?.title ?? '');
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<View style={[styles.bar, style]} {...rest}>
|
|
39
|
+
<Pressable style={styles.body} onPress={onPress}>
|
|
40
|
+
<VideoSurface surfaceId={surfaceId} style={styles.thumb} />
|
|
41
|
+
<Text style={styles.title} numberOfLines={1}>
|
|
42
|
+
{title}
|
|
43
|
+
</Text>
|
|
44
|
+
</Pressable>
|
|
45
|
+
<Pressable
|
|
46
|
+
style={styles.button}
|
|
47
|
+
hitSlop={8}
|
|
48
|
+
onPress={() => manager.toggle()}
|
|
49
|
+
>
|
|
50
|
+
<Text style={styles.icon}>{playing ? '❙❙' : '▶'}</Text>
|
|
51
|
+
</Pressable>
|
|
52
|
+
{onClose ? (
|
|
53
|
+
<Pressable style={styles.button} hitSlop={8} onPress={onClose}>
|
|
54
|
+
<Text style={styles.icon}>✕</Text>
|
|
55
|
+
</Pressable>
|
|
56
|
+
) : null}
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const styles = StyleSheet.create({
|
|
62
|
+
bar: {
|
|
63
|
+
flexDirection: 'row',
|
|
64
|
+
alignItems: 'center',
|
|
65
|
+
backgroundColor: '#1c1c1e',
|
|
66
|
+
height: 56,
|
|
67
|
+
paddingRight: 8,
|
|
68
|
+
},
|
|
69
|
+
body: {
|
|
70
|
+
flex: 1,
|
|
71
|
+
flexDirection: 'row',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
gap: 10,
|
|
74
|
+
},
|
|
75
|
+
thumb: {
|
|
76
|
+
width: 56 * (16 / 9),
|
|
77
|
+
height: 56,
|
|
78
|
+
backgroundColor: '#000',
|
|
79
|
+
},
|
|
80
|
+
title: {
|
|
81
|
+
flex: 1,
|
|
82
|
+
color: '#fff',
|
|
83
|
+
fontSize: 14,
|
|
84
|
+
},
|
|
85
|
+
button: {
|
|
86
|
+
paddingHorizontal: 10,
|
|
87
|
+
paddingVertical: 8,
|
|
88
|
+
},
|
|
89
|
+
icon: {
|
|
90
|
+
color: '#fff',
|
|
91
|
+
fontSize: 16,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
Text,
|
|
6
|
+
View,
|
|
7
|
+
type GestureResponderEvent,
|
|
8
|
+
type LayoutChangeEvent,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import { useVideoManager } from '../provider/VideoContext';
|
|
11
|
+
import { usePlayback } from '../hooks/usePlayback';
|
|
12
|
+
import { formatTime } from '../utils/formatTime';
|
|
13
|
+
import { GestureOverlay } from './GestureOverlay';
|
|
14
|
+
|
|
15
|
+
export interface VideoControlsProps {
|
|
16
|
+
/** Seconds jumped by double-tap. Default 10. */
|
|
17
|
+
doubleTapSeek?: number;
|
|
18
|
+
/** Auto-hide delay in ms. Default 3000. */
|
|
19
|
+
hideAfter?: number;
|
|
20
|
+
/** Show the fullscreen toggle button. Default true. */
|
|
21
|
+
showFullscreenButton?: boolean;
|
|
22
|
+
/** Called by the close (✕) button; button hidden when omitted. */
|
|
23
|
+
onClose?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Minimal built-in chrome: play/pause, seek bar, time, mute and fullscreen
|
|
28
|
+
* toggles, with tap-to-show / double-tap-to-seek gestures. Apps wanting a
|
|
29
|
+
* custom design can ignore this and build on usePlayback()/useVideo().
|
|
30
|
+
*/
|
|
31
|
+
export function VideoControls({
|
|
32
|
+
doubleTapSeek = 10,
|
|
33
|
+
hideAfter = 3000,
|
|
34
|
+
showFullscreenButton = true,
|
|
35
|
+
onClose,
|
|
36
|
+
}: VideoControlsProps) {
|
|
37
|
+
const manager = useVideoManager();
|
|
38
|
+
const playing = usePlayback((s) => s.playing);
|
|
39
|
+
const buffering = usePlayback((s) => s.buffering);
|
|
40
|
+
const position = usePlayback((s) => s.position);
|
|
41
|
+
const duration = usePlayback((s) => s.duration);
|
|
42
|
+
const muted = usePlayback((s) => s.muted);
|
|
43
|
+
const fullscreen = usePlayback((s) => s.fullscreen);
|
|
44
|
+
|
|
45
|
+
const [visible, setVisible] = useState(true);
|
|
46
|
+
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
47
|
+
const trackWidth = useRef(0);
|
|
48
|
+
|
|
49
|
+
const scheduleHide = useCallback(() => {
|
|
50
|
+
if (hideTimer.current) {
|
|
51
|
+
clearTimeout(hideTimer.current);
|
|
52
|
+
}
|
|
53
|
+
hideTimer.current = setTimeout(() => setVisible(false), hideAfter);
|
|
54
|
+
}, [hideAfter]);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (visible && playing) {
|
|
58
|
+
scheduleHide();
|
|
59
|
+
}
|
|
60
|
+
return () => {
|
|
61
|
+
if (hideTimer.current) {
|
|
62
|
+
clearTimeout(hideTimer.current);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}, [visible, playing, scheduleHide]);
|
|
66
|
+
|
|
67
|
+
const toggleVisible = useCallback(() => setVisible((v) => !v), []);
|
|
68
|
+
|
|
69
|
+
const onTrackLayout = useCallback((e: LayoutChangeEvent) => {
|
|
70
|
+
trackWidth.current = e.nativeEvent.layout.width;
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const onTrackPress = useCallback(
|
|
74
|
+
(e: GestureResponderEvent) => {
|
|
75
|
+
if (trackWidth.current > 0 && duration > 0) {
|
|
76
|
+
const ratio = e.nativeEvent.locationX / trackWidth.current;
|
|
77
|
+
manager.seek(ratio * duration);
|
|
78
|
+
}
|
|
79
|
+
scheduleHide();
|
|
80
|
+
},
|
|
81
|
+
[manager, duration, scheduleHide]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const progress = duration > 0 ? Math.min(position / duration, 1) : 0;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<View style={StyleSheet.absoluteFill} pointerEvents="box-none">
|
|
88
|
+
<GestureOverlay
|
|
89
|
+
onSingleTap={toggleVisible}
|
|
90
|
+
onDoubleTapLeft={() => manager.seekBy(-doubleTapSeek)}
|
|
91
|
+
onDoubleTapRight={() => manager.seekBy(doubleTapSeek)}
|
|
92
|
+
/>
|
|
93
|
+
{visible ? (
|
|
94
|
+
<View style={styles.chrome} pointerEvents="box-none">
|
|
95
|
+
<View style={styles.topRow}>
|
|
96
|
+
{onClose ? (
|
|
97
|
+
<Pressable style={styles.button} onPress={onClose} hitSlop={8}>
|
|
98
|
+
<Text style={styles.icon}>✕</Text>
|
|
99
|
+
</Pressable>
|
|
100
|
+
) : (
|
|
101
|
+
<View />
|
|
102
|
+
)}
|
|
103
|
+
<Pressable
|
|
104
|
+
style={styles.button}
|
|
105
|
+
onPress={() => (muted ? manager.unmute() : manager.mute())}
|
|
106
|
+
hitSlop={8}
|
|
107
|
+
>
|
|
108
|
+
<Text style={styles.icon}>{muted ? '🔇' : '🔊'}</Text>
|
|
109
|
+
</Pressable>
|
|
110
|
+
</View>
|
|
111
|
+
|
|
112
|
+
<Pressable
|
|
113
|
+
style={styles.playButton}
|
|
114
|
+
onPress={() => {
|
|
115
|
+
manager.toggle();
|
|
116
|
+
scheduleHide();
|
|
117
|
+
}}
|
|
118
|
+
hitSlop={16}
|
|
119
|
+
>
|
|
120
|
+
<Text style={styles.playIcon}>
|
|
121
|
+
{buffering ? '…' : playing ? '❙❙' : '▶'}
|
|
122
|
+
</Text>
|
|
123
|
+
</Pressable>
|
|
124
|
+
|
|
125
|
+
<View style={styles.bottomRow}>
|
|
126
|
+
<Text style={styles.time}>{formatTime(position)}</Text>
|
|
127
|
+
<Pressable
|
|
128
|
+
style={styles.track}
|
|
129
|
+
onLayout={onTrackLayout}
|
|
130
|
+
onPress={onTrackPress}
|
|
131
|
+
>
|
|
132
|
+
<View style={styles.trackBg} />
|
|
133
|
+
<View
|
|
134
|
+
style={[styles.trackFill, { width: `${progress * 100}%` }]}
|
|
135
|
+
/>
|
|
136
|
+
</Pressable>
|
|
137
|
+
<Text style={styles.time}>{formatTime(duration)}</Text>
|
|
138
|
+
{showFullscreenButton ? (
|
|
139
|
+
<Pressable
|
|
140
|
+
style={styles.button}
|
|
141
|
+
onPress={() => manager.toggleFullscreen()}
|
|
142
|
+
hitSlop={8}
|
|
143
|
+
>
|
|
144
|
+
<Text style={styles.icon}>{fullscreen ? '⤡' : '⤢'}</Text>
|
|
145
|
+
</Pressable>
|
|
146
|
+
) : null}
|
|
147
|
+
</View>
|
|
148
|
+
</View>
|
|
149
|
+
) : null}
|
|
150
|
+
</View>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const styles = StyleSheet.create({
|
|
155
|
+
chrome: {
|
|
156
|
+
position: 'absolute',
|
|
157
|
+
top: 0,
|
|
158
|
+
left: 0,
|
|
159
|
+
right: 0,
|
|
160
|
+
bottom: 0,
|
|
161
|
+
backgroundColor: 'rgba(0,0,0,0.35)',
|
|
162
|
+
justifyContent: 'space-between',
|
|
163
|
+
},
|
|
164
|
+
topRow: {
|
|
165
|
+
flexDirection: 'row',
|
|
166
|
+
justifyContent: 'space-between',
|
|
167
|
+
padding: 12,
|
|
168
|
+
},
|
|
169
|
+
playButton: {
|
|
170
|
+
alignSelf: 'center',
|
|
171
|
+
},
|
|
172
|
+
playIcon: {
|
|
173
|
+
color: '#fff',
|
|
174
|
+
fontSize: 34,
|
|
175
|
+
},
|
|
176
|
+
bottomRow: {
|
|
177
|
+
flexDirection: 'row',
|
|
178
|
+
alignItems: 'center',
|
|
179
|
+
paddingHorizontal: 12,
|
|
180
|
+
paddingBottom: 12,
|
|
181
|
+
gap: 8,
|
|
182
|
+
},
|
|
183
|
+
time: {
|
|
184
|
+
color: '#fff',
|
|
185
|
+
fontSize: 12,
|
|
186
|
+
fontVariant: ['tabular-nums'],
|
|
187
|
+
},
|
|
188
|
+
track: {
|
|
189
|
+
flex: 1,
|
|
190
|
+
height: 24,
|
|
191
|
+
justifyContent: 'center',
|
|
192
|
+
},
|
|
193
|
+
trackBg: {
|
|
194
|
+
position: 'absolute',
|
|
195
|
+
left: 0,
|
|
196
|
+
right: 0,
|
|
197
|
+
height: 3,
|
|
198
|
+
borderRadius: 1.5,
|
|
199
|
+
backgroundColor: 'rgba(255,255,255,0.35)',
|
|
200
|
+
},
|
|
201
|
+
trackFill: {
|
|
202
|
+
height: 3,
|
|
203
|
+
borderRadius: 1.5,
|
|
204
|
+
backgroundColor: '#fff',
|
|
205
|
+
},
|
|
206
|
+
button: {
|
|
207
|
+
padding: 4,
|
|
208
|
+
},
|
|
209
|
+
icon: {
|
|
210
|
+
color: '#fff',
|
|
211
|
+
fontSize: 18,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { StyleSheet, View, type ViewProps } from 'react-native';
|
|
3
|
+
import { useVideoManager } from '../provider/VideoContext';
|
|
4
|
+
import type { ResizeMode, VideoSource } from '../types/video';
|
|
5
|
+
import { VideoControls } from './VideoControls';
|
|
6
|
+
import { VideoSurface } from './VideoSurface';
|
|
7
|
+
|
|
8
|
+
export interface VideoPlayerProps extends ViewProps {
|
|
9
|
+
source: VideoSource;
|
|
10
|
+
/** Default true. */
|
|
11
|
+
autoplay?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Surface id to register/attach under. Defaults to `player:<source.id>`,
|
|
14
|
+
* so a feed item and a detail screen showing the same video naturally
|
|
15
|
+
* hand the player off to whichever mounted last.
|
|
16
|
+
*/
|
|
17
|
+
surfaceId?: string;
|
|
18
|
+
/** Show built-in controls. Default true. */
|
|
19
|
+
controls?: boolean;
|
|
20
|
+
resizeMode?: ResizeMode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convenience all-in-one player: `setSource` (same-video handoff aware) +
|
|
25
|
+
* `attach` + surface + optional controls. Rendering two VideoPlayers with
|
|
26
|
+
* the same source id moves the ONE engine — it never creates a second one.
|
|
27
|
+
*
|
|
28
|
+
* ```tsx
|
|
29
|
+
* <VideoPlayer source={{ id: '123', uri }} style={{ aspectRatio: 16 / 9 }} />
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function VideoPlayer({
|
|
33
|
+
source,
|
|
34
|
+
autoplay = true,
|
|
35
|
+
surfaceId,
|
|
36
|
+
controls = true,
|
|
37
|
+
resizeMode,
|
|
38
|
+
style,
|
|
39
|
+
...rest
|
|
40
|
+
}: VideoPlayerProps) {
|
|
41
|
+
const manager = useVideoManager();
|
|
42
|
+
const id = surfaceId ?? `player:${source.id}`;
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
manager.setSource(source, { autoplay, surfaceId: id });
|
|
46
|
+
// Attach on mount / when the video identity changes. Other source
|
|
47
|
+
// fields (title, headers) don't retrigger: identity is source.id.
|
|
48
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
49
|
+
}, [manager, source.id, id]);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (resizeMode) {
|
|
53
|
+
manager.setResizeMode(resizeMode);
|
|
54
|
+
}
|
|
55
|
+
}, [manager, resizeMode]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<View style={[styles.container, style]} {...rest}>
|
|
59
|
+
<VideoSurface surfaceId={id} style={styles.surface} />
|
|
60
|
+
{controls ? <VideoControls /> : null}
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const styles = StyleSheet.create({
|
|
66
|
+
container: {
|
|
67
|
+
backgroundColor: '#000',
|
|
68
|
+
overflow: 'hidden',
|
|
69
|
+
},
|
|
70
|
+
surface: {
|
|
71
|
+
position: 'absolute',
|
|
72
|
+
top: 0,
|
|
73
|
+
left: 0,
|
|
74
|
+
right: 0,
|
|
75
|
+
bottom: 0,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import type { ViewProps } from 'react-native';
|
|
3
|
+
import AuVideoSurfaceNativeComponent from '../AuVideoSurfaceNativeComponent';
|
|
4
|
+
import { useVideoManager } from '../provider/VideoContext';
|
|
5
|
+
|
|
6
|
+
export interface VideoSurfaceProps extends ViewProps {
|
|
7
|
+
/** Id this surface registers under. Attach the player with `attach(id)`. */
|
|
8
|
+
surfaceId: string;
|
|
9
|
+
/** Attach the player to this surface as soon as it mounts. Default false. */
|
|
10
|
+
autoAttach?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A dumb rendering surface. It never creates a player — it only marks a
|
|
15
|
+
* spot where the singleton engine can render. Mount as many as you like;
|
|
16
|
+
* the player shows in at most one at a time.
|
|
17
|
+
*
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <VideoSurface surfaceId="feed" style={{ aspectRatio: 16 / 9 }} />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function VideoSurface({
|
|
23
|
+
surfaceId,
|
|
24
|
+
autoAttach = false,
|
|
25
|
+
...rest
|
|
26
|
+
}: VideoSurfaceProps) {
|
|
27
|
+
const manager = useVideoManager();
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (autoAttach) {
|
|
31
|
+
manager.attach(surfaceId);
|
|
32
|
+
}
|
|
33
|
+
return () => {
|
|
34
|
+
manager.handleSurfaceUnmount(surfaceId);
|
|
35
|
+
};
|
|
36
|
+
}, [manager, surfaceId, autoAttach]);
|
|
37
|
+
|
|
38
|
+
return <AuVideoSurfaceNativeComponent surfaceId={surfaceId} {...rest} />;
|
|
39
|
+
}
|