HTML5 Video Player For WebKit

Posted by in Code

I had to play some videos in Chrome recently, but the player had to be custom for the project. Rather than tinker with some pre-made solutions out there, I instead decided to do this myself and see what I could accomplish (disclaimer: I’ve actually written something like this a few times before, but I never got around to writing about it).

Looking Good

The end goal was to achieve something that looks like this (in a modal overlay):

The main challenge I could see would be the progress bar. Being that I only had WebKit browsers with which to concern myself, my first instinct was to use the <progress> element. However, is it possible to style this element with CSS?

Absolutely sort of! As long as you’re using a WebKit browser, it turns out that you can use the vendor-specific selector pseudo element ::-webkit-progress-bar-value (relevant Stack Overflow post) to style the value portion of the element in addition to the actual element itself. I’m sure the other engines will follow suit eventually, but it’s just WebKit for now.

Here’s the HTML structure I ended up with:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="video-player-wrapper">
    <div class="video-player">
        <video>
            <source src="/video.ogv" type='video/ogg; codecs="theora, vorbis"'>
        </video>
        <div class="video-player-bar">
            <button class="video-player-play"></button>
            <span class="video-player-timer">0.00</span>
            <progress class="video-player-seek">0.00</progress>
            <span class="video-player-duration">0.00</span>
            <a class="video-player-knob"></a>
        </div>
    </div>
    <a class="video-player-skip"></a>
</div>

As a side note, I put an .ogv video file in there just to remind you that they are the preferred format for HTML5 videos. In my experience, other formats such as .mp4 will occasionally freeze Chrome and probably make you sad. I’ve encountered some issues with .webm, too, while .ogv and .m4v have yet to halt my browser.

Here’s the CSS to make things look like they should:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
.video-player-wrapper {
    top:0;
    left:0;
    width:100%;
    height:100%;
    z-index:9999;
    color:#FFFFFF;
    position:absolute;
    background:rgba(0,0,0,.85);
    -webkit-user-select:none;
}
    .video-player-wrapper .video-player-skip {
        width:40px;
        height:40px;
        border:2px solid #FFFFFF;
        border-radius:20px;
        background:#000000;
        position:absolute;
        text-align:center;
        font-size:30px;
    }
        .video-player-wrapper .video-player-skip::before {
            content:'x';
        }
.video-player {
    position:absolute;
    overflow:hidden;
}
.video-player-bar {
    position:absolute;
    bottom:0;
    left:0;
    width:100%;
    height:65px;
    background:-webkit-linear-gradient(left,rgba(0,0,0,.7) 0px,rgba(0,0,0,.7) 95px,transparent 95px,transparent 98px,rgba(0,0,0,.7) 98px);
    -webkit-transition:bottom .4s ease-in;
}
    .video-player-bar.hide {
        bottom:-65px;
    }
    .video-player-knob {
        display:block;
        position:absolute;
        background:#FFF;
        border-radius:22px;
        width:44px;
        height:44px;
        bottom:46px;
        left:161px;
        opacity:.6;
        -webkit-box-shadow:0 0 3px rgba(0,0,0,.8);
        -webkit-transition:opacity .4s ease-in;
    }
        .video-player-knob::before {
            content:'';
            display:block;
            width:1px;
            background:#FFF;
            height:16px;
            bottom:-16px;
            left:21px;
            position:absolute;
        }
        .video-player-bar.hide .video-player-knob {
            opacity:0;
        }
    .video-player-bar .video-player-play {
        display:inline-block;
        height:100%;
        width:95px;
        border:none;
        -webkit-box-sizing:border-box;
        padding:0;
        margin-right:16px;
        background:transparent;
        text-align:center;
    }
        .video-player-bar .video-player-play:before, .video-player-bar .video-player-play:after {
            content:'';
            display:inline-block;
        }
        .video-player-bar .video-player-play.play:before {
            border-style:solid;
            border-width:12px;
            border-color:transparent transparent transparent #FFF;
            margin:5px 0 0 9px;
        }
        .video-player-bar .video-player-play.pause:before, .video-player-bar .video-player-play.pause:after {
            width:5px;
            height:24px;
            background:#FFF;
            margin:5px 3px 0 3px;
        }
    .video-player-bar span {
        font-size:21px;
        width:50px;
        margin:23px 11px 0 11px;
        display:inline-block;
        vertical-align:top;
        text-shadow:0 0 4px rgba(0,0,0,.8);
    }
