import { expect, test } from "@playwright/test"; import fs from "node:fs"; import path from "node:path"; const imagePath = path.resolve(import.meta.dirname, "../../../tests/files/web/media/sequencer-frame.png"); const audioPath = path.resolve(import.meta.dirname, "../../../tests/files/web/media/sequencer-silence.wav"); test("indexes, seeks, cancels and reopens a bounded one-million-frame media timeline", async ({ page }) => { test.setTimeout(120_000); await page.goto("/"); const image = fs.readFileSync(imagePath); const audio = fs.readFileSync(audioPath); const result = await page.evaluate(async ({ imageBytes, audioBytes }) => { const [mediaModule, storageModule] = await Promise.all([ import("/src/sequencer/LongMediaTimeline.ts"), import("/src/storage/StorageClient.ts"), ]); const { buildLongMediaTimelineIndex, deserializeLongMediaSessionManifest, gateSequencerCodec, parseLongMediaSessionManifest, sequencerRuntimeCapabilities, serializeLongMediaSessionManifest, } = mediaModule; const { StorageClient } = storageModule; const started = performance.now(); const projectId = `long-media-${Date.now()}`; const cacheMaxBytes = 20 * 1024; const frameCount = 1_000_000; const stripFrames = 100; const imageStripCount = frameCount / stripFrames; const timeline = { schemaVersion: 1 as const, id: "sequencer:long-media", revision: 1, frameStart: 0, frameEnd: frameCount, fpsNumerator: 24_000, fpsDenominator: 1_001, strips: [ { id: "strip:long-audio", name: "Long Audio", type: "SOUND" as const, channel: 1, frameStart: 0, frameEnd: frameCount, sourceStart: 0, sourceEnd: frameCount, speed: 1, muted: false, locked: false, sourceId: "media:audio", sourcePath: "//media/sequencer-silence.wav", mimeType: "audio/wav", }, ...Array.from({ length: imageStripCount }, (_, index) => ({ id: `strip:image:${index}`, name: `Image ${index}`, type: "IMAGE" as const, channel: 2, frameStart: index * stripFrames, frameEnd: (index + 1) * stripFrames, sourceStart: 0, sourceEnd: 1, speed: 1, muted: false, locked: false, sourceId: "media:image", sourcePath: "//media/sequencer-frame.png", mimeType: "image/png", })), ], }; const writer = new StorageClient(); const storedImage = await writer.putAsset(projectId, imageBytes.buffer.slice(imageBytes.byteOffset, imageBytes.byteOffset + imageBytes.byteLength), "image/png", "media/sequencer-frame.png"); const storedAudio = await writer.putAsset(projectId, audioBytes.buffer.slice(audioBytes.byteOffset, audioBytes.byteOffset + audioBytes.byteLength), "audio/wav", "media/sequencer-silence.wav"); const cancellation = new AbortController(); const cancelledBuild = buildLongMediaTimelineIndex(timeline, cancellation.signal) .then(() => "unexpected-success") .catch((error: unknown) => error instanceof Error ? error.message.split(":", 1)[0] : String(error)); setTimeout(() => cancellation.abort(), 0); const indexCancelCode = await cancelledBuild; const indexStarted = performance.now(); const directIndex = await buildLongMediaTimelineIndex(timeline, new AbortController().signal); const indexBuildMs = Math.round(performance.now() - indexStarted); const seekCancellation = new AbortController(); seekCancellation.abort(); let seekCancelCode = ""; try { directIndex.resolve(0, seekCancellation.signal); } catch (error) { seekCancelCode = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } interface WorkerClient { worker: Worker; init(timelineValue: unknown, assets: Array<{ sourceId: string; data: ArrayBuffer }>): Promise<{ stripCount: number; bucketCount: number; referenceCount: number; estimatedBytes: number }>; seek(frame: number, decodeDelayMs?: number): Promise; cancel(): void; dispose(): Promise<{ releasedCacheBytes: number; cacheBytesAfter: number }>; terminate(): void; } const createWorkerClient = (): WorkerClient => { const worker = new Worker("/src/workers/long-media.worker.ts", { type: "module" }); let counter = 0; let readyResolve: ((value: { stripCount: number; bucketCount: number; referenceCount: number; estimatedBytes: number }) => void) | undefined; let readyReject: ((reason: Error) => void) | undefined; let disposeResolve: ((status: { releasedCacheBytes: number; cacheBytesAfter: number }) => void) | undefined; const pending = new Map void; reject: (reason: Error) => void }>(); worker.onmessage = (event: MessageEvent) => { const message = event.data; if (message.type === "ready") { readyResolve?.(message.index); readyResolve = undefined; readyReject = undefined; return; } if (message.type === "disposed") { disposeResolve?.({ releasedCacheBytes: message.releasedCacheBytes, cacheBytesAfter: message.cacheBytesAfter }); disposeResolve = undefined; return; } if (message.type === "error") { if (message.requestId) { pending.get(message.requestId)?.reject(new Error(message.message)); pending.delete(message.requestId); } else { readyReject?.(new Error(message.message)); readyResolve = undefined; readyReject = undefined; } return; } if (message.type === "seekResult") { pending.get(message.requestId)?.resolve(message.result); pending.delete(message.requestId); } }; worker.onerror = (event) => { const error = new Error(event.message); readyReject?.(error); for (const request of pending.values()) request.reject(error); pending.clear(); }; return { worker, init: (timelineValue, assets) => new Promise((resolve, reject) => { readyResolve = resolve; readyReject = reject; worker.postMessage({ type: "init", timeline: timelineValue, assets, cacheMaxBytes }, assets.map((asset) => asset.data)); }), seek: (frame, decodeDelayMs = 0) => new Promise((resolve, reject) => { const requestId = `seek-${++counter}`; pending.set(requestId, { resolve, reject }); worker.postMessage({ type: "seek", requestId, frame, decodeDelayMs }); }), cancel: () => worker.postMessage({ type: "cancel" }), dispose: () => new Promise((resolve) => { disposeResolve = resolve; worker.postMessage({ type: "dispose" }); }), terminate: () => worker.terminate(), }; }; const sourceImage = await writer.readAsset(projectId, storedImage.sha256); const sourceAudio = await writer.readAsset(projectId, storedAudio.sha256); const client = createWorkerClient(); const workerIndex = await client.init(timeline, [ { sourceId: "media:image", data: sourceImage.data }, { sourceId: "media:audio", data: sourceAudio.data }, ]); const coldStarted = performance.now(); const cold = await client.seek(123_450); const coldSeekMs = Math.round(performance.now() - coldStarted); const hotStarted = performance.now(); const hot = await client.seek(123_450); const hotSeekMs = Math.round(performance.now() - hotStarted); const endpoints = await Promise.all([0, 500_000, 999_999].map((frame) => client.seek(frame))); const randomFrames = Array.from({ length: 64 }, (_, index) => (index * 104_729) % frameCount); const randomStarted = performance.now(); const random = []; for (const frame of randomFrames) random.push(await client.seek(frame)); const randomSeekMs = Math.round(performance.now() - randomStarted); const supersededPromise = client.seek(250_000, 50); const latestPromise = client.seek(765_432, 0); const [superseded, latest] = await Promise.all([supersededPromise, latestPromise]); const cancelledSeekPromise = client.seek(333_333, 100); setTimeout(() => client.cancel(), 0); const cancelledSeek = await cancelledSeekPromise; const manifest = parseLongMediaSessionManifest({ schemaVersion: 1, timeline, currentFrame: latest.frame, cacheMaxBytes, assets: [ { sourceId: "media:image", sha256: storedImage.sha256, mimeType: "image/png" }, { sourceId: "media:audio", sha256: storedAudio.sha256, mimeType: "audio/wav" }, ], }); const serialized = serializeLongMediaSessionManifest(manifest); const manifestSha256 = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", serialized.slice(0))), (value) => value.toString(16).padStart(2, "0")).join(""); const storedSession = await writer.putAsset(projectId, serialized, "application/vnd.blender.long-media+json", "cache/long-media-session.json"); const disposed = await client.dispose(); client.terminate(); writer.terminate(); const reader = new StorageClient(); const reopenedSessionAsset = await reader.readAsset(projectId, storedSession.sha256); const reopenedManifest = deserializeLongMediaSessionManifest(reopenedSessionAsset.data); const reopenedImage = await reader.readAsset(projectId, reopenedManifest.assets.find((asset) => asset.sourceId === "media:image")!.sha256); const reopenedAudio = await reader.readAsset(projectId, reopenedManifest.assets.find((asset) => asset.sourceId === "media:audio")!.sha256); reader.terminate(); const restarted = createWorkerClient(); const restartedIndex = await restarted.init(reopenedManifest.timeline, [ { sourceId: "media:image", data: reopenedImage.data }, { sourceId: "media:audio", data: reopenedAudio.data }, ]); const reopenedSeek = await restarted.seek(reopenedManifest.currentFrame); const restartedDisposed = await restarted.dispose(); restarted.terminate(); const codecRequest = { schemaVersion: 1 as const, stripType: "MOVIE" as const, mimeType: "video/mp4", byteLength: 1, sourceSha256: "0".repeat(64), }; const codec = gateSequencerCodec(codecRequest, { ...codecRequest, status: "BLOCKED", backend: null, reason: "RUNTIME_UNAVAILABLE", decoded: null, }); const runtime = sequencerRuntimeCapabilities(); let corruptManifestCode = ""; try { parseLongMediaSessionManifest({ ...manifest, assets: [{ ...manifest.assets[0], sha256: "bad" }, manifest.assets[1]] }); } catch (error) { corruptManifestCode = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } return { frameCount, stripCount: timeline.strips.length, localBytes: { image: imageBytes.byteLength, audio: audioBytes.byteLength, session: reopenedSessionAsset.data.byteLength }, indexCancelCode, seekCancelCode, indexBuildMs, directIndex: directIndex.stats, workerIndex, restartedIndex, cold: { status: cold.status, strips: cold.strips.length, cache: cold.cache, elapsedMs: coldSeekMs }, hot: { status: hot.status, strips: hot.strips.length, cache: hot.cache, elapsedMs: hotSeekMs }, endpoints: endpoints.map((item) => [item.frame, item.status, item.strips.length]), randomCompleted: random.filter((item) => item.status === "COMPLETED").length, randomCache: random.at(-1)?.cache, randomSeekMs, latest: [superseded.status, latest.status, latest.frame], cancelledSeek: cancelledSeek.status, disposed, restartedDisposed, manifestSha256, storedSessionSha256: storedSession.sha256, reopenedFrame: reopenedSeek.frame, reopenedStatus: reopenedSeek.status, codec: { status: codec.status, code: codec.issues[0]?.code }, localEncoding: runtime.localEncoding, corruptManifestCode, elapsedMs: Math.round(performance.now() - started), }; }, { imageBytes: new Uint8Array(image), audioBytes: new Uint8Array(audio) }); console.log("long-media-performance", JSON.stringify({ frameCount: result.frameCount, stripCount: result.stripCount, referenceCount: result.workerIndex.referenceCount, indexBuildMs: result.indexBuildMs, coldSeekMs: result.cold.elapsedMs, hotSeekMs: result.hot.elapsedMs, randomSeekMs: result.randomSeekMs, cacheBytes: result.randomCache?.bytes, evictions: result.randomCache?.evictions, releasedCacheBytes: result.disposed.releasedCacheBytes, cacheBytesAfterDispose: result.disposed.cacheBytesAfter, restartedReleasedCacheBytes: result.restartedDisposed.releasedCacheBytes, restartedCacheBytesAfterDispose: result.restartedDisposed.cacheBytesAfter, workerRestartRecovered: result.reopenedStatus === "COMPLETED", manifestSha256: result.manifestSha256, elapsedMs: result.elapsedMs, })); expect(result).toMatchObject({ frameCount: 1_000_000, stripCount: 10_001, indexCancelCode: "SEQUENCER_CANCELLED", seekCancelCode: "SEQUENCER_CANCELLED", latest: ["SUPERSEDED", "COMPLETED", 765_432], cancelledSeek: "CANCELLED", reopenedFrame: 765_432, reopenedStatus: "COMPLETED", codec: { status: "BLOCKED", code: "SEQUENCER_CODEC_UNSUPPORTED" }, localEncoding: "BLOCKED", corruptManifestCode: "SEQUENCER_SCHEMA_INVALID", }); expect(result.localBytes).toMatchObject({ image: 261, audio: 16_044 }); expect(result.localBytes.session).toBeGreaterThan(1_000_000); expect(result.directIndex).toEqual(result.workerIndex); expect(result.restartedIndex).toEqual(result.workerIndex); expect(result.workerIndex.stripCount).toBe(10_001); expect(result.workerIndex.referenceCount).toBeLessThan(25_000); expect(result.workerIndex.estimatedBytes).toBeLessThan(256 * 1024); expect(result.indexBuildMs).toBeLessThan(10_000); expect(result.cold).toMatchObject({ status: "COMPLETED", strips: 2 }); expect(result.hot).toMatchObject({ status: "COMPLETED", strips: 2 }); expect(result.hot.cache.hits).toBeGreaterThan(result.cold.cache.hits); expect(result.hot.cache.bytes).toBeLessThanOrEqual(result.hot.cache.maxBytes); expect(result.endpoints).toEqual([[0, "COMPLETED", 2], [500_000, "COMPLETED", 2], [999_999, "COMPLETED", 2]]); expect(result.randomCompleted).toBe(64); expect(result.randomCache?.bytes).toBeLessThanOrEqual(20 * 1024); expect(result.randomCache?.evictions).toBeGreaterThan(0); expect(result.randomCache?.keys).toEqual(expect.arrayContaining(["media:audio:597927", "media:image:1"])); expect(result.randomSeekMs).toBeLessThan(15_000); expect(result.disposed.releasedCacheBytes).toBeLessThanOrEqual(20 * 1024); expect(result.disposed.cacheBytesAfter).toBe(0); expect(result.restartedDisposed.releasedCacheBytes).toBeLessThanOrEqual(20 * 1024); expect(result.restartedDisposed.cacheBytesAfter).toBe(0); expect(result.manifestSha256).toBe(result.storedSessionSha256); expect(result.manifestSha256).toBe("d2e7e3c5ed9ae4fda6fe358cebb474f5774f1d2037dff3df9b6dfde9e0bebd2d"); expect(result.elapsedMs).toBeLessThan(30_000); });