131 lines
3.6 KiB
TypeScript
131 lines
3.6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import Hls from 'hls.js';
|
|
|
|
interface HLSPlayerProps {
|
|
src: string;
|
|
autoPlay?: boolean;
|
|
loop?: boolean;
|
|
muted?: boolean;
|
|
}
|
|
|
|
export default function HLSPlayer({
|
|
src,
|
|
autoPlay = true,
|
|
loop = true,
|
|
muted = true,
|
|
}: HLSPlayerProps) {
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const hlsRef = useRef<Hls | null>(null);
|
|
|
|
useEffect(() => {
|
|
const video = videoRef.current;
|
|
if (!video) return;
|
|
|
|
let mounted = true;
|
|
|
|
const initializePlayer = () => {
|
|
if (Hls.isSupported()) {
|
|
const hls = new Hls({
|
|
enableWorker: true,
|
|
lowLatencyMode: true,
|
|
});
|
|
hlsRef.current = hls;
|
|
|
|
hls.loadSource(src);
|
|
hls.attachMedia(video);
|
|
|
|
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
if (mounted) {
|
|
setIsLoading(false);
|
|
if (autoPlay) {
|
|
video.play().catch((err) => {
|
|
console.error('Autoplay failed:', err);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
hls.on(Hls.Events.ERROR, (event, data) => {
|
|
console.error('HLS error:', data);
|
|
if (data.fatal && mounted) {
|
|
switch (data.type) {
|
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
setError('Network error - attempting to recover');
|
|
hls.startLoad();
|
|
break;
|
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
setError('Media error - attempting to recover');
|
|
hls.recoverMediaError();
|
|
break;
|
|
default:
|
|
setError('Fatal error - cannot recover');
|
|
hls.destroy();
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
// For Safari native HLS support
|
|
video.src = src;
|
|
const handleLoadedMetadata = () => {
|
|
if (mounted) {
|
|
setIsLoading(false);
|
|
if (autoPlay) {
|
|
video.play().catch((err) => {
|
|
console.error('Autoplay failed:', err);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
video.addEventListener('loadedmetadata', handleLoadedMetadata);
|
|
|
|
return () => {
|
|
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
|
};
|
|
} else if (mounted) {
|
|
setError('HLS is not supported in this browser');
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
initializePlayer();
|
|
|
|
return () => {
|
|
mounted = false;
|
|
if (hlsRef.current) {
|
|
hlsRef.current.destroy();
|
|
hlsRef.current = null;
|
|
}
|
|
};
|
|
}, [src, autoPlay]);
|
|
|
|
return (
|
|
<div className="relative w-full h-full bg-black">
|
|
<video
|
|
ref={videoRef}
|
|
className="w-full h-full object-contain"
|
|
loop={loop}
|
|
muted={muted}
|
|
playsInline
|
|
controls
|
|
/>
|
|
{isLoading && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-black/80">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div className="w-12 h-12 border-4 border-cyan-500 border-t-transparent rounded-full animate-spin"></div>
|
|
<p className="text-cyan-400 font-mono text-sm">Loading stream...</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{error && (
|
|
<div className="absolute top-4 left-1/2 -translate-x-1/2 bg-red-500/90 text-white px-4 py-2 rounded-lg font-mono text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|