@umituz/react-native-video-editor 1.1.44 → 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.44",
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",
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";
@@ -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";
@@ -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,59 @@ 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
  };
91
+
92
+ /**
93
+ * Safely seek to a specific time position (in seconds)
94
+ */
95
+ export const safeSeekTo = (player: VideoPlayer | null, seconds: number): boolean => {
96
+ if (!player) return false;
97
+
98
+ try {
99
+ player.currentTime = Math.max(0, seconds);
100
+ return true;
101
+ } catch (error) {
102
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
103
+ console.log("[VideoPlayer] SeekTo error:", error);
104
+ }
105
+ return false;
106
+ }
107
+ };
108
+
109
+ /**
110
+ * Safely set muted state
111
+ */
112
+ export const safeMute = (player: VideoPlayer | null, muted: boolean): boolean => {
113
+ if (!player) return false;
114
+
115
+ try {
116
+ player.muted = muted;
117
+ return true;
118
+ } catch (error) {
119
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
120
+ console.log("[VideoPlayer] Mute error:", error);
121
+ }
122
+ return false;
123
+ }
124
+ };
125
+
126
+ /**
127
+ * Safely replay video from beginning
128
+ */
129
+ export const safeReplay = (player: VideoPlayer | null): boolean => {
130
+ if (!player) return false;
131
+
132
+ try {
133
+ player.currentTime = 0;
134
+ player.play();
135
+ return true;
136
+ } catch (error) {
137
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
138
+ console.log("[VideoPlayer] Replay error:", error);
139
+ }
140
+ return false;
141
+ }
142
+ };
@@ -0,0 +1,170 @@
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
+ // Only cache/play when modal is visible
40
+ const { localUri, isDownloading, downloadProgress, error } = useVideoCaching(
41
+ visible ? source : null,
42
+ );
43
+
44
+ const { player, state, controls } = useVideoPlayerControl({
45
+ source: visible ? localUri : null,
46
+ loop,
47
+ muted: false,
48
+ autoPlay,
49
+ });
50
+
51
+ const { visible: controlsVisible, toggle: toggleControls } = useControlsAutoHide({
52
+ isPlaying: state.isPlaying,
53
+ autoHideDelay: 3000,
54
+ });
55
+
56
+ const handleClose = useCallback(() => {
57
+ controls.pause();
58
+ onClose();
59
+ }, [controls, onClose]);
60
+
61
+ const styles = useMemo(
62
+ () =>
63
+ StyleSheet.create({
64
+ container: {
65
+ flex: 1,
66
+ backgroundColor: "#000000",
67
+ justifyContent: "center",
68
+ alignItems: "center",
69
+ },
70
+ video: {
71
+ width: "100%",
72
+ height: "100%",
73
+ },
74
+ centerContent: {
75
+ ...StyleSheet.absoluteFillObject,
76
+ justifyContent: "center",
77
+ alignItems: "center",
78
+ backgroundColor: "rgba(0,0,0,0.5)",
79
+ },
80
+ progressText: {
81
+ color: "#FFFFFF",
82
+ fontSize: 18,
83
+ fontWeight: "600",
84
+ },
85
+ errorText: {
86
+ color: tokens.colors.error,
87
+ fontSize: 16,
88
+ textAlign: "center",
89
+ padding: 24,
90
+ },
91
+ thumbnail: {
92
+ ...StyleSheet.absoluteFillObject,
93
+ },
94
+ }),
95
+ [tokens.colors.error],
96
+ );
97
+
98
+ const renderContent = () => {
99
+ if (error) {
100
+ return (
101
+ <View style={styles.container}>
102
+ <AtomicText style={styles.errorText}>{error}</AtomicText>
103
+ </View>
104
+ );
105
+ }
106
+
107
+ if (isDownloading) {
108
+ return (
109
+ <View style={styles.container}>
110
+ {thumbnailUrl && (
111
+ <Image source={{ uri: thumbnailUrl }} style={styles.thumbnail} contentFit="cover" />
112
+ )}
113
+ <View style={styles.centerContent}>
114
+ <AtomicText style={styles.progressText}>
115
+ {Math.round(downloadProgress * 100)}%
116
+ </AtomicText>
117
+ </View>
118
+ </View>
119
+ );
120
+ }
121
+
122
+ if (state.isPlayerValid && player) {
123
+ return (
124
+ <View style={styles.container}>
125
+ <VideoView
126
+ player={player}
127
+ style={styles.video}
128
+ contentFit="contain"
129
+ nativeControls={false}
130
+ />
131
+ <VideoPlayerOverlay
132
+ visible={controlsVisible}
133
+ isPlaying={state.isPlaying}
134
+ isMuted={state.isMuted}
135
+ currentTime={state.currentTime}
136
+ duration={state.duration}
137
+ title={title}
138
+ subtitle={subtitle}
139
+ onTogglePlay={controls.toggle}
140
+ onToggleMute={controls.toggleMute}
141
+ onSeek={controls.seekTo}
142
+ onBack={handleClose}
143
+ onTap={toggleControls}
144
+ />
145
+ </View>
146
+ );
147
+ }
148
+
149
+ return (
150
+ <View style={styles.container}>
151
+ {thumbnailUrl && (
152
+ <Image source={{ uri: thumbnailUrl }} style={styles.thumbnail} contentFit="cover" />
153
+ )}
154
+ </View>
155
+ );
156
+ };
157
+
158
+ return (
159
+ <Modal
160
+ visible={visible}
161
+ animationType="fade"
162
+ presentationStyle="fullScreen"
163
+ supportedOrientations={["portrait", "landscape"]}
164
+ onRequestClose={handleClose}
165
+ >
166
+ <StatusBar hidden={visible} />
167
+ {renderContent()}
168
+ </Modal>
169
+ );
170
+ };
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * VideoPlayer Component
3
- * Reusable video player with caching, thumbnail and controls
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,17 +22,20 @@ 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
- declare const __DEV__: boolean;
28
+ const DEFAULT_ASPECT_RATIO = 16 / 9;
27
29
 
