From d9dd55bb50a8986a3ba0b43fbb276a029964b7f0 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 1 Jul 2024 07:06:06 +0000 Subject: [PATCH 01/15] utils: Print verbose output inside run_pipe_command_with_std_output Print the verbose output inside run_pipe_command_with_std_output instead of from its callers. Signed-off-by: Jonas Karlman --- fluster/decoders/gstreamer.py | 16 ++++------------ fluster/utils.py | 2 ++ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/fluster/decoders/gstreamer.py b/fluster/decoders/gstreamer.py index 7e2e7dc1..fc122adf 100644 --- a/fluster/decoders/gstreamer.py +++ b/fluster/decoders/gstreamer.py @@ -111,13 +111,10 @@ def gen_pipeline( self.cmd, input_filepath, self.decoder_bin, self.caps, self.sink, output ) - def parse_videocodectestsink_md5sum(self, data: List[str], verbose: bool) -> str: + def parse_videocodectestsink_md5sum(self, data: List[str]) -> str: """Parse the MD5 sum out of commandline output produced when using videocodectestsink.""" - md5sum = None for line in data: - if verbose: - print(line) pattern = ( "conformance/checksum, checksum-type=(string)MD5, checksum=(string)" ) @@ -135,14 +132,9 @@ def parse_videocodectestsink_md5sum(self, data: List[str], verbose: bool) -> str continue else: sum_end += sum_start - md5sum = line[sum_start:sum_end] - if not verbose: - return md5sum + return line[sum_start:sum_end] - if not md5sum: - raise Exception("No MD5 found in the program trace.") - - return md5sum + raise Exception("No MD5 found in the program trace.") def decode( self, @@ -165,7 +157,7 @@ def decode( data = run_pipe_command_with_std_output( command, timeout=timeout, verbose=verbose ) - return self.parse_videocodectestsink_md5sum(data, verbose) + return self.parse_videocodectestsink_md5sum(data) pipeline = self.gen_pipeline(input_filepath, output_filepath, output_format) run_command(shlex.split(pipeline), timeout=timeout, verbose=verbose) diff --git a/fluster/utils.py b/fluster/utils.py index 0277d80b..07c63800 100644 --- a/fluster/utils.py +++ b/fluster/utils.py @@ -84,6 +84,8 @@ def run_pipe_command_with_std_output( data = subprocess.check_output( command, stderr=serr, timeout=timeout, universal_newlines=True ) + if verbose: + print(data) return data.splitlines() except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as ex: odata: List[str] = [] From 05512454d2a1ed3455de28eebcdeb5c5888b533c Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 1 Jul 2024 07:06:06 +0000 Subject: [PATCH 02/15] utils: Rename and refactor run_pipe_command_with_std_output Rename to run_command_with_output and change it to return the full output as a string instead of a list of lines. Signed-off-by: Jonas Karlman --- fluster/decoders/gstreamer.py | 6 +++--- fluster/utils.py | 26 +++++++++++--------------- scripts/gen_jct_vc.py | 2 +- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/fluster/decoders/gstreamer.py b/fluster/decoders/gstreamer.py index fc122adf..153cc738 100644 --- a/fluster/decoders/gstreamer.py +++ b/fluster/decoders/gstreamer.py @@ -28,7 +28,7 @@ from fluster.utils import ( file_checksum, run_command, - run_pipe_command_with_std_output, + run_command_with_output, normalize_binary_cmd, ) @@ -154,9 +154,9 @@ def decode( pipeline = self.gen_pipeline(input_filepath, output_param, output_format) command = shlex.split(pipeline) command.append("-m") - data = run_pipe_command_with_std_output( + data = run_command_with_output( command, timeout=timeout, verbose=verbose - ) + ).splitlines() return self.parse_videocodectestsink_md5sum(data) pipeline = self.gen_pipeline(input_filepath, output_filepath, output_format) diff --git a/fluster/utils.py b/fluster/utils.py index 07c63800..ff7cb25b 100644 --- a/fluster/utils.py +++ b/fluster/utils.py @@ -69,38 +69,34 @@ def run_command( raise ex -def run_pipe_command_with_std_output( +def run_command_with_output( command: List[str], verbose: bool = False, check: bool = True, timeout: Optional[int] = None, -) -> List[str]: +) -> str: """Runs a command and returns std output trace""" serr = subprocess.DEVNULL if not verbose else subprocess.STDOUT if verbose: print(f'\nRunning command "{" ".join(command)}"') try: - data = subprocess.check_output( + output = subprocess.check_output( command, stderr=serr, timeout=timeout, universal_newlines=True ) - if verbose: - print(data) - return data.splitlines() + if verbose and output: + print(output) + return output or "" except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as ex: - odata: List[str] = [] - if verbose or check: + if verbose and ex.output: # Workaround inconsistent Python implementation - if isinstance(ex, subprocess.CalledProcessError): - odata = ex.output.splitlines() + if isinstance(ex, subprocess.TimeoutExpired): + print(ex.output.decode("utf-8")) else: - odata = ex.output.decode("utf-8").splitlines() - if verbose: - for line in odata: - print(line) + print(ex.output) if isinstance(ex, subprocess.CalledProcessError) and not check: - return odata + return ex.output or "" # Developer experience improvement (facilitates copy/paste) ex.cmd = " ".join(ex.cmd) diff --git a/scripts/gen_jct_vc.py b/scripts/gen_jct_vc.py index 2b86b544..02837e0a 100755 --- a/scripts/gen_jct_vc.py +++ b/scripts/gen_jct_vc.py @@ -160,7 +160,7 @@ def generate(self, download, jobs): 'default=nokey=1:noprint_wrappers=1', absolute_input_path] - result = utils.run_pipe_command_with_std_output(command) + result = utils.run_command_with_output(command).splitlines() pix_fmt = result[0] try: test_vector.output_format = OutputFormat[pix_fmt.upper()] From ebcca56a509bb97afd7b0b0e0cb1e2c2e67a3783 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 1 Jul 2024 07:06:06 +0000 Subject: [PATCH 03/15] ffmpeg: Simplify decoders and hwaccels check Add a run_ffmpeg_command helper and use it to simplify the check for supported decoders and hwaccels. Signed-off-by: Jonas Karlman --- fluster/decoders/ffmpeg.py | 54 ++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/fluster/decoders/ffmpeg.py b/fluster/decoders/ffmpeg.py index 39fc67c3..be841664 100644 --- a/fluster/decoders/ffmpeg.py +++ b/fluster/decoders/ffmpeg.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . -import os from functools import lru_cache from typing import List, Optional, Match import shlex @@ -25,11 +24,27 @@ from fluster.codec import Codec, OutputFormat from fluster.decoder import Decoder, register_decoder -from fluster.utils import file_checksum, run_command +from fluster.utils import file_checksum, run_command, run_command_with_output FFMPEG_TPL = "{} -nostdin -i {} {} -vf {}format=pix_fmts={} -f rawvideo {}" +@lru_cache(maxsize=128) +def _run_ffmpeg_command( + binary: str, + *args: str, + verbose: bool = False, +) -> str: + """Runs a ffmpeg command and returns the output or an empty string""" + try: + return run_command_with_output( + [binary, "-hide_banner", *args], + verbose=verbose, + ) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired): + return "" + + def output_format_to_ffformat(output_format: OutputFormat) -> str: """Return GStreamer pixel format""" mapping = { @@ -127,30 +142,17 @@ def decode( @lru_cache(maxsize=128) def check(self, verbose: bool) -> bool: """Checks whether the decoder can be run""" - # pylint: disable=broad-except - if self.hw_acceleration: - try: - command = None - - if self.wrapper: - command = [self.binary, "-decoders"] - else: - command = [self.binary, "-hwaccels"] - - output = subprocess.check_output( - command, stderr=subprocess.DEVNULL - ).decode("utf-8") - if verbose: - print(f'{" ".join(command)}\n{output}') - - if self.wrapper: - return self.api.lower() in output - - return f"{os.linesep}{self.api.lower()}{os.linesep}" in output - except Exception: - return False - else: - return super().check(verbose) + if not super().check(verbose): + return False + + if not self.hw_acceleration: + return True + + # Check if hw decoder or hwaccel is supported + command = "-decoders" if self.wrapper else "-hwaccels" + output = _run_ffmpeg_command(self.binary, command, verbose=verbose) + api = re.escape(self.api.lower()) + return re.search(rf"\s+{api}\s+", output) is not None @register_decoder From a63f19941c8edf2d03dc036d50d3940a3476e470 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 1 Jul 2024 07:06:06 +0000 Subject: [PATCH 04/15] ffmpeg: Improve version check to support major.minor releases Current version check does not work on FFmpeg releases using a major.minor version schema, e.g. 7.0 vs 7.0.1. Save ffmpeg_version as a tuple, e.g. (7, 0) or (7, 0, 1), and use this to simplify the check for use of -fps_mode or -vsync. Signed-off-by: Jonas Karlman --- fluster/decoders/ffmpeg.py | 50 +++++++++++++++----------------------- 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/fluster/decoders/ffmpeg.py b/fluster/decoders/ffmpeg.py index be841664..d5e19fe9 100644 --- a/fluster/decoders/ffmpeg.py +++ b/fluster/decoders/ffmpeg.py @@ -17,7 +17,7 @@ # License along with this library. If not, see . from functools import lru_cache -from typing import List, Optional, Match +from typing import List, Optional, Tuple import shlex import subprocess import re @@ -83,45 +83,28 @@ def __init__(self) -> None: self.cmd += f" -hwaccel {self.api.lower()}" self.name = f'FFmpeg-{self.codec.value}{"-" + self.api if self.api else ""}' self.description = f'FFmpeg {self.codec.value} {self.api if self.hw_acceleration else "SW"} decoder' - - @lru_cache(maxsize=128) - def ffmpeg_version(self) -> Optional[Match[str]]: - """Returns the ffmpeg version as a re.Match object""" - cmd = shlex.split("ffmpeg -version") - output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode("utf-8") - version = re.search(r"\d\.\d\.\d", output) - return version + self.ffmpeg_version: Optional[Tuple[int, ...]] = None def ffmpeg_cmd( self, input_filepath: str, output_filepath: str, output_format: OutputFormat ) -> List[str]: """Returns the formatted ffmpeg command based on the current ffmpeg version""" - version = self.ffmpeg_version() + passthrough = "-fps_mode passthrough" + if self.ffmpeg_version and self.ffmpeg_version < (5, 1): + passthrough = "-vsync passthrough" download = "" if self.hw_acceleration and self.hw_download: download = f"hwdownload,format={output_format_to_ffformat(output_format)}," - if version and int(version.group(0)[0]) >= 5 and int(version.group(0)[2]) >= 1: - cmd = shlex.split( - FFMPEG_TPL.format( - self.cmd, - input_filepath, - "-fps_mode passthrough", - download, - str(output_format.value), - output_filepath, - ) - ) - else: - cmd = shlex.split( - FFMPEG_TPL.format( - self.cmd, - input_filepath, - "-vsync passthrough", - download, - str(output_format.value), - output_filepath, - ) + cmd = shlex.split( + FFMPEG_TPL.format( + self.cmd, + input_filepath, + passthrough, + download, + str(output_format.value), + output_filepath, ) + ) return cmd def decode( @@ -145,6 +128,11 @@ def check(self, verbose: bool) -> bool: if not super().check(verbose): return False + # Get ffmpeg version + output = _run_ffmpeg_command(self.binary, "-version", verbose=verbose) + version = re.search(r" version n?(\d+)\.(\d+)(?:\.(\d+))?", output) + self.ffmpeg_version = tuple(map(int, version.groups())) if version else None + if not self.hw_acceleration: return True From 7cab44d502deac6c42275e5dff902f53656a38f9 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 1 Jul 2024 07:06:06 +0000 Subject: [PATCH 05/15] ffmpeg: Merge and build command line in decode The ffmpeg command line is built using three different functions, simplify and create the entire command line inside the decode method. Signed-off-by: Jonas Karlman --- fluster/decoders/ffmpeg.py | 55 ++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/fluster/decoders/ffmpeg.py b/fluster/decoders/ffmpeg.py index d5e19fe9..35a89f71 100644 --- a/fluster/decoders/ffmpeg.py +++ b/fluster/decoders/ffmpeg.py @@ -17,7 +17,7 @@ # License along with this library. If not, see . from functools import lru_cache -from typing import List, Optional, Tuple +from typing import Optional, Tuple import shlex import subprocess import re @@ -64,8 +64,6 @@ class FFmpegDecoder(Decoder): """Generic class for FFmpeg decoder""" binary = "ffmpeg" - description = "" - cmd = "" api = "" wrapper = False hw_download = False @@ -73,31 +71,38 @@ class FFmpegDecoder(Decoder): def __init__(self) -> None: super().__init__() - self.cmd = self.binary - if self.hw_acceleration: - if self.init_device: - self.cmd += f' -init_hw_device "{self.init_device}" -hwaccel_output_format {self.api.lower()}' - if self.wrapper: - self.cmd += f" -c:v {self.api.lower()}" - else: - self.cmd += f" -hwaccel {self.api.lower()}" self.name = f'FFmpeg-{self.codec.value}{"-" + self.api if self.api else ""}' self.description = f'FFmpeg {self.codec.value} {self.api if self.hw_acceleration else "SW"} decoder' self.ffmpeg_version: Optional[Tuple[int, ...]] = None - def ffmpeg_cmd( - self, input_filepath: str, output_filepath: str, output_format: OutputFormat - ) -> List[str]: - """Returns the formatted ffmpeg command based on the current ffmpeg version""" + def decode( + self, + input_filepath: str, + output_filepath: str, + output_format: OutputFormat, + timeout: int, + verbose: bool, + keep_files: bool, + ) -> str: + """Decodes input_filepath in output_filepath""" + # pylint: disable=unused-argument + cmd = self.binary + if self.hw_acceleration: + if self.init_device: + cmd += f' -init_hw_device "{self.init_device}" -hwaccel_output_format {self.api.lower()}' + if self.wrapper: + cmd += f" -c:v {self.api.lower()}" + else: + cmd += f" -hwaccel {self.api.lower()}" passthrough = "-fps_mode passthrough" if self.ffmpeg_version and self.ffmpeg_version < (5, 1): passthrough = "-vsync passthrough" download = "" if self.hw_acceleration and self.hw_download: download = f"hwdownload,format={output_format_to_ffformat(output_format)}," - cmd = shlex.split( + command = shlex.split( FFMPEG_TPL.format( - self.cmd, + cmd, input_filepath, passthrough, download, @@ -105,21 +110,7 @@ def ffmpeg_cmd( output_filepath, ) ) - return cmd - - def decode( - self, - input_filepath: str, - output_filepath: str, - output_format: OutputFormat, - timeout: int, - verbose: bool, - keep_files: bool, - ) -> str: - """Decodes input_filepath in output_filepath""" - # pylint: disable=unused-argument - cmd = self.ffmpeg_cmd(input_filepath, output_filepath, output_format) - run_command(cmd, timeout=timeout, verbose=verbose) + run_command(command, timeout=timeout, verbose=verbose) return file_checksum(output_filepath) @lru_cache(maxsize=128) From b5bbb55a7670cce237f6f76be3d8d735f9bde171 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 1 Jul 2024 07:06:06 +0000 Subject: [PATCH 06/15] ffmpeg: Support using a hwaccel_output_format not matching the api name Normally the name used for -hwaccel and -hwaccel_output_format match. However, that may not always be the case, add hw_output_format to improve support for future hwaccels. Signed-off-by: Jonas Karlman --- fluster/decoders/ffmpeg.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/fluster/decoders/ffmpeg.py b/fluster/decoders/ffmpeg.py index 35a89f71..bf09a088 100644 --- a/fluster/decoders/ffmpeg.py +++ b/fluster/decoders/ffmpeg.py @@ -67,7 +67,8 @@ class FFmpegDecoder(Decoder): api = "" wrapper = False hw_download = False - init_device = "" + init_hw_device = "" + hw_output_format = "" def __init__(self) -> None: super().__init__() @@ -88,8 +89,10 @@ def decode( # pylint: disable=unused-argument cmd = self.binary if self.hw_acceleration: - if self.init_device: - cmd += f' -init_hw_device "{self.init_device}" -hwaccel_output_format {self.api.lower()}' + if self.init_hw_device: + cmd += f' -init_hw_device "{self.init_hw_device}"' + if self.hw_output_format: + cmd += f" -hwaccel_output_format {self.hw_output_format}" if self.wrapper: cmd += f" -c:v {self.api.lower()}" else: @@ -309,7 +312,8 @@ class FFmpegVulkanDecoder(FFmpegDecoder): hw_acceleration = True api = "Vulkan" - init_device = "vulkan" + init_hw_device = "vulkan" + hw_output_format = "vulkan" hw_download = True From 8fc50d9423cc1afa54f76eb7578b3beec3002872 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 1 Jul 2024 07:06:06 +0000 Subject: [PATCH 07/15] ffmpeg: Change output_format_to_ffformat to a property Change output_format_to_ffformat to a property to improve support for future hwaccels that need a different hwdownload format mapping. Signed-off-by: Jonas Karlman --- fluster/decoders/ffmpeg.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/fluster/decoders/ffmpeg.py b/fluster/decoders/ffmpeg.py index bf09a088..4ec5116d 100644 --- a/fluster/decoders/ffmpeg.py +++ b/fluster/decoders/ffmpeg.py @@ -17,7 +17,7 @@ # License along with this library. If not, see . from functools import lru_cache -from typing import Optional, Tuple +from typing import Dict, Optional, Tuple import shlex import subprocess import re @@ -45,21 +45,6 @@ def _run_ffmpeg_command( return "" -def output_format_to_ffformat(output_format: OutputFormat) -> str: - """Return GStreamer pixel format""" - mapping = { - OutputFormat.YUV420P: "nv12", - OutputFormat.YUV422P: "nv12", # vulkan - OutputFormat.YUV420P10LE: "p010", - OutputFormat.YUV422P10LE: "p012", - } - if output_format not in mapping: - raise Exception( - f"No matching output format found in FFmpeg for {output_format}" - ) - return mapping[output_format] - - class FFmpegDecoder(Decoder): """Generic class for FFmpeg decoder""" @@ -67,6 +52,7 @@ class FFmpegDecoder(Decoder): api = "" wrapper = False hw_download = False + hw_download_mapping: Dict[OutputFormat, str] = {} init_hw_device = "" hw_output_format = "" @@ -102,7 +88,11 @@ def decode( passthrough = "-vsync passthrough" download = "" if self.hw_acceleration and self.hw_download: - download = f"hwdownload,format={output_format_to_ffformat(output_format)}," + if output_format not in self.hw_download_mapping: + raise Exception( + f"No matching ffmpeg pixel format found for {output_format}" + ) + download = f"hwdownload,format={self.hw_download_mapping[output_format]}," command = shlex.split( FFMPEG_TPL.format( cmd, @@ -315,6 +305,12 @@ class FFmpegVulkanDecoder(FFmpegDecoder): init_hw_device = "vulkan" hw_output_format = "vulkan" hw_download = True + hw_download_mapping = { + OutputFormat.YUV420P: "nv12", + OutputFormat.YUV422P: "nv12", + OutputFormat.YUV420P10LE: "p010", + OutputFormat.YUV422P10LE: "p012", + } @register_decoder From 7a954738d2570a967cfa4ff7b2e28eb90b9f24e0 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 1 Jul 2024 07:06:06 +0000 Subject: [PATCH 08/15] ffmpeg: Refactor building command line Refactor how the ffmpeg command line is built, change to use a list and pass it directly to run_command. Also add hide_banner and change to use the verbose version of parameters, e.g. -codec instead of -c:v. Signed-off-by: Jonas Karlman --- fluster/decoders/ffmpeg.py | 49 +++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/fluster/decoders/ffmpeg.py b/fluster/decoders/ffmpeg.py index 4ec5116d..b9035d06 100644 --- a/fluster/decoders/ffmpeg.py +++ b/fluster/decoders/ffmpeg.py @@ -18,7 +18,6 @@ from functools import lru_cache from typing import Dict, Optional, Tuple -import shlex import subprocess import re @@ -26,8 +25,6 @@ from fluster.decoder import Decoder, register_decoder from fluster.utils import file_checksum, run_command, run_command_with_output -FFMPEG_TPL = "{} -nostdin -i {} {} -vf {}format=pix_fmts={} -f rawvideo {}" - @lru_cache(maxsize=128) def _run_ffmpeg_command( @@ -73,19 +70,31 @@ def decode( ) -> str: """Decodes input_filepath in output_filepath""" # pylint: disable=unused-argument - cmd = self.binary + command = [self.binary, "-hide_banner", "-nostdin"] + + # Hardware acceleration if self.hw_acceleration: if self.init_hw_device: - cmd += f' -init_hw_device "{self.init_hw_device}"' + command.extend(["-init_hw_device", self.init_hw_device]) + if not self.wrapper: + command.extend(["-hwaccel", self.api.lower()]) if self.hw_output_format: - cmd += f" -hwaccel_output_format {self.hw_output_format}" - if self.wrapper: - cmd += f" -c:v {self.api.lower()}" - else: - cmd += f" -hwaccel {self.api.lower()}" - passthrough = "-fps_mode passthrough" + command.extend(["-hwaccel_output_format", self.hw_output_format]) + + # Codec + if self.hw_acceleration and self.wrapper: + command.extend(["-codec", self.api.lower()]) + + # Input file + command.extend(["-i", input_filepath]) + + # Passthrough timestamp from the demuxer to the muxer if self.ffmpeg_version and self.ffmpeg_version < (5, 1): - passthrough = "-vsync passthrough" + command.extend(["-vsync", "passthrough"]) + else: + command.extend(["-fps_mode", "passthrough"]) + + # Hardware download download = "" if self.hw_acceleration and self.hw_download: if output_format not in self.hw_download_mapping: @@ -93,16 +102,12 @@ def decode( f"No matching ffmpeg pixel format found for {output_format}" ) download = f"hwdownload,format={self.hw_download_mapping[output_format]}," - command = shlex.split( - FFMPEG_TPL.format( - cmd, - input_filepath, - passthrough, - download, - str(output_format.value), - output_filepath, - ) - ) + + # Output format filter + command.extend(["-filter", f"{download}format=pix_fmts={output_format.value}"]) + + # Output file + command.extend(["-f", "rawvideo", output_filepath]) run_command(command, timeout=timeout, verbose=verbose) return file_checksum(output_filepath) From 9a7839b90d7ad0a2b53eb6e464b9ff12e8f60784 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 1 Jul 2024 07:06:06 +0000 Subject: [PATCH 09/15] ffmpeg: Check and set codec to use Add -codec parameter to avoid ffmpeg having to analyze the stream to figure out the codec of the input file. Signed-off-by: Jonas Karlman --- fluster/decoders/ffmpeg.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/fluster/decoders/ffmpeg.py b/fluster/decoders/ffmpeg.py index b9035d06..dc6b1751 100644 --- a/fluster/decoders/ffmpeg.py +++ b/fluster/decoders/ffmpeg.py @@ -57,6 +57,7 @@ def __init__(self) -> None: super().__init__() self.name = f'FFmpeg-{self.codec.value}{"-" + self.api if self.api else ""}' self.description = f'FFmpeg {self.codec.value} {self.api if self.hw_acceleration else "SW"} decoder' + self.ffmpeg_codec: Optional[str] = None self.ffmpeg_version: Optional[Tuple[int, ...]] = None def decode( @@ -84,6 +85,8 @@ def decode( # Codec if self.hw_acceleration and self.wrapper: command.extend(["-codec", self.api.lower()]) + elif self.ffmpeg_codec: + command.extend(["-codec", self.ffmpeg_codec]) # Input file command.extend(["-i", input_filepath]) @@ -117,11 +120,29 @@ def check(self, verbose: bool) -> bool: if not super().check(verbose): return False + # Check if codec is supported + codec_mapping = { + Codec.H264: "h264", + Codec.H265: "hevc", + Codec.VP8: "vp8", + Codec.VP9: "vp9", + Codec.AV1: "av1", + } + if self.codec not in codec_mapping: + return False + self.ffmpeg_codec = codec_mapping[self.codec] + # Get ffmpeg version output = _run_ffmpeg_command(self.binary, "-version", verbose=verbose) version = re.search(r" version n?(\d+)\.(\d+)(?:\.(\d+))?", output) self.ffmpeg_version = tuple(map(int, version.groups())) if version else None + # Check if codec can be used + output = _run_ffmpeg_command(self.binary, "-codecs", verbose=verbose) + codec = re.escape(self.ffmpeg_codec) + if re.search(rf"\s+{codec}\s+", output) is None: + return False + if not self.hw_acceleration: return True From 2b1642d57aa6e92301249bcadd8435d590ecde6c Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 1 Jul 2024 07:06:06 +0000 Subject: [PATCH 10/15] ffmpeg: Use md5 muxer when supported Use md5 muxer when supported to avoid having to write an output file of rawvideo. Signed-off-by: Jonas Karlman --- fluster/decoders/ffmpeg.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/fluster/decoders/ffmpeg.py b/fluster/decoders/ffmpeg.py index dc6b1751..1d6f239d 100644 --- a/fluster/decoders/ffmpeg.py +++ b/fluster/decoders/ffmpeg.py @@ -59,6 +59,7 @@ def __init__(self) -> None: self.description = f'FFmpeg {self.codec.value} {self.api if self.hw_acceleration else "SW"} decoder' self.ffmpeg_codec: Optional[str] = None self.ffmpeg_version: Optional[Tuple[int, ...]] = None + self.use_md5_muxer: bool = False def decode( self, @@ -70,7 +71,6 @@ def decode( keep_files: bool, ) -> str: """Decodes input_filepath in output_filepath""" - # pylint: disable=unused-argument command = [self.binary, "-hide_banner", "-nostdin"] # Hardware acceleration @@ -109,6 +109,15 @@ def decode( # Output format filter command.extend(["-filter", f"{download}format=pix_fmts={output_format.value}"]) + # MD5 muxer + if self.use_md5_muxer and not keep_files: + command.extend(["-f", "md5", "-"]) + output = run_command_with_output(command, timeout=timeout, verbose=verbose) + md5sum = re.search(r"MD5=([0-9a-fA-F]+)\s*", output) + if not md5sum: + raise Exception("No MD5 found in the program trace.") + return md5sum.group(1).lower() + # Output file command.extend(["-f", "rawvideo", output_filepath]) run_command(command, timeout=timeout, verbose=verbose) @@ -143,6 +152,11 @@ def check(self, verbose: bool) -> bool: if re.search(rf"\s+{codec}\s+", output) is None: return False + # Check if MD5 muxer can be used + output = _run_ffmpeg_command(self.binary, "-formats", verbose=verbose) + muxer = re.escape("md5") + self.use_md5_muxer = re.search(rf"E\s+{muxer}\s+", output) is not None + if not self.hw_acceleration: return True From 527e3eb904ac65268746365f433feefefa1c9237 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 1 Jul 2024 07:06:06 +0000 Subject: [PATCH 11/15] ffmpeg: Use single-threaded decoding Fluster already can start multiple jobs, restrict ffmpeg to use a single thread per job. Signed-off-by: Jonas Karlman --- fluster/decoders/ffmpeg.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fluster/decoders/ffmpeg.py b/fluster/decoders/ffmpeg.py index 1d6f239d..f8c286be 100644 --- a/fluster/decoders/ffmpeg.py +++ b/fluster/decoders/ffmpeg.py @@ -52,6 +52,7 @@ class FFmpegDecoder(Decoder): hw_download_mapping: Dict[OutputFormat, str] = {} init_hw_device = "" hw_output_format = "" + thread_count = 1 def __init__(self) -> None: super().__init__() @@ -71,6 +72,7 @@ def decode( keep_files: bool, ) -> str: """Decodes input_filepath in output_filepath""" + # pylint: disable=too-many-branches command = [self.binary, "-hide_banner", "-nostdin"] # Hardware acceleration @@ -82,6 +84,10 @@ def decode( if self.hw_output_format: command.extend(["-hwaccel_output_format", self.hw_output_format]) + # Number of threads + if self.thread_count: + command.extend(["-threads", str(self.thread_count)]) + # Codec if self.hw_acceleration and self.wrapper: command.extend(["-codec", self.api.lower()]) From 2ff224c7b9ec95a86bec7d31faba4e1880f52062 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 1 Jul 2024 07:06:06 +0000 Subject: [PATCH 12/15] ffmpeg: Use a reduced loglevel when running in non-verbose mode Change to use warning loglevel when non-verbose mode is used. Signed-off-by: Jonas Karlman --- fluster/decoders/ffmpeg.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fluster/decoders/ffmpeg.py b/fluster/decoders/ffmpeg.py index f8c286be..2525b77e 100644 --- a/fluster/decoders/ffmpeg.py +++ b/fluster/decoders/ffmpeg.py @@ -75,6 +75,10 @@ def decode( # pylint: disable=too-many-branches command = [self.binary, "-hide_banner", "-nostdin"] + # Loglevel + if not verbose: + command.extend(["-loglevel", "warning"]) + # Hardware acceleration if self.hw_acceleration: if self.init_hw_device: From eca0c10cdffa07663d0c3ee9f6fe0ca77c146eb0 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 1 Jul 2024 07:06:06 +0000 Subject: [PATCH 13/15] ffmpeg: Add support for VDPAU VP9 decoder FFmpeg VDPAU hwaccel support VP9, add a decoder for it. Signed-off-by: Jonas Karlman --- fluster/decoders/ffmpeg.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fluster/decoders/ffmpeg.py b/fluster/decoders/ffmpeg.py index 2525b77e..8a73f99f 100644 --- a/fluster/decoders/ffmpeg.py +++ b/fluster/decoders/ffmpeg.py @@ -268,6 +268,13 @@ class FFmpegH265VdpauDecoder(FFmpegVdpauDecoder): codec = Codec.H265 +@register_decoder +class FFmpegVP9VdpauDecoder(FFmpegVdpauDecoder): + """FFmpeg VDPAU decoder for VP9""" + + codec = Codec.VP9 + + @register_decoder class FFmpegAV1VdpauDecoder(FFmpegVdpauDecoder): """FFmpeg VDPAU decoder for AV1""" From 788bf108c03de90e5d835ddd99c39eab3e1c475f Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 1 Jul 2024 07:06:06 +0000 Subject: [PATCH 14/15] ffmpeg: Rename V4L2 mem2mem decoders The V4L2 mem2mem decoders contain the codec twice in the name, change to use a more simple name, e.g. FFmpeg-H.264-v4l2m2m instead of FFmpeg-H.264-h264_v4l2m2m. Also add the H.265 decoder. Signed-off-by: Jonas Karlman --- fluster/decoders/ffmpeg.py | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/fluster/decoders/ffmpeg.py b/fluster/decoders/ffmpeg.py index 8a73f99f..4820824f 100644 --- a/fluster/decoders/ffmpeg.py +++ b/fluster/decoders/ffmpeg.py @@ -324,34 +324,48 @@ class FFmpegH265D3d11vaDecoder(FFmpegD3d11vaDecoder): codec = Codec.H265 +class FFmpegV4L2m2mDecoder(FFmpegDecoder): + """Generic class for FFmpeg V4L2 mem2mem decoder""" + + hw_acceleration = True + wrapper = True + + def __init__(self) -> None: + super().__init__() + self.name = f"FFmpeg-{self.codec.value}-v4l2m2m" + self.description = f"FFmpeg {self.codec.value} v4l2m2m decoder" + + @register_decoder -class FFmpegVP8V4L2m2mDecoder(FFmpegDecoder): - """FFmpeg V4L2m2m decoder for VP8""" +class FFmpegVP8V4L2m2mDecoder(FFmpegV4L2m2mDecoder): + """FFmpeg V4L2 mem2mem decoder for VP8""" codec = Codec.VP8 - hw_acceleration = True api = "vp8_v4l2m2m" - wrapper = True @register_decoder -class FFmpegVP9V4L2m2mDecoder(FFmpegDecoder): - """FFmpeg V4L2m2m decoder for VP9""" +class FFmpegVP9V4L2m2mDecoder(FFmpegV4L2m2mDecoder): + """FFmpeg V4L2 mem2mem decoder for VP9""" codec = Codec.VP9 - hw_acceleration = True api = "vp9_v4l2m2m" - wrapper = True @register_decoder -class FFmpegH264V4L2m2mDecoder(FFmpegDecoder): - """FFmpeg V4L2m2m decoder for H264""" +class FFmpegH264V4L2m2mDecoder(FFmpegV4L2m2mDecoder): + """FFmpeg V4L2 mem2mem decoder for H.264""" codec = Codec.H264 - hw_acceleration = True api = "h264_v4l2m2m" - wrapper = True + + +@register_decoder +class FFmpegH265V4L2m2mDecoder(FFmpegV4L2m2mDecoder): + """FFmpeg V4L2 mem2mem decoder for H.265""" + + codec = Codec.H265 + api = "hevc_v4l2m2m" class FFmpegVulkanDecoder(FFmpegDecoder): From 1be6c4bca04186dc990d6b77dee69d0a6c400c2e Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Mon, 1 Jul 2024 07:06:06 +0000 Subject: [PATCH 15/15] ffmpeg: Add CUDA decoders Add FFmpeg CUDA/NVDEC hwaccel decoders. Signed-off-by: Jonas Karlman --- fluster/decoders/ffmpeg.py | 52 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/fluster/decoders/ffmpeg.py b/fluster/decoders/ffmpeg.py index 4820824f..eb38d9b9 100644 --- a/fluster/decoders/ffmpeg.py +++ b/fluster/decoders/ffmpeg.py @@ -403,3 +403,55 @@ class FFmpegAV1VulkanDecoder(FFmpegVulkanDecoder): """FFmpeg Vulkan decoder for AV1""" codec = Codec.AV1 + + +class FFmpegCudaDecoder(FFmpegDecoder): + """Generic class for FFmpeg CUDA decoder""" + + hw_acceleration = True + api = "CUDA" + hw_output_format = "cuda" + hw_download = True + hw_download_mapping = { + OutputFormat.YUV420P: "nv12", + OutputFormat.YUV444P: "yuv444p", + OutputFormat.YUV420P10LE: "p010", + OutputFormat.YUV444P10LE: "yuv444p16le", + OutputFormat.YUV420P12LE: "p016", + OutputFormat.YUV444P12LE: "yuv444p16le", + } + + +@register_decoder +class FFmpegH264CudaDecoder(FFmpegCudaDecoder): + """FFmpeg CUDA decoder for H.264""" + + codec = Codec.H264 + + +@register_decoder +class FFmpegH265CudaDecoder(FFmpegCudaDecoder): + """FFmpeg CUDA decoder for H.265""" + + codec = Codec.H265 + + +@register_decoder +class FFmpegVP8CudaDecoder(FFmpegCudaDecoder): + """FFmpeg CUDA decoder for VP8""" + + codec = Codec.VP8 + + +@register_decoder +class FFmpegVP9CudaDecoder(FFmpegCudaDecoder): + """FFmpeg CUDA decoder for VP9""" + + codec = Codec.VP9 + + +@register_decoder +class FFmpegAV1VCudaDecoder(FFmpegCudaDecoder): + """FFmpeg CUDA decoder for AV1""" + + codec = Codec.AV1