"""Proxy generation — low-res MJPEG for frame-accurate scrubbing. Each completed recording segment gets a lightweight proxy video where every frame is a keyframe (MJPEG). mpv can seek frame-accurately in these files with hr-seek=yes, giving DaVinci Resolve-style scrubbing speed. Proxies are ephemeral — stored in /tmp, regenerated on demand. """ import logging import shutil from pathlib import Path import ffmpeg as ffmpeg_lib log = logging.getLogger(__name__) PROXY_DIR = Path("/tmp/cht_proxy") PROXY_HEIGHT = 360 # pixels — low enough for speed, high enough to see content def proxy_path_for(segment_path: Path, session_id: str | None = None) -> Path: """Return the proxy path for a given segment.""" subdir = session_id or "default" return PROXY_DIR / subdir / f"{segment_path.stem}_proxy.avi" def generate_proxy(segment_path: Path, output_path: Path, height: int = PROXY_HEIGHT) -> Path: """Transcode a segment to MJPEG proxy at reduced resolution. Every frame is a keyframe — enables O(1) seeking. Returns output_path on success. """ output_path.parent.mkdir(parents=True, exist_ok=True) stream = ffmpeg_lib.input(str(segment_path)) output = ( ffmpeg_lib.output( stream, str(output_path), vcodec="mjpeg", vf=f"scale=-2:{height}", # MJPEG: every frame is a keyframe by nature **{"q:v": "5"}, # quality 2-31, lower = better an=None, # strip audio ) .overwrite_output() .global_args("-hide_banner", "-loglevel", "warning") ) log.info("Generating proxy: %s → %s", segment_path.name, output_path) try: output.run(capture_stdout=True, capture_stderr=True) except ffmpeg_lib.Error as e: stderr = (e.stderr or b"").decode("utf-8", errors="replace") log.error("Proxy generation failed for %s: %s", segment_path.name, stderr.strip()) raise log.info("Proxy ready: %s (%.1f MB)", output_path.name, output_path.stat().st_size / 1_000_000) return output_path def ensure_proxy(segment_path: Path, session_id: str | None = None, height: int = PROXY_HEIGHT) -> Path: """Return proxy path, generating it if missing.""" out = proxy_path_for(segment_path, session_id) if out.exists(): return out return generate_proxy(segment_path, out, height) def cleanup_proxies(session_id: str | None = None) -> None: """Delete proxy files for a session, or all proxies if session_id is None.""" if session_id: target = PROXY_DIR / session_id else: target = PROXY_DIR if target.exists(): shutil.rmtree(target) log.info("Cleaned up proxies: %s", target)