@umituz/react-native-video-editor 1.1.45 → 1.1.47
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/player/infrastructure/services/player-control.service.ts +7 -20
- package/src/player/presentation/components/FullScreenVideoPlayer.tsx +4 -3
- package/src/player/presentation/components/VideoPlayer.tsx +27 -32
- package/src/player/presentation/components/VideoPlayerOverlay.tsx +88 -101
- package/src/player/presentation/components/VideoProgressBar.tsx +16 -30
- package/src/player/presentation/hooks/useVideoPlaybackProgress.ts +19 -21
- package/src/player/presentation/hooks/useVideoPlayerControl.ts +17 -18
- package/src/player/types/index.ts +2 -3
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.47",
|
|
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",
|
|
@@ -5,29 +5,18 @@
|
|
|
5
5
|
|
|
6
6
|
import type { VideoPlayer } from "expo-video";
|
|
7
7
|
|
|
8
|
-
declare const __DEV__: boolean;
|
|
9
|
-
|
|
10
8
|
/**
|
|
11
9
|
* Safely play video with error handling
|
|
12
10
|
*/
|
|
13
11
|
export const safePlay = (player: VideoPlayer | null): boolean => {
|
|
14
|
-
if (__DEV__) {
|
|
15
|
-
console.log("[safePlay] called, player:", !!player);
|
|
16
|
-
}
|
|
17
12
|
if (!player) return false;
|
|
18
13
|
|
|
19
14
|
try {
|
|
20
|
-
if (__DEV__) {
|
|
21
|
-
console.log("[safePlay] calling player.play()");
|
|
22
|
-
}
|
|
23
15
|
player.play();
|
|
24
|
-
if (__DEV__) {
|
|
25
|
-
console.log("[safePlay] player.play() called successfully");
|
|
26
|
-
}
|
|
27
16
|
return true;
|
|
28
17
|
} catch (error) {
|
|
29
|
-
if (__DEV__) {
|
|
30
|
-
console.log("[
|
|
18
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
19
|
+
console.log("[VideoPlayer] Play error:", error);
|
|
31
20
|
}
|
|
32
21
|
return false;
|
|
33
22
|
}
|
|
@@ -44,8 +33,7 @@ export const safePause = (player: VideoPlayer | null): boolean => {
|
|
|
44
33
|
return true;
|
|
45
34
|
} catch (error) {
|
|
46
35
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
47
|
-
|
|
48
|
-
console.log("[VideoPlayer] Pause error ignored:", error);
|
|
36
|
+
console.log("[VideoPlayer] Pause error:", error);
|
|
49
37
|
}
|
|
50
38
|
return false;
|
|
51
39
|
}
|
|
@@ -96,8 +84,7 @@ export const configurePlayer = (
|
|
|
96
84
|
}
|
|
97
85
|
} catch (error) {
|
|
98
86
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
99
|
-
|
|
100
|
-
console.log("[VideoPlayer] Configure error ignored:", error);
|
|
87
|
+
console.log("[VideoPlayer] Configure error:", error);
|
|
101
88
|
}
|
|
102
89
|
}
|
|
103
90
|
};
|
|
@@ -113,7 +100,7 @@ export const safeSeekTo = (player: VideoPlayer | null, seconds: number): boolean
|
|
|
113
100
|
return true;
|
|
114
101
|
} catch (error) {
|
|
115
102
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
116
|
-
console.log("[VideoPlayer] SeekTo error
|
|
103
|
+
console.log("[VideoPlayer] SeekTo error:", error);
|
|
117
104
|
}
|
|
118
105
|
return false;
|
|
119
106
|
}
|
|
@@ -130,7 +117,7 @@ export const safeMute = (player: VideoPlayer | null, muted: boolean): boolean =>
|
|
|
130
117
|
return true;
|
|
131
118
|
} catch (error) {
|
|
132
119
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
133
|
-
console.log("[VideoPlayer] Mute error
|
|
120
|
+
console.log("[VideoPlayer] Mute error:", error);
|
|
134
121
|
}
|
|
135
122
|
return false;
|
|
136
123
|
}
|
|
@@ -148,7 +135,7 @@ export const safeReplay = (player: VideoPlayer | null): boolean => {
|
|
|
148
135
|
return true;
|
|
149
136
|
} catch (error) {
|
|
150
137
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
151
|
-
console.log("[VideoPlayer] Replay error
|
|
138
|
+
console.log("[VideoPlayer] Replay error:", error);
|
|
152
139
|
}
|
|
153
140
|
return false;
|
|
154
141
|
}
|
|
@@ -36,6 +36,7 @@ export const FullScreenVideoPlayer: React.FC<FullScreenVideoPlayerProps> = ({
|
|
|
36
36
|
}) => {
|
|
37
37
|
const tokens = useAppDesignTokens();
|
|
38
38
|
|
|
39
|
+
// Only cache/play when modal is visible
|
|
39
40
|
const { localUri, isDownloading, downloadProgress, error } = useVideoCaching(
|
|
40
41
|
visible ? source : null,
|
|
41
42
|
);
|
|
@@ -74,6 +75,7 @@ export const FullScreenVideoPlayer: React.FC<FullScreenVideoPlayerProps> = ({
|
|
|
74
75
|
...StyleSheet.absoluteFillObject,
|
|
75
76
|
justifyContent: "center",
|
|
76
77
|
alignItems: "center",
|
|
78
|
+
backgroundColor: "rgba(0,0,0,0.5)",
|
|
77
79
|
},
|
|
78
80
|
progressText: {
|
|
79
81
|
color: "#FFFFFF",
|
|
@@ -90,7 +92,7 @@ export const FullScreenVideoPlayer: React.FC<FullScreenVideoPlayerProps> = ({
|
|
|
90
92
|
...StyleSheet.absoluteFillObject,
|
|
91
93
|
},
|
|
92
94
|
}),
|
|
93
|
-
[tokens],
|
|
95
|
+
[tokens.colors.error],
|
|
94
96
|
);
|
|
95
97
|
|
|
96
98
|
const renderContent = () => {
|
|
@@ -108,7 +110,7 @@ export const FullScreenVideoPlayer: React.FC<FullScreenVideoPlayerProps> = ({
|
|
|
108
110
|
{thumbnailUrl && (
|
|
109
111
|
<Image source={{ uri: thumbnailUrl }} style={styles.thumbnail} contentFit="cover" />
|
|
110
112
|
)}
|
|
111
|
-
<View style={
|
|
113
|
+
<View style={styles.centerContent}>
|
|
112
114
|
<AtomicText style={styles.progressText}>
|
|
113
115
|
{Math.round(downloadProgress * 100)}%
|
|
114
116
|
</AtomicText>
|
|
@@ -144,7 +146,6 @@ export const FullScreenVideoPlayer: React.FC<FullScreenVideoPlayerProps> = ({
|
|
|
144
146
|
);
|
|
145
147
|
}
|
|
146
148
|
|
|
147
|
-
// Waiting for player
|
|
148
149
|
return (
|
|
149
150
|
<View style={styles.container}>
|
|
150
151
|
{thumbnailUrl && (
|
|
@@ -25,16 +25,17 @@ import { useVideoCaching } from "../hooks/useVideoCaching";
|
|
|
25
25
|
import { useControlsAutoHide } from "../hooks/useControlsAutoHide";
|
|
26
26
|
import { VideoPlayerOverlay } from "./VideoPlayerOverlay";
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
const DEFAULT_ASPECT_RATIO = 16 / 9;
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
/** Check if style provides its own sizing (width/height/aspectRatio/flex) */
|
|
31
|
+
const hasCustomSizing = (style: ViewStyle | undefined): boolean => {
|
|
32
|
+
if (!style) return false;
|
|
33
|
+
return (
|
|
34
|
+
style.width !== undefined ||
|
|
35
|
+
style.height !== undefined ||
|
|
36
|
+
style.aspectRatio !== undefined ||
|
|
37
|
+
style.flex !== undefined
|
|
38
|
+
);
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
@@ -54,7 +55,6 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
54
55
|
onBack,
|
|
55
56
|
onProgress,
|
|
56
57
|
}) => {
|
|
57
|
-
// IMPORTANT: Call useResponsive BEFORE useAppDesignTokens to maintain hook order
|
|
58
58
|
const { width: screenWidth, horizontalPadding } = useResponsive();
|
|
59
59
|
const tokens = useAppDesignTokens();
|
|
60
60
|
const [userTriggeredPlay, setUserTriggeredPlay] = useState(false);
|
|
@@ -63,10 +63,8 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
63
63
|
// When showControls is true, disable native controls and show custom overlay
|
|
64
64
|
const useNativeControls = showControls ? false : nativeControls;
|
|
65
65
|
|
|
66
|
-
// Cache the video first (downloads if needed)
|
|
67
66
|
const { localUri, isDownloading, downloadProgress, error } = useVideoCaching(source);
|
|
68
67
|
|
|
69
|
-
// Use cached local URI for player
|
|
70
68
|
const { player, state, controls } = useVideoPlayerControl({
|
|
71
69
|
source: localUri,
|
|
72
70
|
loop,
|
|
@@ -75,7 +73,6 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
75
73
|
playbackRate,
|
|
76
74
|
});
|
|
77
75
|
|
|
78
|
-
// Auto-hide controls overlay
|
|
79
76
|
const { visible: controlsVisible, toggle: toggleControls } = useControlsAutoHide({
|
|
80
77
|
isPlaying: state.isPlaying,
|
|
81
78
|
autoHideDelay: 3000,
|
|
@@ -89,30 +86,35 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
89
86
|
}, [onProgress, state.currentTime, state.duration]);
|
|
90
87
|
|
|
91
88
|
const handlePlay = useCallback(() => {
|
|
92
|
-
if (__DEV__) {
|
|
93
|
-
console.log("[VideoPlayer] handlePlay, localUri:", localUri);
|
|
94
|
-
}
|
|
95
89
|
setUserTriggeredPlay(true);
|
|
96
90
|
controls.play();
|
|
97
|
-
}, [
|
|
91
|
+
}, [controls]);
|
|
98
92
|
|
|
99
|
-
// Calculate dimensions
|
|
100
|
-
const
|
|
101
|
-
const
|
|
93
|
+
// Calculate fallback dimensions only when style doesn't provide sizing
|
|
94
|
+
const customSizing = hasCustomSizing(style as ViewStyle);
|
|
95
|
+
const videoWidth = customSizing ? undefined : (screenWidth - horizontalPadding * 2);
|
|
96
|
+
const videoHeight = videoWidth ? videoWidth / DEFAULT_ASPECT_RATIO : undefined;
|
|
102
97
|
|
|
103
98
|
const containerStyle = useMemo(() => ({
|
|
104
|
-
width: videoWidth,
|
|
105
|
-
height: videoHeight,
|
|
99
|
+
...(videoWidth !== undefined && { width: videoWidth }),
|
|
100
|
+
...(videoHeight !== undefined && { height: videoHeight }),
|
|
106
101
|
backgroundColor: tokens.colors.surface,
|
|
107
102
|
borderRadius: 16,
|
|
108
103
|
overflow: "hidden" as const,
|
|
109
104
|
}), [tokens.colors.surface, videoWidth, videoHeight]);
|
|
110
105
|
|
|
111
106
|
const styles = useMemo(() => StyleSheet.create({
|
|
112
|
-
video:
|
|
107
|
+
video: videoWidth !== undefined
|
|
108
|
+
? { width: videoWidth, height: videoHeight! }
|
|
109
|
+
: { width: "100%", height: "100%" },
|
|
113
110
|
thumbnailContainer: { flex: 1, justifyContent: "center", alignItems: "center" },
|
|
114
|
-
thumbnail:
|
|
115
|
-
|
|
111
|
+
thumbnail: videoWidth !== undefined
|
|
112
|
+
? { width: videoWidth, height: videoHeight! }
|
|
113
|
+
: { width: "100%", height: "100%" },
|
|
114
|
+
placeholder: {
|
|
115
|
+
...(videoWidth !== undefined ? { width: videoWidth, height: videoHeight! } : { flex: 1, width: "100%" }),
|
|
116
|
+
backgroundColor: tokens.colors.surfaceSecondary,
|
|
117
|
+
},
|
|
116
118
|
playButtonContainer: { ...StyleSheet.absoluteFillObject, justifyContent: "center", alignItems: "center" },
|
|
117
119
|
playButton: {
|
|
118
120
|
width: 64, height: 64, borderRadius: 32,
|
|
@@ -127,7 +129,6 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
127
129
|
errorText: { color: tokens.colors.error, fontSize: 14, textAlign: "center", padding: 16 },
|
|
128
130
|
}), [tokens, videoWidth, videoHeight]);
|
|
129
131
|
|
|
130
|
-
// Show error state
|
|
131
132
|
if (error) {
|
|
132
133
|
return (
|
|
133
134
|
<View style={[containerStyle, style]}>
|
|
@@ -141,7 +142,6 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
141
142
|
);
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
// Show download progress
|
|
145
145
|
if (isDownloading) {
|
|
146
146
|
const progressPercent = Math.round(downloadProgress * 100);
|
|
147
147
|
return (
|
|
@@ -160,11 +160,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
160
160
|
);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
// Show video player
|
|
164
163
|
if (showVideo && state.isPlayerValid && player) {
|
|
165
|
-
if (__DEV__) {
|
|
166
|
-
console.log("[VideoPlayer] Rendering VideoView:", { videoWidth, videoHeight });
|
|
167
|
-
}
|
|
168
164
|
return (
|
|
169
165
|
<View style={[containerStyle, style]}>
|
|
170
166
|
<VideoView
|
|
@@ -202,7 +198,6 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
202
198
|
);
|
|
203
199
|
}
|
|
204
200
|
|
|
205
|
-
// Show thumbnail with play button
|
|
206
201
|
return (
|
|
207
202
|
<TouchableOpacity style={[containerStyle, style]} onPress={handlePlay} activeOpacity={0.8}>
|
|
208
203
|
<View style={styles.thumbnailContainer}>
|
|
@@ -6,11 +6,80 @@
|
|
|
6
6
|
import React, { useMemo } from "react";
|
|
7
7
|
import { View, TouchableOpacity, StyleSheet, TouchableWithoutFeedback } from "react-native";
|
|
8
8
|
import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
9
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
10
9
|
|
|
11
10
|
import type { VideoPlayerOverlayProps } from "../../types";
|
|
12
11
|
import { VideoProgressBar } from "./VideoProgressBar";
|
|
13
12
|
|
|
13
|
+
const styles = StyleSheet.create({
|
|
14
|
+
overlay: {
|
|
15
|
+
...StyleSheet.absoluteFillObject,
|
|
16
|
+
justifyContent: "space-between",
|
|
17
|
+
},
|
|
18
|
+
topBar: {
|
|
19
|
+
flexDirection: "row",
|
|
20
|
+
alignItems: "center",
|
|
21
|
+
paddingHorizontal: 16,
|
|
22
|
+
paddingTop: 12,
|
|
23
|
+
paddingBottom: 8,
|
|
24
|
+
backgroundColor: "rgba(0,0,0,0.5)",
|
|
25
|
+
},
|
|
26
|
+
backButton: {
|
|
27
|
+
width: 40,
|
|
28
|
+
height: 40,
|
|
29
|
+
borderRadius: 20,
|
|
30
|
+
backgroundColor: "rgba(255,255,255,0.2)",
|
|
31
|
+
justifyContent: "center",
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
},
|
|
34
|
+
titleContainer: {
|
|
35
|
+
flex: 1,
|
|
36
|
+
marginLeft: 12,
|
|
37
|
+
},
|
|
38
|
+
titleText: {
|
|
39
|
+
color: "#FFFFFF",
|
|
40
|
+
fontSize: 16,
|
|
41
|
+
fontWeight: "600",
|
|
42
|
+
},
|
|
43
|
+
subtitleText: {
|
|
44
|
+
color: "rgba(255,255,255,0.7)",
|
|
45
|
+
fontSize: 13,
|
|
46
|
+
marginTop: 2,
|
|
47
|
+
},
|
|
48
|
+
centerArea: {
|
|
49
|
+
flex: 1,
|
|
50
|
+
justifyContent: "center",
|
|
51
|
+
alignItems: "center",
|
|
52
|
+
},
|
|
53
|
+
centerPlayButton: {
|
|
54
|
+
width: 72,
|
|
55
|
+
height: 72,
|
|
56
|
+
borderRadius: 36,
|
|
57
|
+
backgroundColor: "rgba(0,0,0,0.6)",
|
|
58
|
+
justifyContent: "center",
|
|
59
|
+
alignItems: "center",
|
|
60
|
+
},
|
|
61
|
+
bottomBar: {
|
|
62
|
+
paddingHorizontal: 16,
|
|
63
|
+
paddingBottom: 16,
|
|
64
|
+
paddingTop: 8,
|
|
65
|
+
backgroundColor: "rgba(0,0,0,0.5)",
|
|
66
|
+
},
|
|
67
|
+
bottomControls: {
|
|
68
|
+
flexDirection: "row",
|
|
69
|
+
alignItems: "center",
|
|
70
|
+
justifyContent: "flex-end",
|
|
71
|
+
marginTop: 10,
|
|
72
|
+
},
|
|
73
|
+
controlButton: {
|
|
74
|
+
width: 36,
|
|
75
|
+
height: 36,
|
|
76
|
+
borderRadius: 18,
|
|
77
|
+
backgroundColor: "rgba(255,255,255,0.2)",
|
|
78
|
+
justifyContent: "center",
|
|
79
|
+
alignItems: "center",
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
14
83
|
export const VideoPlayerOverlay: React.FC<VideoPlayerOverlayProps> = ({
|
|
15
84
|
visible,
|
|
16
85
|
isPlaying,
|
|
@@ -25,85 +94,9 @@ export const VideoPlayerOverlay: React.FC<VideoPlayerOverlayProps> = ({
|
|
|
25
94
|
onBack,
|
|
26
95
|
onTap,
|
|
27
96
|
}) => {
|
|
28
|
-
const
|
|
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
|
-
);
|
|
97
|
+
const hasTopBar = Boolean(onBack || title || subtitle);
|
|
104
98
|
|
|
105
99
|
if (!visible) {
|
|
106
|
-
// Invisible tap area to show controls
|
|
107
100
|
return (
|
|
108
101
|
<TouchableWithoutFeedback onPress={onTap}>
|
|
109
102
|
<View style={styles.overlay} />
|
|
@@ -114,20 +107,22 @@ export const VideoPlayerOverlay: React.FC<VideoPlayerOverlayProps> = ({
|
|
|
114
107
|
return (
|
|
115
108
|
<TouchableWithoutFeedback onPress={onTap}>
|
|
116
109
|
<View style={styles.overlay}>
|
|
117
|
-
{/* Top Bar */}
|
|
118
|
-
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
110
|
+
{/* Top Bar — only render when there's content */}
|
|
111
|
+
{hasTopBar && (
|
|
112
|
+
<View style={styles.topBar}>
|
|
113
|
+
{onBack && (
|
|
114
|
+
<TouchableOpacity style={styles.backButton} onPress={onBack} activeOpacity={0.7}>
|
|
115
|
+
<AtomicIcon name="chevron-back" customSize={22} color="onPrimary" />
|
|
116
|
+
</TouchableOpacity>
|
|
117
|
+
)}
|
|
118
|
+
{(title || subtitle) && (
|
|
119
|
+
<View style={styles.titleContainer}>
|
|
120
|
+
{title && <AtomicText style={styles.titleText} numberOfLines={1}>{title}</AtomicText>}
|
|
121
|
+
{subtitle && <AtomicText style={styles.subtitleText} numberOfLines={1}>{subtitle}</AtomicText>}
|
|
122
|
+
</View>
|
|
123
|
+
)}
|
|
124
|
+
</View>
|
|
125
|
+
)}
|
|
131
126
|
|
|
132
127
|
{/* Center Play/Pause */}
|
|
133
128
|
<View style={styles.centerArea}>
|
|
@@ -140,7 +135,7 @@ export const VideoPlayerOverlay: React.FC<VideoPlayerOverlayProps> = ({
|
|
|
140
135
|
</TouchableOpacity>
|
|
141
136
|
</View>
|
|
142
137
|
|
|
143
|
-
{/* Bottom Bar */}
|
|
138
|
+
{/* Bottom Bar — progress + mute */}
|
|
144
139
|
<View style={styles.bottomBar}>
|
|
145
140
|
<VideoProgressBar
|
|
146
141
|
currentTime={currentTime}
|
|
@@ -149,14 +144,6 @@ export const VideoPlayerOverlay: React.FC<VideoPlayerOverlayProps> = ({
|
|
|
149
144
|
showTimeLabels
|
|
150
145
|
/>
|
|
151
146
|
<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
147
|
<TouchableOpacity style={styles.controlButton} onPress={onToggleMute} activeOpacity={0.7}>
|
|
161
148
|
<AtomicIcon
|
|
162
149
|
name={isMuted ? "volume-x" : "volume-2"}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Seekable progress bar with time labels for video playback
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, { useCallback,
|
|
6
|
+
import React, { useCallback, useRef } from "react";
|
|
7
7
|
import { View, StyleSheet, type LayoutChangeEvent } from "react-native";
|
|
8
8
|
import { AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
9
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
@@ -25,7 +25,7 @@ export const VideoProgressBar: React.FC<VideoProgressBarProps> = ({
|
|
|
25
25
|
}) => {
|
|
26
26
|
const tokens = useAppDesignTokens();
|
|
27
27
|
const progressPercent = duration > 0 ? Math.min(currentTime / duration, 1) * 100 : 0;
|
|
28
|
-
const barWidthRef =
|
|
28
|
+
const barWidthRef = useRef(0);
|
|
29
29
|
|
|
30
30
|
const resolvedTrackColor = trackColor ?? "rgba(255,255,255,0.3)";
|
|
31
31
|
const resolvedFillColor = fillColor ?? tokens.colors.primary;
|
|
@@ -43,44 +43,22 @@ export const VideoProgressBar: React.FC<VideoProgressBarProps> = ({
|
|
|
43
43
|
[onSeek, duration],
|
|
44
44
|
);
|
|
45
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
46
|
return (
|
|
69
|
-
<View style={[
|
|
70
|
-
<View style={
|
|
47
|
+
<View style={[barStyles.container, style]}>
|
|
48
|
+
<View style={barStyles.row}>
|
|
71
49
|
{showTimeLabels && (
|
|
72
|
-
<AtomicText style={
|
|
50
|
+
<AtomicText style={barStyles.timeText}>{formatTimeDisplay(currentTime)}</AtomicText>
|
|
73
51
|
)}
|
|
74
52
|
<View
|
|
75
|
-
style={
|
|
53
|
+
style={[barStyles.barContainer, { height, borderRadius: height / 2, backgroundColor: resolvedTrackColor }]}
|
|
76
54
|
onLayout={handleLayout}
|
|
77
55
|
onStartShouldSetResponder={() => Boolean(onSeek)}
|
|
78
56
|
onResponderRelease={handleSeek}
|
|
79
57
|
>
|
|
80
|
-
<View style={[
|
|
58
|
+
<View style={[barStyles.barFill, { borderRadius: height / 2, backgroundColor: resolvedFillColor, width: `${progressPercent}%` }]} />
|
|
81
59
|
</View>
|
|
82
60
|
{showTimeLabels && (
|
|
83
|
-
<AtomicText style={[
|
|
61
|
+
<AtomicText style={[barStyles.timeText, { textAlign: "right" }]}>
|
|
84
62
|
{formatTimeDisplay(duration)}
|
|
85
63
|
</AtomicText>
|
|
86
64
|
)}
|
|
@@ -88,3 +66,11 @@ export const VideoProgressBar: React.FC<VideoProgressBarProps> = ({
|
|
|
88
66
|
</View>
|
|
89
67
|
);
|
|
90
68
|
};
|
|
69
|
+
|
|
70
|
+
const barStyles = StyleSheet.create({
|
|
71
|
+
container: { width: "100%" },
|
|
72
|
+
row: { flexDirection: "row", alignItems: "center", gap: 8 },
|
|
73
|
+
timeText: { color: "#FFFFFF", fontSize: 12, fontWeight: "500", minWidth: 40 },
|
|
74
|
+
barContainer: { flex: 1, overflow: "hidden" },
|
|
75
|
+
barFill: { height: "100%" },
|
|
76
|
+
});
|
|
@@ -1,60 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useVideoPlaybackProgress Hook
|
|
3
|
-
* Tracks currentTime, duration, and
|
|
3
|
+
* Tracks currentTime, duration, progress, and actual playing state by polling the expo-video player
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
7
7
|
|
|
8
8
|
import type { PlaybackProgressState } from "../../types";
|
|
9
9
|
|
|
10
|
-
declare const __DEV__: boolean;
|
|
11
|
-
|
|
12
10
|
const POLL_INTERVAL_MS = 250;
|
|
13
11
|
|
|
14
12
|
/**
|
|
15
13
|
* Polls the player for currentTime/duration and returns progress (0-1)
|
|
14
|
+
* Also returns the player's actual playing state to detect out-of-sync conditions
|
|
16
15
|
*/
|
|
17
16
|
export const useVideoPlaybackProgress = (
|
|
18
17
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
18
|
player: any,
|
|
20
19
|
isPlayerValid: boolean,
|
|
21
20
|
isPlaying: boolean,
|
|
21
|
+
onPlayingStateChanged?: (actuallyPlaying: boolean) => void,
|
|
22
22
|
): PlaybackProgressState => {
|
|
23
23
|
const [currentTime, setCurrentTime] = useState(0);
|
|
24
24
|
const [duration, setDuration] = useState(0);
|
|
25
|
-
const
|
|
25
|
+
const onPlayingStateChangedRef = useRef(onPlayingStateChanged);
|
|
26
|
+
onPlayingStateChangedRef.current = onPlayingStateChanged;
|
|
26
27
|
|
|
27
28
|
const pollPlayer = useCallback(() => {
|
|
28
29
|
if (!player || !isPlayerValid) return;
|
|
29
30
|
|
|
30
31
|
try {
|
|
31
|
-
const ct = typeof player.currentTime === "number" ? player.currentTime : 0;
|
|
32
|
+
const ct = typeof player.currentTime === "number" ? Math.max(0, player.currentTime) : 0;
|
|
32
33
|
const dur = typeof player.duration === "number" && isFinite(player.duration) ? player.duration : 0;
|
|
33
34
|
|
|
34
35
|
setCurrentTime(ct);
|
|
35
36
|
if (dur > 0) setDuration(dur);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
|
|
38
|
+
// Sync actual playing state — detects when video ends or system pauses
|
|
39
|
+
const actuallyPlaying = Boolean(player.playing);
|
|
40
|
+
if (actuallyPlaying !== isPlaying) {
|
|
41
|
+
onPlayingStateChangedRef.current?.(actuallyPlaying);
|
|
39
42
|
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Player may be in an invalid state during transitions
|
|
40
45
|
}
|
|
41
|
-
}, [player, isPlayerValid]);
|
|
46
|
+
}, [player, isPlayerValid, isPlaying]);
|
|
42
47
|
|
|
43
48
|
useEffect(() => {
|
|
49
|
+
// Poll once immediately to get initial state
|
|
50
|
+
pollPlayer();
|
|
51
|
+
|
|
44
52
|
if (isPlaying && isPlayerValid) {
|
|
45
|
-
pollPlayer
|
|
46
|
-
|
|
47
|
-
} else {
|
|
48
|
-
// Poll once when paused to get final position
|
|
49
|
-
pollPlayer();
|
|
53
|
+
const id = setInterval(pollPlayer, POLL_INTERVAL_MS);
|
|
54
|
+
return () => clearInterval(id);
|
|
50
55
|
}
|
|
51
|
-
|
|
52
|
-
return () => {
|
|
53
|
-
if (intervalRef.current) {
|
|
54
|
-
clearInterval(intervalRef.current);
|
|
55
|
-
intervalRef.current = null;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
56
|
}, [isPlaying, isPlayerValid, pollPlayer]);
|
|
59
57
|
|
|
60
58
|
const progress = duration > 0 ? Math.min(currentTime / duration, 1) : 0;
|
|
@@ -32,8 +32,6 @@ import {
|
|
|
32
32
|
} from "../../infrastructure/services/player-control.service";
|
|
33
33
|
import { useVideoPlaybackProgress } from "./useVideoPlaybackProgress";
|
|
34
34
|
|
|
35
|
-
declare const __DEV__: boolean;
|
|
36
|
-
|
|
37
35
|
/**
|
|
38
36
|
* Hook for managing video player with safe operations
|
|
39
37
|
*/
|
|
@@ -47,24 +45,14 @@ export const useVideoPlayerControl = (
|
|
|
47
45
|
const [playbackRate, setPlaybackRateState] = useState(initialRate);
|
|
48
46
|
const [isMuted, setIsMuted] = useState(muted);
|
|
49
47
|
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
49
|
const player = useExpoVideoPlayer(source || "", (p: any) => {
|
|
51
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
52
|
-
console.log("[useVideoPlayerControl] Player callback, source:", source, "player:", !!p);
|
|
53
|
-
}
|
|
54
50
|
if (source && p) {
|
|
55
51
|
configurePlayer(p, { loop, muted, autoPlay });
|
|
56
52
|
setIsLoading(false);
|
|
57
53
|
if (autoPlay) {
|
|
58
54
|
setIsPlaying(true);
|
|
59
55
|
}
|
|
60
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
61
|
-
console.log("[useVideoPlayerControl] Player configured:", {
|
|
62
|
-
status: p.status,
|
|
63
|
-
playing: p.playing,
|
|
64
|
-
muted: p.muted,
|
|
65
|
-
loop: p.loop,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
56
|
}
|
|
69
57
|
});
|
|
70
58
|
|
|
@@ -73,8 +61,18 @@ export const useVideoPlayerControl = (
|
|
|
73
61
|
[player, source],
|
|
74
62
|
);
|
|
75
63
|
|
|
76
|
-
//
|
|
77
|
-
const
|
|
64
|
+
// Sync isPlaying with actual player state (handles video end, system pause, etc.)
|
|
65
|
+
const handlePlayingStateChanged = useCallback((actuallyPlaying: boolean) => {
|
|
66
|
+
setIsPlaying(actuallyPlaying);
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
// Track playback progress + sync playing state
|
|
70
|
+
const { currentTime, duration, progress } = useVideoPlaybackProgress(
|
|
71
|
+
player,
|
|
72
|
+
isPlayerValid,
|
|
73
|
+
isPlaying,
|
|
74
|
+
handlePlayingStateChanged,
|
|
75
|
+
);
|
|
78
76
|
|
|
79
77
|
const play = useCallback(() => {
|
|
80
78
|
if (!isPlayerValid) return;
|
|
@@ -96,6 +94,7 @@ export const useVideoPlayerControl = (
|
|
|
96
94
|
|
|
97
95
|
const setPlaybackRate = useCallback((rate: number) => {
|
|
98
96
|
if (!isPlayerValid || !player) return;
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
98
|
(player as any).playbackRate = rate;
|
|
100
99
|
setPlaybackRateState(rate);
|
|
101
100
|
}, [player, isPlayerValid]);
|
|
@@ -107,7 +106,7 @@ export const useVideoPlayerControl = (
|
|
|
107
106
|
if (success) setIsMuted(newMuted);
|
|
108
107
|
}, [player, isPlayerValid, isMuted]);
|
|
109
108
|
|
|
110
|
-
const
|
|
109
|
+
const setMutedFn = useCallback((value: boolean) => {
|
|
111
110
|
if (!isPlayerValid) return;
|
|
112
111
|
const success = safeMute(player, value);
|
|
113
112
|
if (success) setIsMuted(value);
|
|
@@ -139,8 +138,8 @@ export const useVideoPlayerControl = (
|
|
|
139
138
|
);
|
|
140
139
|
|
|
141
140
|
const controls: VideoPlayerControls = useMemo(
|
|
142
|
-
() => ({ play, pause, toggle, setPlaybackRate, toggleMute, setMuted, seekTo, replay }),
|
|
143
|
-
[play, pause, toggle, setPlaybackRate, toggleMute,
|
|
141
|
+
() => ({ play, pause, toggle, setPlaybackRate, toggleMute, setMuted: setMutedFn, seekTo, replay }),
|
|
142
|
+
[play, pause, toggle, setPlaybackRate, toggleMute, setMutedFn, seekTo, replay],
|
|
144
143
|
);
|
|
145
144
|
|
|
146
145
|
return { player, state, controls };
|
|
@@ -69,14 +69,13 @@ export interface VideoVisibilityConfig {
|
|
|
69
69
|
*/
|
|
70
70
|
export interface VideoPlayerProps {
|
|
71
71
|
readonly source: string | null;
|
|
72
|
-
readonly isVisible?: boolean;
|
|
73
72
|
readonly loop?: boolean;
|
|
74
73
|
readonly muted?: boolean;
|
|
75
74
|
readonly autoPlay?: boolean;
|
|
75
|
+
/** When true, shows custom overlay controls and disables nativeControls */
|
|
76
76
|
readonly showControls?: boolean;
|
|
77
|
+
/** Native video controls (ignored when showControls is true) */
|
|
77
78
|
readonly nativeControls?: boolean;
|
|
78
|
-
readonly onPlayingChange?: (isPlaying: boolean) => void;
|
|
79
|
-
readonly onError?: (error: Error) => void;
|
|
80
79
|
readonly style?: ViewStyle;
|
|
81
80
|
readonly contentFit?: "contain" | "cover" | "fill";
|
|
82
81
|
readonly thumbnailUrl?: string;
|