DEV Community

Cover image for How to Create a Video Player in React
Francisco Mendes
Francisco Mendes

Posted on

How to Create a Video Player in React

One of the things that interested me most recently is creating a fully customized video player. Obviously, nowadays we have services that provide widgets to be used on our websites.

Or, on the other hand, you already have dependencies that you can install and start using. But these facilities come at a price, which in this case is the absence or difficulty of customization.

That's why I had the idea of creating my own video player and apparently it's not as difficult as I had thought and in the end I found it fun.

Exactly for this reason I had the idea of writing this article, to explain step by step how to make a simple video player, but with the same logic you can go much further.

In today's example we are going to use this video, it has sound and is completely free.

Let's code

Today we are not going to use any external dependencies, so you will be fully familiar with everything.

Regarding the styling, at the end I will give the CSS code, this is because the focus of the article is to teach the logic behind how the video player works.

The first thing I ask you is to download the video mentioned above and then rename the file to video.mp4. Finally create a folder in your project called assets and drag the file into that folder.

So that we don't have the code in a single file, let's create our own hook that will be in charge of controlling the entire operation of our video player.

// @src/hooks/useVideoPlayer.js

const useVideoPlayer = () => {
  // ...
};

export default useVideoPlayer;
Enter fullscreen mode Exit fullscreen mode

In our hook we are going to use only two React hooks, useState() and useEffect().

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = () => {
  // ...
};

export default useVideoPlayer;
Enter fullscreen mode Exit fullscreen mode

Now we can start creating our state, which we'll call playerState. This state of ours will have four properties, isPlaying, isMuted, progress and speed.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = () => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });
  // ...
};

export default useVideoPlayer;
Enter fullscreen mode Exit fullscreen mode

One thing I want you to keep in mind is that our hook has to take a single argument which in this case will be the reference of our video, which we're going to name videoElement.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });
  // ...
};

export default useVideoPlayer;
Enter fullscreen mode Exit fullscreen mode

Now we can create our function that will dictate if the player is paused or not. For that, we'll keep the values of all the other properties of our playerState and we'll just say that whenever the function is executed it's to provide an inverse value of the current state of isPlaying.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });

  const togglePlay = () => {
    setPlayerState({
      ...playerState,
      isPlaying: !playerState.isPlaying,
    });
  };
  // ...
};

export default useVideoPlayer;
Enter fullscreen mode Exit fullscreen mode

Now we need to use useEffect() to pause or not the video through the value of the isPlaying property.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });

  const togglePlay = () => {
    setPlayerState({
      ...playerState,
      isPlaying: !playerState.isPlaying,
    });
  };

  useEffect(() => {
    playerState.isPlaying
      ? videoElement.current.play()
      : videoElement.current.pause();
  }, [playerState.isPlaying, videoElement]);
  // ...
};

export default useVideoPlayer;
Enter fullscreen mode Exit fullscreen mode

Now we have to create a function to help us know the video's progress, ie, by the duration of the video, we want the progress bar to show how much of the video we've seen.

For this we will create a function called handleOnTimeUpdate() so that we can calculate how much we have seen of the video with what remains to be seen. Afterwards we will keep the values of all the other properties of our state and we will only update the progress value.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });

  const togglePlay = () => {
    setPlayerState({
      ...playerState,
      isPlaying: !playerState.isPlaying,
    });
  };

  useEffect(() => {
    playerState.isPlaying
      ? videoElement.current.play()
      : videoElement.current.pause();
  }, [playerState.isPlaying, videoElement]);

  const handleOnTimeUpdate = () => {
    const progress = (videoElement.current.currentTime / videoElement.current.duration) * 100;
    setPlayerState({
      ...playerState,
      progress,
    });
  };
  // ...
};

export default useVideoPlayer;
Enter fullscreen mode Exit fullscreen mode

One of the things we're going to want to implement is the possibility that we can drag the progress bar so we can choose where we want to view the video.

This way we will create a function called handleVideoProgress() which will have a single argument which in this case will be the event.

