HTML 5 Video Scripting with Javascript

Javascript: Scene List, Progress Bar, Fast Forward and Backward, Volume controls.


Language: Deutsch, English,   Index: entire TOC

Table of Contents


Introduction: the video tag, Converting Videos, HTML5 versus Flash
Flash Video Fallback: Ensuring Browser Compatibility
for enhanced navigation facilities enable Javascript.

Studies of a Flying Heron at Lake Ossiach

Our Video Divided into Scenes

Here again comes our heron with 600 kbit/s and 848x480 pixels. This time we will be able to fully disfruit the video by slow motion, one-frame-steps and a scene directory (as far as your browser does already support all of it).




Lautstärke: 100%

Attention: under Firefox 14.0.1 the slider for seeking to a given point in time does not work yet. Firefox 14.0.1 and Opera 12.01 do only support the standard playback speed, no slow motion and no rewinding. Safari or Konqueror (4.8.4: no rewind) on the other hand seem to be a good tip.





Reference; the video tag: Javascript Functions, Properties and Event Handlers

For all of you who are a little bit impatient and who just wanna know the methods, properties and event handlers used with the video object in order to set something up themselves quickly here comes a table with the most used functions sorted by topic. Of course we have also elaborated oncarrying methods and concepts.

basic features and properties
video.play() start to play (can jump to the beginning if already playing)
video.pause() pause (a video.pause() followed by a video.play() may not take the desired effect.)
video.paused true if the video is paused, false otherwise.
<video oncanplay='myfn()' initialization function myfn() is called as soon as the video has been loaded.
<video ontimeupdate='myfn()' trigger called while the video is playing and whenever there is a seek
video.duration the duration of the video in seconds
video.currentTime actual point in time of playback, seeks to the given point in time
video.videoWidth, video.videoHeight width and height of the shown frame or image
video.playBackRate 1 ~ default playback rate; 0.5 ~ half, 2 ~ doubled playback rate, -1,-2,-3,..: rewind
video.muted true whenever the sound has been disabled
video.volume volume as fraction or real number (value between 0 or 1)

A video tag can be inserted with document.createElement("VIDEO") into the document; its properties can be controlled via video.src, .autoplay, .loop, .controls und .preload (see my introductory article). video.addEventListener('oncanplay', fnname, false) does finally add an event listener to our new video object. "error" is another event listener for a function like function(e) { alert(e.code); }.

e.code
1 == MEDIA_ERROR_ABORTED the user has aborted fetching the video
2 == MEDIA_ERROR_NETWORK network error
3 == MEDIA_ERR_DECODE error at decodation time
4 == MEDIA_ERR_SRC_NOT_SUPPORTED media format not supported

You can detect the progress in fetching the video by video.networkState.

video.networkState
0 == NETWORK_EMPTY not yet initialized
1 == NETWORK_IDLE source chosen; not in fetching state
2 == NETWORK_LOADING actively fetches the source
3 == NETWORK_NO_SOURCE no source in a supported format can be spotted

According to the status field video.networkState the following events exist: loadstart, progress, suspend, abort, error, emptied, stalled.

video.currentSrc denotes the source from out of which the source is being fetched. video.canPlayType(type) == '' / 'maybe' / 'probably' determines whether the given format can not be loaded at all, can maybe or probably be loaded.

In order to start a video at the right time video.readyState exists:

video.readyState
0 == HAVE_NOTHING no data
1 == HAVE_METADATA duration, width, height and other metadata of the video have been fetched.
2 == HAVE_CURRENT_DATA There has not been sufficiently data loaded in order to start or continue playback.
3 == HAVE_FUTURE_DATA enough data to start playback
4 == HAVE_ENOUGH_DATA it should be possible to play the media stream without interruption till the end.

Again we have matching events like loadedmetadata, loadeddata, waiting, playing, canplay and canplaythrough. At last we do also have events for the given user actions: play, pause, timeupdate, ended, ratechange and durationchange.

It is even possible to determine whether a given time period has already been buffered, played or whether it is seekable (seek = jump to a certain point in time). buffered, played and seekable are TimeRanges objects. The number of time periods can be determined with timeranges.length; the start and end point of a given time frame with timeranges.start(i) and timeranges.end(i). We do additionally have the status flags seeking and ended which are set while seeking to a new position or whenever the end of the video has been reached. video.startTime at last gives the position from which on we have started the last playback time period.