.video-player-bar .video-player-seek {
    height:4px;
    width:100px;
    border-radius:2px;
    background:#FFF;
    margin:31px 10px 0 0;
    vertical-align:top;
    -webkit-appearance:none;
}
    .video-player-bar .video-player-seek::-webkit-progress-bar-value {
        background:-webkit-linear-gradient(top, #37d0ff, #35addb);
        border-radius:2px;
    }

You’ll notice all the special CSS3 things like box shadows and gradients are WebKit-specific. Other vendor prefixes for Mozilla and Opera (and the CSS3 standard) can be added if necessary.

The jQuery Magic

Although not written as a jQuery plug-in (which will probably happen at some point down the line), this puppy does use it to make things easier on the JavaScript side of things.

First is to set up the videoPlayer object. This includes extending user options with the defaults, instantiating the object status variables, and creating and adding the new DOM elements:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var $this = this;

this.settings = {autoOpen:true, autoPlay:true, autoClose:true, controlTimeout:3500, skip:function(){ $this.close(); }, close:function(){}, open:function(){}};
$.extend(this.settings, options);

this.opened = false;
this.htimer = null;
this.seeking = false;
this.control = false;

this.$elem = $elem.css('pointer-events','none');
this.video = $elem.get(0);
this.$play = $('<button class="video-player-play pause">');
this.$time = $('<span class="video-player-timer">0.00</span>');
this.$seek = $('<progress class="video-player-seek" value="0" max="0">0%</progress>');
this.$maxt = $('<span class="video-player-duration">0.00</span>');
this.$ctrl = $('<div class="video-player-bar hide">');
this.$knob = $('<a class="video-player-knob">');
this.$skip = $('<a class="video-player-skip">');
this.$skin = $('<div class="video-player">').width($elem.width()).height($elem.height()).append($elem).append($this.$ctrl.append($this.$play).append($this.$time).append($this.$seek).append($this.$maxt).append($this.$knob));
this.$wrap = $('<div class="video-player-wrapper">').css('display','none').appendTo('body').append(this.$skin).append(this.$skip);

Most of the methods like play() and pause() are pretty self-explanatory, but setTime() and setPosition() are somewhat complex:

1
2
3
4
5
6
7
8
9
10
11
12
13
this.setTime = function(time){
    time = parseFloat(time);
    var vduration = parseFloat($this.$elem.attr('duration'));
    if(time > vduration) time = vduration;
    else if(time < 0) time = 0;
    $this.$elem.attr('currentTime',time);
    $this.$time.html($this.formatTime(time));
    $this.$seek.val(time).html(time / vduration);
    $this.$knob.css('left',($this.$seek.offset().left - $this.$skin.offset().left) + ($this.$seek.width() * (time / vduration)) - ($this.$knob.width() / 2));
};
this.setPosition = function(pageX) {
    $this.setTime(parseFloat((pageX - $this.$seek.offset().left) / $this.$seek.width()) * parseFloat($this.$elem.attr('duration')));
};

setPosition() takes an x-position of the page and converts it to a time within the range of the video’s duration. If the x-position is to the left of the seek bar, the time is set to zero. If the x-position is to the right of the seek bar, the time is set to the video’s duration (and subsequently will stop). If the x-position is within the left and right x-positions of the seek bar, then we take its position ratio against the video’s duration and uses setTime() to set the place in the video playback.

One other thing that might jump out at you is the createSeek():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Things don't really start until the video is ready, i.e. video.readyState == true
var createSeek = function(){
    if($this.$elem.attr('readyState')) {
        var updateTime = function(ev){
            if(!$this.seeking) {
                $this.setTime($this.$elem.attr('currentTime'));
                if($this.$elem.attr('currentTime') >= $this.$elem.attr('duration')) {
                    $this.stop();
                    if($this.settings.autoClose) $this.close();
                }
            }
        };
        $this.$seek.attr('max', $this.$elem.attr('duration'));
        $this.$maxt.html($this.formatTime($this.$elem.attr('duration')));
        $this.$elem.bind('timeupdate', updateTime);
        if($this.settings.autoOpen) $this.open();
        updateTime();
    }
    else {
        setTimeout(createSeek, 150);
    }
};
createSeek();

This function isn’t called ever again (or at least shouldn’t be) and is truly only used for object instantiation. You have to wait for the video DOM element to return a readyState attribute so you know the video is loaded and ready to play. Otherwise, you just set a timeout to keep checking the readyState until the video is ready so you can apply the updateTime() function to the timeupdate event of the video.

Lastly, there’s just a little bit of fun geometry. It moves the skip/close button to the very upper-right corner of the video player.

This is done by finding the distance from the center of the element to one of its corners (its a square in the DOM even though it renders as a circle), subtracting the radius of the circle (which is also half the width of the square), and using the resulting value as the hypotenuse of an isosceles right triangle whose other sides are used as offsets from the top right corner of the video player.

1
2
3
4
5
// Move the skip/close button to the very top right corner of the player
var w = this.$skip.width(),
    n = (Math.sqrt(Math.pow(w, 2) + Math.pow(w, 2)) / 2) - (w / 2),
    o = Math.sqrt(Math.pow(n, 2) / 2);
this.$skip.offset({left:(parseInt(this.$skin.css('left')) + this.$skin.width()) - o, top:parseInt(this.$skin.css('top')) - this.$skip.height() + o});

In Conclusion

That’s that! If you want to read about how to use it, check out my GitHub for this particular little doodad. There are still a few things left to do, or at least things I’d like to eventually get around to doing since in its current state, this guy does exactly what I need it to do.

  • Add volume controls – it’s simple enough. It works almost like the seek bar except it controls the volume.
  • Turn into a jQuery plug-in – it currently relies on jQuery, so I should probably make it a jQuery plug-in instead of calling it as a new object (that, or just get rid of the dependency).
  • Improved seek controls – if you jump around too fast, the events fire out of order and the video will go back to its pre-seek position after mouse release. Currently, you can avoid this by going a bit slower or by moving the mouse slightly either direction before releasing.
  • Expand to other HTML5 browsers – currently only supports WebKit browsers even though <video> elements belong to HTML5 in general, so it would make sense to make this plug-in do the same.

View the demo (requires a WebKit browser). Video is from Oceans.