Table of Contents
Today I found myself in a somewhat annoying situation. After obtaining in a totally legal and lawful way a large collection of mp4 videos, I loaded them on my external hard drive, plugged the hard drive into my TV, hit play on the first video… and the audio was not recognized. Drama and despair, there is no possibility of finding these videos in another format, they only exist in this one, so I got down to work.
What is audio encoding?
If you, like me, are somewhat ignorant in these aspects of multimedia, I will briefly explain why my TV could not play the audio of these mp4 files.
An mp4 file can be explained as a package that compresses audio and video separately. Both video and audio have their own encoding, which in short is the way their data is stored and transmitted.
Therefore, if the player (my TV) does not have the appropriate codecs, it will not know how to read that encoding format. Codecs are the packages of algorithms that allow encoding, in this case, decoding, the video/audio data.
And if my TV does not have the codecs to read that particular audio encoding, what do I do? Well, change the encoding of the videos. And this is where FFmpeg comes in.
FFmpeg is an open-source project to manage multimedia files that allows you to see the detail of these with FFprobe, play them with FFplay, and also allows you to modify them with FFmpeg.
rruiz@rruiz:~/Videos$ ffprobe video1.mp4
ffprobe version 4.2.7-0ubuntu0.1 Copyright (c) 2007-2022 the FFmpeg developers
built with gcc 9 (Ubuntu 9.4.0-1ubuntu1~20.04.1)
configuration: --prefix=/usr --extra-version=0ubuntu0.1 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-nvenc --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
libavutil 56. 31.100 / 56. 31.100
libavcodec 58. 54.100 / 58. 54.100
libavformat 58. 29.100 / 58. 29.100
libavdevice 58. 8.100 / 58. 8.100
libavfilter 7. 57.100 / 7. 57.100
libavresample 4. 0. 0 / 4. 0. 0
libswscale 5. 5.100 / 5. 5.100
libswresample 3. 5.100 / 3. 5.100
libpostproc 55. 5.100 / 55. 5.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'video1.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
title : video 1 (1080p)
artist : WinX HD Video Converter Deluxe
encoder : Lavf58.29.100
Duration: 00:24:39.56, start: 0.000000, bitrate: 3737 kb/s
Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 1920x1080 [SAR 1:1 DAR 16:9], 3455 kb/s, 30 fps, 30 tbr, 15360 tbn, 60 tbc (default)
Metadata:
handler_name : VideoHandler
Stream #0:1(cat): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 276 kb/s (default)
Metadata:
handler_name : SoundHandler
Here we can see that stream #0:0(und) contains the video track in h264 encoding, which a quick search on the internet will tell us is a video encoding standard also known as AVC (advanced video encoding) or MPEG-4 Part 10. But this is not the part that interests us.
In the stream #0:1(cat) we find the audio track, with aac encoding (advanced audio encoding), this is the encoding that, for some reason, my TV does not know how to play.
It is not the first time that this happens to me, generally, it happens with mkv videos, a video format that does not have a standard of encodings as defined as mp4 and that causes more easily that the player does not have the appropriate codec. However, for some reason, either the aac version of these videos or any other, the audio of these mp4 videos is not compatible with my TV.
How can I change the video encoding?
If with FFprobe I can get the information from the video, with FFmpeg I can change it.
ffmpeg -i video1.mp4 -acodec mp3 -vcodec copy encoded_video1.mp4
The documentation of FFmpeg is long and complete, but in our case, we only need to know a couple of options:
–i: input, the video we want to modify, in this case, video1.mp4.
–acodec: the audio codec we want the output file to have, in our case mp3.
–vcodec: the video codec we want the output file to have, as we don’t want to change it, we put copy.
Finally, we put the name of the output file, in this case, encoded_video1.mp4.
Automate it
Great, I already know how to change the encoding of a file, but I have many videos. I can’t be typing the command one by one, what options do I have?
for loop
for filename in *.mp4; do
ffmpeg -i $filename -acodec mp3 -vcodec copy encoded_$filename;
done
A for loop would be the first choice, but it has a few drawbacks, it will process the files one by one. My PC has 16 cores and FFmpeg by default uses only one thread. It is inefficient and slow for many medium-large files.
FFmpeg -threads
FFmpeg has the possibility to define a threadpool to be used in the processing:
ffmpeg -threads 4 -i video1.mp4 -acodec mp3 -vcodec copy encoded_video1.mp4
-threads 0 implies that FFmpeg will choose the optimal amount of threads, with -threads 1 it will be monothread, and so on, as long as it can parallelize.
However, the amount of threads it will use really depends on the version of FFmpeg in question, the codec you are using, and the cores of your PC. It is not very consistent, and in my case even if I put 4 threads on it, it has never gone beyond 1.
Forks? Parallel!
If FFmpeg does not allow me to exploit the 16 cores of my PC, I will have to create the threads myself, and just when I was refreshing how to handle a thread pool in bash to process files in batches, I find parallel.
Paralell is a gnu/bash command that allows you to pass it a list of N arguments and a command. It will execute the command N times each time with one of the arguments by default in parallel with as many jobs as your PC has cores.
This way if your PC has 4 cores and you pass it 40 arguments, it will parallelize 4 of them, processing the next one when it has a free core. Great, just what I needed.
However, it allows you to indicate the maximum number of jobs you want it to use, so as not to saturate the PC, with -jobs. They also recommend using -ungroup for more efficiency, this allows you to paint the asap logs, resulting in logs from different commands mixed together.
ls *.mp4 | parallel --ungroup --jobs 6 echo
In this example, the arguments that we pass to parallel are the list of mp4 files in the current folder, and the command that it will execute in parallel is… echo. It is not useful, but it is useful to see the syntax.
It is time to assemble a small script to change the encoding of the videos in a folder:
#!/bin/bash
encode() { ffmpeg -i $1 -acodec mp3 -vcodec copy encoded_$1; }
export -f encode
ls *.mp4 | parallel --ungroup --jobs 14 encode
In this simple script, we define an encode function that contains the use of the FFmpeg command as explained above.
Then we export the function so that it can be used by parallel.
Finally, we call parallel with all the mp4s in the current folder, using a maximum of 14 simultaneous jobs (cores).
Now that we already have a working prototype, we have to improve it to be able to use it in different folders:
#!/bin/bash
DIRECTORY=$1
JOBS=14
encode() { ffmpeg -i $1 -acodec mp3 -vcodec copy encoded_$1; }
export -f encode
cd "$DIRECTORY"
ls *.mp4 | parallel --ungroup --jobs $JOBS encode
We extract the source directory of the files from the $1 argument of the script and take the number of jobs out of a variable to make it more visible and easier to change.
A cd to the directory in question, and that’s it. The reason why the cd uses “” is so that it also works for directory names containing spaces.
Finally, we improve logging and video output:
#!/bin/bash
DIRECTORY=$1
JOBS=14
ENCODED_DIRECTORY=encoded
echo "changing audio encode to aac for mp4 files in directory $DIRECTORY, $JOBS files at a time, storing the newly encoded files in $DIRECTORY/$ENCODED_DIRECTORY"
encode() { echo "processing $2..."; ffmpeg -loglevel 0 -nostats -i $2 -acodec mp3 -vcodec copy $1/$2; }
export -f encode
cd "$DIRECTORY"
mkdir -p $ENCODED_DIRECTORY
ls *.mp4 | parallel --ungroup --jobs $JOBS encode $ENCODED_DIRECTORY
The -loglevel 0 -nostats options of FFmpeg allow its execution with loglevel panic, in case something fails we will not see it unless it is a fatal error, but the normal execution will be faster and cleaner. We can always remove these options later to see what is failing or change the loglevel to fatal(8), error(16), warning(24), etc.
We will create an “encoded” subdirectory, specified in the ENCODED_DIRECTORY variable. We will pass this variable to the encode command inside the parallel so that the function receives it as the first argument, and we will use it in the function to save the output of FFmpeg in a subdirectory while keeping the name.
To understand it better, we can use the -dryrun option of parallel:
rruiz@rruiz:~/Videos$ ls *.mp4 | parallel --dryrun --jobs 14 encode "subdirectory"
encode subdirectory video1.mp4
encode subdirectory video2.mp4
encode subdirectory video3.mp4
encode subdirectory video4.mp4
And that’s it, now we can change the encoding of mp4 files in whole directories by shooting:
rruiz@rruiz:~/Videos$ ./encode_mp4.sh directorio_1
Author
-
Software developer with over 8 years experience working with different code languages, able to work as a FullStack Developer, with the best skills in the backend side. Passionate about new technologies, and the best software development practices involved in a DevOps culture. Enjoying mentoring teams and junior developers and I feel very comfortable with the stakeholders management-wise.
View all posts