From 8c0fe0858a1177ca4f16508c17d52fecebd0f77a Mon Sep 17 00:00:00 2001 From: lizardfish0 <97480684+lizardfish0@users.noreply.github.com> Date: Sun, 18 Feb 2024 15:00:59 -0700 Subject: [PATCH 1/4] Rework setAcceleration to set_decoder, which now considers specified hwaccel_decoders prior to setting opts. This helps prevent the use of a hwaccel when the source codec is unsupported by the hardware (i.e. older generation Nvidia GPUs not supporting AV1 or HEVC). --- .gitignore | 6 ++ resources/mediaprocessor.py | 125 ++++++++++++++++++++---------------- 2 files changed, 75 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index 5b9e9f94..3434c3b0 100644 --- a/.gitignore +++ b/.gitignore @@ -237,3 +237,9 @@ pip-log.txt ## VS Code ################# .vscode + +# PyCharm +.idea + +# pyenv +.python-version \ No newline at end of file diff --git a/resources/mediaprocessor.py b/resources/mediaprocessor.py index 3d0187e1..4099ca2a 100644 --- a/resources/mediaprocessor.py +++ b/resources/mediaprocessor.py @@ -1400,29 +1400,33 @@ def generateOptions(self, inputfile, info=None, original=None, tagdata=None): if vcodec != 'copy': try: - opts, device = self.setAcceleration(info.video.codec, info.video.pix_fmt, codecs, pix_fmts) + # Set the opts necessary for decoding (potentially using hardware accelleration). + opts, device = self.set_decoder(info.video.codec, info.video.pix_fmt) preopts.extend(opts) + + # Additionally init the encoder hwdevice, if different from the decoder. for k in self.settings.hwdevices: if k in vcodec: match = self.settings.hwdevices[k] - self.log.debug("Found a matching device %s for encoder %s [hwdevices]." % (match, vcodec)) + self.log.debug(f"Found a matching device {match} for encoder {vcodec} [hwdevices].") if not device: - self.log.debug("No device was set by the decoder, setting device to %s for encoder %s [hwdevices]." % (match, vcodec)) + self.log.debug(f"No device was set by the decoder, setting device to {match} for encoder {vcodec} [hwdevices].") preopts.extend(['-init_hw_device', '%s=sma:%s' % (k, match)]) options['video']['device'] = "sma" elif device == match: - self.log.debug("Device was already set by the decoder, using same device %s for encoder %s [hwdevices]." % (device, vcodec)) + self.log.debug(f"Device was already set by the decoder, using same device {device} for encoder {vcodec} [hwdevices].") options['video']['device'] = "sma" else: - self.log.debug("Device was already set by the decoder but does not match encoder, using secondary device %s for encoder %s [hwdevices]." % (match, vcodec)) + self.log.debug(f"Device was already set by the decoder but does not match encoder, using secondary device {match} for encoder {vcodec} [hwdevices].") preopts.extend(['-init_hw_device', '%s=sma2:%s' % (k, match)]) options['video']['device'] = "sma2" options['video']['decode_device'] = "sma" break except KeyboardInterrupt: raise - except: + except Exception as e: self.log.exception("Error when trying to determine hardware acceleration support.") + self.log.exception(e) preopts.extend(self.settings.preopts) postopts.extend(self.settings.postopts) @@ -1490,64 +1494,73 @@ def checkDisposition(self, allowed, source): return False return True - # Hardware acceleration options now with bit depth safety checks - def setAcceleration(self, video_codec, pix_fmt, codecs=[], pix_fmts=[]): + def set_decoder(self, video_codec: str, pix_fmt: str): + """ + Return the appropriate opts for decoding, using hardware accelleration if specified & available within ffmpeg. + """ opts = [] - pix_fmts = pix_fmts or self.converter.ffmpeg.pix_fmts - bit_depth = pix_fmts.get(pix_fmt, 0) - device = None - # Look up which codecs and which decoders/encoders are available in this build of ffmpeg - codecs = codecs or self.converter.ffmpeg.codecs + codecs = self.converter.ffmpeg.codecs # Supported ffmpeg codecs. + pix_fmts = self.converter.ffmpeg.pix_fmts # Supported ffmpeg pixel formats. + hwaccels = self.converter.ffmpeg.hwaccels # Supported ffmpeg hwaccels. - # Lookup which hardware acceleration platforms are available in this build of ffmpeg - hwaccels = self.converter.ffmpeg.hwaccels + bit_depth = pix_fmts.get(pix_fmt, 0) # Target bit depth. - self.log.debug("Selected hwaccel options:") - self.log.debug(self.settings.hwaccels) - self.log.debug("Selected hwaccel decoder pairs:") - self.log.debug(self.settings.hwaccel_decoders) - self.log.debug("FFMPEG hwaccels:") - self.log.debug(hwaccels) - self.log.debug("Input format %s bit depth %d." % (pix_fmt, bit_depth)) + device = None - # Find the first of the specified hardware acceleration platform that is available in this build of ffmpeg. The order of specified hardware acceleration platforms determines priority. + self.log.debug(f"Selected hwaccel options: {self.settings.hwaccels}.") + self.log.debug(f"Selected hwaccel decoder pairs: {self.settings.hwaccel_decoders}.") + self.log.debug(f"Supported ffmpeg hwaccels: {hwaccels}.") + self.log.debug(f"Input format: {pix_fmt} with bit depth {bit_depth}.") + + """ + Find the first hwaccel platform that: + 1. Is specified by settings.hwaccels. + 2. Is included in this build of ffmpeg. + 3. Is considered by ffmpeg to be a valid decoder for the input codec. + 4. Is included in hwaccel_decoders. + 5. Supports the given pixel format. + + Given that all these criteria are met, opts will be extended to include -hwaccel and -vcodec. Additional + parameters (-hwaccel_output_format, -init_hw_device/-hwaccel_device) will be included if specified by their + corresponding setting. + """ for hwaccel in self.settings.hwaccels: if hwaccel in hwaccels: - device = self.settings.hwdevices.get(hwaccel) - if device: - self.log.debug("Setting hwaccel device to %s." % device) - opts.extend(['-init_hw_device', '%s=sma:%s' % (hwaccel, device)]) - opts.extend(['-hwaccel_device', 'sma']) - - self.log.info("%s hwaccel is supported by this ffmpeg build and will be used [hwaccels]." % hwaccel) - opts.extend(['-hwaccel', hwaccel]) - if self.settings.hwoutputfmt.get(hwaccel): - opts.extend(['-hwaccel_output_format', self.settings.hwoutputfmt[hwaccel]]) - - # If there's a decoder for this acceleration platform, also use it - decoder = self.converter.ffmpeg.hwaccel_decoder(video_codec, self.settings.hwoutputfmt.get(hwaccel, hwaccel)) - self.log.debug("Decoder: %s." % decoder) - if decoder in codecs[video_codec]['decoders'] and decoder in self.settings.hwaccel_decoders: - if Converter.decoder(decoder).supportsBitDepth(bit_depth): - self.log.info("%s decoder is also supported by this ffmpeg build and will also be used [hwaccel-decoders]." % decoder) - opts.extend(['-vcodec', decoder]) - self.log.debug("Decoder formats:") - self.log.debug(self.converter.ffmpeg.decoder_formats(decoder)) - else: - self.log.debug("Decoder %s is supported but cannot support bit depth %d of format %s." % (decoder, bit_depth, pix_fmt)) - break - if "-vcodec" not in opts: - # No matching decoder found for hwaccel, see if there's still a valid decoder that may not match - for decoder in self.settings.hwaccel_decoders: - if decoder in codecs[video_codec]['decoders'] and decoder in self.settings.hwaccel_decoders and decoder.startswith(video_codec): - if Converter.decoder(decoder).supportsBitDepth(bit_depth): - self.log.info("%s decoder is supported by this ffmpeg build and will also be used [hwaccel-decoders]." % decoder) - opts.extend(['-vcodec', decoder]) - self.log.debug("Decoder formats:") - self.log.debug(self.converter.ffmpeg.decoder_formats(decoder)) + target_decoder = self.converter.ffmpeg.hwaccel_decoder(video_codec, + self.settings.hwoutputfmt.get(hwaccel, + hwaccel)) + self.log.debug(f"Target decoder: {target_decoder}") + + self.log.debug("Target decoder pixel formats:") + self.log.debug(self.converter.ffmpeg.decoder_formats(target_decoder)) + + if target_decoder in codecs[video_codec][ + 'decoders'] and target_decoder in self.settings.hwaccel_decoders: + if Converter.decoder(target_decoder).supportsBitDepth(bit_depth): + self.log.debug( + f"Target decoder {target_decoder} is supported by this codec and included in hwaccel-decoders, using. [hwaccel-decoders]") + + # set hwaccel and hwaccel_output_format, if specified + opts.extend(['-hwaccel', hwaccel]) + if self.settings.hwoutputfmt.get(hwaccel): + opts.extend(['-hwaccel_output_format', self.settings.hwoutputfmt[hwaccel]]) + + # set hw device, if specified + device = self.settings.hwdevices.get(hwaccel) + if device: + self.log.debug("Setting hwaccel device to %s." % device) + opts.extend(['-init_hw_device', '%s=sma:%s' % (hwaccel, device)]) + opts.extend(['-hwaccel_device', 'sma']) + + # set vcodec + opts.extend(['-vcodec', target_decoder]) + break else: - self.log.debug("Decoder %s is supported but cannot support bit depth %d of format %s." % (decoder, bit_depth, pix_fmt)) + self.log.debug( + "Decoder %s is supported & included in hwaccel-decoders, but cannot support bit depth %d of format %s." % ( + target_decoder, bit_depth, pix_fmt)) + return opts, device # Using sorting and filtering to determine which audio track should be flagged as default, only one track will be selected From 701218cdbab66f3269df97d33c172028e78ac5ff Mon Sep 17 00:00:00 2001 From: lizardfish0 <97480684+lizardfish0@users.noreply.github.com> Date: Sun, 18 Feb 2024 19:34:54 -0700 Subject: [PATCH 2/4] Add new setting for hwaccel decoder overrides. --- resources/readsettings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/readsettings.py b/resources/readsettings.py index 2d543c03..adc3369a 100644 --- a/resources/readsettings.py +++ b/resources/readsettings.py @@ -100,6 +100,7 @@ class ReadSettings: 'threads': 0, 'hwaccels': '', 'hwaccel-decoders': '', + 'hwaccel-decoder-override': '', 'hwdevices': '', 'hwaccel-output-format': '', 'output-directory': '', @@ -484,6 +485,7 @@ def readConfig(self, config): self.threads = config.getint(section, 'threads') self.hwaccels = config.getlist(section, 'hwaccels') self.hwaccel_decoders = config.getlist(section, "hwaccel-decoders") + self.hwaccel_decoder_override = config.getdict(section, "hwaccel-decoder-override") self.hwdevices = config.getdict(section, "hwdevices", lower=False, replace=[]) self.hwoutputfmt = config.getdict(section, "hwaccel-output-format") self.output_dir = config.getdirectory(section, "output-directory") From 37dacadf1da157de0a59a052e37753b0914214dd Mon Sep 17 00:00:00 2001 From: lizardfish0 <97480684+lizardfish0@users.noreply.github.com> Date: Sun, 18 Feb 2024 19:35:22 -0700 Subject: [PATCH 3/4] Quick index out-of-bounds fix for pixel format accessors. --- converter/ffmpeg.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/converter/ffmpeg.py b/converter/ffmpeg.py index b842d318..6fc8b3c0 100644 --- a/converter/ffmpeg.py +++ b/converter/ffmpeg.py @@ -540,15 +540,25 @@ def hwaccel_decoder(self, video_codec, hwaccel): def encoder_formats(self, encoder): prefix = "Supported pixel formats:" - formatline = next((line.strip() for line in self._get_stdout([self.ffmpeg_path, '-hide_banner', '-h', 'encoder=%s' % encoder]).split('\n')[1:] if line and line.strip().startswith(prefix)), "") - formats = formatline.split(":") - return formats[1].strip().split(" ") if formats and len(formats) > 0 else [] + format_line = next((line.strip() for line in self._get_stdout([self.ffmpeg_path, '-hide_banner', '-h', f"encoder={encoder}"]).split('\n')[1:] if line and line.strip().startswith(prefix)), "") + + if format_line: + formats = format_line.split(":") + if len(formats) > 1: + return formats[1].strip().split(" ") + else: + return [] def decoder_formats(self, decoder): prefix = "Supported pixel formats:" - formatline = next((line.strip() for line in self._get_stdout([self.ffmpeg_path, '-hide_banner', '-h', 'decoder=%s' % decoder]).split('\n')[1:] if line and line.strip().startswith(prefix)), "") - formats = formatline.split(":") - return formats[1].strip().split(" ") if formats and len(formats) > 0 else [] + format_line = next((line.strip() for line in self._get_stdout([self.ffmpeg_path, '-hide_banner', '-h', f"decoder={decoder}"]).split('\n')[1:] if line and line.strip().startswith(prefix)), "") + + if format_line: + formats = format_line.split(":") + if len(formats) > 1: + return formats[1].strip().split(" ") + else: + return [] @staticmethod def _spawn(cmds): From ec324c29f4ec76cd0210c4632979fb960cf9b583 Mon Sep 17 00:00:00 2001 From: lizardfish0 <97480684+lizardfish0@users.noreply.github.com> Date: Sun, 18 Feb 2024 19:35:55 -0700 Subject: [PATCH 4/4] Add handling for hwaccel decoder overrides. --- resources/mediaprocessor.py | 106 ++++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 47 deletions(-) diff --git a/resources/mediaprocessor.py b/resources/mediaprocessor.py index 4099ca2a..f9cbb1ca 100644 --- a/resources/mediaprocessor.py +++ b/resources/mediaprocessor.py @@ -1512,54 +1512,66 @@ def set_decoder(self, video_codec: str, pix_fmt: str): self.log.debug(f"Supported ffmpeg hwaccels: {hwaccels}.") self.log.debug(f"Input format: {pix_fmt} with bit depth {bit_depth}.") - """ - Find the first hwaccel platform that: - 1. Is specified by settings.hwaccels. - 2. Is included in this build of ffmpeg. - 3. Is considered by ffmpeg to be a valid decoder for the input codec. - 4. Is included in hwaccel_decoders. - 5. Supports the given pixel format. - - Given that all these criteria are met, opts will be extended to include -hwaccel and -vcodec. Additional - parameters (-hwaccel_output_format, -init_hw_device/-hwaccel_device) will be included if specified by their - corresponding setting. - """ - for hwaccel in self.settings.hwaccels: - if hwaccel in hwaccels: - target_decoder = self.converter.ffmpeg.hwaccel_decoder(video_codec, - self.settings.hwoutputfmt.get(hwaccel, - hwaccel)) - self.log.debug(f"Target decoder: {target_decoder}") - - self.log.debug("Target decoder pixel formats:") - self.log.debug(self.converter.ffmpeg.decoder_formats(target_decoder)) - - if target_decoder in codecs[video_codec][ - 'decoders'] and target_decoder in self.settings.hwaccel_decoders: - if Converter.decoder(target_decoder).supportsBitDepth(bit_depth): - self.log.debug( - f"Target decoder {target_decoder} is supported by this codec and included in hwaccel-decoders, using. [hwaccel-decoders]") - - # set hwaccel and hwaccel_output_format, if specified - opts.extend(['-hwaccel', hwaccel]) - if self.settings.hwoutputfmt.get(hwaccel): - opts.extend(['-hwaccel_output_format', self.settings.hwoutputfmt[hwaccel]]) - - # set hw device, if specified - device = self.settings.hwdevices.get(hwaccel) - if device: - self.log.debug("Setting hwaccel device to %s." % device) - opts.extend(['-init_hw_device', '%s=sma:%s' % (hwaccel, device)]) - opts.extend(['-hwaccel_device', 'sma']) - - # set vcodec - opts.extend(['-vcodec', target_decoder]) + def _add_hwaccel_opts(_hwaccel: str, _decoder: str): + # set hwaccel and hwaccel_output_format, if specified + opts.extend(['-hwaccel', _hwaccel]) + if self.settings.hwoutputfmt.get(_hwaccel): + opts.extend(['-hwaccel_output_format', self.settings.hwoutputfmt[_hwaccel]]) - break - else: - self.log.debug( - "Decoder %s is supported & included in hwaccel-decoders, but cannot support bit depth %d of format %s." % ( - target_decoder, bit_depth, pix_fmt)) + # set hw device, if specified + nonlocal device + device = self.settings.hwdevices.get(_hwaccel) + if device: + self.log.debug("Setting hwaccel device to %s." % device) + opts.extend(['-init_hw_device', '%s=sma:%s' % (_hwaccel, device)]) + opts.extend(['-hwaccel_device', 'sma']) + + # Set vcodec + opts.extend(['-vcodec', _decoder]) + + # If there's a manually specified hwaccel/decoder pairing for this codec, use it. + if video_codec in self.settings.hwaccel_decoder_override: + hwaccel, target_decoder = self.settings.hwaccel_decoder_override[video_codec].split('.') + + if target_decoder in codecs[video_codec]['decoders']: + self.log.debug(f"Detecting override for codec={video_codec}, setting hwaccel={hwaccel} and vcodec={target_decoder}. [hwaccel-decoders]") + + _add_hwaccel_opts(hwaccel, target_decoder) + + else: + """ + Find the first hwaccel platform that: + 1. Is specified by settings.hwaccels. + 2. Is included in this build of ffmpeg. + 3. Is considered by ffmpeg to be a valid decoder for the input codec. + 4. Is included in hwaccel_decoders. + 5. Supports the given pixel format. + + Given that all these criteria are met, opts will be extended to include -hwaccel and -vcodec. Additional + parameters (-hwaccel_output_format, -init_hw_device/-hwaccel_device) will be included if specified by their + corresponding setting. + """ + for hwaccel in self.settings.hwaccels: + if hwaccel in hwaccels: + target_decoder = self.converter.ffmpeg.hwaccel_decoder( + video_codec, + self.settings.hwoutputfmt.get(hwaccel, hwaccel) + ) + + self.log.debug(f"Target decoder: {target_decoder}") + self.log.debug(f"Target decoder pixel formats: {self.converter.ffmpeg.decoder_formats(target_decoder)}.") + + is_supported_decoder = target_decoder in codecs[video_codec]['decoders'] + + if is_supported_decoder and target_decoder in self.settings.hwaccel_decoders: + if Converter.decoder(target_decoder).supportsBitDepth(bit_depth): + self.log.debug( + f"Target decoder {target_decoder} is supported by this codec and included in hwaccel-decoders, using. [hwaccel-decoders]") + + _add_hwaccel_opts(hwaccel, target_decoder) + break + else: + self.log.debug(f"Decoder {target_decoder} is supported & included in hwaccel-decoders, but cannot support bit depth {bit_depth} of format {pix_fmt}.") return opts, device