Skip to content

Commit 76836d6

Browse files
authored
Automatically find available h264 encoders and choose the best one according to our heuristics (#67)
* Add a method of finding all the avilable encoders for h264 * Refactor how autodetection is made * Add test to ensure that the encoders actually work with the underlying installation * Cleanup returned encoders * Typos * Use lru_cache in the top function * lint with black * linnt more * Add test for ffmpeg codec testing * Test encoder preference
1 parent b982150 commit 76836d6

File tree

2 files changed

+158
-13
lines changed

2 files changed

+158
-13
lines changed

imageio_ffmpeg/_io.py

Lines changed: 132 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,139 @@
22
import time
33
import pathlib
44
import subprocess
5+
from functools import lru_cache
6+
from collections import defaultdict
57

68
from ._utils import get_ffmpeg_exe, _popen_kwargs, logger
79
from ._parsing import LogCatcher, parse_ffmpeg_header, cvsecs
810

911

1012
ISWIN = sys.platform.startswith("win")
1113

14+
h264_encoder_preference = defaultdict(lambda: -1)
15+
# The libx264 was the default encoder for a longe time with imageio
16+
h264_encoder_preference["libx264"] = 100
17+
18+
# Encoder with the nvidia graphics card dedicated hardware
19+
h264_encoder_preference["h264_nvenc"] = 90
20+
# Deprecated names for the same encoder
21+
h264_encoder_preference["nvenc_h264"] = 90
22+
h264_encoder_preference["nvenc"] = 90
23+
24+
# vaapi provides hardware encoding with intel integrated graphics chipsets
25+
h264_encoder_preference["h264_vaapi"] = 80
26+
27+
# openh264 is cisco's open source encoder
28+
h264_encoder_preference["libopenh264"] = 70
29+
30+
h264_encoder_preference["libx264rgb"] = 50
31+
32+
33+
def ffmpeg_test_encoder(encoder):
34+
# Use the null streams to validate if we can encode anything
35+
# https://trac.ffmpeg.org/wiki/Null
36+
cmd = [
37+
_get_exe(),
38+
"-hide_banner",
39+
"-f",
40+
"lavfi",
41+
"-i",
42+
"nullsrc=s=256x256:d=8",
43+
"-vcodec",
44+
encoder,
45+
"-f",
46+
"null",
47+
"-",
48+
]
49+
p = subprocess.run(
50+
cmd,
51+
stdin=subprocess.PIPE,
52+
stdout=subprocess.PIPE,
53+
stderr=subprocess.PIPE,
54+
)
55+
return p.returncode == 0
56+
1257

13-
exe = None
58+
def get_compiled_h264_encoders():
59+
cmd = [_get_exe(), "-hide_banner", "-encoders"]
60+
p = subprocess.run(
61+
cmd,
62+
stdin=subprocess.PIPE,
63+
stdout=subprocess.PIPE,
64+
stderr=subprocess.PIPE,
65+
)
66+
stdout = p.stdout.decode().replace("\r", "")
67+
# 2022/04/08: hmaarrfk
68+
# I couldn't find a good way to get the list of available encoders from
69+
# the ffmpeg command
70+
# The ffmpeg command return a table that looks like
71+
# Notice the leading space at the very beginning
72+
# On ubuntu with libffmpeg-nvenc-dev we get
73+
# $ ffmpeg -hide_banner -encoders | grep -i h.264
74+
#
75+
# Encoders:
76+
# V..... = Video
77+
# A..... = Audio
78+
# S..... = Subtitle
79+
# .F.... = Frame-level multithreading
80+
# ..S... = Slice-level multithreading
81+
# ...X.. = Codec is experimental
82+
# ....B. = Supports draw_horiz_band
83+
# .....D = Supports direct rendering method 1
84+
# ------
85+
# V..... libx264 libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (codec h264)
86+
# V..... libx264rgb libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 RGB (codec h264)
87+
# V....D h264_nvenc NVIDIA NVENC H.264 encoder (codec h264)
88+
# V..... h264_omx OpenMAX IL H.264 video encoder (codec h264)
89+
# V..... h264_qsv H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (Intel Quick Sync Video acceleration) (codec h264)
90+
# V..... h264_v4l2m2m V4L2 mem2mem H.264 encoder wrapper (codec h264)
91+
# V....D h264_vaapi H.264/AVC (VAAPI) (codec h264)
92+
# V..... nvenc NVIDIA NVENC H.264 encoder (codec h264)
93+
# V..... nvenc_h264 NVIDIA NVENC H.264 encoder (codec h264)
94+
#
95+
# However, just because ffmpeg was compiled with the options enabled
96+
# it doesn't mean that it will be successful
97+
header_footer = stdout.split("------")
98+
footer = header_footer[1].strip("\n")
99+
encoders = []
100+
for line in footer.split("\n"):
101+
# Strip to remove any leading spaces
102+
line = line.strip()
103+
encoder = line.split(" ")[1]
104+
105+
if encoder in h264_encoder_preference:
106+
# These encoders are known to support H.264
107+
# We forcibly include them in case their description changes to
108+
# not include the string "H.264"
109+
encoders.append(encoder)
110+
elif (line[0] == "V") and ("H.264" in line):
111+
encoders.append(encoder)
112+
113+
encoders.sort(reverse=True, key=lambda x: h264_encoder_preference[x])
114+
if "h264_nvenc" in encoders:
115+
# Remove deprecated names for the same encoder
116+
for encoder in ["nvenc", "nvenc_h264"]:
117+
if encoder in encoders:
118+
encoders.remove(encoder)
119+
# Return an immutable tuple to avoid users corrupting the lru_cache
120+
return tuple(encoders)
121+
122+
123+
@lru_cache()
124+
def get_first_available_h264_encoder():
125+
compiled_encoders = get_compiled_h264_encoders()
126+
for encoder in compiled_encoders:
127+
if ffmpeg_test_encoder(encoder):
128+
return encoder
129+
else:
130+
raise RuntimeError(
131+
"No valid H.264 encoder was found with the ffmpeg installation"
132+
)
14133

15134

135+
@lru_cache()
16136
def _get_exe():
17-
global exe
18-
if exe is None:
19-
exe = get_ffmpeg_exe()
20-
return exe
137+
return get_ffmpeg_exe()
21138

22139

23140
def count_frames_and_secs(path):
@@ -307,7 +424,8 @@ def write_frames(
307424
quality (float): A measure for quality between 0 and 10. Default 5.
308425
Ignored if bitrate is given.
309426
bitrate (str): The bitrate, e.g. "192k". The defaults are pretty good.
310-
codec (str): The codec. Default "libx264" (or "msmpeg4" for .wmv).
427+
codec (str): The codec. Default "libx264" for .mp4 (if available from
428+
the ffmpeg executable) or "msmpeg4" for .wmv.
311429
macro_block_size (int): You probably want to align the size of frames
312430
to this value to avoid image resizing. Default 16. Can be set
313431
to 1 to avoid block alignment, though this is not recommended.
@@ -375,13 +493,14 @@ def write_frames(
375493
# ----- Prepare
376494

377495
# Get parameters
378-
default_codec = "libx264"
379-
if path.lower().endswith(".wmv"):
380-
# This is a safer default codec on windows to get videos that
381-
# will play in powerpoint and other apps. H264 is not always
382-
# available on windows.
383-
default_codec = "msmpeg4"
384-
codec = codec or default_codec
496+
if not codec:
497+
if path.lower().endswith(".wmv"):
498+
# This is a safer default codec on windows to get videos that
499+
# will play in powerpoint and other apps. H264 is not always
500+
# available on windows.
501+
codec = "msmpeg4"
502+
else:
503+
codec = get_first_available_h264_encoder()
385504

386505
audio_params = ["-an"]
387506
if audio_path is not None and not path.lower().endswith(".gif"):

tests/test_io.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import tempfile
88

99
import imageio_ffmpeg
10+
from imageio_ffmpeg._io import ffmpeg_test_encoder, get_compiled_h264_encoders
11+
from imageio_ffmpeg._io import get_first_available_h264_encoder
1012

1113
from pytest import skip, raises, warns
1214
from testutils import no_warnings_allowed
@@ -413,6 +415,30 @@ def test_write_audio_path():
413415
assert audio_codec == "aac"
414416

415417

418+
def test_get_compiled_h264_encoders():
419+
available_encoders = get_compiled_h264_encoders()
420+
# Assert it is not a mutable type
421+
assert isinstance(available_encoders, tuple)
422+
423+
# Software encoders like libx264 should work regardless of hardware
424+
for encoder in ["libx264", "libopenh264", "libx264rgb"]:
425+
if encoder in available_encoders:
426+
assert ffmpeg_test_encoder(encoder)
427+
else:
428+
assert not ffmpeg_test_encoder(encoder)
429+
430+
assert not ffmpeg_test_encoder("not_a_real_encoder")
431+
432+
433+
def test_prefered_encoder():
434+
available_encoders = get_compiled_h264_encoders()
435+
# historically, libx264 was the preferred encoder for imageio
436+
# However, the user (or distribution) may not have it installed in their
437+
# implementation of ffmpeg.
438+
if "libx264" in available_encoders:
439+
assert "libx264" == get_first_available_h264_encoder()
440+
441+
416442
if __name__ == "__main__":
417443
setup_module()
418444
test_ffmpeg_version()

0 commit comments

Comments
 (0)