Back in the good old days, I wrote lots of apps, including a number of audio/video converters.
They tapped into the popular FFmpeg video converter and brought out many of its complex command-line switches into easy-to-use buttons, sliders and comboboxes. Essentially, they were the ‘front end’ controls for the FFmpeg ‘back end’.
Those apps were written using a rapid application development (RAD) system called RealStudio (now Xojo), but many languages support this kind of thing, including Java. Here we’ll create a simple app using FFmpeg that converts almost any video file into MP4 format.
What you’ll need
Before we get going, you need the latest FFmpeg executable, which you can grab from here.
Download the ‘ffmpeg-latest-win32-static.7z’ file and unzip it somewhere using 7-Zip.
Unzip the source code from the APC website and import the inner ‘videcon.zip’ file into NetBeans using the ‘File, Import Project, From Zip’ menu option sequence.
How it works
Looking at things in broad brushstrokes for a moment, our app has two main components — the graphical user interface and the video conversion control mechanism.
We’ve created the interface using NetBean’s GUIBuilder and we bring in FFmpeg to do the ‘heavy lifting’.
But it’s not just that — we need some way to launch the FFmpeg executable (.exe) file from within our app, a way to read the ongoing output from FFmpeg, and a way to kill FFmpeg should we need to without killing our app itself.
The best method is to use a separate thread.
Runtime & ProcessBuilder
Java has two mechanisms for launching external executable files.
The original method is Runtime.getRuntime().exec(). It’s been around since Java was in nappies and, while it still works, it’s no longer the preferred method. Since Java 5 (1.5), that role has been taken over by the ProcessBuilder() class.
Each time you launch an app, you’re launching an instance of that app, called a ‘process’. Java allows you to create processes you can control, read output from and even kill off if you want and it’s this capability we’ll use here.
What’s the difference between getRuntime.exec() and ProcessBuilder()? For our purposes, its in how they handle the actual FFmpeg commands we feed in.
FFmpeg has a million and one options, but if you’re happy to use the default settings, you can, for example, convert an AVI (audio-video interleaved) file into MP4 using just:
ffmpeg –i “c:\downloads\input.avi” –y “c:\video files\output.mp4”
The command-line tool automatically detects the video and audio codecs used by the AVI input file and knows the codecs required when you specify an output file with a ‘MP4’ suffix (H.264 video and AAC audio codecs).
The ‘-y’ code or ‘switch’ just tells FFmpeg to overwrite the output file, even if it already exists (but this means your app must warn users attempting to select an existing file).
See the space between the words ‘video’ and ‘files’ in that output file path? These spaces need to be accounted for not just by FFmpeg (which is why each path is encased in double-quotes), but also by Java. Otherwise, it won’t work.
The normal method for handling this is to use ‘escape sequencing’, where you add a backslash (\) to ensure Java knows to parse the following special character as a text character and not part of an programming statement.
For example, double-quotes (“) are normally used to surround text for string assignment, but you may want to have them appear in a string also. Here’s an example:
String text1 = “This is a string with no quotes”;
String text2 = “This string has the quote \”coding is fun\””;
Print these strings and for ‘text1’, you’ll get:
This is a string with no quotes
But here’s what you’d see with ‘text2’:
This string has the quote “coding is fun”
The escape sequence tells Java how to handle the next character – you can see the two double-quotes at the end of the ‘text2’ string, the first is displayed; the second signals the end of the string.
Getting back to FFmpeg command-driving, Windows allows spaces in filepaths, but FFmpeg uses a space to separate command parameters, so using the double-quotes ensures FFmpeg knows all of the text within the double-quotes is the filepath and not part of a command-line parameter separator.
In terms of getRuntime.exec() and ProcessBuilder(), getRuntime.exec() has the same trouble with spaces as Ffmpeg. However, in order to send the double-quotes to FFmpeg, we have to use the escape sequence, making things doubly awkward.
ProcessBuilder(), on the other hand, internally handles spaces, letting you send each parameter as part of a parameter list.
We’ve implemented both options in the source code — the convertVideo() method uses the getRuntime.exec() method, while convertVideoPB() uses the newer ProcessBuilder() alternative.
The source is set to use the ProcessBuilder() version initially, but just go to the VideoThread() class and change the last line in the run() method from ‘convertVideoPB’ to ‘convertVideo’ to use the getRuntime.exec() option.
There’s no practical difference in the video conversion speed using either one — you’ll just get a bit of practice in how they work.
In its current form, the user interface has just two FFmpeg function buttons, Convert and Cancel, which start and stop the video conversion process, respectively.
The other three buttons in the GUI are for selecting the source file, the save file location and the location of the FFmpeg executable.
We won’t cover file selection here as they’re all basically the same — we use the JFileChooser() class to create a customised file dialog box, which allows us to grab the filepath of the file for each section and then display them in the corresponding JTextFields.
The app expects you to set all three filepaths before you can convert a file. But once that’s done, you’re ready to hit the Convert (jButton3) button and as soon as you do, the jButton3ActionPerformed() method is triggered and runs its code.
That starts with a warning if you’ve forgotten the Ffmpeg path; otherwise, it ends with:
Thread videoThread = new Thread(new VideoThread());
Here, we’re launching FFmpeg as the second thread — the second line creates a new Thread object called ‘videoThread’ and the third line starts it.
Take a quick look at the VideoThread() class and you’ll see it implements the Runnable interface. Implementing Runnable is the key to enabling a class to run as a separate thread and the code we run has to be located in the class’s run() method.
The little trick is that we call ‘VideoThread.start()’, not ‘VideoThread.run()’ because calling the latter simply runs the code in that run() method — you need the start() method, which first creates the new thread and runs the code inside run().
Cancelling a process
Launching an external app as a separate process and thread is all well and good, but we need some way of being able to stop it.
Imagine watching your car run downhill, off into the sunset…
Java has your back here, with a simple solution that’s easy to implement.
Notice in the source code we’re creating an instance of the Process class called ‘processFfmpeg’ and using it as the handle for either the getRuntime.exec() or ProcessBuilder options, based on which one you activate.
The Process class lets you kill that process by just calling the Process.destroy() method. Look at the source code for the Cancel button (jButton5) and it has just two lines:
jTextArea2.append(“>>> CONVERSION CANCELLED BY USER. \r\n”);
The first line kills the FFmpeg app process, the second provides notification to the screen. That’s about as simple as it gets.
Why threads are important
FFmpeg provides lots of useful output during transcoding — each line gives you a rolling update on the current frame being encoded, overall bit rate, file size, file time position and average frames per second (fps) conversion speed.
Because we’re using Java’s Process class, we can programmatically grab that information relatively easily and display it in real-time.
It comes back from FFmpeg via the process class through two streams — the ‘error’ stream and the ‘input’ stream.
The error stream contains any information output by FFmpeg if it crashes or ends, while the input stream returns the rolling FFmpeg output data (output from FFmpeg becomes input for Java, hence the use of ‘getInputStream’).
Look in the source code for the methods ‘convertVideo’ and ‘convertVideoPB’ and you’ll see we’re implementing a BufferedReader for each, ‘bres’ for the error stream and ‘bris’ for the input stream.
A while() loop combined with the BufferedReader’s readLine() method reads the data from each buffer and appends it to jTextArea2 on our GUI display.
This is one reason why having FFmpeg on a separate thread is crucial. If Ffmpeg runs on the main/GUI thread, you’d see nothing on the jTextArea2 GUI component until FFmpeg completed its run because the GUI/main thread would have to wait for FFmpeg to finish before it could update the GUI itself, which is useless.
One area we haven’t looked at much is error-handling.
You’ll find books on this topic alone, so we can’t do it justice here. But for the moment, it’s enough to say significant app development time normally goes into ensuring users can’t break your app.
For example, let’s say the user has everything selected and presses the Convert button to launch the conversion process. What happens if they press it again?
If they press the button while conversion is continuing, Java will action the button press, start another thread and launch a new version of FFmpeg to begin transcoding the same file. In fact, it’ll create a new version of FFmpeg inside a new thread for each Convert-button press.
That’s bad for lots of reasons, but here’s just two.
Having multiple versions of FFmpeg encoding the same file to the same destination from the start just slows the real conversion process by a factor of the number of Ffmpeg copies running, since each process demands CPU time. And at the end, when each one tries to close the output file, it’ll turn pear-shaped real quick.
However, far worse is this — each time you press the Convert button, you’re telling Java to create a new Process instance and assign it to the variable ‘processFfmpeg’.
But what happens to the previous thread assigned to ‘processFfmpeg’? Assigning a new process to ‘processFfmpeg’ doesn’t kill off the previous one — it just abandons it. The only process your app can now kill is the latest one assigned to ‘processFfmpeg’.
You’ve lost control of any previous processes and the only way to stop them is to go into Windows’ Task Manager (if you’re using Windows), look for the ffmpeg.exe background processes and kill them off manually. That’s hardly text-book user experience!
The simplest solution is to just disable the Convert button as soon as it has been pressed and only re-enable it when either the Cancel button as be pressed or the transcode process is completed or stops.
That way, you shouldn’t have multiple process threads dangling, chewing up resources, with no control — and that’s what the ‘jButton3.setEnabled(false)’ statement does right at the start of the jButton3ActionPerformed() method.
Using the app
Finally, using the app is pretty straightforward — press the FFmpeg path button and locate the ffmpeg.exe file from your downloaded zip (hint: it’s in the ‘bin’ subfolder).
Next, press the Source button and choose your source video file. After that, press the Save file button and choose a savefile location.
When you’re done, press Convert to start, Cancel to stop. FFmpeg will automatically use whatever number of cores your CPU offers to maximise conversion speed.
Overall, the key feature that makes this app work is splitting off that FFmpeg process into a separate Java thread.
If you try to keep it on the ‘main’ GUI thread, the GUI will just lock up until FFmpeg completes the conversion — that not only means no on-the-go output updates, but importantly, you have no way to kill the FFmpeg process if necessary, either.
If you’re trying to convert a 4K rip of a three-hour movie using a single-core Celeron computer, you could lose control of the GUI for days!