Let’s Build: With JavaScript – HTML5 Video Player

Welcome to the next installment to my Let’s Build: With JavaScript series. This tutorial teaches you how to create, customize, and manipulate an HTML5 video player using vanilla JavaScript.

I’ll be using a combination of HTML, CSS, and JavaScript to accomplish this task which is a bit more advanced than the previous installments in this series.

I got the initial idea for manipulating an HTML5 Video element from Wes Bos who did a very similar video tutorial in is JavaScript 30 course. I highly recommend giving his course a go as it teaches you modern tips and tricks for JavaScript workflow.

Much of the credit for the concept and code structure goes to Wes on this one. I wanted to take a stab at doing something similar with my own flair. I used different styles, naming conventions, and more to accomplish what you see on the final CodePen.

View Source Code

Let’s get coding:

The very first step to getting a video element on the page is actually rendering one using the newish video tag.

HTML

My final HTML looks like the following:

<div class="player">
  <video class="player-video" src="https://staging.coverr.co/s3/mp4/Down_by_the_River.mp4"></video>

  <div class="player-controls">

    <div class="progress">
      <div class="filled-progress"></div>
    </div>

        <div class="ply-btn">
        <button class="player-btn toggle-play" title="Toggle Play">
        <svg class="" width="16" height="16" viewBox="0 0 16 16"><title>play</title><path d="M3 2l10 6-10 6z"></path></svg>
        </button>
    </div>

    <div class="sliders">
    <input type="range" name="volume" class="player-slider" min="0" max="1" step="0.05" value="1">

    <input type="range" name="playbackRate" class="player-slider" min="0.5" max="2" step="0.1" value="1">
    </div>

    <button data-skip="-10" class="player-btn">« 10s</button>

    <button data-skip="10" class="player-btn">10s »</button>

  </div>
</div>

We need a variety of controls to manipulate certain parts of the video element’s API. Within each video element there are a huge amount of properties we can access with JavaScript to manipulate. Everything from volume to playbackRate can be adjusted with a few lines of code.

The HTML above contains

  • a containing .player div which we’ll use for styling.
  • a video element with a src attribute referenced
  • a .player-controls div of which will have all the controls we want
  • a .play-btn div for wrapping the play/pause icons we’ll make use of
  • input range sliders for controlling volume and playback rate speeds
  • buttons for skipping ahead and backward

These controls and elements may seem daunting to think about having to manipulate with JavaScript but most of the logic will ultimately deal with user interaction. This means our JavaScript will rely heavily on event listeners to do anything and everything.

CSS

While I called it CSS above, I’ll actually be using SCSS to write my styles. You’ll need a precompiler to write something similar. Check out the codepen for compiled styles if you need to.

$accent-color: #FFEC41;