Then we will convert our event value from string to number. This is because then we want to tell our videoElement directly that the current viewing time is equal to the value of our manual change. Finally, we just keep the values of all the other properties of our state and we update only the progress.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });

  const togglePlay = () => {
    setPlayerState({
      ...playerState,
      isPlaying: !playerState.isPlaying,
    });
  };

  useEffect(() => {
    playerState.isPlaying
      ? videoElement.current.play()
      : videoElement.current.pause();
  }, [playerState.isPlaying, videoElement]);

  const handleOnTimeUpdate = () => {
    const progress = (videoElement.current.currentTime / videoElement.current.duration) * 100;
    setPlayerState({
      ...playerState,
      progress,
    });
  };

  const handleVideoProgress = (event) => {
    const manualChange = Number(event.target.value);
    videoElement.current.currentTime = (videoElement.current.duration / 100) * manualChange;
    setPlayerState({
      ...playerState,
      progress: manualChange,
    });
  };
  // ...
};

export default useVideoPlayer;
Enter fullscreen mode Exit fullscreen mode

Another feature that we will want to implement is the video playback speed, this because I believe that not everyone is 1.0x fans and that there are guys that watch videos at 1.25x.

For that we will create a function called handleVideoSpeed() that will receive an event as a single argument, then the value of that event will be converted to number and finally we will tell the videoElement that the playback rate is equal to the event value.

In our state we keep the values of all properties except speed.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });

  const togglePlay = () => {
    setPlayerState({
      ...playerState,
      isPlaying: !playerState.isPlaying,
    });
  };

  useEffect(() => {
    playerState.isPlaying
      ? videoElement.current.play()
      : videoElement.current.pause();
  }, [playerState.isPlaying, videoElement]);

  const handleOnTimeUpdate = () => {
    const progress = (videoElement.current.currentTime / videoElement.current.duration) * 100;
    setPlayerState({
      ...playerState,
      progress,
    });
  };

  const handleVideoProgress = (event) => {
    const manualChange = Number(event.target.value);
    videoElement.current.currentTime = (videoElement.current.duration / 100) * manualChange;
    setPlayerState({
      ...playerState,
      progress: manualChange,
    });
  };

  const handleVideoSpeed = (event) => {
    const speed = Number(event.target.value);
    videoElement.current.playbackRate = speed;
    setPlayerState({
      ...playerState,
      speed,
    });
  };
  // ...
};

export default useVideoPlayer;
Enter fullscreen mode Exit fullscreen mode

The last feature I want to add is the ability to mute and unmute the video. And how you should calculate the logic is very similar to play/pause.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });

  const togglePlay = () => {
    setPlayerState({
      ...playerState,
      isPlaying: !playerState.isPlaying,
    });
  };

  useEffect(() => {
    playerState.isPlaying
      ? videoElement.current.play()
      : videoElement.current.pause();
  }, [playerState.isPlaying, videoElement]);

  const handleOnTimeUpdate = () => {
    const progress = (videoElement.current.currentTime / videoElement.current.duration) * 100;
    setPlayerState({
      ...playerState,
      progress,
    });
  };

  const handleVideoProgress = (event) => {
    const manualChange = Number(event.target.value);
    videoElement.current.currentTime = (videoElement.current.duration / 100) * manualChange;
    setPlayerState({
      ...playerState,
      progress: manualChange,
    });
  };

  const handleVideoSpeed = (event) => {
    const speed = Number(event.target.value);
    videoElement.current.playbackRate = speed;
    setPlayerState({
      ...playerState,
      speed,
    });
  };

  const toggleMute = () => {
    setPlayerState({
      ...playerState,
      isMuted: !playerState.isMuted,
    });
  };

  useEffect(() => {
    playerState.isMuted
      ? (videoElement.current.muted = true)
      : (videoElement.current.muted = false);
  }, [playerState.isMuted, videoElement]);
  // ...
};

export default useVideoPlayer;
Enter fullscreen mode Exit fullscreen mode

Finally, just return our state and all the functions that were created.

// @src/hooks/useVideoPlayer.js

