Core Systems:

Component-based architecture with lifecycle management
Enhanced error handling and recovery mechanisms
Comprehensive state management and tracking
Event-driven architecture with monitoring
Queue Management:

Multiple processing strategies for different scenarios
Advanced state management with recovery
Comprehensive metrics and health monitoring
Sophisticated cleanup system with multiple strategies
Processing Pipeline:

Enhanced message handling with validation
Improved URL extraction and processing
Better queue management and monitoring
Advanced cleanup mechanisms
Overall Benefits:

Better code organization and maintainability
Improved error handling and recovery
Enhanced monitoring and reporting
More robust and reliable system
This commit is contained in:
pacnpal
2024-11-16 05:01:29 +00:00
parent 537a325807
commit a4ca6e8ea6
47 changed files with 11085 additions and 2110 deletions

View File

@@ -0,0 +1,163 @@
"""Module for managing FFmpeg binaries"""
import logging
import os
from pathlib import Path
from typing import Dict, Optional
from .exceptions import (
FFmpegError,
DownloadError,
VerificationError,
PermissionError,
FFmpegNotFoundError
)
from .ffmpeg_downloader import FFmpegDownloader
from .verification_manager import VerificationManager
logger = logging.getLogger("FFmpegBinaryManager")
class BinaryManager:
"""Manages FFmpeg binary files and their lifecycle"""
def __init__(
self,
base_dir: Path,
system: str,
machine: str,
verification_manager: VerificationManager
):
self.base_dir = base_dir
self.verification_manager = verification_manager
# Initialize downloader
self.downloader = FFmpegDownloader(
system=system,
machine=machine,
base_dir=base_dir
)
self._ffmpeg_path: Optional[Path] = None
self._ffprobe_path: Optional[Path] = None
def initialize_binaries(self, gpu_info: Dict[str, bool]) -> Dict[str, Path]:
"""Initialize FFmpeg and FFprobe binaries
Args:
gpu_info: Dictionary of GPU availability
Returns:
Dict[str, Path]: Paths to FFmpeg and FFprobe binaries
Raises:
FFmpegError: If initialization fails
"""
try:
# Verify existing binaries if they exist
if self._verify_existing_binaries(gpu_info):
return self._get_binary_paths()
# Download and verify binaries
logger.info("Downloading FFmpeg and FFprobe...")
try:
binaries = self.downloader.download()
self._ffmpeg_path = binaries["ffmpeg"]
self._ffprobe_path = binaries["ffprobe"]
except Exception as e:
raise DownloadError(f"Failed to download FFmpeg: {e}")
# Verify downloaded binaries
self._verify_binaries(gpu_info)
return self._get_binary_paths()
except Exception as e:
logger.error(f"Failed to initialize binaries: {e}")
if isinstance(e, (DownloadError, VerificationError, PermissionError)):
raise
raise FFmpegError(f"Failed to initialize binaries: {e}")
def _verify_existing_binaries(self, gpu_info: Dict[str, bool]) -> bool:
"""Verify existing binary files if they exist
Returns:
bool: True if existing binaries are valid
"""
if (self.downloader.ffmpeg_path.exists() and
self.downloader.ffprobe_path.exists()):
logger.info(f"Found existing FFmpeg: {self.downloader.ffmpeg_path}")
logger.info(f"Found existing FFprobe: {self.downloader.ffprobe_path}")
try:
self._ffmpeg_path = self.downloader.ffmpeg_path
self._ffprobe_path = self.downloader.ffprobe_path
self._verify_binaries(gpu_info)
return True
except Exception as e:
logger.warning(f"Existing binaries verification failed: {e}")
return False
return False
def _verify_binaries(self, gpu_info: Dict[str, bool]) -> None:
"""Verify binary files and set permissions"""
try:
# Set permissions
self.verification_manager.verify_binary_permissions(self._ffmpeg_path)
self.verification_manager.verify_binary_permissions(self._ffprobe_path)
# Verify functionality
self.verification_manager.verify_ffmpeg(
self._ffmpeg_path,
self._ffprobe_path,
gpu_info
)
except Exception as e:
self._ffmpeg_path = None
self._ffprobe_path = None
raise VerificationError(f"Binary verification failed: {e}")
def _get_binary_paths(self) -> Dict[str, Path]:
"""Get paths to FFmpeg binaries
Returns:
Dict[str, Path]: Paths to FFmpeg and FFprobe binaries
Raises:
FFmpegNotFoundError: If binaries are not available
"""
if not self._ffmpeg_path or not self._ffprobe_path:
raise FFmpegNotFoundError("FFmpeg binaries not initialized")
return {
"ffmpeg": self._ffmpeg_path,
"ffprobe": self._ffprobe_path
}
def force_download(self, gpu_info: Dict[str, bool]) -> bool:
"""Force re-download of FFmpeg binaries
Returns:
bool: True if download and verification successful
"""
try:
logger.info("Force downloading FFmpeg...")
binaries = self.downloader.download()
self._ffmpeg_path = binaries["ffmpeg"]
self._ffprobe_path = binaries["ffprobe"]
self._verify_binaries(gpu_info)
return True
except Exception as e:
logger.error(f"Failed to force download FFmpeg: {e}")
return False
def get_ffmpeg_path(self) -> str:
"""Get path to FFmpeg binary"""
if not self._ffmpeg_path or not self._ffmpeg_path.exists():
raise FFmpegNotFoundError("FFmpeg is not available")
return str(self._ffmpeg_path)
def get_ffprobe_path(self) -> str:
"""Get path to FFprobe binary"""
if not self._ffprobe_path or not self._ffprobe_path.exists():
raise FFmpegNotFoundError("FFprobe is not available")
return str(self._ffprobe_path)

