Beki
Beki

Reputation: 1746

ReactJS: How to switch video tags just when one is ready to play to not show the loading black screen?

Background

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.

The problem

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.

My solution

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.

Sample source code

// ---------- 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?

Dev environment

Upvotes: 1

Views: 670

Answers (1)

hackape
hackape

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

Related Questions