// file: compressImages.ts import ffmpeg from 'fluent-ffmpeg'; import { Buffer } from 'buffer'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; import { Readable } from 'stream'; export interface ImageInput { buffer: Buffer; contentType: string; } export interface Av1CompressOptions { framerate?: number; crf?: number; preset?: number; pixFmt?: string; } const mimeToExtensionMap: Record = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/webp': 'webp', 'image/bmp': 'bmp', }; /** * 将一个包含不同类型图片的Buffer数组压缩成一个使用SVT-AV1编码的视频Buffer。 * (已修复顺序问题和视频截断问题) * * @param images 包含图片对象(buffer 和 contentType)的数组。 * @param options 压缩选项。 * @returns {Promise} 一个Promise,解析后为包含视频数据的Buffer。 */ export async function compressImagesToAv1Video( images: ImageInput[], options: Av1CompressOptions = {} ): Promise { if (!images || images.length === 0) { throw new Error('Input image array cannot be empty.'); } const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ffmpeg-av1-')); try { const { framerate = 25, crf = 30, preset = 8, pixFmt = 'yuv420p', } = options; // --- 修复点 1: 修正写入和 list.txt 生成的逻辑 --- // 1. 先生成所有文件名,并并行写入所有文件 const fileNames: string[] = []; const writePromises = images.map(async (image, index) => { const extension = mimeToExtensionMap[image.contentType.toLowerCase()]; if (!extension) { throw new Error(`Unsupported content type: ${image.contentType}`); } const frameNumber = (index + 1).toString().padStart(5, '0'); const fileName = `frame-${frameNumber}.${extension}`; fileNames[index] = fileName; // 按正确顺序填充文件名数组 const filePath = path.join(tempDir, fileName); return fs.writeFile(filePath, image.buffer); }); await Promise.all(writePromises); // 2. 所有文件写入成功后,再生成 concat 文件内容 const concatFileContent = fileNames.map(name => `file '${name}'`).join('\n'); const concatFilePath = path.join(tempDir, 'list.txt'); await fs.writeFile(concatFilePath, concatFileContent); // --- 修复点 2: 修正 FFmpeg 命令选项 --- return new Promise((resolve, reject) => { const command = ffmpeg(concatFilePath) .inputOptions([ '-f', 'concat', '-safe', '0', `-r ${framerate}`, // 关键:将帧率作为输入选项 ]) .videoCodec('libsvtav1') .outputOptions([ `-crf ${crf}`, `-preset ${preset}`, '-movflags frag_keyframe+empty_moov', ]) .toFormat('mp4'); // 添加一个start监听器,方便调试,它会打印出最终执行的命令 command.on('start', (commandLine) => { console.log('Spawning FFmpeg with command: ' + commandLine); }); const outputStream = command.pipe() as Readable; const chunks: Buffer[] = []; outputStream.on('data', (chunk) => chunks.push(chunk)); outputStream.on('error', reject); outputStream.on('end', () => resolve(Buffer.concat(chunks))); command.on('error', (err, stdout, stderr) => { reject(new Error(`FFmpeg error: ${err.message}\nStderr: ${stderr}`)); }); }).finally(() => { //fs.rm(tempDir, { recursive: true, force: true }); }); } catch (error) { //await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); throw error; } } // ... 示例代码 main() 函数无需修改,可以正常工作