JavaScript30 - Day 11 (Custom Video Player)

This is day 11 in my #javascript30 journey. This is the free course from Wes Bos that lets you brush up on your JavaScript skills via 30 projects.

Yesterday we discovered how to listen for Shift to Check Multiple Checkboxes. You can keep track of all the projects we're building here.

Today we're exploring how to create a custom HTML5 Video Player.


Day 11 - Custom HTML5 Video Player

Customising the default video player is next to impossible. The easiest way around this is to create our own.

Our custom HTML5 Video player will hide the native controls and instead give the user access to:

  • The volume.
  • The speed of the video playback.
  • Skip forward 25 seconds.
  • Skip backward 10 seconds.
  • Pause/Play the video on click.

This is a big job and will be the first of our projects to have a separate scripts.js document.

There will be three main parts to our scrips.js document:

  • Getting our elements from the page.
  • Building our functions.
  • Hooking up the event listeners.
Get Elements from Page

This is something that we have done many times before. Select the elements from the page using querySelector.

We will need to select the:

  • Player
  • Video
  • Player Controls
    • Progress bar
    • Play/Pause Toggle
    • Volume
    • Playback Rate
    • Skip back
    • Skip forward

This looks like:

// Get our elements
const player = document.querySelector('.player')  
//dget the elements within the player
const video = player.querySelector('.viewer')  
const progress = player.querySelector('.progress')  
const progressBar = player.querySelector('.progress__filled')  
const toggle = player.querySelector('.toggle')  
//get the elements with multiple instances
const skipButtons = player.querySelectorAll('[data-skip]')  
const ranges = player.querySelectorAll('.player__slider')  
Hook up the Play/Pause button

The first function that we need to create is the 'Play/Pause' toggle. This needs to be called any time the user clicks on the video OR the toggle button.

function togglePlay() {  
  const method = video.paused ? 'play' : 'pause'
  video[method]()
}

video.addEventListener('click', togglePlay)  
toggle.addEventListener('click', togglePlay)  

We also need to provide some visual feedback to the user that the video has been paused or is now playing. This can be done by toggling the icon shown on the toggle button:

function updatePlayButton() {  
  const icon = this.paused ? '►' : '❚ ❚'
  toggle.textContent = icon
}

video.addEventListener('play', updatePlayButton)  
video.addEventListener('pause', updatePlayButton)  

This looks like:

Video can pause and play

Hook up the Skip Buttons

We want the user to be able to skip forwards or backwards in the video. To do this we will need to take note of the current position in the video and alter it by the requested number of seconds either forward or backwards.

The HTML for our skip buttons looks like this:

<button data-skip="-10" class="player__button">« 10s</button>  
<button data-skip="25" class="player__button">25s »</button>  

You can see the data-skip variable has either a negative or positive number set as the value. This determines whether the video should skip forwards or backward. Having the skip controls based on this custom variable means we can set any button with a variable to skip through the video.

The script for making this work looks like:

const skipButtons = player.querySelectorAll('[data-skip]')

function skip() {  
  video.currentTime += parseFloat(this.dataset.skip)
}

skipButtons.forEach(button => button.addEventListener('click', skip))  

First, we select all of the buttons that have the data-skip variable on them. Then we cycle through all of them and add an event listener. From there we call the skip function that takes the current video time and adds the skip amount to it.

Volume & Playback Speed Controls

Next, we can implement the volume and playback rate controls. To do this we need to listen for a change on those sliders. Then we can throw this into a function to handle the change.

This looks like this:

function handleRangeUpdate() {  
  if (this.name === 'volume') {
    video.volume = this.value
  }
  if (this.name === 'playbackRate') {
    video.playbackRate = this.value
  }
}

ranges.forEach(range => range.addEventListener('change', handleRangeUpdate))  

This was my first attempt at this and it works perfectly but we can modify the function to be a heck of a lost cleaner:

function handleRangeUpdate() {  
  video[this.name] = this.value
}

Holy heck that is much nicer.

Progress Bar

The progress bar serves two purposes. First, it shows the user where they are up to in the video. Second, it allows the user to skip forward to a specific section in the video.

To hook this up we need to:

  • Select the progress bar from the DOM.
  • Hook up an event listener.
  • Create a function to update it based on the video progress.
  • Create a function to update the video progress based on the progress bar changes.

The styling on our progress bar is controlled by the flex-basis:

.progress__filled {
  width:50%;
  background:#ffc600;
  flex:0;
  flex-basis:50%;
}

When we progress through the video we need to update this flex-basis value to show this to the user. We will use the timeupdate event that happens on the video to trigger this update.

function handleProgress() {  
  const percent = video.currentTime / video.duration * 100
  progressBar.style.flexBasis = `${percent}%`
}

video.addEventListener('timeupdate', handleProgress)  

Now we need to make a function that listens for a click on the progress bar. When this happens the video should jump to the specific point in the video. To find the specific point in the video we need to calculate where on the progress bar the click happened and what percentage of the progress bar that works out to be:

function scrub(e) {  
  const scrubTime = e.offsetX / progress.offsetWidth * video.duration
  video.currentTime = scrubTime
}

progress.addEventListener('click', scrub)  

This is all good for when a user clicks but if a user clicks and drags on the progress bar we want to be able to handle this too. We can do this by adding some logic to the event listeners:

let mousedown = false  
progress.addEventListener('click', scrub)  
progress.addEventListener('mousemove', e => mousedown && scrub(e))  
progress.addEventListener('mousedown', () => (mousedown = true))  
progress.addEventListener('mouseup', () => (mousedown = false))  

Now that everything has been done I have pushed this project live to a Firebase project. You can view it live here.

You can keep track of all the projects in this JavaScript30 challenge here.