Convert videos from H265/x265 to H264/x264 in Python with ffmpy

wordcloud

Background

There are times when we need to convert audio and/or video files from one format to another. May be you found an old VHS tape and want to convert it to a digital format, or want to compress high quality audio files to smaller sizes to fit in your portable device, or want to experiment with different AV formats.

A quick internet search will bring up numerous links to several audio/video conversion tools, some free, some paid, some with adware and some with spyware or malware too. My first preference is always to look for opensource solutions and that's when I came across FFmpeg codecs.

FFmpeg

If you happened to listen to music a couple of decades ago, you'll remember that some of the audio files were available in Real Media format and the files had an extension .rm. These files played only Real Media Player and I was not a huge fan of their media player. I preferred the universal MP3 audio format and was a fan of Winamp because it really whips the llama's ass (Winamp users will get this! Play the clip below for nostalgia.).

I wanted to convert my Real Media audio files to MP3, and that is when I came across FFmpeg. FFmpeg is a free and open-source software project consisting of a large suite of libraries and programs for handling video, audio, and other multimedia files and streams. At its core is the FFmpeg program itself, designed for command-line-based processing of video and audio files. Most of the A/V converter tools use the free FFmpeg program in the background. If you are familiar with basic command line concepts, you can download FFmpeg and use it to convert audio and video files. Once I got the hang of it, I was able to write a batch file and convert my Real Media audio library to MP3. FFmpeg also offers another free command-line program called ffplay that is capable of playing almost every kind of audio or video file.

ffmpeg-python

The FFmpeg CLI is extremely powerful and works brilliantly if you can manage to write its somewhat complex code. If you're like me, you might at times prefer doing things in Python instead as I find it both powerful and easily readable. ffmpeg-python is a Python FFmpeg wrapper that supports simple as well as complex signal graphs and makes it easier to write code.

ffmpy

Can it get even more convenient and readable? The answer is yes. ffmpy is another Python FFmpeg wrapper that, in my opinion, is much easier to use than ffmpeg-python. Since I have used FFmpeg CLI before, I was looking for a wrapper that is closest to it in syntax and ffmpy is the best I could find so far.

In layman terms, an FFmpeg command can be thought of consisting of three parts - an input file, a set of arguments, and an output file. The arguments are optional for simple conversions as FFmpeg is clever enough to process coversions based on file extensions.

For example, the command for converting an AVI video file named 'input.avi' to an MP4 video with the name 'output.mp4' is simply the following. FFmpeg knows from the file extensions that it has to convert an AVI file to an MP4 file.

ffmpeg -i input.avi output.mp4

When performing complex conversions that require specific codecs, quality, etc we will need to pass necessary arguments as well. In the example below, the arguments specify that the video codec to be used is x264, that the FFmpeg preset is set to slow for better video quality, that another quality parameter of crf 19 is passed and that the audio is encoded with AAC with a bitrate of 128K. The FFmpeg documentation has more information on arguments, APIs and libraries.

ffmpeg -i input.avi -c:v libx264 -preset slow -crf 19 -c:a libvo_aacenc -b:a 128k output.mp4

ffmpy's syntax has similar components with a different layout. The input/output files and their arguments are passed in the form of a dictionary instead of a string.

ffmpy.FFmpeg(
    inputs={'input.avi': None},
    outputs={'output.mp4': None}
    )

None in the outputs dictionary can be replaced with arguments if any.

ffmpy: converting videos from H265 to H264 format

H265 is the one the latest video formats that provides high compression but also requires more powerful processors to play them. x265 is a common codec that can play an H265 video. H265 encoded video files are comparitively smaller in size than those encoded with H264, which is a predecessor of H265. If you try to play H265 videos, especially the ones in HD and above in an older or a slower PC, you may notice considerable amount of lag and pixelation. The video appears choppy and pauses intermittently waiting for the processor to decode. However, most of these older or slower computers can play H264 files very smoothly even in full HD.

In this example, I will attempt to convert my Star Wars Rebels video collection from H265 video format to H264 format with ffmpy wrapper. My starwars directory contains subdirectories for each season, namely S01, S02, S03 and S04. Each season directory then contains its episodes in H265 encoded MP4 files.

starwars
    |__ S01
    |__ S02
    |__ S03
    |__ S04

I would like to traverse files in each subdirectory (or season), convert them to H264 videos and place them in a new directory for that season as below. The new subdirectories are already created.

starwars
    |__ S01
    |__ S01_264
    |__ S02
    |__ S02_264
    |__ S03
    |__ S03_264
    |__ S04
    |__ S04_264

I will use Python's glob module to traverse through files and get their filenames.

Convert 10-bit H.265 to 10-bit H.264

The FFmpeg command to convert a 10-bit H.265 to a 10-bit H.264 video file is

ffmpeg -i input -c:v libx264 -crf 18 -c:a copy output.mkv

This conversion is almost lossless and maintains equivalent audio/video quality as the source file. If you want true lossless conversion, use -crf 0 instead but also be wary of the output file size.

Install the ffmpy package.

In [1]:
!pip install ffmpy
Collecting ffmpy
  Downloading https://files.pythonhosted.org/packages/9e/12/896dbb507a02a46eb6488fefc0b74a8c33eec59257ee3c35a2f41563c48e/ffmpy-0.2.3.tar.gz
