import { SEQUENCER_MEDIA_PROXY_MAX_BYTES, createSequencerMediaCacheManifest, computeSequencerMediaCacheIdentity, parseSequencerMediaProxyProfile, sequencerMediaCacheKey, verifySequencerMediaCacheEntry, SequencerMediaCacheValidationError, type SequencerMediaCacheManifestIR, type SequencerMediaProxyProfileIR, } from "../../../protocol/sequencer-media-cache"; import { gateSequencerCodec, parseSequencerCodecProbeRequest, parseSequencerCodecProbeResult, type SequencerCodecProbeRequestIR, type SequencerCodecProbeResultIR, } from "../../../protocol/sequencer"; export interface SequencerMediaProxyCacheStatsIR { entries: number; bytes: number; maxBytes: number; hits: number; misses: number; evictions: number; keys: string[]; } export interface SequencerGeneratedProxyFrameIR { manifest: SequencerMediaCacheManifestIR; data: ArrayBuffer; } interface CacheEntry { manifest: SequencerMediaCacheManifestIR; data: ArrayBuffer; } async function sha256(data: ArrayBuffer): Promise { const result = await crypto.subtle.digest("SHA-256", data); return Array.from(new Uint8Array(result), (value) => value.toString(16).padStart(2, "0")).join(""); } function cancelled(signal: AbortSignal | undefined): void { if (signal?.aborted) { throw new SequencerMediaCacheValidationError("SEQUENCER_CANCELLED", "Movie proxy generation was cancelled"); } } function waitForLoadedFrame(video: HTMLVideoElement, timeoutMs: number, signal?: AbortSignal): Promise { if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) return Promise.resolve(); return new Promise((resolve, reject) => { const timeout = window.setTimeout(() => finish(new Error("timeout")), timeoutMs); const finish = (error?: Error): void => { window.clearTimeout(timeout); video.removeEventListener("loadeddata", ready); video.removeEventListener("error", failed); signal?.removeEventListener("abort", aborted); if (error) reject(error); else resolve(); }; const ready = (): void => finish(); const failed = (): void => finish(new Error("decode")); const aborted = (): void => finish(new SequencerMediaCacheValidationError("SEQUENCER_CANCELLED", "Movie proxy generation was cancelled")); video.addEventListener("loadeddata", ready, { once: true }); video.addEventListener("error", failed, { once: true }); signal?.addEventListener("abort", aborted, { once: true }); if (signal?.aborted) aborted(); }); } export async function generateInitialSequencerMovieProxyFrame( sourceValue: unknown, capabilityValue: unknown, profileValue: unknown, sourceData: ArrayBuffer, signal?: AbortSignal, ): Promise { cancelled(signal); const source = parseSequencerCodecProbeRequest(sourceValue); const capability = parseSequencerCodecProbeResult(capabilityValue); const profile = parseSequencerMediaProxyProfile(profileValue); if (source.stripType !== "MOVIE" || capability.status !== "READY" || capability.stripType !== "MOVIE" || gateSequencerCodec(source, capability).status !== "READY") { throw new SequencerMediaCacheValidationError("SEQUENCER_CODEC_UNSUPPORTED", "Movie proxy generation requires a READY movie receipt"); } if (!(sourceData instanceof ArrayBuffer) || sourceData.byteLength !== source.byteLength || await sha256(sourceData) !== source.sourceSha256) { throw new SequencerMediaCacheValidationError("SEQUENCER_CACHE_SOURCE_MISMATCH", "Movie proxy source bytes failed identity verification"); } cancelled(signal); if (typeof document === "undefined" || typeof HTMLMediaElement === "undefined") { throw new SequencerMediaCacheValidationError("SEQUENCER_CODEC_UNSUPPORTED", "HTML media proxy generation is unavailable"); } const video = document.createElement("video"); video.muted = true; video.playsInline = true; video.preload = "auto"; const url = URL.createObjectURL(new Blob([sourceData], { type: source.mimeType })); try { video.src = url; video.load(); await waitForLoadedFrame(video, 10_000, signal); cancelled(signal); const canvas = document.createElement("canvas"); canvas.width = profile.width; canvas.height = profile.height; const context = canvas.getContext("2d", { alpha: true, willReadFrequently: true }); if (!context || video.videoWidth < 1 || video.videoHeight < 1) { throw new SequencerMediaCacheValidationError("SEQUENCER_CODEC_UNSUPPORTED", "Movie proxy frame could not be decoded"); } context.clearRect(0, 0, profile.width, profile.height); context.drawImage(video, 0, 0, profile.width, profile.height); cancelled(signal); const pixels = context.getImageData(0, 0, profile.width, profile.height).data; const data = pixels.buffer.slice(pixels.byteOffset, pixels.byteOffset + pixels.byteLength); return { manifest: await createSequencerMediaCacheManifest(source, capability, profile, 0, data), data, }; } catch (error) { if (error instanceof SequencerMediaCacheValidationError) throw error; throw new SequencerMediaCacheValidationError("SEQUENCER_CODEC_UNSUPPORTED", "Movie proxy frame decode failed"); } finally { video.pause(); video.removeAttribute("src"); video.load(); URL.revokeObjectURL(url); } } export class SequencerMediaProxyCache { readonly maxBytes: number; private readonly entries = new Map(); private currentBytes = 0; private hitCount = 0; private missCount = 0; private evictionCount = 0; constructor(maxBytes: number) { if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > SEQUENCER_MEDIA_PROXY_MAX_BYTES) { throw new SequencerMediaCacheValidationError("SEQUENCER_BUDGET_EXCEEDED", "Sequencer proxy cache budget is invalid"); } this.maxBytes = maxBytes; } async put( manifestValue: unknown, data: ArrayBuffer, currentSource: SequencerCodecProbeRequestIR, currentCapability: SequencerCodecProbeResultIR, ): Promise { const manifest = await verifySequencerMediaCacheEntry(manifestValue, data, currentSource, currentCapability); if (data.byteLength > this.maxBytes) { throw new SequencerMediaCacheValidationError("SEQUENCER_BUDGET_EXCEEDED", "Sequencer proxy entry exceeds the cache budget"); } const key = sequencerMediaCacheKey(manifest); const previous = this.entries.get(key); if (previous) { this.currentBytes -= previous.data.byteLength; this.entries.delete(key); } while (this.currentBytes + data.byteLength > this.maxBytes) { const oldest = this.entries.entries().next().value as [string, CacheEntry] | undefined; if (!oldest) break; this.entries.delete(oldest[0]); this.currentBytes -= oldest[1].data.byteLength; this.evictionCount++; } this.entries.set(key, { manifest, data: data.slice(0) }); this.currentBytes += data.byteLength; return key; } async get( sourceValue: unknown, capabilityValue: unknown, profileValue: unknown, sourceFrame: number, ): Promise { const source = parseSequencerCodecProbeRequest(sourceValue); const capability = parseSequencerCodecProbeResult(capabilityValue); const profile = parseSequencerMediaProxyProfile(profileValue); const identitySha256 = await computeSequencerMediaCacheIdentity(source, capability, profile, sourceFrame); const key = `sequencer-media-cache:v1:${identitySha256}`; const entry = this.entries.get(key); if (!entry) { this.missCount++; return undefined; } this.hitCount++; this.entries.delete(key); this.entries.set(key, entry); return { manifest: entry.manifest, data: entry.data.slice(0) }; } clear(): number { const released = this.currentBytes; this.entries.clear(); this.currentBytes = 0; return released; } stats(): SequencerMediaProxyCacheStatsIR { return { entries: this.entries.size, bytes: this.currentBytes, maxBytes: this.maxBytes, hits: this.hitCount, misses: this.missCount, evictions: this.evictionCount, keys: [...this.entries.keys()], }; } } export type { SequencerMediaCacheManifestIR, SequencerMediaProxyProfileIR };