chore: checkpoint web Blender migration source, tools and docs
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

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:
mes123456
2026-11-10 11:18:01 +08:00
parent 122142c26c
commit f3e21c6b98
6887 changed files with 257154 additions and 12294 deletions

View File

@@ -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);