28
- const ASPECT_RATIO = 16 / 9;
29
-
30
- /** Extract numeric width from style prop */
31
- const getWidthFromStyle = (style: ViewStyle | undefined): number | null => {
32
- if (!style) return null;
33
- const w = style.width;
34
- if (typeof w === "number") return w;
35
- 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
+ );
36
39
  };
37
40
 
38
41
  export const VideoPlayer: React.FC<VideoPlayerProps> = ({
@@ -41,22 +44,27 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
41
44
  loop = true,
42
45
  muted = false,
43
46
  autoPlay = false,
47
+ showControls = false,
44
48
  nativeControls = true,
45
49
  contentFit = "cover",
46
50
  style,
47
51
  playbackRate = 1,
48
52
  filterOverlay,
53
+ title,
54
+ subtitle,
55
+ onBack,
56
+ onProgress,
49
57
  }) => {
50
- // IMPORTANT: Call useResponsive BEFORE useAppDesignTokens to maintain hook order
51
58
  const { width: screenWidth, horizontalPadding } = useResponsive();
52
59
  const tokens = useAppDesignTokens();
53
60
  const [userTriggeredPlay, setUserTriggeredPlay] = useState(false);
54
61
  const showVideo = autoPlay || userTriggeredPlay;
55
62
 
56
- // Cache the video first (downloads if needed)
63
+ // When showControls is true, disable native controls and show custom overlay
64
+ const useNativeControls = showControls ? false : nativeControls;
65
+
57
66
  const { localUri, isDownloading, downloadProgress, error } = useVideoCaching(source);
58
67
 
59
- // Use cached local URI for player
60
68
  const { player, state, controls } = useVideoPlayerControl({
61
69
  source: localUri,
62
70
  loop,
@@ -65,31 +73,48 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
65
73
  playbackRate,
66
74
  });
67
75
 
68
- const handlePlay = useCallback(() => {
69
- if (__DEV__) {
70
- console.log("[VideoPlayer] handlePlay, localUri:", localUri);
76
+ const { visible: controlsVisible, toggle: toggleControls } = useControlsAutoHide({
77
+ isPlaying: state.isPlaying,
78
+ autoHideDelay: 3000,
79
+ });
80
+
81
+ // Notify parent of progress changes
82
+ useEffect(() => {
83
+ if (onProgress && state.duration > 0) {
84
+ onProgress(state.currentTime, state.duration);
71
85
  }
86
+ }, [onProgress, state.currentTime, state.duration]);
87
+
88
+ const handlePlay = useCallback(() => {
72
89
  setUserTriggeredPlay(true);
73
90
  controls.play();
74
- }, [localUri, controls]);
91
+ }, [controls]);
75
92
 
76
- // Calculate dimensions
77
- const videoWidth = getWidthFromStyle(style as ViewStyle) ?? (screenWidth - horizontalPadding * 2);
78
- 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;
79
97
 
80
98
  const containerStyle = useMemo(() => ({
81
- width: videoWidth,
82
- height: videoHeight,
99
+ ...(videoWidth !== undefined && { width: videoWidth }),
100
+ ...(videoHeight !== undefined && { height: videoHeight }),
83
101
  backgroundColor: tokens.colors.surface,
84
102
  borderRadius: 16,
85
103
  overflow: "hidden" as const,
86
104
  }), [tokens.colors.surface, videoWidth, videoHeight]);
87
105
 
88
106
  const styles = useMemo(() => StyleSheet.create({
89
- video: { width: videoWidth, height: videoHeight },
107
+ video: videoWidth !== undefined
108
+ ? { width: videoWidth, height: videoHeight! }
109
+ : { width: "100%", height: "100%" },
90
110
  thumbnailContainer: { flex: 1, justifyContent: "center", alignItems: "center" },
91
- thumbnail: { width: videoWidth, height: videoHeight },
92
- 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
+ },
93
118
  playButtonContainer: { ...StyleSheet.absoluteFillObject, justifyContent: "center", alignItems: "center" },
94
119
  playButton: {
95
120
  width: 64, height: 64, borderRadius: 32,
@@ -104,7 +129,6 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
104
129
  errorText: { color: tokens.colors.error, fontSize: 14, textAlign: "center", padding: 16 },
105
130
  }), [tokens, videoWidth, videoHeight]);
106
131
 
107
- // Show error state
108
132
  if (error) {
109
133
  return (
110
134
  <View style={[containerStyle, style]}>
@@ -118,7 +142,6 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
118
142
  );
119
143
  }
120
144
 
121
- // Show download progress
122
145
  if (isDownloading) {
123
146
  const progressPercent = Math.round(downloadProgress * 100);
124
147
  return (
@@ -137,18 +160,14 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
137
160
  );
138
161
  }
139
162
 
140
- // Show video player
141
163
  if (showVideo && state.isPlayerValid && player) {
142
- if (__DEV__) {
143
- console.log("[VideoPlayer] Rendering VideoView:", { videoWidth, videoHeight });
144
- }
145
164
  return (
146
165
  <View style={[containerStyle, style]}>
147
166
  <VideoView
148
167
  player={player}
149
168
  style={styles.video}
150
169
  contentFit={contentFit}
151
- nativeControls={nativeControls}
170
+ nativeControls={useNativeControls}
152
171
  />
153
172
  {filterOverlay && filterOverlay.opacity > 0 && (
154
173
  <View
@@ -159,11 +178,26 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
159
178
  pointerEvents="none"
160
179
  />
161
180
  )}
181
+ {showControls && (
182
+ <VideoPlayerOverlay
183
+ visible={controlsVisible}
184
+ isPlaying={state.isPlaying}
185
+ isMuted={state.isMuted}
186
+ currentTime={state.currentTime}
187
+ duration={state.duration}
188
+ title={title}
189
+ subtitle={subtitle}
190
+ onTogglePlay={controls.toggle}
191
+ onToggleMute={controls.toggleMute}
192
+ onSeek={controls.seekTo}
193
+ onBack={onBack}
194
+ onTap={toggleControls}
195
+ />
196
+ )}
162
197
  </View>
163
198
  );
164
199
  }
165
200
 
166
- // Show thumbnail with play button
167
201
  return (
168
202
  <TouchableOpacity style={[containerStyle, style]} onPress={handlePlay} activeOpacity={0.8}>
169
203
  <View style={styles.thumbnailContainer}>
@@ -0,0 +1,159 @@
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
+
10
+ import type { VideoPlayerOverlayProps } from "../../types";
11
+ import { VideoProgressBar } from "./VideoProgressBar";
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
+
83
+ export const VideoPlayerOverlay: React.FC<VideoPlayerOverlayProps> = ({
84
+ visible,
85
+ isPlaying,
86
+ isMuted,
87
+ currentTime,
88
+ duration,
89
+ title,
90
+ subtitle,
91
+ onTogglePlay,
92
+ onToggleMute,
93
+ onSeek,
94
+ onBack,
95
+ onTap,
96
+ }) => {
97
+ const hasTopBar = Boolean(onBack || title || subtitle);
98
+
99
+ if (!visible) {
100
+ return (
101
+ <TouchableWithoutFeedback onPress={onTap}>
102
+ <View style={styles.overlay} />
103
+ </TouchableWithoutFeedback>
104
+ );
105
+ }
106
+
107
+ return (
108
+ <TouchableWithoutFeedback onPress={onTap}>
109
+ <View style={styles.overlay}>
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
+ )}
126
+
127
+ {/* Center Play/Pause */}
128
+ <View style={styles.centerArea}>
129
+ <TouchableOpacity style={styles.centerPlayButton} onPress={onTogglePlay} activeOpacity={0.7}>
130
+ <AtomicIcon
131
+ name={isPlaying ? "pause" : "play"}
132
+ customSize={36}
133
+ color="onPrimary"
134
+ />
135
+ </TouchableOpacity>
136
+ </View>
137
+
138
+ {/* Bottom Bar — progress + mute */}
139
+ <View style={styles.bottomBar}>
140
+ <VideoProgressBar
141
+ currentTime={currentTime}
142
+ duration={duration}
143
+ onSeek={onSeek}
144
+ showTimeLabels
145
+ />
146
+ <View style={styles.bottomControls}>
147
+ <TouchableOpacity style={styles.controlButton} onPress={onToggleMute} activeOpacity={0.7}>
148
+ <AtomicIcon
149
+ name={isMuted ? "volume-x" : "volume-2"}
150
+ customSize={18}
151
+ color="onPrimary"
152
+ />
153
+ </TouchableOpacity>
154
+ </View>
155
+ </View>
156
+ </View>
157
+ </TouchableWithoutFeedback>
158
+ );
159
+ };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * VideoProgressBar Component
3
+ * Seekable progress bar with time labels for video playback
4
+ */
5
+
6
+ import React, { useCallback, useRef } 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 = 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
+ return (
47
+ <View style={[barStyles.container, style]}>
48
+ <View style={barStyles.row}>
49
+ {showTimeLabels && (
50
+ <AtomicText style={barStyles.timeText}>{formatTimeDisplay(currentTime)}</AtomicText>
51
+ )}
52
+ <View
53
+ style={[barStyles.barContainer, { height, borderRadius: height / 2, backgroundColor: resolvedTrackColor }]}
54
+ onLayout={handleLayout}
55
+ onStartShouldSetResponder={() => Boolean(onSeek)}
56
+ onResponderRelease={handleSeek}
57
+ >
58
+ <View style={[barStyles.barFill, { borderRadius: height / 2, backgroundColor: resolvedFillColor, width: `${progressPercent}%` }]} />
59
+ </View>
60
+ {showTimeLabels && (
61
+ <AtomicText style={[barStyles.timeText, { textAlign: "right" }]}>
62
+ {formatTimeDisplay(duration)}
63
+ </AtomicText>
64
+ )}
65
+ </View>
66
+ </View>
67
+ );
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
+ });
@@ -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,61 @@
1
+ /**
2
+ * useVideoPlaybackProgress Hook
3
+ * Tracks currentTime, duration, progress, and actual playing state 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
+ const POLL_INTERVAL_MS = 250;
11
+
12
+ /**
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
15
+ */
16
+ export const useVideoPlaybackProgress = (
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ player: any,
19
+ isPlayerValid: boolean,
20
+ isPlaying: boolean,
21
+ onPlayingStateChanged?: (actuallyPlaying: boolean) => void,
22
+ ): PlaybackProgressState => {
23
+ const [currentTime, setCurrentTime] = useState(0);
24
+ const [duration, setDuration] = useState(0);
25
+ const onPlayingStateChangedRef = useRef(onPlayingStateChanged);
26
+ onPlayingStateChangedRef.current = onPlayingStateChanged;
27
+
28
+ const pollPlayer = useCallback(() => {
29
+ if (!player || !isPlayerValid) return;
30
+
31
+ try {
32
+ const ct = typeof player.currentTime === "number" ? Math.max(0, player.currentTime) : 0;
33
+ const dur = typeof player.duration === "number" && isFinite(player.duration) ? player.duration : 0;
34
+
35
+ setCurrentTime(ct);
36
+ if (dur > 0) setDuration(dur);
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);
42
+ }
43
+ } catch {
44
+ // Player may be in an invalid state during transitions
45
+ }
46
+ }, [player, isPlayerValid, isPlaying]);
47
+
48
+ useEffect(() => {
49
+ // Poll once immediately to get initial state
50
+ pollPlayer();
51
+
52
+ if (isPlaying && isPlayerValid) {
53
+ const id = setInterval(pollPlayer, POLL_INTERVAL_MS);
54
+ return () => clearInterval(id);
55
+ }
56
+ }, [isPlaying, isPlayerValid, pollPlayer]);
57
+
58
+ const progress = duration > 0 ? Math.min(currentTime / duration, 1) : 0;
59
+
60
+ return { currentTime, duration, progress };
61
+ };
@@ -26,9 +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";
30
-
31
- declare const __DEV__: boolean;
33
+ import { useVideoPlaybackProgress } from "./useVideoPlaybackProgress";
32
34
 
33
35
  /**
34
36
  * Hook for managing video player with safe operations
@@ -41,25 +43,16 @@ export const useVideoPlayerControl = (
41
43
  const [isPlaying, setIsPlaying] = useState(false);
42
44
  const [isLoading, setIsLoading] = useState(true);
43
45
  const [playbackRate, setPlaybackRateState] = useState(initialRate);
46
+ const [isMuted, setIsMuted] = useState(muted);
44
47
 
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
49
  const player = useExpoVideoPlayer(source || "", (p: any) => {
46
- if (typeof __DEV__ !== "undefined" && __DEV__) {
47
- console.log("[useVideoPlayerControl] Player callback, source:", source, "player:", !!p);
48
- }
49
50
  if (source && p) {
50
51
  configurePlayer(p, { loop, muted, autoPlay });
51
52
  setIsLoading(false);
52
53
  if (autoPlay) {
53
54
  setIsPlaying(true);
54
55
  }
55
- if (typeof __DEV__ !== "undefined" && __DEV__) {
56
- console.log("[useVideoPlayerControl] Player configured:", {
57
- status: p.status,
58
- playing: p.playing,
59
- muted: p.muted,
60
- loop: p.loop,
61
- });
62
- }
63
56
  }
64
57
  });
65
58
 
@@ -68,6 +61,19 @@ export const useVideoPlayerControl = (
68
61
  [player, source],
69
62
  );
70
63
 
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
+ );
76
+
71
77
  const play = useCallback(() => {
72
78
  if (!isPlayerValid) return;
73
79
  const success = safePlay(player);
@@ -88,23 +94,52 @@ export const useVideoPlayerControl = (
88
94
 
89
95
  const setPlaybackRate = useCallback((rate: number) => {
90
96
  if (!isPlayerValid || !player) return;
97
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
98
  (player as any).playbackRate = rate;
92
99
  setPlaybackRateState(rate);
93
100
  }, [player, isPlayerValid]);
94
101
 
102
+ const toggleMute = useCallback(() => {
103
+ if (!isPlayerValid) return;
104
+ const newMuted = !isMuted;
105
+ const success = safeMute(player, newMuted);
106
+ if (success) setIsMuted(newMuted);
107
+ }, [player, isPlayerValid, isMuted]);
108
+
109
+ const setMutedFn = useCallback((value: boolean) => {
110
+ if (!isPlayerValid) return;
111
+ const success = safeMute(player, value);
112
+ if (success) setIsMuted(value);
113
+ }, [player, isPlayerValid]);
114
+
115
+ const seekTo = useCallback((seconds: number) => {
116
+ if (!isPlayerValid) return;
117
+ safeSeekTo(player, seconds);
118
+ }, [player, isPlayerValid]);
119
+
120
+ const replay = useCallback(() => {
121
+ if (!isPlayerValid) return;
122
+ const success = safeReplay(player);
123
+ if (success) setIsPlaying(true);
124
+ }, [player, isPlayerValid]);
125
+
95
126
  const state: VideoPlayerState = useMemo(
96
127
  () => ({
97
128
  isPlaying,
98
129
  isPlayerValid,
99
130
  isLoading: isLoading && Boolean(source),
100
131
  playbackRate,
132
+ isMuted,
133
+ currentTime,
134
+ duration,
135
+ progress,
101
136
  }),
102
- [isPlaying, isPlayerValid, isLoading, source, playbackRate],
137
+ [isPlaying, isPlayerValid, isLoading, source, playbackRate, isMuted, currentTime, duration, progress],
103
138
  );
104
139
 
105
140
  const controls: VideoPlayerControls = useMemo(
106
- () => ({ play, pause, toggle, setPlaybackRate }),
107
- [play, pause, toggle, setPlaybackRate],
141
+ () => ({ play, pause, toggle, setPlaybackRate, toggleMute, setMuted: setMutedFn, seekTo, replay }),
142
+ [play, pause, toggle, setPlaybackRate, toggleMute, setMutedFn, seekTo, replay],
108
143
  );
109
144
 
110
145
  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
  /**
@@ -61,19 +69,95 @@ export interface VideoVisibilityConfig {
61
69
  */
