import React, { useRef, useMemo, useState } from 'react'; import { View, Text, StyleSheet, Dimensions, Pressable, Platform, // @ts-ignore - Peer dependency } from 'react-native'; // @ts-ignore - Peer dependency import Video, { type OnLoadData, type OnProgressData, // @ts-ignore - Peer dependency } from 'react-native-video'; // @ts-ignore - Peer dependency import Animated, { useAnimatedStyle, interpolate, // @ts-ignore - Peer dependency } from 'react-native-reanimated'; // @ts-ignore - Peer dependency import { ScaledSheet } from 'react-native-size-matters'; import { useEditorState } from '../../context/EditorStateContext'; // @ts-ignore - Peer dependency import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { PREVIEW_WIDTH, PREVIEW_HEIGHT, SMALL_PREVIEW_WIDTH, SMALL_PREVIEW_HEIGHT, } from '../../constants/dimensions'; import { DraggableText } from './DraggableText'; import type { TextSegment } from '../../types/segments'; import { deviceUtils } from '../../utils/deviceUtils'; // @ts-ignore - Peer dependency import { Pressable as GHPressable } from 'react-native-gesture-handler'; type Props = { source: string; layoutAnimation: Animated.SharedValue; textSegments?: TextSegment[]; }; const SCREEN_WIDTH = Dimensions.get('window').width; const RNPressable = deviceUtils.isIOS ? Pressable : GHPressable; export const PreviewArea: React.FC = ({ source, layoutAnimation, textSegments = [], }) => { const videoRef = useRef(null); const voiceoverRefs = useRef<{ [key: string]: any }>({}); const { setCurrentTime, setDuration, setOriginalDuration, getTrim, getPlaybackState, isScrubbing, cropRatio, videoNaturalSize, setVideoNaturalSize, isPlaying, isDraggingHandle, setVideoRef, setIsPlaying, isMuted, audioSegments, voiceoverSegments, setEditingTextElement, setIsTextEditorVisible, setIsTextDragging, updateTextSegment, removeTextSegment, setIsTextPinching, } = useEditorState(); // Register video ref with context React.useEffect(() => { setVideoRef(videoRef); return () => setVideoRef(null); }, [setVideoRef]); const { currentTime } = getPlaybackState(); const { top: safeAreaTop } = useSafeAreaInsets(); const [mainVideoVolume, setMainVideoVolume] = useState(1.0); const aspectRatios = useMemo(() => { const originalRatio = videoNaturalSize?.width && videoNaturalSize?.height ? videoNaturalSize.width / videoNaturalSize.height : 9 / 16; return { 'original': originalRatio, '9:16': 9 / 16, '1:1': 1, '16:9': 16 / 9, }; }, [videoNaturalSize]); // Dynamic video style based on aspect ratio const dynamicVideoStyle = useMemo(() => { const videoAspectRatio = aspectRatios[cropRatio as keyof typeof aspectRatios]; if (!videoAspectRatio) { return { height: '100%' }; } if (videoAspectRatio < 1) { return { height: '100%', width: undefined }; } else { return { width: '100%', height: undefined }; } }, [cropRatio, aspectRatios]); React.useEffect(() => { if ((isScrubbing || isDraggingHandle) && videoRef.current) { videoRef.current.seek(currentTime); } }, [currentTime, isScrubbing, isDraggingHandle]); // Force video refresh when audio segments are removed to stop background music const audioSegmentsLengthRef = React.useRef(audioSegments?.length || 0); React.useEffect(() => { const currentLength = audioSegments?.length || 0; const previousLength = audioSegmentsLengthRef.current; // If audio segments were removed (length decreased), force video refresh if ( previousLength > 0 && currentLength < previousLength && videoRef.current && !isScrubbing && !isDraggingHandle ) { const { currentTime: videoTime, duration: videoDuration } = getPlaybackState(); if (videoTime >= 0 && videoDuration > 0) { // Pause, seek, and resume to force native module to update and stop audio const wasPlaying = isPlaying; if (wasPlaying) { setIsPlaying(false); } setTimeout(() => { if (videoRef.current) { const seekTime = Math.max(0, Math.min(videoTime, videoDuration)); // Seek to force native module to update and stop audio videoRef.current.seek(seekTime); // Resume playback if it was playing before if (wasPlaying) { setTimeout(() => { setIsPlaying(true); }, 100); } } }, 100); } } audioSegmentsLengthRef.current = currentLength; }, [ audioSegments?.length, isScrubbing, isDraggingHandle, isPlaying, setIsPlaying, getPlaybackState, ]); // Sync voiceover players when currentTime changes (e.g., during scrubbing) const prevCurrentTimeRef = React.useRef(currentTime); React.useEffect(() => { const timeDiff = Math.abs(currentTime - prevCurrentTimeRef.current); // If time jumped significantly (user seeked), update voiceover positions if (timeDiff > 0.5) { voiceoverSegments?.forEach((seg: any) => { const isActive = currentTime >= seg.start && currentTime < seg.end; const player = voiceoverRefs.current[seg.id]; if (player && isActive) { const relativeTime = currentTime - seg.start; player.seek(Math.max(0, relativeTime)); } }); } prevCurrentTimeRef.current = currentTime; }, [currentTime, voiceoverSegments]); const videoContainerAnimatedStyle = useAnimatedStyle(() => { const width = interpolate( layoutAnimation.value, [0, 1], [PREVIEW_WIDTH, SMALL_PREVIEW_WIDTH] ); const height = interpolate( layoutAnimation.value, [0, 1], [PREVIEW_HEIGHT, SMALL_PREVIEW_HEIGHT] ); const borderRadius = interpolate(layoutAnimation.value, [0, 1], [0, 12]); const top = Platform.OS === 'ios' ? safeAreaTop : 0; const left = interpolate( layoutAnimation.value, [0, 1], [0, (SCREEN_WIDTH - SMALL_PREVIEW_WIDTH) / 2] ); const borderWidth = interpolate(layoutAnimation.value, [0.8, 1], [0, 1]); return { width, height, borderRadius, position: 'absolute', top, left, borderWidth, borderColor: 'rgba(255, 255, 255, 0.3)', }; }); const handleVideoPress = () => { setIsPlaying(!isPlaying); }; const videoSourceProp = useMemo(() => ({ uri: source }), [source]); return ( {/* Hidden voiceover audio players */} {voiceoverSegments?.map((seg: any) => { const isActive = currentTime >= seg.start && currentTime < seg.end; const relativeTime = currentTime - seg.start; return ( ); }; const styles = ScaledSheet.create({ videoContainer: { alignItems: 'center', overflow: 'hidden', justifyContent: 'center', backgroundColor: '#000', }, videoPreview: { backgroundColor: '#000', }, overlay: { ...StyleSheet.absoluteFillObject, justifyContent: 'center', alignItems: 'center', zIndex: 10, }, playButton: { width: '60@ms', height: '60@ms', borderRadius: '30@ms', backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center', }, playIcon: { color: '#fff', fontSize: '24@ms', marginLeft: '4@ms', }, hiddenVoiceover: { width: 0, height: 0, position: 'absolute', }, });