14.01.2013

von Sebastian Golasch

1 Comment

End to End JavaScript Video Streaming

Videostreaming gibt es seit den Anfängen des World Wide Web Mitte der neunziger Jahre. Im Laufe der Zeit wurden diverse Technologien zur Realisierung von Videostreaming im Web eingesetzt: Begonnen hat alles mit der Integration von Java Applets in Browser. Technologisch gestaltete es sich meist so, dass innerhalb eines definierten Intervalls Einzelbilder von einem WebServer nachgeladen wurden. Mit den Anfang der 2000er Jahre aufstrebenden Technologien wie Adobe Flash & Microsofts Silverlight sowie einer immer breiteren Datenanbindung der Haushalte wurden die Frameraten höher und die Auflösung der Videobilder besser. Auch die Art der Kommunikation änderte sich: weg vom Polling der Daten hin zur Push-Übertragung.

Will man nun heute einen einfachen Livestream zur Verfügung stellen, kann man diverse kostenlose Dienste nutzen. Diese unterscheiden sich in der Bandbreite, der Qualität, dem angebotenen Format und der Größe und Anzahl der eingeblendeten Werbeflächen.

Für unseren offenen Bewerbertag, den ADVENTALK, wollten wir auf unserer Informationsseite einen Livestream einbinden, um den potentiellen Neukollegen einen Einblick in unsere tägliche Arbeit gewähren. Für den Stream benötigten wir ein spezielles Seitenformat, auch auf Werbeeinblendungen wollten wir lieber verzichten. Keiner der frei verfügbaren oder kostengünstigen Alternativen erfüllte unsere Ansprüche, also entschieden wir uns für eine Eigenentwicklung.

Nach einer Anforderungsanalyse war klar: Wir benötigen keine 24 oder 16 Frames in der Sekunde, 2 bis 4 sind völlig ausreichend. Relativ mobil und einfach zu administrieren sollte der Stream zudem auch noch sein und am besten realisiert mit Mitteln, die uns sowieso zur Verfügung standen. Auch der Faktor Zeit spielte eine Rolle: Zur Realisierung sollten so wenig Ressourcen wie möglich gebunden werden.

Die Technologiewahl fiel in unserem Fall dann relativ leicht: Video-Daten abgreifen, zu einem Server senden, der diese dann in JPEG-Daten umwandeln sollte, welche wiederrum von einem Browser abgefragt und dargestellt werden sollen – das ermöglicht nur JavaScript.

Um auf die Video-Daten der WebCam zuzugreifen, nutzten wir die getUserMedia APIs, welche in Opera, Chrome und Firefox zur Verfügung stehen. Diese ermöglichen den Zugriff auf an die Rechner angeschlossene Audio- und Video-Peripherie. In unserem Fall benötigten wir nur ein Video-Bild. Der in Chrome ausgeführte clientseitige Code für dieses Szenario gestaltet sich wie folgt:

 <video id="sourcevid" autoplay></video>
 <script>
 var videoStream = document.getElementById('sourcevid');

 // output the video data in the source video element
 var successCallback = function (srm) {
 videoStream.src = window.webkitURL.createObjectURL(srm);
 };

 // log error
 var errorCallback = function (error) {
 console.log('error: ' + error.msg);
 };

 // grab the incoming device data
 window.navigator.webkitGetUserMedia({video: true}, successCallback, errorCallback);
 </script> 

In Chrome existieren diese APIs bisher nur in einer ‘geprifixten’ Variante. In Firefox und Opera würde der Code folgendermaßen aussehen:

 <video id="sourcevid" autoplay></video>
 <script>
 var videoStream = document.getElementById('sourcevid');

 // output the video data in the source video element
 var successCallback = function (stream) {
 videoStream.src = stream;
 };

 // log error
 var errorCallback = function (error) {
 console.log('error: ' + error.msg);
 };

 // grab the incoming device data
 window.navigator.getUserMedia(['video'], successCallback, errorCallback);
 </script> 

Mit diesem Setup haben wir bereits die Möglichkeit, das Videobild im Browser anzuzeigen.
Um das Bild nun an den Server (in einer ihm verständlichen Form) zu schicken, können wir uns der Möglichkeiten des Canvas Elements bedienen, um das Videobild in das JPEG oder PNG Format zu konvertieren:

 <video id="sourcevid" autoplay></video>
 <canvas width="640" height="480" id="output"></canvas>
 <script>
 var convertVideoToJpg = function (stream, canvasElement, ctx) {
 ctx.drawImage(stream, 0, 0);
 var picture = canvasElement.toDataURL('image/jpeg');
 }

 var init = function () {
 var videoStream = document.getElementById('sourcevid');
 var canvas = document.getElementById('output');
 var ctx = canvas.getContext('2d');

 // output the video data in the source video element
 var successCallback = function (srm) {
 videoStream.src = window.webkitURL.createObjectURL(srm);
 };

 // log error
 var errorCallback = function (error) {
 console.log('error: ' + error.msg);
 };

 // grab the incoming device data
 window.navigator.webkitGetUserMedia({video: true}, successCallback, errorCallback);

 // send the video data every 250ms
 setInterval(function () {
 convertVideoToJpg(videoStream, canvas, ctx);
 }, 500);
 }

 window.onload = init;
 </script> 

