Watching and sharing video content is one of the most common uses of the web, and the way video is embedded on a webpage as evolved over the years. Nowadays, adding video files to a webpage is as easy as using the
<video> element which works in all modern browsers and supports a variety of video formats.
The main caveat is that the interface for the video player that is rendered varies on different browsers which is not ideal if you want to provide a consistent user experience. This is why it is useful to build your own interface that features custom controls rather than just using the browser defaults.
The player we’ll be building in this tutorial looks a lot like the one that is found on YouTube, because I decided it would be good to replicate some of the functionality that is in something most people are already familiar with.
We won’t be implementing all the features that is found on the YouTube player because that will make for a longer, more complicated tutorial. However, once you’ve completed this tutorial, I’m confident that you will be able to plug in any new functionality with ease.
I’ve prepared the starter files for this tutorial on Github. You need to clone it to your machine and open the project directory in your text editor. You will find the markup and styles for the player in the included HTML and CSS file, as well as the video file that we’re testing the player with. The included
index.js file is where we’ll hook up all the functionality needed for the player to work.
npm install to install
browser-sync as a development dependency for starting a web server and automatically refreshing the browser when any of the files change, followed
npm start in the terminal to open up the project in your browser.
What has been done so far
The video player uses the native browser controls at the moment which works just as you’d expect. The markup for our custom controls have already been defined in the
#video-controls element, but they’re hidden by default.
Even though we’re going to implement a custom interface for the controls, it is a good idea to include the
controls attribute on the
I have also defined a poster image is defined for the video, and the
preload attribute is set to metadata, which instructs the browser to initially fetch only the video metadata (such as duration). To keep things simple, I’ve only included one source file for the video in MP4 format as it’s supported by all major browser and is a pretty safe default. See this document for more information on video formats and browser compatibility.
Replace native controls with custom interface
The first thing we’ll do is to hide the browser’s controls and provide our own interface once we determine that the browser supports HTML5 video. Enter the piece of code below into your
index.js file to make that happen:
canPlayType method is how we are able to detect support for a video format in the browser. To use it, we need to create a
<video> element instance and check if it supports the
canPlayType method. If it does, it is safe to assume that HTML5 video is supported so the default controls are promptly disabled in favour of our custom controls.
Play or Pause the video
Let’s start with the basics. You should be able to play and pause the video by clicking on the play button and the icon should change to match the state of the video.
We’ll start by selecting the video and the play button at the top of
index.js as follows:
Then we’ll create a function that toggles the playback state of the video:
Finally, create an event listener that runs the
togglePlay function when
playButton is clicked:
Easy enough right? Test it out by clicking the play button in your browser. It should play and pause the video appropriately.
This actually sets the tone for the rest of the tutorial. We’ll usually select one of the video controls, create a function that implements a specific functionality, then hook it up to the control via an event listener.
Let’s continue now to update the play icon depending on the state of the video. This is the HTML for the
We already have both the play and pause icons in the SVG, but only displaying one at a time by hiding the other one (notice the
hidden class). What we need to do now is toggle the hidden class on or off on each icon so that the appropriate icon is shown depending on the state of the video.
First select the icons at the top:
Next, create the function to update the play button under
And finally, add the event listeners at the bottom:
So what this does is, when the video is played or paused, the
updatePlayButton function is executed which toggles the
hidden class on each button. Since we have the
hidden class on the pause icon by default, once the video is played, this icon will be displayed and the play icon will be hidden. If the video is paused again, the reverse occurs. You can test this in your browser.
One more thing we need to do is update the text in the tooltip that appears when you hover over the play button. It reads
play (k) by default, but we need to update it so it reads
pause (k) when the video is playing.
k is the keyboard shortcut that we’ll add to play or pause a video later in the tutorial.
updatePlayButton as shown below:
That should set the appropriate text in the tooltip once you hover on the button when the video is playing or paused.
If you’re wondering how the tooltip is shown, I’m using the
::before psuedo-element in the button and setting its contents to the value of the
data-title attribute. This is the relevant CSS:
Show video duration and time elapsed
It’s necessary to display the length of a video as that’s one of the first things that a user would like to see, so we’ll do that next.
This is the markup for the duration and time elapsed:
Select both controls in your
index.js file as follows:
We’ll show the total duration of the video once the page loads using the video’s
duration property. This property represents the number of seconds of the video, so we’ll need to convert this number to minutes and seconds first before we can display it. To this end, create a
formatTime function that takes in the time in seconds and converts it to minutes and seconds:
Next, create an
initializeVideo function below
As shown above, the video’s duration in seconds is rounded to the nearest integer, formatted to minutes and seconds, and updated on the screen. We also updated the
datetime attribute to a time string that represents the duration of the video.
Now, hook up the
initializeVideo function to the video’s
loadedmetadata event as shown below. This will cause the video duration to be updated when the video’s metadata has been loaded.
Next, we need to update the time elapsed when the video is being played. Here’s the function that helps us achieve what we want:
The event we need to listen for on the video is the
timeupdate event. This event is fired whenever the time indicated by the video’s
currentTime property has been updated.
The above code ensures that once the video’s
currentTime is updated by virtue of playing the video, the elapsed time is also updated appropriately.
Update the progress bar
The next thing we’ll do is to update the progress bar as the video is being played. Here’s the markup for the progress bar:
Here, we have the
progress element which is apt for displaying the progress of any task, while the range input will allow us to scrub through the video quickly and seamlessly. I’ve styled both elements so that they’re the same width and height, and the range input is made transparent (except for the thumb which is the same colour as the value inside the progress bar).
If you’re curious, you can dig into the CSS to find out how I did it. This is kind of a hack to make the progress bar look like it’s a singular element, but I feel it’s justified for our use case.
min attribute on both is set to 0, and the
value attribute indicates the current value of both elements. They also need a
max attribute which will be set to the duration of the video in seconds which is from
video.duration as already demonstrated above. We can do this in the
initializeVideo function, but we need to select the elements first:
initalizeVideo as shown below:
Now the range for both the progress element and range input is between 0 and the video duration in seconds as indicated by the
max attributes on both elements. This allows us to easily sync the progress bar with the range input at any point in time as you’ll see.
Let’s go ahead and update the values of the aforementioned elements when the video is being played so that the progress bar becomes functional. Create a new
updateProgress function below:
Then add a new
timeupdate event listener on the
video element below the first one:
Refresh your browser, and try it out. You should see the progress bar update as the video is being played.
Most video players allow you to click on the progress bar to jump to a particular point in the video, and ours is not going to be any different. First we need to select the tooltip element:
Then add a function that displays the timestamp in the
seekTooltip when the cursor is over the progress bar:
This function uses the position of the cursor on the
seek element to roughly work out where in the range input the user is hovering on, and stores the position in a
data-seek attribute while updating the tooltip to reflect the timestamp at that position.
Hook up the
updateSeekTooltip function to the
mousemove event on the
seek control to see it the effect in action:
Now, once the value of
seek element is changed either by clicking or dragging the thumb, we want the video to jump to the time set in the
data-seek attribute. Create a new
skipAhead function below
This function will be executed when the value of the
seek element changes which we can monitor using the
input event. We then get the value of the
data-seek attribute and check if it exists. If it does, we grab the value and update the video’s elapsed time and the progress bar to that value. If the
data-seek property does not exist (for example, on mobile), the value of the
seek element is used instead.
This creates the effect of jumping ahead to a different position in the video.
In the above snippet, you can find markup for all the volume related controls. We have a button that represents the volume icon depending on the state of the video’s volume, and a range input that controls the volume of the video.
The first thing we need to do is update the volume of the video when the value of the
#volume input changes. We also need to update the icon to reflect the current volume of the video.
You’ll see that the volume ranges from 0 to 1, and each step in the input increases the volume by 0.1. It has been set this way to make it consistent with the video’s volume property which also ranges from 0 to 1 where 0 is the lowest volume, and 1 is the highest.
Go ahead and select the button, icons and input in your
Next, create a new
updateVolume function to update the volume as soon as the volume input is changed:
And hook it up to the
volume element as follows:
At this point, you will notice the volume decrease when you slide the range to the left, and vice versa. We need to add another function to update the icon whenever the volume changes:
When this function runs, all the icons are hidden, then one of them is displayed depending on which condition evaluates to true.
We can run
updateVolumeIcon each time the volume changes by listening for the
volumechange event as follows:
This is what you should get in your browser after making this change:
One more thing we need to add is the ability to mute and unmute the video by clicking on the volume icon. We’ll create a new
toggleMute function for this purpose:
Then run the function when the
volumentButton is clicked:
This function toggles the state of the
muted property on the video to true or false. When the video is muted, the volume is stored in a
data-volume attribute on the
volume element, so that when the video is unmuted, we can restore the state of the volume to its previous value.
Here’s how that looks in practice:
Click the video to play or pause it
In many video player applications, clicking on the video itself is often a quicker way to play or pause the video. So let’s make that possible in our application.
All we need to do is add listen for the
click event on the
video and run the
togglePlay function when the event fires:
While this works, let’s make it more interesting by adding a bit of feedback when you play or pause a video by clicking on it just like the way it’s done on YouTube or Netflix.
Here’s the HTML for our animation:
And here’s the relevant CSS:
By default, the
.playback-animation element is made completely transparent using the
opacity property. To replicate the animation from YouTube, we’ll make use of the Web Animation API to animate the opacity and scale of this element.
Select it first at the top of
Then create the following function below the other functions in the file:
animate method takes in an array of keyframe objects and an options object where you can control the duration of the animation amongst other things.
Now, add a second the
click event listener on the
The effect is that you now see a short animation when you play or pause the video by clicking on it.
Next, let’s make the full-screen button functional. To make the video full-screen (including the controls) we have to select the
.video-container element and ask the browser to place it (and its descendants) in full-screen.
Select both the button and video container in your
Then create a new
And add a
click event listener on the
fullScreenButton as shown below:
toggleFullScreen function checks if the document is in full-screen mode first, if so it exits back to window mode. Otherwise, it places the
videoContainer in full-screen.
One more thing we need to do in this section is update the full-screen icon and the text in the tooltip that appears when you hover on the button. First, select the icons:
Then create a function to update the button when
videoContainer goes in and out of full-screen mode:
updateFullscreenButton to the
onfullscreenchange event handler on the
And it works as expected! Test it in your browser, or see the GIF below.
Add Picture-In-Picture support
The Picture-in-Picture (PiP) API allows users to watch videos in a floating window (always on top of other windows) so they can keep an eye on what they’re watching while interacting with other sites, or applications.
At this point in time, this API is only supported in a handful browsers so we have to use feature detection to hide the PiP button in browsers that do not support it so that users don’t see a button that they can’t use.
Here’s the code that helps us achieve that. Add it below the other event listeners.
As we have been doing throughout this tutorial, we need to select the relevant control first:
Then create the function that toggles Picture-in-Picture mode:
I’ve made the
togglePip function asynchronous so that we can catch errors in case the
requestPictureInPicture() method rejects, which can happen for a number of reasons. In a real word app, you will want to show an error message to the user instead of logging it to the console.
Next, listen for the
click event on the
pipButton and add the
togglePip function as the handler for the event.
Now, clicking the
pipButton should enter or exit Picture-in-Picture mode. You can also close the PiP window by clicking on the close button in the top right.
Show and hide controls appropriately
The controls at the bottom of the video take up some space, and blocks the user’s view of some of the content. It is better to hide them when they are not in use, and show them again on hovering over the video.
Write the two functions below for this purpose:
What we want to do here is hide the controls when the cursor leaves the video interface. But we want to make sure the controls are always shown when the video is paused, hence the conditional in
To acheive this, we use the
onmouseleave event handlers on both the
video element and the
videoControls as shown below:
Add support for keyboard shortcuts
The last feature which we’ll be adding in this tutorial is the ability to use the keyboard to control the video playback. It’s really just a matter of running the functions we’ve already written when a specific key is pressed. The shortcuts we’ll implement here are as follows:
k: Play or pause the video
m: Mute or unmute the video
f: Toggle fullscreen
p: Toggle Picture-in-Picture mode
What we’ll do here is listen for the
keyup event on the document, detect the key that was pressed, and run the relevant functions for key.
We use a switch statement above to detect which key was pressed, and then run the relevant code. The reason
hideControls is called after 2 seconds is to imitate the behaviour on YouTube where, when using the keyboard shortcut to play the video, the controls do not hide immediately once the video starts playing but do so after a short delay.
There are so many ways to improve the video player, but the tutorial was already getting too long so I had to stop here. If you’re interested in extending the functionality of the player, here are some ideas:
- Add support for captions and subtitles
- Add speed support
- Add the ability to fast-forward or rewind the video
- Add ability to choose video resolution (720p, 480p, 360p, 240p)
I hope this tutorial was helpful to you. If you have any questions, please leave a comment below and I’ll get back to you. Don’t forget to checkout the full source code on Github.
Thanks for reading!