Skip to content

Commit

Permalink
Some fixes to NVIDIA decode/encode of h264
Browse files Browse the repository at this point in the history
  • Loading branch information
Josh5 committed Dec 27, 2023
1 parent 1b5e478 commit 8f8c83d
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 50 deletions.
6 changes: 6 additions & 0 deletions description.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)**
Expand Down
2 changes: 1 addition & 1 deletion info.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
"on_worker_process": 1
},
"tags": "video,ffmpeg",
"version": "0.0.9-beta1"
"version": "0.0.9-beta2"
}
95 changes: 60 additions & 35 deletions lib/encoders/nvenc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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",
Expand Down Expand Up @@ -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')),
Expand All @@ -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'))
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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'
Expand All @@ -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": [
Expand All @@ -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",
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion lib/encoders/qsv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 27 additions & 12 deletions lib/plugin_stream_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -250,16 +261,15 @@ 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
return True

# 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:
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Expand Down

0 comments on commit 8f8c83d

Please sign in to comment.