body {
  align-items: center;
  background: #000046;
  background: linear-gradient(to right, #1CB5E0, #000046); 
  display: flex;
  height: 100vh;
  justify-content: center;
  margin: 0;
  padding: 0;
}

.player {
  max-width: 800px;
  border: 6px solid rgba(255, 255, 255, 0.2);
  box-shadow: 0 0 25px rgba(0, 0, 0, 0.1), 0 3px 3px rgba(0, 0, 0, 0.1);
  position: relative;
  overflow: hidden;

  &:hover {
    .progress {
      height: 10px;
    }

    .player-controls {
      transform: translateY(0);
    }
  }
}

.player:-webkit-full-screen,
.player:fullscreen {
  max-width: none;
  width: 100%;
}

.play-btn {
  flex: 1;
}

.player-video {
  width: 100%;
  display: block;
}

.player-btn {
  background: none;
  border: 0;
  color: white;
  text-align: center;
  max-width: 60px;
  padding: 5px 8px;

  svg {
    fill: #FFFFFF;
  }

  &:hover,
  &:focus {
    border-color: $accent-color;
    background: rgba(255, 255, 255, .2);
  }
}

.player-slider {
  width: 10px;
  height: 30px;
}

.player-controls {
  align-items: center;
  display: flex;
  position: absolute;
  bottom: 0;
  width: 100%;
  transform: translateY(100%) translateY(-5px);
  transition: all 0.3s;
  flex-wrap: wrap;
  background: rgba(0, 0, 0, 0.3);
}

.player-controls > * {
  flex: 1;
}

.progress {
  position: relative;
  display: flex;
  flex: 10;
  flex-basis: 100%;
  height: 4px;
  transition: height 0.3s;
  background: rgba(0, 0, 0, 0.5);
}

.filled-progress {
  width: 50%;
  background: $accent-color;
  flex: 0;
  flex-basis: 50%;
}

.sliders {
  max-width: 200px;
  display: flex;
}

input[type=range] {
  -webkit-appearance: none;
  background: transparent;
  width: 100%;
  margin: 0 5px;
}

input[type=range]:focus {
  outline: none;
}

input[type=range]::-webkit-slider-runnable-track {
  width: 100%;
  height: 8px;
  cursor: pointer;
  box-shadow: 1px 1px 1px rgba(0, 0, 0, 0), 0 0 1px rgba(13, 13, 13, 0);
  background: rgba(255, 255, 255, 0.5);
  border-radius: 10px;
  border: 0.2px solid rgba(1, 1, 1, 0);
}

input[type=range]::-webkit-slider-thumb {
  height: 15px;
  width: 15px;
  border-radius: 50px;
  background: white;
  cursor: pointer;
  -webkit-appearance: none;
  margin-top: -3.5px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}

input[type=range]:focus::-webkit-slider-runnable-track {
  background: rgba(255, 255, 255, 0.8);
}

input[type=range]::-moz-range-track {
  width: 100%;
  height: 8px;
  cursor: pointer;
  box-shadow: 1px 1px 1px rgba(0, 0, 0, 0), 0 0 1px rgba(13, 13, 13, 0);
  background: #ffffff;
  border-radius: 10px;
  border: 0.2px solid rgba(1, 1, 1, 0);
}

input[type=range]::-moz-range-thumb {
  box-shadow: 0 0 3px rgba(0, 0, 0, 0), 0 0 1px rgba(13, 13, 13, 0);
  height: 15px;
  width: 15px;
  border-radius: 50px;
  background: white;
  cursor: pointer;
}

A lot of browser default controls for buttons, input ranges, and more are just plain ugly. This code makes up for that as well as sets a max width on our video to keep it centered on the page. Feel free to go nuts here and write your own styles.

The JavaScript

Our JavaScript will focus on an object-oriented approach. This means that we’ll create a lot of little functions that do mostly one thing. Combined these will make the video a fully featured component. Separated, it will make it easier to understand what is going on for any other developer who might see the code. Most of the iteractions on the video itself are invoked by event listeners (listing for user interaction). We’ll write functions that do things when specific actions are captured. The final logic is here:

const player = document.querySelector('.player');
const video = player.querySelector('.player-video');
const progress = player.querySelector('.progress');
const progressFilled = player.querySelector('.filled-progress');
const toggle = player.querySelector('.toggle-play');
const skippers = player.querySelectorAll('[data-skip]');
const ranges = player.querySelectorAll('.player-slider');

// Logic
function togglePlay() {
  const playState = video.paused ? 'play' : 'pause';
  video[playState](); // Call play or paused method 
}

function updateButton() {
  const togglePlayBtn = document.querySelector('.toggle-play');

  if(this.paused) {
    togglePlayBtn.innerHTML = `<svg class="" width="16" height="16" viewBox="0 0 16 16"><title>play</title><path d="M3 2l10 6-10 6z"></path></svg>`;  
  } else {
    togglePlayBtn.innerHTML = `<svg width="16" height="16" viewBox="0 0 16 16"><title>pause</title><path d="M2 2h5v12H2zm7 0h5v12H9z"></path></svg>`;
  }
}

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

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

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

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

// Event listeners
video.addEventListener('click', togglePlay);
video.addEventListener('play', updateButton);
video.addEventListener('pause', updateButton);
video.addEventListener('timeupdate', progressUpdate);

toggle.addEventListener('click', togglePlay);
skippers.forEach(button => button.addEventListener('click', skip));
ranges.forEach(range => range.addEventListener('change', rangeUpdate));
ranges.forEach(range => range.addEventListener('mousemove', rangeUpdate));

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

As you can see, the amount of event listeners we need here gets pretty daunting but nevertheless, the whole things work quite well. Be sure to follow along in the video for complete context. The written versions are for context purposes but also just for reference in case you’d rather reference the code directly rather than codepen.

Thanks so much for watching/reading. There’s much more to come. In case you’re new here be sure to check out the other videos in this series listed below:

Let’s Build: With JavaScript Series