fix(lab): запускать RAV004 с первого кадра
This commit is contained in:
@@ -34,7 +34,9 @@ import {
|
|||||||
RECORDED_BASE_POINT_COLOR_KEY,
|
RECORDED_BASE_POINT_COLOR_KEY,
|
||||||
canPublishRecordedPlaybackController,
|
canPublishRecordedPlaybackController,
|
||||||
createRecordedAutoplayGate,
|
createRecordedAutoplayGate,
|
||||||
|
createRecordedInitialSeekGate,
|
||||||
createRecordedOpenWatchdog,
|
createRecordedOpenWatchdog,
|
||||||
|
shouldReapplyRecordedBlueprint,
|
||||||
} from "../core/observation/recordedRerunLifecycle";
|
} from "../core/observation/recordedRerunLifecycle";
|
||||||
import type {
|
import type {
|
||||||
RecordedPerceptionLayers,
|
RecordedPerceptionLayers,
|
||||||
@@ -59,6 +61,7 @@ export {
|
|||||||
attemptRecordedAutoplay,
|
attemptRecordedAutoplay,
|
||||||
canPublishRecordedPlaybackController,
|
canPublishRecordedPlaybackController,
|
||||||
createRecordedAutoplayGate,
|
createRecordedAutoplayGate,
|
||||||
|
createRecordedInitialSeekGate,
|
||||||
createRecordedOpenWatchdog,
|
createRecordedOpenWatchdog,
|
||||||
isRecordedPlaybackFullyBuffered,
|
isRecordedPlaybackFullyBuffered,
|
||||||
isRecordedPlaybackPresentationReady,
|
isRecordedPlaybackPresentationReady,
|
||||||
@@ -69,6 +72,7 @@ export {
|
|||||||
recordedPlaybackRangeWhenReady,
|
recordedPlaybackRangeWhenReady,
|
||||||
recordedPointColorKey,
|
recordedPointColorKey,
|
||||||
rerunPresentationStatus,
|
rerunPresentationStatus,
|
||||||
|
shouldReapplyRecordedBlueprint,
|
||||||
type RecordedPlaybackBufferState,
|
type RecordedPlaybackBufferState,
|
||||||
} from "../core/observation/recordedRerunLifecycle";
|
} from "../core/observation/recordedRerunLifecycle";
|
||||||
export {
|
export {
|
||||||
@@ -828,6 +832,7 @@ export function RerunViewport({
|
|||||||
let publishPlaybackBufferState: (() => void) | null = null;
|
let publishPlaybackBufferState: (() => void) | null = null;
|
||||||
let playbackState: RerunPlaybackState | null = null;
|
let playbackState: RerunPlaybackState | null = null;
|
||||||
const recordedAutoplay = createRecordedAutoplayGate();
|
const recordedAutoplay = createRecordedAutoplayGate();
|
||||||
|
const recordedInitialSeek = createRecordedInitialSeekGate();
|
||||||
let blueprintChannel: RerunBlueprintChannel | null = null;
|
let blueprintChannel: RerunBlueprintChannel | null = null;
|
||||||
let perceptionChannel: RerunBlueprintChannel | null = null;
|
let perceptionChannel: RerunBlueprintChannel | null = null;
|
||||||
let perceptionReceiver: RerunNativeReceiver | null = null;
|
let perceptionReceiver: RerunNativeReceiver | null = null;
|
||||||
@@ -1144,9 +1149,25 @@ export function RerunViewport({
|
|||||||
}) => {
|
}) => {
|
||||||
if (
|
if (
|
||||||
disposed ||
|
disposed ||
|
||||||
recordingOpened ||
|
|
||||||
(isRecordedSource && event.application_id !== "nodedc_mission_core_recorded")
|
(isRecordedSource && event.application_id !== "nodedc_mission_core_recorded")
|
||||||
) return;
|
) 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;
|
recordingOpened = true;
|
||||||
if (!isRecordedSource) {
|
if (!isRecordedSource) {
|
||||||
// Store discovery only establishes a candidate. Admission is
|
// Store discovery only establishes a candidate. Admission is
|
||||||
@@ -1314,6 +1335,23 @@ export function RerunViewport({
|
|||||||
playing = true;
|
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({
|
emitPlayback({
|
||||||
rangeNs: followLive
|
rangeNs: followLive
|
||||||
? rangeNs
|
? rangeNs
|
||||||
|
|||||||
@@ -196,6 +196,20 @@ export function isRecordedPlaybackFullyBuffered(
|
|||||||
return recordedPlaybackBufferState(rangeNs, expectedTimelineEndSeconds).fullyBuffered;
|
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(
|
export function attemptRecordedAutoplay(
|
||||||
seekToStart: () => void,
|
seekToStart: () => void,
|
||||||
startPlaying: () => 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(): {
|
export function createRecordedAutoplayGate(): {
|
||||||
attempt: (
|
attempt: (
|
||||||
viewerStarted: boolean,
|
viewerStarted: boolean,
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import { createServer } from "vite";
|
|||||||
let server;
|
let server;
|
||||||
let canPublishRecordedPlaybackController;
|
let canPublishRecordedPlaybackController;
|
||||||
let createRecordedAutoplayGate;
|
let createRecordedAutoplayGate;
|
||||||
|
let createRecordedInitialSeekGate;
|
||||||
let isRecordedPlaybackReady;
|
let isRecordedPlaybackReady;
|
||||||
let isRecordedPlaybackPresentationReady;
|
let isRecordedPlaybackPresentationReady;
|
||||||
let isUsableRecordedPlaybackRange;
|
let isUsableRecordedPlaybackRange;
|
||||||
let recordedPlaybackBufferState;
|
let recordedPlaybackBufferState;
|
||||||
let recordedPlaybackRangeWhenReady;
|
let recordedPlaybackRangeWhenReady;
|
||||||
let rerunPresentationStatus;
|
let rerunPresentationStatus;
|
||||||
|
let shouldReapplyRecordedBlueprint;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
server = await createServer({
|
server = await createServer({
|
||||||
@@ -22,12 +24,14 @@ before(async () => {
|
|||||||
({
|
({
|
||||||
canPublishRecordedPlaybackController,
|
canPublishRecordedPlaybackController,
|
||||||
createRecordedAutoplayGate,
|
createRecordedAutoplayGate,
|
||||||
|
createRecordedInitialSeekGate,
|
||||||
isRecordedPlaybackReady,
|
isRecordedPlaybackReady,
|
||||||
isRecordedPlaybackPresentationReady,
|
isRecordedPlaybackPresentationReady,
|
||||||
isUsableRecordedPlaybackRange,
|
isUsableRecordedPlaybackRange,
|
||||||
recordedPlaybackBufferState,
|
recordedPlaybackBufferState,
|
||||||
recordedPlaybackRangeWhenReady,
|
recordedPlaybackRangeWhenReady,
|
||||||
rerunPresentationStatus,
|
rerunPresentationStatus,
|
||||||
|
shouldReapplyRecordedBlueprint,
|
||||||
} = await server.ssrLoadModule("/src/components/RerunViewport.tsx"));
|
} = await server.ssrLoadModule("/src/components/RerunViewport.tsx"));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -159,6 +163,58 @@ test("recorded autoplay waits for the full range and then runs exactly once", ()
|
|||||||
assert.equal(gate.attempted(), true);
|
assert.equal(gate.attempted(), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("paused recorded replay seeks once to its first presentable frame", () => {
|
||||||
|
const gate = createRecordedInitialSeekGate();
|
||||||
|
const seeks = [];
|
||||||
|
const range = { min: 0, max: 535_717_620_042 };
|
||||||
|
|
||||||
|
assert.equal(gate.attempt(
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
range,
|
||||||
|
(value) => seeks.push(value),
|
||||||
|
39_215_263_458,
|
||||||
|
), true);
|
||||||
|
assert.equal(gate.attempt(
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
range,
|
||||||
|
(value) => seeks.push(value),
|
||||||
|
50_000_000_000,
|
||||||
|
), false);
|
||||||
|
assert.deepEqual(seeks, [39_215_263_458]);
|
||||||
|
assert.equal(gate.attempted(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("LAB sidecar store reopen reapplies only its matching recorded blueprint", () => {
|
||||||
|
const identity = {
|
||||||
|
applicationId: "nodedc_mission_core_recorded",
|
||||||
|
recordingId: "recording-001",
|
||||||
|
};
|
||||||
|
const event = {
|
||||||
|
application_id: "nodedc_mission_core_recorded",
|
||||||
|
recording_id: "recording-001",
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
shouldReapplyRecordedBlueprint(true, true, true, identity, event),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
shouldReapplyRecordedBlueprint(false, true, true, identity, event),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
shouldReapplyRecordedBlueprint(true, true, true, identity, {
|
||||||
|
...event,
|
||||||
|
recording_id: "recording-002",
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("recorded autoplay starts at the first presentable camera frame without shrinking the range", () => {
|
test("recorded autoplay starts at the first presentable camera frame without shrinking the range", () => {
|
||||||
const gate = createRecordedAutoplayGate();
|
const gate = createRecordedAutoplayGate();
|
||||||
const seeks = [];
|
const seeks = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user