Reputation: 1746
I have a video player, an instance of VideoJS. The player receives videoData
via a socket event video_data
from the server. Then it changes the video source to the one that is received.
The server is running on-premise, all video files, database is hosted on-premise. So there is close to 0 network latency.
A black screen appears while the browser loads the video data. I would like to tune it further and not show the black screen at all.
Here is my approach: I want to create two video instances (<video>
tags) and switch between them. The first one is hidden and the second one is visible. When it receives video data, rather than changing the source of the current visible <video>
tag, it changes the source of the hidden <video>
tag. Just when hidden <video>
tag is ready to play (onCanPlay
event), it switches the visibility of the two <video>
tags. Ideally that's how the black screen will not be visible at all.
// ---------- Index.tsx ----------
import { useEffect, useRef, useState } from "react";
import videojs, { VideoJsPlayerOptions } from "video.js";
import "video.js/dist/video-js.css";
import { VideoDataWithOptions } from "../../@types/types";
import { socket } from "../../lib/socket";
import VideoPlayer from "./VideoPlayer";
const hlsVideoJsOptions: VideoJsPlayerOptions = {
autoplay: true,
muted: false,
controls: true,
loop: false,
sources: [
{
src: "",
type: "application/x-mpegURL",
},
],
};
const Index = () => {
const [videoData, setVideoData] = useState({} as VideoDataWithOptions);
useEffect((): any => {
socket.on("videoData", data => {
const options = {
...hlsVideoJsOptions,
sources: [
{
src: `/videos/destination/${data.path}`,
type: "application/x-mpegURL",
},
],
};
setVideoData({ ...data, options });
});
return () => socket.off("videoData");
}, []);
return <VideoPlayer videoData={videoData}></VideoPlayer>;
};
export default Index;
// ---------- VideoPlayer.tsx ----------
type Props = { videoData: VideoDataWithOptions }; // ex. => { path: string, title: string, loop: boolean, options: VideoJsPlayerOptions, ... }
const VideoPlayer = ({ videoData }: Props) => {
const videoRef = useRef<HTMLVideoElement>(null);
const playerRef = useRef<any>(null); // null
useEffect(() => {
// Make sure Video.js player is only initialized once
if (!playerRef.current) {
const videoElement = videoRef.current;
if (!videoElement) return;
playerRef.current = videojs(videoElement, videoData.options, () => {});
} else {
}
}, [videoData, videoRef]);
return (
<div data-vjs-player>
<video ref={videoRef} className="video-js" playsInline />
{/* <video ref={videoRef} className="video-js" playsInline /> */}
</div>
);
};
export default VideoPlayer;
// How should I go about this?
Upvotes: 1
Views: 670
Reputation: 19957
If I understand you right, your videoData
is gonna change repeatedly, so you need to switch between the two <video />
elements and there's always only one of them visible.
Now think about it, videoData
is the only variable, and there's very little view reactivity attach to it. My solution is based on this fact, it's very "un-react", very opinionated. In fact I'mma only use react to load and unload two <video />
elements, and the rest is all taken cared by imperative code.
type RefHolder = {
current: HTMLVideoElement | null;
player: any | null;
};
type VideoPlayerInternalState = {
flag: number;
video_1: RefHolder;
video_2: RefHolder;
};
const VideoPlayer = ({ videoData }: Props) => {
const state = useRef<VideoPlayerInternalState>({
flag: 0,
video_1: { current: null, player: null },
video_2: { current: null, player: null },
}).current;
useEffect(() => {
// use a odd/even flag to switch between 2 `<video />` element
let ref: RefHolder, other_ref: RefHolder;
if (state.flag % 2 == 0) {
ref = state.video_1;
other_ref = state.video_2;
} else {
ref = state.video_2;
other_ref = state.video_1;
}
// if, for whatever reason, element not found,
// which normally shouldn't happen, we just abort.
if (!ref.current) return;
// bump the flag, so that next time `videoData` change,
// we'll switch to the other `<video />` element
state.flag += 1;
if (!ref.player) {
ref.player = videojs(ref.current, videoData.options, () => {});
}
// attach once event listener
const onCanPlay = () => {
ref.current.classList.add("visible");
other_ref.current.classList.remove("visible");
// autoplay? you decide
ref.player.play();
};
ref.player.one("canplay", onCanPlay);
ref.player.src(videoData.options.sources);
// I'm not sure about your scenario
// but you might wanna cancel the pending effect
// in case `videoData` switches too fast
const cancelPending = () => {
ref.player.off("canplay", onCanPlay);
}
return cancelPending;
}, [videoData]); // <-- We only react to videoData change
useEffect(() => {
const onUnmounted = () => {
state.video_1.player?.dispose();
state.video_2.player?.dispose();
};
return onUnmounted;
}, [])
return (
<div data-vjs-player>
<video ref={state.video_1} className="video-js visible" playsInline />
<video ref={state.video_2} className="video-js" playsInline />
</div>
);
};
export default VideoPlayer;
Upvotes: 1