feat(ui): stabilize shared M4.8 review viewer

This commit is contained in:
DCCONSTRUCTIONS
2026-08-24 22:36:08 +03:00
parent 992c5a8b74
commit 4fa6597b18
44 changed files with 6463 additions and 147 deletions
+6 -1
View File
@@ -5,7 +5,11 @@ from .active import (
ActiveSessionLeaseError,
recover_stale_active_session_marker,
)
from .camera_frame import RecordedCameraFrame, RecordedCameraFrameService
from .camera_frame import (
RecordedCameraFrame,
RecordedCameraFrameService,
RecordedCameraPlaybackSource,
)
from .lab_cache import publish_lab_replay_cache
from .media import (
RECORDED_MEDIA_MANIFEST_SCHEMA,
@@ -68,6 +72,7 @@ __all__ = [
"RecordedMediaArtifact",
"RecordedCameraFrame",
"RecordedCameraFrameService",
"RecordedCameraPlaybackSource",
"RECORDED_MEDIA_MANIFEST_SCHEMA",
"RecordedMediaFile",
"RecordedMediaInspector",
+211 -17
View File
@@ -5,6 +5,7 @@ import os
import struct
import subprocess
import threading
from collections import OrderedDict
from contextlib import suppress
from dataclasses import dataclass
from pathlib import Path
@@ -15,6 +16,8 @@ from .store import SessionStore
_MAX_KEYFRAME_DISTANCE = 120
_FFMPEG_TIMEOUT_SECONDS = 15.0
_DEFAULT_MAX_DECODE_LANES = 32
_DEFAULT_MAX_SOURCE_MANIFESTS = 32
@dataclass(frozen=True, slots=True)
@@ -24,6 +27,34 @@ class RecordedCameraFrame:
width: int
height: int
sha256: str
source_fragment_sha256: str | None = None
@dataclass(frozen=True, slots=True)
class RecordedCameraPlaybackSource:
"""Immutable single-epoch camera source for bounded review playback."""
session_id: str
public_source_id: str
artifact_id: str
synchronization: str
generation_sha256: str
timeline_start_seconds: float
timeline_end_seconds: float
byte_length: int
media_type: str
segment_sha256s: tuple[str, ...]
segment_start_times_ns: tuple[int, ...]
@property
def segment_count(self) -> int:
return len(self.segment_sha256s)
@dataclass(slots=True)
class _CameraDecodeLane:
active: bool = False
latest_ticket: int = 0
class RecordedCameraFrameService:
@@ -42,6 +73,8 @@ class RecordedCameraFrameService:
*,
ffmpeg_path: Path,
cache_root: Path,
max_decode_lanes: int = _DEFAULT_MAX_DECODE_LANES,
max_source_manifests: int = _DEFAULT_MAX_SOURCE_MANIFESTS,
) -> None:
resolved_ffmpeg = ffmpeg_path.expanduser().resolve(strict=True)
if not resolved_ffmpeg.is_file() or not os.access(resolved_ffmpeg, os.X_OK):
@@ -53,7 +86,16 @@ class RecordedCameraFrameService:
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()
if max_decode_lanes < 1 or max_source_manifests < 1:
raise SessionIntegrityError("camera frame memory cache bounds are invalid")
self._max_decode_lanes = max_decode_lanes
self._max_source_manifests = max_source_manifests
self._coordination = threading.Condition(threading.Lock())
self._lanes: OrderedDict[tuple[str, str], _CameraDecodeLane] = OrderedDict()
self._source_manifests: OrderedDict[
tuple[str, str],
RecordedMediaManifest,
] = OrderedDict()
def extract(
self,
@@ -64,6 +106,108 @@ class RecordedCameraFrameService:
) -> RecordedCameraFrame:
if frame_index < 0:
raise SessionIntegrityError("camera frame index is invalid")
source_key = (session_id, expected_source_name)
lane, ticket = self._acquire_lane(source_key)
try:
manifest = self._source_manifest(
source_key,
session_id=session_id,
expected_source_name=expected_source_name,
)
self._require_latest(lane, ticket)
epoch, sequence = _frame_location(manifest, frame_index)
cache_key = hashlib.sha256(
(
f"{manifest.generation_sha256}\0{manifest.artifact_id}\0"
f"{expected_source_name}\0{frame_index}\0jpeg-q2-v1"
).encode()
).hexdigest()
cache_path = self._cache_root / f"{cache_key}.jpg"
cached = _read_cached_jpeg(
cache_path,
source_fragment_sha256=epoch.segments[sequence - 1].sha256,
)
if cached is not None:
self._require_latest(lane, ticket)
return cached
self._require_latest(lane, ticket)
frame = self._decode(manifest, epoch, sequence)
_publish_cached_jpeg(cache_path, frame.payload)
self._require_latest(lane, ticket)
return frame
finally:
self._release_lane(source_key, lane)
def playback_source(
self,
session_id: str,
*,
expected_source_name: str = "sensor.camera.right",
) -> RecordedCameraPlaybackSource:
"""Return the generation-bound fMP4 source used by the shared LAB viewer.
LAB playback is intentionally admitted from the same cached immutable
manifest as exact JPEG extraction. The contract is limited to one
codec epoch because LAB frame sequence is a direct one-based segment
sequence; a future multi-epoch source must add an explicit mapping
contract instead of guessing across epoch boundaries.
"""
manifest = self._source_manifest(
(session_id, expected_source_name),
session_id=session_id,
expected_source_name=expected_source_name,
)
if manifest.synchronization != "host-arrival-best-effort" or len(manifest.epochs) != 1:
raise SessionIntegrityError("recorded camera playback source is incompatible")
epoch = manifest.epochs[0]
if (
epoch.ordinal != 1
or epoch.timeline_start_seconds != manifest.timeline_start_seconds
or epoch.timeline_end_seconds != manifest.timeline_end_seconds
or not epoch.media_type.startswith("video/mp4;")
or not epoch.segments
or tuple(segment.sequence for segment in epoch.segments)
!= tuple(range(1, len(epoch.segments) + 1))
):
raise SessionIntegrityError("recorded camera playback epoch is incompatible")
starts_ns: list[int] = []
previous_end_seconds = 0.0
for segment in epoch.segments:
starts_ns.append(
round((epoch.timeline_start_seconds + previous_end_seconds) * 1_000_000_000)
)
previous_end_seconds = segment.end_time_seconds
return RecordedCameraPlaybackSource(
session_id=manifest.session_id,
public_source_id=manifest.public_source_id,
artifact_id=manifest.artifact_id,
synchronization=manifest.synchronization,
generation_sha256=manifest.generation_sha256,
timeline_start_seconds=manifest.timeline_start_seconds,
timeline_end_seconds=manifest.timeline_end_seconds,
byte_length=manifest.byte_length,
media_type=epoch.media_type,
segment_sha256s=tuple(segment.sha256 for segment in epoch.segments),
segment_start_times_ns=tuple(starts_ns),
)
def _source_manifest(
self,
source_key: tuple[str, str],
*,
session_id: str,
expected_source_name: str,
) -> RecordedMediaManifest:
"""Bind one immutable recorded source without rescanning it per frame."""
with self._coordination:
cached = self._source_manifests.get(source_key)
if cached is not None:
self._source_manifests.move_to_end(source_key)
if cached is not None:
return cached
replay = self._store.prepare_replay(session_id, speed=1.0, loop=False)
matches = tuple(
artifact
@@ -74,22 +218,66 @@ class RecordedCameraFrameService:
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._coordination:
bound = self._source_manifests.get(source_key)
if bound is None:
bound = manifest
self._source_manifests[source_key] = manifest
self._source_manifests.move_to_end(source_key)
while len(self._source_manifests) > self._max_source_manifests:
self._source_manifests.popitem(last=False)
return bound
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 _acquire_lane(
self,
source_key: tuple[str, str],
) -> tuple[_CameraDecodeLane, int]:
"""Admit only the newest waiter behind one active source decode."""
with self._coordination:
lane = self._lanes.get(source_key)
if lane is None:
lane = _CameraDecodeLane()
self._lanes[source_key] = lane
self._lanes.move_to_end(source_key)
self._evict_inactive_lanes(exclude=source_key)
lane.latest_ticket += 1
ticket = lane.latest_ticket
self._coordination.notify_all()
while lane.active:
if ticket != lane.latest_ticket:
raise SessionIntegrityError("camera frame request was superseded")
self._coordination.wait()
if ticket != lane.latest_ticket:
raise SessionIntegrityError("camera frame request was superseded")
lane.active = True
return lane, ticket
def _evict_inactive_lanes(self, *, exclude: tuple[str, str] | None = None) -> None:
if len(self._lanes) <= self._max_decode_lanes:
return
for source_key, lane in tuple(self._lanes.items()):
if len(self._lanes) <= self._max_decode_lanes:
break
if source_key != exclude and not lane.active:
del self._lanes[source_key]
def _require_latest(self, lane: _CameraDecodeLane, ticket: int) -> None:
with self._coordination:
if ticket != lane.latest_ticket:
raise SessionIntegrityError("camera frame request was superseded")
def _release_lane(
self,
source_key: tuple[str, str],
lane: _CameraDecodeLane,
) -> None:
with self._coordination:
lane.active = False
if self._lanes.get(source_key) is lane:
self._lanes.move_to_end(source_key)
self._evict_inactive_lanes()
self._coordination.notify_all()
def _decode(
self,
@@ -156,6 +344,7 @@ class RecordedCameraFrameService:
width=width,
height=height,
sha256=digest,
source_fragment_sha256=target.sha256,
)
@@ -196,7 +385,11 @@ def _jpeg_dimensions(payload: bytes) -> tuple[int, int]:
raise SessionIntegrityError("camera frame JPEG dimensions are unavailable")
def _read_cached_jpeg(path: Path) -> RecordedCameraFrame | None:
def _read_cached_jpeg(
path: Path,
*,
source_fragment_sha256: str,
) -> RecordedCameraFrame | None:
try:
if path.is_symlink() or not path.is_file():
return None
@@ -210,6 +403,7 @@ def _read_cached_jpeg(path: Path) -> RecordedCameraFrame | None:
width=width,
height=height,
sha256=hashlib.sha256(payload).hexdigest(),
source_fragment_sha256=source_fragment_sha256,
)