fix(viewer): make recorded layers deterministic
This commit is contained in:
@@ -49,6 +49,7 @@ export interface RerunViewportProps {
|
||||
presentationGate?: RecordedAdmissionPhase;
|
||||
expectedTimelineStartSeconds?: number;
|
||||
expectedTimelineEndSeconds?: number;
|
||||
initialPlaybackStartSeconds?: number;
|
||||
onStatusChange?: (status: RerunViewportStatus, message?: string) => void;
|
||||
onSelectionChange?: (selection: RerunSelection | null) => void;
|
||||
onPlaybackChange?: (state: RerunPlaybackState | null) => void;
|
||||
@@ -295,12 +296,21 @@ export function createRecordedAutoplayGate(): {
|
||||
rangeNs: { min: number; max: number } | null,
|
||||
seekToStart: (startNs: number) => void,
|
||||
startPlaying: () => void,
|
||||
preferredStartNs?: number,
|
||||
) => boolean;
|
||||
attempted: () => boolean;
|
||||
} {
|
||||
let consumed = false;
|
||||
return {
|
||||
attempt(viewerStarted, fullyBuffered, presentationReady, rangeNs, seekToStart, startPlaying) {
|
||||
attempt(
|
||||
viewerStarted,
|
||||
fullyBuffered,
|
||||
presentationReady,
|
||||
rangeNs,
|
||||
seekToStart,
|
||||
startPlaying,
|
||||
preferredStartNs,
|
||||
) {
|
||||
if (
|
||||
consumed ||
|
||||
!viewerStarted ||
|
||||
@@ -309,8 +319,11 @@ export function createRecordedAutoplayGate(): {
|
||||
!isUsableRecordedPlaybackRange(rangeNs)
|
||||
) return false;
|
||||
consumed = true;
|
||||
const startNs = Number.isFinite(preferredStartNs)
|
||||
? Math.min(Math.max(preferredStartNs as number, rangeNs.min), rangeNs.max)
|
||||
: rangeNs.min;
|
||||
return attemptRecordedAutoplay(
|
||||
() => seekToStart(rangeNs.min),
|
||||
() => seekToStart(startNs),
|
||||
startPlaying,
|
||||
);
|
||||
},
|
||||
@@ -515,7 +528,8 @@ export async function fetchRecordedBlueprintRrd(
|
||||
custom_color: settings.customColor,
|
||||
active_view: activeView,
|
||||
view_reset_generation: viewResetGeneration,
|
||||
unified_perception: perceptionLayers.enabled,
|
||||
unified_perception:
|
||||
perceptionLayers.detections2d || perceptionLayers.segmentation,
|
||||
show_detections_2d: perceptionLayers.detections2d,
|
||||
show_segmentation: perceptionLayers.segmentation,
|
||||
show_cuboids_3d: perceptionLayers.cuboids3d,
|
||||
@@ -616,6 +630,7 @@ export function RerunViewport({
|
||||
presentationGate = "ready",
|
||||
expectedTimelineStartSeconds,
|
||||
expectedTimelineEndSeconds,
|
||||
initialPlaybackStartSeconds,
|
||||
onStatusChange,
|
||||
onSelectionChange,
|
||||
onPlaybackChange,
|
||||
@@ -982,6 +997,9 @@ export function RerunViewport({
|
||||
() => {
|
||||
viewer.set_playing(event.recording_id, true);
|
||||
},
|
||||
initialPlaybackStartSeconds === undefined
|
||||
? undefined
|
||||
: initialPlaybackStartSeconds * 1_000_000_000,
|
||||
);
|
||||
if (autoplayStarted) {
|
||||
playing = true;
|
||||
@@ -1153,6 +1171,7 @@ export function RerunViewport({
|
||||
expectedTimelineEndSeconds,
|
||||
expectedTimelineStartSeconds,
|
||||
followLive,
|
||||
initialPlaybackStartSeconds,
|
||||
onPlaybackChange,
|
||||
onPlaybackControllerChange,
|
||||
onSelectionChange,
|
||||
|
||||
@@ -336,10 +336,13 @@ function SpatialWorkspace({
|
||||
(source) => source.capabilities.overlay && source.modality !== "point-cloud",
|
||||
);
|
||||
const recordedPerceptionSupported = recordedSource && perceptionAvailability !== "unavailable";
|
||||
// Keep one stable original-video + world composition for every admitted
|
||||
// perception recording. Layer buttons then only change entity visibility;
|
||||
// they never reparent the 3D view or invalidate the operator's camera.
|
||||
const unifiedPerception = recordedPerceptionSupported;
|
||||
const recordedPerceptionEnabled =
|
||||
showDetections2d || showSegmentation || showCuboids3d;
|
||||
// The native recorded camera remains the authoritative original. Only 2D
|
||||
// image-space overlays need Rerun's paired camera/world composition; 3D
|
||||
// cuboids are added directly to the stable spatial view.
|
||||
const unifiedPerception = recordedPerceptionSupported &&
|
||||
(showDetections2d || showSegmentation);
|
||||
const livePerceptionAvailable = !recordedSource && streamActive;
|
||||
const detections2dActive = recordedSource
|
||||
? showDetections2d
|
||||
@@ -354,6 +357,16 @@ function SpatialWorkspace({
|
||||
observationLayout.visibleSourceIds.has(source.id) &&
|
||||
!source.id.startsWith("recorded.perception."),
|
||||
);
|
||||
const initialRecordedPlaybackStartSeconds = recordedSource
|
||||
? mediaSources.reduce<number | undefined>((earliest, source) => {
|
||||
if (
|
||||
source.id.startsWith("recorded.perception.") ||
|
||||
source.delivery?.kind !== "recorded-fmp4-manifest"
|
||||
) return earliest;
|
||||
const start = source.delivery.timelineStartSeconds;
|
||||
return earliest === undefined ? start : Math.min(earliest, start);
|
||||
}, undefined)
|
||||
: undefined;
|
||||
const presentedMediaSourceCount = unifiedPerception ? 0 : visibleMediaSources.length;
|
||||
const pointCloudFocused = Boolean(
|
||||
pointCloudSource && observationLayout.focusedSourceId === pointCloudSource.id,
|
||||
@@ -574,10 +587,11 @@ function SpatialWorkspace({
|
||||
expectedTimelineEndSeconds={recordedSource
|
||||
? state?.observationTimeline?.range?.endSeconds
|
||||
: undefined}
|
||||
initialPlaybackStartSeconds={initialRecordedPlaybackStartSeconds}
|
||||
sceneSettings={sceneSettings}
|
||||
recordedViewResetGeneration={recordedViewResetGeneration}
|
||||
recordedPerceptionLayers={{
|
||||
enabled: unifiedPerception,
|
||||
enabled: recordedPerceptionSupported && recordedPerceptionEnabled,
|
||||
detections2d: showDetections2d,
|
||||
segmentation: showSegmentation,
|
||||
cuboids3d: showCuboids3d,
|
||||
|
||||
@@ -309,6 +309,39 @@ test("recorded blueprint fetch is bounded, strict and sends only display setting
|
||||
show_cuboids_3d: true,
|
||||
});
|
||||
|
||||
await fetchRecordedBlueprintRrd(
|
||||
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/blueprint.rrd",
|
||||
{
|
||||
accumulationSeconds: 0,
|
||||
showGrid: true,
|
||||
showPoints: true,
|
||||
showTrajectory: true,
|
||||
pointSize: 2.5,
|
||||
palette: "turbo",
|
||||
customColor: "#ffffff",
|
||||
},
|
||||
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
|
||||
{
|
||||
origin: "http://127.0.0.1:5174",
|
||||
blueprintSessionId: "b".repeat(32),
|
||||
perceptionLayers: {
|
||||
enabled: true,
|
||||
detections2d: false,
|
||||
segmentation: false,
|
||||
cuboids3d: true,
|
||||
},
|
||||
fetcher: async (input, init) => {
|
||||
calls.push({ input: String(input), init, body: JSON.parse(String(init.body)) });
|
||||
return new Response(payload, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/vnd.rerun.rrd" },
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.equal(calls[1].body.unified_perception, false);
|
||||
assert.equal(calls[1].body.show_cuboids_3d, true);
|
||||
|
||||
await assert.rejects(
|
||||
fetchRecordedBlueprintRrd(
|
||||
"https://outside.invalid/api/v1/observation-sessions/session-1/blueprint.rrd",
|
||||
|
||||
@@ -128,6 +128,51 @@ test("recorded autoplay waits for the full range and then runs exactly once", ()
|
||||
assert.equal(gate.attempted(), true);
|
||||
});
|
||||
|
||||
test("recorded autoplay starts at the first presentable camera frame without shrinking the range", () => {
|
||||
const gate = createRecordedAutoplayGate();
|
||||
const seeks = [];
|
||||
const range = { min: 0, max: 535_717_620_042 };
|
||||
|
||||
assert.equal(gate.attempt(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
range,
|
||||
(value) => seeks.push(value),
|
||||
() => {},
|
||||
35_421_857_292,
|
||||
), true);
|
||||
assert.deepEqual(seeks, [35_421_857_292]);
|
||||
assert.deepEqual(range, { min: 0, max: 535_717_620_042 });
|
||||
});
|
||||
|
||||
test("recorded autoplay clamps an invalid presentation start to the admitted archive", () => {
|
||||
const before = createRecordedAutoplayGate();
|
||||
const after = createRecordedAutoplayGate();
|
||||
const seeks = [];
|
||||
const range = { min: 5, max: 10 };
|
||||
|
||||
assert.equal(before.attempt(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
range,
|
||||
(value) => seeks.push(value),
|
||||
() => {},
|
||||
-50,
|
||||
), true);
|
||||
assert.equal(after.attempt(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
range,
|
||||
(value) => seeks.push(value),
|
||||
() => {},
|
||||
50,
|
||||
), true);
|
||||
assert.deepEqual(seeks, [5, 10]);
|
||||
});
|
||||
|
||||
test("a failed vendor autoplay attempt is consumed instead of rewinding later", () => {
|
||||
const gate = createRecordedAutoplayGate();
|
||||
let seeks = 0;
|
||||
|
||||
Reference in New Issue
Block a user