(Python, QT Designer) Media File Converter Tutorial

Linux User & Developer magazine has recently published a Python tutorial by Kunal Deo (http://www.linuxuser.co.uk/tutorials/build-a-media-converter-with-python-qt-and-ffmpeg). It is not a very satisfying tutorial (one of the comments on the original: “This article is quite simple (sic) the single worst introduction to Python ever”.

For all that, I found it a useful intro to combining QT Designer and Python so I decided to re-write the tutorial. I stress that I am a Python novice and do not pretend to be offering a clever solution: it is just a re-writing of the original demo.
Please let me know if there are any errors in the re-write.

I write the tutorial for Debian-derivatives (Ubuntu, Mint, Kubuntu, Xubuntu etc). Installing the necessary software will be different from, for example, RPM based systems

–Installing Software–

Firstly we need to ensure we have the necessary software installed. Open a terminal and type ‘python3’ (throughout this tutorial do not include the apostrophes) and [enter]. It will be a very unusual distribution that does not have python3 already installed and you should see the Python intro with the cursor next to three right-arrows. Note whether you distribution is using Python 3.2 or 3.3. [ctrl]-d to quit Python but leave the terminal open. Next install ‘qt 4 designer’ from your software centre. I used Muon Software Centre in Kubuntu but am testing with Ubuntu Software Centre in Xubuntu. We will use this to layout the application screen and it will leave us with an xml layout file with the extension .ui.

We will need to convert this .ui file to a .py file and need pyuic to do this. Go back to terminal and ‘pyuic’ [enter]. You will probably get the response that it cannot be found. Install it with ‘sudo apt-get install pyqt4-dev-tools’ [enter].

Finally we need an application to type the Python program. I am not a great fan of Idle but we will use it anyway: go back to your software centre, search on ‘idle’ and install the relevent one for your version of Python as recorded above (in my case Idle for Python 3.2).

Finally we need the media converter to do the actual grunt. The original tutorial uses ffmpeg but I am using avconv. I don’t normally do media (prefer crunching numbers). I own neither an Android nor Apple device so can’t test the output. You will have to do work on the avconv command lines to get the four optional output files. ‘man avconv’ [enter] on the terminal will give help. Go back to terminal and ‘sudo apt-get install libav-tools’ [enter] to install avconv.

— Layout the form —

Start Qt 4 Designer (it is in the development section of your pulldown menu). Select ‘Main Window’ from the new form dialogue and [create]. MainWindow should be highlighted in the object inspector on the right and the property editor below should show the MainWindow’s properties. Click on the triangle next to ‘geometry’ to expand it and change the width property to 750 and the height to 550.

Click on ‘Form’ in the menu bar then ‘Form Settings’ and enable snapping to a 10×10 grid.

Find ‘Label’ under ‘Display Widgets’ in the Widget Box on the left (you may have to drag the widget box wider). Drag a label onto the form, position it near the top. The object inspector will now be highlighting object label of class Qlabel. In the property Editor change the objectname to ‘lblTitle’. Under the font property change the font size to 20. Change text property to ‘LUD Media Converter’. Drag the size of the object on the form so that it is visible.

Save your new form in a convenient place (I created a directory for the project in my Home directory) as ‘mainwindow’. Accept the default extention (.ui).

I am sure you have the idea now so we will move a little quicker: drag a pushbutton onto the form. Change its objectname to ‘btnSelectFile’ and text property to ‘&Select Media File’. Resize the button so that the text can be seen. The ampersand before the S allows [Alt]-s to become a shortcut for the button.

Put another pusbutton to the right of the first. Objectname ‘btnConvert’. Text ‘&Convert’ and resize.

Drag a GroupBox to the right of the label and buttons. objectname: gboxFormatOut. title property: ‘Output Format’. Horizontal alignment: AlignHCenter. Vertical alignment: AlignVCenter. Drag the size of the GroupBox to fill the area to the right of the existing objects. We need it to be big enough to contain four radiobuttons with fairly lengthy descriptions.

Drag a radioButton to the inside of the groupBox. objectname:rbutAndroidHD. text:’Android HD (Galaxy Note 2, S3, Transformer)’. Repeat another three times. {objectname:rbutAndroidqHD. text:’Android qHD (HTC Sensation, Galaxy S2)’} then {objectname:rbutAppleHD. text:’Apple HD (iPhone 4, iPhone 4S)’} and finally {objectname:rbutAppleFullHD. text:’Apple Full HD (iPhone 5, iPad2, iPad 3, iPad 4, iPad Mini)’}. You can align the radiobuttons by selecting all four ([ctrl]-[click] on each). [Right-click] whilst hovering on one of the four and Layout, Layout Vertically. This creates a VBoxLayout around the four radiobuttons highlighted in red.

Drag a label to below the GroupBox but on the left-hand side. This is purely a description so I am not worried about the object name. Leave it as ‘label’ but change the text to ‘File Name:’. Drag another label below the File Name label. This is also a desriptive label so leave objectName as the default ‘label_2’. Change text to ‘Output format:’. Drag the size of the labels to suit.

Now put another two labels, one to the right of each of label and label_2. Name them lblFilename and lblOutputFormat. Clear the text properties of both so the objects on the form are empty. Set the minimum size width property of lblFilename to 400 and lblOutputFormat to 150. You may need to fiddle a little to get the four fields aligned but the snap to grid we set up earlier will help.

The original tutorial loaded two graphics images onto the form. They don’t do much but we will add tham anyway. You can download them (together with the completed project) from PythonTutorial. You can use QT Designer to load them from the directory where you unpack them or you can create a subdirectory ‘media’ within the directory where you have created the mainwindow.ui file and copy the files (h264logo.jpg and webmlogo.png) there. Drag two labels onto the form and name them image1 and image2. If you look at h264logo.jpg with an image viewer you will see that it is 175×110 pixels so set the size of the label the same. Expand the text property by clicking the + to the left and set the filename in the pixmap property. This will set an absolute address which is fine for now although it would be better to create a resource file from QT Designer so the the file location can be referenced relative to the ui file. I have in fact done this with the download. Similarly load webmlogo.png into image2 at a size 201×47 pixels. Move the two images to the right-hand side below the groupbox being sure to avoid the invisible filename label (click on lblFilename in the object inspector to see the handles).

Right. We’re almost there. Drag a label onto the form below the Outout format label. ObjectName: lblStatus. Blank the text property. Change font size to 15. Change Geometry width to 200. Drag a textBrowser onto the form and drag it to fill most of the remaining space at the bottom of the form. ObjectName: tbrowProgress.

Tou should end up with a form which looks like the one below (I have selected the three blank labels so they can be seen). This form does not really conform to good practise in that the contained objects do not change their size as the size of the form changes. This can be achieved in QT Designer but is not required for this demo.

pythonTut

— convert the .ui file to .py —

Save any changes to the mainwindow.ui file. Python can use this file directly but for consistency we will convert it to a standard python text file with pyuic. Open a terminal and navigate to the directory where you have saved the mainwindow.ui. Type ‘pyuic4 mainwindow.ui -o mainwindow.py’ [enter]. You will see that this creates a mainwindow.py file.

— Write the code —

Open the Idle IDE. It should be in the development section of your menu. The Python Shell will open. Click on File, New Window and an untitled, empty window will open. What’s he minimum we have to do to get some signs of life out of our application? Copy the following into the empty window:

#10----------------------------------------------
from PyQt4 import QtCore, QtGui
import sys
from os.path import expanduser
import os
import shlex

#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)
#20-----------------------------------------------

def main():
    app = QtGui.QApplication(sys.argv)
    window = Main()
    window.show()
    sys.exit(app.exec_())

main()
#200---------------------------------------------

Save the file as main.py in the same directory as mainwindow.py. Having saved it, Press [F5] to run (or Run, Run Module) and the form should appear on the screen. If it does not the Python Shell window will give an indication of where the problem lies. Assuming all is well, you will be able to click on the two buttons (although there will be no response because we have not tied any code to the action. duh.) QT is also clever enough to allow us to select one (and only one) of the radio-buttons. If you change the size of the form you will notice that the size of the contained objects does not change and can become truncated. Qt can solve this although it is beyond the scope of this tutorial. We are also able to close the window by clicking on the top-right corner,

How did the program do this? The first line loads the essential elements of the Python-QT4 interface so that we can use the QT designer interface mainwindow.py. Import sys imports (amongst others) two classes that we need: sys.argv allows us to utilise command-line parameters while sys.exit allows us to close the program. The command-line argument is important because when we press [F5] from Idle, the shell is actually running the command ‘python3 main.py’ for us. main.py is therefore a command line parameter. We will use expanduser whilst finding a graphics file. Shlex is used to create the avconv argument.

Next we import the class Ui_MainWindow from the file mainwindow.py. This is our screen definition.

def __init__(self) creates a window and defines the objects it contains from Ui_MainWindow().

Function definition def main() defines which application will be loaded by the interpreter (sys.argv or in this case main.py), shows the window and allows for the exit.

The final line runs the definition main() to display the application.

It is common for Python applications to start with a shebang line so add ‘#!/usr/bin/env python3’ to main.py as the first line. ‘#!’ is the shebang . The program loader takes the presence of these two characters as an indication that the file is a script, and tries to execute that script using the interpreter specified by the rest of the first line in the file. This allows you (subject to file permissions) to run the application from the command line with ‘./main.py’ rather than ‘python3 main.py’. Useful? I thought not.

It is also common in Python to see ‘if __name__ == “__main__”:’ on the line before main(). The variable __name__ is set to __main__ if the application itself is called from the command line. In this case main() is run. You might want however to access some of the functionality of the script from another application in which case __name__ would not be __main__ and the window would not be opened.

Right. Let’s get the application doing something. Copy the following below ‘#20’. The first line here is double indented so make sure you insert these lines from the left-hand margin.

#30-----------------------------------------------------------
        # Connect the Radio buttons
        QtCore.QObject.connect(self.ui.rbutAndroidHD,QtCore.SIGNAL("toggled(bool)"),self.androidHDSelected)
        QtCore.QObject.connect(self.ui.rbutAndroidqHD,QtCore.SIGNAL("toggled(bool)"),self.androidqHDSelected)
        QtCore.QObject.connect(self.ui.rbutAppleHD,QtCore.SIGNAL("toggled(bool)"),self.appleHDSelected)
        QtCore.QObject.connect(self.ui.rbutAppleFullHD,QtCore.SIGNAL("toggled(bool)"),self.appleFullHDSelected)
#40-----------------------------------------------------------
    def androidHDSelected(self):
        self.ui.lblOutputFormat.setText("Android HD")

    def androidqHDSelected(self):
        self.ui.lblOutputFormat.setText("Android qHD")

    def appleHDSelected(self):
        self.ui.lblOutputFormat.setText("Apple HD")

    def appleFullHDSelected(self):
        self.ui.lblOutputFormat.setText("Apple Full HD")
#150-----------------------------------------------------------

The # of the first line should be directly below the S in ‘self.ui.setupUi’. i.e. indented twice. Save main.py and run again with [F5]. Now when you click on a radiobutton, the description is displayed in lblOutputFormat.

Let’s get a button working with the following code which should go below the ‘#40’ line. Once again, the # is indented twice (i.e. part of def __init__(self)) and the ‘def selectFile(self):’ indented once.

#50-----------------------------------------------------
        # Connect the Buttons
        QtCore.QObject.connect(self.ui.btnSelectFile,QtCore.SIGNAL("clicked()"),self.selectFile)
#60----------------------------------------------------
    def selectFile(self):
        fileName = QtGui.QFileDialog.getOpenFileName(self,'Open Media File',expanduser("~"),'Media Files (*.mov *.avi *.mkv *.mpg)')
        self.ui.lblFilename.setText(fileName)
#140--------------------------------------------------

Save and run. Clicking the ‘select media file’ button will display a filechooser window. Select a media file and the fiechooser will close. The full path of the selected file is displayed by lblFilename.

We have selected the input file and the output format so we are now ready to do the actual conversion by programming the ‘convert’ button. Add the following lines below ‘#60’ (so that’s why ‘buttons’ was plural!).

#70------------------------------------------------------
        QtCore.QObject.connect(self.ui.btnConvert,QtCore.SIGNAL("clicked()"),self.convert)

        #Setup Process
        self.process = QtCore.QProcess(self)
#80-----------------------------------------------------
    def convert(self):
        self.convertFile()

    def convertFile(self):
        inputFile = str(self.ui.lblFilename.text())
        outputFormat = str(self.ui.lblOutputFormat.text())

        if inputFile == (''):
            QtGui.QMessageBox.warning(self,"Media Not Selected","Please select a file to convert")
            return ('Selection Not Proper')

        if outputFormat == ('Android HD'):
            cmd = '-i "%s" "%s.webm"'
        elif outputFormat == ('Android qHD'):
            cmd = '-i "%s" "%s.webm"'
        elif outputFormat == ('Apple HD'):
            cmd = '-i "%s" "%s.mp4"'
        elif outputFormat == ('Apple Full HD'):
            cmd = '-i "%s" "%s.mp4"'
        elif outputFormat == (''):
            QtGui.QMessageBox.warning(self,"Input Format Not Selected","Please select an appropriate Output Format")
            return ('Selection Not Proper')

        argument = shlex.split(cmd % (inputFile,inputFile[:-4]))
        command = "avconv"
        self.ui.lblStatus.setText("Please Wait....")
        self.ui.label_2.setText(command)
        self.ui.btnConvert.setDisabled(True)
        self.process.start(command,argument)
#130----------------------------------------------------

The code reads the input file and output format from the screen labels and checks if either has not been defined. Choose an Apple mp4 output as it is quicker for testing. A command line argument is then defined depending on the chosen radiobutton. As mentioned, I am unable to check the two qualities of output for each format so have left them the same. If anyone wants to follow that through, please let me know of the results and I will incorporate them into the tutorial. %s is a placeholder for the filepath we have used. We use shlex to substitute the chosen file path so that the argument becomes “-i ‘input file’ ‘output file'”. We also set a couple of screen texts and disable the convert button. Finally we start the process.

The process runs avconv with the argument and we are left with a window where lblStatus is set at “Please wait…..”. If you direct your file manager to the same directory as your imput file, you will see the output file listed. We could really do with some indication the the application has done its job.

Close the application and delete the created file with the file manager. We have not yet programmed a check on an existing output file and the application will hang if it finds an existing output file demanding a response to the question “Overwrite (y/n)?”. We have no way of giving that answer.

Copy the following line below the ‘#80’ :

#110-------------------------------------------------------
        QtCore.QObject.connect(self.process,QtCore.SIGNAL("finished(int)"),self.processCompleted)
#120-------------------------------------------------------

This will call self.processCompleted when the process is finished. Let’s add this function definition by adding the following below ‘#150’.

#160---------------------------------------------------------
    def processCompleted(self):
        self.ui.lblStatus.setText("Conversion Complete")
        self.ui.btnConvert.setEnabled(True)
#170---------------------------------------------------------

Run the program again making sure that the ouput file does not already exist. The ‘Please wait…’ message will change to ‘Conversion completed’ when the process is complete.

We can show the progress of conversion in the text browser by adding the following line as the last line under ‘#80’

#90----------------------------------------------------
        QtCore.QObject.connect(self.process,QtCore.SIGNAL("readyReadStandardError()"),self.readStdError)
#100----------------------------------------------------

This will pipe and standard output to a function def readStdError. Let’s add this below ‘#170’:

#180-------------------------------------------------------
    def readStdError(self):
        self.ui.tbrowProgress.append(str(self.process.readAllStandardError()))
#190--------------------------------------------------------

Remember to delete previous output files and run the application. The standard output progress is now added to the text browser.

That’s about as far as I want to go. There is always more to do and it would obviously be helpful to check if the output file already exists before running the conversion. I’ll leave that to you!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


%d bloggers like this: