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

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