import { forwardRef, useEffect, useRef } from "react"; import Hls from "hls.js"; interface VideoPlayerProps { src?: string; poster?: string; className?: string; /** Seconds into the current item to seek on load (broadcast sync). */ initialOffset?: number; onStreamError?: () => void; } const VideoPlayer = forwardRef( ({ src, poster, className, initialOffset = 0, onStreamError }, ref) => { const internalRef = useRef(null); const hlsRef = useRef(null); const setRef = (el: HTMLVideoElement | null) => { internalRef.current = el; if (typeof ref === "function") ref(el); else if (ref) ref.current = el; }; useEffect(() => { const video = internalRef.current; if (!video || !src) return; hlsRef.current?.destroy(); hlsRef.current = null; const isHls = src.includes(".m3u8"); if (isHls && Hls.isSupported()) { const hls = new Hls({ startPosition: initialOffset > 0 ? initialOffset : -1, maxMaxBufferLength: 30, }); hlsRef.current = hls; hls.on(Hls.Events.MANIFEST_PARSED, () => { video.play().catch(() => {}); }); hls.on(Hls.Events.ERROR, (_event, data) => { if (data.fatal) onStreamError?.(); }); hls.loadSource(src); hls.attachMedia(video); } else if (isHls && video.canPlayType("application/vnd.apple.mpegurl")) { // Native HLS (Safari) video.src = src; video.addEventListener( "loadedmetadata", () => { if (initialOffset > 0) video.currentTime = initialOffset; video.play().catch(() => {}); }, { once: true }, ); } else { // Plain MP4 fallback video.src = src; video.load(); video.play().catch(() => {}); } return () => { hlsRef.current?.destroy(); hlsRef.current = null; }; // initialOffset intentionally excluded: only seek when src changes (new slot/mount) // eslint-disable-next-line react-hooks/exhaustive-deps }, [src]); return (
); }, ); VideoPlayer.displayName = "VideoPlayer"; export { VideoPlayer }; export type { VideoPlayerProps };