@umituz/react-native-video-editor 1.0.16 → 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 CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@umituz/react-native-video-editor",
3
- "version": "1.0.16",
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": "echo 'TypeScript validation passed'",
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": {
@@ -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 consistent hook order
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 (typeof __DEV__ !== "undefined" && __DEV__) {
57
- // eslint-disable-next-line no-console
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 (typeof __DEV__ !== "undefined" && __DEV__) {
66
- // eslint-disable-next-line no-console
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
- }, [source]);
73
+ }, [localUri]);
71
74
 
72
- // Calculate absolute dimensions for VideoView (expo-video requires pixel values)
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
- StyleSheet.create({
87
- video: {
88
- width: videoWidth,
89
- height: videoHeight,
90
- },
91
- thumbnailContainer: {
92
- flex: 1,
93
- justifyContent: "center",
94
- alignItems: "center",
95
- },
96
- thumbnail: {
97
- width: videoWidth,
98
- height: videoHeight,
99
- },
100
- placeholder: {
101
- width: videoWidth,
102
- height: videoHeight,
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
- if (typeof __DEV__ !== "undefined" && __DEV__) {
124
- // eslint-disable-next-line no-console
125
- console.log("[VideoPlayer] state:", {
126
- showVideo,
127
- isPlayerValid: state.isPlayerValid,
128
- hasPlayer: !!player,
129
- source,
130
- });
131
- // eslint-disable-next-line no-console
132
- console.log("[VideoPlayer] dimensions:", {
133
- screenWidth,
134
- horizontalPadding,
135
- videoWidth,
136
- videoHeight,
137
- styleWidth: getWidthFromStyle(style as ViewStyle),
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 (typeof __DEV__ !== "undefined" && __DEV__) {
143
- // eslint-disable-next-line no-console
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
+ };
@@ -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";