This commit is contained in:
手瓜一十雪
2025-04-02 11:51:51 +08:00
parent ac26a99143
commit dfb7d2e6bf
5 changed files with 196 additions and 44 deletions

View File

@@ -16,6 +16,9 @@ export function recvTask<T>(cb: (taskData: T) => Promise<unknown>) {
} }
}); });
} }
export function sendLog(_log: string) {
//parentPort?.postMessage({ log });
}
class FFmpegService { class FFmpegService {
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> { public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' }); const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
@@ -107,35 +110,175 @@ class FFmpegService {
} }
} }
} }
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> { public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
await FFmpegService.extractThumbnail(videoPath, thumbnailPath); const startTime = Date.now();
const fileType = (await fileTypeFromFile(videoPath))?.ext ?? 'mp4'; sendLog(`开始获取视频信息: ${videoPath}`);
const inputFileName = `${randomUUID()}.${fileType}`;
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' }); // 创建一个超时包装函数
ffmpegInstance.fs.writeFile(inputFileName, readFileSync(videoPath)); const withTimeout = <T>(promise: Promise<T>, timeoutMs: number, taskName: string): Promise<T> => {
ffmpegInstance.setLogging(true); return Promise.race([
let duration = 60; promise,
ffmpegInstance.setLogger((_level, ...msg) => { new Promise<T>((_, reject) => {
const message = msg.join(' '); setTimeout(() => reject(new Error(`任务超时: ${taskName} (${timeoutMs}ms)`)), timeoutMs);
const durationMatch = message.match(/Duration: (\d+):(\d+):(\d+\.\d+)/); })
if (durationMatch) { ]);
const hours = parseInt(durationMatch[1] ?? '0', 10); };
const minutes = parseInt(durationMatch[2] ?? '0', 10);
const seconds = parseFloat(durationMatch[3] ?? '0'); // 并行执行多个任务
duration = hours * 3600 + minutes * 60 + seconds; const [fileInfo, durationInfo] = await Promise.all([
} // 任务1: 获取文件信息和提取缩略图
}); (async () => {
await ffmpegInstance.run('-i', inputFileName); sendLog(`开始任务1: 获取文件信息和提取缩略图`);
const image = imageSize(thumbnailPath);
ffmpegInstance.fs.unlink(inputFileName); // 获取文件信息 (并行)
const fileSize = statSync(videoPath).size; const fileInfoStartTime = Date.now();
const [fileType, fileSize] = await Promise.all([
withTimeout(fileTypeFromFile(videoPath), 10000, '获取文件类型')
.then(result => {
sendLog(`获取文件类型完成,耗时: ${Date.now() - fileInfoStartTime}ms`);
return result;
}),
(async () => {
const result = statSync(videoPath).size;
sendLog(`获取文件大小完成,耗时: ${Date.now() - fileInfoStartTime}ms`);
return result;
})()
]);
// 直接实现缩略图提取 (不调用extractThumbnail方法)
const thumbStartTime = Date.now();
sendLog(`开始提取缩略图`);
const ffmpegInstance = await withTimeout(
FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' }),
15000,
'创建FFmpeg实例(缩略图)'
);
const videoFileName = `${randomUUID()}.mp4`;
const outputFileName = `${randomUUID()}.jpg`;
try {
// 写入视频文件到FFmpeg
const writeFileStartTime = Date.now();
ffmpegInstance.fs.writeFile(videoFileName, readFileSync(videoPath));
sendLog(`写入视频文件到FFmpeg完成耗时: ${Date.now() - writeFileStartTime}ms`);
// 提取缩略图
const extractStartTime = Date.now();
const code = await withTimeout(
ffmpegInstance.run('-i', videoFileName, '-ss', '00:00:01.000', '-vframes', '1', outputFileName),
30000,
'提取缩略图'
);
sendLog(`FFmpeg提取缩略图命令执行完成耗时: ${Date.now() - extractStartTime}ms`);
if (code !== 0) {
throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code);
}
// 读取并保存缩略图
const saveStartTime = Date.now();
const thumbnail = ffmpegInstance.fs.readFile(outputFileName);
writeFileSync(thumbnailPath, thumbnail);
sendLog(`读取并保存缩略图完成,耗时: ${Date.now() - saveStartTime}ms`);
// 获取缩略图尺寸
const imageSizeStartTime = Date.now();
const image = imageSize(thumbnailPath);
sendLog(`获取缩略图尺寸完成,耗时: ${Date.now() - imageSizeStartTime}ms`);
sendLog(`提取缩略图完成,总耗时: ${Date.now() - thumbStartTime}ms`);
return {
format: fileType?.ext ?? 'mp4',
size: fileSize,
width: image.width ?? 100,
height: image.height ?? 100
};
} finally {
// 清理资源
try {
ffmpegInstance.fs.unlink(outputFileName);
} catch (error) {
sendLog(`清理输出文件失败: ${(error as Error).message}`);
}
try {
ffmpegInstance.fs.unlink(videoFileName);
} catch (error) {
sendLog(`清理视频文件失败: ${(error as Error).message}`);
}
}
})(),
// 任务2: 获取视频时长
(async () => {
const task2StartTime = Date.now();
sendLog(`开始任务2: 获取视频时长`);
// 创建FFmpeg实例
const ffmpegCreateStartTime = Date.now();
const ffmpegInstance = await withTimeout(
FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' }),
15000,
'创建FFmpeg实例(时长)'
);
sendLog(`创建FFmpeg实例完成耗时: ${Date.now() - ffmpegCreateStartTime}ms`);
const inputFileName = `${randomUUID()}.mp4`;
try {
// 写入文件
const writeStartTime = Date.now();
ffmpegInstance.fs.writeFile(inputFileName, readFileSync(videoPath));
sendLog(`写入文件到FFmpeg完成耗时: ${Date.now() - writeStartTime}ms`);
ffmpegInstance.setLogging(true);
let duration = 60; // 默认值
ffmpegInstance.setLogger((_level, ...msg) => {
const message = msg.join(' ');
const durationMatch = message.match(/Duration: (\d+):(\d+):(\d+\.\d+)/);
if (durationMatch) {
const hours = parseInt(durationMatch[1] ?? '0', 10);
const minutes = parseInt(durationMatch[2] ?? '0', 10);
const seconds = parseFloat(durationMatch[3] ?? '0');
duration = hours * 3600 + minutes * 60 + seconds;
}
});
// 执行FFmpeg
const runStartTime = Date.now();
await withTimeout(
ffmpegInstance.run('-i', inputFileName),
20000,
'获取视频时长'
);
sendLog(`执行FFmpeg命令完成耗时: ${Date.now() - runStartTime}ms`);
sendLog(`任务2(获取视频时长)完成,总耗时: ${Date.now() - task2StartTime}ms`);
return { time: duration };
} finally {
try {
ffmpegInstance.fs.unlink(inputFileName);
} catch (error) {
sendLog(`清理输入文件失败: ${(error as Error).message}`);
}
}
})()
]);
// 合并结果并返回
const totalDuration = Date.now() - startTime;
sendLog(`获取视频信息完成,总耗时: ${totalDuration}ms`);
return { return {
width: image.width ?? 100, width: fileInfo.width,
height: image.height ?? 100, height: fileInfo.height,
time: duration, time: durationInfo.time,
format: fileType, format: fileInfo.format,
size: fileSize, size: fileInfo.size,
filePath: videoPath filePath: videoPath
}; };
} }

