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 { 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: 使用 libmp3lame,contentType 为 'audio/mpeg' * - aac: 输出 m4a 容器(AAC 编码),contentType 为 'audio/mp4' * - wav: PCM 16-bit,44100Hz,双声道,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 { } } }