Sourcecodes of the Embedded Video

Let us first have a look at the HTML control elements for the video. There is nothing really special to be noted except that the buttons have been labeled with Unicode characters. Some of the Unicode characters have been selected with kcharselect while others can be looked up by their name like the RIGHT-POINTING TRIANGLE.

Let us also have a short look at the embedded Javascript hooks of the video tag oncanplay and ontimeupdate as well as the onclick and change events of the slider, the option list and the push buttons.

<table class=VideoControls id=ReiherVideoControls> <tr><td> <video id=ReiherVideo preload=auto oncanplay='InitControls(this)' ontimeupdate='UpdateTime(this)'> <source src='data/Reiher-848x480-13064.mp4' type='video/mp4'> <source src='data/Reiher-848x480-13000kb.webm' type='video/webm'> <source src='data/Reiher-848x480-20000.ogg' type='video/ogg'> <!--source src='data/Reiher-848x480-600kb.mp4' type='video/mp4'> <source src='data/Reiher-848x480-600kb.webm' type='video/webm'> <source src='data/Reiher-848x480-600kb.ogg' type='video/ogg'--> </video> <td> <select id='Scenes' onchange='SelectScene(ReiherVideo,this)' size=8> <option value='0:00' selected id=StartScene>0:00 Abflug</option> <option value='0:03' id=SecondScene>0:03 Reiher über freiem Wasser</option> <option value='0:07'>0:07 Landeanflug</option> <option value='0:08.6'>0:08 Landung</option> <option value='0:10'>0:10 sitzend (am Baumstamm)</option> </select> <br><br> <input type=button id=butStop value='&#x25FC;' onclick='RewindVideo(ReiherVideo);'> <input type=button id=fstbwdbut value='&#x00AB;' onclick='PlayPause(ReiherVideo,this,-2)'> <input type=button id=butStepBwd value='&#x23B9;&#x2329;' onclick='FrameStep(ReiherVideo,this,-0.04)'> <input type=button id=snailbut value='~' onclick='PlayPause(ReiherVideo,this,0.2)'> <input type=button id=playbutton value='&#x25B6;' onclick='PlayPause(ReiherVideo,this,1)'> <input type=button id=butStepFwd value='&#x232A;&#x23B8;' onclick='FrameStep(ReiherVideo,this,+0.04)'> <input type=button id=fstfwdbut value='&#x00BB;' onclick='PlayPause(ReiherVideo,this,2)'> <br> <table><tr> <td><input type=range id=volume min=0 max=1 step=0.1 value=1 onchange='AdjustVolume(ReiherVideo,this.value)'> <td><input type=button value='&#x266B;' onclick='Mute(ReiherVideo,this)'> <td style="white-space: nowrap;">Lautstärke: <span id=VolumeTxt> 100% </span> </table> <tr><td> <input type=range id=videopos min=0 max=0 step=0.04 onchange='GotoPos(ReiherVideo,this.value)'> <!--progress value=3 min=0 max=10 id=videopos--> </table>

Our Javascript code is not an example of neat programming style but it it is short and powerful; all we need to present the potential of HTML5. We have always passed the video tag as a parameter if we should ever wish to have multiple videos with extended controls on the same page. However to do so we would also have to associate all buttons and controls with the video tag which could be done by an additional hash structure indexed by the id of the video. Hashes are simple objects in Javascript. We need back references to the controls because they change their appearance on play/pause. Alternatively one could devise a unique naming scheme for buttons or even search them live via DOM on initialization. Instead of using document.getElementById or document.all we have simply addressed all elements directly by their id.

Let us start by playing the video. We have the procedure PlayPause which calls PlayVideo and PauseVideo. We will never call video.play() and video.pause() directly because PlayVideo and PauseVideo need to adjust the button labels. video.play() should only be called if the video does not play yet because it may otherwise cause the player to start from the beginning. Even a video.pause() followed by a video.play() would not do what we want as the video would remain stopped (Apparently it takes some time for the .pause() action to take effect.).

If we click onto a scene in the scene list at the right side SelectScene is called. SelectScene does only need to set video.currentTime in order to make the jump. After that we call UpdateTime like each time after making a change to video.currentTime in order to make the time slider show the correct playback position (only exception to this rule is GotoPos which is called by direct clicks on the time slider so that there is no need to update the slider any more.).

