Alexandre Bourget

geek joy

Posts in category: Web development

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 http://webm.googlecode.com/files/libvpx-v0.9.5.tar.bz2
$ wget http://webm.googlecode.com/files/ffmpeg-0.6.1_libvpx-0.9.2-3.diff.gz
$ wget http://www.ffmpeg.org/releases/ffmpeg-0.6.1.tar.bz2
$ 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
<script>
<!--
  var entered = 0;
-->
</script>
<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;" />
  </form>
</body>

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>
    </div>
    ...

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

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

and this route configuration in __init__.py:

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 views.py:

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(f.file.read())
        # send to ffmpeg...
        encode_video(tmpfile)
    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)
    p.communicate()

Also, we'll need something to serve the video file itself (in views.py):

@view_config(route_name="video")
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 __init__.py):

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>

      <script>
        function go() {
          $('#video').html('<video controls preload src="/video.webm" />');
          $('#video video')[0].play();
        }
      </script>
      <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 contact@savoirfairelinux.com mentioning this blog. We'll be glad to help.

Read and Post Comments

New and hot, part 2: First Pyramid setup

March 14, 2011 at 02:15 PM

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

Pyramid installation

Setup of the "Foo" pyramid application

We'll install those packages:

  • pyramid: the web framework.
  • mongokit: which brings in pymongo, the low-level mongo driver.
  • kombu: for AMQP/RabbitMQ communication. To be used later on.
  • redis: to access a Redis server. Used in the last part of this presentation.
  • pyramid_socketio: pyramid and Socket.IO integration for real-time apps. This will bring in all the Gevent machinery. We'll use that later on also.

First, we setup the virtual environment:

$ cd ~
$ virtualenv --distribute env
$ . env/bin/activate
NOTE: each time you will see that (env) prompt prefix, it means we're in the virtual environment. To activate it (after a reboot or whatever), run . env/bin/activate again.

Then install what we need, create a new template and install it in the environment (in development mode):

(env)$ pip install pyramid mongokit kombu pyramid_socketio redis
(env)$ paster create -t pyramid_starter Foo
...
(env)$ python setup.py develop
(env)$ paster serve --reload development.ini

You'll notice that Pyramid was made to use the same Paste server and configuration that we were used to in Pylons 1.0. Let's have a look at the default page at: http://127.0.0.1:6543. Needless to say that it is much prettier than before! We can have a sneak peek at the documentation to notice it is very extensive and beautifully laid out.

site Show/load in the browser the project URL.
docs Show the pre-loaded documentation, or load it.

Exploring the directory layout

(env)$ cd Foo
(env)$ find .
.
./templates
./templates/mytemplate.pt
./static
./static/pyramid.png
./static/pyramid-small.png
./static/transparent.gif
./static/headerbg.png
./static/middlebg.png
./static/favicon.ico
./static/footerbg.png
./static/ie6.css
./static/pylons.css
./resources.py
./__init__.py
./views.py
./tests.py

A notable difference with the original Pylons 1.0, is that Pyramid doesn't impose any structure to the directories you layout. It can easily create a one-file application, as well as allow you to lay your files in the way that fits best with your project. This is very refreshing.

In the pyramid_starter template, the model.py was renamed to resources.py, but you could rename it as you wish. Simply tweak __init__.py accordingly.

The static/ directory contains all the static files, and replaces the public/ directory of Pylons 1.0.

templates/ is our old templates holder, with snippets of HTML in the templating language you like the most. Pyramid isn't stubborn about the templating language you choose - or decide not to choose. It'll run the templating engine you want, based on the extension of your template files. So you can have many templating languages in the same project. By default, it uses the Chameleon templating engine, an XML-based Zope Page Template and Genshi template compiler.

The central part of the projet is in __init__.py... the foo module itself. That's where all the config and wires are tied in. We'll add a couple of config items in here in a moment.

Notice in the __init__.py file that there's nothing about sessions (Beaker) and not much about databases. That's because they're pluggable, and we'll add them as required. Fear not, everything is thoroughly documented.

