|
2 | 2 | import time
|
3 | 3 | import pathlib
|
4 | 4 | import subprocess
|
| 5 | +from functools import lru_cache |
| 6 | +from collections import defaultdict |
5 | 7 |
|
6 | 8 | from ._utils import get_ffmpeg_exe, _popen_kwargs, logger
|
7 | 9 | from ._parsing import LogCatcher, parse_ffmpeg_header, cvsecs
|
8 | 10 |
|
9 | 11 |
|
10 | 12 | ISWIN = sys.platform.startswith("win")
|
11 | 13 |
|
| 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 | + |
12 | 57 |
|
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 | + ) |
14 | 133 |
|
15 | 134 |
|
| 135 | +@lru_cache() |
16 | 136 | def _get_exe():
|
17 |
| - global exe |
18 |
| - if exe is None: |
19 |
| - exe = get_ffmpeg_exe() |
20 |
| - return exe |
| 137 | + return get_ffmpeg_exe() |
21 | 138 |
|
22 | 139 |
|
23 | 140 | def count_frames_and_secs(path):
|
@@ -307,7 +424,8 @@ def write_frames(
|
307 | 424 | quality (float): A measure for quality between 0 and 10. Default 5.
|
308 | 425 | Ignored if bitrate is given.
|
309 | 426 | 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. |
311 | 429 | macro_block_size (int): You probably want to align the size of frames
|
312 | 430 | to this value to avoid image resizing. Default 16. Can be set
|
313 | 431 | to 1 to avoid block alignment, though this is not recommended.
|
@@ -375,13 +493,14 @@ def write_frames(
|
375 | 493 | # ----- Prepare
|
376 | 494 |
|
377 | 495 | # 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() |
385 | 504 |
|
386 | 505 | audio_params = ["-an"]
|
387 | 506 | if audio_path is not None and not path.lower().endswith(".gif"):
|
|
0 commit comments