Alexandre Bourget

geek joy

New and hot, part 3: FFmpeg video, HTML5 and drag'n'drop encoding

March 14, 2011 at 03:15 PM

This is part 3 of my March 9th 2011 Confoo presentation. Refer to the Table of Contents for the other parts.

FFmpeg encoding of an uploaded video file

In this part, we will create a drag'n'drop frame to upload a video file we've encoded with a phone, and have it transcoded server-side in WebM. This way we'll be able to read it back in the browser.

Before doing any WebM related encoding, we need to make sure our FFmpeg encoding has support for it. If yours doesn't have it in, then here are the steps needed to get one:

$ wget
$ wget
$ wget
$ tar -jxvf ffmpeg-0.6.1.tar.bz2
$ gunzip ffmpeg-0.6.1_libvpx-0.9.2-3.diff.gz
$ cd ffmpeg-0.6.1/
$ patch -p1 < ffmpeg-0.6.1_libvpx-0.9.2-3.diff 
$ cd ..
$ tar -jxvf libvpx-v0.9.5.tar.bz2 
$ cd libvpx-v0.9.5/
$ sudo apt-get install yasm
$ ./configure
$ make
$ sudo make install
$ cd ../ffmpeg-0.6.1/
$ ## You'll need libavformat, libavcodec, libswscale, libavutil, libfaad, libfaac, libvorbis, libx264 and things like that.. 
$ ./configure --enable-gpl --enable-version3 --enable-nonfree --enable-postproc --enable-pthreads --enable-x11grab --enable-libvorbis --enable-libvpx --prefix=$PWD
$ ### Optionally add --enable-libx264 if you want to.., you'll need libx264 dev packages.
$ make -j 9
$ mv libvpx-*ffpreset ffpresets/
$ make install
$ export FFMPEG=$PWD/bin/ffmpeg

We'll use this encoding line to transform my Nexus One's video recording into a web-ready media file:

$ $FFMPEG -y -i "INPUT_FILE" -threads 8 -f webm -aspect 16:9 -vcodec libvpx -deinterlace -g 120 -level 216 -profile 0 -qmax 42 -qmin 10 -rc_buf_aggressivity 0.95 -vb 2M -acodec libvorbis -aq 90 -ac 2 /tmp/OUTPUT.webm
$ ## You can optionally take out the -deinterlace flag if you're dealing with progressive material.

Now, we'll want to be able to upload a file from the web interface. For that, we'll use the browser's drag'n'drop support. Since we're lazy, we'll use the method specified here to speed up things. We'll create an IFRAME with everything required to handle the drag'n'drop upload. We'll call it foo/templates/iframe.html:

iframe YAsnippet to paste that file, need to add the % if request.POST: part
  var entered = 0;
<body ondragenter="entered++;document.getElementById('uploadelement').style.display='block'" ondragleave="entered--;if (!entered) document.getElementById('uploadelement').style.display='none'">
  <form method="post" enctype="multipart/form-data" id="uploadform">

    % if request.POST:
      <div>Uploaded, processing...</div>
    % endif

    Drop a video file here to process...
    <input type="file" id="uploadelement" name="file" onchange="if (this.value) { document.getElementById('uploadform').submit(); }" style="display:none;position:absolute;top:0;left:0;right:0;bottom:0;opacity:0;" />

I've added the % if request.POST, some Mako markup that will show "Uploaded" when we submitted something to the form.

Note that the file-upload field is conveniently named file. We'll refer to that when we want to access the uploaded file.

In our index.html file, we'll add this snippet to create the drag'n'drop iframe, and a video tag for the video to be played:

    <div id="main" role="main">
      <div id="video"></div>
      <iframe src="${request.route_url('iframe')}"></iframe>

Then we'll need something to get it to the browser, so we'll use this little view in

@view_config(route_name="iframe", renderer="iframe.html")
def get_iframe(request):
    return {}

and this route configuration in

def main(global_config, **settings):
    config.add_route('iframe', 'iframe.html')

This means we have a named route called iframe but the URL to reach it will be /iframe.html. The named route is used only to map code to URL locations.

Now, here is everything required to get the encoding to work, in

ffmpeg YAsnippet which contains the FFMPEG command line, including quotes.
### tweak get_iframe(), add encode_video():

@view_config(route_name="iframe", renderer="iframe.html")
def get_iframe(request):
    if 'file' in request.params:
        f = request.POST.get('file')
        tmpfile = '/tmp/video.mp4'
        open(tmpfile, 'w').write(
        # send to ffmpeg...
    return {}

def encode_video(filename):
    import subprocess
    cmd = '$FFMPEG -y -i %s -threads 8 -f webm -aspect 16:9 -vcodec libvpx -deinterlace -g 120 -level 216 -profile 0 -qmax 42 -qmin 10 -rc_buf_aggressivity 0.95 -vb 2M -acodec libvorbis -aq 90 -ac 2 /tmp/output.webm'
    p = subprocess.Popen(cmd % filename, shell=True)

Also, we'll need something to serve the video file itself (in

def get_video(request):
    from paste.fileapp import FileApp
    return request.get_response(FileApp("/tmp/output.webm"))

and a way to wire-in the routes (in

def main(global_config, **settings):
    config.add_route('video', 'video.webm')

Now all that's missing, is a way to actually see the video, so let's add a button to do that manually, once we know the video has been encoded. We add the go() function that will start the playback, and add a button to call the function and we're set! We'll do that in index.html:

      <div id="video"></div>

        function go() {
          $('#video').html('<video controls preload src="/video.webm" />');
          $('#video video')[0].play();
      <button onclick="go()">Add video</button>

      <iframe src="${request.route_url('iframe')}"></iframe>

There you go. We now have some encoding going, and we're able to play it back in the web browser!

What if...

Now what if we could have real-time messaging with the application, to know what's going on over there? Why not graph stats about the process, and have the video popped when it's done encoding ? That'll be the concern of our next episodes.

If you would like us to implement anything you've seen here in a real-life project, or to kick start some of your projects, don't hesitate to send out an e-mail at mentioning this blog. We'll be glad to help.

blog comments powered by Disqus