feat: implement HLS streaming support in VideoPlayer and enhance stream URL handling
This commit is contained in:
@@ -1,33 +1,90 @@
|
||||
import { forwardRef } from "react";
|
||||
import { forwardRef, useEffect, useRef } from "react";
|
||||
import Hls from "hls.js";
|
||||
|
||||
interface VideoPlayerProps {
|
||||
src?: string;
|
||||
poster?: string;
|
||||
className?: string;
|
||||
/** Seek to this many seconds after metadata loads (broadcast sync on refresh). */
|
||||
/** Seconds into the current item to seek on load (broadcast sync). */
|
||||
initialOffset?: number;
|
||||
onStreamError?: () => void;
|
||||
}
|
||||
|
||||
const VideoPlayer = forwardRef<HTMLVideoElement, VideoPlayerProps>(
|
||||
({ src, poster, className, initialOffset }, ref) => {
|
||||
({ src, poster, className, initialOffset = 0, onStreamError }, ref) => {
|
||||
const internalRef = useRef<HTMLVideoElement | null>(null);
|
||||
const hlsRef = useRef<Hls | null>(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 (
|
||||
<div className={`relative h-full w-full bg-black ${className ?? ""}`}>
|
||||
<video
|
||||
ref={ref}
|
||||
src={src}
|
||||
ref={setRef}
|
||||
poster={poster}
|
||||
autoPlay
|
||||
playsInline
|
||||
onLoadedMetadata={(e) => {
|
||||
if (initialOffset && initialOffset > 0) {
|
||||
e.currentTarget.currentTime = initialOffset;
|
||||
}
|
||||
}}
|
||||
onError={onStreamError}
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
VideoPlayer.displayName = "VideoPlayer";
|
||||
|
||||
Reference in New Issue
Block a user