@umituz/react-native-video-editor 1.0.1 → 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 +6 -1
- package/src/domain/entities/index.ts +5 -3
- package/src/domain/entities/video-project.types.ts +1 -1
- package/src/index.ts +25 -1
- package/src/infrastructure/constants/animation-layer.constants.ts +1 -1
- package/src/infrastructure/services/image-layer-operations.service.ts +1 -1
- package/src/infrastructure/services/layer-manipulation.service.ts +1 -1
- package/src/infrastructure/services/layer-operations/layer-delete.service.ts +1 -1
- package/src/infrastructure/services/layer-operations/layer-duplicate.service.ts +1 -1
- package/src/infrastructure/services/layer-operations/layer-order.service.ts +1 -1
- package/src/infrastructure/services/layer-operations/layer-transform.service.ts +1 -1
- package/src/infrastructure/services/layer-operations.service.ts +1 -1
- package/src/infrastructure/services/scene-operations.service.ts +1 -1
- package/src/infrastructure/services/shape-layer-operations.service.ts +1 -1
- package/src/infrastructure/services/text-layer-operations.service.ts +1 -1
- package/src/player/index.ts +31 -0
- package/src/player/infrastructure/services/player-control.service.ts +95 -0
- package/src/player/presentation/components/VideoPlayer.tsx +135 -0
- package/src/player/presentation/hooks/useVideoPlayerControl.ts +82 -0
- package/src/player/presentation/hooks/useVideoVisibility.ts +62 -0
- package/src/player/types/index.ts +74 -0
- package/src/presentation/components/AnimationEditor.tsx +1 -1
- package/src/presentation/components/AudioEditor.tsx +1 -1
- package/src/presentation/components/DraggableLayer.tsx +1 -1
- package/src/presentation/components/EditorPreviewArea.tsx +1 -1
- package/src/presentation/components/EditorTimeline.tsx +1 -1
- package/src/presentation/components/EditorToolPanel.tsx +1 -1
- package/src/presentation/components/ExportDialog.tsx +1 -1
- package/src/presentation/components/ImageLayerEditor.tsx +1 -1
- package/src/presentation/components/LayerActionsMenu.tsx +1 -1
- package/src/presentation/components/ShapeLayerEditor.tsx +1 -1
- package/src/presentation/components/TextLayerEditor.tsx +1 -1
- package/src/presentation/components/animation-layer/AnimationTypeSelector.tsx +1 -1
- package/src/presentation/components/draggable-layer/LayerContent.tsx +1 -1
- package/src/presentation/components/export/ExportActions.tsx +2 -2
- package/src/presentation/components/export/ExportProgress.tsx +1 -1
- package/src/presentation/components/export/ProjectInfoBox.tsx +1 -1
- package/src/presentation/hooks/useAnimationLayerForm.ts +1 -1
- package/src/presentation/hooks/useAudioLayerForm.ts +1 -1
- package/src/presentation/hooks/useEditorActions.tsx +1 -1
- package/src/presentation/hooks/useEditorHistory.ts +1 -1
- package/src/presentation/hooks/useEditorLayers.ts +1 -1
- package/src/presentation/hooks/useEditorPlayback.ts +1 -1
- package/src/presentation/hooks/useEditorScenes.ts +1 -1
- package/src/presentation/hooks/useExport.ts +34 -19
- package/src/presentation/hooks/useExportActions.tsx +1 -1
- package/src/presentation/hooks/useExportForm.ts +1 -1
- package/src/presentation/hooks/useImageLayerForm.ts +1 -1
- package/src/presentation/hooks/useImageLayerOperations.ts +1 -1
- package/src/presentation/hooks/useLayerActions.tsx +1 -1
- package/src/presentation/hooks/useLayerManipulation.ts +1 -1
- package/src/presentation/hooks/useShapeLayerForm.ts +1 -1
- package/src/presentation/hooks/useTextLayerForm.ts +1 -1
- package/src/presentation/hooks/useTextLayerOperations.ts +1 -1
- package/src/infrastructure/services/export-orchestrator.service.ts +0 -122
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-video-editor",
|
|
3
|
-
"version": "1.0.
|
|
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"
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
export * from "./video-project.types";
|
|
6
6
|
|
|
7
|
+
import type { VideoProject, Scene } from "./video-project.types";
|
|
8
|
+
|
|
7
9
|
export interface EditorState {
|
|
8
|
-
project:
|
|
10
|
+
project: VideoProject | null;
|
|
9
11
|
currentSceneIndex: number;
|
|
10
12
|
selectedLayerId: string | null;
|
|
11
13
|
isPlaying: boolean;
|
|
@@ -14,13 +16,13 @@ export interface EditorState {
|
|
|
14
16
|
|
|
15
17
|
export interface LayerOperationResult {
|
|
16
18
|
success: boolean;
|
|
17
|
-
updatedScenes:
|
|
19
|
+
updatedScenes: Scene[];
|
|
18
20
|
error?: string;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export interface SceneOperationResult {
|
|
22
24
|
success: boolean;
|
|
23
|
-
updatedScenes:
|
|
25
|
+
updatedScenes: Scene[];
|
|
24
26
|
newSceneIndex?: number;
|
|
25
27
|
error?: string;
|
|
26
28
|
}
|
package/src/index.ts
CHANGED
|
@@ -47,7 +47,6 @@ export { textLayerOperationsService } from "./infrastructure/services/text-layer
|
|
|
47
47
|
export { imageLayerOperationsService } from "./infrastructure/services/image-layer-operations.service";
|
|
48
48
|
export { shapeLayerOperationsService } from "./infrastructure/services/shape-layer-operations.service";
|
|
49
49
|
export { layerManipulationService } from "./infrastructure/services/layer-manipulation.service";
|
|
50
|
-
export { exportOrchestratorService } from "./infrastructure/services/export-orchestrator.service";
|
|
51
50
|
|
|
52
51
|
export {
|
|
53
52
|
layerDeleteService,
|
|
@@ -98,3 +97,28 @@ export { useLayerActions } from "./presentation/hooks/useLayerActions";
|
|
|
98
97
|
export { useSceneActions } from "./presentation/hooks/useSceneActions";
|
|
99
98
|
export { useMenuActions } from "./presentation/hooks/useMenuActions";
|
|
100
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";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { generateUUID } from "@umituz/react-native-uuid";
|
|
7
|
-
import type { Scene, ImageLayer } from "
|
|
7
|
+
import type { Scene, ImageLayer } from "../../../domain/entities";
|
|
8
8
|
import type { LayerOperationResult, AddImageLayerData } from "../../types";
|
|
9
9
|
|
|
10
10
|
class ImageLayerOperationsService {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Orchestrator service that delegates to specialized layer operation services
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { Scene, Animation } from "
|
|
6
|
+
import type { Scene, Animation } from "../../../domain/entities";
|
|
7
7
|
import type { LayerOperationResult, LayerOrderAction } from "../../types";
|
|
8
8
|
import {
|
|
9
9
|
layerDeleteService,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { generateUUID } from "@umituz/react-native-uuid";
|
|
7
|
-
import type { Scene } from "
|
|
7
|
+
import type { Scene } from "../../../domain/entities";
|
|
8
8
|
import type { LayerOperationResult } from "../../../types";
|
|
9
9
|
|
|
10
10
|
class LayerDuplicateService {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Single Responsibility: Handle layer ordering operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { Scene } from "
|
|
6
|
+
import type { Scene } from "../../../domain/entities";
|
|
7
7
|
import type { LayerOperationResult, LayerOrderAction } from "../../../types";
|
|
8
8
|
|
|
9
9
|
class LayerOrderService {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Single Responsibility: Handle layer position, size, and animation updates
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { Scene, Animation, Layer } from "
|
|
6
|
+
import type { Scene, Animation, Layer } from "../../../domain/entities";
|
|
7
7
|
import type { LayerOperationResult } from "../../../types";
|
|
8
8
|
|
|
9
9
|
class LayerTransformService {
|
|
@@ -7,7 +7,7 @@ import { textLayerOperationsService } from "./text-layer-operations.service";
|
|
|
7
7
|
import { imageLayerOperationsService } from "./image-layer-operations.service";
|
|
8
8
|
import { shapeLayerOperationsService } from "./shape-layer-operations.service";
|
|
9
9
|
import { layerManipulationService } from "./layer-manipulation.service";
|
|
10
|
-
import type { Scene, TextLayer, ImageLayer, Animation } from "
|
|
10
|
+
import type { Scene, TextLayer, ImageLayer, Animation } from "../../../domain/entities";
|
|
11
11
|
import type {
|
|
12
12
|
LayerOperationResult,
|
|
13
13
|
LayerOrderAction,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { generateUUID } from "@umituz/react-native-uuid";
|
|
7
|
-
import type { Scene, Audio } from "
|
|
7
|
+
import type { Scene, Audio } from "../../../domain/entities";
|
|
8
8
|
import type { SceneOperationResult } from "../../types";
|
|
9
9
|
|
|
10
10
|
class SceneOperationsService {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { generateUUID } from "@umituz/react-native-uuid";
|
|
7
|
-
import type { Scene, ShapeLayer } from "
|
|
7
|
+
import type { Scene, ShapeLayer } from "../../../domain/entities";
|
|
8
8
|
import type { LayerOperationResult, AddShapeLayerData } from "../../types";
|
|
9
9
|
|
|
10
10
|
class ShapeLayerOperationsService {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { generateUUID } from "@umituz/react-native-uuid";
|
|
7
|
-
import type { Scene, TextLayer } from "
|
|
7
|
+
import type { Scene, TextLayer } from "../../../domain/entities";
|
|
8
8
|
import type { LayerOperationResult, AddTextLayerData } from "../../types";
|
|
9
9
|
|
|
10
10
|
class TextLayerOperationsService {
|
|
@@ -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
|
+
};
|