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; before(async () => { server = await createServer({ appType: "custom", logLevel: "silent", server: { middlewareMode: true }, }); ({ fetchRecordedMediaArchive, recordedMediaPresentationState, recordedMediaSeekableCoverage, } = await server.ssrLoadModule("/src/components/RecordedFmp4Player.tsx")); }); after(async () => { await server?.close(); }); function fixture({ byteLength = 36_000_000_000 } = {}) { 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/v3", 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, }], }; 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); }); 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("recorded player range-streams and never builds a whole-video RAM Blob", 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\(|response\.arrayBuffer\(|SourceBuffer/); }); test("loading and error overlays fully conceal recorded camera pixels", async () => { const css = await readFile( new URL("../src/styles/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/Workspaces.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\)\)/, ); });