Adding Mako support

I love Mako, so we'll tack on Mako support:

def main(global_config, **settings):
    ...
    config.add_renderer('.html', 'pyramid.mako_templating.renderer_factory')
    ...

This will make all future rendering of .html files use the Mako rendering engine. I used .html just for convenience. You might want to use the .mako extension if you want to integrate with other components well.

You'll want to add those config in development.ini to have the templates compiled once. mako.directories is required for Mako to work.

### inside Foo/development.ini
[app:Foo]
...
mako.directories = foo:templates
#mako.module_directory = %(here)s/data/templates
...

The second mako.module_directory is used to write compiled Mako files. Make sure to use this in production, as it speeds things up a lot.

Writing our first view

This year, I'm not going to demonstrate Mako, so I'll simply load the excellent and highly recommended HTML5 boiler-plate (by Paul Irish) directly into my templates dir.

$ cd ~/Foo
$ git clone ~/build/html5-boilerplate
$ cp -r html5-boilerplate/* foo/static
$ # We'll put index.html in the templates/ dir though..
$ mv foo/static/index.html foo/templates

We will need to tweak the HTML5 boilerplate so that the URLs pointing to our scripts and resources use Pyramid's routing. Get the full patch here. This is a sneak peek:

html5 YAsnippet for a stripped-down version of the html5boilerplate.
--- a/foo/templates/index.html
+++ b/foo/templates/index.html
@@ -26,18 +26,18 @@
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
   <!-- Place favicon.ico & apple-touch-icon.png in the root of your domain and delete these references -->
-  <link rel="shortcut icon" href="/favicon.ico">
-  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
+  <link rel="shortcut icon" href="${request.static_url('foo:static/favicon.ico')}">
+  <link rel="apple-touch-icon" href="${request.static_url('foo:static/apple-touch-icon.png')}">

You can apply it this way:

(env)$ cd ~/Foo
(env)$ patch -p1

and paste the patch in, then hit Ctrl+D.

We'll also add a little style in foo/static/css/style.css after the CSS resets. There's a specially marked section for you:

In the demonstration, I'm actually using a stripped-down version of the html5 boilerplate (it's a yasnippet for Emacs).

styles Copy and paste in foo/static/css/style.css
 /* Primary Styles
    Author: Alexandre Bourget
 */
div#container {
  margin: 0 auto;
  width: 800px;
  border: 1px solid #aaa;
  border-radius: 10px;
  padding: 10px;
  -webkit-box-shadow: 5px 5px 5px #ddd;
}
h1 {
  text-shadow: 2px 2px 2px #ddd;
  font-size: 22px;
  margin-bottom: 10px;
}
footer {
  font-size: 10px;
  text-align: center;
  margin-top: 15px;
}
iframe {
  width: 780px;
  height: 70px;
  border: 1px solid black;
}
div#graph {
  width: 750px;
  height: 300px;
}
div#video {
  text-align: center;
}
video {
  width: 640px;
  height: 480px;
}

We'll go in index.html and add some hello world or something:

     ...
     <header>
       <h1>Hello Excellent World!</h1>
     </header>
     ...

Now let's add the required calls to show that on the front-page:

view YASnippet to add a new view

In views.py:

from pyramid.view import view_config

@view_config(route_name="home", renderer="index.html")
def home(request):
    return {'boo': 'ahh'}

In __init__.py:

def main(...):
    ...
    config.add_route('home', '')
    config.scan('foo.views')
    ...

and hop! http://localhost:6543 should now answer the call

That's it for now! But what if we could actually do someting interesting, like dragging a video file from the desktop and have it encoded on the server-side so that it could be played back via the web ? We'll do just that in our next episode!

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 contact@savoirfairelinux.com mentioning this blog.

UPDATE May 3rd 2011: Simplified the Mako integration (from 2 lines to one).

Read and Post Comments

New and hot stuff in The Pylons Project

March 09, 2011 at 04:55 PM

