Advance M8-M11 parity workflows
This commit is contained in:
145
web/app/src/sequencer/SequencerCodecProbe.ts
Normal file
145
web/app/src/sequencer/SequencerCodecProbe.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user