允许后端任务

This commit is contained in:
2026-06-17 17:58:00 +08:00
parent eac6ad2343
commit b9af979582
6 changed files with 1874 additions and 304 deletions
+143 -43
View File
@@ -82,6 +82,60 @@ export type ExtractedVideoFrame = {
timestampMs: number;
};
export type ExtractedAudio = {
buffer: Buffer;
contentType: string;
ext: string;
};
export type ExtractedAudioSegment = ExtractedAudio & {
index: number;
startMs: number;
endMs: number;
};
type AudioFormat = "mp3" | "aac" | "wav";
function resolveAudioEncoding(format: AudioFormat, bitrateKbps: number) {
if (format === "mp3") {
return {
ext: "mp3",
contentType: "audio/mpeg",
codecArgs: ["-c:a", "libmp3lame", "-b:a", `${bitrateKbps}k`],
};
}
if (format === "aac") {
return {
ext: "m4a",
contentType: "audio/mp4",
codecArgs: [
"-c:a",
"aac",
"-b:a",
`${bitrateKbps}k`,
"-movflags",
"+faststart",
],
};
}
return {
ext: "wav",
contentType: "audio/wav",
codecArgs: [
"-f",
"wav",
"-acodec",
"pcm_s16le",
"-ar",
"44100",
"-ac",
"2",
],
};
}
/**
* 使用 ffprobe 获取视频时长(毫秒)
*/
@@ -234,8 +288,8 @@ export async function extractVideoFrames(
*/
export async function extractAudio(
videoBuffer: Buffer,
opts?: { format?: "mp3" | "aac" | "wav"; bitrateKbps?: number },
): Promise<{ buffer: Buffer; contentType: string; ext: string } | null> {
opts?: { format?: AudioFormat; bitrateKbps?: number },
): Promise<ExtractedAudio | null> {
const ffmpegCmd = process.env.FFMPEG_PATH || "ffmpeg";
const tmpDir = os.tmpdir();
const base = `dy_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
@@ -243,47 +297,8 @@ export async function extractAudio(
const format = opts?.format ?? "mp3";
const bitrate = Math.max(32, Math.min(512, opts?.bitrateKbps ?? 192)); // 安全范围 32~512 kbps
// 根据目标格式设置输出路径、MIME 与编码参数
let outPath = "";
let contentType = "";
let ext = "";
let codecArgs: string[] = [];
if (format === "mp3") {
ext = "mp3";
contentType = "audio/mpeg";
outPath = path.join(tmpDir, `${base}.${ext}`);
codecArgs = ["-c:a", "libmp3lame", "-b:a", `${bitrate}k`];
} else if (format === "aac") {
// 使用 m4a 容器更通用
ext = "m4a";
contentType = "audio/mp4";
outPath = path.join(tmpDir, `${base}.${ext}`);
codecArgs = [
"-c:a",
"aac",
"-b:a",
`${bitrate}k`,
"-movflags",
"+faststart",
];
} else {
// wav
ext = "wav";
contentType = "audio/wav";
outPath = path.join(tmpDir, `${base}.${ext}`);
codecArgs = [
"-f",
"wav",
"-acodec",
"pcm_s16le",
"-ar",
"44100",
"-ac",
"2",
];
}
const { ext, contentType, codecArgs } = resolveAudioEncoding(format, bitrate);
const outPath = path.join(tmpDir, `${base}.${ext}`);
try {
await fs.writeFile(inPath, videoBuffer);
@@ -320,3 +335,88 @@ export async function extractAudio(
} catch {}
}
}
/**
* 将视频音轨按时间切成多个音频片段,避免长视频单次请求体过大。
*/
export async function extractAudioSegments(
videoBuffer: Buffer,
opts: {
durationMs: number;
segmentDurationMs: number;
format?: AudioFormat;
bitrateKbps?: number;
},
): Promise<ExtractedAudioSegment[]> {
const ffmpegCmd = process.env.FFMPEG_PATH || "ffmpeg";
const tmpDir = os.tmpdir();
const base = `dy_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const inPath = path.join(tmpDir, `${base}.mp4`);
const format = opts.format ?? "mp3";
const bitrate = Math.max(32, Math.min(512, opts.bitrateKbps ?? 128));
const segmentDurationMs = Math.max(1000, opts.segmentDurationMs);
const durationMs = Math.max(0, opts.durationMs);
const { ext, contentType, codecArgs } = resolveAudioEncoding(format, bitrate);
const outPaths: string[] = [];
try {
await fs.writeFile(inPath, videoBuffer);
const segments: ExtractedAudioSegment[] = [];
let index = 0;
for (let startMs = 0; startMs < durationMs; startMs += segmentDurationMs) {
const endMs = Math.min(durationMs, startMs + segmentDurationMs);
if (endMs - startMs < 500) continue;
const outPath = path.join(tmpDir, `${base}_${index}.${ext}`);
outPaths.push(outPath);
const args = [
"-hide_banner",
"-loglevel",
"error",
"-ss",
(startMs / 1000).toFixed(3),
"-t",
((endMs - startMs) / 1000).toFixed(3),
"-i",
inPath,
"-vn",
...codecArgs,
"-y",
outPath,
];
await execFileAsync(ffmpegCmd, args, { windowsHide: true });
segments.push({
buffer: await fs.readFile(outPath),
contentType,
ext,
index,
startMs,
endMs,
});
index += 1;
}
return segments;
} catch (error) {
if (isMissingExecutableError(error)) {
console.warn(
"系统未检测到 ffmpeg,可安装并配置 PATH 或设置 FFMPEG_PATH 后启用音频分片提取。",
);
return [];
}
console.warn(`提取音频分片失败: ${getErrorMessage(error)}`);
throw error;
} finally {
try {
await fs.unlink(inPath);
} catch {}
for (const outPath of outPaths) {
try {
await fs.unlink(outPath);
} catch {}
}
}
}