In today’s multimedia web it’s becoming an ever increasing part of web design to include videos. Customizing the video player is one of the first things web designers think about when implementing these elements, as often the default player does not match the website, or merely for consistency across browsers and devices. In this tutorial we’re going to be going through exactly how to do that.

The Media Element API

The media element API is a simple API that allows you to manipulate videos with simple Javascript commands. For example, normally when using the HTML5 video tags we type something like this:

<video id="our-video" width="600" height="400" controls> <source src="movie.mp4" type="video/mp4"> <source src="movie-hd.mp4" type="video/mp4"> </video>

We have set controls in the video tag so this particular video will have controls (play, pause, etc). This is all well and good but sometimes we want to control the video with another (custom) player, or maybe we just want a link that makes the video play. To do this we use the Media Elements API. For example, lets say we have something like this:

<a href="#" class="play">Click me to play the video!</a>

Using the Media Elements API with jQuery we can write something as simple as this to make the video play:

$('#our-video')[0].play();

We’re using [0] here so we target a specific element (in the same way that document.getElementById() works!)

Easy, huh? Well it’s also possible to control a variety of video aspects with the media elements API, and you can create some pretty interesting things.

Functions

There are a bunch of features as part of the media elements API which can be used to control a video or how it is displayed. Here’s a bunch of things which become useful when creating a custom player:

$('#our-video')[0].play(); // Play the video $('#our-video')[0].pause(); // Pause the video $('#our-video')[0].volume = 1; // Sets volume, volume ranges from 0 to 1 $('#our-video')[0].currentTime; // Current video time $('#our-video')[0].duration; // Duration of video $('#our-video')[0].buffered; // Amount of video buffered in seconds if($('#our-video')[0].canPlayType('video/mp4')) { .. // If the video can play this type of format $('#our-video')[0].requestFullscreen; // (experimental) makes the video fullscreen

Using these simple functions and a few other tricks we’re going to combine all this to make a custom video player which you can change with just CSS.

Lets Get Started

First of all lets accustom ourselves with the HTML5 video tag. To create a new video element we just use the video tag. It’s best to use multiple video types since not all videos are supported by all browsers. Using just .webm and .mp4 will cover mostly every video type though. Miro provides a simple method to convert files to .webm.

<video width="600" height="340"> <source src="big-video.mp4" type="video/mp4"> <source src="movie.webm" type="video/webm"> </video>

The CSS

CSS is what we’re going to use to style the video. This opens up a lot of possibilities since CSS is very easy to use, and changing the style of your video, therefore, becomes very easy. For this particular video style this is the CSS I’m going to be using, so go ahead and put it in your CSS file:

body { font-size: 62.5%; } .player { background: #2a2a2a; box-sizing: border-box; border-radius: 5px; height: 70px; -moz-box-sizing: border-box; float: left; font-family: Arial, sans-serif; position: absolute; padding: 0; bottom: 20px; z-index: 2; opacity: 1; box-shadow: 0 0 10px rgba(0,0,0,0.3); -webkit-transition: opacity 0.3s ease-in; transition: opacity 0.3s ease-in; -moz-user-select: none; -webkit-user-select: none; user-select: none; } .video { position: relative; } .video:hover .player { opacity: 1; } .player .progress { width: 60%; height: 20px; border-radius: 5px; background: #676767; box-shadow: inset 0 -5px 10px rgba(0,0,0,0.1); float: left; cursor: pointer; margin: 24px 0 0 0; padding: 0; position: relative; } .player .progress-bar { background: #33b5d5; box-shadow: inset -30px 0px 69px -20px #89f6f5; border-radius: 5px; height: 100%; position: relative; z-index: 999; width: 0; } .player .button-holder { position: relative; left: 10px; } .player .progress-button { background: #fff; box-shadow: 0 0 20px rgba(0,0,0,0.3); border-radius: 30px; width: 20px; height: 20px; position: absolute; left: -20px; text-decoration: overline; } .player [class^="buffered"] { background: rgba(255,255,255,0.1); position: absolute; top: 0; left: 30px; height: 100%; border-radius: 5px; z-index: 1; } .player .play-pause { display: inline-block; font-size: 3em; float: left; text-shadow: 0 0 0 #fff; color: rgba(255,255,255,0.8); width: 10%; padding: 17px 0 0 3%; cursor: pointer; font-variant: small-caps; } .player .play, .player .pause-button { -webkit-transition: all 0.2s ease-out; } .player .play .pause-button, .player .pause .play-button { display: none; } .player .pause-button { padding: 5px 2px; box-sizing: border-box; -moz-box-sizing: border-box; height: 34px; } .player .pause-button span { background: #fff; width: 8px; height: 24px; float: left; display: block; } .player .pause-button span:first-of-type { margin: 0 4px 0 0; } .player .time { color: #fff; font-weight: bold; font-size: 1.2em; position: absolute; right: 0; top: 24px; } .player .stime, .ttime { color: #444; } .player .play:hover { text-shadow: 0 0 5px #fff; } .player .play:active, .pause-button:active span { text-shadow: 0 0 7px #fff; } .player .pause-button:hover span { box-shadow: 0 0 5px #fff; } .player .pause-button:active span { box-shadow: 0 0 7px #fff; } .player .volume { position: relative; float: left; width: 8%; margin: 0 0 0 4%; height: 100%; } .player .volume-icon { padding: 1.5%; height: 100%; cursor: pointer; box-sizing: border-box; -moz-box-sizing: border-box; -webkit-transition: all 0.15s linear; } .player .volume-icon-hover { background-color: #4f4f4f; } .player .volume-holder { height: 100px; width: 100%; background: black; position: absolute; display: none; background: #4f4f4f; left: 0; border-radius: 5px 5px 0 0; top: -100px; } .player .volume-bar-holder { background: #333; width: 20px; box-shadow: inset 0px 0px 5px rgba(0,0,0,0.3); margin: 15px auto; height: 80px; border-radius: 5px; position: relative; cursor: pointer; } .player .volume-button { background: #fff; box-shadow: 0 0 20px rgba(0,0,0,0.3); border-radius: 30px; width: 20px; height: 20px; } .player .volume-button-holder { position: relative; top: -10px; } .player .volume-bar { background: #33b5d5; box-shadow: inset -30px 0px 69px -20px #89f6f5; border-radius: 5px; width: 100%; height: 100%; position: absolute; bottom: 0; } .player .fullscreen { width: 12%; cursor: pointer; float: left; height: 100%; } .player .fullscreen a { width: 25px; height: 20px; border-radius: 3px; background: #fff; display: block; position: relative; top: 23px; margin: 0px auto; } .player .volume-icon span { width: 20%; height: 13%; background-color: #fff; display: block; position: relative; z-index: 1; font-weight: bold; top: 40%; color: #fff; left: 22%; } .player .volume-icon span:before, .player .volume-icon span:after { content: ''; position: absolute; } .player .volume-icon span:before { width: 0; height: 0; border: 1em solid transparent; border-left: none; border-right-color: #fff; z-index: 2; top: -2px; left: 10%; margin-top: -40%; } .player .volume-icon span:after { width: 2%; height: 2%; border: 1px solid #fff; left: 190%; border-width: 0px 0px 0 0; top: 5px; border-radius: 0 50px 0 0; -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -ms-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg); font-variant: small-caps; } .player .v-change-11 span:after { border-width: 10px 10px 0 0; top: 0; } .player .v-change-10 span:after { border-width: 9px 9px 0 0; top: 1px; } .player .v-change-9 span:after { border-width: 8px 8px 0 0; top: 1px; } .player .v-change-8 span:after { border-width: 7px 7px 0 0; top: 2px; } .player .v-change-7 span:after { border-width: 6px 6px 0 0; top: 2px; } .player .v-change-6 span:after { border-width: 5px 5px 0 0; top: 3px; } .player .v-change-5 span:after { border-width: 4px 4px 0 0; top: 3px; } .player .v-change-4 span:after { border-width: 3px 3px 0 0; top: 4px; } .player .v-change-3 span:after { border-width: 2px 2px 0 0; top: 4px; } .player .v-change-2 span:after { border-width: 1px 1px 0 0; top: 5px; } .player .v-change-1 span:after { border-width: 0px 0px 0 0; top: 5px; } .player .v-change-1 span:after { content: '+'; -webkit-transform: rotate(45deg); font-size: 20px; top: -6px; left: 25px; }

jQuery

The easiest and most succinct way to go about doing this is to use jQuery and design our code as a plugin. Don’t forget to include the jQuery file! As with all jQuery plugins, make a new Javascript file and paste in the following:

(function($) { $.fn.videoPlayer = function(options) { // videoPlayer is the name of our plugin var settings = { playerWidth : '0.95', // Default is 95% videoClass : 'video' // Video Class } // Extend the options so they work with the plugin if(options) { $.extend(settings, options); } // For each so that we keep chainability. return this.each(function() { // Code goes here }); } })(jQuery);

I’ve added a few customizable options (when you run this plugin you’ll be able to change the options to whatever you want). These include the width of the player for styling purposes (by default 0.95 or95%) and the video class (in case you need to change it because of clashes). All of our code is going to go inside the return this.each(function() { }); brackets.

Now the first thing we want to do is check that the video meta data is ready to run. In some cases this will take a moment to load, so if we don’t check this we’ll end up loading the video player without any data (that wouldn’t work!). We’re also going to set some basic variables, the purpose of which will become more apparent later on.

$(this)[0].addEventListener('loadedmetadata', function() { // Basic Variables var $this = $(this); var $settings = settings; // Wrap the video in a div with the class of your choosing $this.wrap('<div class="'+$settings.videoClass+'"></div>'); // Select the div we just wrapped our video in for easy selection. var $that = $this.parent('.'+$settings.videoClass); // Some other misc variables to check when things are happening var $mclicking = false, $vclicking = false, $vidhover = false, $volhover = false, $playing = false, $drop = false, $begin = false, $draggingProgess = false, $storevol, x = 0, y = 0, vtime = 0, updProgWidth = 0, volume = 0;

Next you want to create the structure of the player. This can be altered if you wish. We’ll also define some more variables and change the width of the player.

// The Structure of our video player { $( '<div class="player">' + '<div class="play-pause play">' + '<span class="play-button">►</span>' + '<div class="pause-button">' + '<span> </span>' + '<span> </span>' + '</div>' + '</div>' + '<div class="progress">' + '<div class="progress-bar">' + '<div class="button-holder">' + '<div class="progress-button"> </div>' + '</div>' + '</div>' + '<div class="time">' + '<span class="ctime">00:00</span>' + '<span class="stime"> / </span>' + '<span class="ttime">00:00</span>' + '</div>' + '</div>' + '<div class="volume">' + '<div class="volume-holder">' + '<div class="volume-bar-holder">' + '<div class="volume-bar">' + '<div class="volume-button-holder">' + '<div class="volume-button"> </div>' + '</div>' + '</div>' + '</div>' + '</div>' + '<div class="volume-icon v-change-0">' + '<span> </span>' + '</div>' + '</div>' + '<div class="fullscreen"> ' + '<a href="#"> </a>' + '</div>' + '</div>').appendTo($that); } // Set the width of the video $videoWidth = $this.width(); $that.width($videoWidth+'px'); // Set width of the player based on previously noted settings $that.find('.player').css({'width' : ($settings.playerWidth*100)+'%', 'left' : ((100-$settings.playerWidth*100)/2)+'%'}); // Video information var $spc = $(this)[0], // Specific video $duration = $spc.duration, // Video Duration $volume = $spc.volume, // Video volume currentTime; // The width of the progress bar var progWidth = $that.find('.progress').width();

Next lets start defining some functions. The first function we’re going to define is for buffering, so the user can see what sections of the video is buffered. The second is for time setting, so the time will increase correctly. The timing function is going to be run every time the current time is updated. I’ve commented in the code so you can see exactly what is happening.

var bufferLength = function() { // The buffered regions of the video var buffered = $spc.buffered; // Rest all buffered regions everytime this function is run $that.find('[class^=buffered]').remove(); // If buffered regions exist if(buffered.length > 0) { // The length of the buffered regions is i var i = buffered.length; while(i--) { // Max and min buffers $maxBuffer = buffered.end(i); $minBuffer = buffered.start(i); // The offset and width of buffered area var bufferOffset = ($minBuffer / $duration) * 100; var bufferWidth = (($maxBuffer - $minBuffer) / $duration) * 100; // Append the buffered regions to the video $('<div class="buffered"></div>').css({"left" : bufferOffset+'%', 'width' : bufferWidth+'%'}).appendTo($that.find('.progress')); } } } // Run the buffer function bufferLength(); // The timing function, updates the time. var timeUpdate = function($ignore) { // The current time of the video based on progress bar position var time = Math.round(($('.progress-bar').width() / progWidth) * $duration); // The 'real' time of the video var curTime = $spc.currentTime; // Seconds are set to 0 by default, minutes are the time divided by 60 // tminutes and tseconds are the total mins and seconds. var seconds = 0, minutes = Math.floor(time / 60), tminutes = Math.round($duration / 60), tseconds = Math.round(($duration) - (tminutes*60)); // If time exists (well, video time) if(time) { // seconds are equal to the time minus the minutes seconds = Math.round(time) - (60*minutes); // So if seconds go above 59 if(seconds > 59) { // Increase minutes, reset seconds seconds = Math.round(time) - (60*minutes); if(seconds == 60) { minutes = Math.round(time / 60); seconds = 0; } } } // Updated progress width updProgWidth = (curTime / $duration) * progWidth // Set a zero before the number if its less than 10. if(seconds < 10) { seconds = '0'+seconds; } if(tseconds < 10) { tseconds = '0'+tseconds; } // A variable set which we'll use later on if($ignore != true) { $that.find('.progress-bar').css({'width' : updProgWidth+'px'}); $that.find('.progress-button').css({'left' : (updProgWidth-$that.find('.progress-button').width())+'px'}); } // Update times $that.find('.ctime').html(minutes+':'+seconds) $that.find('.ttime').html(tminutes+':'+tseconds); // If playing update buffer value if($spc.currentTime > 0 && $spc.paused == false && $spc.ended == false) { bufferLength(); } } // Run the timing function twice, once on init and again when the time updates. timeUpdate(); $spc.addEventListener('timeupdate', timeUpdate);

Next the play button! We’re going to add and remove CSS classes to make the play change to a pause button when clicked. We’ll also set a variable to check if the video is playing, which we will use later in the code:

// When the user clicks play, bind a click event $that.find('.play-pause').bind('click', function() { // Set up a playing variable if($spc.currentTime > 0 && $spc.paused == false && $spc.ended == false) { $playing = false; } else { $playing = true; } // If playing, etc, change classes to show pause or play button if($playing == false) { $spc.pause(); $(this).addClass('play').removeClass('pause'); bufferLength(); } else { $begin = true; $spc.play(); $(this).addClass('pause').removeClass('play'); } });

One of the most complicated parts of this code is the progress slider. First off we need to check when the user clicks on the progress bar and then set a variable telling us that’s what’s happening. Then when the user moves the mouse we’ll be able to adjust it so it slides appropriately. We’re going to use the same process for the volume slider, so I’ve included it here too, as well as an animation function for the volume icon, as well as a way to check if the user is hovering over the volume icon using the jQuery hover() function.

// Bind a function to the progress bar so the user can select a point in the video $that.find('.progress').bind('mousedown', function(e) { // Progress bar is being clicked $mclicking = true; // If video is playing then pause while we change time of the video if($playing == true) { $spc.pause(); } // The x position of the mouse in the progress bar x = e.pageX - $that.find('.progress').offset().left; // Update current time currentTime = (x / progWidth) * $duration; $spc.currentTime = currentTime; }); // When the user clicks on the volume bar holder, initiate the volume change event $that.find('.volume-bar-holder').bind('mousedown', function(e) { // Clicking of volume is true $vclicking = true; // Y position of mouse in volume slider y = $that.find('.volume-bar-holder').height() - (e.pageY - $that.find('.volume-bar-holder').offset().top); // Return false if user tries to click outside volume area if(y < 0 || y > $(this).height()) { $vclicking = false; return false; } // Update CSS to reflect what's happened $that.find('.volume-bar').css({'height' : y+'px'}); $that.find('.volume-button').css({'top' : (y-($that.find('.volume-button').height()/2))+'px'}); // Update some variables $spc.volume = $that.find('.volume-bar').height() / $(this).height(); $storevol = $that.find('.volume-bar').height() / $(this).height(); $volume = $that.find('.volume-bar').height() / $(this).height(); // Run a little animation for the volume icon. volanim(); }); // A quick function for binding the animation of the volume icon var volanim = function() { // Check where volume is and update class depending on that. for(var i = 0; i < 1; i += 0.1) { var fi = parseInt(Math.floor(i*10)) / 10; var volid = (fi * 10)+1; if($volume == 1) { if($volhover == true) { $that.find('.volume-icon').removeClass().addClass('volume-icon volume-icon-hover v-change-11'); } else { $that.find('.volume-icon').removeClass().addClass('volume-icon v-change-11'); } } else if($volume == 0) { if($volhover == true) { $that.find('.volume-icon').removeClass().addClass('volume-icon volume-icon-hover v-change-1'); } else { $that.find('.volume-icon').removeClass().addClass('volume-icon v-change-1'); } } else if($volume > (fi-0.1) && volume < fi && !$that.find('.volume-icon').hasClass('v-change-'+volid)) { if($volhover == true) { $that.find('.volume-icon').removeClass().addClass('volume-icon volume-icon-hover v-change-'+volid); } else { $that.find('.volume-icon').removeClass().addClass('volume-icon v-change-'+volid); } } } } // Run the volanim function volanim(); // Check if the user is hovering over the volume button $that.find('.volume').hover(function() { $volhover = true; }, function() { $volhover = false; });

Next we have to do the actual mouse moving event. There are a bunch of things that depend on mouse move so the function is quite long. For instance, if the user is dragging the mouse while holding the progress bar, the progress bar should slide. Similarly, so should the volume bar. On top of that, if the user is moving the mouse and the video is already playing, the player should disappear so it isn’t a distraction to the actual video. I’ve included the entire function below, commented so you can see what’s going on. I’ve also included a function that fixes a few problems with the play and pause button, so the correct icon is always displaying.

// For usability purposes then bind a function to the body assuming that the user has clicked mouse // down on the progress bar or volume bar $('body, html').bind('mousemove', function(e) { // Hide the player if video has been played and user hovers away from video if($begin == true) { $that.hover(function() { $that.find('.player').stop(true, false).animate({'opacity' : '1'}, 0.5); }, function() { $that.find('.player').stop(true, false).animate({'opacity' : '0'}, 0.5); }); } // For the progress bar controls if($mclicking == true) { // Dragging is happening $draggingProgress = true; // The thing we're going to apply to the CSS (changes based on conditional statements); var progMove = 0; // Width of the progress button (a little button at the end of the progress bar) var buttonWidth = $that.find('.progress-button').width(); // Updated x posititon the user is at x = e.pageX - $that.find('.progress').offset().left; // If video is playing if($playing == true) { // And the current time is less than the duration if(currentTime < $duration) { // Then the play-pause icon should definitely be a pause button $that.find('.play-pause').addClass('pause').removeClass('play'); } } if(x < 0) { // If x is less than 0 then move the progress bar 0px progMove = 0; $spc.currentTime = 0; } else if(x > progWidth) { // If x is more than the progress bar width then set progMove to progWidth $spc.currentTime = $duration; progMove = progWidth; } else { // Otherwise progMove is equal to the mouse x coordinate progMove = x; currentTime = (x / progWidth) * $duration; $spc.currentTime = currentTime; } // Change CSS based on previous conditional statement $that.find('.progress-bar').css({'width' : $progMove+'px'}); $that.find('.progress-button').css({'left' : ($progMove-buttonWidth)+'px'}); } // For the volume controls if($vclicking == true) { // The position of the mouse on the volume slider y = $that.find('.volume-bar-holder').height() - (e.pageY - $that.find('.volume-bar-holder').offset().top); // The position the user is moving to on the slider. var volMove = 0; // If the volume holder box is hidden then just return false if($that.find('.volume-holder').css('display') == 'none') { $vclicking = false; return false; } // Add the hover class to the volume icon if(!$that.find('.volume-icon').hasClass('volume-icon-hover')) { $that.find('.volume-icon').addClass('volume-icon-hover'); } if(y < 0 || y == 0) { // If y is less than 0 or equal to 0 then volMove is 0. $volume = 0; volMove = 0; $that.find('.volume-icon').removeClass().addClass('volume-icon volume-icon-hover v-change-11'); } else if(y > $(this).find('.volume-bar-holder').height() || (y / $that.find('.volume-bar-holder').height()) == 1) { // If y is more than the height then volMove is equal to the height $volume = 1; volMove = $that.find('.volume-bar-holder').height(); $that.find('.volume-icon').removeClass().addClass('volume-icon volume-icon-hover v-change-1'); } else { // Otherwise volMove is just y $volume = $that.find('.volume-bar').height() / $that.find('.volume-bar-holder').height(); volMove = y; } // Adjust the CSS based on the previous conditional statmeent $that.find('.volume-bar').css({'height' : volMove+'px'}); $that.find('.volume-button').css({'top' : (volMove+$that.find('.volume-button').height())+'px'}); // Run the animation function volanim(); // Change the volume and store volume // Store volume is the volume the user last had in place // in case they want to mute the video, unmuting will then // return the user to their previous volume. $spc.volume = $volume; $storevol = $volume; } // If the user hovers over the volume controls, then fade in or out the volume // icon hover class if($volhover == false) { $that.find('.volume-holder').stop(true, false).fadeOut(100); $that.find('.volume-icon').removeClass('volume-icon-hover'); } else { $that.find('.volume-icon').addClass('volume-icon-hover'); $that.find('.volume-holder').fadeIn(100); } }); // When the video ends the play button becomes a pause button $spc.addEventListener('ended', function() { $playing = false; // If the user is not dragging if($draggingProgress == false) { $that.find('.play-pause').addClass('play').removeClass('pause'); } });

Next is a little function that checks if the user is clicking their mouse down onto the volume icon, and if so, whether to mute the video or not. We use our stored volume so we can set the volume to the previous volume should the user unmute the video.

// If the user clicks on the volume icon, mute the video, store previous volume, and then // show previous volume should they click on it again. $that.find('.volume-icon').bind('mousedown', function() { $volume = $spc.volume; // Update volume // If volume is undefined then the store volume is the current volume if(typeof $storevol == 'undefined') { $storevol = $spc.volume; } // If volume is more than 0 if($volume > 0) { // then the user wants to mute the video, so volume will become 0 $spc.volume = 0; $volume = 0; $that.find('.volume-bar').css({'height' : '0'}); volanim(); } else { // Otherwise user is unmuting video, so volume is now store volume. $spc.volume = $storevol; $volume = $storevol; $that.find('.volume-bar').css({'height' : ($storevol*100)+'%'}); volanim(); } });

Next, a quick little function that resets everything when the user lets go of the mouse button:

// If the user lets go of the mouse, clicking is false for both volume and progress. // Also the video will begin playing if it was playing before the drag process began. // We're also running the bufferLength function $('body, html').bind('mouseup', function(e) { $mclicking = false; $vclicking = false; $draggingProgress = false; if($playing == true) { $spc.play(); } bufferLength(); });

Finally we’re going to add a button that allows us to enable fullscreen mode. If the browser doesn’t support fullscreen the button will not be displayed, otherwise we just request it for every browser until one works.

Fullscreen is not supported by every browser. In the below example the fullscreen icon will not appear if the browser doesn’t support it.

// Check if fullscreen supported. If it's not just don't show the fullscreen icon. if(!$spc.requestFullscreen && !$spc.mozRequestFullScreen && !$spc.webkitRequestFullScreen) { $('.fullscreen').hide(); } // Requests fullscreen based on browser. $('.fullscreen').click(function() { if ($spc.requestFullscreen) { $spc.requestFullscreen(); } else if ($spc.mozRequestFullScreen) { $spc.mozRequestFullScreen(); } else if ($spc.webkitRequestFullScreen) { $spc.webkitRequestFullScreen(); } }); }); }); } })(jQuery);

Running the Plugin

And we’re done! That’s all the Javascript. Save the file and include it in your HTML, then run this little bit of Javascript to make it work:

$(document).ready(function() { $('video').videoPlayer({ 'playerWidth' : 0.95, 'videoClass' : 'video' }); });

And that’s it! Now it’s pretty simple to add more features to your video player via jQuery, or just change the general style of it with CSS. These features are widely supported in most browsers with the exception of fullscreen, but that will change with time. Thanks for reading and have a good day!