How to build an Unsplash Search App with Svelte 3
Use Svelte 3 and the Intersection Observer API to build an Unsplash Search App. A great exercise for developing your Svelte skills.
This guide will teach you how to create a cross-browser HTML5 video player with JavaScript using the Media and Fullscreen APIs.
Watching and sharing video content is one of the most common uses of the web,
and the way video is embedded on a webpage has evolved over the years.
Nowadays, adding video files to a webpage can be achieved using the <video>
element which works in all modern browsers and supports a variety of video
formats.
The main caveat when using <video>
is that the rendered video player will vary
depending on the browser 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 instead of using the browser defaults.
In this tutorial, I’ll take you through building a custom video player with JavaScript. The purpose is describe how to leverage the HTML5 Media API in the browser to build an experience that improves on the defaults.
The player we’ll be building in this tutorial will look a lot like the one that is found on YouTube because I think it’s a good idea to replicate some of the functionality that is in something most people are already familiar with.
We won’t be implementing all the features found in the YouTube player as 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.
You can view a live demo of what we’ll be building, or check out the source code on GitHub.
You need to have a basic understanding of JavaScript and the DOM to be able to move forward with this tutorial. I also recommend using the latest version of Chrome as some of the features we’ll be adding (such as Picture-in-Picture) work only in Chrome at the time of writing.
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 index.html
and style.css
files
respectively, as well as the video file that we’ll be testing the player with.
The included index.js
file is where we’ll add all the code necessary for the
player to work.
Run npm install
in the terminal to install
browser-sync as a development
dependency for starting a web server and automatically refreshing the page
when any of the files change, followed by npm start
to open up the project in
your browser on http://localhost:3000.
At the moment, the video player retains the native browser controls which works
just as you’d expect. The markup for the 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’s a
good idea to retain the controls
attribute on the <video>
element so that
users that have JavaScript turned off for whatever reason will still have access
to the browser’s native video controls. For everyone else, the native controls
can be hidden easily and replaced with custom controls as will be demonstrated
shortly.
A poster image has also been added to the video, and the preload
attribute is
set to metadata
which instructs the browser to fetch only the video metadata
(such as duration). To keep things simple, only one source file in the MP4 has been included for the video in MP4 format
because 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.
The first thing we need to do is to hide the default video controls and provide
our own interface once it is determined that the browser supports HTML5 video. Enter the following code snippet into your
index.js
file to make that happen:
The canPlayType
property is how we are able to detect support for a video
format in the browser. To use it, we need to create an instance of the
<video>
element 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.
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 executes 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 via an event listener.
Let’s move on to update the play icon depending on the state of the video. This
is the HTML for the playButton
:
We have both the play and pause icons in the <svg>
element, but only one is
dispyed at a time while the other is hidden. What we need to do now is toggle
the hidden class on each icon so that the appropriate icon is shown depending on
the state of the video.
First, select the icons at the top of index.js
:
Next, create the function to update the play button under togglePlay
:
And finally, add the following event listeners at the bottom:
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 extra thing to do is updating 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.
Update the 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, here’s the relevant CSS:
It’s necessary to display the length of a video as that’s one of the first things that users would like to see, so we’ll do that next.
Here 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 need to convert this number to minutes and seconds before
we can display it. Create a formatTime
function that takes in the
time in seconds and converts it to minutes and seconds:
Next, create an initializeVideo
function below formatTime
:
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. The
datetime
attribute is also updated to a time string that represents the
duration of the video.
Next, let’s 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.
Following that, let’s 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 playback time indicated by the video’s currentTime
property is 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.
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. It’s 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.
The min
attribute on both is set to zero, 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:
Then update initalizeVideo
as shown below:
Now, the range for both the progress element and range input is between zero and
the video duration in seconds as indicated by the min
and 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 to display the timestamp in the tooltip element 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:
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 updateSeekTooltip
:
This function will be executed when the value of the seek
element changes can
be monitored 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 (on mobile for example), 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
element changes. We also need to update the icon to reflect the
current volume of the video.
As you can see, volume input ranges from 0 to 1, and each step in the input increases the volume by 0.01. It is 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 index.js
file:
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 executed, 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 volumeButton
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:
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 player.
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 index.js
:
Then create the following function below the all other functions in index.js
:
The 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 video
:
video.addEventListener('click', animatePlayback);
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 index.js
file:
Then create a new toggleFullScreen
function:
And add a click
event listener on the fullScreenButton
as shown below:
The 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
element 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 the videoContainer
goes in and
out of full-screen mode:
Finally, assign updateFullscreenButton
to the onfullscreenchange
event
handler on the videoContainer
element:
And it works as expected! Test it in your browser or see the GIF below.
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 may 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.
The video controls 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 when 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 hideControls()
.
To achieve this, we’ll use the onmouseenter
and onmouseleave
event handlers
on both the video
element and the videoControls
as shown below:
The last feature we’ll add to our player 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 videom
: Mute or unmute the videof
: Toggle fullscreenp
: Toggle Picture-in-Picture modeWhat 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.
A switch statement is used above to detect which key was pressed and then
execute the relevant code. The reason hideControls
is called after two 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:
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, and happy coding!
Comments
Ground rules
Please keep your comments relevant to the topic, and respectful. I reserve the right to delete any comments that violate this rule. Feel free to request clarification, ask questions or submit feedback.