View File

@@ -1,44 +1,28 @@
"""Main FFmpeg management module"""
import os
import logging
import platform
import multiprocessing
import logging
import subprocess
import traceback
import signal
import psutil
from pathlib import Path
from typing import Dict, Any, Optional, Set
from typing import Dict, Any, Optional
from videoarchiver.ffmpeg.exceptions import (
from .exceptions import (
FFmpegError,
DownloadError,
VerificationError,
EncodingError,
AnalysisError,
GPUError,
HardwareAccelerationError,
FFmpegNotFoundError,
FFprobeError,
CompressionError,
FormatError,
PermissionError,
TimeoutError,
ResourceError,
QualityError,
AudioError,
BitrateError,
handle_ffmpeg_error
FFmpegNotFoundError
)
from videoarchiver.ffmpeg.gpu_detector import GPUDetector
from videoarchiver.ffmpeg.video_analyzer import VideoAnalyzer
from videoarchiver.ffmpeg.encoder_params import EncoderParams
from videoarchiver.ffmpeg.ffmpeg_downloader import FFmpegDownloader
from .gpu_detector import GPUDetector
from .video_analyzer import VideoAnalyzer
from .encoder_params import EncoderParams
from .process_manager import ProcessManager
from .verification_manager import VerificationManager
from .binary_manager import BinaryManager
logger = logging.getLogger("VideoArchiver")
class FFmpegManager:
"""Manages FFmpeg operations and lifecycle"""
def __init__(self):
"""Initialize FFmpeg manager"""
# Set up base directory in videoarchiver/bin
@@ -46,228 +30,39 @@ class FFmpegManager:
self.base_dir = module_dir / "bin"
logger.info(f"FFmpeg base directory: {self.base_dir}")
# Initialize downloader
self.downloader = FFmpegDownloader(
# Initialize managers
self.process_manager = ProcessManager()
self.verification_manager = VerificationManager(self.process_manager)
self.binary_manager = BinaryManager(
base_dir=self.base_dir,
system=platform.system(),
machine=platform.machine(),
base_dir=self.base_dir
verification_manager=self.verification_manager
)
# Get or download FFmpeg and FFprobe
binaries = self._initialize_binaries()
self.ffmpeg_path = binaries["ffmpeg"]
self.ffprobe_path = binaries["ffprobe"]
logger.info(f"Using FFmpeg from: {self.ffmpeg_path}")
logger.info(f"Using FFprobe from: {self.ffprobe_path}")
# Initialize components
self.gpu_detector = GPUDetector(self.ffmpeg_path)
self.video_analyzer = VideoAnalyzer(self.ffmpeg_path)
self.gpu_detector = GPUDetector(self.get_ffmpeg_path)
self.video_analyzer = VideoAnalyzer(self.get_ffmpeg_path)
self._gpu_info = self.gpu_detector.detect_gpu()
self._cpu_cores = multiprocessing.cpu_count()
# Initialize encoder params
self.encoder_params = EncoderParams(self._cpu_cores, self._gpu_info)
# Track active FFmpeg processes
self._active_processes: Set[subprocess.Popen] = set()
# Verify FFmpeg functionality
self._verify_ffmpeg()
# Initialize binaries
binaries = self.binary_manager.initialize_binaries(self._gpu_info)
logger.info(f"Using FFmpeg from: {binaries['ffmpeg']}")
logger.info(f"Using FFprobe from: {binaries['ffprobe']}")
logger.info("FFmpeg manager initialized successfully")
def kill_all_processes(self) -> None:
"""Kill all active FFmpeg processes"""
try:
# First try graceful termination
for process in self._active_processes:
try:
if process.poll() is None: # Process is still running
process.terminate()
except Exception as e:
logger.error(f"Error terminating FFmpeg process: {e}")
# Give processes a moment to terminate
import time
time.sleep(0.5)
# Force kill any remaining processes
for process in self._active_processes:
try:
if process.poll() is None: # Process is still running
process.kill()
except Exception as e:
logger.error(f"Error killing FFmpeg process: {e}")
# Find and kill any orphaned FFmpeg processes
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
try:
if 'ffmpeg' in proc.info['name'].lower():
proc.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
except Exception as e:
logger.error(f"Error killing orphaned FFmpeg process: {e}")
self._active_processes.clear()
logger.info("All FFmpeg processes terminated")
except Exception as e:
logger.error(f"Error killing FFmpeg processes: {e}")
def _initialize_binaries(self) -> Dict[str, Path]:
"""Initialize FFmpeg and FFprobe binaries with proper error handling"""
try:
# Verify existing binaries if they exist
if self.downloader.ffmpeg_path.exists() and self.downloader.ffprobe_path.exists():
logger.info(f"Found existing FFmpeg: {self.downloader.ffmpeg_path}")
logger.info(f"Found existing FFprobe: {self.downloader.ffprobe_path}")
if self.downloader.verify():
# Set executable permissions
if platform.system() != "Windows":
try:
os.chmod(str(self.downloader.ffmpeg_path), 0o755)
os.chmod(str(self.downloader.ffprobe_path), 0o755)
except Exception as e:
raise PermissionError(f"Failed to set binary permissions: {e}")
return {
"ffmpeg": self.downloader.ffmpeg_path,
"ffprobe": self.downloader.ffprobe_path
}
else:
logger.warning("Existing binaries are not functional, downloading new copies")
# Download and verify binaries
logger.info("Downloading FFmpeg and FFprobe...")
try:
binaries = self.downloader.download()
except Exception as e:
raise DownloadError(f"Failed to download FFmpeg: {e}")
if not self.downloader.verify():
raise VerificationError("Downloaded binaries are not functional")
# Set executable permissions
try:
if platform.system() != "Windows":
os.chmod(str(binaries["ffmpeg"]), 0o755)
os.chmod(str(binaries["ffprobe"]), 0o755)
except Exception as e:
raise PermissionError(f"Failed to set binary permissions: {e}")
return binaries
except Exception as e:
logger.error(f"Failed to initialize binaries: {e}")
if isinstance(e, (DownloadError, VerificationError, PermissionError)):
raise
raise FFmpegError(f"Failed to initialize binaries: {e}")
def _verify_ffmpeg(self) -> None:
"""Verify FFmpeg functionality with comprehensive checks"""
try:
# Check FFmpeg version with enhanced error handling
version_cmd = [str(self.ffmpeg_path), "-version"]
try:
result = subprocess.run(
version_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=10,
check=False, # Don't raise on non-zero return code
env={"PATH": os.environ.get("PATH", "")} # Ensure PATH is set
)
except subprocess.TimeoutExpired:
raise TimeoutError("FFmpeg version check timed out")
except Exception as e:
raise VerificationError(f"FFmpeg version check failed: {e}")
if result.returncode != 0:
error = handle_ffmpeg_error(result.stderr)
logger.error(f"FFmpeg version check failed: {result.stderr}")
raise error
logger.info(f"FFmpeg version: {result.stdout.split()[2]}")
# Check FFprobe version with enhanced error handling
probe_cmd = [str(self.ffprobe_path), "-version"]
try:
result = subprocess.run(
probe_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=10,
check=False, # Don't raise on non-zero return code
env={"PATH": os.environ.get("PATH", "")} # Ensure PATH is set
)
except subprocess.TimeoutExpired:
raise TimeoutError("FFprobe version check timed out")
except Exception as e:
raise VerificationError(f"FFprobe version check failed: {e}")
if result.returncode != 0:
error = handle_ffmpeg_error(result.stderr)
logger.error(f"FFprobe version check failed: {result.stderr}")
raise error
logger.info(f"FFprobe version: {result.stdout.split()[2]}")
# Check FFmpeg capabilities with enhanced error handling
caps_cmd = [str(self.ffmpeg_path), "-hide_banner", "-encoders"]
try:
result = subprocess.run(
caps_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=10,
check=False, # Don't raise on non-zero return code
env={"PATH": os.environ.get("PATH", "")} # Ensure PATH is set
)
except subprocess.TimeoutExpired:
raise TimeoutError("FFmpeg capabilities check timed out")
except Exception as e:
raise VerificationError(f"FFmpeg capabilities check failed: {e}")
if result.returncode != 0:
error = handle_ffmpeg_error(result.stderr)
logger.error(f"FFmpeg capabilities check failed: {result.stderr}")
raise error
# 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)}")
if "libx264" in missing_encoders:
raise EncodingError("Required encoder libx264 not available")
logger.info("FFmpeg verification completed successfully")
except Exception as e:
logger.error(f"FFmpeg verification failed: {traceback.format_exc()}")
if isinstance(e, (TimeoutError, EncodingError, VerificationError)):
raise
raise VerificationError(f"FFmpeg verification failed: {e}")
self.process_manager.kill_all_processes()
def analyze_video(self, input_path: str) -> Dict[str, Any]:
"""Analyze video content for optimal encoding settings"""
try:
if not os.path.exists(input_path):
if not input_path or not Path(input_path).exists():
raise FileNotFoundError(f"Input file not found: {input_path}")
return self.video_analyzer.analyze_video(input_path)
except Exception as e:
@@ -307,27 +102,15 @@ class FFmpegManager:
def get_ffmpeg_path(self) -> str:
"""Get path to FFmpeg binary"""
if not self.ffmpeg_path.exists():
raise FFmpegNotFoundError("FFmpeg is not available")
return str(self.ffmpeg_path)
return self.binary_manager.get_ffmpeg_path()
def get_ffprobe_path(self) -> str:
"""Get path to FFprobe binary"""
if not self.ffprobe_path.exists():
raise FFmpegNotFoundError("FFprobe is not available")
return str(self.ffprobe_path)
return self.binary_manager.get_ffprobe_path()
def force_download(self) -> bool:
"""Force re-download of FFmpeg binary"""
try:
logger.info("Force downloading FFmpeg...")
binaries = self.downloader.download()
self.ffmpeg_path = binaries["ffmpeg"]
self.ffprobe_path = binaries["ffprobe"]
return self.downloader.verify()
except Exception as e:
logger.error(f"Failed to force download FFmpeg: {e}")
return False
return self.binary_manager.force_download(self._gpu_info)
@property
def gpu_info(self) -> Dict[str, bool]:

View File

@@ -0,0 +1,127 @@
"""Module for managing FFmpeg processes"""
import logging
import psutil
import subprocess
import time
from typing import Set, Optional
logger = logging.getLogger("FFmpegProcessManager")
class ProcessManager:
"""Manages FFmpeg process execution and lifecycle"""
def __init__(self):
self._active_processes: Set[subprocess.Popen] = set()
def add_process(self, process: subprocess.Popen) -> None:
"""Add a process to track"""
self._active_processes.add(process)
def remove_process(self, process: subprocess.Popen) -> None:
"""Remove a process from tracking"""
self._active_processes.discard(process)
def kill_all_processes(self) -> None:
"""Kill all active FFmpeg processes"""
try:
# First try graceful termination
self._terminate_processes()
# Give processes a moment to terminate
time.sleep(0.5)
# Force kill any remaining processes
self._kill_remaining_processes()
# Find and kill any orphaned FFmpeg processes
self._kill_orphaned_processes()
self._active_processes.clear()
logger.info("All FFmpeg processes terminated")
except Exception as e:
logger.error(f"Error killing FFmpeg processes: {e}")
def _terminate_processes(self) -> None:
"""Attempt graceful termination of processes"""
for process in self._active_processes:
try:
if process.poll() is None: # Process is still running
process.terminate()
except Exception as e:
logger.error(f"Error terminating FFmpeg process: {e}")
def _kill_remaining_processes(self) -> None:
"""Force kill any remaining processes"""
for process in self._active_processes:
try:
if process.poll() is None: # Process is still running
process.kill()
except Exception as e:
logger.error(f"Error killing FFmpeg process: {e}")
def _kill_orphaned_processes(self) -> None:
"""Find and kill any orphaned FFmpeg processes"""
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
try:
if 'ffmpeg' in proc.info['name'].lower():
proc.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
except Exception as e:
logger.error(f"Error killing orphaned FFmpeg process: {e}")
def execute_command(
self,
command: list,
timeout: Optional[int] = None,
check: bool = False
) -> subprocess.CompletedProcess:
"""Execute an FFmpeg command with proper process management
Args:
command: Command list to execute
timeout: Optional timeout in seconds
check: Whether to check return code
Returns:
subprocess.CompletedProcess: Result of command execution
"""
process = None
try:
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
self.add_process(process)
stdout, stderr = process.communicate(timeout=timeout)
result = subprocess.CompletedProcess(
args=command,
returncode=process.returncode,
stdout=stdout,
stderr=stderr
)
if check and process.returncode != 0:
raise subprocess.CalledProcessError(
returncode=process.returncode,
cmd=command,
output=stdout,
stderr=stderr
)
return result
except subprocess.TimeoutExpired:
if process:
process.kill()
_, stderr = process.communicate()
raise
finally:
if process:
self.remove_process(process)

View File

@@ -0,0 +1,160 @@
"""Module for verifying FFmpeg functionality"""
import logging
import os
import subprocess
from pathlib import Path
from typing import Dict, List, Optional
from .exceptions import (
TimeoutError,
VerificationError,
EncodingError,
handle_ffmpeg_error
)
logger = logging.getLogger("FFmpegVerification")
class VerificationManager:
"""Handles verification of FFmpeg functionality"""
def __init__(self, process_manager):
self.process_manager = process_manager
def verify_ffmpeg(
self,
ffmpeg_path: Path,
ffprobe_path: Path,
gpu_info: Dict[str, bool]
) -> None:
"""Verify FFmpeg functionality with comprehensive checks
Args:
ffmpeg_path: Path to FFmpeg binary
ffprobe_path: Path to FFprobe binary
gpu_info: Dictionary of GPU availability
Raises:
VerificationError: If verification fails
TimeoutError: If verification times out
EncodingError: If required encoders are missing
"""
try:
# Check FFmpeg version
self._verify_ffmpeg_version(ffmpeg_path)
# Check FFprobe version
self._verify_ffprobe_version(ffprobe_path)
# Check FFmpeg capabilities
self._verify_ffmpeg_capabilities(ffmpeg_path, gpu_info)
logger.info("FFmpeg verification completed successfully")
except Exception as e:
logger.error(f"FFmpeg verification failed: {e}")
if isinstance(e, (TimeoutError, EncodingError, VerificationError)):
raise
raise VerificationError(f"FFmpeg verification failed: {e}")
def _verify_ffmpeg_version(self, ffmpeg_path: Path) -> None:
"""Verify FFmpeg version"""
try:
result = self._execute_command(
[str(ffmpeg_path), "-version"],
"FFmpeg version check"
)
logger.info(f"FFmpeg version: {result.stdout.split()[2]}")
except Exception as e:
raise VerificationError(f"FFmpeg version check failed: {e}")
def _verify_ffprobe_version(self, ffprobe_path: Path) -> None:
"""Verify FFprobe version"""
try:
result = self._execute_command(
[str(ffprobe_path), "-version"],
"FFprobe version check"
)
logger.info(f"FFprobe version: {result.stdout.split()[2]}")
except Exception as e:
raise VerificationError(f"FFprobe version check failed: {e}")
def _verify_ffmpeg_capabilities(
self,
ffmpeg_path: Path,
gpu_info: Dict[str, bool]
) -> None:
"""Verify FFmpeg capabilities and encoders"""
try:
result = self._execute_command(
[str(ffmpeg_path), "-hide_banner", "-encoders"],
"FFmpeg capabilities check"
)
# Verify required encoders
required_encoders = self._get_required_encoders(gpu_info)
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)}")
if "libx264" in missing_encoders:
raise EncodingError("Required encoder libx264 not available")
except Exception as e:
if isinstance(e, EncodingError):
raise
raise VerificationError(f"FFmpeg capabilities check failed: {e}")
def _execute_command(
self,
command: List[str],
operation: str,
timeout: int = 10
) -> subprocess.CompletedProcess:
"""Execute a command with proper error handling"""
try:
result = self.process_manager.execute_command(
command,
timeout=timeout,
check=False
)
if result.returncode != 0:
error = handle_ffmpeg_error(result.stderr)
logger.error(f"{operation} failed: {result.stderr}")
raise error
return result
except subprocess.TimeoutExpired:
raise TimeoutError(f"{operation} timed out")
except Exception as e:
if isinstance(e, (TimeoutError, EncodingError)):
raise
raise VerificationError(f"{operation} failed: {e}")
def _get_required_encoders(self, gpu_info: Dict[str, bool]) -> List[str]:
"""Get list of required encoders based on GPU availability"""
required_encoders = ["libx264"]
if gpu_info["nvidia"]:
required_encoders.append("h264_nvenc")
elif gpu_info["amd"]:
required_encoders.append("h264_amf")
elif gpu_info["intel"]:
required_encoders.append("h264_qsv")
return required_encoders
def verify_binary_permissions(self, binary_path: Path) -> None:
"""Verify and set binary permissions"""
try:
if os.name != "nt": # Not Windows
os.chmod(str(binary_path), 0o755)
except Exception as e:
raise VerificationError(f"Failed to set binary permissions: {e}")