fix(lab): запускать RAV004 с первого кадра

This commit is contained in:
DCCONSTRUCTIONS
2026-08-30 14:33:17 +03:00
parent 35daf73d5c
commit 9c5259dbc9
3 changed files with 152 additions and 1 deletions
@@ -34,7 +34,9 @@ import {
RECORDED_BASE_POINT_COLOR_KEY,
canPublishRecordedPlaybackController,
createRecordedAutoplayGate,
createRecordedInitialSeekGate,
createRecordedOpenWatchdog,
shouldReapplyRecordedBlueprint,
} from "../core/observation/recordedRerunLifecycle";
import type {
RecordedPerceptionLayers,
@@ -59,6 +61,7 @@ export {
attemptRecordedAutoplay,
canPublishRecordedPlaybackController,
createRecordedAutoplayGate,
createRecordedInitialSeekGate,
createRecordedOpenWatchdog,
isRecordedPlaybackFullyBuffered,
isRecordedPlaybackPresentationReady,
@@ -69,6 +72,7 @@ export {
recordedPlaybackRangeWhenReady,
recordedPointColorKey,
rerunPresentationStatus,
shouldReapplyRecordedBlueprint,
type RecordedPlaybackBufferState,
} from "../core/observation/recordedRerunLifecycle";
export {
@@ -828,6 +832,7 @@ export function RerunViewport({
let publishPlaybackBufferState: (() => void) | null = null;
let playbackState: RerunPlaybackState | null = null;
const recordedAutoplay = createRecordedAutoplayGate();
const recordedInitialSeek = createRecordedInitialSeekGate();
let blueprintChannel: RerunBlueprintChannel | null = null;
let perceptionChannel: RerunBlueprintChannel | null = null;
let perceptionReceiver: RerunNativeReceiver | null = null;
@@ -1144,9 +1149,25 @@ export function RerunViewport({
}) => {
if (
disposed ||
recordingOpened ||
(isRecordedSource && event.application_id !== "nodedc_mission_core_recorded")
) return;
if (recordingOpened) {
const identity = recordedIdentityRef.current;
if (shouldReapplyRecordedBlueprint(
recordingOpened,
isRecordedSource,
recordedBlueprintUrl !== null,
identity,
event,
)) {
// A LAB sidecar is a second upstream receiver for the same
// recording store. Rerun re-opens that store only after the
// sidecar has been decoded, so reapply the canonical blueprint
// at this exact boundary instead of racing it during download.
setBlueprintChannelRevision((revision) => revision + 1);
}
return;
}
recordingOpened = true;
if (!isRecordedSource) {
// Store discovery only establishes a candidate. Admission is
@@ -1314,6 +1335,23 @@ export function RerunViewport({
playing = true;
}
}
if (!autoplayWhenReady && !followLive && readyToRender) {
recordedInitialSeek.attempt(
viewerStartResolved,
recordedBuffer.fullyBuffered,
presentationReady,
rangeNs,
(initialStartNs) => {
viewer.set_current_time(event.recording_id, timeline, initialStartNs);
currentNs = initialStartNs;
},
initialPlaybackStartSeconds === undefined
? expectedTimelineStartSeconds === undefined
? undefined
: expectedTimelineStartSeconds * 1_000_000_000
: initialPlaybackStartSeconds * 1_000_000_000,
);
}
emitPlayback({
rangeNs: followLive
? rangeNs
@@ -196,6 +196,20 @@ export function isRecordedPlaybackFullyBuffered(
return recordedPlaybackBufferState(rangeNs, expectedTimelineEndSeconds).fullyBuffered;
}
export function shouldReapplyRecordedBlueprint(
recordingOpened: boolean,
recordedSource: boolean,
hasBlueprint: boolean,
identity: { applicationId: string; recordingId: string } | null,
event: { application_id: string; recording_id: string },
): boolean {
return recordingOpened
&& recordedSource
&& hasBlueprint
&& identity?.applicationId === event.application_id
&& identity.recordingId === event.recording_id;
}
export function attemptRecordedAutoplay(
seekToStart: () => void,
startPlaying: () => void,
@@ -209,6 +223,49 @@ export function attemptRecordedAutoplay(
}
}
export function createRecordedInitialSeekGate(): {
attempt: (
viewerStarted: boolean,
fullyBuffered: boolean,
presentationReady: boolean,
rangeNs: { min: number; max: number } | null,
seekToStart: (startNs: number) => void,
preferredStartNs?: number,
) => boolean;
attempted: () => boolean;
} {
let consumed = false;
return {
attempt(
viewerStarted,
fullyBuffered,
presentationReady,
rangeNs,
seekToStart,
preferredStartNs,
) {
if (
consumed
|| !viewerStarted
|| !fullyBuffered
|| !presentationReady
|| !isUsableRecordedPlaybackRange(rangeNs)
) return false;
consumed = true;
const startNs = Number.isFinite(preferredStartNs)
? Math.min(Math.max(preferredStartNs as number, rangeNs.min), rangeNs.max)
: rangeNs.min;
try {
seekToStart(startNs);
return true;
} catch {
return false;
}
},
attempted: () => consumed,
};
}
export function createRecordedAutoplayGate(): {
attempt: (
viewerStarted: boolean,