from __future__ import annotations import hashlib import os import struct import subprocess import threading from contextlib import suppress from dataclasses import dataclass from pathlib import Path from .media import RecordedMediaEpoch, RecordedMediaInspector, RecordedMediaManifest from .models import SessionIntegrityError from .store import SessionStore _MAX_KEYFRAME_DISTANCE = 120 _FFMPEG_TIMEOUT_SECONDS = 15.0 @dataclass(frozen=True, slots=True) class RecordedCameraFrame: payload: bytes media_type: str width: int height: int sha256: str class RecordedCameraFrameService: """Decode one exact archived camera frame without preparing the full video. Canonical camera archives contain one video sample per fMP4 fragment. The service validates the sealed manifest, walks back only to the preceding IDR fragment and gives that bounded GOP to ffmpeg. This keeps CAMERA review independent from the 4489-row VIDEO overlay and full-player preparation. """ def __init__( self, store: SessionStore, inspector: RecordedMediaInspector, *, ffmpeg_path: Path, cache_root: Path, ) -> None: resolved_ffmpeg = ffmpeg_path.expanduser().resolve(strict=True) if not resolved_ffmpeg.is_file() or not os.access(resolved_ffmpeg, os.X_OK): raise SessionIntegrityError("ffmpeg is unavailable for camera frame review") self._store = store self._inspector = inspector self._ffmpeg_path = resolved_ffmpeg self._cache_root = cache_root.expanduser().absolute() self._cache_root.mkdir(parents=True, exist_ok=True) if self._cache_root.is_symlink() or not self._cache_root.is_dir(): raise SessionIntegrityError("camera frame cache root is invalid") self._lock = threading.Lock() def extract( self, session_id: str, frame_index: int, *, expected_source_name: str = "sensor.camera.right", ) -> RecordedCameraFrame: if frame_index < 0: raise SessionIntegrityError("camera frame index is invalid") replay = self._store.prepare_replay(session_id, speed=1.0, loop=False) matches = tuple( artifact for artifact in self._store.list_recorded_media(session_id) if artifact.source_path.name == expected_source_name ) if len(matches) != 1: raise SessionIntegrityError("recorded camera source is unavailable") artifact = matches[0] manifest = self._inspector.inspect(artifact, replay) epoch, sequence = _frame_location(manifest, frame_index) cache_key = hashlib.sha256( ( f"{manifest.generation_sha256}\0{artifact.artifact_id}\0" f"{expected_source_name}\0{frame_index}\0jpeg-q2-v1" ).encode() ).hexdigest() cache_path = self._cache_root / f"{cache_key}.jpg" with self._lock: cached = _read_cached_jpeg(cache_path) if cached is not None: return cached frame = self._decode(manifest, epoch, sequence) _publish_cached_jpeg(cache_path, frame.payload) return frame def _decode( self, manifest: RecordedMediaManifest, epoch: RecordedMediaEpoch, sequence: int, ) -> RecordedCameraFrame: target = self._inspector.get_segment(manifest, epoch.ordinal, sequence) fragments: list[bytes] = [target.payload] key_sequence = sequence while not _fragment_is_sync(fragments[0]): key_sequence -= 1 if key_sequence < 1 or sequence - key_sequence > _MAX_KEYFRAME_DISTANCE: raise SessionIntegrityError("camera frame has no bounded sync fragment") previous = self._inspector.get_segment( manifest, epoch.ordinal, key_sequence, ) fragments.insert(0, previous.payload) init = self._inspector.get_init(manifest, epoch.ordinal) select_index = sequence - key_sequence try: completed = subprocess.run( [ str(self._ffmpeg_path), "-hide_banner", "-loglevel", "error", "-f", "mp4", "-i", "pipe:0", "-vf", f"select=eq(n\\,{select_index})", "-fps_mode", "passthrough", "-frames:v", "1", "-f", "image2pipe", "-c:v", "mjpeg", "-q:v", "2", "pipe:1", ], input=b"".join((init.payload, *fragments)), capture_output=True, timeout=_FFMPEG_TIMEOUT_SECONDS, check=False, ) except (OSError, subprocess.TimeoutExpired) as exc: raise SessionIntegrityError("camera frame decoder failed") from exc if completed.returncode != 0: detail = completed.stderr.decode("utf-8", errors="replace").strip()[-500:] raise SessionIntegrityError(f"camera frame decoder rejected sealed media: {detail}") width, height = _jpeg_dimensions(completed.stdout) digest = hashlib.sha256(completed.stdout).hexdigest() return RecordedCameraFrame( payload=completed.stdout, media_type="image/jpeg", width=width, height=height, sha256=digest, ) def _frame_location( manifest: RecordedMediaManifest, frame_index: int, ) -> tuple[RecordedMediaEpoch, int]: remaining = frame_index for epoch in manifest.epochs: if remaining < len(epoch.segments): return epoch, remaining + 1 remaining -= len(epoch.segments) raise SessionIntegrityError("camera frame is outside the recorded media manifest") def _fragment_is_sync(payload: bytes) -> bool: tfhd_default_flags: int | None = None sample_flags: int | None = None sample_count: int | None = None for box_type, body in _walk_boxes(payload): if box_type == b"tfhd": if len(body) < 8: raise SessionIntegrityError("camera fragment tfhd is truncated") flags = int.from_bytes(body[1:4], "big") offset = 8 for mask, size in ((0x000001, 8), (0x000002, 4), (0x000008, 4), (0x000010, 4)): if flags & mask: offset += size if flags & 0x000020: if offset + 4 > len(body): raise SessionIntegrityError("camera fragment default flags are truncated") tfhd_default_flags = struct.unpack_from(">I", body, offset)[0] elif box_type == b"trun": if len(body) < 8: raise SessionIntegrityError("camera fragment trun is truncated") flags = int.from_bytes(body[1:4], "big") sample_count = struct.unpack_from(">I", body, 4)[0] if sample_count != 1: raise SessionIntegrityError("camera fragment must contain exactly one sample") offset = 8 if flags & 0x000001: offset += 4 if flags & 0x000004: if offset + 4 > len(body): raise SessionIntegrityError("camera fragment first flags are truncated") sample_flags = struct.unpack_from(">I", body, offset)[0] offset += 4 per_sample_sizes = ( (0x000100, 4), (0x000200, 4), (0x000400, 4), (0x000800, 4), ) for mask, size in per_sample_sizes: if flags & mask: if offset + size > len(body): raise SessionIntegrityError("camera fragment sample data is truncated") if mask == 0x000400: sample_flags = struct.unpack_from(">I", body, offset)[0] offset += size if sample_count != 1: raise SessionIntegrityError("camera fragment has no unique video sample") effective_flags = sample_flags if sample_flags is not None else tfhd_default_flags if effective_flags is None: raise SessionIntegrityError("camera fragment sample flags are unavailable") return (effective_flags & 0x00010000) == 0 def _walk_boxes(payload: bytes): containers = {b"moof", b"traf"} pending = [(0, len(payload))] boxes = 0 while pending: start, end = pending.pop() offset = start while offset + 8 <= end: boxes += 1 if boxes > 64: raise SessionIntegrityError("camera fragment box budget was exceeded") size = struct.unpack_from(">I", payload, offset)[0] box_type = payload[offset + 4 : offset + 8] header = 8 if size == 1: if offset + 16 > end: raise SessionIntegrityError("camera fragment extended box is truncated") size = struct.unpack_from(">Q", payload, offset + 8)[0] header = 16 elif size == 0: size = end - offset if size < header or offset + size > end: raise SessionIntegrityError("camera fragment box size is invalid") body_start = offset + header body_end = offset + size yield box_type, payload[body_start:body_end] if box_type in containers: pending.append((body_start, body_end)) offset = body_end if offset != end: raise SessionIntegrityError("camera fragment box boundary is invalid") def _jpeg_dimensions(payload: bytes) -> tuple[int, int]: if len(payload) < 4 or payload[:2] != b"\xff\xd8" or payload[-2:] != b"\xff\xd9": raise SessionIntegrityError("camera frame decoder returned an invalid JPEG") offset = 2 while offset + 4 <= len(payload): if payload[offset] != 0xFF: offset += 1 continue marker = payload[offset + 1] offset += 2 if marker in {0xD8, 0xD9} or 0xD0 <= marker <= 0xD7: continue if offset + 2 > len(payload): break length = struct.unpack_from(">H", payload, offset)[0] if length < 2 or offset + length > len(payload): break if marker in {0xC0, 0xC1, 0xC2} and length >= 7: height, width = struct.unpack_from(">HH", payload, offset + 3) if width > 0 and height > 0: return width, height offset += length raise SessionIntegrityError("camera frame JPEG dimensions are unavailable") def _read_cached_jpeg(path: Path) -> RecordedCameraFrame | None: try: if path.is_symlink() or not path.is_file(): return None payload = path.read_bytes() width, height = _jpeg_dimensions(payload) except (OSError, SessionIntegrityError): return None return RecordedCameraFrame( payload=payload, media_type="image/jpeg", width=width, height=height, sha256=hashlib.sha256(payload).hexdigest(), ) def _publish_cached_jpeg(path: Path, payload: bytes) -> None: temporary = path.parent / f".{path.name}.{os.getpid()}.{threading.get_ident()}.tmp" try: with temporary.open("xb") as stream: stream.write(payload) stream.flush() os.fsync(stream.fileno()) os.replace(temporary, path) except OSError as exc: raise SessionIntegrityError("camera frame cache could not be published") from exc finally: with suppress(FileNotFoundError): temporary.unlink()