feat(compute): add bounded camera job contract
This commit is contained in:
@@ -10,6 +10,7 @@ from .media import (
|
||||
RecordedMediaFile,
|
||||
RecordedMediaInspector,
|
||||
RecordedMediaManifest,
|
||||
inspect_recorded_media_epoch,
|
||||
validate_recorded_media_timeline,
|
||||
)
|
||||
from .models import (
|
||||
@@ -64,6 +65,7 @@ __all__ = [
|
||||
"RecordedMediaFile",
|
||||
"RecordedMediaInspector",
|
||||
"RecordedMediaManifest",
|
||||
"inspect_recorded_media_epoch",
|
||||
"ReplayCommand",
|
||||
"ReplayArtifact",
|
||||
"RecordingExporter",
|
||||
|
||||
@@ -331,6 +331,40 @@ class RecordedMediaInspector:
|
||||
)
|
||||
|
||||
|
||||
def inspect_recorded_media_epoch(
|
||||
epoch: Path,
|
||||
*,
|
||||
expected_source_name: str,
|
||||
origin_epoch_ns: int,
|
||||
origin_monotonic_ns: int,
|
||||
) -> RecordedMediaEpoch:
|
||||
"""Validate one canonical camera epoch for a bounded downstream handoff.
|
||||
|
||||
This is the single-epoch form of :class:`RecordedMediaInspector`. It keeps
|
||||
compute-job packaging on the same digest and ISO-BMFF timing contract as
|
||||
saved-session replay without requiring the worker to see the rest of the
|
||||
observation session.
|
||||
"""
|
||||
|
||||
match = _EPOCH_PATTERN.fullmatch(epoch.name)
|
||||
if match is None:
|
||||
raise SessionIntegrityError("recorded media epoch name is invalid")
|
||||
try:
|
||||
resolved = epoch.resolve(strict=True)
|
||||
source = resolved.parent.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise SessionIntegrityError("recorded media epoch is unavailable") from exc
|
||||
if source.name != expected_source_name or resolved.parent != source:
|
||||
raise SessionIntegrityError("recorded media epoch source is inconsistent")
|
||||
return _read_epoch(
|
||||
resolved,
|
||||
ordinal=int(match.group(1)),
|
||||
expected_source_name=expected_source_name,
|
||||
origin_epoch_ns=origin_epoch_ns,
|
||||
origin_monotonic_ns=origin_monotonic_ns,
|
||||
)
|
||||
|
||||
|
||||
def validate_recorded_media_timeline(
|
||||
manifests: tuple[RecordedMediaManifest, ...],
|
||||
*,
|
||||
@@ -863,19 +897,37 @@ def _read_epoch(
|
||||
segment_count = summary.get("segment_count")
|
||||
if not _non_negative_int(segment_count) or not 1 <= int(segment_count) <= MAX_MEDIA_SEGMENTS:
|
||||
raise SessionIntegrityError("recorded media segment count is invalid")
|
||||
match = _EPOCH_PATTERN.fullmatch(epoch.name)
|
||||
if (
|
||||
match is None
|
||||
or summary.get("codec_epoch") != int(match.group(1))
|
||||
or summary.get("entry_count") != int(segment_count)
|
||||
or summary.get("media_segment_count") != int(segment_count)
|
||||
or summary.get("commit_policy") != "per-segment-fsync"
|
||||
or summary.get("artifacts")
|
||||
!= {
|
||||
"init": "init.mp4",
|
||||
"segments": "segments",
|
||||
"index": "index.jsonl",
|
||||
}
|
||||
):
|
||||
raise SessionIntegrityError("recorded media summary aggregate is inconsistent")
|
||||
|
||||
try:
|
||||
raw_lines = (
|
||||
_read_confined_file(
|
||||
epoch / "index.jsonl",
|
||||
epoch,
|
||||
MAX_MEDIA_INDEX_BYTES,
|
||||
)
|
||||
.decode("utf-8")
|
||||
.splitlines()
|
||||
raw_index = _read_confined_file(
|
||||
epoch / "index.jsonl",
|
||||
epoch,
|
||||
MAX_MEDIA_INDEX_BYTES,
|
||||
)
|
||||
raw_lines = raw_index.decode("utf-8").splitlines()
|
||||
except UnicodeDecodeError as exc:
|
||||
raise SessionIntegrityError("recorded media index is unavailable") from exc
|
||||
expected_index_sha256 = summary.get("index_sha256")
|
||||
if (
|
||||
not isinstance(expected_index_sha256, str)
|
||||
or hashlib.sha256(raw_index).hexdigest() != expected_index_sha256
|
||||
):
|
||||
raise SessionIntegrityError("recorded media index digest changed")
|
||||
if len(raw_lines) != int(segment_count):
|
||||
raise SessionIntegrityError("recorded media index length does not match its summary")
|
||||
entries: list[dict[str, Any]] = []
|
||||
@@ -962,6 +1014,8 @@ def _read_epoch(
|
||||
media_type = _mp4_media_type(init_payload)
|
||||
timing = _mp4_video_timing(init_payload, _Mp4ParseBudget())
|
||||
fragment_timings: list[_Mp4VideoFragmentTiming] = []
|
||||
stream_sha256 = hashlib.sha256(init_payload)
|
||||
valid_bytes = len(init_payload)
|
||||
for segment in segments:
|
||||
payload = _read_confined_file(
|
||||
segment.path,
|
||||
@@ -970,6 +1024,8 @@ def _read_epoch(
|
||||
)
|
||||
if hashlib.sha256(payload).hexdigest() != segment.sha256:
|
||||
raise SessionIntegrityError("recorded media segment digest changed")
|
||||
stream_sha256.update(payload)
|
||||
valid_bytes += len(payload)
|
||||
fragment_timings.append(
|
||||
_mp4_video_fragment_timing(
|
||||
payload,
|
||||
@@ -977,6 +1033,11 @@ def _read_epoch(
|
||||
_Mp4ParseBudget(),
|
||||
)
|
||||
)
|
||||
if (
|
||||
summary.get("valid_bytes") != valid_bytes
|
||||
or summary.get("stream_sha256") != stream_sha256.hexdigest()
|
||||
):
|
||||
raise SessionIntegrityError("recorded media stream aggregate changed")
|
||||
for previous, current in zip(
|
||||
fragment_timings,
|
||||
fragment_timings[1:],
|
||||
|
||||
Reference in New Issue
Block a user