diff --git a/package.json b/package.json index 621813cd..7e77f07e 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "@types/cors": "^2.8.17", "@sinclair/typebox": "^0.34.9", "@types/express": "^5.0.0", - "@types/fluent-ffmpeg": "^2.1.24", "@types/node": "^22.0.1", "@types/qrcode-terminal": "^0.12.2", "@types/ws": "^8.5.12", @@ -58,7 +57,6 @@ "@ffmpeg.wasm/core-mt": "^0.13.2", "@ffmpeg.wasm/main": "^0.13.1", "express": "^5.0.0", - "fluent-ffmpeg": "^2.1.2", "piscina": "^4.7.0", "qrcode-terminal": "^0.12.0", "silk-wasm": "^3.6.1", diff --git a/src/common/audio.ts b/src/common/audio.ts index 9358173e..2cd7bbf6 100644 --- a/src/common/audio.ts +++ b/src/common/audio.ts @@ -5,7 +5,7 @@ import { randomUUID } from 'crypto'; import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm'; import { LogWrapper } from '@/common/log'; import { EncodeArgs } from "@/common/audio-worker"; -import { ffmpegService } from "@/common/ffmpeg"; +import { FFmpegService } from "@/common/ffmpeg"; const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000]; @@ -32,7 +32,7 @@ async function handleWavFile( ): Promise<{ input: Buffer; sampleRate: number }> { const { fmt } = getWavFileInfo(file); if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) { - return { input: await ffmpegService.convert(filePath, pcmPath, logger), sampleRate: 24000 }; + return { input: await FFmpegService.convert(filePath, pcmPath, logger), sampleRate: 24000 }; } return { input: file, sampleRate: fmt.sampleRate }; } @@ -46,7 +46,7 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log const pcmPath = `${pttPath}.pcm`; const { input, sampleRate } = isWav(file) ? (await handleWavFile(file, filePath, pcmPath, logger)) - : { input: await ffmpegService.convert(filePath, pcmPath, logger), sampleRate: 24000 }; + : { input: await FFmpegService.convert(filePath, pcmPath, logger), sampleRate: 24000 }; const silk = await piscina.run({ input: input, sampleRate: sampleRate }); await fsPromise.writeFile(pttPath, Buffer.from(silk.data)); logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration); diff --git a/src/common/ffmpeg.ts b/src/common/ffmpeg.ts index fadbf58b..efc37e20 100644 --- a/src/common/ffmpeg.ts +++ b/src/common/ffmpeg.ts @@ -1,104 +1,131 @@ import { FFmpeg } from '@ffmpeg.wasm/main'; import { randomUUID } from 'crypto'; -import { readFileSync, writeFileSync } from 'fs'; +import { readFileSync, statSync, writeFileSync } from 'fs'; import { LogWrapper } from './log'; - -class FFmpegService { - private ffmpegRef: FFmpeg; - - constructor(ffmpegRef: FFmpeg) { - this.ffmpegRef = ffmpegRef; - } - - public async extractThumbnail(videoPath: string, thumbnailPath: string): Promise { +import { VideoInfo } from './video'; +import { fileTypeFromFile } from 'file-type'; +import imageSize from 'image-size'; +export class FFmpegService { + public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise { + const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' }); const videoFileName = `${randomUUID()}.mp4`; const outputFileName = `${randomUUID()}.jpg`; try { - this.ffmpegRef.fs.writeFile(videoFileName, readFileSync(videoPath)); - let code = await this.ffmpegRef.run('-i', videoFileName, '-ss', '00:00:01.000', '-vframes', '1', outputFileName); - if (code! === 0) { + ffmpegInstance.fs.writeFile(videoFileName, readFileSync(videoPath)); + let code = await ffmpegInstance.run('-i', videoFileName, '-ss', '00:00:01.000', '-vframes', '1', outputFileName); + if (code !== 0) { throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code); } - const thumbnail = this.ffmpegRef.fs.readFile(outputFileName); + const thumbnail = ffmpegInstance.fs.readFile(outputFileName); writeFileSync(thumbnailPath, thumbnail); } catch (error) { console.error('Error extracting thumbnail:', error); throw error; } finally { try { - this.ffmpegRef.fs.unlink(outputFileName); + ffmpegInstance.fs.unlink(outputFileName); } catch (unlinkError) { console.error('Error unlinking output file:', unlinkError); } try { - this.ffmpegRef.fs.unlink(videoFileName); + ffmpegInstance.fs.unlink(videoFileName); } catch (unlinkError) { console.error('Error unlinking video file:', unlinkError); } } } - public async convertFile(inputFile: string, outputFile: string, format: string): Promise { + public static async convertFile(inputFile: string, outputFile: string, format: string): Promise { + const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' }); const inputFileName = `${randomUUID()}.pcm`; const outputFileName = `${randomUUID()}.${format}`; try { - this.ffmpegRef.fs.writeFile(inputFileName, readFileSync(inputFile)); + ffmpegInstance.fs.writeFile(inputFileName, readFileSync(inputFile)); const params = format === 'amr' ? ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFileName, '-ar', '8000', '-b:a', '12.2k', outputFileName] : ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFileName, outputFileName]; - let code = await this.ffmpegRef.run(...params); - if (code! === 0) { + let code = await ffmpegInstance.run(...params); + if (code !== 0) { throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code); } - const outputData = this.ffmpegRef.fs.readFile(outputFileName); + const outputData = ffmpegInstance.fs.readFile(outputFileName); writeFileSync(outputFile, outputData); } catch (error) { console.error('Error converting file:', error); throw error; } finally { try { - this.ffmpegRef.fs.unlink(outputFileName); + ffmpegInstance.fs.unlink(outputFileName); } catch (unlinkError) { console.error('Error unlinking output file:', unlinkError); } try { - this.ffmpegRef.fs.unlink(inputFileName); + ffmpegInstance.fs.unlink(inputFileName); } catch (unlinkError) { console.error('Error unlinking input file:', unlinkError); } } } - public async convert(filePath: string, pcmPath: string, logger: LogWrapper): Promise { + public static async convert(filePath: string, pcmPath: string, logger: LogWrapper): Promise { + const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' }); const inputFileName = `${randomUUID()}.input`; const outputFileName = `${randomUUID()}.pcm`; try { - this.ffmpegRef.fs.writeFile(inputFileName, readFileSync(filePath)); + ffmpegInstance.fs.writeFile(inputFileName, readFileSync(filePath)); const params = ['-y', '-i', inputFileName, '-ar', '24000', '-ac', '1', '-f', 's16le', outputFileName]; - let code = await this.ffmpegRef.run(...params); - if (code! === 0) { + let code = await ffmpegInstance.run(...params); + if (code !== 0) { throw new Error('FFmpeg process exited with code ' + code); } - const outputData = this.ffmpegRef.fs.readFile(outputFileName); + const outputData = ffmpegInstance.fs.readFile(outputFileName); writeFileSync(pcmPath, outputData); - return Buffer.from(outputData); + return Buffer.from(outputData); } catch (error: any) { - logger.log('FFmpeg处理转换出错: ', error.message); - throw error; + throw new Error('FFmpeg处理转换出错: ' + error.message); } finally { try { - this.ffmpegRef.fs.unlink(outputFileName); + ffmpegInstance.fs.unlink(outputFileName); } catch (unlinkError) { logger.log('Error unlinking output file:', unlinkError); } try { - this.ffmpegRef.fs.unlink(inputFileName); + ffmpegInstance.fs.unlink(inputFileName); } catch (unlinkError) { logger.log('Error unlinking input file:', unlinkError); } } } -} - -const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' }); -export const ffmpegService = new FFmpegService(ffmpegInstance); \ No newline at end of file + + public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise { + await FFmpegService.extractThumbnail(videoPath, thumbnailPath); + let 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], 10); + const minutes = parseInt(durationMatch[2], 10); + const seconds = parseFloat(durationMatch[3]); + duration = hours * 3600 + minutes * 60 + seconds; + } + }); + await ffmpegInstance.run('-i', inputFileName); + let image = imageSize(thumbnailPath); + ffmpegInstance.fs.unlink(inputFileName); + const fileSize = statSync(videoPath).size; + return { + width: image.width ?? 100, + height: image.height ?? 100, + time: duration, + format: fileType, + size: fileSize, + filePath: videoPath + } + } +} \ No newline at end of file diff --git a/src/common/video.ts b/src/common/video.ts index 5cddb44a..6da318ca 100644 --- a/src/common/video.ts +++ b/src/common/video.ts @@ -1,71 +1,11 @@ -import ffmpeg, { FfprobeStream } from 'fluent-ffmpeg'; -import fs from 'fs'; -import { LogWrapper } from '@/common/log'; - export const defaultVideoThumbB64 = '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'; -interface VideoInfo { +export interface VideoInfo { width: number; height: number; time: number; format: string; size: number; filePath: string; -} - -async function getFileSize(filePath: string): Promise { - const stats = await fs.promises.stat(filePath); - return stats.size; -} - -function setFfmpegPath() { - const ffmpegPath = process.env.FFMPEG_PATH; - if (ffmpegPath) { - ffmpeg.setFfmpegPath(ffmpegPath); - } -} - -function extractVideoStream(metadata: ffmpeg.FfprobeData): FfprobeStream | undefined { - return metadata.streams.find((s: FfprobeStream) => s.codec_type === 'video'); -} - -async function probeVideo(filePath: string): Promise { - return new Promise((resolve, reject) => { - ffmpeg(filePath).ffprobe((err: any, metadata: ffmpeg.FfprobeData) => { - if (err) { - reject(new Error('无法获取视频信息。')); - } else { - resolve(metadata); - } - }); - }); -} - -export async function getVideoInfo(filePath: string, logger: LogWrapper): Promise { - try { - const size = await getFileSize(filePath); - setFfmpegPath(); - const metadata = await probeVideo(filePath); - const videoStream = extractVideoStream(metadata); - - if (!videoStream) { - throw new Error('未找到视频流信息。'); - } - - logger.log(`视频尺寸: ${videoStream.width}x${videoStream.height}`); - if (!videoStream.width || !metadata.format.format_name || !videoStream.height) { - throw new Error('获取基础信息失败'); - } - return { - width: videoStream.width, - height: videoStream.height, - time: +(videoStream.duration ?? 10), - format: metadata.format.format_name, - size, - filePath, - }; - } catch (error) { - throw new Error('无法获取视频信息。'); - } } \ No newline at end of file diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts index 32e54ab9..d78ead7e 100644 --- a/src/core/apis/file.ts +++ b/src/core/apis/file.ts @@ -22,11 +22,11 @@ import { ISizeCalculationResult } from 'image-size/dist/types/interface'; import { RkeyManager } from '@/core/helper/rkey'; import { calculateFileMD5 } from '@/common/file'; import pathLib from 'node:path'; -import { defaultVideoThumbB64, getVideoInfo } from '@/common/video'; -import ffmpeg from 'fluent-ffmpeg'; +import { defaultVideoThumbB64 } from '@/common/video'; import { encodeSilk } from '@/common/audio'; import { SendMessageContext } from '@/onebot/api'; import { getFileTypeForSendType } from '../helper/msg'; +import { FFmpegService } from '@/common/ffmpeg'; export class NTQQFileApi { context: InstanceContext; @@ -40,7 +40,7 @@ export class NTQQFileApi { this.rkeyManager = new RkeyManager([ 'https://rkey.napneko.icu/rkeys' ], - this.context.logger + this.context.logger ); } @@ -149,12 +149,6 @@ export class NTQQFileApi { size: 0, filePath, }; - try { - videoInfo = await getVideoInfo(filePath, this.context.logger); - } catch (e) { - this.context.logger.logError('获取视频信息失败,将使用默认值', e); - } - let fileExt = 'mp4'; try { const tempExt = (await fileTypeFromFile(filePath))?.ext; @@ -162,53 +156,29 @@ export class NTQQFileApi { } catch (e) { this.context.logger.logError('获取文件类型失败', e); } - const newFilePath = filePath + '.' + fileExt; + const newFilePath = `${filePath}.${fileExt}`; fs.copyFileSync(filePath, newFilePath); context.deleteAfterSentFiles.push(newFilePath); filePath = newFilePath; + const { fileName: _fileName, path, fileSize, md5 } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.VIDEO); if (fileSize === 0) { throw new Error('文件异常,大小为0'); } - videoInfo.size = fileSize; - let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`); - thumb = pathLib.dirname(thumb); + const thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`); + const thumbPath = pathLib.join(pathLib.dirname(thumbDir), `${md5}_0.png`); + try { + await FFmpegService.getVideoInfo(filePath, thumbPath); + } catch (error) { + fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64')); + } - const thumbPath = new Map(); - const _thumbPath = await new Promise((resolve, reject) => { - const thumbFileName = `${md5}_0.png`; - const thumbPath = pathLib.join(thumb, thumbFileName); - ffmpeg(filePath) - .on('error', (err) => { - try { - this.context.logger.logDebug('获取视频封面失败,使用默认封面', err); - if (diyThumbPath) { - fsPromises.copyFile(diyThumbPath, thumbPath).then(() => { - resolve(thumbPath); - }).catch(reject); - } else { - fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64')); - resolve(thumbPath); - } - } catch (error) { - this.context.logger.logError('获取视频封面失败,使用默认封面失败', error); - } - }) - .screenshots({ - timestamps: [0], - filename: thumbFileName, - folder: thumb, - size: videoInfo.width + 'x' + videoInfo.height, - }) - .on('end', () => { - resolve(thumbPath); - }); - }); - const thumbSize = _thumbPath ? (await fsPromises.stat(_thumbPath)).size : 0; - thumbPath.set(0, _thumbPath); - const thumbMd5 = _thumbPath ? await calculateFileMD5(_thumbPath) : ''; + videoInfo.size = fileSize; + const thumbSize = (await fsPromises.stat(thumbPath)).size; + const thumbMd5 = await calculateFileMD5(thumbPath); context.deleteAfterSentFiles.push(path); - const uploadName = (fileName || _fileName).toLocaleLowerCase().endsWith('.' + fileExt.toLocaleLowerCase()) ? (fileName || _fileName) : (fileName || _fileName) + '.' + fileExt; + + const uploadName = (fileName || _fileName).toLocaleLowerCase().endsWith(`.${fileExt.toLocaleLowerCase()}`) ? (fileName || _fileName) : `${fileName || _fileName}.${fileExt}`; return { elementType: ElementType.VIDEO, elementId: '', @@ -218,15 +188,14 @@ export class NTQQFileApi { videoMd5: md5, thumbMd5, fileTime: videoInfo.time, - thumbPath: thumbPath, + thumbPath: new Map([[0, thumbPath]]), thumbSize, thumbWidth: videoInfo.width, thumbHeight: videoInfo.height, - fileSize: '' + fileSize, + fileSize: fileSize.toString(), }, }; } - async createValidSendPttElement(pttPath: string): Promise { const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.NapCatTempPath, this.core.context.logger); @@ -305,18 +274,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/action/file/GetRecord.ts b/src/onebot/action/file/GetRecord.ts index 65bced45..d83d4e08 100644 --- a/src/onebot/action/file/GetRecord.ts +++ b/src/onebot/action/file/GetRecord.ts @@ -2,7 +2,7 @@ import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile'; import { ActionName } from '@/onebot/action/router'; import { promises as fs } from 'fs'; import { decode } from 'silk-wasm'; -import { ffmpegService } from '@/common/ffmpeg'; +import { FFmpegService } from '@/common/ffmpeg'; const out_format = ['mp3' , 'amr' , 'wma' , 'm4a' , 'spx' , 'ogg' , 'wav' , 'flac']; @@ -29,7 +29,7 @@ export default class GetRecord extends GetFileBase { await fs.access(outputFile); } catch (error) { await this.decodeFile(inputFile, pcmFile); - await ffmpegService.convertFile(pcmFile, outputFile, payload.out_format); + await FFmpegService.convertFile(pcmFile, outputFile, payload.out_format); } const base64Data = await fs.readFile(outputFile, { encoding: 'base64' }); res.file = outputFile; diff --git a/src/shell/base.ts b/src/shell/base.ts index 72d98d17..9b532ade 100644 --- a/src/shell/base.ts +++ b/src/shell/base.ts @@ -29,7 +29,7 @@ import { InitWebUi } from '@/webui'; import { WebUiDataRuntime } from '@/webui/src/helper/Data'; import { napCatVersion } from '@/common/version'; import { NodeIO3MiscListener } from '@/core/listeners/NodeIO3MiscListener'; -import { ffmpegService } from '@/common/ffmpeg'; +import { FFmpegService } from '@/common/ffmpeg'; // NapCat Shell App ES 入口文件 async function handleUncaughtExceptions(logger: LogWrapper) { process.on('uncaughtException', (err) => { @@ -264,7 +264,8 @@ async function initializeSession( export async function NCoreInitShell() { try { - await ffmpegService.extractThumbnail("F:\\BVideo\\123.mp4","F:\\BVideo\\123.jpg"); + let info = await FFmpegService.getVideoInfo("F:\\BVideo\\123.mp4","F:\\BVideo\\1.jpg"); + console.log(info); } catch (error) { console.log(error); } diff --git a/vite.config.ts b/vite.config.ts index 6a0c5871..642d8ee6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,7 +4,7 @@ import { resolve } from 'path'; import nodeResolve from '@rollup/plugin-node-resolve'; import { builtinModules } from 'module'; //依赖排除 -const external = ['silk-wasm', 'ws', 'express', 'qrcode-terminal', 'fluent-ffmpeg', 'piscina', '@ffmpeg.wasm/core-mt', "@ffmpeg.wasm/main"]; +const external = ['silk-wasm', 'ws', 'express', 'qrcode-terminal', 'piscina', '@ffmpeg.wasm/core-mt', "@ffmpeg.wasm/main"]; const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat(); let startScripts: string[] | undefined = undefined; @@ -79,7 +79,6 @@ const UniversalBaseConfig = () => alias: { '@/core': resolve(__dirname, './src/core'), '@': resolve(__dirname, './src'), - './lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg', '@webapi': resolve(__dirname, './src/webui/src'), }, }, @@ -109,7 +108,6 @@ const ShellBaseConfig = () => alias: { '@/core': resolve(__dirname, './src/core'), '@': resolve(__dirname, './src'), - './lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg', '@webapi': resolve(__dirname, './src/webui/src'), }, }, @@ -138,7 +136,6 @@ const FrameworkBaseConfig = () => alias: { '@/core': resolve(__dirname, './src/core'), '@': resolve(__dirname, './src'), - './lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg', '@webapi': resolve(__dirname, './src/webui/src'), }, },