@umituz/react-native-video-editor 1.0.2 → 1.0.3

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.0.2",
3
+ "version": "1.0.3",
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",
@@ -29,10 +29,15 @@
29
29
  "react": ">=18.2.0",
30
30
  "react-native": ">=0.74.0",
31
31
  "@umituz/react-native-design-system": ">=1.0.0",
32
+ "expo-image": ">=1.0.0",
33
+ "expo-video": ">=3.0.0",
32
34
  "zustand": ">=4.0.0"
33
35
  },
34
36
  "devDependencies": {
35
37
  "@types/react": "~19.1.10",
38
+ "@umituz/react-native-design-system": "latest",
39
+ "expo-image": "^3.0.11",
40
+ "expo-video": "^3.0.15",
36
41
  "react": "19.1.0",
37
42
  "react-native": "0.81.5",
38
43
  "typescript": "~5.9.2"
package/src/index.ts CHANGED
@@ -97,3 +97,28 @@ export { useLayerActions } from "./presentation/hooks/useLayerActions";
97
97
  export { useSceneActions } from "./presentation/hooks/useSceneActions";
98
98
  export { useMenuActions } from "./presentation/hooks/useMenuActions";
99
99
  export { useExportActions } from "./presentation/hooks/useExportActions";
100
+
101
+ // =============================================================================
102
+ // VIDEO PLAYER MODULE
103
+ // =============================================================================
104
+
105
+ export type {
106
+ VideoPlayerConfig,
107
+ VideoPlayerState,
108
+ VideoPlayerControls,
109
+ UseVideoPlayerControlResult,
110
+ VideoVisibilityConfig,
111
+ VideoPlayerProps,
112
+ VideoPlayerType,
113
+ } from "./player";
114
+
115
+ export {
116
+ safePlay,
117
+ safePause,
118
+ safeToggle,
119
+ isPlayerReady,
120
+ configurePlayer,
121
+ useVideoPlayerControl,
122
+ useVideoVisibility,
123
+ VideoPlayer,
124
+ } from "./player";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Video Player Module
3
+ * Exports for video playback functionality
4
+ */
5
+
6
+ // Types
7
+ export type {
8
+ VideoPlayerConfig,
9
+ VideoPlayerState,
10
+ VideoPlayerControls,
11
+ UseVideoPlayerControlResult,
12
+ VideoVisibilityConfig,
13
+ VideoPlayerProps,
14
+ VideoPlayer as VideoPlayerType,
15
+ } from "./types";
16
+
17
+ // Services
18
+ export {
19
+ safePlay,
20
+ safePause,
21
+ safeToggle,
22
+ isPlayerReady,
23
+ configurePlayer,
24
+ } from "./infrastructure/services/player-control.service";
25
+
26
+ // Hooks
27
+ export { useVideoPlayerControl } from "./presentation/hooks/useVideoPlayerControl";
28
+ export { useVideoVisibility } from "./presentation/hooks/useVideoVisibility";
29
+
30
+ // Components
31
+ export { VideoPlayer } from "./presentation/components/VideoPlayer";
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Player Control Service
3
+ * Safe operations for video player control
4
+ */
5
+
6
+ import type { VideoPlayer } from "expo-video";
7
+
8
+ declare const __DEV__: boolean;
9
+
10
+ /**
11
+ * Safely play video with error handling
12
+ */
13
+ export const safePlay = (player: VideoPlayer | null): boolean => {
14
+ if (!player) return false;
15
+
16
+ try {
17
+ player.play();
18
+ return true;
19
+ } catch (error) {
20
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
21
+ // eslint-disable-next-line no-console
22
+ console.log("[VideoPlayer] Play error ignored:", error);
23
+ }
24
+ return false;
25
+ }
26
+ };
27
+
28
+ /**
29
+ * Safely pause video with error handling
30
+ */
31
+ export const safePause = (player: VideoPlayer | null): boolean => {
32
+ if (!player) return false;
33
+
34
+ try {
35
+ player.pause();
36
+ return true;
37
+ } catch (error) {
38
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
39
+ // eslint-disable-next-line no-console
40
+ console.log("[VideoPlayer] Pause error ignored:", error);
41
+ }
42
+ return false;
43
+ }
44
+ };
45
+
46
+ /**
47
+ * Safely toggle play/pause state
48
+ */
49
+ export const safeToggle = (
50
+ player: VideoPlayer | null,
51
+ isPlaying: boolean,
52
+ ): boolean => {
53
+ if (!player) return false;
54
+
55
+ return isPlaying ? safePause(player) : safePlay(player);
56
+ };
57
+
58
+ /**
59
+ * Check if player has valid native object
60
+ */
61
+ export const isPlayerReady = (
62
+ player: VideoPlayer | null,
63
+ source: string | null,
64
+ ): boolean => {
65
+ return Boolean(player && source && source.length > 0);
66
+ };
67
+
68
+ /**
69
+ * Configure player with initial settings
70
+ */
71
+ export const configurePlayer = (
72
+ player: VideoPlayer,
73
+ options: {
74
+ loop?: boolean;
75
+ muted?: boolean;
76
+ autoPlay?: boolean;
77
+ },
78
+ ): void => {
79
+ try {
80
+ if (options.loop !== undefined) {
81
+ player.loop = options.loop;
82
+ }
83
+ if (options.muted !== undefined) {
84
+ player.muted = options.muted;
85
+ }
86
+ if (options.autoPlay) {
87
+ player.play();
88
+ }
89
+ } catch (error) {
90
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
91
+ // eslint-disable-next-line no-console
92
+ console.log("[VideoPlayer] Configure error ignored:", error);
93
+ }
94
+ }
95
+ };
@@ -0,0 +1,135 @@
1
+ /**
2
+ * VideoPlayer Component
3
+ * Reusable video player with thumbnail and controls
4
+ */
5
+
6
+ import React, { useState, useCallback, useMemo } from "react";
7
+ import {
8
+ View,
9
+ TouchableOpacity,
10
+ StyleSheet,
11
+ useWindowDimensions,
12
+ } from "react-native";
13
+ import { Image } from "expo-image";
14
+ import { VideoView } from "expo-video";
15
+ import { useAppDesignTokens, AtomicIcon } from "@umituz/react-native-design-system";
16
+
17
+ import type { VideoPlayerProps } from "../../types";
18
+ import { useVideoPlayerControl } from "../hooks/useVideoPlayerControl";
19
+
20
+ const ASPECT_RATIO = 16 / 9;
21
+
22
+ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
23
+ source,
24
+ thumbnailUrl,
25
+ loop = true,
26
+ muted = false,
27
+ autoPlay = false,
28
+ nativeControls = true,
29
+ contentFit = "cover",
30
+ style,
31
+ }) => {
32
+ const tokens = useAppDesignTokens();
33
+ const { width } = useWindowDimensions();
34
+ const [showVideo, setShowVideo] = useState(autoPlay);
35
+
36
+ const { player, state } = useVideoPlayerControl({
37
+ source: showVideo ? source : null,
38
+ loop,
39
+ muted,
40
+ autoPlay,
41
+ });
42
+
43
+ const handlePlay = useCallback(() => {
44
+ setShowVideo(true);
45
+ // Player will auto-configure with autoPlay from hook
46
+ }, []);
47
+
48
+ const containerStyle = useMemo(() => ({
49
+ width: "100%" as const,
50
+ aspectRatio: ASPECT_RATIO,
51
+ backgroundColor: tokens.colors.surface,
52
+ borderRadius: 16,
53
+ overflow: "hidden" as const,
54
+ }), [tokens.colors.surface]);
55
+
56
+ const styles = useMemo(
57
+ () =>
58
+ StyleSheet.create({
59
+ video: {
60
+ width: "100%",
61
+ height: "100%",
62
+ },
63
+ thumbnailContainer: {
64
+ width: "100%",
65
+ height: "100%",
66
+ justifyContent: "center",
67
+ alignItems: "center",
68
+ },
69
+ thumbnail: {
70
+ width: "100%",
71
+ height: "100%",
72
+ },
73
+ placeholder: {
74
+ width: "100%",
75
+ height: "100%",
76
+ backgroundColor: tokens.colors.surfaceSecondary,
77
+ },
78
+ playButtonContainer: {
79
+ ...StyleSheet.absoluteFillObject,
80
+ justifyContent: "center",
81
+ alignItems: "center",
82
+ },
83
+ playButton: {
84
+ width: 64,
85
+ height: 64,
86
+ borderRadius: 32,
87
+ backgroundColor: tokens.colors.primary,
88
+ justifyContent: "center",
89
+ alignItems: "center",
90
+ paddingLeft: 4,
91
+ },
92
+ }),
93
+ [tokens]
94
+ );
95
+
96
+ // Show video player when playing
97
+ if (showVideo && state.isPlayerValid && player) {
98
+ return (
99
+ <View style={[containerStyle, style]}>
100
+ <VideoView
101
+ player={player}
102
+ style={styles.video}
103
+ contentFit={contentFit}
104
+ nativeControls={nativeControls}
105
+ />
106
+ </View>
107
+ );
108
+ }
109
+
110
+ // Show thumbnail with play button
111
+ return (
112
+ <TouchableOpacity
113
+ style={[containerStyle, style]}
114
+ onPress={handlePlay}
115
+ activeOpacity={0.8}
116
+ >
117
+ <View style={styles.thumbnailContainer}>
118
+ {thumbnailUrl ? (
119
+ <Image
120
+ source={{ uri: thumbnailUrl }}
121
+ style={styles.thumbnail}
122
+ contentFit="cover"
123
+ />
124
+ ) : (
125
+ <View style={styles.placeholder} />
126
+ )}
127
+ <View style={styles.playButtonContainer}>
128
+ <View style={styles.playButton}>
129
+ <AtomicIcon name="play" customSize={32} color="onPrimary" />
130
+ </View>
131
+ </View>
132
+ </View>
133
+ </TouchableOpacity>
134
+ );
135
+ };
@@ -0,0 +1,82 @@
1
+ /**
2
+ * useVideoPlayerControl Hook
3
+ * Main hook for video player control with safe operations
4
+ */
5
+
6
+ import { useState, useCallback, useMemo } from "react";
7
+ import { useVideoPlayer as useExpoVideoPlayer } from "expo-video";
8
+
9
+ import type {
10
+ VideoPlayerConfig,
11
+ VideoPlayerState,
12
+ VideoPlayerControls,
13
+ UseVideoPlayerControlResult,
14
+ } from "../../types";
15
+ import {
16
+ safePlay,
17
+ safePause,
18
+ safeToggle,
19
+ isPlayerReady,
20
+ configurePlayer,
21
+ } from "../../infrastructure/services/player-control.service";
22
+
23
+ /**
24
+ * Hook for managing video player with safe operations
25
+ */
26
+ export const useVideoPlayerControl = (
27
+ config: VideoPlayerConfig,
28
+ ): UseVideoPlayerControlResult => {
29
+ const { source, loop = true, muted = false, autoPlay = false } = config;
30
+
31
+ const [isPlaying, setIsPlaying] = useState(false);
32
+ const [isLoading, setIsLoading] = useState(true);
33
+
34
+ const player = useExpoVideoPlayer(source ?? "", (p) => {
35
+ if (source && p) {
36
+ configurePlayer(p, { loop, muted, autoPlay });
37
+ setIsLoading(false);
38
+ if (autoPlay) {
39
+ setIsPlaying(true);
40
+ }
41
+ }
42
+ });
43
+
44
+ const isPlayerValid = useMemo(
45
+ () => isPlayerReady(player, source),
46
+ [player, source],
47
+ );
48
+
49
+ const play = useCallback(() => {
50
+ if (!isPlayerValid) return;
51
+ const success = safePlay(player);
52
+ if (success) setIsPlaying(true);
53
+ }, [player, isPlayerValid]);
54
+
55
+ const pause = useCallback(() => {
56
+ if (!isPlayerValid) return;
57
+ const success = safePause(player);
58
+ if (success) setIsPlaying(false);
59
+ }, [player, isPlayerValid]);
60
+
61
+ const toggle = useCallback(() => {
62
+ if (!isPlayerValid) return;
63
+ const success = safeToggle(player, isPlaying);
64
+ if (success) setIsPlaying(!isPlaying);
65
+ }, [player, isPlayerValid, isPlaying]);
66
+
67
+ const state: VideoPlayerState = useMemo(
68
+ () => ({
69
+ isPlaying,
70
+ isPlayerValid,
71
+ isLoading: isLoading && Boolean(source),
72
+ }),
73
+ [isPlaying, isPlayerValid, isLoading, source],
74
+ );
75
+
76
+ const controls: VideoPlayerControls = useMemo(
77
+ () => ({ play, pause, toggle }),
78
+ [play, pause, toggle],
79
+ );
80
+
81
+ return { player, state, controls };
82
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * useVideoVisibility Hook
3
+ * Handles auto play/pause based on visibility
4
+ */
5
+
6
+ import { useState, useCallback, useEffect } from "react";
7
+
8
+ import type { VideoVisibilityConfig } from "../../types";
9
+ import {
10
+ safePlay,
11
+ safePause,
12
+ } from "../../infrastructure/services/player-control.service";
13
+
14
+ /**
15
+ * Optional navigation focus hook type
16
+ */
17
+ type UseFocusEffectType = (callback: () => (() => void) | undefined) => void;
18
+
19
+ /**
20
+ * Hook for managing video visibility-based playback
21
+ * @param config - Visibility configuration
22
+ * @param useFocusEffect - Optional navigation focus hook (from @react-navigation/native)
23
+ */
24
+ export const useVideoVisibility = (
25
+ config: VideoVisibilityConfig,
26
+ useFocusEffect?: UseFocusEffectType,
27
+ ): void => {
28
+ const { isVisible, player, isPlayerValid, onPlayingChange } = config;
29
+ const [isScreenFocused, setIsScreenFocused] = useState(true);
30
+
31
+ // Handle screen focus if navigation hook provided
32
+ useEffect(() => {
33
+ if (!useFocusEffect) return;
34
+
35
+ useFocusEffect(
36
+ useCallback(() => {
37
+ setIsScreenFocused(true);
38
+
39
+ return () => {
40
+ setIsScreenFocused(false);
41
+ if (isPlayerValid) {
42
+ safePause(player);
43
+ onPlayingChange?.(false);
44
+ }
45
+ };
46
+ }, [isPlayerValid, player, onPlayingChange]),
47
+ );
48
+ }, [useFocusEffect, isPlayerValid, player, onPlayingChange]);
49
+
50
+ // Handle visibility changes
51
+ useEffect(() => {
52
+ if (!isPlayerValid) return;
53
+
54
+ if (isVisible && isScreenFocused) {
55
+ const success = safePlay(player);
56
+ if (success) onPlayingChange?.(true);
57
+ } else {
58
+ const success = safePause(player);
59
+ if (success) onPlayingChange?.(false);
60
+ }
61
+ }, [isVisible, isScreenFocused, isPlayerValid, player, onPlayingChange]);
62
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Video Player Types
3
+ * Core type definitions for video playback functionality
4
+ */
5
+
6
+ import type { VideoPlayer } from "expo-video";
7
+ import type { ViewStyle } from "react-native";
8
+
9
+ /**
10
+ * Configuration for video player initialization
11
+ */
12
+ export interface VideoPlayerConfig {
13
+ readonly source: string | null;
14
+ readonly loop?: boolean;
15
+ readonly muted?: boolean;
16
+ readonly autoPlay?: boolean;
17
+ }
18
+
19
+ /**
20
+ * Current state of the video player
21
+ */
22
+ export interface VideoPlayerState {
23
+ readonly isPlaying: boolean;
24
+ readonly isPlayerValid: boolean;
25
+ readonly isLoading: boolean;
26
+ }
27
+
28
+ /**
29
+ * Video player control actions
30
+ */
31
+ export interface VideoPlayerControls {
32
+ readonly play: () => void;
33
+ readonly pause: () => void;
34
+ readonly toggle: () => void;
35
+ }
36
+
37
+ /**
38
+ * Combined hook return type
39
+ */
40
+ export interface UseVideoPlayerControlResult {
41
+ readonly player: VideoPlayer | null;
42
+ readonly state: VideoPlayerState;
43
+ readonly controls: VideoPlayerControls;
44
+ }
45
+
46
+ /**
47
+ * Visibility hook configuration
48
+ */
49
+ export interface VideoVisibilityConfig {
50
+ readonly isVisible: boolean;
51
+ readonly player: VideoPlayer | null;
52
+ readonly isPlayerValid: boolean;
53
+ readonly onPlayingChange?: (isPlaying: boolean) => void;
54
+ }
55
+
56
+ /**
57
+ * Video player component props
58
+ */
59
+ export interface VideoPlayerProps {
60
+ readonly source: string | null;
61
+ readonly isVisible?: boolean;
62
+ readonly loop?: boolean;
63
+ readonly muted?: boolean;
64
+ readonly autoPlay?: boolean;
65
+ readonly showControls?: boolean;
66
+ readonly nativeControls?: boolean;
67
+ readonly onPlayingChange?: (isPlaying: boolean) => void;
68
+ readonly onError?: (error: Error) => void;
69
+ readonly style?: ViewStyle;
70
+ readonly contentFit?: "contain" | "cover" | "fill";
71
+ readonly thumbnailUrl?: string;
72
+ }
73
+
74
+ export type { VideoPlayer } from "expo-video";
@@ -8,11 +8,11 @@ import {
8
8
  View,
9
9
  StyleSheet,
10
10
  TouchableOpacity,
11
- ActivityIndicator,
12
11
  } from "react-native";
13
12
  import {
14
13
  AtomicText,
15
14
  AtomicIcon,
15
+ AtomicSpinner,
16
16
  useAppDesignTokens,
17
17
  } from "@umituz/react-native-design-system";
18
18
 
@@ -64,7 +64,7 @@ export const ExportActions: React.FC<ExportActionsProps> = ({
64
64
  disabled={isExporting}
65
65
  >
66
66
  {isExporting ? (
67
- <ActivityIndicator color="#FFFFFF" size="small" />
67
+ <AtomicSpinner size="sm" color="white" />
68
68
  ) : (
69
69
  <AtomicIcon name="Download" size="sm" color="onSurface" />
70
70
  )}