This commit is contained in:
手瓜一十雪
2025-01-27 18:42:26 +08:00
parent dcef3f3c3b
commit ba619986c9
6 changed files with 199 additions and 123 deletions

151
src/common/ffmpeg-worker.ts Normal file
View File

@@ -0,0 +1,151 @@
import { FFmpeg } from '@ffmpeg.wasm/main';
import { randomUUID } from 'crypto';
import { readFileSync, statSync, writeFileSync } from 'fs';
import type { LogWrapper } from './log';
import type { VideoInfo } from './video';
import { fileTypeFromFile } from 'file-type';
import imageSize from 'image-size';
class FFmpegService {
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
const videoFileName = `${randomUUID()}.mp4`;
const outputFileName = `${randomUUID()}.jpg`;
try {
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 = ffmpegInstance.fs.readFile(outputFileName);
writeFileSync(thumbnailPath, thumbnail);
} catch (error) {
console.error('Error extracting thumbnail:', error);
throw error;
} finally {
try {
ffmpegInstance.fs.unlink(outputFileName);
} catch (unlinkError) {
console.error('Error unlinking output file:', unlinkError);
}
try {
ffmpegInstance.fs.unlink(videoFileName);
} catch (unlinkError) {
console.error('Error unlinking video file:', unlinkError);
}
}
}
public static async convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
const inputFileName = `${randomUUID()}.pcm`;
const outputFileName = `${randomUUID()}.${format}`;
try {
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 ffmpegInstance.run(...params);
if (code !== 0) {
throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code);
}
const outputData = ffmpegInstance.fs.readFile(outputFileName);
writeFileSync(outputFile, outputData);
} catch (error) {
console.error('Error converting file:', error);
throw error;
} finally {
try {
ffmpegInstance.fs.unlink(outputFileName);
} catch (unlinkError) {
console.error('Error unlinking output file:', unlinkError);
}
try {
ffmpegInstance.fs.unlink(inputFileName);
} catch (unlinkError) {
console.error('Error unlinking input file:', unlinkError);
}
}
}
public static async convert(filePath: string, pcmPath: string, logger: LogWrapper): Promise<Buffer> {
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
const inputFileName = `${randomUUID()}.input`;
const outputFileName = `${randomUUID()}.pcm`;
try {
ffmpegInstance.fs.writeFile(inputFileName, readFileSync(filePath));
const params = ['-y', '-i', inputFileName, '-ar', '24000', '-ac', '1', '-f', 's16le', outputFileName];
let code = await ffmpegInstance.run(...params);
if (code !== 0) {
throw new Error('FFmpeg process exited with code ' + code);
}
const outputData = ffmpegInstance.fs.readFile(outputFileName);
writeFileSync(pcmPath, outputData);
return Buffer.from(outputData);
} catch (error: any) {
throw new Error('FFmpeg处理转换出错: ' + error.message);
} finally {
try {
ffmpegInstance.fs.unlink(outputFileName);
} catch (unlinkError) {
logger.log('Error unlinking output file:', unlinkError);
}
try {
ffmpegInstance.fs.unlink(inputFileName);
} catch (unlinkError) {
logger.log('Error unlinking input file:', unlinkError);
}
}
}
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
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
}
}
}
type FFmpegMethod = 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo';
interface FFmpegTask {
method: FFmpegMethod;
args: any[];
}
export default async function handleFFmpegTask({ method, args }: FFmpegTask): Promise<any> {
switch (method) {
case 'extractThumbnail':
return await FFmpegService.extractThumbnail(...args as [string, string]);
case 'convertFile':
return await FFmpegService.convertFile(...args as [string, string, string]);
case 'convert':
return await FFmpegService.convert(...args as [string, string, LogWrapper]);
case 'getVideoInfo':
return await FFmpegService.getVideoInfo(...args as [string, string]);
default:
throw new Error(`Unknown method: ${method}`);
}
}