UpdateTime is by the way also the hook which is directly called during video playback in order to update the time slider position. UpdateTime calls CheckForScene in sequence which sorts out the actually played scene basing on the playback position. It does so by accessing the DOM-tree of the scene selector where the option fields are child nodes listed in the correct order as they are displayed. Access to the DOM-tree is only necessary if we leave the current scene which is retained as long as our playback position lies withing curSceneStart and curSceneEnd

Somewhat tricky is also the implementation of the playback buttons because it is always the button with the currently selected playback speed that needs to turn into a pause button (We have different speeds like normal, fast forward, rewind and slow motion.). This can simply be done by resetting the labels of all buttons each time and by only turning the pressed button into a pause button afterwards.

The matching of the current playback speed requires ratedelta because the video.playbackRate may be adjusted a little bit whenever we set it especially when setting it to a real number (rounding).

The implementation of the other handles like Mute and AdjustVolume is very streight forward; I will leave their discovery to the reader.

In order not to leave you in doubt about the usability of the presented code: it is licensed by LGPL and may be used for free and of course free of any charge.

<script type='text/javascript'> function getTime(opt) { minsec = opt.value.split(':'); return parseFloat( minsec[0]*60 + minsec[1] ); } var curScene = StartScene; var curSceneStart = getTime(StartScene); var curSceneEnd = getTime(SecondScene); function CheckForScene(pos) { if( curSceneStart <= pos && pos <= curSceneEnd ) return; curSceneEnd = 0; nextscene = scene = null; for(sno in Scenes.childNodes) { cn = Scenes.childNodes[sno]; if(cn.tagName=="OPTION") { scene = nextscene; nextscene = Scenes.childNodes[sno]; curSceneStart = curSceneEnd; curSceneEnd = getTime(nextscene); if( curSceneStart <= pos && pos <= curSceneEnd && scene ) { scene.selected = true; return; } }} curSceneStart = curSceneEnd; curSceneEnd = ReiherVideo.duration; scene = nextscene; if( curSceneStart <= pos && pos <= curSceneEnd && scene ) scene.selected = true; } function InitControls(video) { videopos.max = video.duration - 1; videopos.style.width = video.videoWidth; // video.offsetWidth || video.innerWidth; StartScene.selected = true; //videopos.style.width = ReiherVideoControls.offsetWidth || ReiherVideoControls.innerWidth; //video = document.querySelector("VIDEO"); //videopos = document.getElementById(""); } var ratedelta = 0; var buttons = [ 'playbutton','fstbwdbut','fstfwdbut','snailbut' ]; var savedicon = {}; function PlayPause(video,but,rate) { if (video.paused||video.playbackRate + ratedelta != rate) PlayVideo(video,but,rate); else PauseVideo(video); } function RestoreButtons() { for(butno in buttons) { but_id = buttons[butno]; but = document.getElementById(but_id); if(but_id in savedicon) but.value = savedicon[but_id]; } } function PlayVideo(video,but,rate) { if(video.paused) video.play(); video.playbackRate = rate; ratedelta = rate - video.playbackRate; RestoreButtons(); if(!(but.id in savedicon)) savedicon[but.id] = but.value; but.value = String.fromCharCode('0x25AE','0x25AE'); } function PauseVideo(video) { video.pause(); RestoreButtons(); } function FrameStep(video,but,step) { PauseVideo(video); video.currentTime = video.currentTime + step; UpdateTime(video); } function RewindVideo(video) { PauseVideo(video); video.currentTime = 0; UpdateTime(video); } function UpdateTime(video) { videopos.value = video.currentTime; CheckForScene(video.currentTime);} function GotoPos(video,newpos) { video.currentTime = newpos; CheckForScene(newpos); } function SelectScene(video,selector) { minsec = selector.value.split(':'); video.currentTime = parseFloat( minsec[0]*60 + minsec[1] ); UpdateTime(video); //PlayVideo(video,playbutton,1); } function Mute(video,but) { video.muted = !video.muted; but.style.color = video.muted ? 'silver' : 'black'; } function AdjustVolume(video,value) { video.volume = value; VolumeTxt.innerHTML = Math.round(value*100)+'%' } </script>