119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
// 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<string, string> = {
|
||
'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<Buffer>} 一个Promise,解析后为包含视频数据的Buffer。
|
||
*/
|
||
export async function compressImagesToAv1Video(
|
||
images: ImageInput[],
|
||
options: Av1CompressOptions = {}
|
||
): Promise<Buffer> {
|
||
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<Buffer>((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() 函数无需修改,可以正常工作
|