Posted on 22 Jan 2011

The video above is a quick sketch of an algorithm I’m working on, which has been exported from a WebGL application.

In theory exporting high-quality video from a WebGL app is fairly trivial. The canvas.toDataURL() method lets you grab frames as data URIs (basically Base64 encoded PNGs), which you can then stitch together to create a video. So far so simple. The difficulty comes when you try to move thousands of PNG from your browser onto your hard-drive.

How not to do it

Using a bit of JavaScript it is simple to take the data URI which is generated and insert it as an image somewhere on the page containing the WebGL application. The problem with this is that you then have to do the right-click→Save Image As… dance 1800 times for 30 seconds of video. My boredom threshold sets in at about the fifth image. Thankfully at this point the DownThemAll Firefox extension normally steps in to take care of tedious bulk downloading tasks, but this was also a dead end as DownThemAll doesn’t currently support images created using data URIs.

Enter Node.js

The solution to the problem of downloading the files came in the form of Node.js. This allowed me to quickly sketch out a web server which took AJAX requests containing the encoded PNG data and a frame number, decoded the PNG and saved it as a new file. Whilst this seems an unnecessarily convoluted way to download some images, it was by far the simplest solution I could come up with. The fact that the process used JavaScript right the way through, from generating the images, capturing them, and exporting them via a web server is really testament to how far it has come as a language, and the flexibility it now offers.

The code

The Node.js code is available below. Each AJAX request need to contain two pieces of POST data: frame, an integer frame number, and data, the output of canvas.toDataURL().

var http = require('http');
var querystring = require('querystring');
var fs = require('fs');

// Override so we don't decode spaces, and mess up the base64 encoding
querystring.unescape = function(s, decodeSpaces) {
    return s;
};

// Pad to follow the processing export format
function pad(num) {
    var s = "000" + num;
    return s.substr(s.length-4);
}

http.createServer(function (request, response) {
    request.content = '';
    request.addListener("data", function(data) {
        request.content += data;
    });

    request.addListener("end", function() {
        if (request.content.trim()) {
            request.content = querystring.parse(request.content);
            var data = request.content['data'];
            var frame = request.content['frame'];
            // Remove data:image/png;base64,
            data = data.substr(data.indexOf(',') + 1);
            var buffer = new Buffer(data, 'base64');
            fs.writeFile('screen-' + pad(frame) + '.png',
                         buffer.toString('binary'), 'binary');
        }
    });
    response.writeHead(200, {
        'Content-Type': 'text/plain',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Headers': 'X-Requested-With'
    });
    response.end();
}).listen(8080, "127.0.0.1");
The server saves the images using the same naming convention as Processing’s saveFrame() function, and can be encoded using FFmpeg using the following command:
ffmpeg -r 60 -i screen-%04d.png -vcodec libx264 -vpre lossless_slow -threads 0 output.mp4