Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework hardware acceleration decoder selection #1705

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,9 @@ pip-log.txt
## VS Code
#################
.vscode

# PyCharm
.idea

# pyenv
.python-version
22 changes: 16 additions & 6 deletions converter/ffmpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)), "")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes avoid an index out-of-bounds-errors when trying to get formats for invalid codecs.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this was just a mistake on my part, the line

return formats[1].strip().split(" ") if formats and len(formats) > 0 else []

should have read

return formats[1].strip().split(" ") if formats and len(formats) > 1 else []

Fixed that with e62addf


if format_line:
formats = format_line.split(":")
if len(formats) > 1:
return formats[1].strip().split(" ")
else:
return []

@staticmethod
def _spawn(cmds):
Expand Down
149 changes: 87 additions & 62 deletions resources/mediaprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1490,64 +1494,85 @@ 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):
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed this as its specific to decoders.

"""
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)
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.

bit_depth = pix_fmts.get(pix_fmt, 0) # Target bit depth.

device = None
# Look up which codecs and which decoders/encoders are available in this build of ffmpeg
codecs = codecs or self.converter.ffmpeg.codecs

# Lookup which hardware acceleration platforms are available in this build of ffmpeg
hwaccels = self.converter.ffmpeg.hwaccels

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))

# 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.
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))
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(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}.")

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]])

# 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:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a specific input codec, users will be able to specify a hwaccel and a decoder using the format <codec>:<hwaccel>.<decoder>.

ex:

hwaccel_decoder_override = av1:vaapi.av1

Happy to add a bit to the wiki about this.

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:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This slightly modifies existing behavior. Only specifying hwaccels= in settings will now do nothing. Most examples I've seen of users attempting hardware acceleration on this repo have been specifying their decoders anyways.

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

# Using sorting and filtering to determine which audio track should be flagged as default, only one track will be selected
Expand Down
2 changes: 2 additions & 0 deletions resources/readsettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class ReadSettings:
'threads': 0,
'hwaccels': '',
'hwaccel-decoders': '',
'hwaccel-decoder-override': '',
'hwdevices': '',
'hwaccel-output-format': '',
'output-directory': '',
Expand Down Expand Up @@ -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")
Expand Down