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.
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.
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:
app.py
command serving our applicationFROM 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
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.
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: