This commit is contained in:
pacnpal
2024-11-16 22:32:08 +00:00
parent b7d99490cf
commit dac21f2fcd
30 changed files with 5854 additions and 2279 deletions

View File

@@ -0,0 +1,210 @@
"""Video compression handling utilities"""
import os
import asyncio
import logging
import subprocess
from datetime import datetime
from typing import Dict, Optional, Callable, Set, Tuple
from videoarchiver.ffmpeg.ffmpeg_manager import FFmpegManager
from videoarchiver.ffmpeg.exceptions import CompressionError
from videoarchiver.utils.exceptions import VideoVerificationError
from videoarchiver.utils.file_operations import FileOperations
from videoarchiver.utils.progress_handler import ProgressHandler
logger = logging.getLogger("VideoArchiver")
class CompressionHandler:
"""Handles video compression operations"""
def __init__(self, ffmpeg_mgr: FFmpegManager, progress_handler: ProgressHandler,
file_ops: FileOperations):
self.ffmpeg_mgr = ffmpeg_mgr
self.progress_handler = progress_handler
self.file_ops = file_ops
self._active_processes: Set[subprocess.Popen] = set()
self._processes_lock = asyncio.Lock()
self._shutting_down = False
self.max_file_size = 0 # Will be set during compression
async def cleanup(self) -> None:
"""Clean up compression resources"""
self._shutting_down = True
try:
async with self._processes_lock:
for process in self._active_processes:
try:
process.terminate()
await asyncio.sleep(0.1)
if process.poll() is None:
process.kill()
except Exception as e:
logger.error(f"Error killing compression process: {e}")
self._active_processes.clear()
finally:
self._shutting_down = False
async def compress_video(
self,
input_file: str,
output_file: str,
max_size_mb: int,
progress_callback: Optional[Callable[[float], None]] = None
) -> Tuple[bool, str]:
"""Compress video to target size"""
if self._shutting_down:
return False, "Compression handler is shutting down"
self.max_file_size = max_size_mb
try:
# Get optimal compression parameters
compression_params = self.ffmpeg_mgr.get_compression_params(
input_file, max_size_mb
)
# Try hardware acceleration first
success = 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("Hardware acceleration failed, falling back to CPU encoding")
success = await self._try_compression(
input_file,
output_file,
compression_params,
progress_callback,
use_hardware=False
)
if not success:
return False, "Failed to compress with both hardware and CPU encoding"
# Verify compressed file
if not self.file_ops.verify_video_file(output_file, str(self.ffmpeg_mgr.get_ffprobe_path())):
return False, "Compressed file verification failed"
# Check final size
within_limit, final_size = self.file_ops.check_file_size(output_file, max_size_mb)
if not within_limit:
return False, f"Failed to compress to target size: {final_size} bytes"
return True, ""
except Exception as 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]] = None,
use_hardware: bool = True,
) -> bool:
"""Attempt video compression with given parameters"""
if self._shutting_down:
return False
try:
# Build FFmpeg command
ffmpeg_path = str(self.ffmpeg_mgr.get_ffmpeg_path())
cmd = [ffmpeg_path, "-y", "-i", input_file]
# Add progress monitoring
cmd.extend(["-progress", "pipe:1"])
# Modify parameters based on hardware acceleration preference
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 all parameters to command
for key, value in params.items():
cmd.extend([f"-{key}", str(value)])
# Add output file
cmd.append(output_file)
# Get video duration for progress calculation
duration = self.file_ops.get_video_duration(input_file, str(self.ffmpeg_mgr.get_ffprobe_path()))
# Initialize compression progress
self.progress_handler.update(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 * 1024 * 1024,
"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"),
})
# Run compression with progress monitoring
try:
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)
start_time = datetime.utcnow()
while True:
if self._shutting_down:
process.terminate()
return False
line = await process.stdout.readline()
if not line:
break
try:
line = line.decode().strip()
if line.startswith("out_time_ms="):
current_time = int(line.split("=")[1]) / 1000000
self.progress_handler.handle_compression_progress(
input_file, current_time, duration,
output_file, start_time, progress_callback
)
except Exception as e:
logger.error(f"Error parsing FFmpeg progress: {e}")
await process.wait()
return os.path.exists(output_file)
except Exception as e:
logger.error(f"Error during compression process: {e}")
return False
finally:
# Remove process from tracking
async with self._processes_lock:
self._active_processes.discard(process)
except Exception as e:
logger.error(f"Compression attempt failed: {str