mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-04 03:36:29 +08:00
148 lines
3.5 KiB
TypeScript
148 lines
3.5 KiB
TypeScript
import React, { useRef, useState, useEffect } from "react";
|
|
import { View, StyleSheet, Text, ActivityIndicator } from "react-native";
|
|
import { Video, ResizeMode, AVPlaybackStatus } from "expo-av";
|
|
|
|
interface LivePlayerProps {
|
|
streamUrl: string | null;
|
|
channelTitle?: string | null;
|
|
onPlaybackStatusUpdate: (status: AVPlaybackStatus) => void;
|
|
}
|
|
|
|
const PLAYBACK_TIMEOUT = 15000; // 15 seconds
|
|
|
|
export default function LivePlayer({ streamUrl, channelTitle, onPlaybackStatusUpdate }: LivePlayerProps) {
|
|
const video = useRef<Video>(null);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isTimeout, setIsTimeout] = useState(false);
|
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current);
|
|
}
|
|
|
|
if (streamUrl) {
|
|
setIsLoading(true);
|
|
setIsTimeout(false);
|
|
timeoutRef.current = setTimeout(() => {
|
|
setIsTimeout(true);
|
|
setIsLoading(false);
|
|
}, PLAYBACK_TIMEOUT);
|
|
} else {
|
|
setIsLoading(false);
|
|
setIsTimeout(false);
|
|
}
|
|
|
|
return () => {
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current);
|
|
}
|
|
};
|
|
}, [streamUrl]);
|
|
|
|
const handlePlaybackStatusUpdate = (status: AVPlaybackStatus) => {
|
|
if (status.isLoaded) {
|
|
if (status.isPlaying) {
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current);
|
|
}
|
|
setIsLoading(false);
|
|
setIsTimeout(false);
|
|
} else if (status.isBuffering) {
|
|
setIsLoading(true);
|
|
}
|
|
} else {
|
|
if (status.error) {
|
|
setIsLoading(false);
|
|
setIsTimeout(true);
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current);
|
|
}
|
|
}
|
|
}
|
|
onPlaybackStatusUpdate(status);
|
|
};
|
|
|
|
if (!streamUrl) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.messageText}>Select a channel to play.</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (isTimeout) {
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.messageText}>Failed to load stream. It might be offline or unavailable.</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<Video
|
|
ref={video}
|
|
style={styles.video}
|
|
source={{
|
|
uri: streamUrl,
|
|
}}
|
|
resizeMode={ResizeMode.CONTAIN}
|
|
shouldPlay
|
|
onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
|
|
onError={(e) => {
|
|
setIsTimeout(true);
|
|
setIsLoading(false);
|
|
}}
|
|
/>
|
|
{isLoading && (
|
|
<View style={styles.loadingOverlay}>
|
|
<ActivityIndicator size="large" color="#fff" />
|
|
<Text style={styles.messageText}>Loading...</Text>
|
|
</View>
|
|
)}
|
|
{channelTitle && !isLoading && !isTimeout && (
|
|
<View style={styles.overlay}>
|
|
<Text style={styles.title}>{channelTitle}</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
backgroundColor: "#000",
|
|
},
|
|
video: {
|
|
flex: 1,
|
|
alignSelf: "stretch",
|
|
},
|
|
overlay: {
|
|
position: "absolute",
|
|
top: 20,
|
|
left: 20,
|
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
padding: 10,
|
|
borderRadius: 5,
|
|
},
|
|
title: {
|
|
color: "#fff",
|
|
fontSize: 18,
|
|
},
|
|
messageText: {
|
|
color: "#fff",
|
|
fontSize: 16,
|
|
marginTop: 10,
|
|
},
|
|
loadingOverlay: {
|
|
...StyleSheet.absoluteFillObject,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
},
|
|
});
|