Convert videos to H.265 / HEVC using FFmpeg and GPU hardware encoding
I’m using VDR for TV recording. Most public TV channels (via satellite) in Germany that I’m interested in are using MPEG-2 streams with H.264 encoding with a bitrate of about 10000-16000 kb/s. So for an hour of video it could take easily around 6 GByte on disk. This is quite a lot if you want to archive your videos.
I was wondering how to shrink these kind of videos without lowering the “visual” quality too much. And of course it should be fast and maybe even power efficient. First I used my CPU AMD 3900X for this task. This works quite well but uses quite some power. So I knew that AMD RX 5700/6700 (XT)
GPU supports H.265 hardware encoding. Without expecting to much I played around a bit and the outcome was surprisingly good - and with way less power needed!
That said I’m by no means a video encoding expert and my main focus just was to keep the same “visual” quality when converting from the MPEG-2 streams in H.264
to H.265
and reduce the size of the original recording by about 50%+. And I also did no audio re-encoding. Audio is copied 1:1.
So what I did to judge about the video quality was comparing two videos side by side with mpv
. On the left the original video and on the right the one with the H.265
encoding. I tried various video settings and compared again.
For the impatient here is what I came up with finally after playing around a while with various FFmpeg
parameters:
ffmpeg \
-vaapi_device /dev/dri/renderD128 \
-i "concat:00001.ts|00002.ts|00003.ts" \
-vf 'format=nv12,hwupload' \
-map 0:0 \
-c:v hevc_vaapi \
-map 0:a \
-c:a copy \
-rc_mode CQP \
-global_quality 25 \
-profile:v main \
-v verbose video.mp4
We’ll talk about the parameters in just a sec. Just to mention it the RX 5700/6700 (XT)
supports additional codecs for fast encoding. To figure out for which codecs GPU hardware video encoding acceleration can be used vainfo
command provides some information:
vainfo: VA-API version: 1.10 (libva 2.10.0)
vainfo: Driver version: Mesa Gallium driver 20.3.4 for AMD Radeon RX 5700 XT (NAVI10, DRM 3.40.0, 5.10.14-arch1-1, LLVM 11.0.1)
vainfo: Supported profile and entrypoints
VAProfileMPEG2Simple : VAEntrypointVLD
VAProfileMPEG2Main : VAEntrypointVLD
VAProfileVC1Simple : VAEntrypointVLD
VAProfileVC1Main : VAEntrypointVLD
VAProfileVC1Advanced : VAEntrypointVLD
VAProfileH264ConstrainedBaseline: VAEntrypointVLD
VAProfileH264ConstrainedBaseline: VAEntrypointEncSlice
VAProfileH264Main : VAEntrypointVLD
VAProfileH264Main : VAEntrypointEncSlice
VAProfileH264High : VAEntrypointVLD
VAProfileH264High : VAEntrypointEncSlice
VAProfileHEVCMain : VAEntrypointVLD
VAProfileHEVCMain : VAEntrypointEncSlice
VAProfileHEVCMain10 : VAEntrypointVLD
VAProfileHEVCMain10 : VAEntrypointEncSlice
VAProfileJPEGBaseline : VAEntrypointVLD
VAProfileVP9Profile0 : VAEntrypointVLD
VAProfileVP9Profile2 : VAEntrypointVLD
VAProfileNone : VAEntrypointVideoProc
For all entries with a value of VAEntrypointEncSlice
hardware encoding can be used and I’m using VAProfileHEVCMain
(which is HEVC/H.265 Main profile
). As you can see also Main 10
profile is supported if you want to use that one and of course H.264
. To give you an impression about encoding speed (H.264
vs H.265
): Using the same parameter just with the different encodings I get around 350-400 frames per second (fps) with H.265
and with H.264
I get around 800 fps. So if you’re in a hurry you might prefer H.264
;-) But for 1080p and even 1280p videos H.265
produces better results and smaller files.
So to get started you obviously need FFmpeg installed. It’s a complete, cross-platform solution to record, convert and stream audio and video (in case you never had heard of it). I’m running Archlinux which means that I’ve normally the latest package versions installed. In case of ffmpeg
that means v4.3.1
at the moment. It should be included in every Linux distribution so just use your package manager to install it.
Let’s talk about the FFmpeg
parameters used above:
-vaapi_device /dev/dri/renderD128
(which is a shortcut to -init_hw_device vaapi=vaapi0:/dev/dri/renderD128 -filter_hw_device vaapi0
) initializes a new hardware device of type vaapi
.
VDR
stores MPEG-2 transport streams (.ts
file suffix) in files using two GByte chunks. So I’ve to concat
the files which happens here: -i "concat:00001.ts|00002.ts|00003.ts"
. -i
specifies the input file(s). concat
is analogous to using cat
on UNIX-like systems. For more information see Concatenate. If your source video file is only one file then -i whatever_file.mp4
works too of course. Btw. not all video files can be concatenated with concat
filter. MPEG-2 streams are one of the few options were it works.
-vf 'format=nv12,hwupload'
creates the filtergraph and use it to filter the stream. The encoders only accept input as VAAPI surfaces. If the input is in normal memory, it will need to be uploaded before giving the frames to the encoder - in the ffmpeg utility, the hwupload
filter can be used for this. It will upload to a surface with the same layout as the software frame, so it may be necessary to add a format filter immediately before to get the input into the right format (hardware generally wants the nv12
layout). The hwupload
filter also requires a device to upload to, which needs to be defined before the filter graph is created which I did with -vaapi_device
.
Selecting which streams from which inputs will go into which output is either done automatically or with the -map
option. So using -map 0:0 -c:v hevc_vaapi
as in my example uses the first stream of the original file (which is the video in H.264
encoding) and converts it to H.265
in the destination file (hevc_vaapi
means H.265 / MPEG-H part 2 (HEVC)
). If you run the command later you’ll see the following debugging output:
Stream mapping:
Stream #0:0 -> #0:0 (h264 (native) -> hevc (hevc_vaapi))
Next we’ve -map 0:a -c:a copy
. This causes to copy the audio streams without modification to the new file. In the output it looks like this:
Stream #0:1 -> #0:1 (copy)
Stream #0:2 -> #0:2 (copy)
Stream #0:3 -> #0:3 (copy)
-rc_mode
sets the rate control mode
. To see all possible modes ffmpeg -h encoder=hevc_vaapi
can be used (this command shows all H.265
VAAPI options btw). These are the available rate control mode’s:
-rc_mode <int> E..V...... Set rate control mode (from 0 to 6) (default auto)
auto 0 E..V...... Choose mode automatically based on other parameters
CQP 1 E..V...... Constant-quality
CBR 2 E..V...... Constant-bitrate
VBR 3 E..V...... Variable-bitrate
ICQ 4 E..V...... Intelligent constant-quality
QVBR 5 E..V...... Quality-defined variable-bitrate
AVBR 6 E..V...... Average variable-bitrate
Currently the driver only supports CQP
, CBR
and VBR
. After testing a while it turned out that COP
works best for me.
The most important parameter for me is -global_quality
. The lower the value the better is the video quality but of course also the files are larger. Values between 20
and 25
are the most interesting ones. The default value is 25
which isn’t that bad. For me this rules works best:
If I’ve a video/movie that was produced in HD a value between 21-23
works best and produces good results. But there are also videos/movies that where produced before HD was en vouge. When broadcasted they are scaled to HD e.g. In this case a value between 24-25
works best.
And finally there is -profile:v main
. This selects the x265
profile. main
should work with every output device. A list of profiles can be found here.
video.mp4
is the output file.
That’s it so far! Happy encoding! 😉
Further links:
Using VAAPI’s hardware accelerated video encoding on Linux with Intel’s hardware on FFmpeg and libav
H.265/HEVC Video Encoding Guide
FFmpeg Hardware/VAAPI