expo-popcore-old/components/sker/video-player/video-player.native.tsx

214 lines
5.3 KiB
TypeScript

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<string | undefined>(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 (
<>
<VideoView
player={player}
contentFit="cover"
nativeControls={false}
style={{
width: '100%',
height: '100%'
}}
/>
{originalImage && (
<Image
source={{ uri: originalImage }}
style={{
width: 64,
height: 64,
position: 'absolute',
bottom: 8,
left: 8,
borderRadius: 12,
borderWidth: 1,
borderColor: '#ffffff',
zIndex: 99
}}
contentFit="cover"
/>
)}
</>
);
}
if (thumbnailUrl && (!isVisible || !shouldPlay || !videoReady)) {
return (
<Image
source={{ uri: thumbnailUrl }}
style={{ width: '100%', height: '100%' }}
contentFit="cover"
contentPosition="center"
cachePolicy="memory-disk"
priority="high"
transition={200}
onError={(error) => {
if (__DEV__) {
console.warn('[VideoPlayer] Thumbnail failed to load:', thumbnailUrl, error);
}
setImageError(true);
}}
/>
);
}
if (imageError) {
return (
<View style={{
width: '100%',
height: '100%',
backgroundColor: '#1A1A1A',
justifyContent: 'center',
alignItems: 'center'
}} />
);
}
return null;
};
return (
<View ref={viewRef as any} style={[{ width: '100%', backgroundColor: '#1A1A1A' }, style]}>
{renderContent()}
</View>
);
}