156 lines
5.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { execFile } from 'child_process';
import { promises as fs } from 'fs';
import os from 'os';
import path from 'path';
import { promisify } from 'util';
const execFileAsync = promisify(execFile);
/**
* 使用 ffmpeg 从视频二进制中提取第一帧,返回 JPEG buffer
*/
export async function extractFirstFrame(videoBuffer: Buffer): Promise<{ buffer: Buffer; contentType: string; ext: string } | null> {
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 outPath = path.join(tmpDir, `${base}.jpg`);
try {
await fs.writeFile(inPath, videoBuffer);
const args = [
'-hide_banner',
'-loglevel', 'error',
'-ss', '0',
'-i', inPath,
'-frames:v', '1',
'-q:v', '2',
'-f', 'image2',
'-y',
outPath,
];
await execFileAsync(ffmpegCmd, args, { windowsHide: true });
const img = await fs.readFile(outPath);
return { buffer: img, contentType: 'image/jpeg', ext: 'jpg' };
} catch (e: any) {
if (e && (e.code === 'ENOENT' || /not found|is not recognized/i.test(String(e.message)))) {
console.warn('系统未检测到 ffmpeg可安装并配置 PATH 或设置 FFMPEG_PATH 后启用封面提取。');
return null;
}
throw e;
} finally {
try { await fs.unlink(inPath); } catch { }
try { await fs.unlink(outPath); } catch { }
}
}
/**
* 使用 ffprobe 获取视频时长(毫秒)
*/
export async function getVideoDuration(videoBuffer: Buffer): Promise<number | null> {
const ffprobeCmd = process.env.FFPROBE_PATH || 'ffprobe';
const tmpDir = os.tmpdir();
const base = `dy_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const inPath = path.join(tmpDir, `${base}.mp4`);
try {
await fs.writeFile(inPath, videoBuffer);
const args = [
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
inPath,
];
const { stdout } = await execFileAsync(ffprobeCmd, args, { windowsHide: true });
const durationSeconds = parseFloat(stdout.trim());
if (isNaN(durationSeconds)) return null;
return Math.round(durationSeconds * 1000); // 转换为毫秒
} catch (e: any) {
if (e && (e.code === 'ENOENT' || /not found|is not recognized/i.test(String(e.message)))) {
console.warn('系统未检测到 ffprobe可安装并配置 PATH 或设置 FFPROBE_PATH 后启用时长提取。');
return null;
}
console.warn(`获取视频时长失败: ${e?.message || e}`);
return null;
} finally {
try { await fs.unlink(inPath); } catch { }
}
}
/**
* 使用 ffmpeg 将视频二进制转换为音频,返回音频 Buffer
*
* 默认输出 mp3可通过 opts.format 指定 'mp3' | 'aac' | 'wav'
* - mp3: 使用 libmp3lamecontentType 为 'audio/mpeg'
* - aac: 输出 m4a 容器AAC 编码contentType 为 'audio/mp4'
* - wav: PCM 16-bit44100Hz双声道contentType 为 'audio/wav'
*
* 若系统未安装 ffmpeg 或未在 PATH 中,返回 null 并给出提示。
*/
export async function extractAudio(
videoBuffer: Buffer,
opts?: { format?: 'mp3' | 'aac' | 'wav'; bitrateKbps?: number }
): Promise<{ buffer: Buffer; contentType: string; ext: string } | null> {
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 ?? 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'];
}
try {
await fs.writeFile(inPath, videoBuffer);
const args = [
'-hide_banner',
'-loglevel', 'error',
'-i', inPath,
'-vn', // 丢弃视频流
...codecArgs,
'-y',
outPath,
];
await execFileAsync(ffmpegCmd, args, { windowsHide: true });
const audio = await fs.readFile(outPath);
return { buffer: audio, contentType, ext };
} catch (e: any) {
if (e && (e.code === 'ENOENT' || /not found|is not recognized/i.test(String(e.message)))) {
console.warn('系统未检测到 ffmpeg可安装并配置 PATH 或设置 FFMPEG_PATH 后启用音频提取。');
return null;
}
// 一些环境可能缺少特定编码器(如 libmp3lame提示并抛出原始错误
console.warn(`提取音频失败: ${e?.message || e}`);
throw e;
} finally {
try { await fs.unlink(inPath); } catch { }
try {
if (outPath) await fs.unlink(outPath);
} catch { }
}
}