fix(lab): make recorded replay seek-safe
This commit is contained in:
@@ -14,6 +14,7 @@ let selectM4ThreatTimelineSequence;
|
||||
let advanceRecordedEvidencePlayback;
|
||||
let synchronizeRecordedEvidencePlayback;
|
||||
let m4ThreatChunkWindowStarts;
|
||||
let cancelM4ThreatChunkRequestsOutsideWindow;
|
||||
let buildM4LocalSurface;
|
||||
|
||||
const resultId = `m4-threat-replay-${"a".repeat(64)}`;
|
||||
@@ -38,7 +39,10 @@ before(async () => {
|
||||
} = await server.ssrLoadModule(
|
||||
"/src/components/laboratory/useRecordedEvidencePlayback.ts",
|
||||
));
|
||||
({ m4ThreatChunkWindowStarts } = await server.ssrLoadModule(
|
||||
({
|
||||
m4ThreatChunkWindowStarts,
|
||||
cancelM4ThreatChunkRequestsOutsideWindow,
|
||||
} = await server.ssrLoadModule(
|
||||
"/src/workspaces/laboratory/useM4ThreatTimeline.ts",
|
||||
));
|
||||
({ buildM4LocalSurface } = await server.ssrLoadModule(
|
||||
@@ -375,6 +379,23 @@ test("M4.6 spatial buffering keeps previous, active and two future chunks", () =
|
||||
assert.deepEqual(m4ThreatChunkWindowStarts(0, 24, 4489), [0, 24, 48]);
|
||||
});
|
||||
|
||||
test("M4.6 spatial buffering drops stale in-flight windows across rapid jumps", () => {
|
||||
const aborted = [];
|
||||
const controller = (start) => ({ abort: () => aborted.push(start) });
|
||||
const inFlight = new Map(
|
||||
m4ThreatChunkWindowStarts(0, 24, 4489).map((start) => [start, controller(start)]),
|
||||
);
|
||||
for (const activeStart of [1488, 4488]) {
|
||||
const desired = m4ThreatChunkWindowStarts(activeStart, 24, 4489);
|
||||
cancelM4ThreatChunkRequestsOutsideWindow(inFlight, desired);
|
||||
for (const start of desired) {
|
||||
if (!inFlight.has(start)) inFlight.set(start, controller(start));
|
||||
}
|
||||
}
|
||||
assert.deepEqual([...inFlight.keys()], [4464, 4488]);
|
||||
assert.deepEqual(aborted, [0, 24, 48, 1464, 1488, 1512, 1536]);
|
||||
});
|
||||
|
||||
test("recorded evidence clock advances by selected rate and stops at the sealed end", () => {
|
||||
const range = { startSeconds: 10, endSeconds: 20 };
|
||||
assert.deepEqual(
|
||||
@@ -448,6 +469,7 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
|
||||
assert.match(visual, /const spatialFrame = frame\?\.spatialAvailable/);
|
||||
assert.match(visual, /<ObservationTimeline/);
|
||||
assert.match(visual, /useM4ThreatTimelineFrame/);
|
||||
assert.match(visual, /resolveObservationSessionReplay\(timeline\.recordedSourceSessionId/);
|
||||
assert.match(visual, /label: "VIDEO"/);
|
||||
assert.match(visual, /label: "CAMERA"/);
|
||||
assert.match(visual, /label: "3D"/);
|
||||
@@ -463,11 +485,11 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
|
||||
assert.match(visual, /secondaryMode=\{\{/);
|
||||
assert.match(visual, /playback=\{playbackController\.playback\}/);
|
||||
assert.match(visual, /clock: mediaMode === "video" \? "external" : "animation"/);
|
||||
assert.match(visual, /segmentSequence=\{frame \? frame\.sequence \+ 1 : undefined\}/);
|
||||
assert.match(visual, /timelineFrame\.activeSequence \+ 1/);
|
||||
assert.match(visual, /segmentCount=\{timeline\.frameCount\}/);
|
||||
assert.match(visual, /onPlaybackChange=\{playbackController\.synchronize\}/);
|
||||
assert.match(visual, /onPlayingRejected=\{\(\) => playbackController\.setPlaying\(false\)\}/);
|
||||
assert.match(visual, /currentSeconds: playbackController\.playback\.currentSeconds/);
|
||||
assert.doesNotMatch(visual, /setPlaying\(false\)/);
|
||||
assert.match(visualCss, /m4-replay-threat-visual__deck > \.nodedc-split-pane/);
|
||||
assert.match(visualCss, /m4-replay-threat-visual__pane-toolbar\[data-pane-toolbar="media"\]/);
|
||||
assert.match(visualCss, /m4-replay-threat-visual__pane-toolbar\[data-pane-toolbar="spatial"\]/);
|
||||
|
||||
@@ -953,7 +953,7 @@ test("replay decodes opaque recorded cameras and their same-origin fMP4 manifest
|
||||
);
|
||||
|
||||
const manifest = decodeObservationRecordedMediaManifest({
|
||||
schema_version: "missioncore.observation-recorded-media/v3",
|
||||
schema_version: "missioncore.observation-recorded-media/v4",
|
||||
source_id: source.id,
|
||||
generation_sha256: "c".repeat(64),
|
||||
byte_length: 3_266,
|
||||
@@ -970,11 +970,17 @@ test("replay decodes opaque recorded cameras and their same-origin fMP4 manifest
|
||||
"/manifest",
|
||||
`/epochs/1/recording.mp4?generation=${"c".repeat(64)}`,
|
||||
),
|
||||
segment_count: 2,
|
||||
random_access_sequences: [1],
|
||||
segment_end_times_seconds: [9.75, 19.75],
|
||||
}],
|
||||
}, decoded.mediaSources[0]);
|
||||
assert.equal(manifest.epochs[0].byteLength, 3_266);
|
||||
assert.match(manifest.epochs[0].streamUrl, /recording\.mp4\?generation=/);
|
||||
assert.equal(manifest.epochs[0].timelineStartSeconds, 0.25);
|
||||
assert.equal(manifest.epochs[0].segmentCount, 2);
|
||||
assert.deepEqual(manifest.epochs[0].randomAccessSequences, [1]);
|
||||
assert.deepEqual(manifest.epochs[0].segmentEndTimesSeconds, [9.75, 19.75]);
|
||||
assert.throws(
|
||||
() => decodeObservationRecordedMediaManifest({
|
||||
...{
|
||||
@@ -992,7 +998,7 @@ test("replay decodes opaque recorded cameras and their same-origin fMP4 manifest
|
||||
);
|
||||
assert.throws(
|
||||
() => decodeObservationRecordedMediaManifest({
|
||||
schema_version: "missioncore.observation-recorded-media/v3",
|
||||
schema_version: "missioncore.observation-recorded-media/v4",
|
||||
source_id: source.id,
|
||||
generation_sha256: "c".repeat(64),
|
||||
byte_length: 3_267,
|
||||
@@ -1106,7 +1112,7 @@ test("recorded camera contracts reject foreign origins, path escapes and unknown
|
||||
}));
|
||||
assert.throws(
|
||||
() => decodeObservationRecordedMediaManifest({
|
||||
schema_version: "missioncore.observation-recorded-media/v3",
|
||||
schema_version: "missioncore.observation-recorded-media/v4",
|
||||
source_id: base.id,
|
||||
generation_sha256: "c".repeat(64),
|
||||
byte_length: 384,
|
||||
@@ -1120,6 +1126,9 @@ test("recorded camera contracts reject foreign origins, path escapes and unknown
|
||||
media_type: 'video/mp4; codecs="avc1.640028"',
|
||||
byte_length: 384,
|
||||
stream_url: "file:///private/recording.mp4",
|
||||
segment_count: 1,
|
||||
random_access_sequences: [1],
|
||||
segment_end_times_seconds: [20],
|
||||
}],
|
||||
}, decoded.mediaSources[0]),
|
||||
/небезопасный API URL/,
|
||||
|
||||
@@ -9,6 +9,8 @@ let fetchRecordedMediaArchive;
|
||||
let recordedMediaPresentationState;
|
||||
let recordedMediaSeekableCoverage;
|
||||
let recordedMediaFragmentUrl;
|
||||
let recordedMediaDecodeStartSequence;
|
||||
let recordedMediaSegmentAppendOrder;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
@@ -21,6 +23,8 @@ before(async () => {
|
||||
recordedMediaPresentationState,
|
||||
recordedMediaSeekableCoverage,
|
||||
recordedMediaFragmentUrl,
|
||||
recordedMediaDecodeStartSequence,
|
||||
recordedMediaSegmentAppendOrder,
|
||||
} = await server.ssrLoadModule("/src/components/RecordedFmp4Player.tsx"));
|
||||
});
|
||||
|
||||
@@ -29,6 +33,7 @@ after(async () => {
|
||||
});
|
||||
|
||||
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(
|
||||
@@ -36,7 +41,7 @@ function fixture({ byteLength = 36_000_000_000 } = {}) {
|
||||
`/epochs/1/recording.mp4?generation=${generation}`,
|
||||
);
|
||||
const manifest = {
|
||||
schema_version: "missioncore.observation-recorded-media/v3",
|
||||
schema_version: "missioncore.observation-recorded-media/v4",
|
||||
source_id: "recorded.camera.camera-1",
|
||||
generation_sha256: generation,
|
||||
byte_length: byteLength,
|
||||
@@ -50,6 +55,12 @@ function fixture({ byteLength = 36_000_000_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 = {
|
||||
@@ -92,6 +103,8 @@ test("multi-hour camera admission fetches only its compact generation-bound mani
|
||||
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 () => {
|
||||
@@ -154,8 +167,16 @@ test("recorded player keeps full-archive range fallback and uses bounded generat
|
||||
assert.match(source, /video\.src\s*=\s*descriptor\.streamUrl/);
|
||||
assert.doesNotMatch(source, /new Blob\(/);
|
||||
assert.match(source, /new MediaSource\(\)/);
|
||||
assert.match(source, /segmentSequence \+ 36/);
|
||||
assert.match(source, /RECORDED_MEDIA_SEGMENTS_AHEAD = 36/);
|
||||
assert.match(source, /pumpRecordedSegmentWindow/);
|
||||
assert.match(source, /waitForRecordedVideoTarget/);
|
||||
assert.match(source, /removeRecordedMediaRange/);
|
||||
|
||||
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 = {
|
||||
@@ -165,6 +186,9 @@ test("recorded player keeps full-archive range fallback and uses bounded generat
|
||||
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"),
|
||||
|
||||
Reference in New Issue
Block a user