// app/components/hls.tsx "use client"; import React from "react"; import Hls from "hls.js"; type Props = { src: string; fallbackSrc?: string; // MP4 fallback URL // optional props if you want controls?: boolean; autoPlay?: boolean; videoId?: string; // Auto-load subtitles from /subtitles/{videoId}.vtt subtitles?: Array<{ src: string; kind?: "subtitles" | "captions" | "descriptions" | "chapters" | "metadata"; srclang?: string; label?: string; }>; }; export const HlsPlayer = React.forwardRef(function HlsPlayer( { src, fallbackSrc, controls = true, autoPlay = false, videoId, subtitles = [] }, ref ) { const internalRef = React.useRef(null); const [error, setError] = React.useState(null); // allow parent ref to point to the underlying video React.useImperativeHandle(ref, () => internalRef.current || ({} as HTMLVideoElement), [internalRef.current]); React.useEffect(() => { const video = internalRef.current; if (!video) return; // avoid attaching multiple Hls instances if src didn't change let hls: Hls | null = null; let hasError = false; const loadHls = (sourceUrl: string) => { // Check if it's an HLS URL if (sourceUrl.endsWith('.m3u8')) { if (video.canPlayType("application/vnd.apple.mpegurl")) { // native HLS (Safari) video.src = sourceUrl; } else if (Hls.isSupported()) { hls = new Hls(); // Handle HLS errors with fallback hls.on(Hls.Events.ERROR, (event, data) => { console.error('HLS Error:', event, data); if (data.fatal) { hasError = true; // Try fallback if available if (fallbackSrc) { console.log('Falling back to MP4:', fallbackSrc); setError(null); loadMp4(fallbackSrc); } else { setError('Failed to load HLS stream'); } } }); hls.loadSource(sourceUrl); hls.attachMedia(video); } else { // HLS not supported, try fallback if (fallbackSrc) { console.log('HLS not supported, using MP4 fallback'); loadMp4(fallbackSrc); } else { setError('HLS streaming not supported on this device'); // Try to load as MP4 anyway video.src = sourceUrl; } } } else { // Non-HLS URL, load as MP4 loadMp4(sourceUrl); } }; const loadMp4 = (sourceUrl: string) => { if (hls) { hls.destroy(); hls = null; } video.src = sourceUrl; }; loadHls(src); // cleanup return () => { if (hls) { hls.destroy(); hls = null; } // optionally pause and clear src if (video) { try { video.pause(); } catch {} // video.src = ""; } }; }, [src, fallbackSrc]); return ( <> {error && (
{error}
)} ); }); export default HlsPlayer;