import { useState, useEffect } from "react";

const useVideoPlayer = (videoElement) => {
  const [playerState, setPlayerState] = useState({
    isPlaying: false,
    progress: 0,
    speed: 1,
    isMuted: false,
  });

  const togglePlay = () => {
    setPlayerState({
      ...playerState,
      isPlaying: !playerState.isPlaying,
    });
  };

  useEffect(() => {
    playerState.isPlaying
      ? videoElement.current.play()
      : videoElement.current.pause();
  }, [playerState.isPlaying, videoElement]);

  const handleOnTimeUpdate = () => {
    const progress = (videoElement.current.currentTime / videoElement.current.duration) * 100;
    setPlayerState({
      ...playerState,
      progress,
    });
  };

  const handleVideoProgress = (event) => {
    const manualChange = Number(event.target.value);
    videoElement.current.currentTime = (videoElement.current.duration / 100) * manualChange;
    setPlayerState({
      ...playerState,
      progress: manualChange,
    });
  };

  const handleVideoSpeed = (event) => {
    const speed = Number(event.target.value);
    videoElement.current.playbackRate = speed;
    setPlayerState({
      ...playerState,
      speed,
    });
  };

  const toggleMute = () => {
    setPlayerState({
      ...playerState,
      isMuted: !playerState.isMuted,
    });
  };

  useEffect(() => {
    playerState.isMuted
      ? (videoElement.current.muted = true)
      : (videoElement.current.muted = false);
  }, [playerState.isMuted, videoElement]);

  return {
    playerState,
    togglePlay,
    handleOnTimeUpdate,
    handleVideoProgress,
    handleVideoSpeed,
    toggleMute,
  };
};

export default useVideoPlayer;
Enter fullscreen mode Exit fullscreen mode

Now we can start working on our App.jsx component and for the record the icon library used was Boxicons and the typography was DM Sans.

First I will give the css code of our App.css.

body {
  background: #EEEEEE;
}

.container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

h1 {
  color: white;
}

video {
  width: 100%;
}

.video-wrapper {
  width: 100%;
  max-width: 700px;
  position: relative;
  display: flex;
  justify-content: center;
  overflow: hidden;
  border-radius: 10px;
}

.video-wrapper:hover .controls {
  transform: translateY(0%);
}

.controls {
  display: flex;
  align-items: center;
  justify-content: space-evenly;
  position: absolute;
  bottom: 30px;
  padding: 14px;
  width: 100%;
  max-width: 500px;
  flex-wrap: wrap;
  background: rgba(255, 255, 255, 0.25);
  box-shadow: 0 8px 32px 0 rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  transform: translateY(150%);
  transition: all 0.3s ease-in-out;
}

.actions button {
  background: none;
  border: none;
  outline: none;
  cursor: pointer;
}

.actions button i {
  background-color: none;
  color: white;
  font-size: 30px;
}

input[type="range"] {
  -webkit-appearance: none !important;
  background: rgba(255, 255, 255, 0.2);
  border-radius: 20px;
  height: 4px;
  width: 350px;
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none !important;
  cursor: pointer;
  height: 6px;
}

input[type="range"]::-moz-range-progress {
  background: white;
}

.velocity {
  appearance: none;
  background: none;
  color: white;
  outline: none;
  border: none;
  text-align: center;
  font-size: 16px;
}

.mute-btn {
  background: none;
  border: none;
  outline: none;
  cursor: pointer;
}

.mute-btn i {
  background-color: none;
  color: white;
  font-size: 20px;
}
Enter fullscreen mode Exit fullscreen mode

Now we can start working on our component and for that we'll import everything we need, in this case it's our styling, our video and our hook.

// @src/App.jsx

import React from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  // ...
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Then we'll import the useRef() hook to create our videoElement's reference. Like this:

// @src/App.jsx

import React, { useRef } from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  const videoElement = useRef(null);
  // ...
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Then we can get our playerState and each of our functions from our hook. Like this:

// @src/App.jsx

