feat: convert img to video

This commit is contained in:
feie9454 2025-06-30 13:11:46 +08:00
parent f34ce4aed0
commit 6e2976e234
2 changed files with 166 additions and 0 deletions

119
lib/encodeVideo.ts Normal file
View File

@ -0,0 +1,119 @@
// 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>} PromiseBuffer
*/
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() 函数无需修改,可以正常工作

View File

@ -0,0 +1,47 @@
import { prisma } from '@/lib/prisma'
import { getFileByObjectName, storeFile } from '../fileStorage'
import { compressImagesToAv1Video } from '../encodeVideo'
export async function compressPics(hostname: string, startTime: Date, endTime: Date) {
// Build query conditions
const whereClause: any = { hostname }
if (startTime || endTime) {
whereClause.timestamp = {}
if (startTime) whereClause.timestamp.gte = startTime
if (endTime) whereClause.timestamp.lte = endTime
}
// Get records
const records = await prisma.record.findMany({
where: whereClause,
include: {
windows: false,
screenshots: true
},
orderBy: {
timestamp: 'desc'
}
})
// 有两种情况不进行处理:
// 1. 每个记录中有多于一个截图
// 2. 时间段内截图有多种分辨率
const picNames = records.map(record => {
return record.screenshots.map(screenshot => screenshot.objectName)
}).flat();
const picBuffers = (await Promise.all(picNames.map(name => getFileByObjectName(name)))).filter(buffer => buffer !== null && buffer !== undefined);
// 先默认不存在null吧
const vBuffer = await compressImagesToAv1Video(picBuffers)
storeFile(vBuffer, 'test.mp4', 'video/mp4', 'other', hostname)
}
//DESKTOP-JHHNH9C startTime=1751104800 1751108400
compressPics('DESKTOP-JHHNH9C', new Date(1751104800000), new Date(1751108400000))