FEAT: Add custom error UI when stream unavailable
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user