import { Image } from 'expo-image'; import { useVideoPlayer, VideoView } from 'expo-video'; import { useEffect, useMemo, useState, useRef } from 'react'; import { Platform, View, ActivityIndicator } from 'react-native'; import type { VideoPlayerProps } from './types'; import * as Device from 'expo-device'; import { useSharedVisibility } from './useSharedVisibility'; import { playerPool } from './player-pool'; import { getVideoThumbnail } from '@/lib/utils/media'; const isEmulator = !Device.isDevice; export default function VideoPlayer({ source, originalImage, style, dwellTime = 800, threshold = 0.2 }: VideoPlayerProps) { const { ref: viewRef, isVisible, shouldPlay } = useSharedVisibility(threshold, dwellTime); const [videoReady, setVideoReady] = useState(false); const [hasSlot, setHasSlot] = useState(false); const [playerCreated, setPlayerCreated] = useState(false); const [imageError, setImageError] = useState(false); const isMountedRef = useRef(true); const lastSourceRef = useRef(undefined); const url = useMemo(() => { const uri = typeof source === 'object' ? source?.uri : source; return uri ? `${uri}` : ''; }, [source]); const isVideo = useMemo(() => /\.(mp4|webm|ogg|mov|avi)$/i.test(url || ''), [url] ); const thumbnailUrl = useMemo(() => { if (!isVideo) return url; return getVideoThumbnail(url, { time: '0s', width: 600, height: 600, fit: 'cover' }); }, [url, isVideo]); const shouldUseVideo = isVideo && (Platform.OS !== 'android' || !isEmulator); const shouldRequestSlot = shouldUseVideo && isVisible && shouldPlay; const shouldCreatePlayer = shouldRequestSlot && hasSlot && playerCreated; const player = useVideoPlayer(shouldCreatePlayer ? source : null, (p) => { p.loop = true; p.muted = true; }); useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; }; }, []); useEffect(() => { if (!shouldRequestSlot) { if (hasSlot) { playerPool.release(); setHasSlot(false); setPlayerCreated(false); } return; } let active = true; playerPool.acquire().then(() => { if (active && isMountedRef.current) { setHasSlot(true); setTimeout(() => { if (active && isMountedRef.current) { setPlayerCreated(true); } }, 100); } else { playerPool.release(); } }); return () => { active = false; if (hasSlot) { playerPool.release(); setHasSlot(false); setPlayerCreated(false); } }; }, [shouldRequestSlot]); useEffect(() => { if (!shouldCreatePlayer || !player) { setVideoReady(false); return; } if (lastSourceRef.current !== url) { setVideoReady(false); lastSourceRef.current = url; } const statusListener = player.addListener('statusChange', (status) => { if (status.status === 'readyToPlay' && isMountedRef.current) { setVideoReady(true); } }); try { player.play(); } catch (error) { if (__DEV__) { console.warn('[VideoPlayer] Play failed:', error); } } return () => { statusListener.remove(); }; }, [shouldCreatePlayer, player, url]); useEffect(() => { return () => { if (player) { try { player.pause(); player.replace(null); } catch (error) { if (__DEV__) { console.warn('[VideoPlayer] Cleanup failed:', error); } } } }; }, [player]); const renderContent = () => { if (shouldUseVideo && player && videoReady) { return ( <> {originalImage && ( )} ); } if (thumbnailUrl && (!isVisible || !shouldPlay || !videoReady)) { return ( { if (__DEV__) { console.warn('[VideoPlayer] Thumbnail failed to load:', thumbnailUrl, error); } setImageError(true); }} /> ); } if (imageError) { return ( ); } return null; }; return ( {renderContent()} ); }