feat(ui): stabilize shared M4.8 review viewer
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.sessions.camera_frame import RecordedCameraFrame, RecordedCameraFrameService
|
||||
from k1link.sessions.media import (
|
||||
RecordedMediaEpoch,
|
||||
RecordedMediaManifest,
|
||||
RecordedMediaSegment,
|
||||
)
|
||||
from k1link.sessions.models import RecordedMediaArtifact, SessionIntegrityError
|
||||
|
||||
_JPEG = b"\xff\xd8\xff\xc0\x00\x07\x08\x00\x01\x00\x01\xff\xd9"
|
||||
|
||||
|
||||
class _Store:
|
||||
def __init__(self, *, block_prepare: bool = False) -> None:
|
||||
self.prepare_calls = 0
|
||||
self.list_calls = 0
|
||||
self.prepare_started = threading.Event()
|
||||
self.release_prepare = threading.Event()
|
||||
if not block_prepare:
|
||||
self.release_prepare.set()
|
||||
self.artifact = RecordedMediaArtifact(
|
||||
session_id="recorded-session",
|
||||
public_source_id="camera",
|
||||
artifact_id="camera-artifact",
|
||||
source_path=Path("/sealed/sensor.camera.right"),
|
||||
byte_length=123,
|
||||
)
|
||||
|
||||
def prepare_replay(self, session_id: str, **_: Any) -> SimpleNamespace:
|
||||
self.prepare_calls += 1
|
||||
self.prepare_started.set()
|
||||
assert self.release_prepare.wait(timeout=2.0)
|
||||
return SimpleNamespace(session_id=session_id)
|
||||
|
||||
def list_recorded_media(self, session_id: str) -> tuple[RecordedMediaArtifact, ...]:
|
||||
self.list_calls += 1
|
||||
return (self.artifact,)
|
||||
|
||||
|
||||
class _Inspector:
|
||||
def __init__(self, manifest: RecordedMediaManifest) -> None:
|
||||
self.manifest = manifest
|
||||
self.inspect_calls = 0
|
||||
|
||||
def inspect(self, artifact: RecordedMediaArtifact, replay: object) -> RecordedMediaManifest:
|
||||
assert artifact.artifact_id == "camera-artifact"
|
||||
assert replay is not None
|
||||
self.inspect_calls += 1
|
||||
return self.manifest
|
||||
|
||||
|
||||
def _manifest(tmp_path: Path) -> RecordedMediaManifest:
|
||||
epoch_path = tmp_path / "epoch-1"
|
||||
segments = tuple(
|
||||
RecordedMediaSegment(
|
||||
sequence=sequence,
|
||||
path=epoch_path / "segments" / f"{sequence:08d}.m4s",
|
||||
byte_length=10,
|
||||
sha256=hashlib.sha256(str(sequence).encode()).hexdigest(),
|
||||
random_access=True,
|
||||
end_time_seconds=float(sequence),
|
||||
)
|
||||
for sequence in range(1, 4)
|
||||
)
|
||||
return RecordedMediaManifest(
|
||||
session_id="recorded-session",
|
||||
public_source_id="camera",
|
||||
artifact_id="camera-artifact",
|
||||
synchronization="recorded",
|
||||
generation_sha256="a" * 64,
|
||||
timeline_start_seconds=0.0,
|
||||
timeline_end_seconds=3.0,
|
||||
byte_length=123,
|
||||
epochs=(
|
||||
RecordedMediaEpoch(
|
||||
ordinal=1,
|
||||
path=epoch_path,
|
||||
init_path=epoch_path / "init.mp4",
|
||||
init_byte_length=10,
|
||||
init_sha256="b" * 64,
|
||||
media_type="video/mp4",
|
||||
timeline_start_seconds=0.0,
|
||||
timeline_end_seconds=3.0,
|
||||
segments=segments,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _service(
|
||||
tmp_path: Path,
|
||||
store: _Store,
|
||||
inspector: _Inspector,
|
||||
*,
|
||||
max_decode_lanes: int = 32,
|
||||
max_source_manifests: int = 32,
|
||||
) -> RecordedCameraFrameService:
|
||||
return RecordedCameraFrameService(
|
||||
store, # type: ignore[arg-type]
|
||||
inspector, # type: ignore[arg-type]
|
||||
ffmpeg_path=Path(sys.executable),
|
||||
cache_root=tmp_path / "cache",
|
||||
max_decode_lanes=max_decode_lanes,
|
||||
max_source_manifests=max_source_manifests,
|
||||
)
|
||||
|
||||
|
||||
def _wait_for_latest(service: RecordedCameraFrameService, ticket: int) -> None:
|
||||
deadline = time.monotonic() + 2.0
|
||||
source_key = ("recorded-session", "sensor.camera.right")
|
||||
while time.monotonic() < deadline:
|
||||
with service._coordination: # noqa: SLF001
|
||||
lane = service._lanes.get(source_key) # noqa: SLF001
|
||||
if lane is not None and lane.latest_ticket == ticket:
|
||||
return
|
||||
time.sleep(0.005)
|
||||
raise AssertionError(f"camera request ticket {ticket} was not registered")
|
||||
|
||||
|
||||
def _frame(epoch: RecordedMediaEpoch, sequence: int) -> RecordedCameraFrame:
|
||||
return RecordedCameraFrame(
|
||||
payload=_JPEG,
|
||||
media_type="image/jpeg",
|
||||
width=1,
|
||||
height=1,
|
||||
sha256=hashlib.sha256(_JPEG).hexdigest(),
|
||||
source_fragment_sha256=epoch.segments[sequence - 1].sha256,
|
||||
)
|
||||
|
||||
|
||||
def test_camera_frame_burst_scans_manifest_once_and_decodes_only_latest(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
store = _Store(block_prepare=True)
|
||||
inspector = _Inspector(_manifest(tmp_path))
|
||||
service = _service(tmp_path, store, inspector)
|
||||
decoded: list[int] = []
|
||||
|
||||
def decode(
|
||||
manifest: RecordedMediaManifest,
|
||||
epoch: RecordedMediaEpoch,
|
||||
sequence: int,
|
||||
) -> RecordedCameraFrame:
|
||||
assert manifest is inspector.manifest
|
||||
decoded.append(sequence)
|
||||
return _frame(epoch, sequence)
|
||||
|
||||
monkeypatch.setattr(service, "_decode", decode)
|
||||
with ThreadPoolExecutor(max_workers=3) as pool:
|
||||
first = pool.submit(service.extract, "recorded-session", 0)
|
||||
assert store.prepare_started.wait(timeout=2.0)
|
||||
second = pool.submit(service.extract, "recorded-session", 1)
|
||||
_wait_for_latest(service, 2)
|
||||
third = pool.submit(service.extract, "recorded-session", 2)
|
||||
_wait_for_latest(service, 3)
|
||||
store.release_prepare.set()
|
||||
|
||||
with pytest.raises(SessionIntegrityError, match="superseded"):
|
||||
first.result(timeout=2.0)
|
||||
with pytest.raises(SessionIntegrityError, match="superseded"):
|
||||
second.result(timeout=2.0)
|
||||
assert third.result(timeout=2.0).source_fragment_sha256 == (
|
||||
inspector.manifest.epochs[0].segments[2].sha256
|
||||
)
|
||||
|
||||
assert store.prepare_calls == 1
|
||||
assert store.list_calls == 1
|
||||
assert inspector.inspect_calls == 1
|
||||
assert decoded == [3]
|
||||
|
||||
|
||||
def test_camera_frame_lane_keeps_only_latest_waiter_behind_active_decode(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
store = _Store()
|
||||
inspector = _Inspector(_manifest(tmp_path))
|
||||
service = _service(tmp_path, store, inspector)
|
||||
decode_started = threading.Event()
|
||||
release_decode = threading.Event()
|
||||
decoded: list[int] = []
|
||||
|
||||
def decode(
|
||||
manifest: RecordedMediaManifest,
|
||||
epoch: RecordedMediaEpoch,
|
||||
sequence: int,
|
||||
) -> RecordedCameraFrame:
|
||||
assert manifest is inspector.manifest
|
||||
decoded.append(sequence)
|
||||
if sequence == 1:
|
||||
decode_started.set()
|
||||
assert release_decode.wait(timeout=2.0)
|
||||
return _frame(epoch, sequence)
|
||||
|
||||
monkeypatch.setattr(service, "_decode", decode)
|
||||
with ThreadPoolExecutor(max_workers=3) as pool:
|
||||
first = pool.submit(service.extract, "recorded-session", 0)
|
||||
assert decode_started.wait(timeout=2.0)
|
||||
second = pool.submit(service.extract, "recorded-session", 1)
|
||||
_wait_for_latest(service, 2)
|
||||
third = pool.submit(service.extract, "recorded-session", 2)
|
||||
_wait_for_latest(service, 3)
|
||||
release_decode.set()
|
||||
|
||||
with pytest.raises(SessionIntegrityError, match="superseded"):
|
||||
first.result(timeout=2.0)
|
||||
with pytest.raises(SessionIntegrityError, match="superseded"):
|
||||
second.result(timeout=2.0)
|
||||
assert third.result(timeout=2.0).width == 1
|
||||
|
||||
assert decoded == [1, 3]
|
||||
assert store.prepare_calls == 1
|
||||
assert inspector.inspect_calls == 1
|
||||
|
||||
|
||||
def test_camera_frame_memory_caches_are_lru_bounded(tmp_path: Path) -> None:
|
||||
store = _Store()
|
||||
inspector = _Inspector(_manifest(tmp_path))
|
||||
service = _service(
|
||||
tmp_path,
|
||||
store,
|
||||
inspector,
|
||||
max_decode_lanes=2,
|
||||
max_source_manifests=2,
|
||||
)
|
||||
|
||||
for ordinal in range(4):
|
||||
source_key = (f"session-{ordinal}", "sensor.camera.right")
|
||||
lane, _ = service._acquire_lane(source_key) # noqa: SLF001
|
||||
service._release_lane(source_key, lane) # noqa: SLF001
|
||||
assert tuple(service._lanes) == ( # noqa: SLF001
|
||||
("session-2", "sensor.camera.right"),
|
||||
("session-3", "sensor.camera.right"),
|
||||
)
|
||||
|
||||
for ordinal in range(3):
|
||||
source_key = (f"session-{ordinal}", "sensor.camera.right")
|
||||
service._source_manifest( # noqa: SLF001
|
||||
source_key,
|
||||
session_id=source_key[0],
|
||||
expected_source_name=source_key[1],
|
||||
)
|
||||
assert tuple(service._source_manifests) == ( # noqa: SLF001
|
||||
("session-1", "sensor.camera.right"),
|
||||
("session-2", "sensor.camera.right"),
|
||||
)
|
||||
|
||||
service._source_manifest( # noqa: SLF001
|
||||
("session-1", "sensor.camera.right"),
|
||||
session_id="session-1",
|
||||
expected_source_name="sensor.camera.right",
|
||||
)
|
||||
service._source_manifest( # noqa: SLF001
|
||||
("session-3", "sensor.camera.right"),
|
||||
session_id="session-3",
|
||||
expected_source_name="sensor.camera.right",
|
||||
)
|
||||
assert tuple(service._source_manifests) == ( # noqa: SLF001
|
||||
("session-1", "sensor.camera.right"),
|
||||
("session-3", "sensor.camera.right"),
|
||||
)
|
||||
assert store.prepare_calls == 4
|
||||
assert inspector.inspect_calls == 4
|
||||
|
||||
|
||||
def test_camera_playback_source_reuses_manifest_and_exposes_exact_segment_clock(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
store = _Store()
|
||||
manifest = _manifest(tmp_path)
|
||||
manifest = RecordedMediaManifest(
|
||||
session_id=manifest.session_id,
|
||||
public_source_id=manifest.public_source_id,
|
||||
artifact_id=manifest.artifact_id,
|
||||
synchronization="host-arrival-best-effort",
|
||||
generation_sha256=manifest.generation_sha256,
|
||||
timeline_start_seconds=manifest.timeline_start_seconds,
|
||||
timeline_end_seconds=manifest.timeline_end_seconds,
|
||||
byte_length=manifest.byte_length,
|
||||
epochs=(
|
||||
RecordedMediaEpoch(
|
||||
ordinal=1,
|
||||
path=manifest.epochs[0].path,
|
||||
init_path=manifest.epochs[0].init_path,
|
||||
init_byte_length=manifest.epochs[0].init_byte_length,
|
||||
init_sha256=manifest.epochs[0].init_sha256,
|
||||
media_type='video/mp4; codecs="avc1.640028"',
|
||||
timeline_start_seconds=0.0,
|
||||
timeline_end_seconds=3.0,
|
||||
segments=manifest.epochs[0].segments,
|
||||
),
|
||||
),
|
||||
)
|
||||
inspector = _Inspector(manifest)
|
||||
service = _service(tmp_path, store, inspector)
|
||||
|
||||
first = service.playback_source("recorded-session")
|
||||
second = service.playback_source("recorded-session")
|
||||
|
||||
assert first == second
|
||||
assert first.segment_count == 3
|
||||
assert first.segment_sha256s == tuple(
|
||||
segment.sha256 for segment in manifest.epochs[0].segments
|
||||
)
|
||||
assert first.segment_start_times_ns == (0, 1_000_000_000, 2_000_000_000)
|
||||
assert store.prepare_calls == 1
|
||||
assert inspector.inspect_calls == 1
|
||||
|
||||
|
||||
def test_camera_frame_rejects_unbounded_memory_cache_configuration(tmp_path: Path) -> None:
|
||||
store = _Store()
|
||||
inspector = _Inspector(_manifest(tmp_path))
|
||||
with pytest.raises(SessionIntegrityError, match="cache bounds"):
|
||||
_service(tmp_path, store, inspector, max_decode_lanes=0)
|
||||
Reference in New Issue
Block a user