HTML 5 Video Scripting mit Javascript

Javascript: Wiedergabepunktliste, Fortschrittsbalken, schnelles Vor- und Zurückspulen, Lautstärke regulieren.


Studien eines Reiherfluges am Ossiacher See

Unser Video unterteilt in einzelne Szenen

Hier ist er noch einmal unser Reiher (600 kbit/s, 848x480). Diesmal werden wir durch eine Abspieloption im Schneckentempo sowie Einzelbildschritte in den vollen Genuß des Videos kommen, sofern ihr Browser alle geforderten Funktionen bereits unterstützt.




Lautstärke: 100%

Achtung: unter Firefox 14.0.1 funktioniert der Slider zum Setzen des Wiedergabezeitpunktes am unteren Rand und der für die Lautstärke noch nicht. Firefox 14.0.1 und Opera 12.01 unterstützen zudem nur die Standardwiedergabegeschwindigkeit, keine Slow-Motion und kein Zurückspulen. Safari oder Konqueror (4.8.4: kein Zurückspulen) hingegen haben sich stets als guter Tipp erwiesen.





Referenz: Javascript Funktionen, Eigenschaften und Eventhandler des video-Tags

Für alle die etwas ungeduldig sind und wissen wollen, welche Methoden, Eigenschaften und Eventhandler das video-Objekt zur Verfügung stellt hier eine Referenz absteigend nach Präzedenz und Themenzusammenhang sortiert. Damit kann man schon einmal selber loslegen ohne viel fremden Code lesen zu müssen. Natürlich sind auch weiterführende Eigenschaften und Fähigkeiten aufgelistet um die bestehende Implementierung erweitern zu können.

grundlegende Eigenschaften und Fähigkeiten
video.play() mit dem Abspielen beginnen (kann zum Start zurückspringen wenn das Video bereits spielt.)
video.pause() Pausieren (ein video.play() direkt nach einem video.pause() ist unter Umständen wirkungslos.)
video.paused true wenn das Video angehalten ist, false wenn es spielt.
<video oncanplay='myfn()' Initialisiergsroutine myfn() wird aufgerufen sobald das Video geladen worden ist.
<video ontimeupdate='myfn()' Trigger der aufgerufen wird während das Video spielt und wanimmer die Wiedergabeposition geändert wird
video.duration die Dauer des Videos in Sekunden
video.currentTime aktuelle Wiedergabeposition in Sekunden
video.videoWidth, video.videoHeight Breite und Höhe des gezeigten Bildausschnittes
video.playBackRate 1 ~ Originalwiedergabegeschwindigkeit; 0.5 ~ halbe, 2 ~ doppelte Geschwindigeit, -x: Zurückspulen
video.muted true wenn Ton ausgeschalten (kein Audio)
video.volume Lautstärke als Bruchteil der Gesamtlautstärke (Wert zwischen 0 und 1)

Ein video-Tag kann mit document.createElement("VIDEO") ins Dokument eingefügt werden; seine Eigenschaften können dann über video.src, .autoplay, .loop, .controls und .preload gesteuert werden (siehe Einführungsartikel). video.addEventListener('oncanplay', fnname, false) fügt schließlich noch eine entsprechende Ereignishandhabung ein. "error" wäre bspw. ein weiterer Eventlistener function(e) { alert(e.code); } (siehe nachfolgende Tabelle).

e.code
1 == MEDIA_ERROR_ABORTED der Benutzer hat den Ladevorgang abgebrochen
2 == MEDIA_ERROR_NETWORK Netzwerkfehler
3 == MEDIA_ERR_DECODE Fehler beim Dekodieren
4 == MEDIA_ERR_SRC_NOT_SUPPORTED Medienformat wird nicht unterstützt

über video.networkState kann man den Ladefortschritt feststellen

video.networkState
0 == NETWORK_EMPTY noch nicht initialisiert
1 == NETWORK_IDLE Quelle ausgewählt; nicht im Ladezustand
2 == NETWORK_LOADING lädt die gewählte Quelle aktiv
3 == NETWORK_NO_SOURCE keine Quelle in passendem Format kann ausgemacht werden.

Passend zu dem Statusfeld video.networkState gibt es auch die Ereignisse loadstart, progress, suspend, abort, error, emptied und stalled.

video.currentSrc bezeichnet die aktuelle Quelle, aus der das Video geladen wird. video.canPlayType(type) == '' / 'maybe' / 'probably' wenn das entsprechende Format gar nicht, vielleicht oder ziemlich sicher geladen werden kann.

Um ein Video automatisch starten zu lassen gibt es schließlich noch video.readyState:

video.readyState
0 == HAVE_NOTHING keine Daten
1 == HAVE_METADATA Länge, Dimension und andere Metadaten bekannt
2 == HAVE_CURRENT_DATA Es sind noch nicht wirklich genug Daten zum Weiterspielen vorhanden.
3 == HAVE_FUTURE_DATA genug Daten um mit dem Abspielen beginnen zu können
4 == HAVE_ENOUGH_DATA Mediastream sollte ohne Unterbrechung durchgespielt werden können.

Auch hier gibt es wieder passende Ereignisse nämlich loadedmetadata, loadeddata, waiting, playing, canplay, canplaythrough. Letztlich gibt es Ereignisse für alle Benutzerinteraktionen wie play, pause, timeupdate, ended, ratechange und durationchange.

