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;
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;
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;
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;
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;
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;
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;
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;
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;
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;
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;
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;
}
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;
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;
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;
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;
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;
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;
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;
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;
The end result should look like this:
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)
Thanks @franciscomendes10866 , this helped me in a project I am working on, Thanks again
I really appreciate your time in this post!, Thank you.
That feels like too many useStates, and could more easily be combined into a single reducer and/or a custom hook
Good tip, the code is definitely easier to read. Thank you! 🙌
css style not work github code ?
How so do you mean?
It is not working? In which component?
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.
Css my friend
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
It's a good approach 🧐 Thanks 🙌
hi, somehow the video is not showing on mobile safari / chrome. anyone who had the same problem and a solution?
Thanks a lot bro! This article is really good and I learned more thing from it.
Thank you very much! I am happy to know! 💪
i want to add full Screen button is there any solution for full screen?
Hi! You can make it by adding to useVideoPlayer hook following func:
Then you can run this func by clicking on the desired icon.