@umituz/react-native-video-editor 1.1.43 → 1.1.45
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/package.json +1 -1
- package/src/index.ts +14 -0
- package/src/player/index.ts +14 -0
- package/src/player/infrastructure/services/player-control.service.ts +53 -1
- package/src/player/presentation/components/FullScreenVideoPlayer.tsx +169 -0
- package/src/player/presentation/components/VideoPlayer.tsx +42 -3
- package/src/player/presentation/components/VideoPlayerOverlay.tsx +172 -0
- package/src/player/presentation/components/VideoProgressBar.tsx +90 -0
- package/src/player/presentation/hooks/useControlsAutoHide.ts +66 -0
- package/src/player/presentation/hooks/useVideoPlaybackProgress.ts +63 -0
- package/src/player/presentation/hooks/useVideoPlayerControl.ts +39 -3
- package/src/player/types/index.ts +85 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-video-editor",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.45",
|
|
4
4
|
"description": "Professional video editor with layer-based timeline, text/image/shape/audio/animation layers, and export functionality",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -125,6 +125,12 @@ export type {
|
|
|
125
125
|
VideoVisibilityConfig,
|
|
126
126
|
VideoPlayerProps,
|
|
127
127
|
VideoPlayerType,
|
|
128
|
+
PlaybackProgressState,
|
|
129
|
+
ControlsAutoHideConfig,
|
|
130
|
+
ControlsAutoHideResult,
|
|
131
|
+
VideoProgressBarProps,
|
|
132
|
+
VideoPlayerOverlayProps,
|
|
133
|
+
FullScreenVideoPlayerProps,
|
|
128
134
|
} from "./player";
|
|
129
135
|
|
|
130
136
|
export {
|
|
@@ -133,7 +139,15 @@ export {
|
|
|
133
139
|
safeToggle,
|
|
134
140
|
isPlayerReady,
|
|
135
141
|
configurePlayer,
|
|
142
|
+
safeSeekTo,
|
|
143
|
+
safeMute,
|
|
144
|
+
safeReplay,
|
|
136
145
|
useVideoPlayerControl,
|
|
137
146
|
useVideoVisibility,
|
|
147
|
+
useVideoPlaybackProgress,
|
|
148
|
+
useControlsAutoHide,
|
|
138
149
|
VideoPlayer,
|
|
150
|
+
VideoProgressBar,
|
|
151
|
+
VideoPlayerOverlay,
|
|
152
|
+
FullScreenVideoPlayer,
|
|
139
153
|
} from "./player";
|
package/src/player/index.ts
CHANGED
|
@@ -15,6 +15,12 @@ export type {
|
|
|
15
15
|
VideoDownloadProgressCallback,
|
|
16
16
|
VideoCacheResult,
|
|
17
17
|
VideoCachingState,
|
|
18
|
+
PlaybackProgressState,
|
|
19
|
+
ControlsAutoHideConfig,
|
|
20
|
+
ControlsAutoHideResult,
|
|
21
|
+
VideoProgressBarProps,
|
|
22
|
+
VideoPlayerOverlayProps,
|
|
23
|
+
FullScreenVideoPlayerProps,
|
|
18
24
|
} from "./types";
|
|
19
25
|
|
|
20
26
|
// Services
|
|
@@ -24,6 +30,9 @@ export {
|
|
|
24
30
|
safeToggle,
|
|
25
31
|
isPlayerReady,
|
|
26
32
|
configurePlayer,
|
|
33
|
+
safeSeekTo,
|
|
34
|
+
safeMute,
|
|
35
|
+
safeReplay,
|
|
27
36
|
} from "./infrastructure/services/player-control.service";
|
|
28
37
|
|
|
29
38
|
export {
|
|
@@ -39,6 +48,11 @@ export {
|
|
|
39
48
|
export { useVideoPlayerControl } from "./presentation/hooks/useVideoPlayerControl";
|
|
40
49
|
export { useVideoVisibility } from "./presentation/hooks/useVideoVisibility";
|
|
41
50
|
export { useVideoCaching } from "./presentation/hooks/useVideoCaching";
|
|
51
|
+
export { useVideoPlaybackProgress } from "./presentation/hooks/useVideoPlaybackProgress";
|
|
52
|
+
export { useControlsAutoHide } from "./presentation/hooks/useControlsAutoHide";
|
|
42
53
|
|
|
43
54
|
// Components
|
|
44
55
|
export { VideoPlayer } from "./presentation/components/VideoPlayer";
|
|
56
|
+
export { VideoProgressBar } from "./presentation/components/VideoProgressBar";
|
|
57
|
+
export { VideoPlayerOverlay } from "./presentation/components/VideoPlayerOverlay";
|
|
58
|
+
export { FullScreenVideoPlayer } from "./presentation/components/FullScreenVideoPlayer";
|
|
@@ -96,8 +96,60 @@ export const configurePlayer = (
|
|
|
96
96
|
}
|
|
97
97
|
} catch (error) {
|
|
98
98
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
console.log("[VideoPlayer] Configure error ignored:", error);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Safely seek to a specific time position (in seconds)
|
|
107
|
+
*/
|
|
108
|
+
export const safeSeekTo = (player: VideoPlayer | null, seconds: number): boolean => {
|
|
109
|
+
if (!player) return false;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
player.currentTime = Math.max(0, seconds);
|
|
113
|
+
return true;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
116
|
+
console.log("[VideoPlayer] SeekTo error ignored:", error);
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Safely set muted state
|
|
124
|
+
*/
|
|
125
|
+
export const safeMute = (player: VideoPlayer | null, muted: boolean): boolean => {
|
|
126
|
+
if (!player) return false;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
player.muted = muted;
|
|
130
|
+
return true;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
133
|
+
console.log("[VideoPlayer] Mute error ignored:", error);
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Safely replay video from beginning
|
|
141
|
+
*/
|
|
142
|
+
export const safeReplay = (player: VideoPlayer | null): boolean => {
|
|
143
|
+
if (!player) return false;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
player.currentTime = 0;
|
|
147
|
+
player.play();
|
|
148
|
+
return true;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
151
|
+
console.log("[VideoPlayer] Replay error ignored:", error);
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FullScreenVideoPlayer Component
|
|
3
|
+
* Modal-based fullscreen video player with custom overlay controls
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo, useCallback } from "react";
|
|
7
|
+
import { View, Modal, StyleSheet, StatusBar } from "react-native";
|
|
8
|
+
import { Image } from "expo-image";
|
|
9
|
+
// expo-video is optional — lazy require
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
let VideoView: React.ComponentType<any> = () => null;
|
|
12
|
+
try {
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
14
|
+
VideoView = require("expo-video").VideoView;
|
|
15
|
+
} catch {
|
|
16
|
+
// expo-video not installed
|
|
17
|
+
}
|
|
18
|
+
import { AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
19
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
20
|
+
|
|
21
|
+
import type { FullScreenVideoPlayerProps } from "../../types";
|
|
22
|
+
import { useVideoPlayerControl } from "../hooks/useVideoPlayerControl";
|
|
23
|
+
import { useVideoCaching } from "../hooks/useVideoCaching";
|
|
24
|
+
import { useControlsAutoHide } from "../hooks/useControlsAutoHide";
|
|
25
|
+
import { VideoPlayerOverlay } from "./VideoPlayerOverlay";
|
|
26
|
+
|
|
27
|
+
export const FullScreenVideoPlayer: React.FC<FullScreenVideoPlayerProps> = ({
|
|
28
|
+
visible,
|
|
29
|
+
source,
|
|
30
|
+
title,
|
|
31
|
+
subtitle,
|
|
32
|
+
thumbnailUrl,
|
|
33
|
+
onClose,
|
|
34
|
+
loop = true,
|
|
35
|
+
autoPlay = true,
|
|
36
|
+
}) => {
|
|
37
|
+
const tokens = useAppDesignTokens();
|
|
38
|
+
|
|
39
|
+
const { localUri, isDownloading, downloadProgress, error } = useVideoCaching(
|
|
40
|
+
visible ? source : null,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const { player, state, controls } = useVideoPlayerControl({
|
|
44
|
+
source: visible ? localUri : null,
|
|
45
|
+
loop,
|
|
46
|
+
muted: false,
|
|
47
|
+
autoPlay,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const { visible: controlsVisible, toggle: toggleControls } = useControlsAutoHide({
|
|
51
|
+
isPlaying: state.isPlaying,
|
|
52
|
+
autoHideDelay: 3000,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const handleClose = useCallback(() => {
|
|
56
|
+
controls.pause();
|
|
57
|
+
onClose();
|
|
58
|
+
}, [controls, onClose]);
|
|
59
|
+
|
|
60
|
+
const styles = useMemo(
|
|
61
|
+
() =>
|
|
62
|
+
StyleSheet.create({
|
|
63
|
+
container: {
|
|
64
|
+
flex: 1,
|
|
65
|
+
backgroundColor: "#000000",
|
|
66
|
+
justifyContent: "center",
|
|
67
|
+
alignItems: "center",
|
|
68
|
+
},
|
|
69
|
+
video: {
|
|
70
|
+
width: "100%",
|
|
71
|
+
height: "100%",
|
|
72
|
+
},
|
|
73
|
+
centerContent: {
|
|
74
|
+
...StyleSheet.absoluteFillObject,
|
|
75
|
+
justifyContent: "center",
|
|
76
|
+
alignItems: "center",
|
|
77
|
+
},
|
|
78
|
+
progressText: {
|
|
79
|
+
color: "#FFFFFF",
|
|
80
|
+
fontSize: 18,
|
|
81
|
+
fontWeight: "600",
|
|
82
|
+
},
|
|
83
|
+
errorText: {
|
|
84
|
+
color: tokens.colors.error,
|
|
85
|
+
fontSize: 16,
|
|
86
|
+
textAlign: "center",
|
|
87
|
+
padding: 24,
|
|
88
|
+
},
|
|
89
|
+
thumbnail: {
|
|
90
|
+
...StyleSheet.absoluteFillObject,
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
[tokens],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const renderContent = () => {
|
|
97
|
+
if (error) {
|
|
98
|
+
return (
|
|
99
|
+
<View style={styles.container}>
|
|
100
|
+
<AtomicText style={styles.errorText}>{error}</AtomicText>
|
|
101
|
+
</View>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (isDownloading) {
|
|
106
|
+
return (
|
|
107
|
+
<View style={styles.container}>
|
|
108
|
+
{thumbnailUrl && (
|
|
109
|
+
<Image source={{ uri: thumbnailUrl }} style={styles.thumbnail} contentFit="cover" />
|
|
110
|
+
)}
|
|
111
|
+
<View style={[styles.centerContent, { backgroundColor: "rgba(0,0,0,0.5)" }]}>
|
|
112
|
+
<AtomicText style={styles.progressText}>
|
|
113
|
+
{Math.round(downloadProgress * 100)}%
|
|
114
|
+
</AtomicText>
|
|
115
|
+
</View>
|
|
116
|
+
</View>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (state.isPlayerValid && player) {
|
|
121
|
+
return (
|
|
122
|
+
<View style={styles.container}>
|
|
123
|
+
<VideoView
|
|
124
|
+
player={player}
|
|
125
|
+
style={styles.video}
|
|
126
|
+
contentFit="contain"
|
|
127
|
+
nativeControls={false}
|
|
128
|
+
/>
|
|
129
|
+
<VideoPlayerOverlay
|
|
130
|
+
visible={controlsVisible}
|
|
131
|
+
isPlaying={state.isPlaying}
|
|
132
|
+
isMuted={state.isMuted}
|
|
133
|
+
currentTime={state.currentTime}
|
|
134
|
+
duration={state.duration}
|
|
135
|
+
title={title}
|
|
136
|
+
subtitle={subtitle}
|
|
137
|
+
onTogglePlay={controls.toggle}
|
|
138
|
+
onToggleMute={controls.toggleMute}
|
|
139
|
+
onSeek={controls.seekTo}
|
|
140
|
+
onBack={handleClose}
|
|
141
|
+
onTap={toggleControls}
|
|
142
|
+
/>
|
|
143
|
+
</View>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Waiting for player
|
|
148
|
+
return (
|
|
149
|
+
<View style={styles.container}>
|
|
150
|
+
{thumbnailUrl && (
|
|
151
|
+
<Image source={{ uri: thumbnailUrl }} style={styles.thumbnail} contentFit="cover" />
|
|
152
|
+
)}
|
|
153
|
+
</View>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<Modal
|
|
159
|
+
visible={visible}
|
|
160
|
+
animationType="fade"
|
|
161
|
+
presentationStyle="fullScreen"
|
|
162
|
+
supportedOrientations={["portrait", "landscape"]}
|
|
163
|
+
onRequestClose={handleClose}
|
|
164
|
+
>
|
|
165
|
+
<StatusBar hidden={visible} />
|
|
166
|
+
{renderContent()}
|
|
167
|
+
</Modal>
|
|
168
|
+
);
|
|
169
|
+
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* VideoPlayer Component
|
|
3
|
-
* Reusable video player with caching, thumbnail and
|
|
3
|
+
* Reusable video player with caching, thumbnail, controls, and optional custom overlay
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, { useState, useCallback, useMemo } from "react";
|
|
6
|
+
import React, { useState, useCallback, useMemo, useEffect } from "react";
|
|
7
7
|
import { View, TouchableOpacity, StyleSheet, type ViewStyle } from "react-native";
|
|
8
8
|
import { Image } from "expo-image";
|
|
9
9
|
// expo-video is optional — lazy require so it is not auto-installed
|
|
@@ -22,6 +22,8 @@ import { useResponsive } from "@umituz/react-native-design-system/responsive";
|
|
|
22
22
|
import type { VideoPlayerProps } from "../../types";
|
|
23
23
|
import { useVideoPlayerControl } from "../hooks/useVideoPlayerControl";
|
|
24
24
|
import { useVideoCaching } from "../hooks/useVideoCaching";
|
|
25
|
+
import { useControlsAutoHide } from "../hooks/useControlsAutoHide";
|
|
26
|
+
import { VideoPlayerOverlay } from "./VideoPlayerOverlay";
|
|
25
27
|
|
|
26
28
|
declare const __DEV__: boolean;
|
|
27
29
|
|
|
@@ -41,11 +43,16 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
41
43
|
loop = true,
|
|
42
44
|
muted = false,
|
|
43
45
|
autoPlay = false,
|
|
46
|
+
showControls = false,
|
|
44
47
|
nativeControls = true,
|
|
45
48
|
contentFit = "cover",
|
|
46
49
|
style,
|
|
47
50
|
playbackRate = 1,
|
|
48
51
|
filterOverlay,
|
|
52
|
+
title,
|
|
53
|
+
subtitle,
|
|
54
|
+
onBack,
|
|
55
|
+
onProgress,
|
|
49
56
|
}) => {
|
|
50
57
|
// IMPORTANT: Call useResponsive BEFORE useAppDesignTokens to maintain hook order
|
|
51
58
|
const { width: screenWidth, horizontalPadding } = useResponsive();
|
|
@@ -53,6 +60,9 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
53
60
|
const [userTriggeredPlay, setUserTriggeredPlay] = useState(false);
|
|
54
61
|
const showVideo = autoPlay || userTriggeredPlay;
|
|
55
62
|
|
|
63
|
+
// When showControls is true, disable native controls and show custom overlay
|
|
64
|
+
const useNativeControls = showControls ? false : nativeControls;
|
|
65
|
+
|
|
56
66
|
// Cache the video first (downloads if needed)
|
|
57
67
|
const { localUri, isDownloading, downloadProgress, error } = useVideoCaching(source);
|
|
58
68
|
|
|
@@ -65,6 +75,19 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
65
75
|
playbackRate,
|
|
66
76
|
});
|
|
67
77
|
|
|
78
|
+
// Auto-hide controls overlay
|
|
79
|
+
const { visible: controlsVisible, toggle: toggleControls } = useControlsAutoHide({
|
|
80
|
+
isPlaying: state.isPlaying,
|
|
81
|
+
autoHideDelay: 3000,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Notify parent of progress changes
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (onProgress && state.duration > 0) {
|
|
87
|
+
onProgress(state.currentTime, state.duration);
|
|
88
|
+
}
|
|
89
|
+
}, [onProgress, state.currentTime, state.duration]);
|
|
90
|
+
|
|
68
91
|
const handlePlay = useCallback(() => {
|
|
69
92
|
if (__DEV__) {
|
|
70
93
|
console.log("[VideoPlayer] handlePlay, localUri:", localUri);
|
|
@@ -148,7 +171,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
148
171
|
player={player}
|
|
149
172
|
style={styles.video}
|
|
150
173
|
contentFit={contentFit}
|
|
151
|
-
nativeControls={
|
|
174
|
+
nativeControls={useNativeControls}
|
|
152
175
|
/>
|
|
153
176
|
{filterOverlay && filterOverlay.opacity > 0 && (
|
|
154
177
|
<View
|
|
@@ -159,6 +182,22 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
159
182
|
pointerEvents="none"
|
|
160
183
|
/>
|
|
161
184
|
)}
|
|
185
|
+
{showControls && (
|
|
186
|
+
<VideoPlayerOverlay
|
|
187
|
+
visible={controlsVisible}
|
|
188
|
+
isPlaying={state.isPlaying}
|
|
189
|
+
isMuted={state.isMuted}
|
|
190
|
+
currentTime={state.currentTime}
|
|
191
|
+
duration={state.duration}
|
|
192
|
+
title={title}
|
|
193
|
+
subtitle={subtitle}
|
|
194
|
+
onTogglePlay={controls.toggle}
|
|
195
|
+
onToggleMute={controls.toggleMute}
|
|
196
|
+
onSeek={controls.seekTo}
|
|
197
|
+
onBack={onBack}
|
|
198
|
+
onTap={toggleControls}
|
|
199
|
+
/>
|
|
200
|
+
)}
|
|
162
201
|
</View>
|
|
163
202
|
);
|
|
164
203
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VideoPlayerOverlay Component
|
|
3
|
+
* Custom overlay controls: top bar (title, back), center play/pause, bottom bar (progress, mute)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet, TouchableWithoutFeedback } from "react-native";
|
|
8
|
+
import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
10
|
+
|
|
11
|
+
import type { VideoPlayerOverlayProps } from "../../types";
|
|
12
|
+
import { VideoProgressBar } from "./VideoProgressBar";
|
|
13
|
+
|
|
14
|
+
export const VideoPlayerOverlay: React.FC<VideoPlayerOverlayProps> = ({
|
|
15
|
+
visible,
|
|
16
|
+
isPlaying,
|
|
17
|
+
isMuted,
|
|
18
|
+
currentTime,
|
|
19
|
+
duration,
|
|
20
|
+
title,
|
|
21
|
+
subtitle,
|
|
22
|
+
onTogglePlay,
|
|
23
|
+
onToggleMute,
|
|
24
|
+
onSeek,
|
|
25
|
+
onBack,
|
|
26
|
+
onTap,
|
|
27
|
+
}) => {
|
|
28
|
+
const tokens = useAppDesignTokens();
|
|
29
|
+
|
|
30
|
+
const styles = useMemo(
|
|
31
|
+
() =>
|
|
32
|
+
StyleSheet.create({
|
|
33
|
+
overlay: {
|
|
34
|
+
...StyleSheet.absoluteFillObject,
|
|
35
|
+
justifyContent: "space-between",
|
|
36
|
+
},
|
|
37
|
+
topBar: {
|
|
38
|
+
flexDirection: "row",
|
|
39
|
+
alignItems: "center",
|
|
40
|
+
paddingHorizontal: 16,
|
|
41
|
+
paddingTop: 12,
|
|
42
|
+
paddingBottom: 8,
|
|
43
|
+
backgroundColor: "rgba(0,0,0,0.5)",
|
|
44
|
+
},
|
|
45
|
+
backButton: {
|
|
46
|
+
width: 40,
|
|
47
|
+
height: 40,
|
|
48
|
+
borderRadius: 20,
|
|
49
|
+
backgroundColor: "rgba(255,255,255,0.2)",
|
|
50
|
+
justifyContent: "center",
|
|
51
|
+
alignItems: "center",
|
|
52
|
+
},
|
|
53
|
+
titleContainer: {
|
|
54
|
+
flex: 1,
|
|
55
|
+
marginLeft: 12,
|
|
56
|
+
},
|
|
57
|
+
titleText: {
|
|
58
|
+
color: "#FFFFFF",
|
|
59
|
+
fontSize: 16,
|
|
60
|
+
fontWeight: "600",
|
|
61
|
+
},
|
|
62
|
+
subtitleText: {
|
|
63
|
+
color: "rgba(255,255,255,0.7)",
|
|
64
|
+
fontSize: 13,
|
|
65
|
+
marginTop: 2,
|
|
66
|
+
},
|
|
67
|
+
centerArea: {
|
|
68
|
+
flex: 1,
|
|
69
|
+
justifyContent: "center",
|
|
70
|
+
alignItems: "center",
|
|
71
|
+
},
|
|
72
|
+
centerPlayButton: {
|
|
73
|
+
width: 72,
|
|
74
|
+
height: 72,
|
|
75
|
+
borderRadius: 36,
|
|
76
|
+
backgroundColor: "rgba(0,0,0,0.6)",
|
|
77
|
+
justifyContent: "center",
|
|
78
|
+
alignItems: "center",
|
|
79
|
+
},
|
|
80
|
+
bottomBar: {
|
|
81
|
+
paddingHorizontal: 16,
|
|
82
|
+
paddingBottom: 16,
|
|
83
|
+
paddingTop: 8,
|
|
84
|
+
backgroundColor: "rgba(0,0,0,0.5)",
|
|
85
|
+
},
|
|
86
|
+
bottomControls: {
|
|
87
|
+
flexDirection: "row",
|
|
88
|
+
alignItems: "center",
|
|
89
|
+
marginTop: 10,
|
|
90
|
+
gap: 12,
|
|
91
|
+
},
|
|
92
|
+
controlButton: {
|
|
93
|
+
width: 36,
|
|
94
|
+
height: 36,
|
|
95
|
+
borderRadius: 18,
|
|
96
|
+
backgroundColor: "rgba(255,255,255,0.2)",
|
|
97
|
+
justifyContent: "center",
|
|
98
|
+
alignItems: "center",
|
|
99
|
+
},
|
|
100
|
+
spacer: { flex: 1 },
|
|
101
|
+
}),
|
|
102
|
+
[tokens],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (!visible) {
|
|
106
|
+
// Invisible tap area to show controls
|
|
107
|
+
return (
|
|
108
|
+
<TouchableWithoutFeedback onPress={onTap}>
|
|
109
|
+
<View style={styles.overlay} />
|
|
110
|
+
</TouchableWithoutFeedback>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<TouchableWithoutFeedback onPress={onTap}>
|
|
116
|
+
<View style={styles.overlay}>
|
|
117
|
+
{/* Top Bar */}
|
|
118
|
+
<View style={styles.topBar}>
|
|
119
|
+
{onBack && (
|
|
120
|
+
<TouchableOpacity style={styles.backButton} onPress={onBack} activeOpacity={0.7}>
|
|
121
|
+
<AtomicIcon name="chevron-back" customSize={22} color="onPrimary" />
|
|
122
|
+
</TouchableOpacity>
|
|
123
|
+
)}
|
|
124
|
+
{(title || subtitle) && (
|
|
125
|
+
<View style={styles.titleContainer}>
|
|
126
|
+
{title && <AtomicText style={styles.titleText} numberOfLines={1}>{title}</AtomicText>}
|
|
127
|
+
{subtitle && <AtomicText style={styles.subtitleText} numberOfLines={1}>{subtitle}</AtomicText>}
|
|
128
|
+
</View>
|
|
129
|
+
)}
|
|
130
|
+
</View>
|
|
131
|
+
|
|
132
|
+
{/* Center Play/Pause */}
|
|
133
|
+
<View style={styles.centerArea}>
|
|
134
|
+
<TouchableOpacity style={styles.centerPlayButton} onPress={onTogglePlay} activeOpacity={0.7}>
|
|
135
|
+
<AtomicIcon
|
|
136
|
+
name={isPlaying ? "pause" : "play"}
|
|
137
|
+
customSize={36}
|
|
138
|
+
color="onPrimary"
|
|
139
|
+
/>
|
|
140
|
+
</TouchableOpacity>
|
|
141
|
+
</View>
|
|
142
|
+
|
|
143
|
+
{/* Bottom Bar */}
|
|
144
|
+
<View style={styles.bottomBar}>
|
|
145
|
+
<VideoProgressBar
|
|
146
|
+
currentTime={currentTime}
|
|
147
|
+
duration={duration}
|
|
148
|
+
onSeek={onSeek}
|
|
149
|
+
showTimeLabels
|
|
150
|
+
/>
|
|
151
|
+
<View style={styles.bottomControls}>
|
|
152
|
+
<TouchableOpacity style={styles.controlButton} onPress={onTogglePlay} activeOpacity={0.7}>
|
|
153
|
+
<AtomicIcon
|
|
154
|
+
name={isPlaying ? "pause" : "play"}
|
|
155
|
+
customSize={18}
|
|
156
|
+
color="onPrimary"
|
|
157
|
+
/>
|
|
158
|
+
</TouchableOpacity>
|
|
159
|
+
<View style={styles.spacer} />
|
|
160
|
+
<TouchableOpacity style={styles.controlButton} onPress={onToggleMute} activeOpacity={0.7}>
|
|
161
|
+
<AtomicIcon
|
|
162
|
+
name={isMuted ? "volume-x" : "volume-2"}
|
|
163
|
+
customSize={18}
|
|
164
|
+
color="onPrimary"
|
|
165
|
+
/>
|
|
166
|
+
</TouchableOpacity>
|
|
167
|
+
</View>
|
|
168
|
+
</View>
|
|
169
|
+
</View>
|
|
170
|
+
</TouchableWithoutFeedback>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VideoProgressBar Component
|
|
3
|
+
* Seekable progress bar with time labels for video playback
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useCallback, useMemo } from "react";
|
|
7
|
+
import { View, StyleSheet, type LayoutChangeEvent } from "react-native";
|
|
8
|
+
import { AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
10
|
+
|
|
11
|
+
import type { VideoProgressBarProps } from "../../types";
|
|
12
|
+
import { formatTimeDisplay } from "../../../infrastructure/utils/srt.utils";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_HEIGHT = 4;
|
|
15
|
+
|
|
16
|
+
export const VideoProgressBar: React.FC<VideoProgressBarProps> = ({
|
|
17
|
+
currentTime,
|
|
18
|
+
duration,
|
|
19
|
+
onSeek,
|
|
20
|
+
showTimeLabels = true,
|
|
21
|
+
height = DEFAULT_HEIGHT,
|
|
22
|
+
trackColor,
|
|
23
|
+
fillColor,
|
|
24
|
+
style,
|
|
25
|
+
}) => {
|
|
26
|
+
const tokens = useAppDesignTokens();
|
|
27
|
+
const progressPercent = duration > 0 ? Math.min(currentTime / duration, 1) * 100 : 0;
|
|
28
|
+
const barWidthRef = React.useRef(0);
|
|
29
|
+
|
|
30
|
+
const resolvedTrackColor = trackColor ?? "rgba(255,255,255,0.3)";
|
|
31
|
+
const resolvedFillColor = fillColor ?? tokens.colors.primary;
|
|
32
|
+
|
|
33
|
+
const handleLayout = useCallback((e: LayoutChangeEvent) => {
|
|
34
|
+
barWidthRef.current = e.nativeEvent.layout.width;
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const handleSeek = useCallback(
|
|
38
|
+
(e: { nativeEvent: { locationX: number } }) => {
|
|
39
|
+
if (!onSeek || duration <= 0 || barWidthRef.current <= 0) return;
|
|
40
|
+
const ratio = Math.max(0, Math.min(1, e.nativeEvent.locationX / barWidthRef.current));
|
|
41
|
+
onSeek(ratio * duration);
|
|
42
|
+
},
|
|
43
|
+
[onSeek, duration],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const styles = useMemo(
|
|
47
|
+
() =>
|
|
48
|
+
StyleSheet.create({
|
|
49
|
+
container: { width: "100%" },
|
|
50
|
+
row: { flexDirection: "row", alignItems: "center", gap: 8 },
|
|
51
|
+
timeText: { color: "#FFFFFF", fontSize: 12, fontWeight: "500", minWidth: 40 },
|
|
52
|
+
barContainer: {
|
|
53
|
+
flex: 1,
|
|
54
|
+
height,
|
|
55
|
+
borderRadius: height / 2,
|
|
56
|
+
backgroundColor: resolvedTrackColor,
|
|
57
|
+
overflow: "hidden",
|
|
58
|
+
},
|
|
59
|
+
barFill: {
|
|
60
|
+
height: "100%",
|
|
61
|
+
borderRadius: height / 2,
|
|
62
|
+
backgroundColor: resolvedFillColor,
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
[height, resolvedTrackColor, resolvedFillColor],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<View style={[styles.container, style]}>
|
|
70
|
+
<View style={styles.row}>
|
|
71
|
+
{showTimeLabels && (
|
|
72
|
+
<AtomicText style={styles.timeText}>{formatTimeDisplay(currentTime)}</AtomicText>
|
|
73
|
+
)}
|
|
74
|
+
<View
|
|
75
|
+
style={styles.barContainer}
|
|
76
|
+
onLayout={handleLayout}
|
|
77
|
+
onStartShouldSetResponder={() => Boolean(onSeek)}
|
|
78
|
+
onResponderRelease={handleSeek}
|
|
79
|
+
>
|
|
80
|
+
<View style={[styles.barFill, { width: `${progressPercent}%` }]} />
|
|
81
|
+
</View>
|
|
82
|
+
{showTimeLabels && (
|
|
83
|
+
<AtomicText style={[styles.timeText, { textAlign: "right" }]}>
|
|
84
|
+
{formatTimeDisplay(duration)}
|
|
85
|
+
</AtomicText>
|
|
86
|
+
)}
|
|
87
|
+
</View>
|
|
88
|
+
</View>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useControlsAutoHide Hook
|
|
3
|
+
* Manages visibility of video player overlay controls with auto-hide timer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
7
|
+
|
|
8
|
+
import type { ControlsAutoHideConfig, ControlsAutoHideResult } from "../../types";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_AUTO_HIDE_DELAY = 3000;
|
|
11
|
+
|
|
12
|
+
export const useControlsAutoHide = (
|
|
13
|
+
config: ControlsAutoHideConfig,
|
|
14
|
+
): ControlsAutoHideResult => {
|
|
15
|
+
const { autoHideDelay = DEFAULT_AUTO_HIDE_DELAY, isPlaying } = config;
|
|
16
|
+
const [visible, setVisible] = useState(true);
|
|
17
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
18
|
+
|
|
19
|
+
const clearTimer = useCallback(() => {
|
|
20
|
+
if (timerRef.current) {
|
|
21
|
+
clearTimeout(timerRef.current);
|
|
22
|
+
timerRef.current = null;
|
|
23
|
+
}
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
const startTimer = useCallback(() => {
|
|
27
|
+
clearTimer();
|
|
28
|
+
if (isPlaying) {
|
|
29
|
+
timerRef.current = setTimeout(() => {
|
|
30
|
+
setVisible(false);
|
|
31
|
+
}, autoHideDelay);
|
|
32
|
+
}
|
|
33
|
+
}, [clearTimer, isPlaying, autoHideDelay]);
|
|
34
|
+
|
|
35
|
+
const show = useCallback(() => {
|
|
36
|
+
setVisible(true);
|
|
37
|
+
startTimer();
|
|
38
|
+
}, [startTimer]);
|
|
39
|
+
|
|
40
|
+
const hide = useCallback(() => {
|
|
41
|
+
clearTimer();
|
|
42
|
+
setVisible(false);
|
|
43
|
+
}, [clearTimer]);
|
|
44
|
+
|
|
45
|
+
const toggle = useCallback(() => {
|
|
46
|
+
if (visible) {
|
|
47
|
+
hide();
|
|
48
|
+
} else {
|
|
49
|
+
show();
|
|
50
|
+
}
|
|
51
|
+
}, [visible, show, hide]);
|
|
52
|
+
|
|
53
|
+
// Auto-hide when playing starts, show when paused
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (isPlaying) {
|
|
56
|
+
startTimer();
|
|
57
|
+
} else {
|
|
58
|
+
clearTimer();
|
|
59
|
+
setVisible(true);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return clearTimer;
|
|
63
|
+
}, [isPlaying, startTimer, clearTimer]);
|
|
64
|
+
|
|
65
|
+
return { visible, show, hide, toggle };
|
|
66
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useVideoPlaybackProgress Hook
|
|
3
|
+
* Tracks currentTime, duration, and progress by polling the expo-video player
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
7
|
+
|
|
8
|
+
import type { PlaybackProgressState } from "../../types";
|
|
9
|
+
|
|
10
|
+
declare const __DEV__: boolean;
|
|
11
|
+
|
|
12
|
+
const POLL_INTERVAL_MS = 250;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Polls the player for currentTime/duration and returns progress (0-1)
|
|
16
|
+
*/
|
|
17
|
+
export const useVideoPlaybackProgress = (
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
+
player: any,
|
|
20
|
+
isPlayerValid: boolean,
|
|
21
|
+
isPlaying: boolean,
|
|
22
|
+
): PlaybackProgressState => {
|
|
23
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
24
|
+
const [duration, setDuration] = useState(0);
|
|
25
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
26
|
+
|
|
27
|
+
const pollPlayer = useCallback(() => {
|
|
28
|
+
if (!player || !isPlayerValid) return;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const ct = typeof player.currentTime === "number" ? player.currentTime : 0;
|
|
32
|
+
const dur = typeof player.duration === "number" && isFinite(player.duration) ? player.duration : 0;
|
|
33
|
+
|
|
34
|
+
setCurrentTime(ct);
|
|
35
|
+
if (dur > 0) setDuration(dur);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
38
|
+
console.log("[useVideoPlaybackProgress] Poll error:", error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}, [player, isPlayerValid]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (isPlaying && isPlayerValid) {
|
|
45
|
+
pollPlayer();
|
|
46
|
+
intervalRef.current = setInterval(pollPlayer, POLL_INTERVAL_MS);
|
|
47
|
+
} else {
|
|
48
|
+
// Poll once when paused to get final position
|
|
49
|
+
pollPlayer();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return () => {
|
|
53
|
+
if (intervalRef.current) {
|
|
54
|
+
clearInterval(intervalRef.current);
|
|
55
|
+
intervalRef.current = null;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}, [isPlaying, isPlayerValid, pollPlayer]);
|
|
59
|
+
|
|
60
|
+
const progress = duration > 0 ? Math.min(currentTime / duration, 1) : 0;
|
|
61
|
+
|
|
62
|
+
return { currentTime, duration, progress };
|
|
63
|
+
};
|
|
@@ -26,7 +26,11 @@ import {
|
|
|
26
26
|
safeToggle,
|
|
27
27
|
isPlayerReady,
|
|
28
28
|
configurePlayer,
|
|
29
|
+
safeSeekTo,
|
|
30
|
+
safeMute,
|
|
31
|
+
safeReplay,
|
|
29
32
|
} from "../../infrastructure/services/player-control.service";
|
|
33
|
+
import { useVideoPlaybackProgress } from "./useVideoPlaybackProgress";
|
|
30
34
|
|
|
31
35
|
declare const __DEV__: boolean;
|
|
32
36
|
|
|
@@ -41,6 +45,7 @@ export const useVideoPlayerControl = (
|
|
|
41
45
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
42
46
|
const [isLoading, setIsLoading] = useState(true);
|
|
43
47
|
const [playbackRate, setPlaybackRateState] = useState(initialRate);
|
|
48
|
+
const [isMuted, setIsMuted] = useState(muted);
|
|
44
49
|
|
|
45
50
|
const player = useExpoVideoPlayer(source || "", (p: any) => {
|
|
46
51
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -68,6 +73,9 @@ export const useVideoPlayerControl = (
|
|
|
68
73
|
[player, source],
|
|
69
74
|
);
|
|
70
75
|
|
|
76
|
+
// Track playback progress
|
|
77
|
+
const { currentTime, duration, progress } = useVideoPlaybackProgress(player, isPlayerValid, isPlaying);
|
|
78
|
+
|
|
71
79
|
const play = useCallback(() => {
|
|
72
80
|
if (!isPlayerValid) return;
|
|
73
81
|
const success = safePlay(player);
|
|
@@ -92,19 +100,47 @@ export const useVideoPlayerControl = (
|
|
|
92
100
|
setPlaybackRateState(rate);
|
|
93
101
|
}, [player, isPlayerValid]);
|
|
94
102
|
|
|
103
|
+
const toggleMute = useCallback(() => {
|
|
104
|
+
if (!isPlayerValid) return;
|
|
105
|
+
const newMuted = !isMuted;
|
|
106
|
+
const success = safeMute(player, newMuted);
|
|
107
|
+
if (success) setIsMuted(newMuted);
|
|
108
|
+
}, [player, isPlayerValid, isMuted]);
|
|
109
|
+
|
|
110
|
+
const setMuted = useCallback((value: boolean) => {
|
|
111
|
+
if (!isPlayerValid) return;
|
|
112
|
+
const success = safeMute(player, value);
|
|
113
|
+
if (success) setIsMuted(value);
|
|
114
|
+
}, [player, isPlayerValid]);
|
|
115
|
+
|
|
116
|
+
const seekTo = useCallback((seconds: number) => {
|
|
117
|
+
if (!isPlayerValid) return;
|
|
118
|
+
safeSeekTo(player, seconds);
|
|
119
|
+
}, [player, isPlayerValid]);
|
|
120
|
+
|
|
121
|
+
const replay = useCallback(() => {
|
|
122
|
+
if (!isPlayerValid) return;
|
|
123
|
+
const success = safeReplay(player);
|
|
124
|
+
if (success) setIsPlaying(true);
|
|
125
|
+
}, [player, isPlayerValid]);
|
|
126
|
+
|
|
95
127
|
const state: VideoPlayerState = useMemo(
|
|
96
128
|
() => ({
|
|
97
129
|
isPlaying,
|
|
98
130
|
isPlayerValid,
|
|
99
131
|
isLoading: isLoading && Boolean(source),
|
|
100
132
|
playbackRate,
|
|
133
|
+
isMuted,
|
|
134
|
+
currentTime,
|
|
135
|
+
duration,
|
|
136
|
+
progress,
|
|
101
137
|
}),
|
|
102
|
-
[isPlaying, isPlayerValid, isLoading, source, playbackRate],
|
|
138
|
+
[isPlaying, isPlayerValid, isLoading, source, playbackRate, isMuted, currentTime, duration, progress],
|
|
103
139
|
);
|
|
104
140
|
|
|
105
141
|
const controls: VideoPlayerControls = useMemo(
|
|
106
|
-
() => ({ play, pause, toggle, setPlaybackRate }),
|
|
107
|
-
[play, pause, toggle, setPlaybackRate],
|
|
142
|
+
() => ({ play, pause, toggle, setPlaybackRate, toggleMute, setMuted, seekTo, replay }),
|
|
143
|
+
[play, pause, toggle, setPlaybackRate, toggleMute, setMuted, seekTo, replay],
|
|
108
144
|
);
|
|
109
145
|
|
|
110
146
|
return { player, state, controls };
|
|
@@ -25,6 +25,10 @@ export interface VideoPlayerState {
|
|
|
25
25
|
readonly isPlayerValid: boolean;
|
|
26
26
|
readonly isLoading: boolean;
|
|
27
27
|
readonly playbackRate: number;
|
|
28
|
+
readonly isMuted: boolean;
|
|
29
|
+
readonly currentTime: number;
|
|
30
|
+
readonly duration: number;
|
|
31
|
+
readonly progress: number;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
/**
|
|
@@ -35,6 +39,10 @@ export interface VideoPlayerControls {
|
|
|
35
39
|
readonly pause: () => void;
|
|
36
40
|
readonly toggle: () => void;
|
|
37
41
|
readonly setPlaybackRate: (rate: number) => void;
|
|
42
|
+
readonly toggleMute: () => void;
|
|
43
|
+
readonly setMuted: (muted: boolean) => void;
|
|
44
|
+
readonly seekTo: (seconds: number) => void;
|
|
45
|
+
readonly replay: () => void;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
/**
|
|
@@ -74,6 +82,83 @@ export interface VideoPlayerProps {
|
|
|
74
82
|
readonly thumbnailUrl?: string;
|
|
75
83
|
readonly playbackRate?: number;
|
|
76
84
|
readonly filterOverlay?: { overlay: string; opacity: number };
|
|
85
|
+
readonly title?: string;
|
|
86
|
+
readonly subtitle?: string;
|
|
87
|
+
readonly onBack?: () => void;
|
|
88
|
+
readonly onProgress?: (currentTime: number, duration: number) => void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Playback progress state
|
|
93
|
+
*/
|
|
94
|
+
export interface PlaybackProgressState {
|
|
95
|
+
readonly currentTime: number;
|
|
96
|
+
readonly duration: number;
|
|
97
|
+
readonly progress: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Controls auto-hide configuration
|
|
102
|
+
*/
|
|
103
|
+
export interface ControlsAutoHideConfig {
|
|
104
|
+
readonly autoHideDelay?: number;
|
|
105
|
+
readonly isPlaying: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Controls auto-hide result
|
|
110
|
+
*/
|
|
111
|
+
export interface ControlsAutoHideResult {
|
|
112
|
+
readonly visible: boolean;
|
|
113
|
+
readonly show: () => void;
|
|
114
|
+
readonly hide: () => void;
|
|
115
|
+
readonly toggle: () => void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Video progress bar props
|
|
120
|
+
*/
|
|
121
|
+
export interface VideoProgressBarProps {
|
|
122
|
+
readonly currentTime: number;
|
|
123
|
+
readonly duration: number;
|
|
124
|
+
readonly onSeek?: (seconds: number) => void;
|
|
125
|
+
readonly showTimeLabels?: boolean;
|
|
126
|
+
readonly height?: number;
|
|
127
|
+
readonly trackColor?: string;
|
|
128
|
+
readonly fillColor?: string;
|
|
129
|
+
readonly style?: ViewStyle;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Video player overlay props
|
|
134
|
+
*/
|
|
135
|
+
export interface VideoPlayerOverlayProps {
|
|
136
|
+
readonly visible: boolean;
|
|
137
|
+
readonly isPlaying: boolean;
|
|
138
|
+
readonly isMuted: boolean;
|
|
139
|
+
readonly currentTime: number;
|
|
140
|
+
readonly duration: number;
|
|
141
|
+
readonly title?: string;
|
|
142
|
+
readonly subtitle?: string;
|
|
143
|
+
readonly onTogglePlay: () => void;
|
|
144
|
+
readonly onToggleMute: () => void;
|
|
145
|
+
readonly onSeek: (seconds: number) => void;
|
|
146
|
+
readonly onBack?: () => void;
|
|
147
|
+
readonly onTap: () => void;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Full screen video player props
|
|
152
|
+
*/
|
|
153
|
+
export interface FullScreenVideoPlayerProps {
|
|
154
|
+
readonly visible: boolean;
|
|
155
|
+
readonly source: string | null;
|
|
156
|
+
readonly title?: string;
|
|
157
|
+
readonly subtitle?: string;
|
|
158
|
+
readonly thumbnailUrl?: string;
|
|
159
|
+
readonly onClose: () => void;
|
|
160
|
+
readonly loop?: boolean;
|
|
161
|
+
readonly autoPlay?: boolean;
|
|
77
162
|
}
|
|
78
163
|
|
|
79
164
|
export type { VideoPlayer } from "expo-video";
|