mirror of
https://github.com/pacnpal/Pac-cogs.git
synced 2025-12-20 10:51:05 -05:00
Added robust error handling and logging Improved binary verification and initialization Added proper GPU detection and hardware acceleration Optimized encoding parameters for different content types Improved File Operations: Added retry mechanisms for file operations Enhanced temporary directory management Improved cleanup of failed downloads Added proper permission handling Enhanced Queue Management: Fixed queue manager initialization Added better error recovery Improved status tracking and logging Enhanced cleanup of failed items Better Error Handling: Added comprehensive exception hierarchy Improved error logging and reporting Added fallback mechanisms for failures Enhanced error recovery strategies
280 lines
11 KiB
Python
280 lines
11 KiB
Python
"""FFmpeg encoding parameters generator"""
|
|
|
|
import os
|
|
import logging
|
|
from typing import Dict, Any
|
|
from .exceptions import CompressionError, QualityError, BitrateError
|
|
|
|
logger = logging.getLogger("VideoArchiver")
|
|
|
|
class EncoderParams:
|
|
"""Manages FFmpeg encoding parameters based on hardware and content"""
|
|
|
|
# Quality presets based on content type
|
|
QUALITY_PRESETS = {
|
|
"gaming": {
|
|
"crf": "20",
|
|
"preset": "fast",
|
|
"tune": "zerolatency",
|
|
"x264opts": "rc-lookahead=20:me=hex:subme=6:ref=3:b-adapt=1:direct=spatial"
|
|
},
|
|
"animation": {
|
|
"crf": "18",
|
|
"preset": "slow",
|
|
"tune": "animation",
|
|
"x264opts": "rc-lookahead=60:me=umh:subme=9:ref=6:b-adapt=2:direct=auto:deblock=-1,-1"
|
|
},
|
|
"film": {
|
|
"crf": "22",
|
|
"preset": "medium",
|
|
"tune": "film",
|
|
"x264opts": "rc-lookahead=50:me=umh:subme=8:ref=4:b-adapt=2:direct=auto"
|
|
}
|
|
}
|
|
|
|
def __init__(self, cpu_cores: int, gpu_info: Dict[str, bool]):
|
|
"""Initialize encoder parameters manager
|
|
|
|
Args:
|
|
cpu_cores: Number of available CPU cores
|
|
gpu_info: Dict containing GPU availability information
|
|
"""
|
|
self.cpu_cores = cpu_cores
|
|
self.gpu_info = gpu_info
|
|
logger.info(f"Initialized encoder with {cpu_cores} CPU cores and GPU info: {gpu_info}")
|
|
|
|
def get_params(self, video_info: Dict[str, Any], target_size_bytes: int) -> Dict[str, str]:
|
|
"""Get optimal FFmpeg parameters based on hardware and video analysis
|
|
|
|
Args:
|
|
video_info: Dict containing video analysis results
|
|
target_size_bytes: Target file size in bytes
|
|
|
|
Returns:
|
|
Dict containing FFmpeg encoding parameters
|
|
"""
|
|
try:
|
|
# Get base parameters
|
|
params = self._get_base_params()
|
|
logger.debug(f"Base parameters: {params}")
|
|
|
|
# Update with content-specific parameters
|
|
content_params = self._get_content_specific_params(video_info)
|
|
params.update(content_params)
|
|
logger.debug(f"Content-specific parameters: {content_params}")
|
|
|
|
# Update with GPU-specific parameters if available
|
|
gpu_params = self._get_gpu_specific_params()
|
|
if gpu_params:
|
|
params.update(gpu_params)
|
|
logger.debug(f"GPU-specific parameters: {gpu_params}")
|
|
|
|
# Calculate and update bitrate parameters
|
|
bitrate_params = self._get_bitrate_params(video_info, target_size_bytes)
|
|
params.update(bitrate_params)
|
|
logger.debug(f"Bitrate parameters: {bitrate_params}")
|
|
|
|
# Validate final parameters
|
|
self._validate_params(params, video_info)
|
|
|
|
logger.info(f"Final encoding parameters: {params}")
|
|
return params
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error generating encoding parameters: {str(e)}")
|
|
# Return safe default parameters
|
|
return self._get_safe_defaults()
|
|
|
|
def _get_base_params(self) -> Dict[str, str]:
|
|
"""Get base encoding parameters"""
|
|
return {
|
|
"c:v": "libx264", # Default to CPU encoding
|
|
"threads": str(self.cpu_cores),
|
|
"preset": "medium",
|
|
"crf": "23",
|
|
"movflags": "+faststart",
|
|
"profile:v": "high",
|
|
"level": "4.1",
|
|
"pix_fmt": "yuv420p",
|
|
"x264opts": "rc-lookahead=60:me=umh:subme=7:ref=4:b-adapt=2:direct=auto",
|
|
"tune": "film",
|
|
"fastfirstpass": "1"
|
|
}
|
|
|
|
def _get_content_specific_params(self, video_info: Dict[str, Any]) -> Dict[str, str]:
|
|
"""Get parameters optimized for specific content types"""
|
|
params = {}
|
|
|
|
# Detect content type
|
|
content_type = self._detect_content_type(video_info)
|
|
if content_type in self.QUALITY_PRESETS:
|
|
params.update(self.QUALITY_PRESETS[content_type])
|
|
|
|
# Additional optimizations based on content analysis
|
|
if video_info.get("has_high_motion", False):
|
|
params.update({
|
|
"tune": "grain",
|
|
"x264opts": "rc-lookahead=60:me=umh:subme=7:ref=4:b-adapt=2:direct=auto:deblock=-1,-1:psy-rd=1.0:aq-strength=0.8"
|
|
})
|
|
|
|
if video_info.get("has_dark_scenes", False):
|
|
x264opts = params.get("x264opts", "rc-lookahead=60:me=umh:subme=7:ref=4:b-adapt=2:direct=auto")
|
|
params.update({
|
|
"x264opts": x264opts + ":aq-mode=3:aq-strength=1.0:deblock=1:1",
|
|
"tune": "film" if not video_info.get("has_high_motion") else "grain"
|
|
})
|
|
|
|
return params
|
|
|
|
def _get_gpu_specific_params(self) -> Dict[str, str]:
|
|
"""Get GPU-specific encoding parameters"""
|
|
if self.gpu_info.get("nvidia", False):
|
|
return {
|
|
"c:v": "h264_nvenc",
|
|
"preset": "p7",
|
|
"rc:v": "vbr",
|
|
"cq:v": "19",
|
|
"b_ref_mode": "middle",
|
|
"spatial-aq": "1",
|
|
"temporal-aq": "1",
|
|
"rc-lookahead": "32",
|
|
"surfaces": "64",
|
|
"max_muxing_queue_size": "1024",
|
|
"gpu": "any"
|
|
}
|
|
elif self.gpu_info.get("amd", False):
|
|
return {
|
|
"c:v": "h264_amf",
|
|
"quality": "quality",
|
|
"rc": "vbr_peak",
|
|
"enforce_hrd": "1",
|
|
"vbaq": "1",
|
|
"preanalysis": "1",
|
|
"max_muxing_queue_size": "1024"
|
|
}
|
|
elif self.gpu_info.get("intel", False):
|
|
return {
|
|
"c:v": "h264_qsv",
|
|
"preset": "veryslow",
|
|
"look_ahead": "1",
|
|
"global_quality": "23",
|
|
"max_muxing_queue_size": "1024"
|
|
}
|
|
return {}
|
|
|
|
def _get_bitrate_params(self, video_info: Dict[str, Any], target_size_bytes: int) -> Dict[str, str]:
|
|
"""Calculate and get bitrate-related parameters"""
|
|
params = {}
|
|
try:
|
|
duration = float(video_info.get("duration", 0))
|
|
input_size = int(video_info.get("bitrate", 0) * duration / 8) # Convert to bytes
|
|
|
|
if duration > 0 and input_size > target_size_bytes:
|
|
# Calculate target bitrates
|
|
video_size_target = int(target_size_bytes * 0.95) # Reserve 5% for container overhead
|
|
total_bitrate = int((video_size_target * 8) / duration)
|
|
|
|
# Calculate audio bitrate
|
|
audio_channels = int(video_info.get("audio_channels", 2))
|
|
min_audio_bitrate = 64000 * audio_channels
|
|
max_audio_bitrate = 192000 * audio_channels
|
|
audio_bitrate = min(
|
|
max_audio_bitrate,
|
|
max(min_audio_bitrate, int(total_bitrate * 0.15))
|
|
)
|
|
|
|
# Calculate video bitrate
|
|
video_bitrate = int(total_bitrate - audio_bitrate)
|
|
if video_bitrate <= 0:
|
|
raise BitrateError("Calculated video bitrate is too low", total_bitrate, 0)
|
|
|
|
# Set bitrate constraints
|
|
params["maxrate"] = str(int(video_bitrate * 1.5))
|
|
params["bufsize"] = str(int(video_bitrate * 2))
|
|
|
|
# Quality adjustments based on compression ratio
|
|
ratio = input_size / target_size_bytes
|
|
if ratio > 4:
|
|
params["crf"] = "26"
|
|
params["preset"] = "faster"
|
|
elif ratio > 2:
|
|
params["crf"] = "23"
|
|
params["preset"] = "medium"
|
|
else:
|
|
params["crf"] = "20"
|
|
params["preset"] = "slow"
|
|
|
|
# Audio settings
|
|
params.update({
|
|
"c:a": "aac",
|
|
"b:a": f"{int(audio_bitrate/1000)}k",
|
|
"ar": str(video_info.get("audio_sample_rate", 48000)),
|
|
"ac": str(audio_channels)
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error calculating bitrates: {str(e)}")
|
|
# Use safe default parameters
|
|
params.update(self._get_safe_defaults())
|
|
|
|
return params
|
|
|
|
def _detect_content_type(self, video_info: Dict[str, Any]) -> str:
|
|
"""Detect content type based on video analysis"""
|
|
try:
|
|
# Check for gaming content
|
|
if video_info.get("has_high_motion", False) and video_info.get("fps", 0) >= 60:
|
|
return "gaming"
|
|
|
|
# Check for animation
|
|
if video_info.get("has_sharp_edges", False) and not video_info.get("has_film_grain", False):
|
|
return "animation"
|
|
|
|
# Default to film
|
|
return "film"
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error detecting content type: {str(e)}")
|
|
return "film"
|
|
|
|
def _validate_params(self, params: Dict[str, str], video_info: Dict[str, Any]) -> None:
|
|
"""Validate encoding parameters"""
|
|
try:
|
|
# Check for required parameters
|
|
required_params = ["c:v", "preset", "pix_fmt"]
|
|
missing_params = [p for p in required_params if p not in params]
|
|
if missing_params:
|
|
raise ValueError(f"Missing required parameters: {missing_params}")
|
|
|
|
# Validate video codec
|
|
if params["c:v"] not in ["libx264", "h264_nvenc", "h264_amf", "h264_qsv"]:
|
|
raise ValueError(f"Invalid video codec: {params['c:v']}")
|
|
|
|
# Validate preset
|
|
valid_presets = ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"]
|
|
if params["preset"] not in valid_presets:
|
|
raise ValueError(f"Invalid preset: {params['preset']}")
|
|
|
|
# Validate pixel format
|
|
if params["pix_fmt"] not in ["yuv420p", "nv12", "yuv444p"]:
|
|
raise ValueError(f"Invalid pixel format: {params['pix_fmt']}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Parameter validation failed: {str(e)}")
|
|
raise
|
|
|
|
def _get_safe_defaults(self) -> Dict[str, str]:
|
|
"""Get safe default encoding parameters"""
|
|
return {
|
|
"c:v": "libx264",
|
|
"preset": "medium",
|
|
"crf": "23",
|
|
"pix_fmt": "yuv420p",
|
|
"profile:v": "high",
|
|
"level": "4.1",
|
|
"c:a": "aac",
|
|
"b:a": "128k",
|
|
"ar": "48000",
|
|
"ac": "2"
|
|
}
|