Es läßt sich sogar feststellen ob gewisse Filmabschnitte schon gepuffert sind, vom Benutzer bereits abgespielt wurden oder ansteuerbar sind: buffered, played und seekable sind TimeRanges Objekte, deren Anzahl an Zeitabschnitten mit timeranges.length abgefragt werden können; nicht zu vergessen die Zeitabschnitte selbst, die mit timeranges.start(i) und timeranges.end(i) abgefragt werden können. Zusätzlich gibt es noch die Statusflags seeking und ended für das Suchen nach dem Springen an eine neue Position und das Ende der Wiedergabe. video.startTime gibt schließlich die Position an, ab der das Abspielen zuletzt begonnen hat.



Sourcecodes des eingebetteten Videos

Schauen wir uns zunächst die HTML-Stuerelemente des eingebetteten Videos an. Hier ist nichts wirklich besonderes zu erkennen, außer daß wir die Druckknöpfe mit Unicodezeichen beschriftet haben. Einige Unicode-Symbolzeichen lassen sich etwa mit kcharselect, der Zeichentabelle von KDE auswählen, während man andere speziell für Multimediazwecke gedachte Zeichen besser nachschlägt. Diese haben so sprechende Namen wie RIGHT-POINTING TRIANGLE.

Anzumerken sind auch die beiden Hooks für den Aufruf von Javascript Code im video-Tag (oncanplay, ontimeupdate) sowie die onchange und onclick Ereignisse der Slider, Optionslisten und Druckknöpfe

<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>

Unser Javascript Code ist zwar kein Musterbeispiel sauberer Programmierkunst. Dennoch der Code ist kurz und bündig und leistet trotzdem sehr viel. Wir haben hier das Video stets als Parameter mit übergeben, falls wir mehrere Videos pro Seite mit unseren erweiterten Kontrollelementen ausstatten wollten. Dieses Ziel ist bis Dato aber noch nicht erreicht, da wir in der Initialiserungsroutine InitControls auch alle Buttons mit dem Video verknüpfen müßten. Wir brauchen nämlich Referenzen auf die Druckknöpfe, da diese ja beim Drücken ihre Beschriftung ändern. Eine Verknüpfung könnte über einen vom video Element getrennten Hash (unter Javascript einfach ein Objekt) erfolgen, das über die video.id indiziert. Alternative könnte man sich auch ein Namensschema für die Druckknöpfe überlegen oder diese gar live mittels DOM vom video-Element aufwärtsgehend suchen. Anstatt Routinen wie document.getElementById oder document.all. zu verwenden haben wir diese der Einfachheit halber stets direkt über ihre ID angesprochen.

Beginnen wir mit dem Abspielen des Videos. Hier haben wir eine Routine PlayPause, die wiederum PlayVideo und PauseVideo aufruft. Wir verwenden niemals video.play() und video.pause() direkt sondern eben PlayVideo und PauseVideo um die Beschriftung der Knöpfe anzupassen. video.play() sollte auch nur aufgerufen werden, wenn das Video noch nicht spielt, da es sonst an den Beginn zurückspringt. Auch ein aufeinanderfolgendes video.pause() und video.play() würde nicht das tun was wir wollen, da hierbei das Video pausiert bliebe (Scheinbar braucht es eine gewisse Zeit bis video.pause() Effekt trägt.).

Beim Anklicken eines Wiedergabezeitpunktes in der Liste rechts wird SelectScene aufgerufen, das eigentlich nur video.currentTime zu setzten braucht. Danach rufen wir wie nach jeder Änderung von video.currentTime UpdateTime auf um unseren Zeitbalken am unteren Rand des Videos anzupassen (einzige Ausnahme: GotoPos, wenn der Zeitbalken direkt angeklickt wird braucht dieser nicht mehr upgedated zu werden, da er ja schon vom Browser gesetzt worden ist.).

UpdateTime ist übrigens auch der Hook der ständig aufgerufen wird während das Video spielt. Er übergibt die Kontrolle an CheckForScene, das in der Liste der aktuellen Wiedergabezeitpunkte (Szenen) die aktuell gespielte heraussucht. Der DOM-SubTree, also alle option-Felder unseres Szenen-selectors, braucht aber nur dann abgegrast zu werden, wenn sich die aktuelle Szene verändert hat (curSceneStart, curSceneEnd).

Etwas trickreich ist auch die Implementierung der Abspielknöpfe, da sich hier immer der Knopf mit der aktuellen Wiedergabegeschwindigkeit (Vor-, Zurückspulen, Schneckentempo, normales Abspielen) in einen Pause-Knopf verwandelt. Das geht ganz einfach, indem die Beschriftung aller Knöpfe zurückgesetzt wird und dann der aktuelle in einen Pauseknopf verwandelt.

Das Erkennen der aktuellen Abspielgeschwindigkeit funktioniert mit ratedelta, da nach dem Setzen der video.playbackRate, diese geringfügig vm gesetzten Wert abweichen kann (ab- oder aufgerundet).

Die Implementierung der anderen Steuerelemente wie Mute und AdjustVolume ist sehr geradlining und spricht wohl für sich selbst; deren Entdeckung will ich dem Leser üebrlassen.

Damit es hinsichtlich der Verwendbarkeit des präsentierten Codes keine Zweifel und Probleme gibt: er darf verwendet werden und ist unter LGPL lizensiert.

<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; } var inited = false; function InitControls(video) { if(inited) return; 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(""); inited = true; } 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>