View File

@@ -30,7 +30,7 @@ export class FFmpegService {
} }
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> { public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
const result = await await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'getVideoInfo', args: [videoPath, thumbnailPath] }); const result = await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'getVideoInfo', args: [videoPath, thumbnailPath] });
return result; return result;
} }
} }

View File

@@ -5,8 +5,11 @@ export async function runTask<T, R>(workerScript: string, taskData: T): Promise<
try { try {
return await new Promise<R>((resolve, reject) => { return await new Promise<R>((resolve, reject) => {
worker.on('message', (result: R) => { worker.on('message', (result: R) => {
if ((result as any)?.log) {
console.error('Worker Log--->:', (result as { log: string }).log);
}
if ((result as any)?.error) { if ((result as any)?.error) {
reject(new Error((result as { error: string }).error)); reject(new Error("Worker error: " + (result as { error: string }).error));
} }
resolve(result); resolve(result);
}); });

View File

@@ -44,7 +44,7 @@ export class NTQQFileApi {
'https://ss.xingzhige.com/music_card/rkey', // 国内 'https://ss.xingzhige.com/music_card/rkey', // 国内
'https://secret-service.bietiaop.com/rkeys',//国内 'https://secret-service.bietiaop.com/rkeys',//国内
], ],
this.context.logger this.context.logger
); );
} }
@@ -188,17 +188,23 @@ export class NTQQFileApi {
const thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`); const thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`);
fs.mkdirSync(pathLib.dirname(thumbDir), { recursive: true }); fs.mkdirSync(pathLib.dirname(thumbDir), { recursive: true });
const thumbPath = pathLib.join(pathLib.dirname(thumbDir), `${md5}_0.png`); const thumbPath = pathLib.join(pathLib.dirname(thumbDir), `${md5}_0.png`);
try {
videoInfo = await FFmpegService.getVideoInfo(filePath, thumbPath);
} catch {
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
}
if (_diyThumbPath) { if (_diyThumbPath) {
try { try {
await this.copyFile(_diyThumbPath, thumbPath); await this.copyFile(_diyThumbPath, thumbPath);
} catch (e) { } catch (e) {
this.context.logger.logError('复制自定义缩略图失败', e); this.context.logger.logError('复制自定义缩略图失败', e);
} }
} else {
try {
videoInfo = await FFmpegService.getVideoInfo(filePath, thumbPath);
if (!fs.existsSync(thumbPath)) {
this.context.logger.logError('获取视频缩略图失败', new Error('缩略图不存在'));
throw new Error('获取视频缩略图失败');
}
} catch (e) {
this.context.logger.logError('获取视频信息失败', e);
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
}
} }
context.deleteAfterSentFiles.push(thumbPath); context.deleteAfterSentFiles.push(thumbPath);
const thumbSize = (await fsPromises.stat(thumbPath)).size; const thumbSize = (await fsPromises.stat(thumbPath)).size;
@@ -301,18 +307,18 @@ export class NTQQFileApi {
element.elementType === ElementType.FILE element.elementType === ElementType.FILE
) { ) {
switch (element.elementType) { switch (element.elementType) {
case ElementType.PIC: case ElementType.PIC:
element.picElement!.sourcePath = elementResults?.[elementIndex] ?? ''; element.picElement!.sourcePath = elementResults?.[elementIndex] ?? '';
break; break;
case ElementType.VIDEO: case ElementType.VIDEO:
element.videoElement!.filePath = elementResults?.[elementIndex] ?? ''; element.videoElement!.filePath = elementResults?.[elementIndex] ?? '';
break; break;
case ElementType.PTT: case ElementType.PTT:
element.pttElement!.filePath = elementResults?.[elementIndex] ?? ''; element.pttElement!.filePath = elementResults?.[elementIndex] ?? '';
break; break;
case ElementType.FILE: case ElementType.FILE:
element.fileElement!.filePath = elementResults?.[elementIndex] ?? ''; element.fileElement!.filePath = elementResults?.[elementIndex] ?? '';
break; break;
} }
elementIndex++; elementIndex++;
} }

View File

@@ -971,7 +971,6 @@ export class OneBotMsgApi {
}); });
const timeout = 10000 + (totalSize / 1024 / 256 * 1000); const timeout = 10000 + (totalSize / 1024 / 256 * 1000);
cleanTaskQueue.addFiles(deleteAfterSentFiles, timeout);
try { try {
const returnMsg = await this.core.apis.MsgApi.sendMsg(peer, sendElements, timeout); const returnMsg = await this.core.apis.MsgApi.sendMsg(peer, sendElements, timeout);
if (!returnMsg) throw new Error('发送消息失败'); if (!returnMsg) throw new Error('发送消息失败');
@@ -984,6 +983,7 @@ export class OneBotMsgApi {
} catch (error) { } catch (error) {
throw new Error((error as Error).message); throw new Error((error as Error).message);
} finally { } finally {
cleanTaskQueue.addFiles(deleteAfterSentFiles, timeout);
// setTimeout(async () => { // setTimeout(async () => {
// const deletePromises = deleteAfterSentFiles.map(async file => { // const deletePromises = deleteAfterSentFiles.map(async file => {
// try { // try {