Enhanced FFmpeg Integration:

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
This commit is contained in:
pacnpal
2024-11-15 03:21:25 +00:00
parent a04c576e0a
commit 8503fc6fdd
13 changed files with 1336 additions and 376 deletions

View File

@@ -1,6 +1,186 @@
"""FFmpeg management package"""
"""FFmpeg module initialization"""
from videoarchiver.ffmpeg.exceptions import FFmpegError, GPUError, DownloadError
from videoarchiver.ffmpeg.ffmpeg_manager import FFmpegManager
import logging
import sys
import os
from pathlib import Path
from typing import Dict, Any, Optional
__all__ = ['FFmpegManager', 'FFmpegError', 'GPUError', 'DownloadError']
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger("VideoArchiver")
# Import components after logging is configured
from .ffmpeg_manager import FFmpegManager
from .video_analyzer import VideoAnalyzer
from .gpu_detector import GPUDetector
from .encoder_params import EncoderParams
from .ffmpeg_downloader import FFmpegDownloader
from .exceptions import (
FFmpegError,
DownloadError,
VerificationError,
EncodingError,
AnalysisError,
GPUError,
HardwareAccelerationError,
FFmpegNotFoundError,
FFprobeError,
CompressionError,
FormatError,
PermissionError,
TimeoutError,
ResourceError,
QualityError,
AudioError,
BitrateError,
)
class FFmpeg:
"""Main FFmpeg interface"""
_instance = None
def __new__(cls, base_dir: Optional[Path] = None):
"""Singleton pattern to ensure only one FFmpeg instance"""
if cls._instance is None:
cls._instance = super(FFmpeg, cls).__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, base_dir: Optional[Path] = None):
"""Initialize FFmpeg interface
Args:
base_dir: Optional base directory for FFmpeg files. If not provided,
will use the default directory in the module.
"""
# Skip initialization if already done
if self._initialized:
return
try:
self._manager = FFmpegManager()
logger.info(f"FFmpeg initialized at {self._manager.get_ffmpeg_path()}")
logger.info(f"GPU support: {self._manager.gpu_info}")
self._initialized = True
except Exception as e:
logger.error(f"Failed to initialize FFmpeg interface: {e}")
raise FFmpegError(f"FFmpeg initialization failed: {e}")
@property
def ffmpeg_path(self) -> Path:
"""Get path to FFmpeg binary"""
return Path(self._manager.get_ffmpeg_path())
@property
def gpu_info(self) -> Dict[str, bool]:
"""Get GPU information"""
return self._manager.gpu_info
def analyze_video(self, input_path: str) -> Dict[str, Any]:
"""Analyze video content
Args:
input_path: Path to input video file
Returns:
Dict containing video analysis results
"""
try:
if not os.path.exists(input_path):
raise FileNotFoundError(f"Input file not found: {input_path}")
return self._manager.analyze_video(input_path)
except Exception as e:
logger.error(f"Video analysis failed: {e}")
raise AnalysisError(f"Failed to analyze video: {e}")
def get_compression_params(self, input_path: str, target_size_mb: int) -> Dict[str, str]:
"""Get optimal compression parameters
Args:
input_path: Path to input video file
target_size_mb: Target file size in megabytes
Returns:
Dict containing FFmpeg encoding parameters
"""
try:
if not os.path.exists(input_path):
raise FileNotFoundError(f"Input file not found: {input_path}")
return self._manager.get_compression_params(input_path, target_size_mb)
except Exception as e:
logger.error(f"Failed to get compression parameters: {e}")
raise EncodingError(f"Failed to get compression parameters: {e}")
def force_download(self) -> bool:
"""Force re-download of FFmpeg binary
Returns:
bool: True if download successful, False otherwise
"""
try:
return self._manager.force_download()
except Exception as e:
logger.error(f"Force download failed: {e}")
raise DownloadError(f"Failed to force download FFmpeg: {e}")
@property
def version(self) -> str:
"""Get FFmpeg version"""
try:
import subprocess
result = subprocess.run(
[str(self.ffmpeg_path), "-version"],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
return result.stdout.split()[2]
raise FFmpegError(f"FFmpeg version check failed: {result.stderr}")
except Exception as e:
logger.error(f"Failed to get FFmpeg version: {e}")
raise FFmpegError(f"Failed to get FFmpeg version: {e}")
# Initialize default instance
try:
ffmpeg = FFmpeg()
logger.info(f"Default FFmpeg instance initialized (version {ffmpeg.version})")
except Exception as e:
logger.error(f"Failed to initialize default FFmpeg instance: {e}")
ffmpeg = None
__all__ = [
'FFmpeg',
'ffmpeg',
'FFmpegManager',
'VideoAnalyzer',
'GPUDetector',
'EncoderParams',
'FFmpegDownloader',
'FFmpegError',
'DownloadError',
'VerificationError',
'EncodingError',
'AnalysisError',
'GPUError',
'HardwareAccelerationError',
'FFmpegNotFoundError',
'FFprobeError',
'CompressionError',
'FormatError',
'PermissionError',
'TimeoutError',
'ResourceError',
'QualityError',
'AudioError',
'BitrateError',
]

View File

@@ -3,21 +3,87 @@
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"""
params = self._get_base_params()
params.update(self._get_content_specific_params(video_info))
params.update(self._get_gpu_specific_params())
params.update(self._get_bitrate_params(video_info, target_size_bytes))
return params
"""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"""
@@ -39,13 +105,19 @@ class EncoderParams:
"""Get parameters optimized for specific content types"""
params = {}
if video_info.get("has_high_motion"):
# 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"):
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",
@@ -56,7 +128,7 @@ class EncoderParams:
def _get_gpu_specific_params(self) -> Dict[str, str]:
"""Get GPU-specific encoding parameters"""
if self.gpu_info["nvidia"]:
if self.gpu_info.get("nvidia", False):
return {
"c:v": "h264_nvenc",
"preset": "p7",
@@ -70,7 +142,7 @@ class EncoderParams:
"max_muxing_queue_size": "1024",
"gpu": "any"
}
elif self.gpu_info["amd"]:
elif self.gpu_info.get("amd", False):
return {
"c:v": "h264_amf",
"quality": "quality",
@@ -80,7 +152,7 @@ class EncoderParams:
"preanalysis": "1",
"max_muxing_queue_size": "1024"
}
elif self.gpu_info["intel"]:
elif self.gpu_info.get("intel", False):
return {
"c:v": "h264_qsv",
"preset": "veryslow",
@@ -94,15 +166,16 @@ class EncoderParams:
"""Calculate and get bitrate-related parameters"""
params = {}
try:
duration = video_info.get("duration", 0)
input_size = video_info.get("bitrate", 0) * duration / 8 # Estimate from bitrate
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:
video_size_target = int(target_size_bytes * 0.95)
total_bitrate = (video_size_target * 8) / duration
# 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)
# Audio bitrate calculation
audio_channels = video_info.get("audio_channels", 2)
# 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(
@@ -110,8 +183,10 @@ class EncoderParams:
max(min_audio_bitrate, int(total_bitrate * 0.15))
)
# Video bitrate calculation
video_bitrate = int((video_size_target * 8) / duration - audio_bitrate)
# 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))
@@ -120,42 +195,85 @@ class EncoderParams:
# Quality adjustments based on compression ratio
ratio = input_size / target_size_bytes
if ratio > 4:
params["crf"] = "26" if params.get("c:v", "libx264") == "libx264" else "23"
params["crf"] = "26"
params["preset"] = "faster"
elif ratio > 2:
params["crf"] = "23" if params.get("c:v", "libx264") == "libx264" else "21"
params["crf"] = "23"
params["preset"] = "medium"
else:
params["crf"] = "20" if params.get("c:v", "libx264") == "libx264" else "19"
params["crf"] = "20"
params["preset"] = "slow"
# Dark scene adjustments
if video_info.get("has_dark_scenes"):
if params.get("c:v", "libx264") == "libx264":
params["crf"] = str(max(18, int(params["crf"]) - 2))
elif params.get("c:v") == "h264_nvenc":
params["cq:v"] = str(max(15, int(params.get("cq:v", "19")) - 2))
# 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(video_info.get("audio_channels", 2))
"ac": str(audio_channels)
})
except Exception as e:
logger.error(f"Error calculating bitrates: {str(e)}")
# Use safe default parameters
params.update({
"crf": "23",
"preset": "medium",
"maxrate": f"{2 * 1024 * 1024}", # 2 Mbps
"bufsize": f"{4 * 1024 * 1024}", # 4 Mbps buffer
"c:a": "aac",
"b:a": "128k",
"ar": "48000",
"ac": "2"
})
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"
}

View File

@@ -4,10 +4,106 @@ class FFmpegError(Exception):
"""Base exception for FFmpeg-related errors"""
pass
class GPUError(FFmpegError):
"""Raised when GPU operations fail"""
class DownloadError(FFmpegError):
"""Exception raised when FFmpeg download fails"""
pass
class DownloadError(FFmpegError):
"""Raised when FFmpeg download fails"""
class VerificationError(FFmpegError):
"""Exception raised when FFmpeg verification fails"""
pass
class EncodingError(FFmpegError):
"""Exception raised when video encoding fails"""
pass
class AnalysisError(FFmpegError):
"""Exception raised when video analysis fails"""
pass
class GPUError(FFmpegError):
"""Exception raised when GPU operations fail"""
pass
class HardwareAccelerationError(FFmpegError):
"""Exception raised when hardware acceleration fails"""
def __init__(self, message: str, fallback_used: bool = False):
self.fallback_used = fallback_used
super().__init__(message)
class FFmpegNotFoundError(FFmpegError):
"""Exception raised when FFmpeg binary is not found"""
pass
class FFprobeError(FFmpegError):
"""Exception raised when FFprobe operations fail"""
pass
class CompressionError(FFmpegError):
"""Exception raised when video compression fails"""
def __init__(self, message: str, input_size: int, target_size: int):
self.input_size = input_size
self.target_size = target_size
super().__init__(f"{message} (Input: {input_size}B, Target: {target_size}B)")
class FormatError(FFmpegError):
"""Exception raised when video format is invalid or unsupported"""
pass
class PermissionError(FFmpegError):
"""Exception raised when file permissions prevent operations"""
pass
class TimeoutError(FFmpegError):
"""Exception raised when FFmpeg operations timeout"""
pass
class ResourceError(FFmpegError):
"""Exception raised when system resources are insufficient"""
def __init__(self, message: str, resource_type: str):
self.resource_type = resource_type
super().__init__(f"{message} (Resource: {resource_type})")
class QualityError(FFmpegError):
"""Exception raised when video quality requirements cannot be met"""
def __init__(self, message: str, target_quality: int, achieved_quality: int):
self.target_quality = target_quality
self.achieved_quality = achieved_quality
super().__init__(
f"{message} (Target: {target_quality}p, Achieved: {achieved_quality}p)"
)
class AudioError(FFmpegError):
"""Exception raised when audio processing fails"""
pass
class BitrateError(FFmpegError):
"""Exception raised when bitrate requirements cannot be met"""
def __init__(self, message: str, target_bitrate: int, actual_bitrate: int):
self.target_bitrate = target_bitrate
self.actual_bitrate = actual_bitrate
super().__init__(
f"{message} (Target: {target_bitrate}bps, Actual: {actual_bitrate}bps)"
)
def handle_ffmpeg_error(error_output: str) -> FFmpegError:
"""Convert FFmpeg error output to appropriate exception"""
error_output = error_output.lower()
if "no such file" in error_output:
return FFmpegNotFoundError("FFmpeg binary not found")
elif "permission denied" in error_output:
return PermissionError("Insufficient permissions")
elif "hardware acceleration" in error_output:
return HardwareAccelerationError("Hardware acceleration failed", fallback_used=True)
elif "invalid data" in error_output:
return FormatError("Invalid or corrupted video format")
elif "insufficient memory" in error_output:
return ResourceError("Insufficient memory", "memory")
elif "audio" in error_output:
return AudioError("Audio processing failed")
elif "bitrate" in error_output:
return BitrateError("Bitrate requirements not met", 0, 0)
elif "timeout" in error_output:
return TimeoutError("Operation timed out")
else:
return FFmpegError(f"FFmpeg operation failed: {error_output}")

View File

@@ -8,9 +8,12 @@ import tarfile
import zipfile
import subprocess
import tempfile
import platform
import hashlib
from pathlib import Path
from contextlib import contextmanager
from typing import Optional
import time
from .exceptions import DownloadError
@@ -71,6 +74,9 @@ class FFmpegDownloader:
self.machine = "aarch64" # Normalize ARM64 naming
self.base_dir = base_dir
self.ffmpeg_path = self.base_dir / self._get_binary_name()
logger.info(f"Initialized FFmpeg downloader for {system}/{machine}")
logger.info(f"FFmpeg binary path: {self.ffmpeg_path}")
def _get_binary_name(self) -> str:
"""Get the appropriate binary name for the current system"""
@@ -87,37 +93,58 @@ class FFmpegDownloader:
raise DownloadError(f"Unsupported system/architecture: {self.system}/{self.machine}")
def download(self) -> Path:
"""Download and set up FFmpeg binary"""
try:
# Ensure base directory exists with proper permissions
self.base_dir.mkdir(parents=True, exist_ok=True)
os.chmod(str(self.base_dir), 0o777)
"""Download and set up FFmpeg binary with retries"""
max_retries = 3
retry_delay = 5
last_error = None
# Clean up any existing file or directory
if self.ffmpeg_path.exists():
if self.ffmpeg_path.is_dir():
shutil.rmtree(str(self.ffmpeg_path))
else:
self.ffmpeg_path.unlink()
for attempt in range(max_retries):
try:
logger.info(f"Download attempt {attempt + 1}/{max_retries}")
# Ensure base directory exists with proper permissions
self.base_dir.mkdir(parents=True, exist_ok=True)
os.chmod(str(self.base_dir), 0o777)
with temp_path_context() as temp_dir:
# Download archive
archive_path = self._download_archive(temp_dir)
# Extract FFmpeg binary
self._extract_binary(archive_path, temp_dir)
# Set proper permissions
os.chmod(str(self.ffmpeg_path), 0o755)
return self.ffmpeg_path
# Clean up any existing file or directory
if self.ffmpeg_path.exists():
if self.ffmpeg_path.is_dir():
shutil.rmtree(str(self.ffmpeg_path))
else:
self.ffmpeg_path.unlink()
except Exception as e:
logger.error(f"Failed to download FFmpeg: {str(e)}")
raise DownloadError(str(e))
with temp_path_context() as temp_dir:
# Download archive
archive_path = self._download_archive(temp_dir)
# Verify download
if not self._verify_download(archive_path):
raise DownloadError("Downloaded file verification failed")
# Extract FFmpeg binary
self._extract_binary(archive_path, temp_dir)
# Set proper permissions
os.chmod(str(self.ffmpeg_path), 0o755)
# Verify binary
if not self.verify():
raise DownloadError("FFmpeg binary verification failed")
logger.info(f"Successfully downloaded FFmpeg to {self.ffmpeg_path}")
return self.ffmpeg_path
except Exception as e:
last_error = str(e)
logger.error(f"Download attempt {attempt + 1} failed: {last_error}")
if attempt < max_retries - 1:
time.sleep(retry_delay * (attempt + 1)) # Exponential backoff
continue
raise DownloadError(f"All download attempts failed: {last_error}")
def _download_archive(self, temp_dir: str) -> Path:
"""Download FFmpeg archive"""
"""Download FFmpeg archive with progress tracking"""
url = self._get_download_url()
archive_path = Path(temp_dir) / f"ffmpeg_archive{'.zip' if self.system == 'Windows' else '.tar.xz'}"
@@ -126,14 +153,46 @@ class FFmpegDownloader:
response = requests.get(url, stream=True, timeout=30)
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
block_size = 8192
downloaded = 0
with open(archive_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
for chunk in response.iter_content(chunk_size=block_size):
f.write(chunk)
downloaded += len(chunk)
if total_size > 0:
percent = (downloaded / total_size) * 100
logger.debug(f"Download progress: {percent:.1f}%")
return archive_path
except Exception as e:
raise DownloadError(f"Failed to download FFmpeg: {str(e)}")
def _verify_download(self, archive_path: Path) -> bool:
"""Verify downloaded archive integrity"""
try:
if not archive_path.exists():
return False
# Check file size
size = archive_path.stat().st_size
if size < 1000000: # Less than 1MB is suspicious
logger.error(f"Downloaded file too small: {size} bytes")
return False
# Check file hash
with open(archive_path, 'rb') as f:
file_hash = hashlib.sha256(f.read()).hexdigest()
logger.debug(f"Archive hash: {file_hash}")
return True
except Exception as e:
logger.error(f"Download verification failed: {str(e)}")
return False
def _extract_binary(self, archive_path: Path, temp_dir: str):
"""Extract FFmpeg binary from archive"""
logger.info("Extracting FFmpeg binary")
@@ -182,7 +241,13 @@ class FFmpegDownloader:
timeout=5
)
return result.returncode == 0
if result.returncode == 0:
version = result.stdout.decode().split('\n')[0]
logger.info(f"FFmpeg verification successful: {version}")
return True
else:
logger.error(f"FFmpeg verification failed: {result.stderr.decode()}")
return False
except Exception as e:
logger.error(f"FFmpeg verification failed: {e}")

View File

@@ -4,6 +4,7 @@ import os
import platform
import multiprocessing
import logging
import subprocess
from pathlib import Path
from typing import Dict, Any, Optional
@@ -21,6 +22,7 @@ class FFmpegManager:
# Set up base directory in videoarchiver/bin
module_dir = Path(__file__).parent.parent
self.base_dir = module_dir / "bin"
logger.info(f"FFmpeg base directory: {self.base_dir}")
# Initialize downloader
self.downloader = FFmpegDownloader(
@@ -31,6 +33,7 @@ class FFmpegManager:
# Get or download FFmpeg
self.ffmpeg_path = self._initialize_ffmpeg()
logger.info(f"Using FFmpeg from: {self.ffmpeg_path}")
# Initialize components
self.gpu_detector = GPUDetector(self.ffmpeg_path)
@@ -40,35 +43,130 @@ class FFmpegManager:
# Initialize encoder params
self.encoder_params = EncoderParams(self._cpu_cores, self._gpu_info)
# Verify FFmpeg functionality
self._verify_ffmpeg()
logger.info("FFmpeg manager initialized successfully")
def _initialize_ffmpeg(self) -> Path:
"""Initialize FFmpeg binary"""
# Verify existing FFmpeg if it exists
if self.downloader.ffmpeg_path.exists() and self.downloader.verify():
logger.info(f"Using existing FFmpeg: {self.downloader.ffmpeg_path}")
return self.downloader.ffmpeg_path
# Download and verify FFmpeg
logger.info("Downloading FFmpeg...")
"""Initialize FFmpeg binary with proper error handling"""
try:
# Verify existing FFmpeg if it exists
if self.downloader.ffmpeg_path.exists():
logger.info(f"Found existing FFmpeg: {self.downloader.ffmpeg_path}")
if self.downloader.verify():
return self.downloader.ffmpeg_path
else:
logger.warning("Existing FFmpeg is not functional, downloading new copy")
# Download and verify FFmpeg
logger.info("Downloading FFmpeg...")
ffmpeg_path = self.downloader.download()
if not self.downloader.verify():
raise FFmpegError("Downloaded FFmpeg binary is not functional")
# Set executable permissions
try:
if platform.system() != "Windows":
os.chmod(str(ffmpeg_path), 0o755)
except Exception as e:
logger.error(f"Failed to set FFmpeg permissions: {e}")
return ffmpeg_path
except Exception as e:
logger.error(f"Failed to initialize FFmpeg: {e}")
raise FFmpegError(f"Failed to initialize FFmpeg: {e}")
def _verify_ffmpeg(self) -> None:
"""Verify FFmpeg functionality with comprehensive checks"""
try:
# Check FFmpeg version
version_cmd = [str(self.ffmpeg_path), "-version"]
result = subprocess.run(
version_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=10
)
if result.returncode != 0:
raise FFmpegError("FFmpeg version check failed")
logger.info(f"FFmpeg version: {result.stdout.split()[2]}")
# Check FFmpeg capabilities
caps_cmd = [str(self.ffmpeg_path), "-hide_banner", "-encoders"]
result = subprocess.run(
caps_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=10
)
if result.returncode != 0:
raise FFmpegError("FFmpeg capabilities check failed")
# Verify encoders
required_encoders = ["libx264"]
if self._gpu_info["nvidia"]:
required_encoders.append("h264_nvenc")
elif self._gpu_info["amd"]:
required_encoders.append("h264_amf")
elif self._gpu_info["intel"]:
required_encoders.append("h264_qsv")
available_encoders = result.stdout.lower()
missing_encoders = [
encoder for encoder in required_encoders
if encoder not in available_encoders
]
if missing_encoders:
logger.warning(f"Missing encoders: {', '.join(missing_encoders)}")
logger.info("FFmpeg verification completed successfully")
except subprocess.TimeoutExpired:
raise FFmpegError("FFmpeg verification timed out")
except Exception as e:
raise FFmpegError(f"FFmpeg verification failed: {e}")
def analyze_video(self, input_path: str) -> Dict[str, Any]:
"""Analyze video content for optimal encoding settings"""
return self.video_analyzer.analyze_video(input_path)
try:
if not os.path.exists(input_path):
raise FileNotFoundError(f"Input file not found: {input_path}")
return self.video_analyzer.analyze_video(input_path)
except Exception as e:
logger.error(f"Video analysis failed: {e}")
return {}
def get_compression_params(self, input_path: str, target_size_mb: int) -> Dict[str, str]:
"""Get optimal compression parameters for the given input file"""
# Analyze video first
video_info = self.analyze_video(input_path)
# Get encoding parameters
return self.encoder_params.get_params(video_info, target_size_mb * 1024 * 1024)
try:
# Analyze video first
video_info = self.analyze_video(input_path)
if not video_info:
raise FFmpegError("Failed to analyze video")
# Convert target size to bytes
target_size_bytes = target_size_mb * 1024 * 1024
# Get encoding parameters
params = self.encoder_params.get_params(video_info, target_size_bytes)
logger.info(f"Generated compression parameters: {params}")
return params
except Exception as e:
logger.error(f"Failed to get compression parameters: {e}")
# Return safe default parameters
return {
"c:v": "libx264",
"preset": "medium",
"crf": "23",
"c:a": "aac",
"b:a": "128k"
}
def get_ffmpeg_path(self) -> str:
"""Get path to FFmpeg binary"""

View File

@@ -1,130 +1,271 @@
"""GPU detection functionality for FFmpeg"""
import os
import json
import subprocess
import logging
import platform
import re
from typing import Dict, List
from pathlib import Path
from typing import Dict
logger = logging.getLogger("VideoArchiver")
class GPUDetector:
def __init__(self, ffmpeg_path: Path):
self.ffmpeg_path = ffmpeg_path
"""Initialize GPU detector
Args:
ffmpeg_path: Path to FFmpeg binary
"""
self.ffmpeg_path = Path(ffmpeg_path)
if not self.ffmpeg_path.exists():
raise FileNotFoundError(f"FFmpeg not found at {self.ffmpeg_path}")
def detect_gpu(self) -> Dict[str, bool]:
"""Detect available GPU and its capabilities"""
gpu_info = {"nvidia": False, "amd": False, "intel": False, "arm": False}
"""Detect available GPU acceleration support
Returns:
Dict containing boolean flags for each GPU type
"""
gpu_info = {
"nvidia": False,
"amd": False,
"intel": False
}
try:
if os.name == "posix": # Linux/Unix
gpu_info.update(self._detect_linux_gpu())
elif os.name == "nt": # Windows
# Check system-specific GPU detection first
system = platform.system().lower()
if system == "windows":
gpu_info.update(self._detect_windows_gpu())
elif system == "linux":
gpu_info.update(self._detect_linux_gpu())
elif system == "darwin":
gpu_info.update(self._detect_macos_gpu())
# Verify GPU support in FFmpeg
gpu_info.update(self._verify_ffmpeg_gpu_support())
# Log detection results
detected_gpus = [name for name, detected in gpu_info.items() if detected]
if detected_gpus:
logger.info(f"Detected GPUs: {', '.join(detected_gpus)}")
else:
logger.info("No GPU acceleration support detected")
return gpu_info
except Exception as e:
logger.warning(f"GPU detection failed: {str(e)}")
return gpu_info
def _test_encoder(self, encoder: str) -> bool:
"""Test if a specific encoder works"""
try:
test_cmd = [
str(self.ffmpeg_path),
"-f", "lavfi",
"-i", "testsrc=duration=1:size=1280x720:rate=30",
"-c:v", encoder,
"-f", "null",
"-"
]
result = subprocess.run(
test_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=5
)
return result.returncode == 0
except Exception:
return False
def _detect_linux_gpu(self) -> Dict[str, bool]:
"""Detect GPUs on Linux systems"""
gpu_info = {"nvidia": False, "amd": False, "intel": False, "arm": False}
# Check for NVIDIA GPU
try:
nvidia_smi = subprocess.run(
["nvidia-smi", "-q", "-d", "ENCODER"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=5
)
if nvidia_smi.returncode == 0 and b"Encoder" in nvidia_smi.stdout:
gpu_info["nvidia"] = self._test_encoder("h264_nvenc")
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
# Check for AMD GPU
try:
if os.path.exists("/dev/dri/renderD128"):
with open("/sys/class/drm/renderD128/device/vendor", "r") as f:
vendor = f.read().strip()
if vendor == "0x1002": # AMD vendor ID
gpu_info["amd"] = self._test_encoder("h264_amf")
except (IOError, OSError):
pass
# Check for Intel GPU
try:
lspci = subprocess.run(
["lspci"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=5
)
output = lspci.stdout.decode().lower()
if "intel" in output and ("vga" in output or "display" in output):
gpu_info["intel"] = self._test_encoder("h264_qsv")
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
# Check for ARM GPU
if os.uname().machine.startswith(("aarch64", "armv7")):
gpu_info["arm"] = True
return gpu_info
logger.error(f"Error during GPU detection: {str(e)}")
return {"nvidia": False, "amd": False, "intel": False}
def _detect_windows_gpu(self) -> Dict[str, bool]:
"""Detect GPUs on Windows systems"""
gpu_info = {"nvidia": False, "amd": False, "intel": False, "arm": False}
"""Detect GPUs on Windows using PowerShell"""
gpu_info = {"nvidia": False, "amd": False, "intel": False}
try:
# Use PowerShell to get GPU info
ps_command = "Get-WmiObject Win32_VideoController | ConvertTo-Json"
result = subprocess.run(
["powershell", "-Command", ps_command],
capture_output=True,
text=True,
timeout=10
)
cmd = ["powershell", "-Command", "Get-WmiObject Win32_VideoController | Select-Object Name"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
gpu_data = json.loads(result.stdout)
if not isinstance(gpu_data, list):
gpu_data = [gpu_data]
output = result.stdout.lower()
gpu_info["nvidia"] = "nvidia" in output
gpu_info["amd"] = any(x in output for x in ["amd", "radeon"])
gpu_info["intel"] = "intel" in output
except Exception as e:
logger.error(f"Windows GPU detection failed: {str(e)}")
return gpu_info
for gpu in gpu_data:
name = gpu.get("Name", "").lower()
if "nvidia" in name:
gpu_info["nvidia"] = self._test_encoder("h264_nvenc")
if "amd" in name or "radeon" in name:
gpu_info["amd"] = self._test_encoder("h264_amf")
if "intel" in name:
gpu_info["intel"] = self._test_encoder("h264_qsv")
def _detect_linux_gpu(self) -> Dict[str, bool]:
"""Detect GPUs on Linux using lspci and other tools"""
gpu_info = {"nvidia": False, "amd": False, "intel": False}
try:
# Try lspci first
try:
result = subprocess.run(
["lspci", "-v"],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
output = result.stdout.lower()
gpu_info["nvidia"] = "nvidia" in output
gpu_info["amd"] = any(x in output for x in ["amd", "radeon"])
gpu_info["intel"] = "intel" in output
except FileNotFoundError:
pass
# Check for NVIDIA using nvidia-smi
if not gpu_info["nvidia"]:
try:
result = subprocess.run(
["nvidia-smi"],
capture_output=True,
timeout=10
)
gpu_info["nvidia"] = result.returncode == 0
except FileNotFoundError:
pass
# Check for AMD using rocm-smi
if not gpu_info["amd"]:
try:
result = subprocess.run(
["rocm-smi"],
capture_output=True,
timeout=10
)
gpu_info["amd"] = result.returncode == 0
except FileNotFoundError:
pass
# Check for Intel using intel_gpu_top
if not gpu_info["intel"]:
try:
result = subprocess.run(
["intel_gpu_top", "-L"],
capture_output=True,
timeout=10
)
gpu_info["intel"] = result.returncode == 0
except FileNotFoundError:
pass
except Exception as e:
logger.error(f"Error during Windows GPU detection: {str(e)}")
logger.error(f"Linux GPU detection failed: {str(e)}")
return gpu_info
def _detect_macos_gpu(self) -> Dict[str, bool]:
"""Detect GPUs on macOS using system_profiler"""
gpu_info = {"nvidia": False, "amd": False, "intel": False}
try:
cmd = ["system_profiler", "SPDisplaysDataType"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
output = result.stdout.lower()
gpu_info["nvidia"] = "nvidia" in output
gpu_info["amd"] = any(x in output for x in ["amd", "radeon"])
gpu_info["intel"] = "intel" in output
except Exception as e:
logger.error(f"macOS GPU detection failed: {str(e)}")
return gpu_info
def _verify_ffmpeg_gpu_support(self) -> Dict[str, bool]:
"""Verify GPU support in FFmpeg installation"""
gpu_support = {"nvidia": False, "amd": False, "intel": False}
try:
# Check FFmpeg encoders
cmd = [str(self.ffmpeg_path), "-hide_banner", "-encoders"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
output = result.stdout.lower()
# Check for specific GPU encoders
gpu_support["nvidia"] = "h264_nvenc" in output
gpu_support["amd"] = "h264_amf" in output
gpu_support["intel"] = "h264_qsv" in output
# Log available encoders
encoders = []
if gpu_support["nvidia"]:
encoders.append("NVENC")
if gpu_support["amd"]:
encoders.append("AMF")
if gpu_support["intel"]:
encoders.append("QSV")
if encoders:
logger.info(f"FFmpeg supports GPU encoders: {', '.join(encoders)}")
else:
logger.info("No GPU encoders available in FFmpeg")
except Exception as e:
logger.error(f"FFmpeg GPU support verification failed: {str(e)}")
return gpu_support
def get_gpu_info(self) -> Dict[str, List[str]]:
"""Get detailed GPU information
Returns:
Dict containing lists of GPU names by type
"""
gpu_info = {
"nvidia": [],
"amd": [],
"intel": []
}
try:
system = platform.system().lower()
if system == "windows":
cmd = ["powershell", "-Command", "Get-WmiObject Win32_VideoController | Select-Object Name"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
for line in result.stdout.splitlines():
line = line.strip().lower()
if line:
if "nvidia" in line:
gpu_info["nvidia"].append(line)
elif any(x in line for x in ["amd", "radeon"]):
gpu_info["amd"].append(line)
elif "intel" in line:
gpu_info["intel"].append(line)
elif system == "linux":
try:
result = subprocess.run(
["lspci", "-v"],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
for line in result.stdout.splitlines():
if "vga" in line.lower() or "3d" in line.lower():
if "nvidia" in line.lower():
gpu_info["nvidia"].append(line.strip())
elif any(x in line.lower() for x in ["amd", "radeon"]):
gpu_info["amd"].append(line.strip())
elif "intel" in line.lower():
gpu_info["intel"].append(line.strip())
except FileNotFoundError:
pass
elif system == "darwin":
cmd = ["system_profiler", "SPDisplaysDataType"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
current_gpu = None
for line in result.stdout.splitlines():
line = line.strip().lower()
if "chipset model" in line:
if "nvidia" in line:
current_gpu = "nvidia"
gpu_info["nvidia"].append(line.split(":")[1].strip())
elif any(x in line for x in ["amd", "radeon"]):
current_gpu = "amd"
gpu_info["amd"].append(line.split(":")[1].strip())
elif "intel" in line:
current_gpu = "intel"
gpu_info["intel"].append(line.split(":")[1].strip())
except Exception as e:
logger.error(f"Error getting detailed GPU info: {str(e)}")
return gpu_info

View File

@@ -4,11 +4,12 @@ import os
import subprocess
import logging
from pathlib import Path
from typing import Dict, Any
from typing import Dict, Any, Optional
from contextlib import contextmanager
import tempfile
import shutil
import json
import re
logger = logging.getLogger("VideoArchiver")
@@ -27,32 +28,69 @@ def temp_path_context():
class VideoAnalyzer:
def __init__(self, ffmpeg_path: Path):
self.ffmpeg_path = ffmpeg_path
"""Initialize video analyzer with FFmpeg path
Args:
ffmpeg_path: Path to FFmpeg binary
"""
self.ffmpeg_path = Path(ffmpeg_path)
self.ffprobe_path = self.ffmpeg_path.parent / (
"ffprobe.exe" if os.name == "nt" else "ffprobe"
)
# Verify paths exist
if not self.ffmpeg_path.exists():
raise FileNotFoundError(f"FFmpeg not found at {self.ffmpeg_path}")
if not self.ffprobe_path.exists():
raise FileNotFoundError(f"FFprobe not found at {self.ffprobe_path}")
logger.info(f"Initialized VideoAnalyzer with FFmpeg: {self.ffmpeg_path}, FFprobe: {self.ffprobe_path}")
def analyze_video(self, input_path: str) -> Dict[str, Any]:
"""Analyze video content for optimal encoding settings"""
"""Analyze video content for optimal encoding settings
Args:
input_path: Path to input video file
Returns:
Dict containing video analysis results
"""
try:
if not os.path.exists(input_path):
logger.error(f"Input file not found: {input_path}")
return {}
# Use ffprobe to get video information
probe_result = self._probe_video(input_path)
if not probe_result:
logger.error("Failed to probe video")
return {}
# Get video stream info
video_info = next(
(s for s in probe_result["streams"] if s["codec_type"] == "video"),
None
)
if not video_info:
logger.error("No video stream found")
return {}
# Get video properties
width = int(video_info.get("width", 0))
height = int(video_info.get("height", 0))
fps = eval(video_info.get("r_frame_rate", "30/1"))
duration = float(probe_result["format"].get("duration", 0))
bitrate = float(probe_result["format"].get("bit_rate", 0))
# Get video properties with validation
try:
width = int(video_info.get("width", 0))
height = int(video_info.get("height", 0))
fps = self._parse_frame_rate(video_info.get("r_frame_rate", "30/1"))
duration = float(probe_result["format"].get("duration", 0))
bitrate = float(probe_result["format"].get("bit_rate", 0))
except (ValueError, ZeroDivisionError) as e:
logger.error(f"Error parsing video properties: {e}")
return {}
# Advanced analysis
# Advanced analysis with progress logging
logger.info("Starting motion detection analysis...")
has_high_motion = self._detect_high_motion(video_info)
logger.info("Starting dark scene analysis...")
has_dark_scenes = self._analyze_dark_scenes(input_path)
# Get audio properties
@@ -62,7 +100,7 @@ class VideoAnalyzer:
)
audio_props = self._get_audio_properties(audio_info)
return {
result = {
"width": width,
"height": height,
"fps": fps,
@@ -70,9 +108,12 @@ class VideoAnalyzer:
"bitrate": bitrate,
"has_high_motion": has_high_motion,
"has_dark_scenes": has_dark_scenes,
"has_complex_scenes": False, # Reserved for future use
"has_complex_scenes": self._detect_complex_scenes(video_info),
**audio_props
}
logger.info(f"Video analysis complete: {result}")
return result
except Exception as e:
logger.error(f"Error analyzing video: {str(e)}")
@@ -82,38 +123,71 @@ class VideoAnalyzer:
"""Use ffprobe to get video information"""
try:
cmd = [
str(self.ffmpeg_path).replace('ffmpeg', 'ffprobe'),
str(self.ffprobe_path),
"-v", "quiet",
"-print_format", "json",
"-show_format",
"-show_streams",
"-show_frames",
"-read_intervals", "%+#10", # Only analyze first 10 frames for speed
input_path
]
logger.debug(f"Running ffprobe command: {' '.join(cmd)}")
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
text=True,
timeout=30 # Add timeout
)
if result.returncode == 0:
return json.loads(result.stdout)
try:
return json.loads(result.stdout)
except json.JSONDecodeError as e:
logger.error(f"Failed to parse ffprobe output: {e}")
else:
logger.error(f"FFprobe failed: {result.stderr}")
except subprocess.TimeoutExpired:
logger.error("FFprobe timed out")
except Exception as e:
logger.error(f"Error probing video: {str(e)}")
return {}
def _detect_high_motion(self, video_info: Dict) -> bool:
"""Detect high motion content based on frame rate analysis"""
def _parse_frame_rate(self, rate_str: str) -> float:
"""Parse frame rate string to float"""
try:
if video_info.get("avg_frame_rate"):
avg_fps = eval(video_info["avg_frame_rate"])
fps = eval(video_info.get("r_frame_rate", "30/1"))
return abs(avg_fps - fps) > 5 # Significant frame rate variation
if "/" in rate_str:
num, den = map(float, rate_str.split("/"))
return num / den if den != 0 else 0
return float(rate_str)
except (ValueError, ZeroDivisionError):
return 30.0 # Default to 30fps
def _detect_high_motion(self, video_info: Dict) -> bool:
"""Detect high motion content based on frame rate and codec parameters"""
try:
# Check frame rate variation
if video_info.get("avg_frame_rate") and video_info.get("r_frame_rate"):
avg_fps = self._parse_frame_rate(video_info["avg_frame_rate"])
fps = self._parse_frame_rate(video_info["r_frame_rate"])
if abs(avg_fps - fps) > 5: # Significant frame rate variation
return True
# Check codec parameters for motion indicators
if "codec_tag_string" in video_info:
high_motion_codecs = ["avc1", "h264", "hevc"]
if any(codec in video_info["codec_tag_string"].lower() for codec in high_motion_codecs):
return True
except Exception as e:
logger.warning(f"Frame rate analysis failed: {str(e)}")
return False
def _analyze_dark_scenes(self, input_path: str) -> bool:
"""Analyze video for dark scenes"""
"""Analyze video for dark scenes using FFmpeg signalstats filter"""
try:
with temp_path_context() as temp_dir:
sample_cmd = [
@@ -122,40 +196,77 @@ class VideoAnalyzer:
"-vf", "select='eq(pict_type,I)',signalstats",
"-show_entries", "frame_tags=lavfi.signalstats.YAVG",
"-f", "null",
"-t", "30", # Only analyze first 30 seconds
"-"
]
logger.debug(f"Running dark scene analysis: {' '.join(sample_cmd)}")
result = subprocess.run(
sample_cmd,
capture_output=True,
text=True
text=True,
timeout=60 # Add timeout
)
dark_frames = 0
total_frames = 0
for line in result.stderr.split("\n"):
if "YAVG" in line:
avg_brightness = float(line.split("=")[1])
if avg_brightness < 40: # Dark scene threshold
dark_frames += 1
total_frames += 1
try:
avg_brightness = float(line.split("=")[1])
if avg_brightness < 40: # Dark scene threshold
dark_frames += 1
total_frames += 1
except (ValueError, IndexError):
continue
return total_frames > 0 and (dark_frames / total_frames) > 0.2
except subprocess.TimeoutExpired:
logger.warning("Dark scene analysis timed out")
except Exception as e:
logger.warning(f"Dark scene analysis failed: {str(e)}")
return False
return False
def _get_audio_properties(self, audio_info: Dict) -> Dict[str, Any]:
def _detect_complex_scenes(self, video_info: Dict) -> bool:
"""Detect complex scenes based on codec parameters and bitrate"""
try:
# Check for high profile/level
profile = video_info.get("profile", "").lower()
level = video_info.get("level", -1)
if "high" in profile or level >= 41: # Level 4.1 or higher
return True
# Check for high bitrate
if "bit_rate" in video_info:
bitrate = int(video_info["bit_rate"])
if bitrate > 4000000: # Higher than 4Mbps
return True
except Exception as e:
logger.warning(f"Complex scene detection failed: {str(e)}")
return False
def _get_audio_properties(self, audio_info: Optional[Dict]) -> Dict[str, Any]:
"""Extract audio properties from stream info"""
if not audio_info:
return {
"audio_bitrate": 0,
"audio_bitrate": 128000, # Default to 128kbps
"audio_channels": 2,
"audio_sample_rate": 48000
}
return {
"audio_bitrate": int(audio_info.get("bit_rate", 0)),
"audio_channels": int(audio_info.get("channels", 2)),
"audio_sample_rate": int(audio_info.get("sample_rate", 48000))
}
try:
return {
"audio_bitrate": int(audio_info.get("bit_rate", 128000)),
"audio_channels": int(audio_info.get("channels", 2)),
"audio_sample_rate": int(audio_info.get("sample_rate", 48000))
}
except (ValueError, TypeError) as e:
logger.warning(f"Error parsing audio properties: {e}")
return {
"audio_bitrate": 128000,
"audio_channels": 2,
"audio_sample_rate": 48000
}