fix(canvas): 预演录制转码 - 奇数分辨率白膜录像按偶数缩放并无音轨导出 MP4
WebGL 录制没有音轨,dpr 1.5 下宽高经常是奇数,libx264 的 yuv420p 会直接失败。转码改为偶数缩放、去掉音轨,并用 ultrafast 单线程适配关闭汇编的 ffmpeg 内核。
This commit is contained in:
9
web/src/lib/canvas/canvas-video-merge-args.ts
Normal file
9
web/src/lib/canvas/canvas-video-merge-args.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// 画布录制转 MP4 的 ffmpeg 参数,纯函数便于单测。
|
||||
// 预演台白膜用 WebGL canvas.captureStream 录制,没有音轨,画布在 dpr 1.5 下宽高经常是奇数。
|
||||
// @ffmpeg/core 的 libx264 + yuv420p 要求宽高都能被 2 整除,奇数帧会直接失败:
|
||||
// "width not divisible by 2" → exit 1,界面只看到「视频转码失败,请重试」。
|
||||
// 该内核还关闭了 pthreads 和 asm,默认 medium preset 在视口分辨率下转不动,所以用 ultrafast 单线程。
|
||||
|
||||
export function buildRecordingTranscodeArgs(inputName: string, outputName: string): string[] {
|
||||
return ["-i", inputName, "-an", "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p", "-movflags", "+faststart", "-threads", "1", outputName];
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import ffmpegCoreURL from "@ffmpeg/core?url";
|
||||
import ffmpegWasmURL from "@ffmpeg/core/wasm?url";
|
||||
import { fetchFile } from "@ffmpeg/util";
|
||||
import { getMediaBlob } from "@/services/file-storage";
|
||||
import { buildRecordingTranscodeArgs } from "./canvas-video-merge-args";
|
||||
|
||||
export type MergeVideoInput = { id: string; url?: string; storageKey?: string };
|
||||
export type MergeVideoProgress = { phase: "loading" | "reading" | "encoding"; progress: number };
|
||||
@@ -40,15 +41,21 @@ export async function transcodeVideoToMp4(input: Blob, onProgress?: (progress: M
|
||||
try {
|
||||
await ffmpeg.writeFile(inputName, await fetchFile(input));
|
||||
onProgress?.({ phase: "encoding", progress: 55 });
|
||||
const exitCode = await ffmpeg.exec([
|
||||
"-i", inputName,
|
||||
"-c:v", "libx264",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-c:a", "aac",
|
||||
"-movflags", "+faststart",
|
||||
outputName,
|
||||
]);
|
||||
if (exitCode !== 0) throw new Error("视频转码失败,请重试");
|
||||
const logs: string[] = [];
|
||||
const onLog = ({ message }: { message: string }) => {
|
||||
if (message) logs.push(message);
|
||||
};
|
||||
ffmpeg.on("log", onLog);
|
||||
let exitCode = 1;
|
||||
try {
|
||||
exitCode = await ffmpeg.exec(buildRecordingTranscodeArgs(inputName, outputName));
|
||||
} finally {
|
||||
ffmpeg.off("log", onLog);
|
||||
}
|
||||
if (exitCode !== 0) {
|
||||
console.error("白膜视频转码失败", logs.slice(-12));
|
||||
throw new Error("视频转码失败,请重试");
|
||||
}
|
||||
const output = await ffmpeg.readFile(outputName);
|
||||
onProgress?.({ phase: "encoding", progress: 100 });
|
||||
return new Blob([output as BlobPart], { type: "video/mp4" });
|
||||
|
||||
15
web/test/canvas-video-merge-args.test.ts
Normal file
15
web/test/canvas-video-merge-args.test.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { buildRecordingTranscodeArgs } from "../src/lib/canvas/canvas-video-merge-args";
|
||||
|
||||
describe("buildRecordingTranscodeArgs", () => {
|
||||
test("奇数画布先裁成偶数,并且不要求音轨", () => {
|
||||
const args = buildRecordingTranscodeArgs("recording.webm", "recording-transcoded.mp4");
|
||||
expect(args).toContain("-an");
|
||||
expect(args).toContain("scale=trunc(iw/2)*2:trunc(ih/2)*2");
|
||||
expect(args).not.toContain("-c:a");
|
||||
expect(args.indexOf("-vf")).toBeGreaterThan(args.indexOf("-i"));
|
||||
expect(args.indexOf("-vf")).toBeLessThan(args.indexOf("recording-transcoded.mp4"));
|
||||
expect(args).toEqual(["-i", "recording.webm", "-an", "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p", "-movflags", "+faststart", "-threads", "1", "recording-transcoded.mp4"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user