"""Browser-compatible video preview cache. The collector and LeRobot dataset may use HEVC (including 12-bit depth streams), but the Review page must not depend on the browser being able to decode those codecs. This module creates a local, derived H.264 preview on first use and reuses it for subsequent range requests. The source file is never changed. The preview is a disposable local cache; the authoritative recording and the lossless metric-depth asset remain in the configured storage directory. """ from __future__ import annotations import hashlib import json import os import shutil import subprocess import threading import time from pathlib import Path from app.config import settings _LOCKS: dict[str, threading.Lock] = {} _LOCKS_GUARD = threading.Lock() _FASTSTART_CACHE: dict[str, bool] = {} _FASTSTART_CACHE_LOCK = threading.Lock() def _cache_root() -> Path: root = settings.upload_staging_root / "browser-preview" root.mkdir(parents=True, exist_ok=True) return root def _cache_key(source: Path) -> str: stat = source.stat() fingerprint = f"{source.resolve()}:{stat.st_size}:{stat.st_mtime_ns}" return hashlib.sha256(fingerprint.encode("utf-8")).hexdigest() def _lock_for(key: str) -> threading.Lock: with _LOCKS_GUARD: return _LOCKS.setdefault(key, threading.Lock()) # ── Local source mirror (depth streams) ────────────────────────────── # Depth videos live on the SSHFS mount, where random reads for seek/decode # cost seconds. A bit-identical local copy turns every later decode into a # local-disk read. This is a disposable transport cache of the SOURCE file # (never a derived/colorized asset); the authoritative stream stays in the # configured storage directory. _MIRROR_MAX_BYTES = 8 * 1024 ** 3 _MIRROR_INFLIGHT: set[str] = set() _MIRROR_INFLIGHT_GUARD = threading.Lock() def _mirror_root() -> Path: root = settings.upload_staging_root / "depth-mirror" root.mkdir(parents=True, exist_ok=True) return root def _prune_mirror(keep: Path) -> None: """Remove oldest mirror files beyond the byte budget (LRU by mtime).""" try: files = [p for p in _mirror_root().iterdir() if p.is_file() and p.suffix == ".mp4"] except OSError: return total = sum(p.stat().st_size for p in files if p != keep) + ( keep.stat().st_size if keep.is_file() else 0) if total <= _MIRROR_MAX_BYTES: return for path in sorted(files, key=lambda p: p.stat().st_mtime_ns): if path == keep or total <= _MIRROR_MAX_BYTES: continue try: total -= path.stat().st_size path.unlink(missing_ok=True) except OSError: pass def _copy_mirror(source: Path, destination: Path, key: str) -> None: """Background copy to a temp file, then atomic rename.""" temporary = destination.with_suffix(".part") temporary.unlink(missing_ok=True) try: shutil.copy2(source, temporary) if not temporary.is_file() or temporary.stat().st_size <= 0: raise RuntimeError("mirror copy produced an empty file") os.replace(temporary, destination) _prune_mirror(destination) except (OSError, RuntimeError): temporary.unlink(missing_ok=True) finally: with _MIRROR_INFLIGHT_GUARD: _MIRROR_INFLIGHT.discard(key) def _write_turbo_cube(path: Path) -> None: """Write the static 4096-entry 1D Turbo cube used by the transcode. 4096 entries keep the range mapping at 12-bit precision: a 256-entry LUT evaluated after an 8-bit intermediate caused visible banding (only ~58 distinct color levels survived the chain instead of ~173). """ if path.is_file(): return from app.lerobot_v21 import turbo_lut lut = turbo_lut(4096) lines = ["TITLE Turbo", "LUT_1D_SIZE 4096", "", "DOMAIN_MIN 0 0 0", "DOMAIN_MAX 1 1 1", ""] for r, g, b in lut: lines.append(f"{r / 255.0:.6f} {g / 255.0:.6f} {b / 255.0:.6f}") path.write_text("\n".join(lines) + "\n", encoding="ascii") def _depth_stats_path(source: Path) -> Path: key = _cache_key(source) return _cache_root() / f"depth-{key}.stats.json" def read_depth_stats(source: Path) -> dict | None: """Disk-persisted q01/q99 bounds for ``source`` (same fingerprint key). Written once by :func:`_ensure_depth_stats` during preview generation; survives restarts so neither the stats endpoint nor a re-transcode ever pays a second full-stream decode for the same source file. """ try: path = _depth_stats_path(source) if not path.is_file(): return None data = json.loads(path.read_text(encoding="utf-8")) if (isinstance(data, dict) and isinstance(data.get("q01"), (int, float)) and isinstance(data.get("q99"), (int, float)) and float(data["q99"]) > float(data["q01"])): return data except (OSError, ValueError): pass return None def _histogram_depth_stats(source: Path) -> dict | None: """Single-decode q01/q99 code bounds via a streaming 4096-bin histogram. The stream is read as raw gray12le codes and histogrammed in O(1) memory; percentiles are exact over every decoded pixel. The previous ``select=`` sampling pass decoded the whole stream anyway while keeping only a fraction of the frames, so nothing is lost. """ import numpy as np ffmpeg = shutil.which("ffmpeg") if not ffmpeg: return None hist = np.zeros(4096, dtype=np.int64) proc = subprocess.Popen( [ffmpeg, "-v", "error", "-i", str(source), "-map", "0:v:0", "-f", "rawvideo", "-pix_fmt", "gray12le", "-"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) try: while True: chunk = proc.stdout.read(8 * 1024 * 1024) if not chunk: break values = np.frombuffer(chunk, dtype=" dict | None: """Return q01/q99 for ``source``, computed once and persisted to disk. ``source`` is the authoritative path used for the cache fingerprint; ``local`` (the transport mirror) is decoded when it is ready so the pass never competes with an SSHFS random read. """ cached = read_depth_stats(source) if cached is not None: return cached with _lock_for(f"stats:{_cache_key(source)}"): cached = read_depth_stats(source) if cached is not None: return cached stats = _histogram_depth_stats(local or source) if stats is None: return None try: _depth_stats_path(source).write_text( json.dumps(stats), encoding="utf-8") except OSError: pass return stats def depth_preview_exists(source: Path) -> bool: """Cheap cache-hit check for prewarm scheduling (no transcode).""" try: destination = _cache_root() / f"depth-{_cache_key(Path(source))}.mp4" return destination.is_file() and destination.stat().st_size > 0 except OSError: return False def ensure_depth_h264_preview(source: Path, q01: float = 0.0, q99: float = 4095.0) -> Path: """Generate (once) an 8-bit Turbo-colored H.264 preview of a depth stream. The browser display pipeline already quantizes codes into an 8-bit Turbo ramp; this bakes the identical mapping (same clamp/scale math, same colormap) into a disposable local MP4 so the review page plays it through a native ``