@umituz/react-native-video-editor 1.0.14 → 1.0.17
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 +13 -4
- package/src/player/index.ts +13 -0
- package/src/player/infrastructure/services/video-cache.service.ts +136 -0
- package/src/player/presentation/components/VideoPlayer.tsx +72 -80
- package/src/player/presentation/hooks/useVideoCaching.ts +104 -0
- package/src/player/presentation/hooks/useVideoPlayerControl.ts +1 -26
- package/src/player/types/index.ts +9 -0
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-video-editor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
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",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"typecheck": "
|
|
8
|
+
"typecheck": "npx tsc --noEmit",
|
|
9
9
|
"lint": "echo 'Lint passed'",
|
|
10
10
|
"version:patch": "npm version patch -m 'chore: release v%s'",
|
|
11
11
|
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
@@ -26,22 +26,31 @@
|
|
|
26
26
|
"url": "https://github.com/umituz/react-native-video-editor"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"react": ">=18.2.0",
|
|
30
|
-
"react-native": ">=0.74.0",
|
|
31
29
|
"@umituz/react-native-design-system": ">=1.0.0",
|
|
32
30
|
"expo-document-picker": ">=14.0.0",
|
|
31
|
+
"expo-file-system": ">=17.0.0",
|
|
33
32
|
"expo-image": ">=1.0.0",
|
|
34
33
|
"expo-video": ">=3.0.0",
|
|
34
|
+
"react": ">=18.2.0",
|
|
35
|
+
"react-native": ">=0.74.0",
|
|
35
36
|
"zustand": ">=4.0.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
39
|
+
"@gorhom/bottom-sheet": "^5.2.8",
|
|
38
40
|
"@types/react": "~19.1.10",
|
|
39
41
|
"@umituz/react-native-design-system": "latest",
|
|
42
|
+
"@umituz/react-native-filesystem": "latest",
|
|
43
|
+
"expo-application": "^7.0.8",
|
|
44
|
+
"expo-device": "^8.0.10",
|
|
40
45
|
"expo-document-picker": "^14.0.8",
|
|
46
|
+
"expo-file-system": "^19.0.0",
|
|
41
47
|
"expo-image": "^3.0.11",
|
|
48
|
+
"expo-linear-gradient": "^15.0.7",
|
|
42
49
|
"expo-video": "^3.0.15",
|
|
43
50
|
"react": "19.1.0",
|
|
44
51
|
"react-native": "0.81.5",
|
|
52
|
+
"react-native-gesture-handler": "^2.30.0",
|
|
53
|
+
"react-native-reanimated": "^4.2.1",
|
|
45
54
|
"typescript": "~5.9.2"
|
|
46
55
|
},
|
|
47
56
|
"publishConfig": {
|
package/src/player/index.ts
CHANGED
|
@@ -12,6 +12,9 @@ export type {
|
|
|
12
12
|
VideoVisibilityConfig,
|
|
13
13
|
VideoPlayerProps,
|
|
14
14
|
VideoPlayer as VideoPlayerType,
|
|
15
|
+
VideoDownloadProgressCallback,
|
|
16
|
+
VideoCacheResult,
|
|
17
|
+
VideoCachingState,
|
|
15
18
|
} from "./types";
|
|
16
19
|
|
|
17
20
|
// Services
|
|
@@ -23,9 +26,19 @@ export {
|
|
|
23
26
|
configurePlayer,
|
|
24
27
|
} from "./infrastructure/services/player-control.service";
|
|
25
28
|
|
|
29
|
+
export {
|
|
30
|
+
isVideoCached,
|
|
31
|
+
getCachedVideoUri,
|
|
32
|
+
downloadVideo,
|
|
33
|
+
getOrDownloadVideo,
|
|
34
|
+
clearVideoCache,
|
|
35
|
+
deleteSpecificCachedVideo,
|
|
36
|
+
} from "./infrastructure/services/video-cache.service";
|
|
37
|
+
|
|
26
38
|
// Hooks
|
|
27
39
|
export { useVideoPlayerControl } from "./presentation/hooks/useVideoPlayerControl";
|
|
28
40
|
export { useVideoVisibility } from "./presentation/hooks/useVideoVisibility";
|
|
41
|
+
export { useVideoCaching } from "./presentation/hooks/useVideoCaching";
|
|
29
42
|
|
|
30
43
|
// Components
|
|
31
44
|
export { VideoPlayer } from "./presentation/components/VideoPlayer";
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Cache Service
|
|
3
|
+
* Downloads and caches remote videos for playback
|
|
4
|
+
* Bypasses CDN Range request issues by downloading entire file first
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
downloadFileWithProgress,
|
|
9
|
+
getCachedFileUri,
|
|
10
|
+
isUrlCached,
|
|
11
|
+
deleteCachedFile,
|
|
12
|
+
getCacheDirectory,
|
|
13
|
+
clearCache,
|
|
14
|
+
type DownloadProgressCallback,
|
|
15
|
+
type DownloadProgress,
|
|
16
|
+
} from "@umituz/react-native-filesystem";
|
|
17
|
+
|
|
18
|
+
declare const __DEV__: boolean;
|
|
19
|
+
|
|
20
|
+
const VIDEO_CACHE_SUBDIR = "video-cache";
|
|
21
|
+
|
|
22
|
+
/** Cache result type */
|
|
23
|
+
export interface VideoCacheResult {
|
|
24
|
+
readonly uri: string;
|
|
25
|
+
readonly fromCache: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Re-export progress callback type */
|
|
29
|
+
export type VideoDownloadProgressCallback = (progress: number) => void;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get video cache directory path
|
|
33
|
+
*/
|
|
34
|
+
const getVideoCacheDir = (): string => {
|
|
35
|
+
const cacheDir = getCacheDirectory();
|
|
36
|
+
return `${cacheDir}${VIDEO_CACHE_SUBDIR}/`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if video is already cached
|
|
41
|
+
*/
|
|
42
|
+
export const isVideoCached = (url: string): boolean => {
|
|
43
|
+
if (!url || url.startsWith("file://")) return false;
|
|
44
|
+
return isUrlCached(url, getVideoCacheDir());
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get cached video URI if exists
|
|
49
|
+
*/
|
|
50
|
+
export const getCachedVideoUri = (url: string): string | null => {
|
|
51
|
+
if (!url) return null;
|
|
52
|
+
if (url.startsWith("file://")) return url;
|
|
53
|
+
return getCachedFileUri(url, getVideoCacheDir());
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Download video to cache with progress tracking
|
|
58
|
+
*/
|
|
59
|
+
export const downloadVideo = async (
|
|
60
|
+
url: string,
|
|
61
|
+
onProgress?: VideoDownloadProgressCallback,
|
|
62
|
+
): Promise<string> => {
|
|
63
|
+
if (!url) throw new Error("URL is required");
|
|
64
|
+
if (url.startsWith("file://")) return url;
|
|
65
|
+
|
|
66
|
+
if (__DEV__) {
|
|
67
|
+
console.log("[VideoCache] Downloading:", url);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const progressCallback: DownloadProgressCallback | undefined = onProgress
|
|
71
|
+
? (progress: DownloadProgress) => {
|
|
72
|
+
const percent = progress.totalBytesExpectedToWrite > 0
|
|
73
|
+
? progress.totalBytesWritten / progress.totalBytesExpectedToWrite
|
|
74
|
+
: 0;
|
|
75
|
+
onProgress(percent);
|
|
76
|
+
}
|
|
77
|
+
: undefined;
|
|
78
|
+
|
|
79
|
+
const result = await downloadFileWithProgress(
|
|
80
|
+
url,
|
|
81
|
+
getVideoCacheDir(),
|
|
82
|
+
progressCallback,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (!result.success || !result.uri) {
|
|
86
|
+
throw new Error(result.error || "Download failed");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (__DEV__) {
|
|
90
|
+
console.log("[VideoCache] Download complete:", result.uri);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result.uri;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get video from cache or download if not cached
|
|
98
|
+
*/
|
|
99
|
+
export const getOrDownloadVideo = async (
|
|
100
|
+
url: string,
|
|
101
|
+
onProgress?: VideoDownloadProgressCallback,
|
|
102
|
+
): Promise<VideoCacheResult> => {
|
|
103
|
+
if (!url) throw new Error("URL is required");
|
|
104
|
+
|
|
105
|
+
if (url.startsWith("file://")) {
|
|
106
|
+
return { uri: url, fromCache: true };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check cache first
|
|
110
|
+
const cachedUri = getCachedVideoUri(url);
|
|
111
|
+
if (cachedUri) {
|
|
112
|
+
if (__DEV__) {
|
|
113
|
+
console.log("[VideoCache] Using cached video:", cachedUri);
|
|
114
|
+
}
|
|
115
|
+
return { uri: cachedUri, fromCache: true };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Download if not cached
|
|
119
|
+
const downloadedUri = await downloadVideo(url, onProgress);
|
|
120
|
+
return { uri: downloadedUri, fromCache: false };
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Clear all cached videos
|
|
125
|
+
*/
|
|
126
|
+
export const clearVideoCache = async (): Promise<void> => {
|
|
127
|
+
await clearCache();
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Delete a specific cached video
|
|
132
|
+
*/
|
|
133
|
+
export const deleteSpecificCachedVideo = (url: string): boolean => {
|
|
134
|
+
if (!url || url.startsWith("file://")) return false;
|
|
135
|
+
return deleteCachedFile(url, getVideoCacheDir());
|
|
136
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* VideoPlayer Component
|
|
3
|
-
* Reusable video player with thumbnail and controls
|
|
3
|
+
* Reusable video player with caching, thumbnail and controls
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React, { useState, useCallback, useMemo, useEffect } from "react";
|
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
useResponsive,
|
|
12
12
|
useAppDesignTokens,
|
|
13
13
|
AtomicIcon,
|
|
14
|
+
AtomicText,
|
|
14
15
|
} from "@umituz/react-native-design-system";
|
|
15
16
|
|
|
16
17
|
import type { VideoPlayerProps } from "../../types";
|
|
17
18
|
import { useVideoPlayerControl } from "../hooks/useVideoPlayerControl";
|
|
19
|
+
import { useVideoCaching } from "../hooks/useVideoCaching";
|
|
18
20
|
|
|
19
21
|
declare const __DEV__: boolean;
|
|
20
22
|
|
|
@@ -38,14 +40,17 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
38
40
|
contentFit = "cover",
|
|
39
41
|
style,
|
|
40
42
|
}) => {
|
|
41
|
-
// IMPORTANT: Call useResponsive BEFORE useAppDesignTokens to maintain
|
|
42
|
-
// useAppDesignTokens internally calls useResponsive, so order matters
|
|
43
|
+
// IMPORTANT: Call useResponsive BEFORE useAppDesignTokens to maintain hook order
|
|
43
44
|
const { width: screenWidth, horizontalPadding } = useResponsive();
|
|
44
45
|
const tokens = useAppDesignTokens();
|
|
45
46
|
const [showVideo, setShowVideo] = useState(autoPlay);
|
|
46
47
|
|
|
48
|
+
// Cache the video first (downloads if needed)
|
|
49
|
+
const { localUri, isDownloading, downloadProgress, error } = useVideoCaching(source);
|
|
50
|
+
|
|
51
|
+
// Use cached local URI for player
|
|
47
52
|
const { player, state, controls } = useVideoPlayerControl({
|
|
48
|
-
source,
|
|
53
|
+
source: localUri,
|
|
49
54
|
loop,
|
|
50
55
|
muted,
|
|
51
56
|
autoPlay: false,
|
|
@@ -53,23 +58,21 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
53
58
|
|
|
54
59
|
useEffect(() => {
|
|
55
60
|
if (showVideo && state.isPlayerValid && player) {
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
console.log("[VideoPlayer] Starting playback...");
|
|
61
|
+
if (__DEV__) {
|
|
62
|
+
console.log("[VideoPlayer] Starting playback from:", localUri);
|
|
59
63
|
}
|
|
60
64
|
controls.play();
|
|
61
65
|
}
|
|
62
|
-
}, [showVideo, state.isPlayerValid, player, controls]);
|
|
66
|
+
}, [showVideo, state.isPlayerValid, player, controls, localUri]);
|
|
63
67
|
|
|
64
68
|
const handlePlay = useCallback(() => {
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
console.log("[VideoPlayer] handlePlay called, source:", source);
|
|
69
|
+
if (__DEV__) {
|
|
70
|
+
console.log("[VideoPlayer] handlePlay, localUri:", localUri);
|
|
68
71
|
}
|
|
69
72
|
setShowVideo(true);
|
|
70
|
-
}, [
|
|
73
|
+
}, [localUri]);
|
|
71
74
|
|
|
72
|
-
// Calculate
|
|
75
|
+
// Calculate dimensions
|
|
73
76
|
const videoWidth = getWidthFromStyle(style as ViewStyle) ?? (screenWidth - horizontalPadding * 2);
|
|
74
77
|
const videoHeight = videoWidth / ASPECT_RATIO;
|
|
75
78
|
|
|
@@ -81,67 +84,63 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
81
84
|
overflow: "hidden" as const,
|
|
82
85
|
}), [tokens.colors.surface, videoWidth, videoHeight]);
|
|
83
86
|
|
|
84
|
-
const styles = useMemo(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
backgroundColor: tokens.colors.surfaceSecondary,
|
|
104
|
-
},
|
|
105
|
-
playButtonContainer: {
|
|
106
|
-
...StyleSheet.absoluteFillObject,
|
|
107
|
-
justifyContent: "center",
|
|
108
|
-
alignItems: "center",
|
|
109
|
-
},
|
|
110
|
-
playButton: {
|
|
111
|
-
width: 64,
|
|
112
|
-
height: 64,
|
|
113
|
-
borderRadius: 32,
|
|
114
|
-
backgroundColor: tokens.colors.primary,
|
|
115
|
-
justifyContent: "center",
|
|
116
|
-
alignItems: "center",
|
|
117
|
-
paddingLeft: 4,
|
|
118
|
-
},
|
|
119
|
-
}),
|
|
120
|
-
[tokens, videoWidth, videoHeight]
|
|
121
|
-
);
|
|
87
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
88
|
+
video: { width: videoWidth, height: videoHeight },
|
|
89
|
+
thumbnailContainer: { flex: 1, justifyContent: "center", alignItems: "center" },
|
|
90
|
+
thumbnail: { width: videoWidth, height: videoHeight },
|
|
91
|
+
placeholder: { width: videoWidth, height: videoHeight, backgroundColor: tokens.colors.surfaceSecondary },
|
|
92
|
+
playButtonContainer: { ...StyleSheet.absoluteFillObject, justifyContent: "center", alignItems: "center" },
|
|
93
|
+
playButton: {
|
|
94
|
+
width: 64, height: 64, borderRadius: 32,
|
|
95
|
+
backgroundColor: tokens.colors.primary,
|
|
96
|
+
justifyContent: "center", alignItems: "center", paddingLeft: 4,
|
|
97
|
+
},
|
|
98
|
+
progressContainer: {
|
|
99
|
+
...StyleSheet.absoluteFillObject,
|
|
100
|
+
justifyContent: "center", alignItems: "center",
|
|
101
|
+
backgroundColor: "rgba(0,0,0,0.5)",
|
|
102
|
+
},
|
|
103
|
+
progressText: { color: "#fff", fontSize: 16, fontWeight: "600" },
|
|
104
|
+
errorText: { color: tokens.colors.error, fontSize: 14, textAlign: "center", padding: 16 },
|
|
105
|
+
}), [tokens, videoWidth, videoHeight]);
|
|
122
106
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
107
|
+
// Show error state
|
|
108
|
+
if (error) {
|
|
109
|
+
return (
|
|
110
|
+
<View style={[containerStyle, style]}>
|
|
111
|
+
<View style={styles.thumbnailContainer}>
|
|
112
|
+
<View style={styles.placeholder} />
|
|
113
|
+
<View style={styles.progressContainer}>
|
|
114
|
+
<AtomicText style={styles.errorText}>{error}</AtomicText>
|
|
115
|
+
</View>
|
|
116
|
+
</View>
|
|
117
|
+
</View>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Show download progress
|
|
122
|
+
if (isDownloading) {
|
|
123
|
+
const progressPercent = Math.round(downloadProgress * 100);
|
|
124
|
+
return (
|
|
125
|
+
<View style={[containerStyle, style]}>
|
|
126
|
+
<View style={styles.thumbnailContainer}>
|
|
127
|
+
{thumbnailUrl ? (
|
|
128
|
+
<Image source={{ uri: thumbnailUrl }} style={styles.thumbnail} contentFit="cover" />
|
|
129
|
+
) : (
|
|
130
|
+
<View style={styles.placeholder} />
|
|
131
|
+
)}
|
|
132
|
+
<View style={styles.progressContainer}>
|
|
133
|
+
<AtomicText style={styles.progressText}>{progressPercent}%</AtomicText>
|
|
134
|
+
</View>
|
|
135
|
+
</View>
|
|
136
|
+
</View>
|
|
137
|
+
);
|
|
139
138
|
}
|
|
140
139
|
|
|
140
|
+
// Show video player
|
|
141
141
|
if (showVideo && state.isPlayerValid && player) {
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
console.log("[VideoPlayer] Rendering VideoView with dimensions:", { videoWidth, videoHeight });
|
|
142
|
+
if (__DEV__) {
|
|
143
|
+
console.log("[VideoPlayer] Rendering VideoView:", { videoWidth, videoHeight });
|
|
145
144
|
}
|
|
146
145
|
return (
|
|
147
146
|
<View style={[containerStyle, style]}>
|
|
@@ -155,19 +154,12 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|
|
155
154
|
);
|
|
156
155
|
}
|
|
157
156
|
|
|
157
|
+
// Show thumbnail with play button
|
|
158
158
|
return (
|
|
159
|
-
<TouchableOpacity
|
|
160
|
-
style={[containerStyle, style]}
|
|
161
|
-
onPress={handlePlay}
|
|
162
|
-
activeOpacity={0.8}
|
|
163
|
-
>
|
|
159
|
+
<TouchableOpacity style={[containerStyle, style]} onPress={handlePlay} activeOpacity={0.8}>
|
|
164
160
|
<View style={styles.thumbnailContainer}>
|
|
165
161
|
{thumbnailUrl ? (
|
|
166
|
-
<Image
|
|
167
|
-
source={{ uri: thumbnailUrl }}
|
|
168
|
-
style={styles.thumbnail}
|
|
169
|
-
contentFit="cover"
|
|
170
|
-
/>
|
|
162
|
+
<Image source={{ uri: thumbnailUrl }} style={styles.thumbnail} contentFit="cover" />
|
|
171
163
|
) : (
|
|
172
164
|
<View style={styles.placeholder} />
|
|
173
165
|
)}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useVideoCaching Hook
|
|
3
|
+
* Manages video caching state and download progress
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect, useCallback } from "react";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
getOrDownloadVideo,
|
|
10
|
+
isVideoCached,
|
|
11
|
+
type VideoCacheResult,
|
|
12
|
+
} from "../../infrastructure/services/video-cache.service";
|
|
13
|
+
|
|
14
|
+
declare const __DEV__: boolean;
|
|
15
|
+
|
|
16
|
+
/** Video caching state */
|
|
17
|
+
export interface VideoCachingState {
|
|
18
|
+
readonly localUri: string | null;
|
|
19
|
+
readonly isDownloading: boolean;
|
|
20
|
+
readonly downloadProgress: number;
|
|
21
|
+
readonly fromCache: boolean;
|
|
22
|
+
readonly error: string | null;
|
|
23
|
+
readonly retry: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Hook for managing video caching
|
|
28
|
+
* Downloads remote videos to local cache for playback
|
|
29
|
+
*/
|
|
30
|
+
export const useVideoCaching = (sourceUrl: string | null): VideoCachingState => {
|
|
31
|
+
const [localUri, setLocalUri] = useState<string | null>(null);
|
|
32
|
+
const [isDownloading, setIsDownloading] = useState(false);
|
|
33
|
+
const [downloadProgress, setDownloadProgress] = useState(0);
|
|
34
|
+
const [fromCache, setFromCache] = useState(false);
|
|
35
|
+
const [error, setError] = useState<string | null>(null);
|
|
36
|
+
|
|
37
|
+
const loadVideo = useCallback(async () => {
|
|
38
|
+
if (!sourceUrl) {
|
|
39
|
+
setLocalUri(null);
|
|
40
|
+
setIsDownloading(false);
|
|
41
|
+
setDownloadProgress(0);
|
|
42
|
+
setFromCache(false);
|
|
43
|
+
setError(null);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (sourceUrl.startsWith("file://")) {
|
|
48
|
+
setLocalUri(sourceUrl);
|
|
49
|
+
setIsDownloading(false);
|
|
50
|
+
setDownloadProgress(1);
|
|
51
|
+
setFromCache(true);
|
|
52
|
+
setError(null);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const cached = isVideoCached(sourceUrl);
|
|
57
|
+
setIsDownloading(!cached);
|
|
58
|
+
setDownloadProgress(cached ? 1 : 0);
|
|
59
|
+
setError(null);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const result: VideoCacheResult = await getOrDownloadVideo(
|
|
63
|
+
sourceUrl,
|
|
64
|
+
(progress) => setDownloadProgress(progress),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
setLocalUri(result.uri);
|
|
68
|
+
setFromCache(result.fromCache);
|
|
69
|
+
setIsDownloading(false);
|
|
70
|
+
setDownloadProgress(1);
|
|
71
|
+
|
|
72
|
+
if (__DEV__) {
|
|
73
|
+
console.log("[useVideoCaching] Video ready:", result.uri);
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const message = err instanceof Error ? err.message : "Download failed";
|
|
77
|
+
setError(message);
|
|
78
|
+
setIsDownloading(false);
|
|
79
|
+
setLocalUri(null);
|
|
80
|
+
|
|
81
|
+
if (__DEV__) {
|
|
82
|
+
console.log("[useVideoCaching] Error:", message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}, [sourceUrl]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
loadVideo();
|
|
89
|
+
}, [loadVideo]);
|
|
90
|
+
|
|
91
|
+
const retry = useCallback(() => {
|
|
92
|
+
setError(null);
|
|
93
|
+
loadVideo();
|
|
94
|
+
}, [loadVideo]);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
localUri,
|
|
98
|
+
isDownloading,
|
|
99
|
+
downloadProgress,
|
|
100
|
+
fromCache,
|
|
101
|
+
error,
|
|
102
|
+
retry,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Main hook for video player control with safe operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState, useCallback, useMemo
|
|
6
|
+
import { useState, useCallback, useMemo } from "react";
|
|
7
7
|
import { useVideoPlayer as useExpoVideoPlayer } from "expo-video";
|
|
8
8
|
|
|
9
9
|
import type {
|
|
@@ -58,31 +58,6 @@ export const useVideoPlayerControl = (
|
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
// Listen to player status changes and errors
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
if (!player) return;
|
|
64
|
-
|
|
65
|
-
const subscription = player.addListener("statusChange", ({ status, error }) => {
|
|
66
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
67
|
-
// eslint-disable-next-line no-console
|
|
68
|
-
console.log("[useVideoPlayerControl] Status changed:", status, "Error:", error);
|
|
69
|
-
}
|
|
70
|
-
if (status === "readyToPlay") {
|
|
71
|
-
setIsLoading(false);
|
|
72
|
-
}
|
|
73
|
-
if (error) {
|
|
74
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
75
|
-
// eslint-disable-next-line no-console
|
|
76
|
-
console.error("[useVideoPlayerControl] Player error:", error);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
return () => {
|
|
82
|
-
subscription.remove();
|
|
83
|
-
};
|
|
84
|
-
}, [player]);
|
|
85
|
-
|
|
86
61
|
const isPlayerValid = useMemo(
|
|
87
62
|
() => isPlayerReady(player, source),
|
|
88
63
|
[player, source],
|
|
@@ -72,3 +72,12 @@ export interface VideoPlayerProps {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export type { VideoPlayer } from "expo-video";
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Video caching types
|
|
78
|
+
*/
|
|
79
|
+
export type {
|
|
80
|
+
VideoDownloadProgressCallback,
|
|
81
|
+
VideoCacheResult,
|
|
82
|
+
} from "../infrastructure/services/video-cache.service";
|
|
83
|
+
export type { VideoCachingState } from "../presentation/hooks/useVideoCaching";
|