refactor(lab): restore canonical RAV004 replay

This commit is contained in:
DCCONSTRUCTIONS
2026-08-30 01:22:44 +03:00
parent 74da6437e9
commit f1cbe0061a
21 changed files with 1181 additions and 719 deletions
@@ -182,6 +182,27 @@ function recordedMediaTimeRangesContain(
return false;
}
export function recordedMediaTimestampStallRecoveryTarget(
currentSeconds: number,
bufferedRanges: readonly (readonly [number, number])[],
skipSeconds = 0.18,
): number | null {
if (!Number.isFinite(currentSeconds) || !Number.isFinite(skipSeconds) || skipSeconds <= 0) {
return null;
}
for (const [startSeconds, endSeconds] of bufferedRanges) {
if (
!Number.isFinite(startSeconds)
|| !Number.isFinite(endSeconds)
|| currentSeconds < startSeconds - 0.05
|| currentSeconds > endSeconds
) continue;
const targetSeconds = Math.min(currentSeconds + skipSeconds, endSeconds - 0.05);
return targetSeconds >= currentSeconds + 0.04 ? targetSeconds : null;
}
return null;
}
export function recordedMediaPresentationState(
state: "loading" | "ready" | "error",
readyGeneration: string | null,
@@ -783,6 +804,7 @@ export function RecordedFmp4Player({
onPlayingRejected,
playbackAuthority = "media",
playbackTransport = "segmented",
recoverTimestampStalls = false,
}: {
source: ObservationSourceDescriptor;
playback?: RecordedObservationPlayback | null;
@@ -797,6 +819,7 @@ export function RecordedFmp4Player({
onPlayingRejected?: () => void;
playbackAuthority?: "media" | "host";
playbackTransport?: "segmented" | "epoch-stream";
recoverTimestampStalls?: boolean;
}) {
const videoRef = useRef<HTMLVideoElement>(null);
const onAdmissionChangeRef = useRef(onAdmissionChange);
@@ -1450,6 +1473,51 @@ export function RecordedFmp4Player({
visualState,
]);
useEffect(() => {
const video = videoRef.current;
if (!video || !recoverTimestampStalls || !playback?.playing || visualState !== "ready") {
return;
}
let lastSeconds = video.currentTime;
let lastProgressAtMs = performance.now();
const interval = window.setInterval(() => {
if (
!playbackPlayingRef.current
|| video.paused
|| video.ended
|| video.seeking
|| video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA
) {
lastSeconds = video.currentTime;
lastProgressAtMs = performance.now();
return;
}
const nowMs = performance.now();
if (video.currentTime >= lastSeconds + 0.02) {
lastSeconds = video.currentTime;
lastProgressAtMs = nowMs;
return;
}
if (nowMs - lastProgressAtMs < 1_250) return;
const bufferedRanges = Array.from(
{ length: video.buffered.length },
(_, index) => [video.buffered.start(index), video.buffered.end(index)] as const,
);
const targetSeconds = recordedMediaTimestampStallRecoveryTarget(
video.currentTime,
bufferedRanges,
);
lastProgressAtMs = nowMs;
if (targetSeconds === null) return;
// Field recordings can contain non-monotonic or corrupt H.264 timestamps.
// If decoded time is frozen despite proven buffered media ahead, skip only
// the broken timestamp interval and return authority to the media clock.
video.currentTime = targetSeconds;
lastSeconds = targetSeconds;
}, 250);
return () => window.clearInterval(interval);
}, [playback?.playing, recoverTimestampStalls, visualState]);
useEffect(() => {
const video = videoRef.current;
if (!video || !segmented || !playback?.playing || visualState !== "ready") return;
@@ -39,6 +39,7 @@ export function RecordedEvidenceVideoScene({
onAdmissionChange,
playbackAuthority = "media",
playbackTransport = "segmented",
recoverTimestampStalls = false,
}: {
source: ObservationSourceDescriptor;
playback: RecordedObservationPlayback;
@@ -56,6 +57,7 @@ export function RecordedEvidenceVideoScene({
onAdmissionChange?: (state: RecordedCameraAdmissionState) => void;
playbackAuthority?: "media" | "host";
playbackTransport?: "segmented" | "epoch-stream";
recoverTimestampStalls?: boolean;
}) {
const generation = source.delivery?.kind === "recorded-fmp4-manifest"
? source.delivery.manifestGenerationSha256
@@ -63,12 +65,29 @@ export function RecordedEvidenceVideoScene({
const [admissionPhase, setAdmissionPhase] = useState<RecordedCameraAdmissionState["phase"]>(
"loading",
);
useEffect(() => setAdmissionPhase("loading"), [generation, source.id]);
const [presentedSeconds, setPresentedSeconds] = useState<number | null>(null);
useEffect(() => {
setAdmissionPhase("loading");
setPresentedSeconds(null);
}, [generation, source.id]);
const handleAdmissionChange = (next: RecordedCameraAdmissionState) => {
setAdmissionPhase(next.phase);
onAdmissionChange?.(next);
};
const sourceReady = admissionPhase === "ready";
const overlaysPresented = sourceReady
&& presentedSeconds !== null
&& Math.abs(presentedSeconds - playback.currentSeconds) <= 0.25;
const handlePlaybackChange = (next: RecordedObservationPlayback) => {
setPresentedSeconds(next.currentSeconds);
// During a paused operator seek the existing media element can emit its old
// timestamp while the requested MSE window is being rebuilt. That stale
// callback must not undo the host target before the decoder reaches it.
if (!playback.playing && Math.abs(next.currentSeconds - playback.currentSeconds) > 0.35) {
return;
}
onPlaybackChange?.(next);
};
return (
<div className="recorded-evidence-video-scene">
<RecordedFmp4Player
@@ -78,27 +97,28 @@ export function RecordedEvidenceVideoScene({
prepare
segmentSequence={segmentSequence}
segmentCount={segmentCount}
onPlaybackChange={onPlaybackChange}
onPlaybackChange={handlePlaybackChange}
onPlayingRejected={onPlayingRejected}
onAdmissionChange={handleAdmissionChange}
playbackAuthority={playbackAuthority}
playbackTransport={playbackTransport}
recoverTimestampStalls={recoverTimestampStalls}
/>
{sourceReady && semanticOverlay ? (
{overlaysPresented && semanticOverlay ? (
<RecordedEvidenceSemanticMaskOverlay
{...semanticOverlay}
imageWidth={imageWidth}
imageHeight={imageHeight}
/>
) : null}
{sourceReady && pointCloudOverlay ? (
{overlaysPresented && pointCloudOverlay ? (
<RecordedEvidencePointCloudOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
overlay={pointCloudOverlay}
/>
) : null}
{sourceReady ? (
{overlaysPresented ? (
<RecordedEvidenceBoxOverlay
imageWidth={imageWidth}
imageHeight={imageHeight}
@@ -130,9 +130,9 @@ export function useRecordedEvidencePlayback(
const synchronize = useCallback((next: RecordedObservationPlayback) => {
if (!validRange(range) || !Number.isFinite(next.currentSeconds)) return;
// The canonical LAB host clock is authoritative. Native media callbacks
// are observational only in this mode: a stalled decoder must never stop
// the common timeline or let an independently playing spatial view drift.
// Animation-clock mode is retained only for non-media diagnostics. A
// recorded LAB with video uses the external media clock so spatial and
// overlays never advance past the frame the decoder actually presented.
if (clock === "animation") return;
setPlayback((current) => synchronizeRecordedEvidencePlayback(current, next, range));
}, [clock, range]);