View File

@@ -1,131 +1,50 @@
import { FFmpeg } from '@ffmpeg.wasm/main';
import { randomUUID } from 'crypto';
import { readFileSync, statSync, writeFileSync } from 'fs';
import { LogWrapper } from './log';
import { VideoInfo } from './video';
import { fileTypeFromFile } from 'file-type';
import imageSize from 'image-size';
import Piscina from "piscina";
import { VideoInfo } from "./video";
import type { LogWrapper } from "./log";
type EncodeArgs = {
method: 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo';
args: any[];
};
type EncodeResult = any;
async function getWorkerPath() {
return new URL(/* @vite-ignore */ './ffmpeg-worker.mjs', import.meta.url).href;
}
export class FFmpegService {
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
const videoFileName = `${randomUUID()}.mp4`;
const outputFileName = `${randomUUID()}.jpg`;
try {
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 = ffmpegInstance.fs.readFile(outputFileName);
writeFileSync(thumbnailPath, thumbnail);
} catch (error) {
console.error('Error extracting thumbnail:', error);
throw error;
} finally {
try {
ffmpegInstance.fs.unlink(outputFileName);
} catch (unlinkError) {
console.error('Error unlinking output file:', unlinkError);
}
try {
ffmpegInstance.fs.unlink(videoFileName);
} catch (unlinkError) {
console.error('Error unlinking video file:', unlinkError);
}
}
const piscina = new Piscina<EncodeArgs, EncodeResult>({
filename: await getWorkerPath(),
});
await piscina.run({ method: 'extractThumbnail', args: [videoPath, thumbnailPath] });
await piscina.destroy();
}
public static async convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
const inputFileName = `${randomUUID()}.pcm`;
const outputFileName = `${randomUUID()}.${format}`;
try {
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 ffmpegInstance.run(...params);
if (code !== 0) {
throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code);
}
const outputData = ffmpegInstance.fs.readFile(outputFileName);
writeFileSync(outputFile, outputData);
} catch (error) {
console.error('Error converting file:', error);
throw error;
} finally {
try {
ffmpegInstance.fs.unlink(outputFileName);
} catch (unlinkError) {
console.error('Error unlinking output file:', unlinkError);
}
try {
ffmpegInstance.fs.unlink(inputFileName);
} catch (unlinkError) {
console.error('Error unlinking input file:', unlinkError);
}
}
const piscina = new Piscina<EncodeArgs, EncodeResult>({
filename: await getWorkerPath(),
});
await piscina.run({ method: 'convertFile', args: [inputFile, outputFile, format] });
await piscina.destroy();
}
public static async convert(filePath: string, pcmPath: string, logger: LogWrapper): Promise<Buffer> {
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
const inputFileName = `${randomUUID()}.input`;
const outputFileName = `${randomUUID()}.pcm`;
try {
ffmpegInstance.fs.writeFile(inputFileName, readFileSync(filePath));
const params = ['-y', '-i', inputFileName, '-ar', '24000', '-ac', '1', '-f', 's16le', outputFileName];
let code = await ffmpegInstance.run(...params);
if (code !== 0) {
throw new Error('FFmpeg process exited with code ' + code);
}
const outputData = ffmpegInstance.fs.readFile(outputFileName);
writeFileSync(pcmPath, outputData);
return Buffer.from(outputData);
} catch (error: any) {
throw new Error('FFmpeg处理转换出错: ' + error.message);
} finally {
try {
ffmpegInstance.fs.unlink(outputFileName);
} catch (unlinkError) {
logger.log('Error unlinking output file:', unlinkError);
}
try {
ffmpegInstance.fs.unlink(inputFileName);
} catch (unlinkError) {
logger.log('Error unlinking input file:', unlinkError);
}
}
const piscina = new Piscina<EncodeArgs, EncodeResult>({
filename: await getWorkerPath(),
});
const result = await piscina.run({ method: 'convert', args: [filePath, pcmPath, logger] });
await piscina.destroy();
return result;
}
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
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;
}
const piscina = new Piscina<EncodeArgs, EncodeResult>({
filename: await getWorkerPath(),
});
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
}
const result = await piscina.run({ method: 'getVideoInfo', args: [videoPath, thumbnailPath] });
await piscina.destroy();
return result;
}
}

