import { ThemedView } from '@/components/themed-view'; import { useThemeColor } from '@/hooks/use-theme-color'; import { calculateOptimalVideoSize, getVideoFileType, isValidVideoUrl } from '@/utils/video-utils'; import { Image } from 'expo-image'; import { VideoView, useVideoPlayer } from 'expo-video'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, Dimensions, Platform, StyleSheet, TouchableOpacity, View, } from 'react-native'; import * as Device from 'expo-device'; const isEmulator = !Device.isDevice; // ResizeMode 兼容映射 const ResizeMode = { CONTAIN: 'contain' as const, COVER: 'cover' as const, STRETCH: 'stretch' as const, }; // 兼容性状态类型 interface AVPlaybackStatus { isLoaded: boolean; isPlaying?: boolean; durationMillis?: number; naturalSize?: { width: number; height: number; }; positionMillis?: number; } const { width: screenWidth } = Dimensions.get('window'); export interface VideoPlayerProps { source: { uri: string }; poster?: string; style?: any; resizeMode?: 'contain' | 'cover' | 'stretch'; shouldPlay?: boolean; isLooping?: boolean; isMuted?: boolean; useNativeControls?: boolean; showPoster?: boolean; autoPlay?: boolean; maxHeight?: number; aspectRatio?: number; onReady?: (status: AVPlaybackStatus) => void; onWebLoadedData?: () => void; onError?: (error: any) => void; onPress?: () => void; fullscreenMode?: boolean; } export function VideoPlayer({ source, poster, style, resizeMode = ResizeMode.CONTAIN, shouldPlay = false, isLooping = true, isMuted = true, useNativeControls = false, showPoster = true, autoPlay = false, maxHeight, aspectRatio, onReady, onWebLoadedData, onError, onPress, fullscreenMode = false, }: VideoPlayerProps) { // 使用useMemo稳定source对象,避免不必要的重新渲染 const stableSource = useMemo(() => ({ uri: source.uri }), [source.uri]); // 使用useRef存储player实例,避免无限循环 const playerRef = useRef(null); // 始终调用useVideoPlayer,但只在source变化时更新player const newPlayer = useVideoPlayer( stableSource, (player) => { // 空初始化回调 } ); // 只有当newPlayer变化时才更新player useEffect(() => { playerRef.current = newPlayer; }, [newPlayer]); // 单独处理播放属性变化 useEffect(() => { const player = playerRef.current; if (player) { player.loop = isLooping; player.muted = isMuted; } }, [isLooping, isMuted]); // 处理自动播放 useEffect(() => { const player = playerRef.current; if (player) { if (autoPlay && shouldPlay) { player.play(); } else if (!shouldPlay) { player.pause(); } } }, [autoPlay, shouldPlay]); // 获取当前的player实例 const player = playerRef.current || newPlayer; const [videoMetadata, setVideoMetadata] = useState(null); const [isLoading, setIsLoading] = useState(true); const [hasVideoError, setHasVideoError] = useState(false); const [shouldShowAsImage, setShouldShowAsImage] = useState(false); const backgroundColor = useThemeColor({}, 'background'); const placeholderColor = useThemeColor({}, 'imagePlaceholder'); // 验证视频URL const isValidUrl = isValidVideoUrl(source.uri); const videoFileType = getVideoFileType(source.uri); const shouldDisableVideoOnEmulator = Platform.OS === 'android' && isEmulator; // 如果URL看起来不是视频,或者在Android模拟器上,显示为图片 useEffect(() => { if (!isValidUrl || !videoFileType) { setShouldShowAsImage(true); setHasVideoError(true); setIsLoading(false); } else if (shouldDisableVideoOnEmulator) { setShouldShowAsImage(true); setHasVideoError(true); setIsLoading(false); } }, [isValidUrl, videoFileType, source.uri, shouldDisableVideoOnEmulator]); // 计算视频容器的最佳尺寸 const calculateVideoDimensions = () => { const containerWidth = screenWidth - 40; const defaultHeight = maxHeight || screenWidth * 0.75; if (aspectRatio) { const calculatedHeight = containerWidth / aspectRatio; return { width: containerWidth, height: maxHeight ? Math.min(calculatedHeight, maxHeight) : calculatedHeight, }; } if (videoMetadata) { const contentFit = resizeMode === ResizeMode.COVER ? 'cover' : 'contain'; return calculateOptimalVideoSize( containerWidth, maxHeight || defaultHeight, videoMetadata, contentFit as any ); } return { width: containerWidth, height: defaultHeight, }; }; const videoSize = calculateVideoDimensions(); // 处理视频加载完成 const handleVideoReady = () => { setIsLoading(false); onReady?.({ isLoaded: true, } as AVPlaybackStatus); }; // 处理视频加载错误 const handleVideoError = (error: any) => { setIsLoading(false); setHasVideoError(true); console.warn('视频加载失败:', error); const isImageUrl = source.uri.match(/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i); if (isImageUrl) { setShouldShowAsImage(true); } onError?.(error); }; // 处理容器点击 const handleContainerPress = () => { if (onPress) { onPress(); return; } if (fullscreenMode) { return; } if (Platform.OS === 'web') { return; } if (player) { try { if (player.playing) { player.pause(); } else { player.play().catch((e: any) => { console.warn('视频播放失败:', e); handleVideoError(e); }); } } catch (e) { console.warn('视频控制失败:', e); handleVideoError(e); } } }; // 如果应该显示为图片,显示图片而不是视频 if (shouldShowAsImage) { return ( setIsLoading(false)} onError={(error) => { console.error('图片加载失败:', error); setIsLoading(false); }} /> ); } return ( {/* 视频播放器 */} {!hasVideoError && ( <> {Platform.OS === 'web' ? ( ); } const styles = StyleSheet.create({ container: { width: '100%', borderRadius: 12, overflow: 'hidden', }, videoContainer: { position: 'relative', width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center', overflow: 'hidden', }, video: { position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', }, poster: { position: 'absolute', top: 0, left: 0, zIndex: 1, }, loadingOverlay: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0, 0, 0, 0.1)', zIndex: 2, }, }); export default VideoPlayer;