Skip to content

useVideo

React hook to manage a video.

Add the hook via the CLI:

sh
npx @novajslabs/cli add useVideo
sh
npx @novajslabs/cli add useVideo
sh
pnpm dlx @novajslabs/cli add useVideo

Or copy and paste the code into your project:

ts
import { useEffect, useState, RefObject } from "react";

export const useVideo = (ref: RefObject<HTMLVideoElement>) => {
  const video = ref.current;

  const [videoState, setVideoState] = useState({
    isPaused: video ? video?.paused : true,
    isMuted: video ? video?.muted : false,
    currentVolume: video ? video?.volume : 100,
    currentTime: video ? video?.currentTime : 0,
  });

  const play = () => {
    video?.play();
    setVideoState((prev) => {
      return {
        ...prev,
        isPaused: false,
        isMuted: video ? video.muted : prev.isMuted,
      };
    });
  };

  const pause = () => {
    video?.pause();
    setVideoState((prev) => {
      return {
        ...prev,
        isPaused: true,
      };
    });
  };

  const handlePlayPauseControl = (e: Event) => {
    setVideoState((prev) => {
      return {
        ...prev,
        isPaused: (e.target as HTMLVideoElement).paused,
      };
    });
  };

  const togglePause = () => (video?.paused ? play() : pause());

  const handleVolume = (delta: number) => {
    const deltaDecimal = delta / 100;

    if (video) {
      let newVolume = video?.volume + deltaDecimal;

      if (newVolume >= 1) {
        newVolume = 1;
      } else if (newVolume <= 0) {
        newVolume = 0;
      }

      video.volume = newVolume;
      setVideoState((prev) => {
        return {
          ...prev,
          currentVolume: newVolume * 100,
        };
      });
    }
  };

  const handleVolumeControl = (e: Event) => {
    if (e.target && video) {
      const newVolume = (e.target as HTMLVideoElement).volume * 100;

      if (newVolume === videoState.currentVolume) {
        handleMute(video.muted);
        return;
      }

      setVideoState((prev) => ({
        ...prev,
        currentVolume: (e.target as HTMLVideoElement).volume * 100,
      }));
    }
  };

  const handleMute = (mute: boolean) => {
    if (video) {
      video.muted = mute;
      setVideoState((prev) => {
        return {
          ...prev,
          isMuted: mute,
        };
      });
    }
  };

  const handleTime = (delta: number = 5) => {
    if (video) {
      let newTime = video.currentTime + delta;

      if (newTime >= video.duration) {
        newTime = video.duration;
      } else if (newTime <= 0) {
        newTime = 0;
      }

      video.currentTime = newTime;
      setVideoState((prev) => {
        return {
          ...prev,
          currentTime: newTime,
        };
      });
    }
  };

  const handleTimeControl = (e: Event) => {
    setVideoState((prev) => {
      return {
        ...prev,
        currentTime: (e.target as HTMLVideoElement).currentTime,
      };
    });
  };

  const toggleFullscreen = () => {
    if (!document.fullscreenElement) {
      video?.requestFullscreen().catch((err) => {
        console.log(err);
      });
    } else {
      document.exitFullscreen();
    }
  };

  useEffect(() => {
    return () => {
      pause();
    };
  }, []);

  useEffect(() => {
    if (video) {
      video.addEventListener("volumechange", handleVolumeControl);
      video.addEventListener("play", handlePlayPauseControl);
      video.addEventListener("pause", handlePlayPauseControl);
      video.addEventListener("timeupdate", handleTimeControl);

      return () => {
        video.removeEventListener("volumechange", handleVolumeControl);
        video.removeEventListener("play", handlePlayPauseControl);
        video.removeEventListener("pause", handlePlayPauseControl);
        video.removeEventListener("timeupdate", handleTimeControl);
      };
    }
  }, [video]);

  return {
    ...videoState,
    play,
    pause,
    togglePause,
    increaseVolume: (increase: number = 5) => handleVolume(increase),
    decreaseVolume: (decrease: number = 5) => handleVolume(decrease * -1),
    mute: () => handleMute(true),
    unmute: () => handleMute(false),
    toggleMute: () => handleMute(!video?.muted),
    forward: (increase: number = 5) => handleTime(increase),
    back: (decrease: number = 5) => handleTime(decrease * -1),
    toggleFullscreen,
  };
};
js
import { useEffect, useState } from "react";