View File

@@ -7,6 +7,7 @@ import { RequestUtil } from '@/common/request';
import { HttpClientConfig } from '@/onebot/config/config';
import { ActionMap } from '@/onebot/action';
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
import json5 from 'json5';
export class OB11HttpClientAdapter extends IOB11NetworkAdapter<HttpClientConfig> {
constructor(
@@ -34,7 +35,7 @@ export class OB11HttpClientAdapter extends IOB11NetworkAdapter<HttpClientConfig>
}
const data = await RequestUtil.HttpGetText(this.config.url, 'POST', msgStr, headers);
const resJson: QuickAction = data ? JSON.parse(data) : {};
const resJson: QuickAction = data ? json5.parse(data) : {};
await this.obContext.apis.QuickActionApi.handleQuickOperation(event as QuickActionEvent, resJson);
}

View File

@@ -9,6 +9,7 @@ import { LifeCycleSubType, OB11LifeCycleEvent } from '@/onebot/event/meta/OB11Li
import { WebsocketClientConfig } from '@/onebot/config/config';
import { NapCatOneBot11Adapter } from "@/onebot";
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
import json5 from 'json5';
export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketClientConfig> {
private connection: WebSocket | null = null;
@@ -129,7 +130,7 @@ export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter<WebsocketCli
let echo = undefined;
try {
receiveData = JSON.parse(message.toString());
receiveData = json5.parse(message.toString());
echo = receiveData.echo;
this.logger.logDebug('[OneBot] [WebSocket Client] 收到正向Websocket消息', receiveData);
} catch (e) {

View File

@@ -12,6 +12,7 @@ import { LifeCycleSubType, OB11LifeCycleEvent } from '@/onebot/event/meta/OB11Li
import { WebsocketServerConfig } from '@/onebot/config/config';
import { NapCatOneBot11Adapter } from "@/onebot";
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
import json5 from 'json5';
export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketServerConfig> {
wsServer: WebSocketServer;
@@ -162,7 +163,7 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
let receiveData: { action: typeof ActionName[keyof typeof ActionName], params?: any, echo?: any } = { action: ActionName.Unknown, params: {} };
let echo = undefined;
try {
receiveData = JSON.parse(message.toString());
receiveData = json5.parse(message.toString());
echo = receiveData.echo;
//this.logger.logDebug('收到正向Websocket消息', receiveData);
} catch (e) {

View File

@@ -90,6 +90,7 @@ const UniversalBaseConfig = () =>
entry: {
napcat: 'src/universal/napcat.ts',
'audio-worker': 'src/common/audio-worker.ts',
'ffmpeg-worker': 'src/common/ffmpeg-worker.ts',
},
formats: ['es'],
fileName: (_, entryName) => `${entryName}.mjs`,
@@ -119,6 +120,7 @@ const ShellBaseConfig = () =>
entry: {
napcat: 'src/shell/napcat.ts',
'audio-worker': 'src/common/audio-worker.ts',
'ffmpeg-worker': 'src/common/ffmpeg-worker.ts',
},
formats: ['es'],
fileName: (_, entryName) => `${entryName}.mjs`,
@@ -147,6 +149,7 @@ const FrameworkBaseConfig = () =>
entry: {
napcat: 'src/framework/napcat.ts',
'audio-worker': 'src/common/audio-worker.ts',
'ffmpeg-worker': 'src/common/ffmpeg-worker.ts',
},
formats: ['es'],
fileName: (_, entryName) => `${entryName}.mjs`,