Files
silk/app/src/screens/VideoPlayerScreen.tsx
T
2026-08-13 22:18:35 +08:00

189 lines
5.0 KiB
TypeScript

import React, { useState, useRef, useEffect } from 'react';
import { StyleSheet, View, StatusBar, ActivityIndicator } from 'react-native';
import { Text, IconButton, Surface } from 'react-native-paper';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRoute, useNavigation } from '@react-navigation/native';
import Video, { type VideoRef } from 'react-native-video';
export default function VideoPlayerScreen() {
const route = useRoute<any>();
const navigation = useNavigation<any>();
const { cameraName, streamUrl } = route.params;
const videoRef = useRef<VideoRef>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [paused, setPaused] = useState(false);
const [muted, setMuted] = useState(true);
useEffect(() => {
if (!streamUrl) {
setError('无视频流地址');
setLoading(false);
}
}, [streamUrl]);
const handleLoad = () => {
setLoading(false);
setError(null);
};
const handleError = (e: any) => {
setLoading(false);
const msg = e?.error?.errorString || e?.error?.localizedDescription || '视频加载失败';
setError(msg);
};
return (
<View style={styles.container}>
<StatusBar barStyle="light-content" />
<SafeAreaView style={styles.header} edges={['top']}>
<IconButton
icon="arrow-left"
iconColor="white"
size={24}
onPress={() => navigation.goBack()}
/>
<Text style={styles.headerTitle}>{cameraName}</Text>
<View style={{ width: 48 }} />
</SafeAreaView>
<View style={styles.videoContainer}>
{error ? (
<View style={styles.errorContainer}>
<IconButton icon="alert-circle-outline" size={48} iconColor="#B8B3AA" />
<Text style={styles.errorText}>{error}</Text>
</View>
) : (
<>
{streamUrl ? (
<Video
ref={videoRef}
source={{ uri: streamUrl }}
style={styles.video}
resizeMode="contain"
paused={paused}
muted={muted}
onLoad={handleLoad}
onError={handleError}
playInBackground={false}
controls={false}
bufferConfig={{
minBufferMs: 1000,
maxBufferMs: 3000,
bufferForPlaybackMs: 500,
bufferForPlaybackAfterRebufferMs: 1000,
}}
/>
) : null}
{loading ? (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color="white" />
<Text style={styles.loadingText}>加载中...</Text>
</View>
) : null}
{!loading && !error ? (
<View style={styles.controlsOverlay}>
<Surface style={styles.controlBar}>
<IconButton
icon={paused ? 'play' : 'pause'}
iconColor="white"
size={28}
onPress={() => setPaused(!paused)}
/>
<IconButton
icon={muted ? 'volume-off' : 'volume-high'}
iconColor="white"
size={24}
onPress={() => setMuted(!muted)}
/>
</Surface>
</View>
) : null}
</>
)}
</View>
<View style={styles.footer}>
<Text style={styles.streamUrl}>流地址: {streamUrl || 'N/A'}</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#1A1815',
paddingHorizontal: 4,
},
headerTitle: {
color: 'white',
fontSize: 18,
fontWeight: '500',
flex: 1,
textAlign: 'center',
},
videoContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#000',
},
video: {
width: '100%',
height: '100%',
},
loadingOverlay: {
...StyleSheet.absoluteFillObject,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(26,24,21,0.5)',
},
loadingText: {
color: 'white',
marginTop: 12,
},
controlsOverlay: {
position: 'absolute',
bottom: 24,
left: 0,
right: 0,
alignItems: 'center',
},
controlBar: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(26,24,21,0.6)',
borderRadius: 28,
paddingHorizontal: 8,
elevation: 0,
},
errorContainer: {
alignItems: 'center',
justifyContent: 'center',
},
errorText: {
color: '#B8B3AA',
fontSize: 16,
marginTop: 8,
textAlign: 'center',
paddingHorizontal: 32,
},
footer: {
padding: 12,
backgroundColor: '#1A1815',
},
streamUrl: {
color: '#8C8780',
fontSize: 11,
},
});