@umituz/react-native-video-editor 1.1.45 → 1.1.46

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-video-editor",
3
- "version": "1.1.45",
3
+ "version": "1.1.46",
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("[safePlay] Play error:", error);
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 ignored:", 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 ignored:", 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 ignored:", 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={[styles.centerContent, { backgroundColor: "rgba(0,0,0,0.5)" }]}>
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
- declare const __DEV__: boolean;
28
+ const DEFAULT_ASPECT_RATIO = 16 / 9;
29
29
 
30
- const ASPECT_RATIO = 16 / 9;
31
-
32
- /** Extract numeric width from style prop */
33
- const getWidthFromStyle = (style: ViewStyle | undefined): number | null => {
34
- if (!style) return null;
35
- const w = style.width;
36
- if (typeof w === "number") return w;
37
- return null;
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
- }, [localUri, controls]);
91
+ }, [controls]);
98
92
 
99
- // Calculate dimensions
100
- const videoWidth = getWidthFromStyle(style as ViewStyle) ?? (screenWidth - horizontalPadding * 2);
101
- const videoHeight = videoWidth / ASPECT_RATIO;
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: { width: videoWidth, height: videoHeight },
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: { width: videoWidth, height: videoHeight },
115
- placeholder: { width: videoWidth, height: videoHeight, backgroundColor: tokens.colors.surfaceSecondary },
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 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
- );
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
- <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>
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, useMemo } from "react";
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 = React.useRef(0);
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={[styles.container, style]}>
70
- <View style={styles.row}>
47
+ <View style={[barStyles.container, style]}>
48
+ <View style={barStyles.row}>
71
49
  {showTimeLabels && (
72
- <AtomicText style={styles.timeText}>{formatTimeDisplay(currentTime)}</AtomicText>
50
+ <AtomicText style={barStyles.timeText}>{formatTimeDisplay(currentTime)}</AtomicText>
73
51
  )}
74
52
  <View
75
- style={styles.barContainer}
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={[styles.barFill, { width: `${progressPercent}%` }]} />
58
+ <View style={[barStyles.barFill, { borderRadius: height / 2, backgroundColor: resolvedFillColor, width: `${progressPercent}%` }]} />
81
59
  </View>
82
60
  {showTimeLabels && (
83
- <AtomicText style={[styles.timeText, { textAlign: "right" }]}>
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 progress by polling the expo-video player
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 intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
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
- } catch (error) {
37
- if (typeof __DEV__ !== "undefined" && __DEV__) {
38
- console.log("[useVideoPlaybackProgress] Poll error:", error);
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
- intervalRef.current = setInterval(pollPlayer, POLL_INTERVAL_MS);
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
- // Track playback progress
77
- const { currentTime, duration, progress } = useVideoPlaybackProgress(player, isPlayerValid, isPlaying);
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 setMuted = useCallback((value: boolean) => {
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, setMuted, seekTo, replay],
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;