62
70
  export interface VideoPlayerProps {
63
71
  readonly source: string | null;
64
- readonly isVisible?: boolean;
65
72
  readonly loop?: boolean;
66
73
  readonly muted?: boolean;
67
74
  readonly autoPlay?: boolean;
75
+ /** When true, shows custom overlay controls and disables nativeControls */
68
76
  readonly showControls?: boolean;
77
+ /** Native video controls (ignored when showControls is true) */
69
78
  readonly nativeControls?: boolean;
70
- readonly onPlayingChange?: (isPlaying: boolean) => void;
71
- readonly onError?: (error: Error) => void;
72
79
  readonly style?: ViewStyle;
73
80
  readonly contentFit?: "contain" | "cover" | "fill";
74
81
  readonly thumbnailUrl?: string;
75
82
  readonly playbackRate?: number;
76
83
  readonly filterOverlay?: { overlay: string; opacity: number };
84
+ readonly title?: string;
85
+ readonly subtitle?: string;
86
+ readonly onBack?: () => void;
87
+ readonly onProgress?: (currentTime: number, duration: number) => void;
88
+ }
89
+
90
+ /**
91
+ * Playback progress state
92
+ */
93
+ export interface PlaybackProgressState {
94
+ readonly currentTime: number;
95
+ readonly duration: number;
96
+ readonly progress: number;
97
+ }
98
+
99
+ /**
100
+ * Controls auto-hide configuration
101
+ */
102
+ export interface ControlsAutoHideConfig {
103
+ readonly autoHideDelay?: number;
104
+ readonly isPlaying: boolean;
105
+ }
106
+
107
+ /**
108
+ * Controls auto-hide result
109
+ */
110
+ export interface ControlsAutoHideResult {
111
+ readonly visible: boolean;
112
+ readonly show: () => void;
113
+ readonly hide: () => void;
114
+ readonly toggle: () => void;
115
+ }
116
+
117
+ /**
118
+ * Video progress bar props
119
+ */
120
+ export interface VideoProgressBarProps {
121
+ readonly currentTime: number;
122
+ readonly duration: number;
123
+ readonly onSeek?: (seconds: number) => void;
124
+ readonly showTimeLabels?: boolean;
125
+ readonly height?: number;
126
+ readonly trackColor?: string;
127
+ readonly fillColor?: string;
128
+ readonly style?: ViewStyle;
129
+ }
130
+
131
+ /**
132
+ * Video player overlay props
133
+ */
134
+ export interface VideoPlayerOverlayProps {
135
+ readonly visible: boolean;
136
+ readonly isPlaying: boolean;
137
+ readonly isMuted: boolean;
138
+ readonly currentTime: number;
139
+ readonly duration: number;
140
+ readonly title?: string;
141
+ readonly subtitle?: string;
142
+ readonly onTogglePlay: () => void;
143
+ readonly onToggleMute: () => void;
144
+ readonly onSeek: (seconds: number) => void;
145
+ readonly onBack?: () => void;
146
+ readonly onTap: () => void;
147
+ }
148
+
149
+ /**
150
+ * Full screen video player props
151
+ */
152
+ export interface FullScreenVideoPlayerProps {
153
+ readonly visible: boolean;
154
+ readonly source: string | null;
155
+ readonly title?: string;
156
+ readonly subtitle?: string;
157
+ readonly thumbnailUrl?: string;
158
+ readonly onClose: () => void;
159
+ readonly loop?: boolean;
160
+ readonly autoPlay?: boolean;
77
161
  }
78
162
 
79
163
  export type { VideoPlayer } from "expo-video";