import React, { useRef } from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  const videoElement = useRef(null);
  const {
    playerState,
    togglePlay,
    handleOnTimeUpdate,
    handleVideoProgress,
    handleVideoSpeed,
    toggleMute,
  } = useVideoPlayer(videoElement);
  // ...
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Now we can finally start working on our template, this way we will start working our video element which will have three props, the source will be our video and we will still pass our reference and our handleOnTimeUpdate() function.

// @src/App.jsx

import React, { useRef } from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  const videoElement = useRef(null);
  const {
    playerState,
    togglePlay,
    handleOnTimeUpdate,
    handleVideoProgress,
    handleVideoSpeed,
    toggleMute,
  } = useVideoPlayer(videoElement);
  return (
    <div className="container">
      <div className="video-wrapper">
        <video
          src={video}
          ref={videoElement}
          onTimeUpdate={handleOnTimeUpdate}
        />
        // ...
      </div>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Now we can start working on our video controls, let's start with the play and pause button. To which we will pass the togglePlay() function and we will do a conditional rendering, so that it shows the indicated icons according to the value of the isPlaying property.

// @src/App.jsx

import React, { useRef } from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  const videoElement = useRef(null);
  const {
    playerState,
    togglePlay,
    handleOnTimeUpdate,
    handleVideoProgress,
    handleVideoSpeed,
    toggleMute,
  } = useVideoPlayer(videoElement);
  return (
    <div className="container">
      <div className="video-wrapper">
        <video
          src={video}
          ref={videoElement}
          onTimeUpdate={handleOnTimeUpdate}
        />
        <div className="controls">
          <div className="actions">
            <button onClick={togglePlay}>
              {!playerState.isPlaying ? (
                <i className="bx bx-play"></i>
              ) : (
                <i className="bx bx-pause"></i>
              )}
            </button>
          </div>
          // ...
        </div>
      </div>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Now we can start by working on our input, which will be of the range type, which will have a minimum value of zero and a maximum value of one hundred. In the same way we will pass the handleVideoProgress() function and the value of the progress property.

// @src/App.jsx

import React, { useRef } from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  const videoElement = useRef(null);
  const {
    playerState,
    togglePlay,
    handleOnTimeUpdate,
    handleVideoProgress,
    handleVideoSpeed,
    toggleMute,
  } = useVideoPlayer(videoElement);
  return (
    <div className="container">
      <div className="video-wrapper">
        <video
          src={video}
          ref={videoElement}
          onTimeUpdate={handleOnTimeUpdate}
        />
        <div className="controls">
          <div className="actions">
            <button onClick={togglePlay}>
              {!playerState.isPlaying ? (
                <i className="bx bx-play"></i>
              ) : (
                <i className="bx bx-pause"></i>
              )}
            </button>
          </div>
          <input
            type="range"
            min="0"
            max="100"
            value={playerState.progress}
            onChange={(e) => handleVideoProgress(e)}
          />
          // ...
        </div>
      </div>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Now we are going to work on the element to select our video playback speed. To which we will pass the value of the speed property and handleVideoSpeed() function.

// @src/App.jsx

import React, { useRef } from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  const videoElement = useRef(null);
  const {
    playerState,
    togglePlay,
    handleOnTimeUpdate,
    handleVideoProgress,
    handleVideoSpeed,
    toggleMute,
  } = useVideoPlayer(videoElement);
  return (
    <div className="container">
      <div className="video-wrapper">
        <video
          src={video}
          ref={videoElement}
          onTimeUpdate={handleOnTimeUpdate}
        />
        <div className="controls">
          <div className="actions">
            <button onClick={togglePlay}>
              {!playerState.isPlaying ? (
                <i className="bx bx-play"></i>
              ) : (
                <i className="bx bx-pause"></i>
              )}
            </button>
          </div>
          <input
            type="range"
            min="0"
            max="100"
            value={playerState.progress}
            onChange={(e) => handleVideoProgress(e)}
          />
          <select
            className="velocity"
            value={playerState.speed}
            onChange={(e) => handleVideoSpeed(e)}
          >
            <option value="0.50">0.50x</option>
            <option value="1">1x</option>
            <option value="1.25">1.25x</option>
            <option value="2">2x</option>
          </select>
          // ...
        </div>
      </div>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Last but not least we will have the button that will be responsible for mute and unmute the video. To which we will pass the toggleMute() function and we will do conditional rendering to show the indicated icons according to the isMuted property.

// @src/App.jsx

import React, { useRef } from "react";
import "./App.css";

import video from "./assets/video.mp4";
import useVideoPlayer from "./hooks/useVideoPlayer";

const App = () => {
  const videoElement = useRef(null);
  const {
    playerState,
    togglePlay,
    handleOnTimeUpdate,
    handleVideoProgress,
    handleVideoSpeed,
    toggleMute,
  } = useVideoPlayer(videoElement);
  return (
    <div className="container">
      <div className="video-wrapper">
        <video
          src={video}
          ref={videoElement}
          onTimeUpdate={handleOnTimeUpdate}
        />
        <div className="controls">
          <div className="actions">
            <button onClick={togglePlay}>
              {!playerState.isPlaying ? (
                <i className="bx bx-play"></i>
              ) : (
                <i className="bx bx-pause"></i>
              )}
            </button>
          </div>
          <input
            type="range"
            min="0"
            max="100"
            value={playerState.progress}
            onChange={(e) => handleVideoProgress(e)}
          />
          <select
            className="velocity"
            value={playerState.speed}
            onChange={(e) => handleVideoSpeed(e)}
          >
            <option value="0.50">0.50x</option>
            <option value="1">1x</option>
            <option value="1.25">1.25x</option>
            <option value="2">2x</option>
          </select>
          <button className="mute-btn" onClick={toggleMute}>
            {!playerState.isMuted ? (
              <i className="bx bxs-volume-full"></i>
            ) : (
              <i className="bx bxs-volume-mute"></i>
            )}
          </button>
        </div>
      </div>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

The end result should look like this:

final app

Conclusion

As always, I hope you found it interesting. If you noticed any errors in this article, please mention them in the comments. 🥳

Hope you have a great day! 🙌

Top comments (27)

Collapse
 
code_rabbi profile image
Emeka Orji

Thanks @franciscomendes10866 , this helped me in a project I am working on, Thanks again

Collapse
 
carlosjara profile image
Carlos Jaramillo

I really appreciate your time in this post!, Thank you.

Collapse
 
georgewl profile image
George WL

That feels like too many useStates, and could more easily be combined into a single reducer and/or a custom hook

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

Good tip, the code is definitely easier to read. Thank you! 🙌

Collapse
 
jackieli123723 profile image
Jackieli

css style not work github code ?

Collapse
 
georgewl profile image
George WL

How so do you mean?

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

It is not working? In which component?

Collapse
 
beepboop profile image
Kode Creer

This isn't working in the progress bar even after pasting your code into the temple and the same libraries. Please provide a GitHub link please, it makes solving these issues easier.

Collapse
 
cristian327 profile image
David P.

Css my friend

Collapse
 
georgewl profile image
George WL

I would recommend instead of using a large state object, you instead make use of the useReducer hook, this would also simplify your handlers by bundling them into the reducer

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

It's a good approach 🧐 Thanks 🙌

Collapse
 
nikoteresin profile image
spooner.eth

hi, somehow the video is not showing on mobile safari / chrome. anyone who had the same problem and a solution?

Collapse
 
tinapc profile image
Hung Nguyen

Thanks a lot bro! This article is really good and I learned more thing from it.

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

Thank you very much! I am happy to know! 💪

Collapse
 
cyberfolks profile image
M Ibrahim Hayat

i want to add full Screen button is there any solution for full screen?

Collapse
 
nosovandrew profile image
Andrew Nosov

Hi! You can make it by adding to useVideoPlayer hook following func:

const toggleFullscreen = () => {
    if (videoElement.current) {
            videoElement.current.requestFullscreen();
    }
}
Enter fullscreen mode Exit fullscreen mode

Then you can run this func by clicking on the desired icon.