From 8f8c83d773b2e6438c6ed5e1ccf1da244a1c402c Mon Sep 17 00:00:00 2001 From: "Josh.5" Date: Thu, 28 Dec 2023 08:52:45 +1300 Subject: [PATCH] Some fixes to NVIDIA decode/encode of h264 --- description.md | 6 +++ info.json | 2 +- lib/encoders/nvenc.py | 95 +++++++++++++++++++++++-------------- lib/encoders/qsv.py | 2 +- lib/plugin_stream_mapper.py | 39 ++++++++++----- plugin.py | 2 +- 6 files changed, 96 insertions(+), 50 deletions(-) diff --git a/description.md b/description.md index f7b0bc7..162fab2 100644 --- a/description.md +++ b/description.md @@ -21,6 +21,12 @@ For information on the available encoder settings: - VAAPI - [FFmpeg - VAAPI](https://trac.ffmpeg.org/wiki/Hardware/VAAPI) - [FFmpeg - HWAccelIntro](https://trac.ffmpeg.org/wiki/HWAccelIntro#VAAPI) +- NVENC + - [FFmpeg - HWAccelIntro](https://trac.ffmpeg.org/wiki/HWAccelIntro#NVENC) + - [NVIDIA GPU compatibility table](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new). + - [NVIDIA NVDEC (decoder) compatibility chart](https://en.wikipedia.org/wiki/Nvidia_NVDEC#GPU_support) + - [NVIDIA NVENC (encoder) compatibility chart](https://en.wikipedia.org/wiki/Nvidia_NVENC#Versions) + - [NVIDIA FFmpeg Transcoding Guide](https://developer.nvidia.com/blog/nvidia-ffmpeg-transcoding-guide/) :::important **Legacy Intel Hardware (Broadwell or older)** diff --git a/info.json b/info.json index da3c059..9386dce 100644 --- a/info.json +++ b/info.json @@ -12,5 +12,5 @@ "on_worker_process": 1 }, "tags": "video,ffmpeg", - "version": "0.0.9-beta1" + "version": "0.0.9-beta2" } diff --git a/lib/encoders/nvenc.py b/lib/encoders/nvenc.py index bb9e0c8..49f2466 100644 --- a/lib/encoders/nvenc.py +++ b/lib/encoders/nvenc.py @@ -34,14 +34,11 @@ def list_available_cuda_devices(): :return: """ gpu_dicts = [] - try: # Run the nvidia-smi command result = subprocess.check_output(['nvidia-smi', '-L'], encoding='utf-8') - # Use regular expression to find device IDs, names, and UUIDs gpu_info = re.findall(r'GPU (\d+): (.+) \(UUID: (.+)\)', result) - # Populate the list of dictionaries for each GPU for gpu_id, gpu_name, gpu_uuid in gpu_info: gpu_dict = { @@ -55,11 +52,36 @@ def list_available_cuda_devices(): except subprocess.CalledProcessError: # nvidia-smi command failed, likely no NVIDIA GPU present return [] - # Return the list of GPUs return gpu_dicts +def get_configured_device(settings): + """ + Returns the currently configured device + Checks to ensure that the configured device exists and otherwise will return the first device available + :param settings: + :return: + """ + hardware_device = None + # Set the hardware device + hardware_devices = list_available_cuda_devices() + if not hardware_devices: + # Return no options. No hardware device was found + raise Exception("No NVIDIA device found") + # If we have configured a hardware device + if settings.get_setting('nvenc_device') not in ['none']: + # Attempt to match to that configured hardware device + for hw_device in hardware_devices: + if settings.get_setting('nvenc_device') == hw_device.get('hwaccel_device'): + hardware_device = hw_device + break + # If no matching hardware device is set, then select the first one + if not hardware_device: + hardware_device = hardware_devices[0] + return hardware_device + + class NvencEncoder: encoders = [ "h264_nvenc", @@ -95,52 +117,42 @@ def generate_default_args(settings): :param settings: :return: """ - # Set the hardware device - hardware_devices = list_available_cuda_devices() - if not hardware_devices: - # Return no options. No hardware device was found - raise Exception("No VAAPI device found") - - hardware_device = None - # If we have configured a hardware device - if settings.get_setting('nvenc_device') not in ['none']: - # Attempt to match to that configured hardware device - for hw_device in hardware_devices: - if settings.get_setting('nvenc_device') == hw_device.get('hwaccel_device'): - hardware_device = hw_device - break - # If no matching hardware device is set, then select the first one - if not hardware_device: - hardware_device = hardware_devices[0] + hardware_device = get_configured_device(settings) generic_kwargs = {} advanced_kwargs = {} # Check if we are using a HW accelerated decoder also - if settings.get_setting('nvenc_decoding_method') != 'cpu': + if settings.get_setting('nvenc_decoding_method') in ['cuda', 'nvdec', 'cuvid']: generic_kwargs = { - "-hwaccel": settings.get_setting('enabled_hw_decoding'), - "-hwaccel_device": hardware_device, - "-init_hw_device": "{}=hw".format(settings.get_setting('enabled_hw_decoding')), + "-hwaccel_device": hardware_device.get('hwaccel_device'), + "-hwaccel": settings.get_setting('nvenc_decoding_method'), + "-init_hw_device": "cuda=hw", "-filter_hw_device": "hw", } + if settings.get_setting('nvenc_decoding_method') in ['cuda', 'nvdec']: + generic_kwargs["-hwaccel_output_format"] = "cuda" return generic_kwargs, advanced_kwargs @staticmethod def generate_filtergraphs(): """ - Generate the required filter for enabling QSV HW acceleration + Generate the required filter for enabling NVENC HW acceleration :return: """ - return ["hwupload_cuda"] + return [] - def args(self, stream_id): + def args(self, stream_info, stream_id): + generic_kwargs = {} stream_encoding = [] + # Specify the GPU to use for encoding + hardware_device = get_configured_device(self.settings) + stream_encoding += ['-gpu', str(hardware_device.get('hwaccel_device', '0'))] + # Use defaults for basic mode if self.settings.get_setting('mode') in ['basic']: defaults = self.options() - # Use default LA_ICQ mode stream_encoding += [ '-preset', str(defaults.get('nvenc_preset')), '-profile:v:{}'.format(stream_id), str(defaults.get('nvenc_profile')), @@ -150,13 +162,13 @@ def args(self, stream_id): # Add the preset and tune if self.settings.get_setting('nvenc_preset'): stream_encoding += ['-preset', str(self.settings.get_setting('nvenc_preset'))] - if self.settings.get_setting('nvenc_tune'): + if self.settings.get_setting('nvenc_tune') and self.settings.get_setting('nvenc_tune') != 'auto': stream_encoding += ['-tune', str(self.settings.get_setting('nvenc_tune'))] if self.settings.get_setting('nvenc_tune'): stream_encoding += ['-profile:v:{}'.format(stream_id), str(self.settings.get_setting('nvenc_profile'))] # Apply rate control config - if self.settings.get_setting('nvenc_encoder_ratecontrol_method', 'auto') != 'auto': + if self.settings.get_setting('nvenc_encoder_ratecontrol_method') in ['constqp', 'vbr', 'vbr_hq', 'cbr', 'cbr_hq']: # Set the rate control method stream_encoding += [ '-rc:v:{}'.format(stream_id), str(self.settings.get_setting('nvenc_encoder_ratecontrol_method')) @@ -175,7 +187,13 @@ def args(self, stream_id): if self.settings.get_setting('enable_spatial_aq') or self.settings.get_setting('enable_temporal_aq'): stream_encoding += ['-aq-strength:v:{}'.format(stream_id), str(self.settings.get_setting('aq_strength'))] - return stream_encoding + # If CUVID is enabled, return generic_kwargs + if self.settings.get_setting('nvenc_decoding_method') in ['cuvid']: + generic_kwargs = { + '-c:v:{}'.format(stream_id): '{}_cuvid'.format(stream_info.get('codec_name', 'unknown_codec_name')), + } + + return generic_kwargs, stream_encoding def __set_default_option(self, select_options, key, default_option=None): """ @@ -214,7 +232,7 @@ def get_nvenc_device_form_settings(self): default_option = hw_device.get('hwaccel_device', 'none') values['select_options'].append({ "value": hw_device.get('hwaccel_device', 'none'), - "label": "NVIDIA device '{}'".format(hw_device.get('hwaccel_device_path', 'not found')), + "label": "NVIDIA device '{}'".format(hw_device.get('hwaccel_device_name', 'not found')), }) if not default_option: default_option = 'none' @@ -227,7 +245,10 @@ def get_nvenc_device_form_settings(self): def get_nvenc_decoding_method_form_settings(self): values = { "label": "Enable HW Decoding", - "description": "Warning. Ensure your device supports decoding the source video codec or it will fail.", + "description": "Warning. Ensure your device supports decoding the source video codec or it will fail.\n" + "Decode the video stream using hardware accelerated decoding.\n" + "This enables full hardware transcode with NVDEC and NVENC, using only GPU memory for the entire video transcode.\n" + "It is recommended that for 10-bit encodes, disable this option.", "sub_setting": True, "input_type": "select", "select_options": [ @@ -237,11 +258,15 @@ def get_nvenc_decoding_method_form_settings(self): }, { "value": "cuda", - "label": "CUDA - Use NVIDIA CUDA for decoding the video source (best compatibility with older GPUs)", + "label": "CUDA - Use the GPUs general purpose CUDA for HW decoding the video source (recommended)", }, { "value": "nvdec", "label": "NVDEC - Use the GPUs dedicated video decoder", + }, + { + "value": "cuvid", + "label": "CUVID - Older interface for HW video decoding. Older GPUs may perform better with CUVID over CUDA", } ] } diff --git a/lib/encoders/qsv.py b/lib/encoders/qsv.py index 79fe2f1..1aa1b8a 100644 --- a/lib/encoders/qsv.py +++ b/lib/encoders/qsv.py @@ -90,7 +90,7 @@ def args(self, stream_id): # Add the preset and tune if self.settings.get_setting('preset'): stream_encoding += ['-preset', str(self.settings.get_setting('preset'))] - if self.settings.get_setting('tune'): + if self.settings.get_setting('tune') and self.settings.get_setting('tune') != 'auto': stream_encoding += ['-tune', str(self.settings.get_setting('tune'))] # TODO: Split this into encoder specific functions diff --git a/lib/plugin_stream_mapper.py b/lib/plugin_stream_mapper.py index a2c889f..a8d1de7 100644 --- a/lib/plugin_stream_mapper.py +++ b/lib/plugin_stream_mapper.py @@ -24,10 +24,10 @@ import logging from video_transcoder.lib import tools -from video_transcoder.lib.encoders import vaapi from video_transcoder.lib.encoders.libx import LibxEncoder from video_transcoder.lib.encoders.qsv import QsvEncoder from video_transcoder.lib.encoders.vaapi import VaapiEncoder +from video_transcoder.lib.encoders.nvenc import NvencEncoder from video_transcoder.lib.ffmpeg import StreamMapper # Configure plugin logger @@ -41,7 +41,6 @@ def __init__(self): self.settings = None self.complex_video_filters = {} self.crop_value = None - self.vaapi_encoders = ['hevc_vaapi', 'h264_vaapi'] self.forced_encode = False def set_default_values(self, settings, abspath, probe): @@ -101,12 +100,15 @@ def set_default_values(self, settings, abspath, probe): generic_kwargs, advanced_kwargs = QsvEncoder.generate_default_args(self.settings) self.set_ffmpeg_generic_options(**generic_kwargs) self.set_ffmpeg_advanced_options(**advanced_kwargs) - elif self.settings.get_setting('video_encoder') in self.vaapi_encoders: + elif self.settings.get_setting('video_encoder') in VaapiEncoder.encoders: generic_kwargs, advanced_kwargs = VaapiEncoder.generate_default_args(self.settings) self.set_ffmpeg_generic_options(**generic_kwargs) self.set_ffmpeg_advanced_options(**advanced_kwargs) + elif self.settings.get_setting('video_encoder') in NvencEncoder.encoders: + generic_kwargs, advanced_kwargs = NvencEncoder.generate_default_args(self.settings) + self.set_ffmpeg_generic_options(**generic_kwargs) + self.set_ffmpeg_advanced_options(**advanced_kwargs) # TODO: Disable any options not compatible with this encoder - # TODO: Add NVENC args def scale_resolution(self, stream_info: dict): def get_test_resolution(settings): @@ -177,13 +179,20 @@ def build_filter_chain(self, stream_info, stream_id): if self.settings.get_setting('video_encoder') in QsvEncoder.encoders: # Add filtergraph required for using QSV encoding hardware_filters += QsvEncoder.generate_filtergraphs() - elif self.settings.get_setting('video_encoder') in self.vaapi_encoders: + elif self.settings.get_setting('video_encoder') in VaapiEncoder.encoders: # Add filtergraph required for using VAAPI encoding hardware_filters += VaapiEncoder.generate_filtergraphs() # If we are using software filters, then disable vaapi surfaces. # Instead, putput software frames if software_filters: self.set_ffmpeg_generic_options(**{'-hwaccel_output_format': 'nv12'}) + elif self.settings.get_setting('video_encoder') in NvencEncoder.encoders: + # Add filtergraph required for using CUDA encoding + hardware_filters += NvencEncoder.generate_filtergraphs() + # If we are using software filters, then disable cuda surfaces. + # Instead, putput software frames + if software_filters: + self.set_ffmpeg_generic_options(**{'-hwaccel_output_format': 'nv12'}) # TODO: Add HW scaling filter if available (disable software filter above) @@ -238,9 +247,11 @@ def test_stream_needs_processing(self, stream_info: dict): return False # Check if video filters need to be applied (build_filter_chain) + codec_type = stream_info.get('codec_type', '').lower() + codec_name = stream_info.get('codec_name', '').lower() if self.settings.get_setting('apply_smart_filters'): # Video filters - if stream_info.get('codec_type', '').lower() in ['video']: + if codec_type in ['video']: # Check if autocrop filter needs to be applied if self.settings.get_setting('autocrop_black_bars') and self.crop_value: return True @@ -250,7 +261,7 @@ def test_stream_needs_processing(self, stream_info: dict): if vid_width: return True # Data/Attachment filters - if stream_info.get('codec_type', '').lower() in ['data', 'attachment']: + if codec_type in ['data', 'attachment']: # Enable removal of data and attachment streams if self.settings.get_setting('remove_data_and_attachment_streams'): # Remove it @@ -258,8 +269,7 @@ def test_stream_needs_processing(self, stream_info: dict): # If the stream is a video, add a final check if the codec is already the correct format # (Ignore checks if force transcode is set) - if stream_info.get('codec_type', '').lower() in ['video'] and stream_info.get( - 'codec_name', '').lower() == self.settings.get_setting('video_codec'): + if codec_type in ['video'] and codec_name == self.settings.get_setting('video_codec'): if not self.settings.get_setting('force_transcode'): return False else: @@ -286,7 +296,7 @@ def custom_stream_mapping(self, stream_info: dict, stream_id: int): codec_type = stream_info.get('codec_type', '').lower() stream_specifier = '{}:{}'.format(ident.get(codec_type), stream_id) map_identifier = '0:{}'.format(stream_specifier) - if stream_info.get('codec_type', '').lower() in ['video']: + if codec_type in ['video']: if self.settings.get_setting('mode') == 'advanced': stream_encoding = ['-c:{}'.format(stream_specifier)] stream_encoding += self.settings.get_setting('custom_options').split() @@ -311,7 +321,12 @@ def custom_stream_mapping(self, stream_info: dict, stream_id: int): elif self.settings.get_setting('video_encoder') in VaapiEncoder.encoders: vaapi_encoder = VaapiEncoder(self.settings) stream_encoding += vaapi_encoder.args(stream_id) - elif stream_info.get('codec_type', '').lower() in ['data']: + elif self.settings.get_setting('video_encoder') in NvencEncoder.encoders: + nvenc_encoder = NvencEncoder(self.settings) + generic_kwargs, stream_encoding_args = nvenc_encoder.args(stream_info, stream_id) + self.set_ffmpeg_generic_options(**generic_kwargs) + stream_encoding += stream_encoding_args + elif codec_type in ['data']: if not self.settings.get_setting('apply_smart_filters'): # If smart filters are not enabled, return 'False' to let the default mapping just copy the data stream return False @@ -323,7 +338,7 @@ def custom_stream_mapping(self, stream_info: dict, stream_id: int): } # Resort to returning 'False' to let the default mapping just copy the data stream return False - elif stream_info.get('codec_type', '').lower() in ['attachment']: + elif codec_type in ['attachment']: if not self.settings.get_setting('apply_smart_filters'): # If smart filters are not enabled, return 'False' to let the default mapping just copy the attachment # stream diff --git a/plugin.py b/plugin.py index 1b67a43..2ee1354 100644 --- a/plugin.py +++ b/plugin.py @@ -146,7 +146,7 @@ def file_marked_as_force_transcoded(path): has_been_force_transcoded = '' if has_been_force_transcoded == 'force_transcoded': - # This file has already has been force transcoded + # This file has already been force transcoded return True # Default to...