FEAT: Add custom error UI when stream unavailable

This commit is contained in:
Chris-1010
2025-03-02 14:53:21 +00:00
parent 37122e1cea
commit a1e8c92240

View File

@@ -4,106 +4,119 @@ import videojs from "video.js";
import type Player from "video.js/dist/types/player"; import type Player from "video.js/dist/types/player";
import "video.js/dist/video-js.css"; import "video.js/dist/video-js.css";
interface VideoPlayerProps { interface VideoPlayerProps {
streamer?: string; streamer?: string;
extraClasses?: string; extraClasses?: string;
onStreamDetected?: (isStreamAvailable: boolean) => void; onStreamDetected?: (isStreamAvailable: boolean) => void;
} }
const VideoPlayer: React.FC<VideoPlayerProps> = ({ const VideoPlayer: React.FC<VideoPlayerProps> = ({ streamer, extraClasses = "", onStreamDetected }) => {
streamer, const { streamerName: urlStreamerName } = useParams<{
extraClasses = "", streamerName: string;
onStreamDetected, }>();
}) => { const videoRef = useRef<HTMLDivElement>(null);
const { streamerName: urlStreamerName } = useParams<{ const playerRef = useRef<Player | null>(null);
streamerName: string;
}>();
const videoRef = useRef<HTMLDivElement>(null);
const playerRef = useRef<Player | null>(null);
// Use URL param if available, otherwise fall back to prop // Use URL param if available, otherwise fall back to prop
const streamerName = urlStreamerName || streamer; const streamerName = urlStreamerName || streamer;
useEffect(() => { useEffect(() => {
if (!videoRef.current || !streamerName) { if (!videoRef.current || !streamerName) {
console.log("No video ref or streamer name"); console.log("No video ref or streamer name");
return; return;
} }
const setupPlayer = async () => { const setupPlayer = async () => {
const streamUrl = `/stream/${streamerName}/index.m3u8`; const streamUrl = `/stream/${streamerName}/index.m3u8`;
if (!playerRef.current) { if (!playerRef.current) {
const videoElement = document.createElement("video"); const videoElement = document.createElement("video");
videoElement.classList.add( videoElement.classList.add("video-js", "vjs-big-play-centered", "w-full", "h-full");
"video-js", videoElement.setAttribute("playsinline", "true");
"vjs-big-play-centered", if (videoRef.current) {
"w-full", videoRef.current.appendChild(videoElement);
"h-full" }
);
videoElement.setAttribute("playsinline", "true");
if (videoRef.current) {
videoRef.current.appendChild(videoElement);
}
playerRef.current = videojs(videoElement, { playerRef.current = videojs(videoElement, {
controls: false, controls: false,
autoplay: true, autoplay: true,
muted: false, muted: false,
fluid: true, fluid: true,
responsive: true, responsive: true,
aspectRatio: "16:9", aspectRatio: "16:9",
liveui: false, liveui: false,
sources: [ sources: [
{ {
src: streamUrl, src: streamUrl,
type: "application/x-mpegURL", type: "application/x-mpegURL",
}, },
], ],
}); });
playerRef.current.on("loadeddata", () => { playerRef.current.on("loadeddata", () => {
if (onStreamDetected) onStreamDetected(true); if (onStreamDetected) onStreamDetected(true);
}); });
playerRef.current.on("error", () => { playerRef.current.on("error", () => {
console.error(`Stream failed to load: ${streamUrl}`); console.error(`Stream failed to load: ${streamUrl}`);
if (onStreamDetected) onStreamDetected(false); if (onStreamDetected) onStreamDetected(false);
setTimeout(() => {
console.log("Retrying stream...");
playerRef.current?.src({
src: streamUrl,
type: "application/x-mpegURL",
});
playerRef.current?.play();
}, 5000);
});
} else {
playerRef.current.src({
src: streamUrl,
type: "application/x-mpegURL",
});
playerRef.current.play();
}
};
setupPlayer(); if (videoRef.current) {
const errorDisplay = videoRef.current.querySelector(".vjs-error-display") as HTMLElement;
if (errorDisplay) {
errorDisplay.style.display = "none";
}
return () => { // Custom error UI
if (playerRef.current) { const errorElement = document.createElement("div");
playerRef.current.dispose(); errorElement.className = "absolute top-0 left-0 right-0 flex flex-col items-center justify-center h-full bg-gray-800 text-white p-4 rounded";
playerRef.current = null; errorElement.innerHTML = `
} <div class="text-xl mb-2">Stream Currently Unavailable</div>
}; <div class="mb-4">The streamer may be offline</div>
}, [streamerName]); `;
videoRef.current.appendChild(errorElement);
}
return ( setTimeout(() => {
<div console.log("Retrying stream...");
id="video-player" playerRef.current?.src({
className={`${extraClasses} w-full h-full mx-auto flex justify-center items-center bg-gray-900 rounded-lg`} src: streamUrl,
> type: "application/x-mpegURL",
<div ref={videoRef} className="w-full max-w-[160vh] self-center" /> });
</div> playerRef.current?.play();
);
// Remove the custom error UI before retrying
if (videoRef.current) {
const errorElement = videoRef.current.querySelector(".flex.flex-col");
if (errorElement) {
videoRef.current.removeChild(errorElement);
}
}
}, 5000);
});
} else {
playerRef.current.src({
src: streamUrl,
type: "application/x-mpegURL",
});
playerRef.current.play();
}
};
setupPlayer();
return () => {
if (playerRef.current) {
playerRef.current.dispose();
playerRef.current = null;
}
};
}, [streamerName]);
return (
<div id="video-player" className={`${extraClasses} w-full h-full mx-auto flex justify-center items-center bg-gray-900 rounded-lg`}>
<div ref={videoRef} className="w-full max-w-[160vh] self-center" />
</div>
);
}; };
export default VideoPlayer; export default VideoPlayer;