chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -20,19 +20,23 @@ function blocked(
|
||||
return { ...request, status: "BLOCKED", backend, reason, decoded: null };
|
||||
}
|
||||
|
||||
function waitForMedia(video: HTMLVideoElement, eventName: "loadeddata" | "seeked", timeoutMs: number): Promise<void> {
|
||||
function waitForMedia(video: HTMLVideoElement, eventName: "loadeddata" | "seeked", timeoutMs: number, signal?: AbortSignal): 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);
|
||||
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 DOMException("Movie probe cancelled", "AbortError"));
|
||||
video.addEventListener(eventName, ready, { once: true });
|
||||
video.addEventListener("error", failed, { once: true });
|
||||
signal?.addEventListener("abort", aborted, { once: true });
|
||||
if (signal?.aborted) aborted();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,7 +70,8 @@ async function probeSound(request: SequencerCodecProbeRequestIR, data: ArrayBuff
|
||||
catch { return blocked(request, "WEB_AUDIO", "DECODE_FAILED"); }
|
||||
}
|
||||
|
||||
async function probeMovie(request: SequencerCodecProbeRequestIR, data: ArrayBuffer): Promise<SequencerCodecProbeResultIR> {
|
||||
async function probeMovie(request: SequencerCodecProbeRequestIR, data: ArrayBuffer, signal?: AbortSignal): Promise<SequencerCodecProbeResultIR> {
|
||||
signal?.throwIfAborted();
|
||||
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");
|
||||
@@ -79,14 +84,14 @@ async function probeMovie(request: SequencerCodecProbeRequestIR, data: ArrayBuff
|
||||
try {
|
||||
video.src = url;
|
||||
video.load();
|
||||
await waitForMedia(video, "loadeddata", 10_000);
|
||||
await waitForMedia(video, "loadeddata", 10_000, signal);
|
||||
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);
|
||||
await waitForMedia(video, "seeked", 10_000, signal);
|
||||
}
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 1;
|
||||
@@ -107,7 +112,10 @@ async function probeMovie(request: SequencerCodecProbeRequestIR, data: ArrayBuff
|
||||
},
|
||||
};
|
||||
}
|
||||
catch { return blocked(request, "HTML_MEDIA", "DECODE_FAILED"); }
|
||||
catch (error) {
|
||||
if (signal?.aborted || error instanceof DOMException && error.name === "AbortError") throw error;
|
||||
return blocked(request, "HTML_MEDIA", "DECODE_FAILED");
|
||||
}
|
||||
finally {
|
||||
video.removeAttribute("src");
|
||||
video.load();
|
||||
@@ -119,14 +127,19 @@ async function probeMovie(request: SequencerCodecProbeRequestIR, data: ArrayBuff
|
||||
export async function probeSequencerCodec(
|
||||
requestValue: unknown,
|
||||
source: ArrayBuffer,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SequencerCodecProbeResultIR> {
|
||||
signal?.throwIfAborted();
|
||||
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);
|
||||
signal?.throwIfAborted();
|
||||
const result = request.stripType === "IMAGE" ? await probeImage(request, source)
|
||||
: request.stripType === "SOUND" ? await probeSound(request, source)
|
||||
: await probeMovie(request, source, signal);
|
||||
signal?.throwIfAborted();
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Probe a movie with the browser decoder first, then use the local ffmpeg.wasm
|
||||
@@ -136,7 +149,9 @@ export async function probeSequencerMovieCodecWithFallback(
|
||||
source: ArrayBuffer,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SequencerCodecProbeResultIR> {
|
||||
const browserResult = await probeSequencerCodec(requestValue, source);
|
||||
signal?.throwIfAborted();
|
||||
const browserResult = await probeSequencerCodec(requestValue, source, signal);
|
||||
signal?.throwIfAborted();
|
||||
if (browserResult.status === "READY") return browserResult;
|
||||
const { probeMovieWithFFmpeg } = await import("./FFmpegMovieProbe");
|
||||
return probeMovieWithFFmpeg(requestValue, source, signal);
|
||||
|
||||
Reference in New Issue
Block a user