fix(lab): make recorded replay seek-safe
This commit is contained in:
+188
-4
@@ -41,6 +41,7 @@ from k1link.web.session_api import (
|
||||
RecordedPerceptionRequest,
|
||||
RecordedPointColorsRequest,
|
||||
ReplayRequest,
|
||||
_recorded_media_manifest_document,
|
||||
build_session_router,
|
||||
)
|
||||
|
||||
@@ -128,6 +129,11 @@ def make_recorded_h264_fixture(
|
||||
timescale: int = 1_000,
|
||||
sample_duration: int = 500,
|
||||
base_decode_time: int = 0,
|
||||
trex_default_sample_flags: int = 0,
|
||||
tfhd_default_sample_flags: int | None = None,
|
||||
first_sample_flags: int | None = None,
|
||||
sample_flags: int | None = None,
|
||||
fragment_sample_count: int = 1,
|
||||
) -> tuple[bytes, bytes]:
|
||||
def box(box_type: bytes, payload: bytes = b"") -> bytes:
|
||||
return (8 + len(payload)).to_bytes(4, "big") + box_type + payload
|
||||
@@ -156,14 +162,32 @@ def make_recorded_h264_fixture(
|
||||
track_id.to_bytes(4, "big")
|
||||
+ (1).to_bytes(4, "big")
|
||||
+ sample_duration.to_bytes(4, "big")
|
||||
+ b"\x00" * 8,
|
||||
+ b"\x00" * 4
|
||||
+ trex_default_sample_flags.to_bytes(4, "big"),
|
||||
)
|
||||
avcc = box(b"avcC", b"\x01\x64\x00\x28")
|
||||
init = box(b"ftyp", b"isom") + box(b"moov", trak + box(b"mvex", trex) + avcc)
|
||||
|
||||
tfhd = full_box(b"tfhd", track_id.to_bytes(4, "big"), flags=0x020000)
|
||||
tfhd_flags = 0x020000
|
||||
tfhd_payload = track_id.to_bytes(4, "big")
|
||||
if tfhd_default_sample_flags is not None:
|
||||
tfhd_flags |= 0x000020
|
||||
tfhd_payload += tfhd_default_sample_flags.to_bytes(4, "big")
|
||||
tfhd = full_box(b"tfhd", tfhd_payload, flags=tfhd_flags)
|
||||
tfdt = full_box(b"tfdt", base_decode_time.to_bytes(4, "big"))
|
||||
trun = full_box(b"trun", (1).to_bytes(4, "big"))
|
||||
trun_flags = 0
|
||||
trun_payload = fragment_sample_count.to_bytes(4, "big")
|
||||
if first_sample_flags is not None:
|
||||
trun_flags |= 0x000004
|
||||
trun_payload += first_sample_flags.to_bytes(4, "big")
|
||||
if sample_flags is not None:
|
||||
trun_flags |= 0x000400
|
||||
trun_payload += sample_flags.to_bytes(4, "big") * fragment_sample_count
|
||||
trun = full_box(
|
||||
b"trun",
|
||||
trun_payload,
|
||||
flags=trun_flags,
|
||||
)
|
||||
fragment = box(b"moof", box(b"traf", tfhd + tfdt + trun)) + box(b"mdat", b"frame")
|
||||
return init, fragment
|
||||
|
||||
@@ -1644,7 +1668,7 @@ def test_session_router_exposes_opaque_recorded_media_manifest_and_ranges(
|
||||
if_match=f'"sha256:{source["manifest_generation_sha256"]}"',
|
||||
)
|
||||
manifest = json.loads(manifest_response.body)
|
||||
assert manifest["schema_version"] == "missioncore.observation-recorded-media/v3"
|
||||
assert manifest["schema_version"] == "missioncore.observation-recorded-media/v4"
|
||||
assert manifest["source_id"] == source["id"]
|
||||
assert manifest["generation_sha256"] == source["manifest_generation_sha256"]
|
||||
assert manifest["byte_length"] == source["byte_length"]
|
||||
@@ -1659,6 +1683,9 @@ def test_session_router_exposes_opaque_recorded_media_manifest_and_ranges(
|
||||
"timeline_end_seconds": 0.5,
|
||||
"media_type": 'video/mp4; codecs="avc1.640028"',
|
||||
"byte_length": len(init) + len(segment),
|
||||
"segment_count": 1,
|
||||
"random_access_sequences": [1],
|
||||
"segment_end_times_seconds": [0.5],
|
||||
"stream_url": (
|
||||
f"{source['manifest_url'].removesuffix('/manifest')}/epochs/1/recording.mp4"
|
||||
f"?generation={generation}"
|
||||
@@ -1815,6 +1842,136 @@ def test_session_router_exposes_opaque_recorded_media_manifest_and_ranges(
|
||||
assert confined.value.status_code == 409
|
||||
|
||||
|
||||
def test_recorded_media_persists_and_exposes_random_access_sequences(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
init, first = make_recorded_h264_fixture()
|
||||
_, second = make_recorded_h264_fixture(
|
||||
base_decode_time=500,
|
||||
tfhd_default_sample_flags=0x00010000,
|
||||
)
|
||||
_, third = make_recorded_h264_fixture(
|
||||
base_decode_time=1_000,
|
||||
tfhd_default_sample_flags=0x00010000,
|
||||
first_sample_flags=0,
|
||||
)
|
||||
_, fourth = make_recorded_h264_fixture(
|
||||
base_decode_time=1_500,
|
||||
sample_flags=0x00010000,
|
||||
)
|
||||
writer = CameraArchiveWriter(session, "sensor.camera.private-left", 1)
|
||||
writer.append("init", init)
|
||||
for sequence, fragment in enumerate((first, second, third, fourth), start=1):
|
||||
writer.append(
|
||||
"media",
|
||||
fragment,
|
||||
host_epoch_ns=1_000_000_000 + sequence * 500_000_000,
|
||||
host_monotonic_ns=2_000_000_000 + sequence * 500_000_000,
|
||||
)
|
||||
writer.close()
|
||||
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
command = store.prepare_replay(session.name)
|
||||
artifact = store.list_recorded_media(session.name)[0]
|
||||
cache_root = tmp_path / "prepared-media"
|
||||
manifest = RecordedMediaInspector(cache_root).inspect(artifact, command)
|
||||
|
||||
assert [segment.random_access for segment in manifest.epochs[0].segments] == [
|
||||
True,
|
||||
False,
|
||||
True,
|
||||
False,
|
||||
]
|
||||
document = _recorded_media_manifest_document(manifest)
|
||||
assert document["schema_version"] == "missioncore.observation-recorded-media/v4"
|
||||
assert document["epochs"][0]["segment_count"] == 4
|
||||
assert document["epochs"][0]["random_access_sequences"] == [1, 3]
|
||||
assert document["epochs"][0]["segment_end_times_seconds"] == [0.5, 1.0, 1.5, 2.0]
|
||||
assert document["epochs"][0]["segment_end_times_seconds"][-1] == (
|
||||
manifest.epochs[0].timeline_end_seconds - manifest.epochs[0].timeline_start_seconds
|
||||
)
|
||||
|
||||
sidecar = json.loads(next(cache_root.glob("*.json")).read_text(encoding="utf-8"))
|
||||
assert sidecar["schema_version"] == "missioncore.recorded-media-preparation/v3"
|
||||
assert sidecar["manifest"]["schema_version"] == ("missioncore.observation-recorded-media/v4")
|
||||
assert [
|
||||
segment["random_access"] for segment in sidecar["manifest"]["epochs"][0]["segments"]
|
||||
] == [True, False, True, False]
|
||||
assert [
|
||||
segment["end_time_seconds"] for segment in sidecar["manifest"]["epochs"][0]["segments"]
|
||||
] == [0.5, 1.0, 1.5, 2.0]
|
||||
restarted = RecordedMediaInspector(cache_root).inspect(artifact, command)
|
||||
assert [segment.end_time_seconds for segment in restarted.epochs[0].segments] == [
|
||||
0.5,
|
||||
1.0,
|
||||
1.5,
|
||||
2.0,
|
||||
]
|
||||
|
||||
|
||||
def test_recorded_media_rejects_ambiguous_trun_sample_flags() -> None:
|
||||
init, fragment = make_recorded_h264_fixture(
|
||||
first_sample_flags=0,
|
||||
sample_flags=0,
|
||||
)
|
||||
timing = recorded_media_module._mp4_video_timing(
|
||||
init,
|
||||
recorded_media_module._Mp4ParseBudget(),
|
||||
)
|
||||
|
||||
with pytest.raises(SessionIntegrityError, match="sample flags are ambiguous"):
|
||||
recorded_media_module._mp4_video_fragment_timing(
|
||||
fragment,
|
||||
timing,
|
||||
recorded_media_module._Mp4ParseBudget(),
|
||||
)
|
||||
|
||||
|
||||
def test_recorded_media_rejects_multi_sample_video_fragment() -> None:
|
||||
init, fragment = make_recorded_h264_fixture(fragment_sample_count=2)
|
||||
timing = recorded_media_module._mp4_video_timing(
|
||||
init,
|
||||
recorded_media_module._Mp4ParseBudget(),
|
||||
)
|
||||
|
||||
with pytest.raises(SessionIntegrityError, match="exactly one sample"):
|
||||
recorded_media_module._mp4_video_fragment_timing(
|
||||
fragment,
|
||||
timing,
|
||||
recorded_media_module._Mp4ParseBudget(),
|
||||
)
|
||||
|
||||
|
||||
def test_recorded_media_rejects_codec_epoch_without_initial_random_access(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
init, non_random_access = make_recorded_h264_fixture(sample_flags=0x00010000)
|
||||
writer = CameraArchiveWriter(session, "sensor.camera.private-left", 1)
|
||||
writer.append("init", init)
|
||||
writer.append(
|
||||
"media",
|
||||
non_random_access,
|
||||
host_epoch_ns=1_500_000_000,
|
||||
host_monotonic_ns=2_500_000_000,
|
||||
)
|
||||
writer.close()
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
with pytest.raises(SessionIntegrityError, match="does not begin with a random-access"):
|
||||
RecordedMediaInspector().inspect(
|
||||
store.list_recorded_media(session.name)[0],
|
||||
store.prepare_replay(session.name),
|
||||
)
|
||||
|
||||
|
||||
def test_replay_exposes_generation_bound_perception_video_with_native_ranges(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -2373,6 +2530,10 @@ def test_recorded_media_preparation_sidecar_reuses_and_rebuilds_generation(
|
||||
sidecar = sidecars[0]
|
||||
assert stat.S_IMODE(sidecar.stat().st_mode) == 0o600
|
||||
assert str(session) not in sidecar.read_text(encoding="utf-8")
|
||||
prepared = json.loads(sidecar.read_text(encoding="utf-8"))
|
||||
assert prepared["schema_version"] == "missioncore.recorded-media-preparation/v3"
|
||||
assert prepared["manifest"]["epochs"][0]["segments"][0]["random_access"] is True
|
||||
assert prepared["manifest"]["epochs"][0]["segments"][0]["end_time_seconds"] == 0.5
|
||||
|
||||
def forbidden_reparse(*_args: object, **_kwargs: object) -> None:
|
||||
raise AssertionError("restart must reuse the prepared media sidecar")
|
||||
@@ -2383,6 +2544,7 @@ def test_recorded_media_preparation_sidecar_reuses_and_rebuilds_generation(
|
||||
restarted = RecordedMediaInspector(cache_root).inspect(artifact, command)
|
||||
assert restarted.generation_sha256 == initial.generation_sha256
|
||||
assert restarted.timeline_end_seconds == initial.timeline_end_seconds
|
||||
assert [segment.end_time_seconds for segment in restarted.epochs[0].segments] == [0.5]
|
||||
|
||||
original_read_manifest = recorded_media_module._read_manifest
|
||||
reparses = 0
|
||||
@@ -2392,6 +2554,28 @@ def test_recorded_media_preparation_sidecar_reuses_and_rebuilds_generation(
|
||||
reparses += 1
|
||||
return original_read_manifest(*args, **kwargs)
|
||||
|
||||
old_body = json.loads(json.dumps(prepared))
|
||||
old_body.pop("checksum_sha256")
|
||||
old_body["schema_version"] = "missioncore.recorded-media-preparation/v2"
|
||||
old_body["manifest"]["schema_version"] = "missioncore.observation-recorded-media/v3"
|
||||
for epoch in old_body["manifest"]["epochs"]:
|
||||
for old_segment in epoch["segments"]:
|
||||
old_segment.pop("end_time_seconds")
|
||||
old_checksum = hashlib.sha256(recorded_media_module._canonical_json(old_body)).hexdigest()
|
||||
sidecar.write_bytes(
|
||||
recorded_media_module._canonical_json({**old_body, "checksum_sha256": old_checksum})
|
||||
)
|
||||
with monkeypatch.context() as context:
|
||||
context.setattr(recorded_media_module, "_read_manifest", counted_reparse)
|
||||
migrated = RecordedMediaInspector(cache_root).inspect(artifact, command)
|
||||
assert migrated.generation_sha256 == initial.generation_sha256
|
||||
assert reparses == 1
|
||||
migrated_sidecar = json.loads(sidecar.read_text(encoding="utf-8"))
|
||||
assert migrated_sidecar["schema_version"] == "missioncore.recorded-media-preparation/v3"
|
||||
assert migrated_sidecar["manifest"]["epochs"][0]["segments"][0]["random_access"] is True
|
||||
assert migrated_sidecar["manifest"]["epochs"][0]["segments"][0]["end_time_seconds"] == 0.5
|
||||
|
||||
reparses = 0
|
||||
sidecar.write_bytes(b"{corrupt")
|
||||
with monkeypatch.context() as context:
|
||||
context.setattr(recorded_media_module, "_read_manifest", counted_reparse)
|
||||
|
||||
Reference in New Issue
Block a user