export const useVideo = (ref) => {
  const video = ref.current;

  const [videoState, setVideoState] = useState({
    isPaused: video ? video?.paused : true,
    isMuted: video ? video?.muted : false,
    currentVolume: video ? video?.volume : 100,
    currentTime: video ? video?.currentTime : 0,
  });

  const play = () => {
    video?.play();
    setVideoState((prev) => {
      return {
        ...prev,
        isPaused: false,
        isMuted: video ? video.muted : prev.isMuted,
      };
    });
  };

  const pause = () => {
    video?.pause();
    setVideoState((prev) => {
      return {
        ...prev,
        isPaused: true,
      };
    });
  };

  const handlePlayPauseControl = (e) => {
    setVideoState((prev) => {
      return {
        ...prev,
        isPaused: e.target.paused,
      };
    });
  };

  const togglePause = () => (video?.paused ? play() : pause());

  const handleVolume = (delta) => {
    const deltaDecimal = delta / 100;

    if (video) {
      let newVolume = video?.volume + deltaDecimal;

      if (newVolume >= 1) {
        newVolume = 1;
      } else if (newVolume <= 0) {
        newVolume = 0;
      }

      video.volume = newVolume;
      setVideoState((prev) => {
        return {
          ...prev,
          currentVolume: newVolume * 100,
        };
      });
    }
  };

  const handleVolumeControl = (e) => {
    if (e.target && video) {
      const newVolume = e.target.volume * 100;

      if (newVolume === videoState.currentVolume) {
        handleMute(video.muted);
        return;
      }

      setVideoState((prev) => ({
        ...prev,
        currentVolume: e.target.volume * 100,
      }));
    }
  };

  const handleMute = (mute) => {
    if (video) {
      video.muted = mute;
      setVideoState((prev) => {
        return {
          ...prev,
          isMuted: mute,
        };
      });
    }
  };

  const handleTime = (delta = 5) => {
    if (video) {
      let newTime = video.currentTime + delta;

      if (newTime >= video.duration) {
        newTime = video.duration;
      } else if (newTime <= 0) {
        newTime = 0;
      }

      video.currentTime = newTime;
      setVideoState((prev) => {
        return {
          ...prev,
          currentTime: newTime,
        };
      });
    }
  };

  const handleTimeControl = (e) => {
    setVideoState((prev) => {
      return {
        ...prev,
        currentTime: e.target.currentTime,
      };
    });
  };

  const toggleFullscreen = () => {
    if (!document.fullscreenElement) {
      video?.requestFullscreen().catch((err) => {
        console.log(err);
      });
    } else {
      document.exitFullscreen();
    }
  };

  useEffect(() => {
    return () => {
      pause();
    };
  }, []);

  useEffect(() => {
    if (video) {
      video.addEventListener("volumechange", handleVolumeControl);
      video.addEventListener("play", handlePlayPauseControl);
      video.addEventListener("pause", handlePlayPauseControl);
      video.addEventListener("timeupdate", handleTimeControl);

      return () => {
        video.removeEventListener("volumechange", handleVolumeControl);
        video.removeEventListener("play", handlePlayPauseControl);
        video.removeEventListener("pause", handlePlayPauseControl);
        video.removeEventListener("timeupdate", handleTimeControl);
      };
    }
  }, [video]);

  return {
    ...videoState,
    play,
    pause,
    togglePause,
    increaseVolume: (increase = 5) => handleVolume(increase),
    decreaseVolume: (decrease = 5) => handleVolume(decrease * -1),
    mute: () => handleMute(true),
    unmute: () => handleMute(false),
    toggleMute: () => handleMute(!video?.muted),
    forward: (increase = 5) => handleTime(increase),
    back: (decrease = 5) => handleTime(decrease * -1),
    toggleFullscreen,
  };
};

Requirements

React 16.8 or higher

Parameters

ref required

Type: HTMLVideoElement

The HTML video element to be managed.

Return values

isPaused

Type: boolean

Indicates whether the video is currently paused (true) or playing (false).

isMuted

Type: boolean

Indicates whether the video is currently muted (true) or unmuted (false).

currentVolume

Type: number

The current volume level of the video, ranging from 0 to 100.

currentTime

Type: number

The current playback position (in seconds) of the video.

play

Type: function

Play the video.

pause

Type: function

Pause the video.

togglePause

Type: function

Toggle between playing and pausing the video.

increaseVolume

Type: function

Increase the volume by a specified amount.

decreaseVolume

Type: function

Decrease the volume by a specified amount.

mute

Type: function

Mute the video.

unmute

Type: function

Unmute the video.

toggleMute

Type: function

Toggle between muting and unmuting the video.

forward

Type: function

Fast forward the video by a specified number of seconds.

back

Type: function

Rewind the video by a specified number of seconds.

toggleFullscreen

Type: function

Toggle fullscreen mode of the video.

Example

tsx
import { useRef } from "react";
import { useVideo } from "./hooks/useVideo";

const App = () => {
  const videoRef = useRef<HTMLVideoElement>(null);
  const {
    isPaused,
    isMuted,
    currentVolume,
    currentTime,
    play,
    pause,
    togglePause,
    increaseVolume,
    decreaseVolume,
    mute,
    unmute,
    toggleMute,
    forward,
    back,
    toggleFullscreen,
  } = useVideo(videoRef);

  return (
    <div>
      <video ref={videoRef} src="video.mp4" />
      <div>
        <button onClick={togglePause}>{isPaused ? "Play" : "Pause"}</button>
        <button onClick={toggleMute}>{isMuted ? "Unmute" : "Mute"}</button>
        <input
          type="range"
          min={0}
          max={100}
          value={currentVolume}
          onChange={(e) => increaseVolume(Number(e.target.value))}
        />
        <input
          type="range"
          min={0}
          max={videoRef.current?.duration || 0}
          value={currentTime}
          onChange={(e) =>
            videoRef.current &&
            (videoRef.current.currentTime = Number(e.target.value))
          }
        />
        <button onClick={() => forward(10)}>Forward</button>
        <button onClick={() => back(10)}>Back</button>
        <button onClick={toggleFullscreen}>Fullscreen</button>
      </div>
    </div>
  );
};

export default App;

Use cases

Here are some use cases where this React hook is useful:

  • Implementing custom video controls
  • Building a multimedia learning platform with interactive video playback controls
  • Creating a video carousel component with autoplay functionality
  • Developing a video editing tool with precise seeking and playback controls

Released under the MIT License.