diff --git a/src/common/ffmpeg-worker.ts b/src/common/ffmpeg-worker.ts index 40228a8d..d56abdba 100644 --- a/src/common/ffmpeg-worker.ts +++ b/src/common/ffmpeg-worker.ts @@ -16,6 +16,9 @@ export function recvTask(cb: (taskData: T) => Promise) { } }); } +export function sendLog(_log: string) { + //parentPort?.postMessage({ log }); +} class FFmpegService { public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise { 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 { - await FFmpegService.extractThumbnail(videoPath, thumbnailPath); - const fileType = (await fileTypeFromFile(videoPath))?.ext ?? 'mp4'; - const inputFileName = `${randomUUID()}.${fileType}`; - const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' }); - ffmpegInstance.fs.writeFile(inputFileName, readFileSync(videoPath)); - 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; - } - }); - await ffmpegInstance.run('-i', inputFileName); - const image = imageSize(thumbnailPath); - ffmpegInstance.fs.unlink(inputFileName); - const fileSize = statSync(videoPath).size; + const startTime = Date.now(); + sendLog(`开始获取视频信息: ${videoPath}`); + + // 创建一个超时包装函数 + const withTimeout = (promise: Promise, timeoutMs: number, taskName: string): Promise => { + return Promise.race([ + promise, + new Promise((_, reject) => { + setTimeout(() => reject(new Error(`任务超时: ${taskName} (${timeoutMs}ms)`)), timeoutMs); + }) + ]); + }; + + // 并行执行多个任务 + const [fileInfo, durationInfo] = await Promise.all([ + // 任务1: 获取文件信息和提取缩略图 + (async () => { + sendLog(`开始任务1: 获取文件信息和提取缩略图`); + + // 获取文件信息 (并行) + 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 { - width: image.width ?? 100, - height: image.height ?? 100, - time: duration, - format: fileType, - size: fileSize, + width: fileInfo.width, + height: fileInfo.height, + time: durationInfo.time, + format: fileInfo.format, + size: fileInfo.size, filePath: videoPath }; } diff --git a/src/common/ffmpeg.ts b/src/common/ffmpeg.ts index 737c761a..22497d6e 100644 --- a/src/common/ffmpeg.ts +++ b/src/common/ffmpeg.ts @@ -30,7 +30,7 @@ export class FFmpegService { } public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise { - const result = await await runTask(getWorkerPath(), { method: 'getVideoInfo', args: [videoPath, thumbnailPath] }); + const result = await runTask(getWorkerPath(), { method: 'getVideoInfo', args: [videoPath, thumbnailPath] }); return result; } } diff --git a/src/common/worker.ts b/src/common/worker.ts index 55e55cd8..da5c1321 100644 --- a/src/common/worker.ts +++ b/src/common/worker.ts @@ -5,8 +5,11 @@ export async function runTask(workerScript: string, taskData: T): Promise< try { return await new Promise((resolve, reject) => { worker.on('message', (result: R) => { + if ((result as any)?.log) { + console.error('Worker Log--->:', (result as { log: string }).log); + } 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); }); diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts index f058a15a..4c848695 100644 --- a/src/core/apis/file.ts +++ b/src/core/apis/file.ts @@ -44,7 +44,7 @@ export class NTQQFileApi { 'https://ss.xingzhige.com/music_card/rkey', // 国内 '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}`); fs.mkdirSync(pathLib.dirname(thumbDir), { recursive: true }); 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) { try { await this.copyFile(_diyThumbPath, thumbPath); } catch (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); const thumbSize = (await fsPromises.stat(thumbPath)).size; @@ -301,18 +307,18 @@ export class NTQQFileApi { element.elementType === ElementType.FILE ) { switch (element.elementType) { - case ElementType.PIC: + case ElementType.PIC: element.picElement!.sourcePath = elementResults?.[elementIndex] ?? ''; - break; - case ElementType.VIDEO: + break; + case ElementType.VIDEO: element.videoElement!.filePath = elementResults?.[elementIndex] ?? ''; - break; - case ElementType.PTT: + break; + case ElementType.PTT: element.pttElement!.filePath = elementResults?.[elementIndex] ?? ''; - break; - case ElementType.FILE: + break; + case ElementType.FILE: element.fileElement!.filePath = elementResults?.[elementIndex] ?? ''; - break; + break; } elementIndex++; } diff --git a/src/onebot/api/msg.ts b/src/onebot/api/msg.ts index 595c1736..49accdc7 100644 --- a/src/onebot/api/msg.ts +++ b/src/onebot/api/msg.ts @@ -971,7 +971,6 @@ export class OneBotMsgApi { }); const timeout = 10000 + (totalSize / 1024 / 256 * 1000); - cleanTaskQueue.addFiles(deleteAfterSentFiles, timeout); try { const returnMsg = await this.core.apis.MsgApi.sendMsg(peer, sendElements, timeout); if (!returnMsg) throw new Error('发送消息失败'); @@ -984,6 +983,7 @@ export class OneBotMsgApi { } catch (error) { throw new Error((error as Error).message); } finally { + cleanTaskQueue.addFiles(deleteAfterSentFiles, timeout); // setTimeout(async () => { // const deletePromises = deleteAfterSentFiles.map(async file => { // try {