import { SEQUENCER_CODEC_PROBE_SCHEMA, parseSequencerCodecProbeRequest, type SequencerCodecProbeBackend, type SequencerCodecProbeBlockReason, type SequencerCodecProbeRequestIR, type SequencerCodecProbeResultIR, } from "../../../protocol/sequencer"; async function sha256(data: ArrayBuffer): Promise { const digest = await crypto.subtle.digest("SHA-256", data); return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); } function blocked( request: SequencerCodecProbeRequestIR, backend: SequencerCodecProbeBackend | null, reason: SequencerCodecProbeBlockReason, ): SequencerCodecProbeResultIR { return { ...request, status: "BLOCKED", backend, reason, decoded: null }; } function waitForMedia(video: HTMLVideoElement, eventName: "loadeddata" | "seeked", timeoutMs: number): Promise { return new Promise((resolve, reject) => { const timeout = window.setTimeout(() => finish(new Error("timeout")), timeoutMs); const finish = (error?: Error): void => { window.clearTimeout(timeout); video.removeEventListener(eventName, ready); video.removeEventListener("error", failed); if (error) reject(error); else resolve(); }; const ready = (): void => finish(); const failed = (): void => finish(new Error("decode")); video.addEventListener(eventName, ready, { once: true }); video.addEventListener("error", failed, { once: true }); }); } async function probeImage(request: SequencerCodecProbeRequestIR, data: ArrayBuffer): Promise { if (typeof createImageBitmap !== "function") return blocked(request, null, "RUNTIME_UNAVAILABLE"); try { const bitmap = await createImageBitmap(new Blob([data], { type: request.mimeType })); try { if (bitmap.width < 1 || bitmap.height < 1) return blocked(request, "IMAGE_BITMAP", "DECODE_FAILED"); return { ...request, status: "READY", backend: "IMAGE_BITMAP", reason: null, decoded: { width: bitmap.width, height: bitmap.height } }; } finally { bitmap.close(); } } catch { return blocked(request, "IMAGE_BITMAP", "DECODE_FAILED"); } } async function probeSound(request: SequencerCodecProbeRequestIR, data: ArrayBuffer): Promise { if (typeof OfflineAudioContext === "undefined") return blocked(request, null, "RUNTIME_UNAVAILABLE"); const context = new OfflineAudioContext(1, 1, 48_000); try { const decoded = await context.decodeAudioData(data.slice(0)); if (decoded.sampleRate < 1 || decoded.numberOfChannels < 1 || decoded.length < 1) return blocked(request, "WEB_AUDIO", "DECODE_FAILED"); return { ...request, status: "READY", backend: "WEB_AUDIO", reason: null, decoded: { sampleRate: decoded.sampleRate, channels: decoded.numberOfChannels, durationFrames: decoded.length }, }; } catch { return blocked(request, "WEB_AUDIO", "DECODE_FAILED"); } } async function probeMovie(request: SequencerCodecProbeRequestIR, data: ArrayBuffer): Promise { if (typeof document === "undefined" || typeof HTMLMediaElement === "undefined") return blocked(request, null, "RUNTIME_UNAVAILABLE"); const video = document.createElement("video"); if (video.canPlayType(request.mimeType) === "") return blocked(request, "HTML_MEDIA", "MIME_UNSUPPORTED"); const url = URL.createObjectURL(new Blob([data], { type: request.mimeType })); video.preload = "auto"; video.muted = true; video.playsInline = true; video.style.display = "none"; document.body.append(video); try { video.src = url; video.load(); await waitForMedia(video, "loadeddata", 10_000); if (!Number.isFinite(video.duration) || video.duration <= 0 || video.videoWidth < 1 || video.videoHeight < 1) { return blocked(request, "HTML_MEDIA", "DECODE_FAILED"); } const target = Math.min(video.duration / 2, Math.max(0, video.duration - 0.001)); if (target > 0) { video.currentTime = target; await waitForMedia(video, "seeked", 10_000); } const canvas = document.createElement("canvas"); canvas.width = 1; canvas.height = 1; const context = canvas.getContext("2d", { willReadFrequently: true }); if (!context) return blocked(request, "HTML_MEDIA", "RUNTIME_UNAVAILABLE"); context.drawImage(video, 0, 0, 1, 1); context.getImageData(0, 0, 1, 1); return { ...request, status: "READY", backend: "HTML_MEDIA", reason: null, decoded: { width: video.videoWidth, height: video.videoHeight, durationMicros: Math.max(1, Math.round(video.duration * 1_000_000)), }, }; } catch { return blocked(request, "HTML_MEDIA", "DECODE_FAILED"); } finally { video.removeAttribute("src"); video.load(); video.remove(); URL.revokeObjectURL(url); } } export async function probeSequencerCodec( requestValue: unknown, source: ArrayBuffer, ): Promise { const request = parseSequencerCodecProbeRequest(requestValue); if (!(source instanceof ArrayBuffer) || source.byteLength !== request.byteLength || await sha256(source) !== request.sourceSha256) { return blocked(request, null, "SOURCE_IDENTITY_MISMATCH"); } if (request.stripType === "IMAGE") return probeImage(request, source); if (request.stripType === "SOUND") return probeSound(request, source); return probeMovie(request, source); } export function createSequencerCodecProbeRequest( stripType: SequencerCodecProbeRequestIR["stripType"], mimeType: string, byteLength: number, sourceSha256: string, ): SequencerCodecProbeRequestIR { return parseSequencerCodecProbeRequest({ schemaVersion: SEQUENCER_CODEC_PROBE_SCHEMA, stripType, mimeType, byteLength, sourceSha256, }); }