Um die Daten nun an den Server zu senden, nutzen wir die WebSocket Technologie, bzw. das Socket.io Framework:

 <video id="sourcevid" autoplay></video>
 <canvas width="640" height="480" id="output"></canvas>
 <script src="http://remote.server.url.io:8080/socket.io/socket.io.js"></script>
 <script>
 var socket = io.connect('http://remote.server.url.io:8080/');

 var convertVideoToJpgAndSendToServer = function (stream, canvasElement, ctx) {
 ctx.drawImage(stream, 0, 0);
 var picture = canvasElement.toDataURL('image/jpeg');
 socket.emit('vs-stream', {
 picture: picture
 });
 }

 var init = function () {
 var videoStream = document.getElementById('sourcevid');
 var canvas = document.getElementById('output');
 var ctx = canvas.getContext('2d');
 // output the video data in the source video element
 var successCallback = function (srm) {
 videoStream.src = window.webkitURL.createObjectURL(srm);
 };
 // log error
 var errorCallback = function (error) {
 console.log('error: ' + error.msg);
 };
 // grab the incoming device data
 window.navigator.webkitGetUserMedia({video: true}, successCallback, errorCallback);
 // send the video data every 250ms
 setInterval(function () {
 convertVideoToJpgAndSendToServer(videoStream, canvas, ctx);
 }, 500);
 }

 window.onload = init;
 </script> 

Dieses Code Snippet sendet nun in einem Intervall von 500 Millisekunden ein in Base64 textkodiertes JPEG via einer WebSocket Verbindung an unseren Server.

Der Servercode gestaltet sich ähnlich simpel. Dank der großen Standard Library von Node.js benötigen wir als einzige externe Dependencies noch das oben bereits angesprochene Socket.io WebSocket Framework sowie die WebServer Abstraktion Express:

 // load and configure socket.io & express
 var express = require('express');
 var app = express();
 var server = require('http').createServer(app);
 var io = require('socket.io').listen(server);

 // defines the port, the server is running on
 // either the snd. argument from the server call
 // node server.js :port or falls back to port 8080 if none given
 var port = process.argv[2] || 8080;

 // holds the base64 text from the last received images
 var lastImage = '';

 // returns the jpg image ressouce if the url
 // image/any_random_valid_ressource_string.jpg is called
 app.get('/image/*.jpg', function (req, res) {
 res.set('Content-Type', 'image/jpeg');
 // convert the base64 text into a string that the node Buffer object understands
 // and send the composed binary image data to the client
 res.send(new Buffer(lastImage.replace(/^data:image\/jpeg;base64,/,""), 'base64'));
 });

 // get our little server up & running
 server.listen(port, function () {
 console.log('Server running @ http://localhost:' + port);
 });

 // get our stream up and running
 io.sockets.on('connection', function (socket) {
 // if socket data with the 'vs-stream' namespace is received,
 // write the contents to the global ´lastImage´ variable
 socket.on('vs-stream', function (data) {
 if (data.picture !== '') lastImage = data.picture;
 });
 }); 

Der Code an sich ist ziemlich selbsterklärend: Immer wenn ein Bild vom streamenden Client via WebSockets an den Server geschickt wird, wird der Text in der Variable ‘lastImage’ zwischengespeichert. Greift jetzt ein Client auf die URL ‘http://remote.server.url.io:8080/image/jeder_valide_string.jpg’ zu, bekommt er das letzte Bild als binäres JPG ausgeliefert.

Was noch fehlt zu unseren ‘Livestreaming’-Glück, ist der Code, welcher für die kontinuierliche Anzeige bzw. das Durchwechseln der Einzelbilder im Ausgebebrowser zuständig ist. Durch unsere Konvertierung in JPG-Daten ist jeder Browser zur Anzeige fähig, welcher Bilder darstellen kann. Wir benötigen also nur ein kleines JavaScript, welches in dem von uns definierten Intervall (500ms) das Bild auf dem Client ändert.

 <img id="stream" width="974" height="400" src="http://remote.server.url.io:8080/lastxxx.jpg"/>
 <script>
 var image = document.getElementById('stream');
 setInterval(function () {
 image.setAttribute('src', 'http://remote.server.url.io:8080/last' + Math.floor(Math.random()*111) + '.jpg');
 }, 500);
 </script> 

Mehr als dieses Code Snippet wird nicht benötigt. In einem Intervall von 500ms wird das ‘src’ Attribut des Image Tags geändert, was den Browser dazu veranlasst, das neu generierte Bild vom Server herunterzuladen und anzuzeigen. Dadurch entsteht die Illusion eines Bewegtbildes.

Einen Eindruck, wie sich der Stream darstellt, kann man sich im Video unterhalb verschaffen.

Sicherlich ist dies wegen des fehlenden Audio-Streams kein Livestream, wie er für Sport- oder Konzertübertragungen genutzt werden kann. Durch die Nutzung von WebSockets mit Binärdaten kann das Beispiel sicherlich noch verbessert werden und je nach Endnutzer-Verbindung könnten durchaus bis zu 10 Frames in der Sekunde erreicht werden. Dass dies jedoch kein Modell für “Premium Content” darstellt, ist klar – hierfür haben sich die Browserhersteller auch auf WebRTC geeinigt. Weitere Informationen zu WebRTC und Video/Audio im Browser finden sich hier.

1 Kommentar

  1. Revision 103: Keine Themen | Working Draft schrieb am 17.01.2013 um 18:05

    [...] End to End JavaScript Video Streaming [...]

Einen Kommentar hinterlassen

*Pflichtfelder

nach oben