Add full source parity validation workflow
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

This commit is contained in:
mes123456
2026-09-07 16:45:07 +08:00
parent a3588d41a3
commit f68c6c9678
59 changed files with 4556 additions and 842 deletions

View File

@@ -0,0 +1,77 @@
import {
parseSequencerCodecProbeRequest,
type SequencerCodecProbeRequestIR,
type SequencerCodecProbeResultIR,
} from "../../../protocol/sequencer";
interface WorkerResponse {
requestId: string;
status: "READY" | "BLOCKED";
reason: "RUNTIME_UNAVAILABLE" | "DECODE_FAILED" | null;
decoded: { width: number; height: number; durationMicros: number } | null;
data?: ArrayBuffer;
}
const digest = async (source: ArrayBuffer): Promise<string> => {
const result = await crypto.subtle.digest("SHA-256", source);
return Array.from(new Uint8Array(result), (value) => value.toString(16).padStart(2, "0")).join("");
};
export interface FFmpegMovieFrameIR {
decoded: { width: number; height: number; durationMicros: number };
rgba: ArrayBuffer;
}
function blocked(request: SequencerCodecProbeRequestIR, reason: "RUNTIME_UNAVAILABLE" | "DECODE_FAILED"): SequencerCodecProbeResultIR {
return { ...request, status: "BLOCKED", backend: "FFMPEG_WASM", reason, decoded: null };
}
async function requestFFmpeg(
request: SequencerCodecProbeRequestIR,
source: ArrayBuffer,
type: "probe" | "frame",
options: { width?: number; height?: number; signal?: AbortSignal } = {},
): Promise<WorkerResponse> {
const worker = new Worker(new URL("./ffmpeg-movie-probe.worker.ts", import.meta.url), { type: "module" });
const requestId = `ffmpeg-movie:${crypto.randomUUID()}`;
return new Promise((resolve, reject) => {
const cleanup = (): void => {
worker.onmessage = null;
worker.onerror = null;
options.signal?.removeEventListener("abort", abort);
worker.terminate();
};
const abort = (): void => { cleanup(); reject(new DOMException("FFmpeg movie probe cancelled", "AbortError")); };
worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
if (event.data.requestId !== requestId) return;
cleanup();
resolve(event.data);
};
worker.onerror = (event) => { cleanup(); reject(new Error(event.message || "FFmpeg movie worker failed")); };
options.signal?.addEventListener("abort", abort, { once: true });
if (options.signal?.aborted) { abort(); return; }
const payload = source.slice(0);
worker.postMessage({ requestId, type, bytes: payload, mimeType: request.mimeType, width: options.width, height: options.height }, [payload]);
});
}
export async function probeMovieWithFFmpeg(requestValue: unknown, source: ArrayBuffer, signal?: AbortSignal): Promise<SequencerCodecProbeResultIR> {
const request = parseSequencerCodecProbeRequest(requestValue);
if (request.stripType !== "MOVIE" || !(source instanceof ArrayBuffer) || source.byteLength !== request.byteLength || await digest(source) !== request.sourceSha256) return blocked(request, "DECODE_FAILED");
try {
const result = await requestFFmpeg(request, source, "probe", { signal });
return result.status === "READY" && result.decoded ? { ...request, status: "READY", backend: "FFMPEG_WASM", reason: null, decoded: result.decoded } : blocked(request, result.reason ?? "DECODE_FAILED");
}
catch (error) {
if (error instanceof DOMException && error.name === "AbortError") throw error;
return blocked(request, "RUNTIME_UNAVAILABLE");
}
}
export async function decodeMovieFrameWithFFmpeg(requestValue: unknown, source: ArrayBuffer, width: number, height: number, signal?: AbortSignal): Promise<FFmpegMovieFrameIR> {
const request = parseSequencerCodecProbeRequest(requestValue);
if (request.stripType !== "MOVIE" || !(source instanceof ArrayBuffer) || source.byteLength !== request.byteLength || await digest(source) !== request.sourceSha256) throw new Error("SEQUENCER_CACHE_SOURCE_MISMATCH");
const result = await requestFFmpeg(request, source, "frame", { width, height, signal });
if (result.status !== "READY" || !result.decoded || !result.data) throw new Error(result.reason ?? "SEQUENCER_CODEC_UNSUPPORTED");
return { decoded: result.decoded, rgba: result.data };
}

