NapCatQQ source snapshot
This commit is contained in:
commit
f954252a34
1111 files changed
+139867
No files matched your search
@@ -0,0 +1,176 @@
|
||||
import { statSync, existsSync, writeFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
import type { VideoInfo } from './video';
|
||||
import { LogWrapper } from '@/napcat-core/helper/log';
|
||||
import { FFmpegAdapterFactory } from './ffmpeg-adapter-factory';
|
||||
import type { IFFmpegAdapter } from './ffmpeg-adapter-interface';
|
||||
import { FFmpegExecAdapter } from './ffmpeg-exec-adapter';
|
||||
import { FFmpegAddonAdapter } from './ffmpeg-addon-adapter';
|
||||
|
||||
export const getFFmpegPath = (tool: 'ffmpeg' | 'ffprobe', binaryPath?: string): string => {
|
||||
const configuredPath = process.env[tool === 'ffmpeg' ? 'FFMPEG_PATH' : 'FFPROBE_PATH'];
|
||||
if (configuredPath) return configuredPath;
|
||||
const executable = tool + (process.platform === 'win32' ? '.exe' : '');
|
||||
if (binaryPath) {
|
||||
const localPath = path.join(binaryPath, 'ffmpeg', executable);
|
||||
if (existsSync(localPath)) return localPath;
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
const directories = (process.env['PATH'] ?? '').split(':').filter(Boolean);
|
||||
directories.push(process.arch === 'arm64' ? '/opt/homebrew/bin' : '/usr/local/bin');
|
||||
for (const directory of directories) {
|
||||
const candidate = path.join(directory, executable);
|
||||
if (existsSync(candidate)) return candidate;
|
||||
}
|
||||
}
|
||||
return executable;
|
||||
};
|
||||
|
||||
export let FFMPEG_CMD = 'ffmpeg';
|
||||
export let FFPROBE_CMD = 'ffprobe';
|
||||
export class FFmpegService {
|
||||
private static adapter: IFFmpegAdapter | null = null;
|
||||
private static initialized = false;
|
||||
private static initializationError: unknown;
|
||||
|
||||
/**
|
||||
* 初始化 FFmpeg 服务
|
||||
* @param binaryPath 二进制文件路径(来自 pathWrapper.binaryPath)
|
||||
* @param logger 日志记录器
|
||||
*/
|
||||
public static async init (binaryPath: string, logger: LogWrapper): Promise<void> {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查本地 ffmpeg 路径
|
||||
FFMPEG_CMD = getFFmpegPath('ffmpeg', binaryPath);
|
||||
FFPROBE_CMD = getFFmpegPath('ffprobe', binaryPath);
|
||||
|
||||
try {
|
||||
this.adapter = await FFmpegAdapterFactory.getAdapter(
|
||||
logger,
|
||||
FFMPEG_CMD,
|
||||
FFPROBE_CMD,
|
||||
binaryPath
|
||||
);
|
||||
} catch (error) {
|
||||
this.initializationError = error;
|
||||
logger.logError('[FFmpeg] 媒体功能初始化失败:', error);
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public static getAdapterName (): string {
|
||||
if (!this.adapter) {
|
||||
throw this.initializationError ?? new Error('FFmpeg service not initialized. Please call FFmpegService.init() first.');
|
||||
}
|
||||
return this.adapter.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 FFmpeg 适配器
|
||||
*/
|
||||
private static async getAdapter (): Promise<IFFmpegAdapter> {
|
||||
if (!this.adapter) {
|
||||
throw this.initializationError ?? new Error('FFmpeg service not initialized. Please call FFmpegService.init() first.');
|
||||
}
|
||||
return this.adapter;
|
||||
}
|
||||
|
||||
public static async convertToNTSilkTct (inputFile: string, outputFile: string): Promise<void> {
|
||||
const adapter = await this.getAdapter();
|
||||
await adapter.convertToNTSilkTct(inputFile, outputFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 FFmpeg 路径并更新适配器
|
||||
* @deprecated 建议使用 init() 方法初始化
|
||||
*/
|
||||
public static async setFfmpegPath (ffmpegPath: string, logger: LogWrapper): Promise<void> {
|
||||
const suffix = process.platform === 'win32' ? '.exe' : '';
|
||||
const ffmpegCommand = path.join(ffmpegPath, 'ffmpeg' + suffix);
|
||||
const ffprobeCommand = path.join(ffmpegPath, 'ffprobe' + suffix);
|
||||
this.adapter = await FFmpegAdapterFactory.updateFFmpegPath(logger, ffmpegCommand, ffprobeCommand);
|
||||
FFMPEG_CMD = ffmpegCommand;
|
||||
FFPROBE_CMD = ffprobeCommand;
|
||||
this.initializationError = undefined;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取视频缩略图
|
||||
*/
|
||||
public static async extractThumbnail (videoPath: string, thumbnailPath: string): Promise<void> {
|
||||
const adapter = await this.getAdapter();
|
||||
await adapter.extractThumbnail(videoPath, thumbnailPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接发送到 QQ kernel 的旧视频元素使用 PNG 缩略图。
|
||||
* Packet/forward 路径继续使用当前适配器生成的 JPEG。
|
||||
*/
|
||||
public static async extractLegacyVideoThumbnail (videoPath: string, thumbnailPath: string): Promise<void> {
|
||||
const currentAdapter = await this.getAdapter();
|
||||
if (currentAdapter instanceof FFmpegAddonAdapter) {
|
||||
await currentAdapter.extractThumbnail(videoPath, thumbnailPath, 'png');
|
||||
return;
|
||||
}
|
||||
const adapter = new FFmpegExecAdapter(FFMPEG_CMD, FFPROBE_CMD);
|
||||
await adapter.extractThumbnail(videoPath, thumbnailPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换音频文件
|
||||
*/
|
||||
public static async convertAudioFmt (inputFile: string, outputFile: string, format: string): Promise<void> {
|
||||
const adapter = await this.getAdapter();
|
||||
await adapter.convertFile(inputFile, outputFile, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频时长
|
||||
*/
|
||||
public static async getDuration (filePath: string): Promise<number> {
|
||||
const adapter = await this.getAdapter();
|
||||
return adapter.getDuration(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 Silk 格式
|
||||
*/
|
||||
public static async isSilk (filePath: string): Promise<boolean> {
|
||||
const adapter = await this.getAdapter();
|
||||
return adapter.isSilk(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 PCM 格式
|
||||
*/
|
||||
public static async convert (filePath: string, pcmPath: string): Promise<{ result: boolean, sampleRate: number; }> {
|
||||
const adapter = await this.getAdapter();
|
||||
return adapter.convertToPCM(filePath, pcmPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取视频信息
|
||||
*/
|
||||
public static async getVideoInfo (videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
|
||||
const adapter = await this.getAdapter();
|
||||
|
||||
const fileSize = statSync(videoPath).size;
|
||||
const videoInfo = await adapter.getVideoInfo(videoPath);
|
||||
if (thumbnailPath && videoInfo.thumbnail) {
|
||||
writeFileSync(thumbnailPath, videoInfo.thumbnail);
|
||||
}
|
||||
return {
|
||||
width: videoInfo.width,
|
||||
height: videoInfo.height,
|
||||
time: videoInfo.duration,
|
||||
format: videoInfo.format,
|
||||
size: fileSize,
|
||||
filePath: videoPath,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user