70 lines
1.8 KiB
TypeScript
70 lines
1.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { choosePlayback, supportsNativeHls } from "@/lib/video-playback";
|
|
|
|
export interface VideoPlayerProps {
|
|
hlsUrl: string;
|
|
poster: string | null;
|
|
label: string;
|
|
}
|
|
|
|
/**
|
|
* Real <video controls>. Native HLS where the browser has it, otherwise hls.js,
|
|
* loaded only when a video card mounts. Never autoplays.
|
|
*/
|
|
export function VideoPlayer({ hlsUrl, poster, label }: VideoPlayerProps) {
|
|
const ref = useRef<HTMLVideoElement>(null);
|
|
const [failed, setFailed] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const video = ref.current;
|
|
if (!video) return;
|
|
let destroy: (() => void) | undefined;
|
|
let cancelled = false;
|
|
const native = supportsNativeHls(video);
|
|
if (native) {
|
|
if (choosePlayback(hlsUrl, { nativeHls: true, hlsJsSupported: false }) === "native") {
|
|
video.src = hlsUrl;
|
|
}
|
|
return;
|
|
}
|
|
import("hls.js")
|
|
.then(({ default: Hls }) => {
|
|
if (cancelled) return;
|
|
const mode = choosePlayback(hlsUrl, { nativeHls: false, hlsJsSupported: Hls.isSupported() });
|
|
if (mode !== "hlsjs") {
|
|
setFailed(true);
|
|
return;
|
|
}
|
|
const hls = new Hls();
|
|
hls.on(Hls.Events.ERROR, (_e, data) => {
|
|
if (data.fatal) setFailed(true);
|
|
});
|
|
hls.loadSource(hlsUrl);
|
|
hls.attachMedia(video);
|
|
destroy = () => hls.destroy();
|
|
})
|
|
.catch(() => setFailed(true));
|
|
return () => {
|
|
cancelled = true;
|
|
destroy?.();
|
|
};
|
|
}, [hlsUrl]);
|
|
|
|
if (failed && poster) {
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
return <img src={poster} alt="Video thumbnail" loading="lazy" />;
|
|
}
|
|
return (
|
|
<video
|
|
ref={ref}
|
|
controls
|
|
playsInline
|
|
preload="none"
|
|
poster={poster ?? undefined}
|
|
aria-label={label}
|
|
/>
|
|
);
|
|
}
|