View File

@@ -0,0 +1,89 @@
import { FFmpeg } from "@ffmpeg/ffmpeg";
interface ProbeRequest {
type: "probe" | "frame";
requestId: string;
bytes: ArrayBuffer;
mimeType: string;
width?: number;
height?: number;
}
interface ProbeResponse {
requestId: string;
status: "READY" | "BLOCKED";
reason: "RUNTIME_UNAVAILABLE" | "DECODE_FAILED" | null;
decoded: { width: number; height: number; durationMicros: number } | null;
data?: ArrayBuffer;
error?: string;
}
const ffmpeg = new FFmpeg();
let loaded = false;
const workerScope = self as unknown as { postMessage: (message: unknown, transfer?: Transferable[]) => void };
function response(requestId: string, patch: Omit<ProbeResponse, "requestId">): void {
workerScope.postMessage({ requestId, ...patch }, patch.data ? [patch.data] : []);
}
async function ensureLoaded(): Promise<void> {
if (loaded) return;
const vendorRoot = new URL(self.location.href).pathname.startsWith("/src/") ? "/src/vendor/ffmpeg" : "/vendor/ffmpeg";
await ffmpeg.load({
coreURL: `${vendorRoot}/ffmpeg-core.js`,
wasmURL: `${vendorRoot}/ffmpeg-core.wasm`,
});
loaded = true;
}
async function readText(path: string): Promise<string> {
const value = await ffmpeg.readFile(path, "utf8");
return typeof value === "string" ? value : new TextDecoder().decode(value);
}
async function probe(request: ProbeRequest): Promise<ProbeResponse["decoded"]> {
await ensureLoaded();
await ffmpeg.writeFile("input.movie", new Uint8Array(request.bytes));
await ffmpeg.ffprobe([
"-v", "error", "-show_entries", "stream=width,height:format=duration", "-of", "json", "input.movie", "-o", "probe.json",
]);
const parsed = JSON.parse(await readText("probe.json")) as { streams?: Array<{ width?: number; height?: number }>; format?: { duration?: string } };
const stream = parsed.streams?.find((candidate) => Number.isFinite(candidate.width) && Number.isFinite(candidate.height));
const width = Math.trunc(stream?.width ?? 0);
const height = Math.trunc(stream?.height ?? 0);
const duration = Number(parsed.format?.duration ?? 0);
if (width < 1 || height < 1 || !Number.isFinite(duration) || duration <= 0) return null;
return { width, height, durationMicros: Math.max(1, Math.round(duration * 1_000_000)) };
}
self.onmessage = (event: MessageEvent<ProbeRequest>) => {
const request = event.data;
void (async () => {
try {
const decoded = await probe(request);
if (!decoded) {
response(request.requestId, { status: "BLOCKED", reason: "DECODE_FAILED", decoded: null });
return;
}
if (request.type === "probe") {
response(request.requestId, { status: "READY", reason: null, decoded });
return;
}
const width = Math.max(1, Math.min(4096, Math.trunc(request.width ?? decoded.width)));
const height = Math.max(1, Math.min(4096, Math.trunc(request.height ?? decoded.height)));
await ffmpeg.exec([
"-i", "input.movie", "-vf", `scale=${width}:${height}`, "-frames:v", "1", "-f", "rawvideo", "-pix_fmt", "rgba", "frame.rgba",
]);
const data = await ffmpeg.readFile("frame.rgba");
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
if (bytes.byteLength !== width * height * 4) {
response(request.requestId, { status: "BLOCKED", reason: "DECODE_FAILED", decoded: null });
return;
}
response(request.requestId, { status: "READY", reason: null, decoded: { width, height, durationMicros: decoded.durationMicros }, data: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer });
}
catch (error) {
response(request.requestId, { status: "BLOCKED", reason: "RUNTIME_UNAVAILABLE", decoded: null, error: error instanceof Error ? error.message : String(error) });
}
})();
};