885 lines
30 KiB
JavaScript
885 lines
30 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import { after, before, test } from "node:test";
|
||
|
||
import { createElement } from "react";
|
||
import { renderToStaticMarkup } from "react-dom/server";
|
||
import { createServer } from "vite";
|
||
|
||
let server;
|
||
let xgridsK1Manifest;
|
||
let xgridsK1ObservationSources;
|
||
let openObservationSource;
|
||
let shouldRestartObservationSource;
|
||
let consumeCameraLeaseRetry;
|
||
let resetCameraLeaseRetryBudget;
|
||
let initialObservationWindowRect;
|
||
let ObservationTimeline;
|
||
let shouldCaptureWorkspacePointer;
|
||
let normalizeTimelineRange;
|
||
let timelineOffsetSeconds;
|
||
let formatTimelineDuration;
|
||
let normalizeAccumulationSeconds;
|
||
let formatAccumulationDuration;
|
||
let resolveRerunSourceUrl;
|
||
let resolveRecordedBlueprintUrl;
|
||
let fetchRecordedBlueprintRrd;
|
||
let resolveRecordedPerceptionUrl;
|
||
let fetchRecordedPerceptionRrd;
|
||
let resolveRecordedPointColorsUrl;
|
||
let fetchRecordedPointColorsRrd;
|
||
let recordedPointColorKey;
|
||
let isRecordedPlaybackFullyBuffered;
|
||
let recordedObservationSources;
|
||
let selectRecordedMediaEpoch;
|
||
let recordedMediaLocalTime;
|
||
|
||
before(async () => {
|
||
server = await createServer({
|
||
appType: "custom",
|
||
logLevel: "silent",
|
||
server: { middlewareMode: true },
|
||
});
|
||
({ xgridsK1Manifest } = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/manifest.ts",
|
||
));
|
||
({ xgridsK1ObservationSources } = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/observationSources.ts",
|
||
));
|
||
({ openObservationSource, shouldRestartObservationSource } = await server.ssrLoadModule(
|
||
"/src/core/observation/layoutPolicy.ts",
|
||
));
|
||
({ consumeCameraLeaseRetry, resetCameraLeaseRetryBudget } = await server.ssrLoadModule(
|
||
"/src/components/MseFmp4WebSocketPlayer.tsx",
|
||
));
|
||
({ initialObservationWindowRect, shouldCaptureWorkspacePointer } = await server.ssrLoadModule(
|
||
"/src/components/FloatingObservationWindow.tsx",
|
||
));
|
||
({
|
||
ObservationTimeline,
|
||
normalizeTimelineRange,
|
||
timelineOffsetSeconds,
|
||
formatTimelineDuration,
|
||
normalizeAccumulationSeconds,
|
||
formatAccumulationDuration,
|
||
} =
|
||
await server.ssrLoadModule("/src/components/ObservationTimeline.tsx"));
|
||
({
|
||
resolveRerunSourceUrl,
|
||
resolveRecordedBlueprintUrl,
|
||
fetchRecordedBlueprintRrd,
|
||
resolveRecordedPerceptionUrl,
|
||
fetchRecordedPerceptionRrd,
|
||
resolveRecordedPointColorsUrl,
|
||
fetchRecordedPointColorsRrd,
|
||
recordedPointColorKey,
|
||
isRecordedPlaybackFullyBuffered,
|
||
} = await server.ssrLoadModule(
|
||
"/src/components/RerunViewport.tsx",
|
||
));
|
||
({ recordedObservationSources } = await server.ssrLoadModule(
|
||
"/src/core/observation/recordedObservationSources.ts",
|
||
));
|
||
({ selectRecordedMediaEpoch, recordedMediaLocalTime } = await server.ssrLoadModule(
|
||
"/src/components/RecordedFmp4Player.tsx",
|
||
));
|
||
});
|
||
|
||
test("observation camera windows tile from the bottom-right above the live timeline", () => {
|
||
const bounds = { width: 1280, height: 720 };
|
||
const left = initialObservationWindowRect(0, 2, bounds);
|
||
const right = initialObservationWindowRect(1, 2, bounds);
|
||
|
||
assert.equal(left.y, right.y);
|
||
assert.ok(left.x + left.width < right.x);
|
||
assert.equal(right.x + right.width, bounds.width - 18);
|
||
assert.ok(right.y + right.height <= bounds.height - 64);
|
||
});
|
||
|
||
test("observation camera windows stack without overlap when the viewport is narrow", () => {
|
||
const bounds = { width: 420, height: 700 };
|
||
const lower = initialObservationWindowRect(0, 2, bounds);
|
||
const upper = initialObservationWindowRect(1, 2, bounds);
|
||
|
||
assert.equal(lower.x, upper.x);
|
||
assert.ok(upper.y + upper.height < lower.y);
|
||
for (const rect of [lower, upper]) {
|
||
assert.ok(rect.x >= 0 && rect.y >= 0);
|
||
assert.ok(rect.x + rect.width <= bounds.width);
|
||
assert.ok(rect.y + rect.height <= bounds.height - 64);
|
||
}
|
||
});
|
||
|
||
test("observation camera tiling remains in bounds on a constrained viewport", () => {
|
||
const bounds = { width: 260, height: 280 };
|
||
const rects = Array.from({ length: 2 }, (_, index) => (
|
||
initialObservationWindowRect(index, 2, bounds)
|
||
));
|
||
|
||
for (const rect of rects) {
|
||
assert.ok(rect.width > 0 && rect.height > 0);
|
||
assert.ok(rect.x >= 0 && rect.y >= 0);
|
||
assert.ok(rect.x + rect.width <= bounds.width);
|
||
assert.ok(rect.y + rect.height <= bounds.height - 64);
|
||
}
|
||
assert.ok(rects[1].y + rects[1].height <= rects[0].y);
|
||
});
|
||
|
||
test("floating observation interaction captures resize and movable header pointers", () => {
|
||
const target = (matches) => ({ closest: (selector) => matches.includes(selector) });
|
||
assert.equal(
|
||
shouldCaptureWorkspacePointer(0, target([".nodedc-workspace-window__resize"])),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
shouldCaptureWorkspacePointer(0, target([".nodedc-workspace-window__head"])),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
shouldCaptureWorkspacePointer(
|
||
0,
|
||
target([".nodedc-workspace-window__head", "button, input, select, textarea, a"]),
|
||
),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
shouldCaptureWorkspacePointer(2, target([".nodedc-workspace-window__resize"])),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("recorded observation timeline clamps relative seek time without epoch precision in the UI", () => {
|
||
const range = normalizeTimelineRange({
|
||
min: 0,
|
||
max: 12_500_000_000,
|
||
});
|
||
assert.ok(range);
|
||
assert.equal(timelineOffsetSeconds(range, range.min + 2_250_000_000), 2.25);
|
||
assert.equal(timelineOffsetSeconds(range, range.min - 1), 0);
|
||
assert.equal(timelineOffsetSeconds(range, range.max + 1), 12.5);
|
||
assert.equal(formatTimelineDuration(62.125), "01:02.125");
|
||
});
|
||
|
||
test("recorded observation timeline rejects empty and non-finite ranges", () => {
|
||
assert.equal(normalizeTimelineRange(null), null);
|
||
assert.equal(normalizeTimelineRange({ min: 10, max: 10 }), null);
|
||
assert.equal(normalizeTimelineRange({ min: Number.NaN, max: 10 }), null);
|
||
});
|
||
|
||
test("accumulation control normalizes UI values and distinguishes a single frame", () => {
|
||
assert.equal(normalizeAccumulationSeconds(-3), 0);
|
||
assert.equal(normalizeAccumulationSeconds(12.6), 13);
|
||
assert.equal(normalizeAccumulationSeconds(999), 120);
|
||
assert.equal(normalizeAccumulationSeconds(Number.NaN), 0);
|
||
assert.equal(formatAccumulationDuration(0), "Кадр");
|
||
assert.equal(formatAccumulationDuration(12), "12 с");
|
||
});
|
||
|
||
test("spatial timeline renders synchronized accumulation and playback controls", () => {
|
||
const markup = renderToStaticMarkup(createElement(ObservationTimeline, {
|
||
active: true,
|
||
sourceCount: 2,
|
||
mode: "recorded",
|
||
seekable: true,
|
||
rangeNs: { min: 0, max: 20_000_000_000 },
|
||
currentNs: 5_000_000_000,
|
||
accumulationSeconds: 12,
|
||
onAccumulationChange: () => undefined,
|
||
onAccumulationCommit: () => undefined,
|
||
onSeek: () => undefined,
|
||
}));
|
||
|
||
assert.match(markup, /data-accumulation="true"/);
|
||
assert.match(markup, /Накопление/);
|
||
assert.match(markup, /aria-label="Окно накопления облака точек"/);
|
||
assert.match(markup, /aria-valuetext="12 с"/);
|
||
assert.match(markup, /aria-label="Позиция воспроизведения"/);
|
||
assert.equal((markup.match(/type="range"/g) ?? []).length, 2);
|
||
});
|
||
|
||
test("Rerun expands only root-relative session recordings onto the current origin", () => {
|
||
assert.equal(
|
||
resolveRerunSourceUrl(
|
||
" /api/v1/observation-sessions/session-1/recording.rrd ",
|
||
"http://127.0.0.1:5174",
|
||
),
|
||
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/recording.rrd",
|
||
);
|
||
assert.equal(
|
||
resolveRerunSourceUrl("rerun+http://127.0.0.1:9877/proxy", "http://127.0.0.1:5174"),
|
||
"rerun+http://127.0.0.1:9877/proxy",
|
||
);
|
||
assert.equal(
|
||
resolveRerunSourceUrl("//different-authority.invalid/session.rrd", "http://127.0.0.1:5174"),
|
||
"//different-authority.invalid/session.rrd",
|
||
);
|
||
});
|
||
|
||
test("recorded blueprint endpoint is derived only from canonical same-origin RRD sources", () => {
|
||
assert.equal(
|
||
resolveRecordedBlueprintUrl(
|
||
"/api/v1/observation-sessions/session-1/recording.rrd",
|
||
"http://127.0.0.1:5174",
|
||
),
|
||
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/blueprint.rrd",
|
||
);
|
||
assert.equal(
|
||
resolveRecordedBlueprintUrl(
|
||
"https://outside.invalid/api/v1/observation-sessions/session-1/recording.rrd",
|
||
"http://127.0.0.1:5174",
|
||
),
|
||
null,
|
||
);
|
||
assert.equal(
|
||
resolveRecordedBlueprintUrl(
|
||
"/api/v1/observation-sessions/../recording.rrd",
|
||
"http://127.0.0.1:5174",
|
||
),
|
||
null,
|
||
);
|
||
});
|
||
|
||
test("recorded replay becomes ready only after the complete declared timeline is buffered", () => {
|
||
assert.equal(isRecordedPlaybackFullyBuffered(null, 20), false);
|
||
assert.equal(
|
||
isRecordedPlaybackFullyBuffered({ min: 0, max: 19_500_000_000 }, 20),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
isRecordedPlaybackFullyBuffered({ min: 0, max: 19_999_500_000 }, 20),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
isRecordedPlaybackFullyBuffered({ min: 0, max: 1 }, undefined),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
isRecordedPlaybackFullyBuffered({ min: 0, max: 20_000_000_000 }, Number.NaN),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("recorded blueprint fetch is bounded, strict and sends only display settings", async () => {
|
||
const calls = [];
|
||
const payload = Uint8Array.from([0x52, 0x52, 0x46, 0x32, 0x01]);
|
||
const result = await fetchRecordedBlueprintRrd(
|
||
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/blueprint.rrd",
|
||
{
|
||
accumulationSeconds: 24,
|
||
showGrid: false,
|
||
showPoints: true,
|
||
showTrajectory: false,
|
||
pointSize: 4.5,
|
||
colorMode: "height",
|
||
palette: "custom",
|
||
customColor: "#35d7c1",
|
||
},
|
||
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
|
||
{
|
||
origin: "http://127.0.0.1:5174",
|
||
blueprintSessionId: "a".repeat(32),
|
||
activeView: "perception3d",
|
||
viewResetGeneration: 1,
|
||
followTrajectory: true,
|
||
perceptionLayers: {
|
||
enabled: true,
|
||
detections2d: true,
|
||
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.deepEqual([...result], [...payload]);
|
||
assert.equal(calls[0].init.method, "POST");
|
||
assert.equal(calls[0].init.credentials, "same-origin");
|
||
assert.deepEqual(calls[0].body, {
|
||
application_id: "nodedc_mission_core_recorded",
|
||
recording_id: "recording-001",
|
||
blueprint_session_id: "a".repeat(32),
|
||
accumulation_seconds: 24,
|
||
show_grid: false,
|
||
show_points: true,
|
||
show_trajectory: false,
|
||
point_size: 4.5,
|
||
color_mode: "height",
|
||
palette: "custom",
|
||
custom_color: "#35d7c1",
|
||
active_view: "perception3d",
|
||
view_reset_generation: 1,
|
||
follow_trajectory: true,
|
||
unified_perception: true,
|
||
show_detections_2d: true,
|
||
show_segmentation: false,
|
||
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,
|
||
colorMode: "intensity",
|
||
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",
|
||
{
|
||
accumulationSeconds: 24,
|
||
showGrid: false,
|
||
showPoints: true,
|
||
showTrajectory: false,
|
||
pointSize: 4.5,
|
||
colorMode: "height",
|
||
palette: "custom",
|
||
customColor: "#35d7c1",
|
||
},
|
||
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
|
||
{
|
||
origin: "http://127.0.0.1:5174",
|
||
blueprintSessionId: "a".repeat(32),
|
||
fetcher: async () => new Response(payload),
|
||
},
|
||
),
|
||
/Unsafe recorded blueprint request/,
|
||
);
|
||
});
|
||
|
||
test("recorded point colors use one strict same-origin component overlay", async () => {
|
||
const endpoint = resolveRecordedPointColorsUrl(
|
||
"/api/v1/observation-sessions/session-1/recording.rrd",
|
||
"http://127.0.0.1:5174",
|
||
);
|
||
assert.equal(
|
||
endpoint,
|
||
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/point-colors.rrd",
|
||
);
|
||
assert.equal(
|
||
resolveRecordedPointColorsUrl(
|
||
"https://outside.invalid/api/v1/observation-sessions/session-1/recording.rrd",
|
||
"http://127.0.0.1:5174",
|
||
),
|
||
null,
|
||
);
|
||
assert.equal(
|
||
recordedPointColorKey({
|
||
colorMode: "intensity",
|
||
palette: "turbo",
|
||
customColor: "#112233",
|
||
}),
|
||
"intensity|turbo|-",
|
||
);
|
||
assert.equal(
|
||
recordedPointColorKey({
|
||
colorMode: "class",
|
||
palette: "turbo",
|
||
customColor: "#112233",
|
||
}),
|
||
"class|turbo|#112233",
|
||
);
|
||
|
||
const calls = [];
|
||
const payload = Uint8Array.from([0x52, 0x52, 0x46, 0x32, 0x01]);
|
||
const result = await fetchRecordedPointColorsRrd(
|
||
endpoint,
|
||
{
|
||
colorMode: "distance",
|
||
palette: "viridis",
|
||
customColor: "#35d7c1",
|
||
},
|
||
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
|
||
{
|
||
origin: "http://127.0.0.1:5174",
|
||
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",
|
||
"Content-Length": String(payload.byteLength),
|
||
},
|
||
});
|
||
},
|
||
},
|
||
);
|
||
assert.deepEqual([...result], [...payload]);
|
||
assert.deepEqual(calls[0].body, {
|
||
application_id: "nodedc_mission_core_recorded",
|
||
recording_id: "recording-001",
|
||
color_mode: "distance",
|
||
palette: "viridis",
|
||
custom_color: "#35d7c1",
|
||
});
|
||
});
|
||
|
||
test("recorded perception fetch admits one complete same-origin RRD or no layer", async () => {
|
||
const endpoint = resolveRecordedPerceptionUrl(
|
||
"/api/v1/observation-sessions/session-1/recording.rrd",
|
||
"http://127.0.0.1:5174",
|
||
);
|
||
assert.equal(
|
||
endpoint,
|
||
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/perception.rrd",
|
||
);
|
||
const payload = Uint8Array.from([0x52, 0x52, 0x46, 0x32, 0x01]);
|
||
const progress = [];
|
||
const result = await fetchRecordedPerceptionRrd(
|
||
endpoint,
|
||
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
|
||
{
|
||
origin: "http://127.0.0.1:5174",
|
||
onProgress: (receivedBytes, totalBytes) => {
|
||
progress.push([receivedBytes, totalBytes]);
|
||
},
|
||
fetcher: async (_input, init) => {
|
||
assert.deepEqual(JSON.parse(String(init.body)), {
|
||
application_id: "nodedc_mission_core_recorded",
|
||
recording_id: "recording-001",
|
||
});
|
||
return new Response(payload, {
|
||
status: 200,
|
||
headers: {
|
||
"Content-Type": "application/vnd.rerun.rrd",
|
||
"Content-Length": String(payload.byteLength),
|
||
},
|
||
});
|
||
},
|
||
},
|
||
);
|
||
assert.deepEqual([...result], [...payload]);
|
||
assert.deepEqual(progress[0], [0, payload.byteLength]);
|
||
assert.deepEqual(progress.at(-1), [payload.byteLength, payload.byteLength]);
|
||
|
||
const absent = await fetchRecordedPerceptionRrd(
|
||
endpoint,
|
||
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
|
||
{
|
||
origin: "http://127.0.0.1:5174",
|
||
fetcher: async () => new Response(null, { status: 204 }),
|
||
},
|
||
);
|
||
assert.equal(absent, null);
|
||
});
|
||
|
||
test("recorded replay creates an isolated source catalog without live device bindings", () => {
|
||
const sources = recordedObservationSources({
|
||
kind: "rerun-recording",
|
||
sessionId: "session-1",
|
||
sourceUrl: "/api/v1/observation-sessions/session-1/recording.rrd",
|
||
viewerSourceUrl: `/api/v1/observation-sessions/session-1/recording.rrd?generation=${"a".repeat(64)}`,
|
||
mediaType: "application/vnd.rerun.rrd",
|
||
timeline: "session_time",
|
||
timelineStartSeconds: 0,
|
||
timelineEndSeconds: 20,
|
||
seekable: true,
|
||
byteLength: 123,
|
||
sha256: "a".repeat(64),
|
||
playback: { speed: 1, loop: false },
|
||
mediaSources: [{
|
||
id: "recorded.camera.abc123",
|
||
label: "Записанная камера 1",
|
||
modality: "video",
|
||
manifestUrl: "/api/v1/observation-sessions/session-1/media/recorded-video-abc123/manifest",
|
||
manifestGenerationSha256: "c".repeat(64),
|
||
byteLength: 1_024,
|
||
mediaType: "video/mp4",
|
||
timelineStartSeconds: 0.25,
|
||
timelineEndSeconds: 20,
|
||
seekable: true,
|
||
synchronization: "host-arrival-best-effort",
|
||
}],
|
||
});
|
||
assert.deepEqual(sources.map(({ modality }) => modality), ["point-cloud", "video"]);
|
||
assert.equal(sources[1].delivery.kind, "recorded-fmp4-manifest");
|
||
assert.equal(sources[1].delivery.manifestGenerationSha256, "c".repeat(64));
|
||
assert.deepEqual(sources[1].binding, {});
|
||
assert.equal(sources.some(({ provider }) => provider.pluginId.includes("xgrids")), false);
|
||
assert.equal(JSON.stringify(sources).includes("192.168"), false);
|
||
});
|
||
|
||
test("recorded camera epoch selection and shared-clock offset are deterministic", () => {
|
||
const epochs = [
|
||
{ ordinal: 1, timelineStartSeconds: 0.25, timelineEndSeconds: 9 },
|
||
{ ordinal: 2, timelineStartSeconds: 10, timelineEndSeconds: 20 },
|
||
];
|
||
assert.equal(selectRecordedMediaEpoch(epochs, 0), null);
|
||
assert.equal(selectRecordedMediaEpoch(epochs, 0.25).ordinal, 1);
|
||
assert.equal(selectRecordedMediaEpoch(epochs, 9), epochs[0]);
|
||
assert.equal(selectRecordedMediaEpoch(epochs, 9.9), null);
|
||
assert.equal(selectRecordedMediaEpoch(epochs, 10).ordinal, 2);
|
||
assert.equal(recordedMediaLocalTime(10, 12.5), 2.5);
|
||
assert.equal(recordedMediaLocalTime(10, 8), 0);
|
||
assert.equal(recordedMediaLocalTime(10, 30, 4), 4);
|
||
});
|
||
|
||
after(async () => {
|
||
await server?.close();
|
||
});
|
||
|
||
function model() {
|
||
const activeModel = xgridsK1Manifest.spec.models[0];
|
||
assert.ok(activeModel, "XGRIDS plugin must declare at least one model");
|
||
return activeModel;
|
||
}
|
||
|
||
function cameraRow(sourceId, label) {
|
||
return {
|
||
stream_id: `camera.preview.${sourceId.split(".").at(-1)}`,
|
||
source_id: sourceId,
|
||
semantic_channel_id: "camera.preview.live",
|
||
label,
|
||
sensor_kind: "camera",
|
||
modality: "encoded-video",
|
||
availability: "available",
|
||
endpoint_label: "MSE · fMP4",
|
||
activation: {
|
||
group_id: "camera.preview.decoder",
|
||
max_active: 1,
|
||
selected: false,
|
||
controllable: true,
|
||
},
|
||
delivery: null,
|
||
};
|
||
}
|
||
|
||
function declaredState(cameraRows = [
|
||
cameraRow("sensor.camera.left", "K1 · камера слева"),
|
||
cameraRow("sensor.camera.right", "K1 · камера справа"),
|
||
]) {
|
||
const activeModel = model();
|
||
return {
|
||
phase: "connected",
|
||
source_mode: "idle",
|
||
k1_ip: "192.168.7.10",
|
||
foxglove_ws_url: "ws://192.168.7.10:8765",
|
||
foxglove_viewer_url: "http://192.168.7.10:8765/vendor-viewer",
|
||
compatibility: {
|
||
profile_id: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
|
||
camera_preview: "rtsp://192.168.7.10:8554/vendor-preview",
|
||
},
|
||
device_ref: {
|
||
device_id: "device-k1-001",
|
||
model_id: activeModel.id,
|
||
identity_stability: "stable",
|
||
identity_basis: "hardware-identifier",
|
||
},
|
||
device_session: {
|
||
device_session_id: "device-session-001",
|
||
device_id: "device-k1-001",
|
||
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
|
||
connectivity: "connected",
|
||
},
|
||
sensor_catalog: {
|
||
schema_version: "missioncore.sensor-catalog/v1alpha2",
|
||
revision: "test-profile",
|
||
streams: [
|
||
{ stream_id: "spatial.point-cloud.live", modality: "point-cloud", availability: "observed" },
|
||
...cameraRows,
|
||
],
|
||
},
|
||
camera_preview: {
|
||
phase: "idle",
|
||
revision: 1,
|
||
generation: 0,
|
||
active_source_id: null,
|
||
delivery: null,
|
||
},
|
||
};
|
||
}
|
||
|
||
function pointCloudStreamingState() {
|
||
return {
|
||
...declaredState(),
|
||
phase: "streaming",
|
||
source_mode: "live",
|
||
rerun_grpc_url: "rerun+http://127.0.0.1:9877/proxy",
|
||
acquisition: {
|
||
acquisition_id: "acquisition-001",
|
||
device_id: "device-k1-001",
|
||
device_session_id: "device-session-001",
|
||
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
|
||
control_mode: "operator-manual",
|
||
requested_streams: ["spatial.point-cloud.live"],
|
||
target_host: "127.0.0.1",
|
||
duration_seconds: 0,
|
||
evidence_policy: "required",
|
||
state: "acquiring",
|
||
state_revision: 3,
|
||
},
|
||
};
|
||
}
|
||
|
||
function cameraStreamingState(sourceId) {
|
||
const state = pointCloudStreamingState();
|
||
const delivery = {
|
||
id: `preview-generation-7:${sourceId}`,
|
||
kind: "mse-fmp4-websocket",
|
||
url: "/api/v1/device-plugins/xgrids-k1/camera-preview/ws?generation=7",
|
||
media_type: 'video/mp4; codecs="avc1.640028"',
|
||
};
|
||
state.sensor_catalog = {
|
||
...state.sensor_catalog,
|
||
streams: state.sensor_catalog.streams.map((stream) =>
|
||
stream.source_id === sourceId
|
||
? {
|
||
...stream,
|
||
availability: "streaming",
|
||
activation: { ...stream.activation, selected: true },
|
||
delivery,
|
||
}
|
||
: stream),
|
||
};
|
||
state.camera_preview = {
|
||
phase: "streaming",
|
||
revision: 4,
|
||
generation: 7,
|
||
active_source_id: sourceId,
|
||
delivery,
|
||
};
|
||
return state;
|
||
}
|
||
|
||
function collectUrlLikeStrings(value, found = []) {
|
||
if (typeof value === "string") {
|
||
if (value.includes("://")) found.push(value);
|
||
return found;
|
||
}
|
||
if (Array.isArray(value)) {
|
||
for (const item of value) collectUrlLikeStrings(item, found);
|
||
return found;
|
||
}
|
||
if (value && typeof value === "object") {
|
||
for (const item of Object.values(value)) collectUrlLikeStrings(item, found);
|
||
}
|
||
return found;
|
||
}
|
||
|
||
test("K1 maps zero, one or N catalog cameras without model-specific source ids", () => {
|
||
const zero = xgridsK1ObservationSources(declaredState([]), model());
|
||
const one = xgridsK1ObservationSources(
|
||
declaredState([cameraRow("rig.front", "Передняя камера")]),
|
||
model(),
|
||
);
|
||
const many = xgridsK1ObservationSources(declaredState(), model());
|
||
|
||
assert.deepEqual(zero.map(({ modality }) => modality), ["point-cloud"]);
|
||
assert.deepEqual(one.map(({ sourceId }) => sourceId), ["sensor.lidar.primary", "rig.front"]);
|
||
assert.deepEqual(many.map(({ sourceId }) => sourceId), [
|
||
"sensor.lidar.primary",
|
||
"sensor.camera.left",
|
||
"sensor.camera.right",
|
||
]);
|
||
assert.equal(new Set(many.map(({ id }) => id)).size, many.length);
|
||
assert.ok(many.every(({ provider }) => provider.pluginId && provider.modelId));
|
||
assert.ok(many.every(({ binding }) => binding.deviceId === "device-k1-001"));
|
||
});
|
||
|
||
test("descriptor ids remain stable while point cloud and selected camera start streaming", () => {
|
||
const declared = xgridsK1ObservationSources(declaredState(), model());
|
||
const streaming = xgridsK1ObservationSources(
|
||
cameraStreamingState("sensor.camera.left"),
|
||
model(),
|
||
);
|
||
|
||
assert.deepEqual(streaming.map(({ id }) => id), declared.map(({ id }) => id));
|
||
assert.equal(declared[0].availability, "available");
|
||
assert.equal(streaming[0].availability, "streaming");
|
||
assert.equal(streaming[0].previewUrl, "rerun+http://127.0.0.1:9877/proxy");
|
||
});
|
||
|
||
test("only the authoritative selected camera receives browser delivery", () => {
|
||
const sources = xgridsK1ObservationSources(
|
||
cameraStreamingState("sensor.camera.left"),
|
||
model(),
|
||
);
|
||
const cameras = sources.filter(({ modality }) => modality === "video");
|
||
const [left, right] = cameras;
|
||
|
||
assert.equal(cameras.length, 2);
|
||
assert.equal(left.activation.selected, true);
|
||
assert.equal(left.activation.maxActive, 1);
|
||
assert.equal(left.availability, "streaming");
|
||
assert.equal(left.delivery.kind, "mse-fmp4-websocket");
|
||
assert.equal(left.transport, "websocket");
|
||
assert.equal(right.activation.selected, false);
|
||
assert.equal(right.availability, "available");
|
||
assert.equal(right.delivery, null);
|
||
assert.equal(left.activation.groupId, right.activation.groupId);
|
||
assert.match(left.activation.groupId, /device-session-001/);
|
||
});
|
||
|
||
test("switching left to right keeps ids stable and never exposes both deliveries", () => {
|
||
const left = xgridsK1ObservationSources(
|
||
cameraStreamingState("sensor.camera.left"),
|
||
model(),
|
||
).filter(({ modality }) => modality === "video");
|
||
const right = xgridsK1ObservationSources(
|
||
cameraStreamingState("sensor.camera.right"),
|
||
model(),
|
||
).filter(({ modality }) => modality === "video");
|
||
|
||
assert.deepEqual(right.map(({ id }) => id), left.map(({ id }) => id));
|
||
assert.deepEqual(left.filter(({ delivery }) => delivery).map(({ sourceId }) => sourceId), [
|
||
"sensor.camera.left",
|
||
]);
|
||
assert.deepEqual(right.filter(({ delivery }) => delivery).map(({ sourceId }) => sourceId), [
|
||
"sensor.camera.right",
|
||
]);
|
||
});
|
||
|
||
test("camera descriptors never leak vendor RTSP endpoints or device IP addresses", () => {
|
||
const sources = xgridsK1ObservationSources(
|
||
cameraStreamingState("sensor.camera.left"),
|
||
model(),
|
||
);
|
||
const cameras = sources.filter(({ modality }) => modality === "video");
|
||
|
||
assert.ok(cameras.every(({ previewUrl }) => previewUrl === null));
|
||
assert.deepEqual(collectUrlLikeStrings(sources), [
|
||
"rerun+http://127.0.0.1:9877/proxy",
|
||
]);
|
||
const serialized = JSON.stringify(cameras);
|
||
assert.doesNotMatch(serialized, /rtsp:\/\//i);
|
||
assert.doesNotMatch(serialized, /192\.168\.7\.10/);
|
||
assert.doesNotMatch(serialized, /vendor-(?:preview|viewer)/);
|
||
});
|
||
|
||
test("unattested, duplicate and unsafe camera entries fail closed", () => {
|
||
const duplicate = cameraRow("sensor.camera.left", "Duplicate");
|
||
const state = cameraStreamingState("sensor.camera.left");
|
||
state.compatibility.profile_id = null;
|
||
state.device_session.compatibility_profile_id = null;
|
||
state.sensor_catalog.streams.push(duplicate);
|
||
state.camera_preview.delivery = {
|
||
...state.camera_preview.delivery,
|
||
url: "ws://192.168.7.10:9000/leak",
|
||
};
|
||
|
||
const cameras = xgridsK1ObservationSources(state, model()).filter(
|
||
({ modality }) => modality === "video",
|
||
);
|
||
assert.deepEqual(cameras.map(({ sourceId }) => sourceId), ["sensor.camera.right"]);
|
||
assert.equal(cameras[0].availability, "unverified");
|
||
assert.equal(cameras[0].activation.controllable, false);
|
||
assert.equal(cameras[0].delivery, null);
|
||
});
|
||
|
||
test("camera delivery rejects literal and encoded endpoint or credential leaks", () => {
|
||
const unsafeUrls = [
|
||
"/api/preview?upstream=rtsp://camera.local/live",
|
||
"/api/preview?upstream=rtsp%3A%2F%2Fcamera.local%2Flive",
|
||
"/api/preview?upstream=rtsp%253A%252F%252Fcamera.local%252Flive",
|
||
"/api/preview?endpoint=192.0.2.52:8554",
|
||
"/api/preview?endpoint=192%2E168%2E68%2E52",
|
||
"/api/preview?password=not-for-the-browser",
|
||
"/api/preview?%70%61%73%73%77%6f%72%64=not-for-the-browser",
|
||
"/api/preview/camera:secret@device",
|
||
"/%2f%2fevil.example/preview",
|
||
];
|
||
|
||
for (const url of unsafeUrls) {
|
||
const state = cameraStreamingState("sensor.camera.left");
|
||
state.camera_preview.delivery = { ...state.camera_preview.delivery, url };
|
||
state.sensor_catalog.streams = state.sensor_catalog.streams.map((stream) =>
|
||
stream.source_id === "sensor.camera.left"
|
||
? { ...stream, delivery: { ...stream.delivery, url } }
|
||
: stream,
|
||
);
|
||
const left = xgridsK1ObservationSources(state, model()).find(
|
||
({ sourceId }) => sourceId === "sensor.camera.left",
|
||
);
|
||
assert.ok(left, `left camera descriptor missing for ${url}`);
|
||
assert.equal(left.delivery, null, `unsafe delivery escaped for ${url}`);
|
||
}
|
||
});
|
||
|
||
test("manual camera reconnect restores an exhausted lease retry budget", () => {
|
||
let budget = resetCameraLeaseRetryBudget("delivery-7");
|
||
for (const expectedDelay of [400, 1_000, 2_000]) {
|
||
const retry = consumeCameraLeaseRetry(budget, "delivery-7");
|
||
assert.equal(retry.delay, expectedDelay);
|
||
budget = retry.budget;
|
||
}
|
||
assert.equal(consumeCameraLeaseRetry(budget, "delivery-7").delay, null);
|
||
|
||
budget = resetCameraLeaseRetryBudget("delivery-7");
|
||
const retryAfterManualReset = consumeCameraLeaseRetry(budget, "delivery-7");
|
||
assert.equal(retryAfterManualReset.delay, 400);
|
||
assert.equal(retryAfterManualReset.budget.count, 1);
|
||
});
|
||
|
||
test("layout policy evicts only exclusive camera peers", () => {
|
||
const sources = xgridsK1ObservationSources(
|
||
cameraStreamingState("sensor.camera.left"),
|
||
model(),
|
||
);
|
||
const left = sources.find(({ sourceId }) => sourceId === "sensor.camera.left");
|
||
const right = sources.find(({ sourceId }) => sourceId === "sensor.camera.right");
|
||
const pointCloud = sources.find(({ modality }) => modality === "point-cloud");
|
||
assert.ok(left && right && pointCloud);
|
||
|
||
const change = openObservationSource(
|
||
[pointCloud.id, left.id],
|
||
right.id,
|
||
sources,
|
||
);
|
||
assert.deepEqual(change.visibleIds, [pointCloud.id, right.id]);
|
||
assert.deepEqual(change.removedIds, [left.id]);
|
||
});
|
||
|
||
test("selected camera without delivery is explicitly restartable", () => {
|
||
const state = cameraStreamingState("sensor.camera.left");
|
||
state.camera_preview = {
|
||
...state.camera_preview,
|
||
phase: "error",
|
||
delivery: null,
|
||
};
|
||
state.sensor_catalog.streams = state.sensor_catalog.streams.map((stream) =>
|
||
stream.source_id === "sensor.camera.left"
|
||
? { ...stream, availability: "error", delivery: null }
|
||
: stream,
|
||
);
|
||
const sources = xgridsK1ObservationSources(state, model());
|
||
const left = sources.find(({ sourceId }) => sourceId === "sensor.camera.left");
|
||
const right = sources.find(({ sourceId }) => sourceId === "sensor.camera.right");
|
||
assert.ok(left && right);
|
||
|
||
assert.equal(left.activation.selected, true);
|
||
assert.equal(left.delivery, null);
|
||
assert.equal(left.availability, "error");
|
||
assert.equal(shouldRestartObservationSource(left), true);
|
||
assert.equal(shouldRestartObservationSource(right), false);
|
||
});
|