Build a media converter with Python, Qt and FFmpeg
Python is arguably the best programming language to build applications for Linux. This tutorial teaches Python by building a handy media converter application
In this tutorial we will build our very own application called LUD Media Converter. Along the way we will learn about the technologies that we have used in building the application. Generally speaking we will be covering Python and Python Qt bindings called PyQt and FFmpeg. At the end of the tutorial there are some tasks for you as well. As a reader, this will be an opportunity to practise what you have learnt and explore on your own. Don’t worry, we will have hints for you which will help you in completing the tasks.
Python is now the most popular language choice for building GUI (and command-line) applications on Linux. It is an official language of Ubuntu Software Centre. Many distributions use Python to provide built-in tools and applications (for example in Fedora/Ubuntu, all system configuration tools, including the installer, are written using Python). The reasons for using Python is simple: it is easy to learn. You do not have to compile it before running, yet it can be compiled when you want it. You can build GUI applications with Python which will look no different than other C/C++ applications.
As mentioned earlier, we will be building a media transcoder application called LUD Media Converter, which can be used to convert various media files to Android and iOS compatible formats.
As with any software application, we should always begin with a design. This gives a rough idea of what we are trying to achieve and then it acts as a guide as we move forward through the development process. A design could be a rough sketch of how the application will work, but since we have already completed the application we can show you the completed application itself.
Now we understand how our Media Converter will work and what it’ll look like. There are still few details that we need to look at, which we will cover as we go through the steps of building the application.
Python 3: This tutorial was written using
PyQt4: PyQt4 provides Python bindings for the Qt4 library. This tutorial was written using version 4.9.3
Qt4 Designer: Qt Designer is a tool for designing and building graphical user interfaces (GUIs) from Qt components.
FFmpeg Static Build: We are using FFmpeg as the back-end of the application. FFmpeg static version N-47648-g4c554c9 is already provided in the ffmpeg folder in the tutorial folder of the disc. You will need to extract and copy it into the project folder.
PySide: This tutorial can also be used with an alternative Python binding called PySide (LGPL licensed).
Python IDE: While you can use a standard text editor for all your Python programming, it is not fun. Having an IDE will help you quickly find syntax errors (tab spacing issues, anyone?) and code completion. We recommend using the Monkey Studio or Wingware Python IDE. These are the only IDEs for Python that support PyQt out of the box – Wingware Python IDE is the best when it comes to PyQt support, however it is commercial.
Step by Step
Laying out the user interface
Laying out the user interface can be tricky. Qt Designer makes this process very easy. It allows you to graphically design user interface for Qt applications. Qt Designer stores all the information in the form of an XML file called UI file (.ui). You can either load the UI file directly at runtime or convert it to the Python code to make it part of your application. Our application uses the later approach. Open Qt Designer, click File>New… Then from the New Form dialog select ‘Main Window’. This will create a Main Window for our application. Now you will need to Drag the following from the Widget Box onto the Main Window (see image below).
Note that we are not listing all the widgets which are there on the form, but only the ones which will be programmatically interacting. Fields which are not provided may also be added. Size and Text of the widget is given where deemed necessary; you can use your own judgment while designing the UI.
Once you are done creating the UI file, save it as mainwindow.ui. You will need to create the equivalent Python file. To do so, use the following command:
$ pyuic4 mainwindow.ui -o mainwindow.py
Creating main module (main.py)
The main.py file acts as the primary file for your application. In a typical Python application, main.py is the file which gets loaded first during the execution. In our case, our whole application is written in main.py for
the sake of simplicity.
Note 1: Make sure you place all the source files inside the same project directory otherwise Python may not be able to import them.
Note 2: Python is tab sensitive; publishing restrictions may not always reflect the proper tab spacing for the code.
For accuracy, a Python IDE is highly recommended as most of the syntax errors related to tab spacing would be automatically handled by the IDE itself. Also, remember that to accommodate longer lines in print, single lines of code may appear on multiple lines. Unless the code line is split with ‘+ \’, you do not need a carriage return while typing the lines of the code.
Create file main.py in your project directory with the following code:
#!/usr/bin/env python3 from PyQt4 import QtCore, QtGui from os.path import expanduser import os import shlex __author__ = ‘Kunal Deo’ #import Converted Python UI File from mainwindow import Ui_MainWindow class Main(QtGui.QMainWindow): def __init__(self): QtGui.QMainWindow.__init__(self) self.ui = Ui_MainWindow() self.ui.setupUi(self) def main(): app = QtGui.QApplication(sys.argv) window = Main() window.show() sys.exit(app.exec_()) if __name__ == “__main__”: main()
This is mostly boilerplate code necessary to import the necessary PyQt libraries and set up the UI. The first line indicates that we are interested in using Python 3 for this script. In the next line we are importing the QtCore and QtGui modules from PyQt4. QtCore provides essential non-GUI classes which are the building blocks of any Qt application. QtGui provides all the UI-related classes.
In the next line we are importing the Ui_MainWindow class from mainwindow.py – the Python-converted UI file that we created earlier in step 1.
Following that, we’re initialising the Main class.
Programming the buttons
Buttons and most of the event-driven code are done using a mechanism called ‘signals and slots’. The signal and slots provide a way of communicating between the objects. A signal is emitted when a particular event is triggered. For example, clicking the ‘Select Media File Button’ should call a function (slot) that will open the file browser. Most of Qt’s widgets have predefined signals, but you can subclass them to add custom signals as well. A slot is a function that is called when a particular signal is emitted.
You should keep in mind that signals and slots are pretty flexible; for example, a signal may be connected to many slots, a signal may also be connected to another signal, or a slot may be connected to many signals.
We can connect a Qt object as signal QtSig() to a slot pyFunction using the following syntax:
QtCore.QObject.connect(a, QtCore. SIGNAL(‘QtSig()’), pyFunction)
There is also a new style of connecting signals and slots which was introduced in PyQt 4.5. The syntax for the new style connection is:
We are using the old-style signal-slots mechanism because it is well documented and can be looked up in Qt’s C++ documentation as well. Besides that, pyuic4 generates old-style signal and slots.
The following code defines the connection of selectFileButton and convertButton.
The above code button’s clicked() signal goes to the following Python functions.
def selectFile(self): fileName = QtGui.QFileDialog. getOpenFileName(self,’Open Media File’,expanduser(“~”),’Media Files (*.mov *.avi *.mkv *.mpg)’) self.ui.fileName. setText(fileName) def convert(self): self.convertFile() @
The selectFile function opens a QFileDailog in the home directory. It also filters the file list for .mov, .avi, .mkv and .mpg.
Programming the radio buttons
We are using radio buttons to give users an option to select the desired output format. As with normal buttons, for radio buttons we can use the signal “toggled(bool)” to capture the signals emitted by the radio button.
QtCore.QObject.connect(self. ui.androidHDRadioButton,QtCore. SIGNAL(“toggled(bool)”),self.androidHDSelected) QtCore.QObject.connect(self.ui.androidQHDRadioButton,QtCore.SIGNAL(“toggled(bool)”),self.androidqHDSelected) QtCore.QObject.connect(self.ui.appleHDRadioButton,QtCore.SIGNAL(“toggled(bool)”),self.appleHDSelected) QtCore.QObject.connect(self.ui.appleFullHDRadioButton,QtCore.SIGNAL(“toggled(bool)”),self. appleFullHDSelected)
Based on the selected checkbox, we will set the text of outputFormat:
def androidHDSelected(self): self.ui.outputFormat.setText(“Android HD”) def androidqHDSelected(self): self.ui.outputFormat.setText(“Android qHD”) def appleHDSelected(self): self.ui.outputFormat.setText(“Apple HD”) def appleFullHDSelected(self): self.ui.outputFormat.setText(“Apple Full HD”)
Our application shows WebM and H.264 logos in the application window. To hold these images, we have created two labels on the form called imageLabel1 and imageLabel2. We will use QPixmap. QPixmap is an off-screen image representation that can be used as the paint device.
The following code snippet builds a QPixmap from a PNG file, then we set it to the labels that we have.
@code snippet: Loading images using QPixmap pixmap = QtGui.QPixmap(“images/webmlogo.png”) self.ui.imageLabel1.setPixmap(pixmap) pixmap = QtGui.QPixmap(“images/h264logo.jpg”) self.ui.imageLabel2.setPixmap(pixmap) @ @note: If you want to load images from Qt resources file you need to use the following: “:images/webmlogo.png” @
We have now come to heart of our application. We are using FFmpeg as the back- end for the application. It does all the heavy lifting for media conversion.
FFmpeg is a cross-platform open source utility for recording, converting and streaming audio and video. We are interested in its conversion capability. Following is an example of converting a file into another type:
ffmpeg -i “
” -codec:v libvpx -quality good -cpu-used 0 -b:v 2000k -qmin 10 -qmax 42 -maxrate 2000k -y -bufsize 2000k -vf scale=-1:720 -threads 2 ‘ + \ ‘-codec:a libvorbis -b:a 128k “ .webm"
We need to construct and run the same command and capture its output. Our application supports four options (see image below), laid out in the form of radio buttons.
Before we call FFmpeg we also need to manage the execution of it. Since FFmpeg is a separate process, we need to find a way to execute it from our Python application. Python already provides a module for doing this: it is called subprocess. While it is good for running external programs, it does not provide an optimum way to read the output of the program it is running. Instead we will use a Qt module called QProcess. QProcess is a class used to start external programs and to communicate with them.
QProcess supports the signal and slots mechanism. We are interested in two signals: readyReadStandardError() and finished(). ‘readyReadStandardError()’ is emitted when the process has made new data available via its standard error channel (stderr) – in this case, the command-line output of FFmpeg.
‘finished(int exitCode, QProcess::ExitStatus exitStatus)signal’ is emitted when the process inishes. exitCode is the exit code of the process, and exitStatus is the exit status.
This enables us to deliver a more fluid user experience to our users. They can be notified when FFmpeg has completed the conversion, and also see the progress of FFmpeg as it is converting the media file.
self.process = QtCore.QProcess(self) QtCore.QObject.connect(self.process,QtCore.SIGNAL(“finished(int)”),self.processCompleted) QtCore.QObject.connect(self.process,QtCore.SIGNAL(“readyReadStandardError()”),self.readStdError) @
The above code should be entered at __init__ of the Main class, just where all other signal and slots are set up.
Let’s take a look at our convertfile() function. This function is called when the user clicks the Convert button.
def convertFile(self): inputFile = str(self.ui.fileName.text()) outputFormat = str(self.ui.outputFormat.text()) if outputFormat == (‘Android HD’): cmd = ‘-i “%s” -codec:v libvpx -quality good -cpu-used 0 -b:v 2000k -qmin 10 -qmax 42 -maxrate 2000k -y -bufsize 2000k -vf scale=-1:720 -threads 2 ‘ + \ ‘-codec:a libvorbis -b:a 128k “%s.webm”’ elif outputFormat == (‘Android qHD’): . . .. . .. . . . . . . . . . . . . .. . elif outputFormat == (‘Output Format’): QtGui.QMessageBox.warning(self,”Input Format Not Selected”,”Please select an appropriate Output Format”) return (‘Selection Not Proper’) if inputFile == (‘File Name’): QtGui.QMessageBox.warning(self,”Media Not Selected”,”Please select a file to convert”) return (‘Selection Not Proper’) argument = shlex.split(cmd % (inputFile,inputFile[:-4])) command = os.path.join(os.getcwd(),”ffmpeg”) self.ui.statusText.setText(“Please Wait....”) self.ui.convertButton.setDisabled(True) self.process.start(command,argument) @
We are reading the input file and output format from the labels. If these labels are not set, it alerts the user about the respective fields using QMessageBox. If it has all the data required, it checks the output format selected and the filename and builds a command string. This command string is then split into shell-like syntax – eg (‘param1’,’param2’,’param2’) – and is stored in the argument variable. Note that we are formatting the string with the value of inputFile. ,inputFile[:-4] indicates that we are removing the last four characters (ie the extension .xyz) so that it can be replaced with
the one that is provided in the cmd string.
For storing the command, we are using os.path.join(os.getcwd(),”ffmpeg”). Instead of using the absolute path, this gives us immense flexibility when we are running this program on other platforms as well. os.path.join(os. getcwd(),”ffmpeg”) always returns the path of the ffmpeg in the current directory formatted according to the platform it is running on.
On Linux it returns: /home/kunal/Desktop/lud/ffmpeg
On Windows 8 it returns: C:\Users\Kunal\Desktop\lud\ffmpeg
Towards the end we are setting the status text to “Please Wait…”. Then we have disabled the convertButton so that it cannot be pressed while the FFmpeg processing is going on.
As set by the finished() signal, upon the completion of execution of FFmpeg the following function gets called:
def processCompleted(self): self.ui.statusText. setText(“Conversion Complete”) self.ui.convertButton. setEnabled(True) self.ui.textBrowser. append(‘Conversion Complete’)
This sets the status text as conversion complete and re-enables the Convert button.
To provide the real-time status of FFmpeg we have added a TextBrowser widget to our UI, which we can set now to provide the output of the ffmpeg command.
def readStdError(self): self.ui.textBrowser. append(str(self.process. readAllStandardError()))
After completing the application, you can run it by using the following command:
$ python3 main.py
The output file will be written to the same directory where the original is located.
You’ve seen how easy and fun it is to build a complete application using Python and Qt. If you have taken the time to finish this tutorial, we would highly recommend you to complete some tasks as well (see boxout above), as they will enable you to learn even more as you explore on your own. We will continue to bring you more features and tutorials that’ll help you build on everything that you have learnt here.
LUD Media Converter is also an open source application hosted on GitHub. You are welcome to contribute on the project page.