fix(canvas): 视频片段 - 精确裁切视频并提取音轨
Co-authored-by: dlh <2773843782@qq.com> Original-PR: https://github.com/ddcat-ai/open-ai-canvas/pull/224
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
"dev": "vite --host 0.0.0.0 --port 3000",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "bun test test/agent-prompt-cache.test.ts test/canvas-character-reference.test.ts test/canvas-document.test.ts test/canvas-drawing-engine.test.ts test/canvas-generation-layout.test.ts test/canvas-leafer-viewport.test.ts test/canvas-model-policy.test.ts test/canvas-resource-references.test.ts test/canvas-video-regeneration.test.ts test/canvas-video-timeline-segments.test.ts test/client-id.test.ts test/timeline-to-ffmpeg.test.ts",
|
||||
"test": "bun test test/agent-prompt-cache.test.ts test/canvas-character-reference.test.ts test/canvas-document.test.ts test/canvas-drawing-engine.test.ts test/canvas-generation-layout.test.ts test/canvas-leafer-viewport.test.ts test/canvas-model-policy.test.ts test/canvas-resource-references.test.ts test/canvas-video-regeneration.test.ts test/canvas-video-timeline-segments.test.ts test/client-id.test.ts test/timeline-to-ffmpeg.test.ts test/canvas-video-segment-args.test.ts",
|
||||
"start": "vite preview --host 0.0.0.0 --port 3000",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
|
||||
17
web/src/lib/canvas/canvas-video-segment-args.ts
Normal file
17
web/src/lib/canvas/canvas-video-segment-args.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// 视频片段处理(裁切/提音轨)的 ffmpeg 参数构造,纯函数便于单测。
|
||||
// -ss 必须放在 -i 之后(输出 seek):放在 -i 之前是输入 seek,MP4/H.264 只会定位到目标时间戳
|
||||
// 之前最近的关键帧,切点会偏移最多一个 GOP(常见 0.5-2s)、片尾被 -t 截掉、音视频在切点处错位;
|
||||
// 本流程已 -c:v libx264 重编码,输出 seek 帧精确,代价只是多解码。
|
||||
|
||||
export const SEGMENT_INPUT_NAME = "segment-input.mp4";
|
||||
export const SEGMENT_OUTPUT_NAME = "segment-output.mp4";
|
||||
|
||||
/** 视频片段裁切参数:输出统一编码 MP4。 */
|
||||
export function buildSegmentTrimArgs(startSec: string, durationSec: string): string[] {
|
||||
return ["-i", SEGMENT_INPUT_NAME, "-ss", startSec, "-t", durationSec, "-c:v", "libx264", "-preset", "veryfast", "-crf", "20", "-c:a", "aac", "-movflags", "+faststart", SEGMENT_OUTPUT_NAME];
|
||||
}
|
||||
|
||||
/** 从视频片段提取声音:同样使用输出 seek,保证起点与裁切路径一致。 */
|
||||
export function buildExtractAudioArgs(audioCodec: string, startSec: string, durationSec: string): string[] {
|
||||
return ["-i", SEGMENT_INPUT_NAME, "-ss", startSec, "-t", durationSec, "-vn", "-c:a", audioCodec, "-q:a", "2", SEGMENT_OUTPUT_NAME];
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fetchFile } from "@ffmpeg/util";
|
||||
|
||||
import { getMediaBlob } from "@/services/file-storage";
|
||||
import { buildExtractAudioArgs, buildSegmentTrimArgs, SEGMENT_INPUT_NAME, SEGMENT_OUTPUT_NAME } from "./canvas-video-segment-args";
|
||||
import { loadFFmpeg } from "./canvas-video-merge";
|
||||
|
||||
export type VideoSegmentRange = {
|
||||
@@ -18,8 +19,8 @@ export type VideoSegmentProgress = {
|
||||
progress: number;
|
||||
};
|
||||
|
||||
const INPUT_NAME = "segment-input.mp4";
|
||||
const OUTPUT_NAME = "segment-output.mp4";
|
||||
const INPUT_NAME = SEGMENT_INPUT_NAME;
|
||||
const OUTPUT_NAME = SEGMENT_OUTPUT_NAME;
|
||||
|
||||
function assertValidRange(range: VideoSegmentRange, durationMs?: number) {
|
||||
const startMs = Math.max(0, Math.round(range.startMs));
|
||||
@@ -70,14 +71,7 @@ async function runSegmentJob(
|
||||
|
||||
/** 按片段范围截取视频,输出统一编码 MP4(复用时间线 trim 的参数模板)。 */
|
||||
export async function trimVideoSegment(source: VideoSegmentSource, range: VideoSegmentRange, durationMs?: number, onProgress?: (progress: VideoSegmentProgress) => void) {
|
||||
return runSegmentJob(
|
||||
source,
|
||||
range,
|
||||
durationMs,
|
||||
(startSec, durationSec) => ["-ss", startSec, "-i", INPUT_NAME, "-t", durationSec, "-c:v", "libx264", "-preset", "veryfast", "-crf", "20", "-c:a", "aac", "-movflags", "+faststart", OUTPUT_NAME],
|
||||
onProgress,
|
||||
"video/mp4",
|
||||
);
|
||||
return runSegmentJob(source, range, durationMs, (startSec, durationSec) => buildSegmentTrimArgs(startSec, durationSec), onProgress, "video/mp4");
|
||||
}
|
||||
|
||||
/** 从视频片段提取声音为 MP3;优先 libmp3lame,内核不支持时回退默认 mp3 编码器。 */
|
||||
@@ -91,7 +85,7 @@ export async function extractVideoAudio(source: VideoSegmentSource, range: Video
|
||||
const durationSec = String((range.endMs - range.startMs) / 1000);
|
||||
onProgress?.({ phase: "encoding", progress: 55 });
|
||||
try {
|
||||
const args = (audioCodec: string) => ["-ss", startSec, "-i", INPUT_NAME, "-t", durationSec, "-vn", "-c:a", audioCodec, "-q:a", "2", OUTPUT_NAME];
|
||||
const args = (audioCodec: string) => buildExtractAudioArgs(audioCodec, startSec, durationSec);
|
||||
let exitCode = await ffmpeg.exec(["-y", ...args("libmp3lame")]);
|
||||
if (exitCode !== 0) exitCode = await ffmpeg.exec(["-y", ...args("mp3")]);
|
||||
if (exitCode !== 0) throw new Error("音频提取失败:当前 FFmpeg 内核不支持 MP3 编码");
|
||||
|
||||
19
web/test/canvas-video-segment-args.test.ts
Normal file
19
web/test/canvas-video-segment-args.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { buildExtractAudioArgs, buildSegmentTrimArgs, SEGMENT_INPUT_NAME, SEGMENT_OUTPUT_NAME } from "../src/lib/canvas/canvas-video-segment-args";
|
||||
|
||||
describe("buildSegmentTrimArgs seek 顺序", () => {
|
||||
test("-ss 必须放在 -i 之后(输出 seek):输入 seek 按关键帧对齐,切点会偏移最多一个 GOP", () => {
|
||||
const args = buildSegmentTrimArgs("10.5", "4");
|
||||
expect(args.indexOf("-ss")).toBeGreaterThan(args.indexOf("-i"));
|
||||
expect(args).toEqual(["-i", SEGMENT_INPUT_NAME, "-ss", "10.5", "-t", "4", "-c:v", "libx264", "-preset", "veryfast", "-crf", "20", "-c:a", "aac", "-movflags", "+faststart", SEGMENT_OUTPUT_NAME]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildExtractAudioArgs seek 顺序", () => {
|
||||
test("音频提取与裁切路径一致,同样使用输出 seek", () => {
|
||||
const args = buildExtractAudioArgs("libmp3lame", "10.5", "4");
|
||||
expect(args.indexOf("-ss")).toBeGreaterThan(args.indexOf("-i"));
|
||||
expect(args).toEqual(["-i", SEGMENT_INPUT_NAME, "-ss", "10.5", "-t", "4", "-vn", "-c:a", "libmp3lame", "-q:a", "2", SEGMENT_OUTPUT_NAME]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user