Cleanup: Removed unrelated video optimizer files
This commit is contained in:
@@ -1,74 +0,0 @@
|
||||
require 'optparse'
|
||||
require 'open3'
|
||||
require 'json'
|
||||
|
||||
options = {}
|
||||
OptionParser.new do |opts|
|
||||
opts.banner = "Usage: ruby delete_av1.rb [directory]"
|
||||
end.parse!
|
||||
|
||||
directory = ARGV[0]
|
||||
if directory.nil? || !Dir.exist?(directory)
|
||||
puts "Usage: ruby delete_av1.rb [directory]"
|
||||
exit 1
|
||||
end
|
||||
|
||||
def is_av1?(file_path)
|
||||
# Check only video stream
|
||||
cmd = ['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_streams', '-select_streams', 'v:0', file_path]
|
||||
stdout, _, status = Open3.capture3(*cmd)
|
||||
return false unless status.success?
|
||||
|
||||
begin
|
||||
data = JSON.parse(stdout)
|
||||
stream = data['streams'][0]
|
||||
return stream && stream['codec_name'] == 'av1'
|
||||
rescue
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
puts "Scanning #{directory} for AV1 files..."
|
||||
puts "This might take a while..."
|
||||
|
||||
av1_files = []
|
||||
|
||||
Dir.glob("#{directory}/**/*").each do |file|
|
||||
next if File.directory?(file)
|
||||
next unless ['.mp4', '.mkv', '.avi', '.mov', '.m4v'].include?(File.extname(file).downcase)
|
||||
|
||||
if is_av1?(file)
|
||||
puts "Found: #{file}"
|
||||
av1_files << file
|
||||
end
|
||||
end
|
||||
|
||||
if av1_files.empty?
|
||||
puts "\nNo AV1 files found."
|
||||
exit
|
||||
end
|
||||
|
||||
puts "\n" + "="*40
|
||||
puts "Found #{av1_files.length} AV1 files:"
|
||||
av1_files.each { |f| puts " - #{f}" }
|
||||
puts "="*40
|
||||
puts "\nFound #{av1_files.length} files encoded with AV1."
|
||||
print "Do you want to DELETE these files? [y/N]: "
|
||||
STDOUT.flush
|
||||
confirm = STDIN.gets.chomp.strip.downcase
|
||||
|
||||
if confirm == 'y'
|
||||
deleted_count = 0
|
||||
av1_files.each do |file|
|
||||
begin
|
||||
File.delete(file)
|
||||
puts "Deleted: #{file}"
|
||||
deleted_count += 1
|
||||
rescue => e
|
||||
puts "Failed to delete #{file}: #{e.message}"
|
||||
end
|
||||
end
|
||||
puts "\nDeletion complete. #{deleted_count} files removed."
|
||||
else
|
||||
puts "\nOperation cancelled. No files were deleted."
|
||||
end
|
||||
598
nvenc.py
598
nvenc.py
@@ -1,598 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
plugins.nvenc.py
|
||||
|
||||
Written by: Josh.5 <jsunnex@gmail.com>
|
||||
Date: 27 Dec 2023, (11:21 AM)
|
||||
|
||||
Copyright:
|
||||
Copyright (C) 2021 Josh Sunnex
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General
|
||||
Public License as published by the Free Software Foundation, version 3.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
|
||||
implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with this program.
|
||||
If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
"""
|
||||
Notes:
|
||||
- Listing available encoder options:
|
||||
ffmpeg -h encoder=h264_nvenc
|
||||
ffmpeg -h encoder=hevc_nvenc
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from video_transcoder.lib.encoders.base import Encoder
|
||||
|
||||
logger = logging.getLogger("Unmanic.Plugin.video_transcoder")
|
||||
|
||||
|
||||
def list_available_cuda_devices():
|
||||
"""
|
||||
Return a list of available CUDA devices via nvidia-smi.
|
||||
"""
|
||||
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_dicts.append({
|
||||
'hwaccel_device': gpu_id,
|
||||
'hwaccel_device_name': f"{gpu_name} (UUID: {gpu_uuid})",
|
||||
})
|
||||
except FileNotFoundError:
|
||||
# nvidia-smi executable not found
|
||||
return []
|
||||
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(Encoder):
|
||||
def __init__(self, settings=None, probe=None):
|
||||
super().__init__(settings=settings, probe=probe)
|
||||
|
||||
def _map_pix_fmt(self, is_h264: bool, is_10bit: bool) -> str:
|
||||
if is_10bit and not is_h264:
|
||||
return "p010le"
|
||||
else:
|
||||
return "nv12"
|
||||
|
||||
def provides(self):
|
||||
return {
|
||||
"h264_nvenc": {
|
||||
"codec": "h264",
|
||||
"label": "NVENC - h264_nvenc",
|
||||
},
|
||||
"hevc_nvenc": {
|
||||
"codec": "hevc",
|
||||
"label": "NVENC - hevc_nvenc",
|
||||
},
|
||||
"av1_nvenc": {
|
||||
"codec": "av1",
|
||||
"label": "NVENC - av1_nvenc",
|
||||
},
|
||||
}
|
||||
|
||||
def options(self):
|
||||
return {
|
||||
"nvenc_device": "none",
|
||||
"nvenc_decoding_method": "cpu",
|
||||
"nvenc_preset": "p4",
|
||||
"nvenc_tune": "auto",
|
||||
"nvenc_profile": "main",
|
||||
"nvenc_encoder_ratecontrol_method": "auto",
|
||||
"nvenc_encoder_ratecontrol_lookahead": 0,
|
||||
"nvenc_enable_spatial_aq": False,
|
||||
"nvenc_enable_temporal_aq": False,
|
||||
"nvenc_aq_strength": 8,
|
||||
}
|
||||
|
||||
def generate_default_args(self):
|
||||
"""
|
||||
Generate a list of args for using a NVENC decoder
|
||||
|
||||
REF: https://trac.ffmpeg.org/wiki/HWAccelIntro#NVDECCUVID
|
||||
|
||||
:return:
|
||||
"""
|
||||
hardware_device = get_configured_device(self.settings)
|
||||
|
||||
generic_kwargs = {}
|
||||
advanced_kwargs = {}
|
||||
# Check if we are using a HW accelerated decoder also
|
||||
if self.settings.get_setting('nvenc_decoding_method') in ['cuda', 'nvdec', 'cuvid']:
|
||||
generic_kwargs = {
|
||||
"-hwaccel_device": hardware_device.get('hwaccel_device'),
|
||||
"-hwaccel": self.settings.get_setting('nvenc_decoding_method'),
|
||||
"-init_hw_device": "cuda=hw",
|
||||
"-filter_hw_device": "hw",
|
||||
}
|
||||
if self.settings.get_setting('nvenc_decoding_method') in ['cuda', 'nvdec']:
|
||||
generic_kwargs["-hwaccel_output_format"] = "cuda"
|
||||
|
||||
return generic_kwargs, advanced_kwargs
|
||||
|
||||
def generate_filtergraphs(self, current_filter_args, smart_filters, encoder_name):
|
||||
"""
|
||||
Generate the required filter for enabling NVENC/CUDA HW acceleration.
|
||||
|
||||
:return:
|
||||
"""
|
||||
generic_kwargs = {}
|
||||
advanced_kwargs = {}
|
||||
start_filter_args = []
|
||||
end_filter_args = []
|
||||
|
||||
# Loop over any HW smart filters to be applied and add them as required.
|
||||
hw_smart_filters = []
|
||||
remaining_smart_filters = []
|
||||
for sf in smart_filters:
|
||||
if sf.get("scale"):
|
||||
w = sf["scale"]["values"]["width"]
|
||||
hw_smart_filters.append(f"scale_cuda={w}:-1")
|
||||
else:
|
||||
remaining_smart_filters.append(sf)
|
||||
|
||||
# Check for HW accelerated decode mode
|
||||
# All decode methods ('cuda', 'nvdec', 'cuvid') are handled by the same
|
||||
# filtergraph logic and output CUDA frames. The main FFmpeg command handles the specific decoder.
|
||||
hw_decode = (self.settings.get_setting('nvenc_decoding_method') or '').lower() in ('cuda', 'nvdec', 'cuvid')
|
||||
|
||||
# Check software format to use
|
||||
target_fmt = self._target_pix_fmt_for_encoder(encoder_name)
|
||||
|
||||
# Handle HDR
|
||||
enc_supports_hdr = (encoder_name in ["hevc_nvenc", "av1_nvenc"])
|
||||
target_color_config = self._target_color_config_for_encoder(encoder_name)
|
||||
|
||||
# If we have SW filters:
|
||||
if remaining_smart_filters or current_filter_args:
|
||||
# If we have SW filters and HW decode (CUDA/NVDEC) is enabled, make decoder produce SW frames
|
||||
if hw_decode:
|
||||
generic_kwargs['-hwaccel_output_format'] = target_fmt
|
||||
|
||||
# Add filter to upload software frames to CUDA for CUDA filters
|
||||
# Note, format conversion (if any - eg yuv422p10le -> p010le) happens after the software filters.
|
||||
# If a user applies a custom software filter that does not support the pix_fmt, then will need to prefix it with 'format=p010le'
|
||||
chain = [f"format={target_fmt}"]
|
||||
if enc_supports_hdr and target_color_config.get('apply_color_params'):
|
||||
# Apply setparams filter if software filters exist (apply at the start of the filters list) to preserve HDR tags
|
||||
chain.append(target_color_config['setparams_filter'])
|
||||
chain += ["hwupload_cuda"]
|
||||
end_filter_args.append(",".join(chain))
|
||||
# If we have no software filters:
|
||||
elif not hw_decode:
|
||||
# CPU decode -> setparams (if HDR) -> upload to CUDA
|
||||
chain = [f"format={target_fmt}"]
|
||||
if enc_supports_hdr and target_color_config.get('apply_color_params'):
|
||||
chain.append(target_color_config['setparams_filter'])
|
||||
chain.append("hwupload_cuda")
|
||||
start_filter_args.append(",".join(chain))
|
||||
|
||||
# Add the smart filters to the end
|
||||
end_filter_args += hw_smart_filters
|
||||
|
||||
# Return built args
|
||||
return {
|
||||
"generic_kwargs": generic_kwargs,
|
||||
"advanced_kwargs": advanced_kwargs,
|
||||
"smart_filters": remaining_smart_filters,
|
||||
"start_filter_args": start_filter_args,
|
||||
"end_filter_args": end_filter_args,
|
||||
}
|
||||
|
||||
def encoder_details(self, encoder):
|
||||
hardware_devices = list_available_cuda_devices()
|
||||
if not hardware_devices:
|
||||
# Return no options. No hardware device was found
|
||||
return {}
|
||||
provides = self.provides()
|
||||
return provides.get(encoder, {})
|
||||
|
||||
def stream_args(self, stream_info, stream_id, encoder_name):
|
||||
generic_kwargs = {}
|
||||
advanced_kwargs = {}
|
||||
encoder_args = []
|
||||
stream_args = []
|
||||
|
||||
# Specify the GPU to use for encoding
|
||||
hardware_device = get_configured_device(self.settings)
|
||||
stream_args += ['-gpu', str(hardware_device.get('hwaccel_device', '0'))]
|
||||
|
||||
# Handle HDR
|
||||
enc_supports_hdr = (encoder_name in ["hevc_nvenc", "av1_nvenc"])
|
||||
if enc_supports_hdr:
|
||||
target_color_config = self._target_color_config_for_encoder(encoder_name)
|
||||
else:
|
||||
target_color_config = {
|
||||
"apply_color_params": False
|
||||
}
|
||||
if enc_supports_hdr and target_color_config.get('apply_color_params'):
|
||||
# Force Main10 profile
|
||||
stream_args += [f'-profile:v:{stream_id}', 'main10']
|
||||
|
||||
# Use defaults for basic mode
|
||||
if self.settings.get_setting('mode') in ['basic']:
|
||||
# Read defaults
|
||||
defaults = self.options()
|
||||
|
||||
if enc_supports_hdr and target_color_config.get('apply_color_params'):
|
||||
# Add HDR color tags to the encoder output stream
|
||||
for k, v in target_color_config.get('stream_color_params', {}).items():
|
||||
stream_args += [k, v]
|
||||
|
||||
stream_args += ['-preset', str(defaults.get('nvenc_preset'))]
|
||||
|
||||
return {
|
||||
"generic_kwargs": generic_kwargs,
|
||||
"advanced_kwargs": advanced_kwargs,
|
||||
"encoder_args": encoder_args,
|
||||
"stream_args": stream_args,
|
||||
}
|
||||
|
||||
# Add the preset and tune
|
||||
if self.settings.get_setting('nvenc_preset'):
|
||||
stream_args += ['-preset', str(self.settings.get_setting('nvenc_preset'))]
|
||||
if self.settings.get_setting('nvenc_tune') and self.settings.get_setting('nvenc_tune') != 'auto':
|
||||
stream_args += ['-tune', str(self.settings.get_setting('nvenc_tune'))]
|
||||
if self.settings.get_setting('nvenc_profile') and self.settings.get_setting('nvenc_profile') != 'auto':
|
||||
stream_args += [f'-profile:v:{stream_id}', str(self.settings.get_setting('nvenc_profile'))]
|
||||
|
||||
# Apply rate control config
|
||||
if self.settings.get_setting('nvenc_encoder_ratecontrol_method') in ['constqp', 'vbr', 'cbr']:
|
||||
# Set the rate control method
|
||||
stream_args += [f'-rc:v:{stream_id}', str(self.settings.get_setting('nvenc_encoder_ratecontrol_method'))]
|
||||
rc_la = int(self.settings.get_setting('nvenc_encoder_ratecontrol_lookahead') or 0)
|
||||
if rc_la > 0:
|
||||
stream_args += [f'-rc-lookahead:v:{stream_id}', str(rc_la)]
|
||||
|
||||
# Apply adaptive quantization
|
||||
if self.settings.get_setting('nvenc_enable_spatial_aq'):
|
||||
stream_args += ['-spatial-aq', '1']
|
||||
if self.settings.get_setting('nvenc_enable_spatial_aq') or self.settings.get_setting('nvenc_enable_temporal_aq'):
|
||||
stream_args += [f'-aq-strength:v:{stream_id}', str(self.settings.get_setting('nvenc_aq_strength'))]
|
||||
if self.settings.get_setting('nvenc_enable_temporal_aq'):
|
||||
stream_args += ['-temporal-aq', '1']
|
||||
|
||||
# If CUVID is enabled, return generic_kwargs
|
||||
if (self.settings.get_setting('nvenc_decoding_method') or '').lower() in ['cuvid']:
|
||||
in_codec = stream_info.get('codec_name', 'unknown_codec_name')
|
||||
generic_kwargs = {f'-c:v:{stream_id}': f'{in_codec}_cuvid'}
|
||||
|
||||
# Add stream color args
|
||||
if enc_supports_hdr and target_color_config.get('apply_color_params'):
|
||||
# Add HDR color tags to the encoder output stream
|
||||
for k, v in target_color_config.get('stream_color_params', {}).items():
|
||||
stream_args += [k, v]
|
||||
|
||||
# Return built args
|
||||
return {
|
||||
"generic_kwargs": generic_kwargs,
|
||||
"advanced_kwargs": advanced_kwargs,
|
||||
"encoder_args": encoder_args,
|
||||
"stream_args": stream_args,
|
||||
}
|
||||
|
||||
def __set_default_option(self, select_options, key, default_option=None):
|
||||
"""
|
||||
Sets the default option if the currently set option is not available
|
||||
|
||||
:param select_options:
|
||||
:param key:
|
||||
:return:
|
||||
"""
|
||||
available_options = []
|
||||
for option in select_options:
|
||||
available_options.append(option.get('value'))
|
||||
if not default_option:
|
||||
default_option = option.get('value')
|
||||
current_value = self.settings.get_setting(key)
|
||||
if not getattr(self.settings, 'apply_default_fallbacks', True):
|
||||
return current_value
|
||||
if current_value not in available_options:
|
||||
# Update in-memory setting for display only.
|
||||
# IMPORTANT: do not persist settings from plugin.
|
||||
# Only the Unmanic API calls should persist to JSON file.
|
||||
self.settings.settings_configured[key] = default_option
|
||||
return default_option
|
||||
return current_value
|
||||
|
||||
def get_nvenc_device_form_settings(self):
|
||||
values = {
|
||||
"label": "NVIDIA Device",
|
||||
"sub_setting": True,
|
||||
"input_type": "select",
|
||||
"select_options": [
|
||||
{
|
||||
"value": "none",
|
||||
"label": "No NVIDIA devices available",
|
||||
}
|
||||
]
|
||||
}
|
||||
default_option = None
|
||||
hardware_devices = list_available_cuda_devices()
|
||||
if hardware_devices:
|
||||
values['select_options'] = []
|
||||
for hw_device in hardware_devices:
|
||||
if not default_option:
|
||||
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_name', 'not found')),
|
||||
})
|
||||
if not default_option:
|
||||
default_option = 'none'
|
||||
|
||||
self.__set_default_option(values['select_options'], 'nvenc_device', default_option=default_option)
|
||||
if self.settings.get_setting('mode') not in ['standard']:
|
||||
values["display"] = "hidden"
|
||||
return values
|
||||
|
||||
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.\n"
|
||||
"This enables full hardware transcode with NVDEC and NVENC, using only GPU memory for the entire video transcode.\n"
|
||||
"If filters are configured in the plugin, decoder will output NV12 software surfaces which are slightly slower.\n"
|
||||
"Note: It is recommended that you disable this option for 10-bit encodes.",
|
||||
"sub_setting": True,
|
||||
"input_type": "select",
|
||||
"select_options": [
|
||||
{
|
||||
"value": "cpu",
|
||||
"label": "Disabled - Use CPU to decode of video source (provides best compatibility)",
|
||||
},
|
||||
{
|
||||
"value": "cuda",
|
||||
"label": "NVDEC/CUDA - Use the GPUs HW decoding the video source and upload surfaces to CUDA (recommended)",
|
||||
},
|
||||
{
|
||||
"value": "cuvid",
|
||||
"label": "CUVID - Older interface for HW video decoding. Kepler or older hardware may perform better with this option",
|
||||
}
|
||||
]
|
||||
}
|
||||
if self.settings.get_setting('mode') not in ['standard']:
|
||||
values["display"] = "hidden"
|
||||
return values
|
||||
|
||||
def get_nvenc_preset_form_settings(self):
|
||||
values = {
|
||||
"label": "Encoder quality preset",
|
||||
"sub_setting": True,
|
||||
"input_type": "select",
|
||||
"select_options": [
|
||||
{
|
||||
"value": "p1",
|
||||
"label": "Fastest (P1)",
|
||||
},
|
||||
{
|
||||
"value": "p2",
|
||||
"label": "Faster (P2)",
|
||||
},
|
||||
{
|
||||
"value": "p3",
|
||||
"label": "Fast (P3)",
|
||||
},
|
||||
{
|
||||
"value": "p4",
|
||||
"label": "Medium (P4) - Balanced performance and quality",
|
||||
},
|
||||
{
|
||||
"value": "p5",
|
||||
"label": "Slow (P5)",
|
||||
},
|
||||
{
|
||||
"value": "p6",
|
||||
"label": "Slower (P6)",
|
||||
},
|
||||
{
|
||||
"value": "p7",
|
||||
"label": "Slowest (P7)",
|
||||
},
|
||||
],
|
||||
}
|
||||
self.__set_default_option(values['select_options'], 'nvenc_preset', default_option='p4')
|
||||
if self.settings.get_setting('mode') not in ['standard']:
|
||||
values["display"] = "hidden"
|
||||
return values
|
||||
|
||||
def get_nvenc_tune_form_settings(self):
|
||||
values = {
|
||||
"label": "Tune for a particular type of source or situation",
|
||||
"sub_setting": True,
|
||||
"input_type": "select",
|
||||
"select_options": [
|
||||
{
|
||||
"value": "auto",
|
||||
"label": "Disabled – Do not apply any tune",
|
||||
},
|
||||
{
|
||||
"value": "hq",
|
||||
"label": "HQ – High quality (ffmpeg default)",
|
||||
},
|
||||
{
|
||||
"value": "ll",
|
||||
"label": "LL – Low latency",
|
||||
},
|
||||
{
|
||||
"value": "ull",
|
||||
"label": "ULL – Ultra low latency",
|
||||
},
|
||||
{
|
||||
"value": "lossless",
|
||||
"label": "Lossless",
|
||||
},
|
||||
],
|
||||
}
|
||||
self.__set_default_option(values['select_options'], 'nvenc_tune', default_option='auto')
|
||||
if self.settings.get_setting('mode') not in ['standard']:
|
||||
values["display"] = "hidden"
|
||||
return values
|
||||
|
||||
def get_nvenc_profile_form_settings(self):
|
||||
values = {
|
||||
"label": "Profile",
|
||||
"description": "The profile determines which features of the codec are available and enabled,\n"
|
||||
"while also affecting other restrictions.\n"
|
||||
"Any of these profiles are capable of 4:2:0, 4:2:2 and 4:4:4, however the support\n"
|
||||
"depends on the installed hardware.",
|
||||
"sub_setting": True,
|
||||
"input_type": "select",
|
||||
"select_options": [
|
||||
{
|
||||
"value": "auto",
|
||||
"label": "Auto – Let ffmpeg automatically select the required profile (recommended)",
|
||||
},
|
||||
{
|
||||
"value": "baseline",
|
||||
"label": "Baseline",
|
||||
},
|
||||
{
|
||||
"value": "main",
|
||||
"label": "Main",
|
||||
},
|
||||
{
|
||||
"value": "main10",
|
||||
"label": "Main10",
|
||||
},
|
||||
{
|
||||
"value": "high444p",
|
||||
"label": "High444p",
|
||||
},
|
||||
],
|
||||
}
|
||||
self.__set_default_option(values['select_options'], 'nvenc_profile', default_option='main')
|
||||
if self.settings.get_setting('mode') not in ['standard']:
|
||||
values["display"] = "hidden"
|
||||
return values
|
||||
|
||||
def get_nvenc_encoder_ratecontrol_method_form_settings(self):
|
||||
values = {
|
||||
"label": "Encoder ratecontrol method",
|
||||
"description": "Note that the rate control is already defined in the Encoder Quality Preset option.\n"
|
||||
"Selecting anything other than 'Disabled' will override the preset rate-control.",
|
||||
"sub_setting": True,
|
||||
"input_type": "select",
|
||||
"select_options": [
|
||||
{
|
||||
"value": "auto",
|
||||
"label": "Auto – Use the rate control setting pre-defined in the preset option (recommended)",
|
||||
},
|
||||
{
|
||||
"value": "constqp",
|
||||
"label": "CQP - Quality based mode using constant quantizer scale",
|
||||
},
|
||||
{
|
||||
"value": "vbr",
|
||||
"label": "VBR - Bitrate based mode using variable bitrate",
|
||||
},
|
||||
{
|
||||
"value": "cbr",
|
||||
"label": "CBR - Bitrate based mode using constant bitrate",
|
||||
},
|
||||
]
|
||||
}
|
||||
self.__set_default_option(values['select_options'], 'nvenc_encoder_ratecontrol_method', default_option='auto')
|
||||
if self.settings.get_setting('mode') not in ['standard']:
|
||||
values["display"] = "hidden"
|
||||
return values
|
||||
|
||||
def get_nvenc_encoder_ratecontrol_lookahead_form_settings(self):
|
||||
# Lower is better
|
||||
values = {
|
||||
"label": "Configure the number of frames to look ahead for rate-control",
|
||||
"sub_setting": True,
|
||||
"input_type": "slider",
|
||||
"slider_options": {
|
||||
"min": 0,
|
||||
"max": 30,
|
||||
},
|
||||
}
|
||||
if self.settings.get_setting('mode') not in ['standard']:
|
||||
values["display"] = "hidden"
|
||||
return values
|
||||
|
||||
def get_nvenc_enable_spatial_aq_form_settings(self):
|
||||
values = {
|
||||
"label": "Enable Spatial Adaptive Quantization",
|
||||
"description": "This adjusts the quantization parameter within each frame based on spatial complexity.\n"
|
||||
"This helps in improving the quality of areas within a frame that are more detailed or complex.",
|
||||
"sub_setting": True,
|
||||
}
|
||||
if self.settings.get_setting('mode') not in ['standard']:
|
||||
values["display"] = 'hidden'
|
||||
return values
|
||||
|
||||
def get_nvenc_enable_temporal_aq_form_settings(self):
|
||||
values = {
|
||||
"label": "Enable Temporal Adaptive Quantization",
|
||||
"description": "This adjusts the quantization parameter across frames, based on the motion and temporal complexity.\n"
|
||||
"This is particularly effective in scenes with varying levels of motion, enhancing quality where it's most needed.\n"
|
||||
"This option requires Turing or newer hardware.",
|
||||
"sub_setting": True,
|
||||
}
|
||||
if self.settings.get_setting('mode') not in ['standard']:
|
||||
values["display"] = 'hidden'
|
||||
return values
|
||||
|
||||
def get_nvenc_aq_strength_form_settings(self):
|
||||
# Lower is better
|
||||
values = {
|
||||
"label": "Strength of the adaptive quantization",
|
||||
"description": "Controls the strength of the adaptive quantization (both spatial and temporal).\n"
|
||||
"A higher value indicates stronger adaptation, which can lead to better preservation\n"
|
||||
"of detail but might also increase the bitrate.",
|
||||
"sub_setting": True,
|
||||
"input_type": "slider",
|
||||
"slider_options": {
|
||||
"min": 0,
|
||||
"max": 15,
|
||||
},
|
||||
}
|
||||
if self.settings.get_setting('mode') not in ['standard']:
|
||||
values["display"] = "hidden"
|
||||
if not self.settings.get_setting('nvenc_enable_spatial_aq'):
|
||||
values["display"] = "hidden"
|
||||
return values
|
||||
Submodule time_tracker deleted from 89e5e8a303
@@ -1,288 +0,0 @@
|
||||
require 'optparse'
|
||||
require 'fileutils'
|
||||
require 'open3'
|
||||
require 'json'
|
||||
|
||||
# Configuration
|
||||
# Default target is loosely based on user preference, but we use auto-density usually.
|
||||
options = {
|
||||
threshold: 5.0, # Default 5 GB/hr
|
||||
dry_run: false,
|
||||
quality: 29 # Default CQ
|
||||
}
|
||||
|
||||
OptionParser.new do |opts|
|
||||
opts.banner = "Usage: ruby video_optimizer.rb [directory] [options]"
|
||||
|
||||
opts.on("-t", "--threshold N", Float, "Minimum GB per hour to trigger processing (default: 5.0)") do |v|
|
||||
options[:threshold] = v
|
||||
end
|
||||
|
||||
opts.on("-n", "--dry-run", "Identify files but do not transcode") do
|
||||
options[:dry_run] = true
|
||||
end
|
||||
|
||||
opts.on("-q", "--quality N", Integer, "Set Constant Quality (CQ/CRF) value (Overrides auto-density)") do |v|
|
||||
options[:cq] = v
|
||||
options[:mode] = :cq
|
||||
end
|
||||
|
||||
opts.on("-b", "--bitrate RATE", "Set fixed bitrate (e.g. '2M', '2000k')") do |v|
|
||||
options[:bitrate] = v
|
||||
options[:mode] = :bitrate
|
||||
end
|
||||
|
||||
opts.on("-s", "--target-size GB", Float, "Target file size in GB") do |v|
|
||||
options[:target_size] = v
|
||||
options[:mode] = :size
|
||||
end
|
||||
|
||||
opts.on("-h", "--help", "Prints this help") do
|
||||
puts opts
|
||||
exit
|
||||
end
|
||||
end.parse!
|
||||
|
||||
# Default mode
|
||||
options[:mode] ||= :auto_density
|
||||
|
||||
directory = ARGV[0]
|
||||
if directory.nil? || !Dir.exist?(directory)
|
||||
puts "Error: Please provide a valid directory."
|
||||
exit 1
|
||||
end
|
||||
|
||||
# --- ENCODER LOGIC ---
|
||||
|
||||
def detect_encoders
|
||||
stdout, _, _ = Open3.capture3('ffmpeg', '-v', 'quiet', '-encoders')
|
||||
encoders = []
|
||||
encoders << :nvenc if stdout.include?("hevc_nvenc")
|
||||
encoders << :qsv if stdout.include?("hevc_qsv")
|
||||
encoders << :cpu if stdout.include?("libx265")
|
||||
encoders
|
||||
end
|
||||
|
||||
def select_best_encoder(available_encoders)
|
||||
if available_encoders.include?(:nvenc)
|
||||
puts " [Encoder] Selected: NVIDIA (hevc_nvenc)"
|
||||
return :nvenc
|
||||
elsif available_encoders.include?(:qsv)
|
||||
puts " [Encoder] Selected: Intel QSV (hevc_qsv)"
|
||||
return :qsv
|
||||
elsif available_encoders.include?(:cpu)
|
||||
puts " [Encoder] Selected: CPU (libx265)"
|
||||
return :cpu
|
||||
else
|
||||
puts " [Error] No suitable HEVC encoder found!"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def build_encoder_args(encoder, mode, target_val)
|
||||
args = []
|
||||
|
||||
case encoder
|
||||
when :nvenc
|
||||
args << '-c:v' << 'hevc_nvenc' << '-preset' << 'p4'
|
||||
case mode
|
||||
when :cq
|
||||
args << '-rc' << 'constqp' << '-qp' << target_val.to_s
|
||||
when :bitrate, :size # Both use target bitrate
|
||||
args << '-rc' << 'vbr' << '-b:v' << target_val << '-maxrate' << target_val << '-bufsize' << (target_val.to_i * 2).to_s
|
||||
end
|
||||
|
||||
when :qsv
|
||||
args << '-c:v' << 'hevc_qsv' << '-preset' << 'veryfast' << '-load_plugin' << 'hevc_hw'
|
||||
case mode
|
||||
when :cq
|
||||
# QSV uses -global_quality or -q:v for ICQ. roughly maps to CRF.
|
||||
args << '-global_quality' << target_val.to_s
|
||||
when :bitrate, :size
|
||||
# QSV VBR
|
||||
args << '-b:v' << target_val << '-maxrate' << target_val
|
||||
end
|
||||
|
||||
when :cpu
|
||||
args << '-c:v' << 'libx265' << '-preset' << 'medium'
|
||||
case mode
|
||||
when :cq
|
||||
args << '-crf' << target_val.to_s
|
||||
when :bitrate, :size
|
||||
args << '-b:v' << target_val << '-maxrate' << target_val << '-bufsize' << (target_val.to_i * 2).to_s
|
||||
end
|
||||
end
|
||||
|
||||
args
|
||||
end
|
||||
|
||||
# --- METADATA & UTILS ---
|
||||
|
||||
def get_video_metadata(file_path)
|
||||
cmd = ['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', '-select_streams', 'v:0', file_path]
|
||||
stdout, stderr, status = Open3.capture3(*cmd)
|
||||
return nil unless status.success?
|
||||
|
||||
begin
|
||||
data = JSON.parse(stdout)
|
||||
format_info = data['format']
|
||||
stream_info = data['streams'][0]
|
||||
return nil unless format_info && stream_info
|
||||
|
||||
size_bytes = format_info['size'].to_i
|
||||
duration_sec = format_info['duration'].to_f
|
||||
codec = stream_info['codec_name']
|
||||
|
||||
fps_str = stream_info['avg_frame_rate']
|
||||
if fps_str && fps_str.include?('/')
|
||||
num, den = fps_str.split('/').map(&:to_f)
|
||||
fps = (den > 0) ? (num / den) : 30.0
|
||||
else
|
||||
fps = 30.0
|
||||
end
|
||||
|
||||
return nil if size_bytes == 0 || duration_sec == 0
|
||||
|
||||
{ size_bytes: size_bytes, duration_sec: duration_sec, codec: codec, fps: fps }
|
||||
rescue => e
|
||||
puts "Failed to parse metadata: #{e.message}"
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_target_bitrate_from_size(duration_sec, target_gb)
|
||||
target_bytes = (target_gb * 1024 * 1024 * 1024).to_i
|
||||
audio_bitrate_bps = 192 * 1000
|
||||
estimated_audio_size = duration_sec * (audio_bitrate_bps / 8.0)
|
||||
target_video_size = target_bytes - estimated_audio_size
|
||||
return 500_000 if target_video_size <= 0
|
||||
(target_video_size * 8) / duration_sec
|
||||
end
|
||||
|
||||
# --- PROCESS ---
|
||||
|
||||
AVAILABLE_ENCODERS = detect_encoders
|
||||
BEST_ENCODER = select_best_encoder(AVAILABLE_ENCODERS)
|
||||
|
||||
def process_file(file_path, options)
|
||||
metadata = get_video_metadata(file_path)
|
||||
return unless metadata
|
||||
|
||||
accepted_codecs = ['h264', 'avc1']
|
||||
return unless accepted_codecs.include?(metadata[:codec].to_s.downcase)
|
||||
|
||||
size_gb = metadata[:size_bytes].to_f / (1024**3)
|
||||
hours = metadata[:duration_sec] / 3600.0
|
||||
gb_per_hour = size_gb / hours
|
||||
|
||||
if gb_per_hour >= options[:threshold]
|
||||
puts "\n[MATCH] #{file_path}"
|
||||
puts " Size: #{size_gb.round(2)} GB, Density: #{gb_per_hour.round(2)} GB/hr"
|
||||
|
||||
if options[:dry_run]
|
||||
puts " [Dry Run] Would transcode this file."
|
||||
return
|
||||
end
|
||||
|
||||
# Determine Encode Mode & Target Value
|
||||
mode = :cq
|
||||
target_val = 29 # Default CQ
|
||||
|
||||
if options[:mode] == :auto_density
|
||||
target_gb_hr = options[:threshold] / 2.0
|
||||
target_gb = hours * target_gb_hr
|
||||
bitrate = calculate_target_bitrate_from_size(metadata[:duration_sec], target_gb)
|
||||
|
||||
mode = :bitrate # Use bitrate targeting for density match
|
||||
target_val = bitrate.to_i.to_s # e.g. "2000000"
|
||||
puts " Auto-Density Target: #{target_gb.round(2)} GB (#{target_gb_hr} GB/hr) -> #{target_val.to_i/1000} kbps"
|
||||
|
||||
elsif options[:mode] == :size
|
||||
bitrate = calculate_target_bitrate_from_size(metadata[:duration_sec], options[:target_size])
|
||||
mode = :bitrate
|
||||
target_val = bitrate.to_i.to_s
|
||||
puts " Target Size: #{options[:target_size]} GB -> #{target_val.to_i/1000} kbps"
|
||||
|
||||
elsif options[:mode] == :bitrate
|
||||
mode = :bitrate
|
||||
target_val = options[:bitrate] # User string e.g. "2M"
|
||||
puts " Target Bitrate: #{target_val}"
|
||||
|
||||
else # :cq
|
||||
mode = :cq
|
||||
target_val = options[:cq] || 29
|
||||
puts " Target Quality (CQ/CRF): #{target_val}"
|
||||
end
|
||||
|
||||
# Build flags
|
||||
encoder_args = build_encoder_args(BEST_ENCODER, mode, target_val)
|
||||
|
||||
# Common Plex/Optimization Args
|
||||
gop_size = (metadata[:fps] * 2).round
|
||||
common_args = [
|
||||
'-pix_fmt', 'yuv420p', # 8-bit HEVC (Main Profile)
|
||||
'-g', "#{gop_size}", # Fixed GOP
|
||||
'-keyint_min', "#{gop_size}",
|
||||
'-movflags', '+faststart', # Streaming optimization
|
||||
'-c:a', 'copy'
|
||||
]
|
||||
|
||||
original_ext = File.extname(file_path)
|
||||
temp_file = file_path.sub(original_ext, ".tmp#{original_ext}")
|
||||
|
||||
cmd = ['ffmpeg', '-y', '-hwaccel', 'auto', '-i', file_path] + encoder_args + common_args + [temp_file]
|
||||
|
||||
puts " Starting transcode with #{BEST_ENCODER}..."
|
||||
start_time = Time.now
|
||||
|
||||
stdout, stderr, status = Open3.capture3(*cmd)
|
||||
|
||||
if status.success?
|
||||
duration = Time.now - start_time
|
||||
puts " Done in #{duration.round(1)}s."
|
||||
|
||||
# Validation
|
||||
if File.exist?(temp_file) && File.size(temp_file) > 1000
|
||||
new_size_gb = File.size(temp_file).to_f / (1024**3)
|
||||
reduction = ((size_gb - new_size_gb) / size_gb * 100.0).round(1)
|
||||
|
||||
puts " New Size: #{new_size_gb.round(2)} GB (#{reduction}% reduction)."
|
||||
|
||||
if reduction < 33.0
|
||||
puts " [SKIP] Reduction too low (<33%). Keeping original."
|
||||
File.delete(temp_file)
|
||||
else
|
||||
# Verification
|
||||
new_meta = get_video_metadata(temp_file)
|
||||
if new_meta && (new_meta[:duration_sec] - metadata[:duration_sec]).abs < 30
|
||||
File.delete(file_path)
|
||||
File.rename(temp_file, file_path)
|
||||
puts " [SUCCESS] File replaced."
|
||||
else
|
||||
puts " [ERROR] Duration mismatch. Keeping original."
|
||||
File.delete(temp_file)
|
||||
end
|
||||
end
|
||||
else
|
||||
puts " [ERROR] Output invalid."
|
||||
end
|
||||
else
|
||||
puts " [ERROR] Transcode failed."
|
||||
puts stderr # Debug
|
||||
File.delete(temp_file) if File.exist?(temp_file)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
puts "Scanning #{directory}..."
|
||||
files = []
|
||||
Dir.glob("#{directory}/**/*").each do |f|
|
||||
next if File.directory?(f)
|
||||
next unless ['.mp4', '.mkv', '.avi', '.mov', '.m4v'].include?(File.extname(f).downcase)
|
||||
files << [f, File.size(f)] rescue nil
|
||||
end
|
||||
|
||||
puts "Sorting #{files.count} files by size..."
|
||||
files.sort_by! { |_, size| -size }
|
||||
|
||||
files.each { |f, _| process_file(f, options) }
|
||||
Reference in New Issue
Block a user