This is the layout of a talk I gave at the Confoo conference on March 9th 2011, in Montreal, Canada. There will be a post on each of the following topics, each covering about 7 minutes of my talk. It will be accompanied by the video of the talk, as well as all the references, code and links.

Table of contents

I hope to publish all parts within a few days. I just want to make sure they are proof-read.

All the series was tested with Google Chrome. If something doesn't work with your favorite browser, you might want to try with Chrome.

Here is the YouTube presentation playlist:

UPDATED April 1st 2011: Added video, and links to part 1 and part 6.

Read and Post Comments

Pyramid and Mako: how to do i18n the Pylons way

January 13, 2011 at 09:07 PM

In reply to this post about the Pylons way to do translation using Pyramid, here is my code snippet so that you don't need to put the request object in, each time you want to translate something:

__init__.py:

def main(...):
    ...
    config.add_subscriber('YOURPROJECT.subscribers.add_renderer_globals',
                          'pyramid.events.BeforeRender')
    config.add_subscriber('YOURPROJECT.subscribers.add_localizer',
                          'pyramid.events.NewRequest')

or wherever the following add_renderer_globals will be.

Then add, let's say in subscribers.py:

from pyramid.i18n import get_localizer, TranslationStringFactory

def add_renderer_globals(event):
    ...
    request = event.request
    event['_'] = request.translate
    event['localizer'] = request.localizer
    ...

tsf = TranslationStringFactory('YOUR_GETTEXT_DOMAIN')

def add_localizer(event):
    request = event.request
    localizer = get_localizer(request)
    def auto_translate(string):
        return localizer.translate(tsf(string))
    request.localizer = localizer
    request.translate = auto_translate

Now, in your Mako template, you'll be able to use the simple ${_(u"Translate this string please")} without having to specify the request explicitly, as it's enclosed in this request's "_" function. localizer will also be available for plural forms and fancy stuff.

This version will also allow you to use translation in your view code, using something like:

def my_view(request):
    _ = request.translate
    request.session.flash(_("Welcome home"))
    ...

Now for all that to work, you'll need to:

(env)$ pip install Babel

first, and also run these commands in your project's directory:

(env)$ python setup.py extract_messages
(env)$ python setup.py init_catalog -l en
(env)$ python setup.py init_catalog -l fr
(env)$ python setup.py init_catalog -l es
(env)$ python setup.py init_catalog -l it
(env)$ python setup.py update_catalog
(env)$ python setup.py compile_catalog

... for the langauges you need. Note that the sub-directory of your project is locale/ in Pyramid, and not i18n/ as it was in Pylons. You'll notice that in the default setup.cfg of a Pyramid project.

Lastly, you'll want to have your Mako files extracted when you run extract_messages, so add these to your setup.py (yes, you read me right, in setup.py so that Babel can use it when invoking it's commands):

python setup.py 
setup(
    ...
    install_requires=[
        ...
        Babel,
        ...
        ],
    message_extractors = {'yourpackage': [
            ('**.py', 'python', None),
            ('templates/**.html', 'mako', None),
            ('templates/**.mako', 'mako', None),
            ('static/**', 'ignore', None)]},
    ...
    )

Hope this helps.

UPDATED Jan 14th, 00:56: Added support for in-view translation.

Read and Post Comments

My new Blogofile blog

January 11, 2011 at 05:30 PM

Hello all.

With every New Year comes new resolutions, and mine was to revamp my blog. So here it is, shining new, and all with flat files. It's computed using the awesome little blogging engine called Blogofile. It's updated using Emacs and git, with a hook that automatically rebuilds every file from source files and Mako templates.

I migrated from a Zine blog which was leaking to death (probably due to a bad/weird paster setup of mine). The whole operation took about 5 hours, including the migration of my 20 other blog posts.

It uses the Disqus commenting system so it really doesn't need anything dynamic on the server side.

If things are wrong or have changed in an unexpected way, please leave a comment below. Thanks

Read and Post Comments

« Previous Page -- Next Page »