mirror of
https://github.com/pacnpal/Pac-cogs.git
synced 2025-12-20 10:51:05 -05:00
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
331 lines
11 KiB
Python
331 lines
11 KiB
Python
"""Module for managing video compression"""
|
|
|
|
import os
|
|
import logging
|
|
import asyncio
|
|
import json
|
|
import subprocess
|
|
from datetime import datetime
|
|
from typing import Dict, Optional, Tuple, Callable, Set
|
|
|
|
from .exceptions import CompressionError, VideoVerificationError
|
|
|
|
logger = logging.getLogger("CompressionManager")
|
|
|
|
class CompressionManager:
|
|
"""Manages video compression operations"""
|
|
|
|
def __init__(self, ffmpeg_mgr, max_file_size: int):
|
|
self.ffmpeg_mgr = ffmpeg_mgr
|
|
self.max_file_size = max_file_size * 1024 * 1024 # Convert to bytes
|
|
self._active_processes: Set[subprocess.Popen] = set()
|
|
self._processes_lock = asyncio.Lock()
|
|
self._shutting_down = False
|
|
|
|
async def compress_video(
|
|
self,
|
|
input_file: str,
|
|
output_file: str,
|
|
progress_callback: Optional[Callable[[float], None]] = None
|
|
) -> Tuple[bool, str]:
|
|
"""Compress a video file
|
|
|
|
Args:
|
|
input_file: Path to input video file
|
|
output_file: Path to output video file
|
|
progress_callback: Optional callback for compression progress
|
|
|
|
Returns:
|
|
Tuple[bool, str]: (Success status, Error message if any)
|
|
"""
|
|
if self._shutting_down:
|
|
return False, "Compression manager is shutting down"
|
|
|
|
try:
|
|
# Get optimal compression parameters
|
|
compression_params = self.ffmpeg_mgr.get_compression_params(
|
|
input_file,
|
|
self.max_file_size // (1024 * 1024) # Convert to MB
|
|
)
|
|
|
|
# Try hardware acceleration first
|
|
success, error = await self._try_compression(
|
|
input_file,
|
|
output_file,
|
|
compression_params,
|
|
progress_callback,
|
|
use_hardware=True
|
|
)
|
|
|
|
# Fall back to CPU if hardware acceleration fails
|
|
if not success:
|
|
logger.warning(f"Hardware acceleration failed: {error}, falling back to CPU encoding")
|
|
success, error = await self._try_compression(
|
|
input_file,
|
|
output_file,
|
|
compression_params,
|
|
progress_callback,
|
|
use_hardware=False
|
|
)
|
|
|
|
if not success:
|
|
return False, f"Compression failed: {error}"
|
|
|
|
# Verify output file
|
|
if not await self._verify_output(input_file, output_file):
|
|
return False, "Output file verification failed"
|
|
|
|
return True, ""
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error during compression: {e}")
|
|
return False, str(e)
|
|
|
|
async def _try_compression(
|
|
self,
|
|
input_file: str,
|
|
output_file: str,
|
|
params: Dict[str, str],
|
|
progress_callback: Optional[Callable[[float], None]],
|
|
use_hardware: bool
|
|
) -> Tuple[bool, str]:
|
|
"""Attempt video compression with given parameters"""
|
|
if self._shutting_down:
|
|
return False, "Compression manager is shutting down"
|
|
|
|
try:
|
|
# Build FFmpeg command
|
|
cmd = await self._build_ffmpeg_command(
|
|
input_file,
|
|
output_file,
|
|
params,
|
|
use_hardware
|
|
)
|
|
|
|
# Get video duration for progress calculation
|
|
duration = await self._get_video_duration(input_file)
|
|
|
|
# Initialize compression progress tracking
|
|
await self._init_compression_progress(
|
|
input_file,
|
|
params,
|
|
use_hardware,
|
|
duration
|
|
)
|
|
|
|
# Run compression
|
|
process = await asyncio.create_subprocess_exec(
|
|
*cmd,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE
|
|
)
|
|
|
|
# Track the process
|
|
async with self._processes_lock:
|
|
self._active_processes.add(process)
|
|
|
|
try:
|
|
success = await self._monitor_compression(
|
|
process,
|
|
input_file,
|
|
output_file,
|
|
duration,
|
|
progress_callback
|
|
)
|
|
return success, ""
|
|
|
|
finally:
|
|
async with self._processes_lock:
|
|
self._active_processes.discard(process)
|
|
|
|
except Exception as e:
|
|
return False, str(e)
|
|
|
|
async def _build_ffmpeg_command(
|
|
self,
|
|
input_file: str,
|
|
output_file: str,
|
|
params: Dict[str, str],
|
|
use_hardware: bool
|
|
) -> List[str]:
|
|
"""Build FFmpeg command with appropriate parameters"""
|
|
ffmpeg_path = str(self.ffmpeg_mgr.get_ffmpeg_path())
|
|
cmd = [ffmpeg_path, "-y", "-i", input_file, "-progress", "pipe:1"]
|
|
|
|
# Modify parameters for hardware acceleration
|
|
if use_hardware:
|
|
gpu_info = self.ffmpeg_mgr.gpu_info
|
|
if gpu_info["nvidia"] and params.get("c:v") == "libx264":
|
|
params["c:v"] = "h264_nvenc"
|
|
elif gpu_info["amd"] and params.get("c:v") == "libx264":
|
|
params["c:v"] = "h264_amf"
|
|
elif gpu_info["intel"] and params.get("c:v") == "libx264":
|
|
params["c:v"] = "h264_qsv"
|
|
else:
|
|
params["c:v"] = "libx264"
|
|
|
|
# Add parameters to command
|
|
for key, value in params.items():
|
|
cmd.extend([f"-{key}", str(value)])
|
|
|
|
cmd.append(output_file)
|
|
return cmd
|
|
|
|
async def _monitor_compression(
|
|
self,
|
|
process: asyncio.subprocess.Process,
|
|
input_file: str,
|
|
output_file: str,
|
|
duration: float,
|
|
progress_callback: Optional[Callable[[float], None]]
|
|
) -> bool:
|
|
"""Monitor compression progress"""
|
|
start_time = datetime.utcnow()
|
|
|
|
while True:
|
|
if self._shutting_down:
|
|
process.terminate()
|
|
return False
|
|
|
|
line = await process.stdout.readline()
|
|
if not line:
|
|
break
|
|
|
|
try:
|
|
await self._update_progress(
|
|
line.decode().strip(),
|
|
input_file,
|
|
output_file,
|
|
duration,
|
|
start_time,
|
|
progress_callback
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error updating progress: {e}")
|
|
|
|
await process.wait()
|
|
return os.path.exists(output_file)
|
|
|
|
async def _verify_output(
|
|
self,
|
|
input_file: str,
|
|
output_file: str
|
|
) -> bool:
|
|
"""Verify compressed output file"""
|
|
try:
|
|
# Check file exists and is not empty
|
|
if not os.path.exists(output_file) or os.path.getsize(output_file) == 0:
|
|
return False
|
|
|
|
# Check file size is within limit
|
|
if os.path.getsize(output_file) > self.max_file_size:
|
|
return False
|
|
|
|
# Verify video integrity
|
|
return await self.ffmpeg_mgr.verify_video_file(output_file)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error verifying output file: {e}")
|
|
return False
|
|
|
|
async def cleanup(self) -> None:
|
|
"""Clean up resources"""
|
|
self._shutting_down = True
|
|
await self._terminate_processes()
|
|
|
|
async def force_cleanup(self) -> None:
|
|
"""Force cleanup of resources"""
|
|
self._shutting_down = True
|
|
await self._kill_processes()
|
|
|
|
async def _terminate_processes(self) -> None:
|
|
"""Terminate active processes gracefully"""
|
|
async with self._processes_lock:
|
|
for process in self._active_processes:
|
|
try:
|
|
process.terminate()
|
|
await asyncio.sleep(0.1)
|
|
if process.returncode is None:
|
|
process.kill()
|
|
except Exception as e:
|
|
logger.error(f"Error terminating process: {e}")
|
|
self._active_processes.clear()
|
|
|
|
async def _kill_processes(self) -> None:
|
|
"""Kill active processes immediately"""
|
|
async with self._processes_lock:
|
|
for process in self._active_processes:
|
|
try:
|
|
process.kill()
|
|
except Exception as e:
|
|
logger.error(f"Error killing process: {e}")
|
|
self._active_processes.clear()
|
|
|
|
async def _get_video_duration(self, file_path: str) -> float:
|
|
"""Get video duration in seconds"""
|
|
try:
|
|
return await self.ffmpeg_mgr.get_video_duration(file_path)
|
|
except Exception as e:
|
|
logger.error(f"Error getting video duration: {e}")
|
|
return 0
|
|
|
|
async def _init_compression_progress(
|
|
self,
|
|
input_file: str,
|
|
params: Dict[str, str],
|
|
use_hardware: bool,
|
|
duration: float
|
|
) -> None:
|
|
"""Initialize compression progress tracking"""
|
|
from videoarchiver.processor import _compression_progress
|
|
|
|
_compression_progress[input_file] = {
|
|
"active": True,
|
|
"filename": os.path.basename(input_file),
|
|
"start_time": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"),
|
|
"percent": 0,
|
|
"elapsed_time": "0:00",
|
|
"input_size": os.path.getsize(input_file),
|
|
"current_size": 0,
|
|
"target_size": self.max_file_size,
|
|
"codec": params.get("c:v", "unknown"),
|
|
"hardware_accel": use_hardware,
|
|
"preset": params.get("preset", "unknown"),
|
|
"crf": params.get("crf", "unknown"),
|
|
"duration": duration,
|
|
"bitrate": params.get("b:v", "unknown"),
|
|
"audio_codec": params.get("c:a", "unknown"),
|
|
"audio_bitrate": params.get("b:a", "unknown"),
|
|
"last_update": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"),
|
|
}
|
|
|
|
async def _update_progress(
|
|
self,
|
|
line: str,
|
|
input_file: str,
|
|
output_file: str,
|
|
duration: float,
|
|
start_time: datetime,
|
|
progress_callback: Optional[Callable[[float], None]]
|
|
) -> None:
|
|
"""Update compression progress"""
|
|
if line.startswith("out_time_ms="):
|
|
current_time = int(line.split("=")[1]) / 1000000
|
|
if duration > 0:
|
|
progress = min(100, (current_time / duration) * 100)
|
|
|
|
# Update compression progress
|
|
from videoarchiver.processor import _compression_progress
|
|
if input_file in _compression_progress:
|
|
elapsed = datetime.utcnow() - start_time
|
|
_compression_progress[input_file].update({
|
|
"percent": progress,
|
|
"elapsed_time": str(elapsed).split(".")[0],
|
|
"current_size": os.path.getsize(output_file) if os.path.exists(output_file) else 0,
|
|
"current_time": current_time,
|
|
"last_update": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"),
|
|
})
|
|
|
|
if progress_callback:
|
|
progress_callback(progress)
|