"""Echtzeit-Parsing der ffmpeg stderr-Ausgabe""" import re import asyncio import logging from typing import Callable, Awaitable from app.models.job import ConversionJob from app.models.media import MediaFile class ProgressParser: """ Liest ffmpeg stderr und extrahiert Fortschrittsinformationen. Ruft Callback fuer WebSocket-Updates auf. Speichert letzte stderr-Zeilen fuer Fehlerdiagnose. """ def __init__(self, on_progress: Callable[[ConversionJob], Awaitable[None]]): self.on_progress = on_progress self.last_lines: list[str] = [] self._max_lines = 50 async def monitor(self, job: ConversionJob) -> None: """ Hauptschleife: Liest stderr des ffmpeg-Prozesses, parst Fortschritt und ruft Callback auf. """ if not job.process or not job.process.stderr: return empty_reads = 0 max_empty_reads = 30 update_counter = 0 while True: try: data = await job.process.stderr.read(1024) except Exception: break if not data: empty_reads += 1 if empty_reads > max_empty_reads: break await asyncio.sleep(0.5) continue empty_reads = 0 line = data.decode(errors="replace") # Letzte Zeilen fuer Fehlerdiagnose speichern for part in line.splitlines(): part = part.strip() if part: self.last_lines.append(part) if len(self.last_lines) > self._max_lines: self.last_lines.pop(0) self._extract_values(job, line) self._calculate_progress(job) job.update_stats(job.progress_fps, job.progress_speed, job.progress_bitrate) # WebSocket-Update senden await self.on_progress(job) # Ausfuehrliches Logging alle 100 Reads update_counter += 1 if update_counter % 100 == 0: logging.info( f"[{job.media.source_filename}] " f"{job.progress_percent:.1f}% | " f"FPS: {job.progress_fps} | " f"Speed: {job.progress_speed}x | " f"ETA: {MediaFile.format_time(job.progress_eta_sec)}" ) def get_error_output(self) -> str: """Gibt die letzten stderr-Zeilen als String zurueck""" return "\n".join(self.last_lines[-10:]) @staticmethod def _extract_values(job: ConversionJob, line: str) -> None: """Regex-Extraktion aus ffmpeg stderr""" # Frame match = re.findall(r"frame=\s*(\d+)", line) if match: frames = int(match[-1]) if frames > job.progress_frames: job.progress_frames = frames # FPS match = re.findall(r"fps=\s*(\d+\.?\d*)", line) if match: job.progress_fps = float(match[-1]) # Speed match = re.findall(r"speed=\s*(\d+\.?\d*)", line) if match: job.progress_speed = float(match[-1]) # Bitrate match = re.findall(r"bitrate=\s*(\d+)", line) if match: job.progress_bitrate = int(match[-1]) # Size (KiB von ffmpeg) match = re.findall(r"size=\s*(\d+)", line) if match: size_kib = int(match[-1]) size_bytes = size_kib * 1024 if size_bytes > job.progress_size_bytes: job.progress_size_bytes = size_bytes # Time (HH:MM:SS) match = re.findall(r"time=\s*(\d+:\d+:\d+\.?\d*)", line) if match: seconds = MediaFile.time_to_seconds(match[-1]) if seconds > job.progress_time_sec: job.progress_time_sec = seconds @staticmethod def _calculate_progress(job: ConversionJob) -> None: """Berechnet Fortschritt in % und ETA""" total_frames = job.media.total_frames if total_frames > 0: job.progress_percent = min( (job.progress_frames / total_frames) * 100, 100.0 ) # ETA basierend auf durchschnittlicher FPS if job.avg_fps > 0 and total_frames > 0: remaining_frames = total_frames - job.progress_frames job.progress_eta_sec = max(0, remaining_frames / job.avg_fps)