Building wheels for collected packages: ffmpy
  Building wheel for ffmpy (setup.py) ... done
  Created wheel for ffmpy: filename=ffmpy-0.2.3-cp36-none-any.whl size=4623 sha256=be1a48cc4c3e4375dee1a71c997845755593fd5e842a04cfacec472c3b5fa50f
  Stored in directory: /root/.cache/pip/wheels/99/51/33/ca9d92c9fa38a196ad8647f5fcc9eadbca8a96bd5f8a2b5f7d
Successfully built ffmpy
Installing collected packages: ffmpy
Successfully installed ffmpy-0.2.3
In [2]:
# Importing libraries
from ffmpy import FFmpeg
from glob import glob
In [3]:
# Input the season number to be converted.
season=input("Season:")
# Get the full directory path for the season.
# If you're running this on Google Colab, don't forget to mount the drive first.
dirpath='/content/drive/MyDrive/starwars/S0'+season+'/'
# For each .mkv file in the given directory
for infile in sorted(glob(dirpath+'*.mkv')):
    # Create an output filename and path using string replacements.  
    outfile=infile.replace('265-HETeam','264').replace('/S0'+season+'/','/S0'+season+'_264/')
    # Define ffmpy parameters  
    ff=FFmpeg(
      inputs={infile:None},
      outputs={outfile:['-c:v', 'libx264', '-crf', '18', '-c:a','copy']}
    )
    # Convert
    ff.run()
    # If successful, print full output filename
    print(outfile)
Season: 1
/content/drive/MyDrive/starwars/S01_264/Star.Wars.Rebels.S01E01.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S01_264/Star.Wars.Rebels.S01E02.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S01_264/Star.Wars.Rebels.S01E03.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S01_264/Star.Wars.Rebels.S01E04.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S01_264/Star.Wars.Rebels.S01E05.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S01_264/Star.Wars.Rebels.S01E06.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S01_264/Star.Wars.Rebels.S01E07.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S01_264/Star.Wars.Rebels.S01E08.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S01_264/Star.Wars.Rebels.S01E09.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S01_264/Star.Wars.Rebels.S01E10.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S01_264/Star.Wars.Rebels.S01E11.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S01_264/Star.Wars.Rebels.S01E12.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S01_264/Star.Wars.Rebels.S01E13.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S01_264/Star.Wars.Rebels.S01E14.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S01_264/Star.Wars.Rebels.S01E15.720p.BluRay.x264.mkv
  • In the above example, 'infile' refers to each input filename sorted in alphabetical order. glob returns the filename along with its full path, so there is no need to add path again.

    for infile in sorted(glob(dirpath+'*.mkv'))

  • The output filename 'outfile' is created from 'infile' by two string replacements. First the '265-HETeam' part of the filename is replaced with '264', then the directory name 'S01' in the path is replaced with 'S01_264'.

    outfile=infile.replace('265-HETeam','264').replace('/S0'+season+'/','/S0'+season+'_264/')

  • It is important to pass the FFmpeg arguments in the form of a list of strings and not as a single string. outputs={outfile:['-c:v', 'libx264', '-crf', '18', '-c:a','copy']}

I have successfully converted all videos in my S01 directory to H264 format. If I have to convert another season, let's say season 2, then I run the same code block from above and enter '2' when prompted.

In [4]:
# Input the season number to be converted.
season=input("Season:")
# Get the full directory path for the season.
# If you're running this on Google Colab, don't forget to mount the drive first.
dirpath='/content/drive/MyDrive/starwars/S0'+season+'/'
# For each .mkv file in the given directory
for infile in sorted(glob(dirpath+'*.mkv')):
    # Create an output filename and path using string replacements.  
    outfile=infile.replace('265-HETeam','264').replace('/S0'+season+'/','/S0'+season+'_264/')
    # Define ffmpy parameters  
    ff=FFmpeg(
      inputs={infile:None},
      outputs={outfile:['-c:v', 'libx264', '-crf', '18', '-c:a','copy']}
    )
    # Convert
    ff.run()
    # If successful, print full output filename
    print(outfile)
Season: 2
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E01.Part1.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E01.Part2.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E02.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E03.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E04.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E05.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E06.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E07.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E08.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E09.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E10.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E11.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E12.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E13.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E14.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E15.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E16.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E17.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E18.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E19.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E20.Part1.720p.BluRay.x264.mkv
/content/drive/MyDrive/starwars/S02_264/Star.Wars.Rebels.S02E20.Part2.720p.BluRay.x264.mkv

Convert 10-bit H.265 to 8-bit H.264

Following is the FFmpeg command to convert an H265 video to an H264 video with a smaller file size.

ffmpeg -i input -c:v libx264 -crf 18 -vf format=yuv420p -c:a copy output.mkv

8-bit output can be obtained by chosing the yuv420p pixel format. As discussed earlier, for a true lossless conversion, use -crf 0 instead.

The output arguments for the ffmpy wrapper would be

outputs={outfile:['-c:v', 'libx264', '-crf', '18', '-vf', 'format=yuv420p', '-c:a','copy']}

Last updated 2020-12-09 22:59:49.140846 IST

Comments