import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import { after, before, test } from "node:test"; import { createServer } from "vite"; let server; let fetchVerifiedRecordedMediaArchive; let recordedMediaPresentationState; let recordedMediaSeekableCoverage; let appendRecordedMediaBuffer; before(async () => { server = await createServer({ appType: "custom", logLevel: "silent", server: { middlewareMode: true }, }); ({ fetchVerifiedRecordedMediaArchive, recordedMediaPresentationState, recordedMediaSeekableCoverage, appendRecordedMediaBuffer, } = await server.ssrLoadModule("/src/components/RecordedFmp4Player.tsx")); }); after(async () => { await server?.close(); }); function digest(payload) { return createHash("sha256").update(payload).digest("hex"); } function fixture() { const manifestUrl = "/api/v1/observation-sessions/session-1/media/camera-1/manifest"; const init = Buffer.from("canonical-init"); const first = Buffer.from("canonical-first-fragment"); const second = Buffer.from("canonical-second-fragment"); const generation = "a".repeat(64); const segmentPrefix = manifestUrl.replace("/manifest", "/epochs/1/segments/"); const manifest = { schema_version: "missioncore.observation-recorded-media/v2", source_id: "recorded.camera.camera-1", generation_sha256: generation, byte_length: init.byteLength + first.byteLength + second.byteLength, timeline_start_seconds: 0, timeline_end_seconds: 20, synchronization: "host-arrival-best-effort", epochs: [{ ordinal: 1, timeline_start_seconds: 0, timeline_end_seconds: 20, media_type: 'video/mp4; codecs="avc1.640028"', init_url: manifestUrl.replace("/manifest", "/epochs/1/init.mp4"), init_byte_length: init.byteLength, init_sha256: digest(init), segment_count: 2, segment_url_prefix: segmentPrefix, segments: [first, second].map((payload, index) => ({ sequence: index + 1, url: `${segmentPrefix}${index + 1}.m4s`, byte_length: payload.byteLength, sha256: digest(payload), })), }], }; const source = { id: manifest.source_id, label: "Записанная камера", modality: "video", manifestUrl, manifestGenerationSha256: generation, byteLength: manifest.byte_length, mediaType: "video/mp4", timelineStartSeconds: 0, timelineEndSeconds: 20, seekable: true, synchronization: "host-arrival-best-effort", }; return { source, manifest, generation, init, first, second }; } function jsonResponse(payload, generation) { return new Response(JSON.stringify(payload), { status: 200, headers: { "Content-Type": "application/json", ETag: `"sha256:${generation}"`, }, }); } function mediaResponse(payload, sha = digest(payload), length = payload.byteLength) { return new Response(payload, { status: 200, headers: { "Content-Length": String(length), ETag: `"sha256:${sha}"`, }, }); } function deferred() { let resolve; let reject; const promise = new Promise((resolvePromise, rejectPromise) => { resolve = resolvePromise; reject = rejectPromise; }); return { promise, resolve, reject }; } async function flushAsync() { await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0)); } test("recorded camera archive stays pending until every canonical byte is verified", async () => { const { source, manifest, generation, init, first, second } = fixture(); const finalSegment = deferred(); const requested = []; let manifestRequests = 0; const fetcher = async (input, request = {}) => { const url = String(input); requested.push(url); const headers = new Headers(request.headers); if (url === source.manifestUrl) { manifestRequests += 1; assert.equal(headers.get("If-Match"), `"sha256:${generation}"`); return jsonResponse(manifest, generation); } const epoch = manifest.epochs[0]; if (url === epoch.init_url) { assert.equal(headers.get("If-Match"), `"sha256:${epoch.init_sha256}"`); return mediaResponse(init); } if (url === epoch.segments[0].url) return mediaResponse(first); if (url === epoch.segments[1].url) return finalSegment.promise; throw new Error(`Unexpected URL ${url}`); }; let settled = false; const archivePromise = fetchVerifiedRecordedMediaArchive(source, { fetcher }) .then((archive) => { settled = true; return archive; }); await flushAsync(); assert.equal(settled, false, "init and a partial segment prefix must not publish the camera"); assert.deepEqual(requested.slice(0, 4), [ source.manifestUrl, manifest.epochs[0].init_url, manifest.epochs[0].segments[0].url, manifest.epochs[0].segments[1].url, ]); finalSegment.resolve(mediaResponse(second)); const archive = await archivePromise; assert.equal(manifestRequests, 2, "the immutable generation is revalidated after transfer"); assert.equal(archive.byteLength, init.byteLength + first.byteLength + second.byteLength); assert.equal(archive.epochs[0].segments.length, 2); }); test("first camera manifest request is launch-generation bound and rejects replacement", async () => { const { source, manifest, generation } = fixture(); const replacementGeneration = "d".repeat(64); let requests = 0; const fetcher = async (input, request = {}) => { requests += 1; assert.equal(String(input), source.manifestUrl); assert.equal( new Headers(request.headers).get("If-Match"), `"sha256:${generation}"`, ); return jsonResponse( { ...manifest, generation_sha256: replacementGeneration }, replacementGeneration, ); }; await assert.rejects( fetchVerifiedRecordedMediaArchive(source, { fetcher }), /несовместим|заменён/, ); assert.equal(requests, 1); }); test("recorded camera archive fails closed on truncation despite plausible headers", async () => { const { source, manifest, generation, init, first, second } = fixture(); const fetcher = async (input) => { const url = String(input); if (url === source.manifestUrl) return jsonResponse(manifest, generation); const epoch = manifest.epochs[0]; if (url === epoch.init_url) return mediaResponse(init); if (url === epoch.segments[0].url) return mediaResponse(first); if (url === epoch.segments[1].url) { return mediaResponse(second.subarray(0, second.byteLength - 1), digest(second), second.byteLength); } throw new Error(`Unexpected URL ${url}`); }; await assert.rejects( fetchVerifiedRecordedMediaArchive(source, { fetcher }), /усечён/, ); }); test("recorded camera archive fails closed when bytes are replaced under an old ETag", async () => { const { source, manifest, generation, init, first, second } = fixture(); const replacement = Buffer.from(second.map((value) => value ^ 0xff)); assert.equal(replacement.byteLength, second.byteLength); const fetcher = async (input) => { const url = String(input); if (url === source.manifestUrl) return jsonResponse(manifest, generation); const epoch = manifest.epochs[0]; if (url === epoch.init_url) return mediaResponse(init); if (url === epoch.segments[0].url) return mediaResponse(first); if (url === epoch.segments[1].url) { return mediaResponse(replacement, digest(second), second.byteLength); } throw new Error(`Unexpected URL ${url}`); }; await assert.rejects( fetchVerifiedRecordedMediaArchive(source, { fetcher }), /SHA-256/, ); }); test("camera presentation gate opens only for the completely appended 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("aborting SourceBuffer append removes every temporary listener", async () => { const listeners = new Map(); const sourceBuffer = { addEventListener(type, listener) { const bucket = listeners.get(type) ?? new Set(); bucket.add(listener); listeners.set(type, bucket); }, removeEventListener(type, listener) { listeners.get(type)?.delete(listener); }, appendBuffer() {}, }; const abort = new AbortController(); const pending = appendRecordedMediaBuffer( sourceBuffer, new ArrayBuffer(8), abort.signal, ); assert.equal(listeners.get("updateend")?.size, 1); assert.equal(listeners.get("error")?.size, 1); abort.abort(); await assert.rejects(pending, (error) => error?.name === "AbortError"); assert.equal(listeners.get("updateend")?.size, 0); assert.equal(listeners.get("error")?.size, 0); }); test("verified camera archives remain immutable for safe player remount", async () => { const source = await readFile( new URL("../src/components/RecordedFmp4Player.tsx", import.meta.url), "utf8", ); assert.doesNotMatch(source, /\.init\s*=\s*new ArrayBuffer/); assert.doesNotMatch(source, /\.segments\.length\s*=\s*0/); }); 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, ); });