Files
workinf_Blender_Wasm/web/app/src/sequencer/SequencerCodecProbe.ts
mes123456 0fe8d2bb56
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

146 lines
5.7 KiB
TypeScript

import {
SEQUENCER_CODEC_PROBE_SCHEMA,
parseSequencerCodecProbeRequest,
type SequencerCodecProbeBackend,
type SequencerCodecProbeBlockReason,
type SequencerCodecProbeRequestIR,
type SequencerCodecProbeResultIR,
} from "../../../protocol/sequencer";
async function sha256(data: ArrayBuffer): Promise<string> {
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<void> {
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<SequencerCodecProbeResultIR> {
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<SequencerCodecProbeResultIR> {
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<SequencerCodecProbeResultIR> {
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<SequencerCodecProbeResultIR> {
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,
});
}