import React, { useState, useEffect, useRef, useCallback, memo } from 'react'; import { View, StyleSheet, TouchableOpacity, Dimensions, Platform, ActivityIndicator, AppState, AppStateStatus, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import Video, { VideoRef, OnLoadData, OnProgressData } from 'react-native-video'; import { LinearGradient } from 'expo-linear-gradient'; import Animated, { useAnimatedStyle, useSharedValue, withTiming, withDelay, runOnJS, } from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; import { useTrailer } from '../../contexts/TrailerContext'; import { logger } from '../../utils/logger'; const { width, height } = Dimensions.get('window'); const isTablet = width >= 768; interface TrailerPlayerProps { trailerUrl: string; autoPlay?: boolean; muted?: boolean; onLoadStart?: () => void; onLoad?: () => void; onError?: (error: string) => void; onProgress?: (data: OnProgressData) => void; onPlaybackStatusUpdate?: (status: { isLoaded: boolean; didJustFinish: boolean }) => void; onEnd?: () => void; style?: any; hideLoadingSpinner?: boolean; onFullscreenToggle?: () => void; hideControls?: boolean; contentType?: 'movie' | 'series'; paused?: boolean; // External control to pause/play } const TrailerPlayer = React.forwardRef(({ trailerUrl, autoPlay = true, muted = true, onLoadStart, onLoad, onError, onProgress, onPlaybackStatusUpdate, onEnd, style, hideLoadingSpinner = false, onFullscreenToggle, hideControls = false, contentType = 'movie', paused, }, ref) => { const { currentTheme } = useTheme(); const { isTrailerPlaying: globalTrailerPlaying } = useTrailer(); const videoRef = useRef(null); const [isLoading, setIsLoading] = useState(true); const [isPlaying, setIsPlaying] = useState(autoPlay); const [isMuted, setIsMuted] = useState(muted); const [hasError, setHasError] = useState(false); const [showControls, setShowControls] = useState(false); const [duration, setDuration] = useState(0); const [position, setPosition] = useState(0); const [isFullscreen, setIsFullscreen] = useState(false); const [isComponentMounted, setIsComponentMounted] = useState(true); // Animated values const controlsOpacity = useSharedValue(0); const loadingOpacity = useSharedValue(1); const playButtonScale = useSharedValue(1); // Auto-hide controls after 3 seconds const hideControlsTimeout = useRef(null); const appState = useRef(AppState.currentState); // Cleanup function to stop video and reset state const cleanupVideo = useCallback(() => { try { if (videoRef.current) { // Pause the video setIsPlaying(false); // Seek to beginning to stop any background processing videoRef.current.seek(0); // Clear any pending timeouts if (hideControlsTimeout.current) { clearTimeout(hideControlsTimeout.current); hideControlsTimeout.current = null; } logger.info('TrailerPlayer', 'Video cleanup completed'); } } catch (error) { logger.error('TrailerPlayer', 'Error during video cleanup:', error); } }, []); // Handle app state changes to pause video when app goes to background useEffect(() => { const handleAppStateChange = (nextAppState: AppStateStatus) => { if (appState.current === 'active' && nextAppState.match(/inactive|background/)) { // App going to background - pause video logger.info('TrailerPlayer', 'App going to background - pausing video'); setIsPlaying(false); } else if (appState.current.match(/inactive|background/) && nextAppState === 'active') { // App coming to foreground - resume if it was playing and autoPlay is enabled logger.info('TrailerPlayer', 'App coming to foreground'); // Only resume if autoPlay is true and component is still mounted // Add a small delay to ensure the app is fully active if (autoPlay && isComponentMounted) { setTimeout(() => { if (isComponentMounted) { logger.info('TrailerPlayer', 'Resuming video after app foreground'); setIsPlaying(true); } }, 200); } } appState.current = nextAppState; }; const subscription = AppState.addEventListener('change', handleAppStateChange); return () => subscription?.remove(); }, [autoPlay, isComponentMounted]); // Component mount/unmount tracking useEffect(() => { setIsComponentMounted(true); return () => { setIsComponentMounted(false); cleanupVideo(); }; }, [cleanupVideo]); // Handle autoPlay prop changes to keep internal state synchronized // But only if no external paused prop is provided useEffect(() => { if (isComponentMounted && paused === undefined) { setIsPlaying(autoPlay); } }, [autoPlay, isComponentMounted, paused]); // Handle muted prop changes to keep internal state synchronized useEffect(() => { if (isComponentMounted) { setIsMuted(muted); } }, [muted, isComponentMounted]); // Handle external paused prop to override playing state (highest priority) useEffect(() => { if (paused !== undefined) { setIsPlaying(!paused); logger.info('TrailerPlayer', `External paused prop changed: ${paused}, setting isPlaying to ${!paused}`); } }, [paused]); // Respond to global trailer state changes (e.g., when modal opens) // Only apply if no external paused prop is controlling this useEffect(() => { if (isComponentMounted && paused === undefined) { // Always sync with global trailer state when pausing // This ensures all trailers pause when one screen loses focus if (!globalTrailerPlaying) { logger.info('TrailerPlayer', 'Global trailer paused - pausing this trailer'); setIsPlaying(false); } // Don't automatically resume from global state // Each trailer should manage its own resume logic based on its screen focus } }, [globalTrailerPlaying, isComponentMounted, paused]); const showControlsWithTimeout = useCallback(() => { if (!isComponentMounted) return; setShowControls(true); controlsOpacity.value = withTiming(1, { duration: 200 }); // Clear existing timeout if (hideControlsTimeout.current) { clearTimeout(hideControlsTimeout.current); } // Set new timeout to hide controls hideControlsTimeout.current = setTimeout(() => { if (isComponentMounted) { setShowControls(false); controlsOpacity.value = withTiming(0, { duration: 200 }); } }, 3000); }, [controlsOpacity, isComponentMounted]); const handleVideoPress = useCallback(() => { if (!isComponentMounted) return; if (showControls) { // If controls are visible, toggle play/pause handlePlayPause(); } else { // If controls are hidden, show them showControlsWithTimeout(); } }, [showControls, showControlsWithTimeout, isComponentMounted]); const handlePlayPause = useCallback(async () => { try { if (!videoRef.current || !isComponentMounted) return; playButtonScale.value = withTiming(0.8, { duration: 100 }, () => { if (isComponentMounted) { playButtonScale.value = withTiming(1, { duration: 100 }); } }); setIsPlaying(!isPlaying); showControlsWithTimeout(); } catch (error) { logger.error('TrailerPlayer', 'Error toggling playback:', error); } }, [isPlaying, playButtonScale, showControlsWithTimeout, isComponentMounted]); const handleMuteToggle = useCallback(async () => { try { if (!videoRef.current || !isComponentMounted) return; setIsMuted(!isMuted); showControlsWithTimeout(); } catch (error) { logger.error('TrailerPlayer', 'Error toggling mute:', error); } }, [isMuted, showControlsWithTimeout, isComponentMounted]); const handleLoadStart = useCallback(() => { if (!isComponentMounted) return; setIsLoading(true); setHasError(false); // Only show loading spinner if not hidden loadingOpacity.value = hideLoadingSpinner ? 0 : 1; onLoadStart?.(); logger.info('TrailerPlayer', 'Video load started'); }, [loadingOpacity, onLoadStart, hideLoadingSpinner, isComponentMounted]); const handleLoad = useCallback((data: OnLoadData) => { if (!isComponentMounted) return; setIsLoading(false); loadingOpacity.value = withTiming(0, { duration: 300 }); setDuration(data.duration * 1000); // Convert to milliseconds onLoad?.(); logger.info('TrailerPlayer', 'Video loaded successfully'); }, [loadingOpacity, onLoad, isComponentMounted]); const handleError = useCallback((error: any) => { if (!isComponentMounted) return; setIsLoading(false); setHasError(true); loadingOpacity.value = withTiming(0, { duration: 300 }); const message = typeof error === 'string' ? error : (error?.errorString || error?.error?.string || error?.error?.message || JSON.stringify(error)); onError?.(message); logger.error('TrailerPlayer', 'Video error details:', error); }, [loadingOpacity, onError, isComponentMounted]); const handleProgress = useCallback((data: OnProgressData) => { if (!isComponentMounted) return; setPosition(data.currentTime * 1000); // Convert to milliseconds onProgress?.(data); if (onPlaybackStatusUpdate) { onPlaybackStatusUpdate({ isLoaded: data.currentTime > 0, didJustFinish: false }); } }, [onProgress, onPlaybackStatusUpdate, isComponentMounted]); // Sync internal muted state with prop useEffect(() => { if (isComponentMounted) { setIsMuted(muted); } }, [muted, isComponentMounted]); // Cleanup timeout and animated values on unmount useEffect(() => { return () => { if (hideControlsTimeout.current) { clearTimeout(hideControlsTimeout.current); hideControlsTimeout.current = null; } // Reset all animated values to prevent memory leaks try { controlsOpacity.value = 0; loadingOpacity.value = 0; playButtonScale.value = 1; } catch (error) { logger.error('TrailerPlayer', 'Error cleaning up animation values:', error); } // Ensure video is stopped cleanupVideo(); }; }, [controlsOpacity, loadingOpacity, playButtonScale, cleanupVideo]); // Forward the ref to the video element React.useImperativeHandle(ref, () => ({ presentFullscreenPlayer: () => { if (videoRef.current && isComponentMounted) { return videoRef.current.presentFullscreenPlayer(); } }, dismissFullscreenPlayer: () => { if (videoRef.current && isComponentMounted) { return videoRef.current.dismissFullscreenPlayer(); } } })); // Animated styles const controlsAnimatedStyle = useAnimatedStyle(() => ({ opacity: controlsOpacity.value, })); const loadingAnimatedStyle = useAnimatedStyle(() => ({ opacity: loadingOpacity.value, })); const playButtonAnimatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: playButtonScale.value }], })); const progressPercentage = duration > 0 ? (position / duration) * 100 : 0; if (hasError) { return ( ); } return ( ); }); const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#000', overflow: 'hidden', }, video: { flex: 1, width: '100%', height: '100%', }, movieVideoScale: { transform: [{ scale: 1.30 }], // Custom scale for movies to crop black bars }, videoOverlay: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, }, loadingContainer: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.5)', }, errorContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#000', }, controlsContainer: { flex: 1, justifyContent: 'space-between', }, topGradient: { height: 100, width: '100%', }, centerControls: { flex: 1, justifyContent: 'center', alignItems: 'center', }, playButton: { width: isTablet ? 100 : 80, height: isTablet ? 100 : 80, borderRadius: isTablet ? 50 : 40, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center', borderWidth: 2, borderColor: 'rgba(255,255,255,0.8)', }, bottomGradient: { paddingBottom: Platform.OS === 'ios' ? 20 : 16, paddingTop: 20, }, bottomControls: { paddingHorizontal: isTablet ? 32 : 16, }, progressContainer: { marginBottom: 16, }, progressBar: { height: 3, backgroundColor: 'rgba(255,255,255,0.3)', borderRadius: 1.5, overflow: 'hidden', }, progressFill: { height: '100%', backgroundColor: '#fff', borderRadius: 1.5, }, controlButtons: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, controlButton: { padding: 8, borderRadius: 20, backgroundColor: 'rgba(0,0,0,0.4)', }, }); export default TrailerPlayer;