Cameron Lane

Erstwhile educator. Python programmer.

Convert Quicktime to MPEG-4 with Docker and Python

I recently needed the ability to convert Quicktime movie formats (.mov) to MPEG-4 (.mp4). I needed the solution to be:

Some google searching quickly led me to two main options. One, I could use one of the numerous video conversion sites. I rejected this because they seem sketchy and it's not entirely clear what they will do with the video. Two, I could use ffmpeg, which emerged as the unix tool most often used for this kind of task. It checks all the boxes except for the last one; installing and using ffmpeg from the command line was not really something my users would be comfortable with. In fact, my users may not even have permissions to install the program at all! Furthermore, there are numerous arcane ffmpeg options that make using it in a simple manner daunting for novice users, especially if they're not already comfortable on the command line. So I did what any developer would do in this situation: expose ffmpeg video conversion as a web service.

Design

The design for this particular project is minimal: the back end will be a simple flask HTTP service that accepts Quicktime file uploads and converts them to MPEG-4 format in a single request. This is not a high-traffic application, so I'm not too concerned about the number of requests. Simplicity is preferred here over raw performance. So it's a single endpoint to display the upload form (GET) and process the uploaded file (POST). The conversion is acheived through a very simple wrapper around ffmpeg called ffmpy. The files will be uploaded to a temporary directory, converted, and served as a static file from the same directory.

To avoid problems with dependencies and reproducibility, I decided to deploy the entire web service in a docker container. This prevents me from having to install ffmpeg and all it's dependencies on my host system, and I can move the service anywhere and have it running pretty quickly.

Implementation

The full project is available from my github, but it's small enough that I can reproduce it here.

The flask web service is adapted from the file upload example in the Flask documentation. A single view accepts both GET and POST requests. If it is a POST, the file included in the submission is converted and returned. If the HTTP method is GET, the server simply returns the HTML form required to upload a file. This is not a pretty HTML page, but it works nicely.

The conversion is a straightforward using ffmpeg, and is taken almost verbatim from one of the ffmpy documention examples. It uses no flags or special conversion parameters; rather it accepts two arguments: the name of the original file and the name of the file we're converting to. It would be simple to add some additional functionality here, by accepting additional inputs from the form and passing them to the conversion function. This is left as an exercise to the reader, as I've already solved the problem I set out to solve ;).

Below is the full source of the Flask service:

import os

from ffmpy import FFmpeg

from flask import Flask, request, redirect, send_from_directory

app = Flask(__name__)
UPLOAD_DIR = '/tmp/video_conversion'

app.config['UPLOAD_DIR'] = UPLOAD_DIR


def _new_filename(filename):
    name, ext = os.path.splitext(filename)
    return f'{name}.mp4'


def _convert_to_mov(original, new):
    app.logger.info('Converting %s -> %s', original, new)
    ff = FFmpeg(
        inputs={original: None},
        outputs={new: None}
    )
    ff.run()
    app.logger.info('Conversion successful: %s', new)
    return new


@app.route('/convert', methods=['GET', 'POST'])
def convert_video():
    if request.method == 'POST':
        # check if the post request has the file part
        if 'file' not in request.files:
            app.logger.error('No file submitted')
            return redirect(request.url)

        file_to_convert = request.files['file']
        original = file_to_convert.filename
        app.logger.info('File received: %s', original)
        original_path = os.path.join(app.config['UPLOAD_DIR'], original)
        new_path = os.path.join(app.config['UPLOAD_DIR'], _new_filename(original))
        if file_to_convert:
            file_to_convert.save(original_path)
            new_file = _convert_to_mov(original_path, new_path)
            app.logger.info('Will return file: %s', os.path.basename(new_file))
            return send_from_directory(app.config['UPLOAD_DIR'], os.path.basename(new_file), as_attachment=True)

    return '''
    <!doctype html>
    <title>Upload MOV File to Convert</title>
    <h1>Upload new File</h1>
    <form method=post enctype=multipart/form-data>
    <p><input type=file name=file>
    <input type=submit value=Upload>
    </form>
    '''


if __name__ == '__main__':
    app.run(host='0.0.0.0')

The docker image is defined below. It is also minimal. For the sake of being explicit for those unaware of how Dockerfiles work, this defines a docker image. When building the image, we complete the following tasks:

  1. Setting the base image to the latest python 3.6 release
  2. Adding the jessie-backports mirror to the sources.list so that we can install ffmpeg
  3. Updates the apt sources and installs ffmpeg
  4. Installs flask and ffmpy using pip
  5. Creates a temporary directory to use for the video conversion
  6. Copies the python source file into the container
  7. Exposes the tcp port our HTTP application will be served over
  8. Executes the app.py command serving our application
FROM python:3.6

RUN echo 'deb http://ftp.debian.org/debian jessie-backports main' >> /etc/apt/sources.list
RUN apt-get update && apt-get install -y ffmpeg
RUN pip install flask ffmpy
RUN mkdir -p /tmp/video_conversion
COPY app.py app.py
EXPOSE 5000
CMD python app.py

Using the Service

Once these are defined, it's easy to get started. Simply build the image with docker, then run it. It's assumed these commands are run from the same directory where the two files above live.

$ docker build -t mov-converter .
$ docker run --rm -t -p5000:5000 mov-converter

After these complete successfully, you'll have your movie converter service running on port 5000 of localhost. . Visit localhost:5000/convert in your browser and use the form to convert your videos.

Conclusion

This was a simple and fun weekend project to solve a very specific problem. It's not meant for heavy 'production' use, but it's an interesting proof-of-concept. There are several caveats and pitfalls with this design that warrant further consideration:

comments powered by Disqus