137 lines
3.5 KiB
TypeScript
137 lines
3.5 KiB
TypeScript
import { useVideoPlayer, VideoView } from "expo-video";
|
|
import { forwardRef, useEffect, useState } from "react";
|
|
import { View, Pressable, Text, ActivityIndicator } from "react-native";
|
|
import { cn } from "../../lib/utils";
|
|
import Image from "./Img";
|
|
|
|
interface VideoPlayerProps {
|
|
source: string | { uri: string };
|
|
poster?: string;
|
|
visible?: boolean;
|
|
className?: string;
|
|
autoPlay?: boolean;
|
|
loop?: boolean;
|
|
muted?: boolean;
|
|
controls?: boolean;
|
|
onError?: (error: string) => void;
|
|
onLoad?: () => void;
|
|
}
|
|
|
|
const VideoPlayer = forwardRef<VideoView, VideoPlayerProps>(
|
|
(
|
|
{
|
|
source,
|
|
poster,
|
|
visible = true,
|
|
className,
|
|
autoPlay = false,
|
|
loop = false,
|
|
muted = false,
|
|
controls = true,
|
|
onError,
|
|
onLoad,
|
|
},
|
|
ref
|
|
) => {
|
|
const videoSource = typeof source === "string" ? { uri: source } : source;
|
|
|
|
const player = useVideoPlayer(videoSource, (player) => {
|
|
player.loop = loop;
|
|
player.muted = muted;
|
|
});
|
|
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [showPoster, setShowPoster] = useState(!!poster);
|
|
|
|
useEffect(() => {
|
|
if (!visible) {
|
|
player.pause();
|
|
if (poster) {
|
|
setShowPoster(true);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (autoPlay && visible) {
|
|
player.play();
|
|
}
|
|
}, [visible, autoPlay, player, poster]);
|
|
|
|
useEffect(() => {
|
|
const playingSubscription = player.addListener("playingChange", ({ isPlaying }) => {
|
|
setIsPlaying(isPlaying);
|
|
if (isPlaying) {
|
|
setShowPoster(false);
|
|
}
|
|
});
|
|
|
|
const statusSubscription = player.addListener("statusChange", (payload) => {
|
|
if (payload.status === "readyToPlay") {
|
|
setIsLoading(false);
|
|
onLoad?.();
|
|
}
|
|
|
|
if (payload.status === "error" && payload.error) {
|
|
onError?.(payload.error.message);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
playingSubscription.remove();
|
|
statusSubscription.remove();
|
|
};
|
|
}, [player, onError, onLoad]);
|
|
|
|
const togglePlayback = () => {
|
|
if (isPlaying) {
|
|
player.pause();
|
|
} else {
|
|
player.play();
|
|
setShowPoster(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<View className={cn("!relative !bg-black !overflow-hidden", className)}>
|
|
{visible && (
|
|
<VideoView
|
|
ref={ref}
|
|
player={player}
|
|
className="!absolute !top-0 !left-0 !bottom-0 !right-0 !inset-0 !w-full !h-full"
|
|
style={{ width: '100%', height: '100%', position: 'absolute', left: 0, top: 0 }}
|
|
nativeControls={false}
|
|
contentFit="cover"
|
|
/>
|
|
)}
|
|
|
|
{showPoster && poster && (
|
|
<Image
|
|
source={poster}
|
|
className="!absolute !top-0 !left-0 !bottom-0 !right-0 !inset-0 !w-full !h-full"
|
|
contentFit="cover"
|
|
/>
|
|
)}
|
|
|
|
|
|
{controls && !isLoading && visible && (
|
|
<Pressable
|
|
onPress={togglePlayback}
|
|
className="absolute inset-0 items-center justify-center active:opacity-80"
|
|
>
|
|
{(!isPlaying || showPoster) && (
|
|
<View className="w-16 h-16 items-center justify-center bg-black/50 rounded-full">
|
|
<Text className="text-white text-2xl">▶</Text>
|
|
</View>
|
|
)}
|
|
</Pressable>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
);
|
|
|
|
VideoPlayer.displayName = "VideoPlayer";
|
|
|
|
export { VideoPlayer, type VideoPlayerProps };
|