This commit is contained in:
imeepos
2025-10-21 10:21:45 +08:00
parent 7e3f94bae3
commit 3a99ff96d5
28 changed files with 6981 additions and 176 deletions

View File

@@ -5,6 +5,7 @@ import {
TouchableOpacity,
Dimensions,
ActivityIndicator,
Platform,
} from 'react-native';
import { Video, ResizeMode, AVPlaybackStatus } from 'expo-av';
import { Image } from 'expo-image';
@@ -33,8 +34,10 @@ export interface VideoPlayerProps {
maxHeight?: number;
aspectRatio?: number;
onReady?: (status: AVPlaybackStatus) => void;
onWebLoadedData?: () => void;
onError?: (error: any) => void;
onPress?: () => void;
fullscreenMode?: boolean; // 新增:全屏模式标志
}
export function VideoPlayer({
@@ -51,14 +54,18 @@ export function VideoPlayer({
maxHeight,
aspectRatio,
onReady,
onWebLoadedData,
onError,
onPress,
fullscreenMode = false, // 新增:默认非全屏模式
}: VideoPlayerProps) {
const videoRef = useRef<Video>(null);
const [videoStatus, setVideoStatus] = useState<AVPlaybackStatus>();
const [videoMetadata, setVideoMetadata] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const [showPosterOverlay, setShowPosterOverlay] = useState(showPoster);
const [hasVideoError, setHasVideoError] = useState(false);
const [shouldShowAsImage, setShouldShowAsImage] = useState(false);
const backgroundColor = useThemeColor({}, 'background');
const placeholderColor = useThemeColor({}, 'imagePlaceholder');
@@ -67,6 +74,16 @@ export function VideoPlayer({
const isValidUrl = isValidVideoUrl(source.uri);
const videoFileType = getVideoFileType(source.uri);
// 如果URL看起来不是视频显示为图片
useEffect(() => {
if (!isValidUrl || !videoFileType) {
console.log('URL不是有效的视频格式将作为图片显示:', source.uri);
setShouldShowAsImage(true);
setHasVideoError(true);
setIsLoading(false);
}
}, [isValidUrl, videoFileType, source.uri]);
// 计算视频容器的最佳尺寸
const calculateVideoDimensions = () => {
const containerWidth = screenWidth - 40; // 减去padding
@@ -132,7 +149,16 @@ export function VideoPlayer({
// 处理视频加载错误
const handleVideoError = (error: any) => {
setIsLoading(false);
setHasVideoError(true);
console.error('视频加载失败:', error);
// 尝试作为图片显示
const isImageUrl = source.uri.match(/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i);
if (isImageUrl) {
console.log('视频加载失败,尝试作为图片显示:', source.uri);
setShouldShowAsImage(true);
}
onError?.(error);
};
@@ -153,6 +179,11 @@ export function VideoPlayer({
return;
}
// 全屏模式下不处理视频播放逻辑,只传递点击事件
if (fullscreenMode) {
return;
}
// 如果视频已加载但未播放,开始播放
if (videoStatus?.isLoaded && !videoStatus.isPlaying && !useNativeControls) {
videoRef.current?.playAsync();
@@ -166,19 +197,50 @@ export function VideoPlayer({
}
}, [autoPlay, videoStatus]);
const isVideoLoaded = videoStatus?.isLoaded;
const isVideoPlaying = videoStatus?.isLoaded && videoStatus.isPlaying;
const isVideoLoaded = Platform.OS === 'web' ? !isLoading : videoStatus?.isLoaded;
const isVideoPlaying = Platform.OS === 'web' ? true : (videoStatus?.isLoaded && videoStatus.isPlaying);
// 如果应该显示为图片,显示图片而不是视频
if (shouldShowAsImage) {
return (
<TouchableOpacity
style={[styles.container, { height: videoSize.height }, style]}
onPress={handleContainerPress}
activeOpacity={0.9}
>
<ThemedView style={[styles.videoContainer, { backgroundColor }]}>
<Image
source={{ uri: source.uri }}
style={[
styles.poster,
{
width: '100%',
height: '100%',
backgroundColor: placeholderColor,
},
]}
contentFit="cover"
onLoad={() => setIsLoading(false)}
onError={(error) => {
console.error('图片加载失败:', error);
setIsLoading(false);
}}
/>
</ThemedView>
</TouchableOpacity>
);
}
return (
<TouchableOpacity
style={[styles.container, { height: videoSize.height }, style]}
onPress={handleContainerPress}
activeOpacity={0.9}
disabled={!onPress && useNativeControls}
activeOpacity={fullscreenMode ? 0.8 : 0.9}
disabled={fullscreenMode ? false : (!onPress && useNativeControls)}
>
<ThemedView style={[styles.videoContainer, { backgroundColor }]}>
{/* 封面图 */}
{poster && showPosterOverlay && (
{/* 封面图 - 只在非 Web 平台显示 */}
{poster && showPosterOverlay && Platform.OS !== 'web' && !hasVideoError && (
<Image
source={{ uri: poster }}
style={[
@@ -194,35 +256,87 @@ export function VideoPlayer({
)}
{/* 视频播放器 */}
<Video
ref={videoRef}
source={source}
style={[
styles.video,
{
width: '100%',
height: '100%',
},
]}
resizeMode={resizeMode}
shouldPlay={shouldPlay}
isLooping={isLooping}
isMuted={isMuted}
useNativeControls={useNativeControls}
onReadyForDisplay={handleVideoReady}
onError={handleVideoError}
onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
/>
{!hasVideoError && (
<>
{Platform.OS === 'web' ? (
<video
ref={videoRef}
src={source.uri}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
overflow: 'hidden',
cursor: fullscreenMode ? 'pointer' : 'default',
}}
autoPlay={autoPlay}
loop={isLooping}
muted={isMuted}
playsInline
onClick={(e) => {
// 全屏模式下,点击视频直接触发关闭
if (fullscreenMode && onPress) {
e.stopPropagation();
onPress();
}
}}
onLoadedData={() => {
setIsLoading(false);
onWebLoadedData?.(); // 通知父组件Web平台视频已加载
}}
onError={handleVideoError}
/>
) : (
<Video
ref={videoRef}
source={source}
style={styles.video}
resizeMode={resizeMode}
shouldPlay={shouldPlay}
isLooping={isLooping}
isMuted={isMuted}
useNativeControls={useNativeControls}
onReadyForDisplay={handleVideoReady}
onError={handleVideoError}
onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
/>
)}
</>
)}
{/* 加载指示器 */}
{isLoading && (
{/* 错误状态下显示为图片 */}
{hasVideoError && !shouldShowAsImage && (
<Image
source={{ uri: source.uri }}
style={[
styles.poster,
{
width: '100%',
height: '100%',
backgroundColor: placeholderColor,
},
]}
contentFit="cover"
onLoad={() => setIsLoading(false)}
onError={(error) => {
console.error('错误回退:图片也加载失败:', error);
setIsLoading(false);
}}
/>
)}
{/* 加载指示器 - 只在非 Web 平台显示 */}
{isLoading && Platform.OS !== 'web' && !hasVideoError && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color="#4ECDC4" />
</View>
)}
{/* 播放按钮覆盖层(当视频暂停且无原生控件时显示) */}
{!useNativeControls && isVideoLoaded && !isVideoPlaying && !isLoading && (
{/* 播放按钮覆盖层(当视频暂停且无原生控件时显示) - 全屏模式下不显示 */}
{!useNativeControls && isVideoLoaded && !isVideoPlaying && !isLoading && !hasVideoError && !fullscreenMode && (
<View style={styles.playButtonOverlay}>
<View style={styles.playButton}>
<ThemedView style={styles.playIcon}>
@@ -232,8 +346,8 @@ export function VideoPlayer({
</View>
)}
{/* 错误状态 */}
{!isLoading && !isVideoLoaded && (
{/* 错误状态 - 只在非 Web 平台显示 */}
{!isLoading && !isVideoLoaded && hasVideoError && Platform.OS !== 'web' && !shouldShowAsImage && (
<View style={styles.errorOverlay}>
<ThemedView style={styles.errorMessage}>
{'❌'}
@@ -265,8 +379,10 @@ const styles = StyleSheet.create({
left: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
overflow: 'hidden',
},
poster: {
poster: {
position: 'absolute',
top: 0,
left: 0,