mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
fix: #918
This commit is contained in:
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
});
|
});
|
||||||
|
@@ -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++;
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
Reference in New Issue
Block a user