Preserve the completed teach-and-repeat laboratory stage: reference preparation, cascaded acquisition, local tracking and recovery, recording lifecycle, replay qualification, and persistent Rerun scene controls. Document the open grid-picking regression and Rerun upgrade contract. No autonomous driving or loop-closure optimization is claimed.
312 lines
13 KiB
JavaScript
312 lines
13 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import { after, before, test } from "node:test";
|
|
|
|
import { createServer } from "vite";
|
|
|
|
let server;
|
|
let fetchRecordedMediaArchive;
|
|
let recordedMediaPresentationState;
|
|
let recordedMediaSeekableCoverage;
|
|
let recordedMediaFragmentUrl;
|
|
let recordedMediaDecodeStartSequence;
|
|
let recordedMediaSegmentAppendOrder;
|
|
let recordedMediaSegmentSequenceAtTime;
|
|
let recordedMediaCanRollTarget;
|
|
let recordedMediaTimestampStallRecoveryTarget;
|
|
let nextRecordedMediaRandomAccessSequence;
|
|
let recordedMediaRecoveryTargetSequence;
|
|
let selectRecordedMediaPreparationEpoch;
|
|
|
|
before(async () => {
|
|
server = await createServer({
|
|
appType: "custom",
|
|
logLevel: "silent",
|
|
server: { middlewareMode: true },
|
|
});
|
|
({
|
|
fetchRecordedMediaArchive,
|
|
recordedMediaPresentationState,
|
|
recordedMediaSeekableCoverage,
|
|
recordedMediaFragmentUrl,
|
|
recordedMediaDecodeStartSequence,
|
|
recordedMediaSegmentAppendOrder,
|
|
recordedMediaSegmentSequenceAtTime,
|
|
recordedMediaCanRollTarget,
|
|
recordedMediaTimestampStallRecoveryTarget,
|
|
nextRecordedMediaRandomAccessSequence,
|
|
recordedMediaRecoveryTargetSequence,
|
|
selectRecordedMediaPreparationEpoch,
|
|
} = await server.ssrLoadModule("/src/components/RecordedFmp4Player.tsx"));
|
|
});
|
|
|
|
after(async () => {
|
|
await server?.close();
|
|
});
|
|
|
|
function fixture({ byteLength = 36_000_000_000 } = {}) {
|
|
const segmentCount = 1_501;
|
|
const manifestUrl = "/api/v1/observation-sessions/session-1/media/camera-1/manifest";
|
|
const generation = "a".repeat(64);
|
|
const streamUrl = manifestUrl.replace(
|
|
"/manifest",
|
|
`/epochs/1/recording.mp4?generation=${generation}`,
|
|
);
|
|
const manifest = {
|
|
schema_version: "missioncore.observation-recorded-media/v4",
|
|
source_id: "recorded.camera.camera-1",
|
|
generation_sha256: generation,
|
|
byte_length: byteLength,
|
|
timeline_start_seconds: 0,
|
|
timeline_end_seconds: 36_000,
|
|
synchronization: "host-arrival-best-effort",
|
|
epochs: [{
|
|
ordinal: 1,
|
|
timeline_start_seconds: 0,
|
|
timeline_end_seconds: 36_000,
|
|
media_type: 'video/mp4; codecs="avc1.640028"',
|
|
byte_length: byteLength,
|
|
stream_url: streamUrl,
|
|
segment_count: segmentCount,
|
|
random_access_sequences: [1, 1491, 1501],
|
|
segment_end_times_seconds: Array.from(
|
|
{ length: segmentCount },
|
|
(_value, index) => ((index + 1) * 36_000) / segmentCount,
|
|
),
|
|
}],
|
|
};
|
|
const source = {
|
|
id: manifest.source_id,
|
|
label: "Записанная камера",
|
|
modality: "video",
|
|
manifestUrl,
|
|
manifestGenerationSha256: generation,
|
|
byteLength,
|
|
mediaType: "video/mp4",
|
|
timelineStartSeconds: 0,
|
|
timelineEndSeconds: 36_000,
|
|
seekable: true,
|
|
synchronization: "host-arrival-best-effort",
|
|
};
|
|
return { source, manifest, generation, streamUrl };
|
|
}
|
|
|
|
function jsonResponse(payload, generation) {
|
|
return new Response(JSON.stringify(payload), {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
ETag: `"sha256:${generation}"`,
|
|
},
|
|
});
|
|
}
|
|
|
|
test("multi-hour camera admission fetches only its compact generation-bound manifest", async () => {
|
|
const { source, manifest, generation, streamUrl } = fixture();
|
|
const requested = [];
|
|
const archive = await fetchRecordedMediaArchive(source, {
|
|
fetcher: async (input, request = {}) => {
|
|
requested.push(String(input));
|
|
assert.equal(new Headers(request.headers).get("If-Match"), `"sha256:${generation}"`);
|
|
return jsonResponse(manifest, generation);
|
|
},
|
|
});
|
|
|
|
assert.deepEqual(requested, [source.manifestUrl]);
|
|
assert.equal(archive.byteLength, 36_000_000_000);
|
|
assert.equal(archive.manifest.epochs[0].streamUrl, streamUrl);
|
|
assert.equal(archive.manifest.epochs[0].segmentCount, 1_501);
|
|
assert.deepEqual(archive.manifest.epochs[0].randomAccessSequences, [1, 1491, 1501]);
|
|
});
|
|
|
|
test("first camera manifest request rejects a replaced generation", async () => {
|
|
const { source, manifest, generation } = fixture();
|
|
const replacementGeneration = "d".repeat(64);
|
|
await assert.rejects(
|
|
fetchRecordedMediaArchive(source, {
|
|
fetcher: async (_input, request = {}) => {
|
|
assert.equal(new Headers(request.headers).get("If-Match"), `"sha256:${generation}"`);
|
|
return jsonResponse(
|
|
{ ...manifest, generation_sha256: replacementGeneration },
|
|
replacementGeneration,
|
|
);
|
|
},
|
|
}),
|
|
/несовместим|заменён/,
|
|
);
|
|
});
|
|
|
|
test("camera manifest fails closed when epoch bytes do not match launch bytes", async () => {
|
|
const { source, manifest, generation } = fixture();
|
|
const mismatched = {
|
|
...manifest,
|
|
epochs: [{ ...manifest.epochs[0], byte_length: manifest.byte_length - 1 }],
|
|
};
|
|
await assert.rejects(
|
|
fetchRecordedMediaArchive(source, {
|
|
fetcher: async () => jsonResponse(mismatched, generation),
|
|
}),
|
|
/не совпадает/,
|
|
);
|
|
});
|
|
|
|
test("camera presentation gate opens only for the seekable selected epoch", () => {
|
|
assert.equal(recordedMediaPresentationState("loading", null, "g1", false), "loading");
|
|
assert.equal(recordedMediaPresentationState("ready", null, "g1", false), "loading");
|
|
assert.equal(recordedMediaPresentationState("ready", "g2", "g1", false), "loading");
|
|
assert.equal(recordedMediaPresentationState("ready", "g1", "g1", false), "ready");
|
|
assert.equal(recordedMediaPresentationState("ready", "g1", null, true), "waiting");
|
|
assert.equal(recordedMediaPresentationState("error", "g1", "g1", false), "error");
|
|
assert.equal(recordedMediaPresentationState("ready", "g1", "g1", false, "loading"), "loading");
|
|
assert.equal(recordedMediaPresentationState("ready", "g1", null, true, "loading"), "loading");
|
|
assert.equal(recordedMediaPresentationState("ready", "g1", "g1", false, "error"), "error");
|
|
});
|
|
|
|
test("decoded duration and seekable range cover the complete declared epoch", () => {
|
|
assert.equal(recordedMediaSeekableCoverage(20, 20, 20), true);
|
|
assert.equal(recordedMediaSeekableCoverage(19, 19, 20), true);
|
|
assert.equal(recordedMediaSeekableCoverage(18.99, 20, 20), false);
|
|
assert.equal(recordedMediaSeekableCoverage(20, 18.99, 20), false);
|
|
assert.equal(recordedMediaSeekableCoverage(20, 20, 20, 1, 1), true);
|
|
assert.equal(recordedMediaSeekableCoverage(20, 20, 20, 1, 1.01), false);
|
|
});
|
|
|
|
test("production replay derives bounded fragments and retains native range fallback", async () => {
|
|
const source = await readFile(
|
|
new URL("../src/components/RecordedFmp4Player.tsx", import.meta.url),
|
|
"utf8",
|
|
);
|
|
assert.match(source, /video\.src\s*=\s*descriptor\.streamUrl/);
|
|
assert.doesNotMatch(source, /new Blob\(/);
|
|
assert.match(source, /new MediaSource\(\)/);
|
|
assert.match(source, /RECORDED_MEDIA_SEGMENTS_AHEAD = 36/);
|
|
assert.match(source, /pumpRecordedSegmentWindow/);
|
|
assert.match(source, /waitForRecordedVideoTarget/);
|
|
assert.match(source, /removeRecordedMediaRange/);
|
|
assert.match(source, /hasPresentedFrame/);
|
|
assert.match(source, /retainForwardFrame/);
|
|
assert.match(source, /candidateTarget\.sequence >= previousTarget\.sequence/);
|
|
assert.match(source, /effectiveSegmentCount = segmentCount \?\? epoch\?\.segmentCount \?\? null/);
|
|
assert.match(source, /requestedSegmentSequence = segmentSequence \?\?/);
|
|
assert.match(source, /waitForRecordedVideoInitialFrame/);
|
|
assert.match(source, /setSegmentRecoveryGeneration/);
|
|
assert.match(source, /resumePlaybackIfRequested/);
|
|
assert.match(source, /!playbackPlayingRef\.current/);
|
|
assert.match(source, /await video\.play\(\)/);
|
|
|
|
assert.equal(recordedMediaDecodeStartSequence([1, 1491, 1501], 1500), 1491);
|
|
assert.deepEqual(
|
|
recordedMediaSegmentAppendOrder(new Set([1491, 1492]), 1491, 1500),
|
|
[1493, 1494, 1495, 1496, 1497, 1498, 1499, 1500],
|
|
);
|
|
|
|
const { manifest, generation } = fixture();
|
|
const epoch = {
|
|
ordinal: 1,
|
|
timelineStartSeconds: 0,
|
|
timelineEndSeconds: 36_000,
|
|
mediaType: manifest.epochs[0].media_type,
|
|
byteLength: manifest.epochs[0].byte_length,
|
|
streamUrl: manifest.epochs[0].stream_url,
|
|
segmentCount: manifest.epochs[0].segment_count,
|
|
randomAccessSequences: manifest.epochs[0].random_access_sequences,
|
|
segmentEndTimesSeconds: manifest.epochs[0].segment_end_times_seconds,
|
|
};
|
|
assert.equal(
|
|
recordedMediaFragmentUrl(epoch, generation, "init"),
|
|
`/api/v1/observation-sessions/session-1/media/camera-1/epochs/1/init.mp4?generation=${generation}`,
|
|
);
|
|
assert.equal(
|
|
recordedMediaFragmentUrl(epoch, generation, 17),
|
|
`/api/v1/observation-sessions/session-1/media/camera-1/epochs/1/segments/17.m4s?generation=${generation}`,
|
|
);
|
|
assert.throws(
|
|
() => recordedMediaFragmentUrl(epoch, "b".repeat(64), 17),
|
|
/не привязан/,
|
|
);
|
|
});
|
|
|
|
test("production replay derives its bounded fragment directly from the source clock", () => {
|
|
const ends = [0.101, 0.185, 0.286, 0.401];
|
|
const epochStart = 39.215263458;
|
|
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart), 1);
|
|
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 0.101), 1);
|
|
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 0.103), 2);
|
|
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 0.4), 4);
|
|
assert.equal(recordedMediaSegmentSequenceAtTime(ends, epochStart, epochStart + 1), null);
|
|
});
|
|
|
|
test("a corrupt fragment advances recovery to the next random-access frame", () => {
|
|
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 1), 11);
|
|
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 20), 21);
|
|
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 49), null);
|
|
assert.equal(nextRecordedMediaRandomAccessSequence([1, 11, 21, 49], 0), null);
|
|
});
|
|
|
|
test("paused seek displays the recovery keyframe until the source clock leaves the corrupt GOP", () => {
|
|
assert.equal(recordedMediaRecoveryTargetSequence(120, 120, 149), 149);
|
|
assert.equal(recordedMediaRecoveryTargetSequence(130, 120, 149), 149);
|
|
assert.equal(recordedMediaRecoveryTargetSequence(119, 120, 149), 119);
|
|
assert.equal(recordedMediaRecoveryTargetSequence(149, 120, 149), 149);
|
|
assert.equal(recordedMediaRecoveryTargetSequence(151, 120, 149), 151);
|
|
assert.equal(recordedMediaRecoveryTargetSequence(null, 120, 149), null);
|
|
assert.equal(recordedMediaRecoveryTargetSequence(120, null, null), 120);
|
|
});
|
|
|
|
test("camera admission selects one bounded epoch while the shared clock is outside video", () => {
|
|
const epochs = [
|
|
{ ordinal: 1, timelineStartSeconds: 39, timelineEndSeconds: 100 },
|
|
{ ordinal: 2, timelineStartSeconds: 120, timelineEndSeconds: 180 },
|
|
];
|
|
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 0).ordinal, 1);
|
|
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 70).ordinal, 1);
|
|
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 110).ordinal, 2);
|
|
assert.equal(selectRecordedMediaPreparationEpoch(epochs, 200).ordinal, 2);
|
|
});
|
|
|
|
test("recorded player preserves forward rolling playback but seeks backward clip loops", () => {
|
|
assert.equal(recordedMediaCanRollTarget(20, 21, true, true), true);
|
|
assert.equal(recordedMediaCanRollTarget(20, 20, true, true), true);
|
|
assert.equal(recordedMediaCanRollTarget(20, 1, true, true), false);
|
|
assert.equal(recordedMediaCanRollTarget(20, 21, false, true), false);
|
|
assert.equal(recordedMediaCanRollTarget(20, 21, true, false), false);
|
|
});
|
|
|
|
test("recorded player skips only a proven buffered corrupt timestamp interval", () => {
|
|
assert.equal(recordedMediaTimestampStallRecoveryTarget(11.422, [[0, 16.287]]), 11.602);
|
|
assert.equal(recordedMediaTimestampStallRecoveryTarget(16.25, [[0, 16.287]]), null);
|
|
assert.equal(recordedMediaTimestampStallRecoveryTarget(20, [[0, 16.287]]), null);
|
|
});
|
|
|
|
test("loading and error overlays fully conceal recorded camera pixels", async () => {
|
|
const css = await readFile(
|
|
new URL("../../../packages/spatial-ui/src/observation.css", import.meta.url),
|
|
"utf8",
|
|
);
|
|
assert.match(
|
|
css,
|
|
/\.recorded-media-player:not\(\[data-state="ready"\]\) \.observation-media__asset\s*\{[^}]*visibility:\s*hidden/s,
|
|
);
|
|
assert.match(
|
|
css,
|
|
/\.recorded-media-player__notice\s*\{[^}]*inset:\s*0;[^}]*background:\s*#070809/s,
|
|
);
|
|
});
|
|
|
|
test("point-cloud fullscreen keeps the admitted recorded camera worker mounted", async () => {
|
|
const source = await readFile(
|
|
new URL("../src/workspaces/spatial/SpatialWorkspace.tsx", import.meta.url),
|
|
"utf8",
|
|
);
|
|
assert.match(source, /\{visibleMediaSources\.map\(\(source, index\) => \(/);
|
|
assert.match(source, /hidden=\{pointCloudFocused \|\| unifiedPerception\}/);
|
|
assert.doesNotMatch(
|
|
source,
|
|
/\{!pointCloudFocused \? visibleMediaSources\.map/,
|
|
);
|
|
assert.doesNotMatch(
|
|
source,
|
|
/\(pointCloudFocused \|\| !observationLayout\.visibleSourceIds\.has\(source\.id\)\)/,
|
|
);
|
|
});
|