@deepfish-ai/ffmpeg7-media-tools 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,987 @@
1
+ /**
2
+ * 基于FFmpeg 7的集成媒体处理工具
3
+ * 合并了ffmpegTools.js和index.js的功能,去除重复部分
4
+ * @version 1.0.0
5
+ */
6
+
7
+ const functions = {};
8
+
9
+ // ============ 内部辅助函数 ============
10
+
11
+ /**
12
+ * 内部函数:检测ffmpeg是否安装
13
+ */
14
+ const checkFfmpeg = async () => {
15
+ try {
16
+ // 根据不同平台执行不同的命令
17
+ const platform = process.platform;
18
+ let command;
19
+ if (platform === 'win32') {
20
+ command = 'where ffmpeg';
21
+ } else {
22
+ command = 'which ffmpeg';
23
+ }
24
+
25
+ await this.Tools.executeCommand(command);
26
+ return { installed: true, error: null };
27
+ } catch (error) {
28
+ return {
29
+ installed: false,
30
+ error: error.message,
31
+ message: 'FFmpeg未安装或未在系统路径中。请访问 https://ffmpeg.p2hp.com/download.html 下载并安装FFmpeg。'
32
+ };
33
+ }
34
+ };
35
+
36
+ /**
37
+ * 内部函数:获取ffmpeg版本
38
+ */
39
+ const getFfmpegVersion = async () => {
40
+ try {
41
+ const result = await this.Tools.executeCommand('ffmpeg -version');
42
+ // 从输出中提取版本号
43
+ const versionMatch = result.match(/ffmpeg version (\S+)/);
44
+ const version = versionMatch ? versionMatch[1] : '未知版本';
45
+ return {
46
+ version,
47
+ details: result.split('\n').slice(0, 5).join('\n') // 返回前5行信息
48
+ };
49
+ } catch (error) {
50
+ return { version: null, error: error.message };
51
+ }
52
+ };
53
+
54
+
55
+ // ============ 公共功能函数定义 ============
56
+
57
+ /**
58
+ * 检测ffmpeg是否安装和版本检测
59
+ *
60
+ * @param {Object} params - 参数对象,具体属性见描述
61
+ * @returns {Promise<Object>} 处理结果
62
+ */
63
+ functions.ffmpeg_checkFfmpegInstallation = async (params = {}) => {
64
+ const { minVersion } = params;
65
+ // 检查是否安装
66
+ const checkResult = await checkFfmpeg();
67
+ if (!checkResult.installed) {
68
+ return {
69
+ installed: false,
70
+ message: checkResult.message,
71
+ error: checkResult.error
72
+ };
73
+ }
74
+ // 获取版本信息
75
+ const versionInfo = await getFfmpegVersion();
76
+ if (versionInfo.error) {
77
+ return {
78
+ installed: false,
79
+ message: '无法获取FFmpeg版本信息,请确保ffmpeg正确安装',
80
+ error: versionInfo.error
81
+ };
82
+ }
83
+ // 检查版本是否满足要求
84
+ let versionOk = true;
85
+ let versionMessage = '';
86
+ if (minVersion && versionInfo.version) {
87
+ const currentVersion = versionInfo.version.split('-')[0]; // 移除可能的构建信息
88
+ const currentParts = currentVersion.split('.').map(Number);
89
+ const minParts = minVersion.split('.').map(Number);
90
+ // 简单的版本比较
91
+ for (let i = 0; i < Math.min(currentParts.length, minParts.length); i++) {
92
+ if (currentParts[i] > minParts[i]) {
93
+ versionOk = true;
94
+ break;
95
+ } else if (currentParts[i] < minParts[i]) {
96
+ versionOk = false;
97
+ break;
98
+ }
99
+ }
100
+ if (!versionOk) {
101
+ versionMessage = `当前版本${versionInfo.version}低于要求版本${minVersion}`;
102
+ }
103
+ }
104
+ return {
105
+ installed: true,
106
+ version: versionInfo.version,
107
+ versionOk,
108
+ versionMessage,
109
+ details: versionInfo.details,
110
+ message: versionOk
111
+ ? `FFmpeg已安装,版本: ${versionInfo.version}`
112
+ : `FFmpeg版本不符合要求: ${versionMessage}`
113
+ };
114
+ };
115
+
116
+ /**
117
+ * 转换视频格式
118
+ *
119
+ * @param {Object} params - 参数对象,具体属性见描述
120
+ * @returns {Promise<Object>} 处理结果
121
+ */
122
+ functions.ffmpeg_convertVideoFormat = async (params) => {
123
+ const { inputPath, outputPath, format, quality = '-crf 23' } = params;
124
+ // 检查ffmpeg安装
125
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
126
+ if (!checkResult.installed || !checkResult.versionOk) {
127
+ throw new Error(`FFmpeg检查失败: ${checkResult.message}`);
128
+ }
129
+ // 确保输出路径有正确的扩展名
130
+ let finalOutputPath = outputPath;
131
+ if (format && !finalOutputPath.toLowerCase().endsWith(`.${format.toLowerCase()}`)) {
132
+ finalOutputPath = finalOutputPath.replace(/\.[^/.]+$/, "") + `.${format}`;
133
+ }
134
+ const command = `ffmpeg -i "${inputPath}" ${quality} "${finalOutputPath}"`;
135
+ await this.Tools.executeCommand(command);
136
+ return {
137
+ success: true,
138
+ message: `视频格式转换完成: ${finalOutputPath}`,
139
+ outputPath: finalOutputPath
140
+ };
141
+ };
142
+
143
+ /**
144
+ * 从视频中提取音频
145
+ *
146
+ * @param {Object} params - 参数对象,具体属性见描述
147
+ * @returns {Promise<Object>} 处理结果
148
+ */
149
+ functions.ffmpeg_extractAudioFromVideo = async (params) => {
150
+ const { videoPath, audioPath, audioFormat = 'mp3' } = params;
151
+ // 检查ffmpeg安装
152
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
153
+ if (!checkResult.installed || !checkResult.versionOk) {
154
+ throw new Error(`FFmpeg检查失败: ${checkResult.message}`);
155
+ }
156
+ // 确保输出路径有正确的扩展名
157
+ let finalAudioPath = audioPath;
158
+ if (!finalAudioPath.toLowerCase().endsWith(`.${audioFormat.toLowerCase()}`)) {
159
+ finalAudioPath = finalAudioPath.replace(/\.[^/.]+$/, "") + `.${audioFormat}`;
160
+ }
161
+ const command = `ffmpeg -i "${videoPath}" -q:a 0 -map a "${finalAudioPath}"`;
162
+ await this.Tools.executeCommand(command);
163
+ return {
164
+ success: true,
165
+ message: `音频提取完成: ${finalAudioPath}`,
166
+ audioPath: finalAudioPath
167
+ };
168
+ };
169
+
170
+ /**
171
+ * 调整视频尺寸
172
+ *
173
+ * @param {Object} params - 参数对象,具体属性见描述
174
+ * @returns {Promise<Object>} 处理结果
175
+ */
176
+ functions.ffmpeg_resizeVideo = async (params) => {
177
+ const { inputPath, outputPath, width, height, keepAspectRatio = true } = params;
178
+ // 检查ffmpeg安装
179
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
180
+ if (!checkResult.installed || !checkResult.versionOk) {
181
+ throw new Error(`FFmpeg检查失败: ${checkResult.message}`);
182
+ }
183
+ let scaleFilter = '';
184
+ if (keepAspectRatio) {
185
+ scaleFilter = `scale=w=${width}:h=${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`;
186
+ } else {
187
+ scaleFilter = `scale=${width}:${height}`;
188
+ }
189
+ const command = `ffmpeg -i "${inputPath}" -vf "${scaleFilter}" -c:a copy "${outputPath}"`;
190
+ await this.Tools.executeCommand(command);
191
+ return {
192
+ success: true,
193
+ message: `视频尺寸调整完成: ${width}x${height}`,
194
+ outputPath: outputPath
195
+ };
196
+ };
197
+
198
+ /**
199
+ * 剪切视频片段
200
+ *
201
+ * @param {Object} params - 参数对象,具体属性见描述
202
+ * @returns {Promise<Object>} 处理结果
203
+ */
204
+ functions.ffmpeg_trimVideo = async (params) => {
205
+ const { inputPath, outputPath, startTime, duration } = params;
206
+ // 检查ffmpeg安装
207
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
208
+ if (!checkResult.installed || !checkResult.versionOk) {
209
+ throw new Error(`FFmpeg检查失败: ${checkResult.message}`);
210
+ }
211
+ const command = `ffmpeg -i "${inputPath}" -ss ${startTime} -t ${duration} -c copy "${outputPath}"`;
212
+ await this.Tools.executeCommand(command);
213
+ return {
214
+ success: true,
215
+ message: `视频剪切完成: 从 ${startTime} 开始,持续 ${duration}`,
216
+ outputPath: outputPath
217
+ };
218
+ };
219
+
220
+ /**
221
+ * 合并多个视频文件
222
+ *
223
+ * @param {Object} params - 参数对象,具体属性见描述
224
+ * @returns {Promise<Object>} 处理结果
225
+ */
226
+ functions.ffmpeg_mergeVideos = async (params) => {
227
+ const { videoPaths, outputPath, method = 'concat' } = params;
228
+ // 检查ffmpeg安装
229
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
230
+ if (!checkResult.installed || !checkResult.versionOk) {
231
+ throw new Error(`FFmpeg检查失败: ${checkResult.message}`);
232
+ }
233
+ if (method === 'concat') {
234
+ // 创建文件列表
235
+ const listFilePath = outputPath.replace(/\.[^/.]+$/, "") + '_list.txt';
236
+ let listContent = '';
237
+ videoPaths.forEach(path => {
238
+ listContent += `file '${path}'\n`;
239
+ });
240
+ await this.Tools.createFile(listFilePath, listContent);
241
+ const command = `ffmpeg -f concat -safe 0 -i "${listFilePath}" -c copy "${outputPath}"`;
242
+ await this.Tools.executeCommand(command);
243
+ // 删除临时文件
244
+ await this.Tools.deleteFile(listFilePath);
245
+ } else if (method === 'overlay') {
246
+ // 简单叠加示例(实际应用中可能需要更复杂的处理)
247
+ if (videoPaths.length === 2) {
248
+ const command = `ffmpeg -i "${videoPaths[0]}" -i "${videoPaths[1]}" -filter_complex "[0:v][1:v]overlay=10:10" "${outputPath}"`;
249
+ await this.Tools.executeCommand(command);
250
+ } else {
251
+ throw new Error('叠加模式目前仅支持两个视频');
252
+ }
253
+ } else {
254
+ throw new Error(`不支持的合并方法: ${method}`);
255
+ }
256
+ return {
257
+ success: true,
258
+ message: `视频合并完成,使用 ${method} 方法`,
259
+ outputPath: outputPath
260
+ };
261
+ };
262
+
263
+ /**
264
+ * 转换音频格式
265
+ *
266
+ * @param {Object} params - 参数对象,具体属性见描述
267
+ * @returns {Promise<Object>} 处理结果
268
+ */
269
+ functions.ffmpeg_convertAudioFormat = async (params) => {
270
+ const { inputPath, outputPath, format, bitrate = '' } = params;
271
+ // 检查ffmpeg安装
272
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
273
+ if (!checkResult.installed || !checkResult.versionOk) {
274
+ throw new Error(`FFmpeg检查失败: ${checkResult.message}`);
275
+ }
276
+ // 确保输出路径有正确的扩展名
277
+ let finalOutputPath = outputPath;
278
+ if (!finalOutputPath.toLowerCase().endsWith(`.${format.toLowerCase()}`)) {
279
+ finalOutputPath = finalOutputPath.replace(/\.[^/.]+$/, "") + `.${format}`;
280
+ }
281
+ let bitrateOption = '';
282
+ if (bitrate) {
283
+ bitrateOption = `-b:a ${bitrate}`;
284
+ }
285
+ const command = `ffmpeg -i "${inputPath}" ${bitrateOption} "${finalOutputPath}"`;
286
+ await this.Tools.executeCommand(command);
287
+ return {
288
+ success: true,
289
+ message: `音频格式转换完成: ${format}`,
290
+ outputPath: finalOutputPath
291
+ };
292
+ };
293
+
294
+ /**
295
+ * 调整音频音量
296
+ *
297
+ * @param {Object} params - 参数对象,具体属性见描述
298
+ * @returns {Promise<Object>} 处理结果
299
+ */
300
+ functions.ffmpeg_adjustAudioVolume = async (params) => {
301
+ const { inputPath, outputPath, volume } = params;
302
+ // 检查ffmpeg安装
303
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
304
+ if (!checkResult.installed || !checkResult.versionOk) {
305
+ throw new Error(`FFmpeg检查失败: ${checkResult.message}`);
306
+ }
307
+ const command = `ffmpeg -i "${inputPath}" -filter:a "volume=${volume}" "${outputPath}"`;
308
+ await this.Tools.executeCommand(command);
309
+ return {
310
+ success: true,
311
+ message: `音频音量调整完成: ${volume}倍`,
312
+ outputPath: outputPath
313
+ };
314
+ };
315
+
316
+ /**
317
+ * 添加水印
318
+ *
319
+ * @param {Object} params - 参数对象,具体属性见描述
320
+ * @returns {Promise<Object>} 处理结果
321
+ */
322
+ functions.ffmpeg_extractVideoThumbnail = async (params) => {
323
+ const { videoPath, outputPath, time = '00:00:01', size = '' } = params;
324
+ // 检查ffmpeg安装
325
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
326
+ if (!checkResult.installed || !checkResult.versionOk) {
327
+ throw new Error(`FFmpeg检查失败: ${checkResult.message}`);
328
+ }
329
+ // 确保输出路径有图片扩展名
330
+ let finalOutputPath = outputPath;
331
+ if (!finalOutputPath.toLowerCase().match(/\.(jpg|jpeg|png|bmp|gif)$/)) {
332
+ finalOutputPath = finalOutputPath.replace(/\.[^/.]+$/, "") + '.jpg';
333
+ }
334
+ let sizeOption = '';
335
+ if (size) {
336
+ sizeOption = `-s ${size}`;
337
+ }
338
+ const command = `ffmpeg -i "${videoPath}" -ss ${time} -vframes 1 ${sizeOption} "${finalOutputPath}"`;
339
+ await this.Tools.executeCommand(command);
340
+ return {
341
+ success: true,
342
+ message: `视频缩略图提取完成: ${time}`,
343
+ outputPath: finalOutputPath
344
+ };
345
+ };
346
+
347
+ /**
348
+ * 调整视频比特率
349
+ *
350
+ * @param {Object} params - 参数对象,具体属性见描述
351
+ * @returns {Promise<Object>} 处理结果
352
+ */
353
+ functions.ffmpeg_getMediaInfo = async (params) => {
354
+ const { mediaPath } = params;
355
+ // 检查ffmpeg安装
356
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
357
+ if (!checkResult.installed || !checkResult.versionOk) {
358
+ throw new Error(`FFmpeg检查失败: ${checkResult.message}`);
359
+ }
360
+ try {
361
+ const command = `ffprobe -v quiet -print_format json -show_format -show_streams "${mediaPath}"`;
362
+ const result = await this.Tools.executeCommand(command);
363
+ // 解析JSON结果
364
+ const info = JSON.parse(result);
365
+ // 提取关键信息
366
+ const formatInfo = info.format || {};
367
+ const streams = info.streams || [];
368
+ const videoStreams = streams.filter(s => s.codec_type === 'video');
369
+ const audioStreams = streams.filter(s => s.codec_type === 'audio');
370
+ const mediaInfo = {
371
+ format: formatInfo.format_name || 'unknown',
372
+ duration: formatInfo.duration || '0',
373
+ size: formatInfo.size || '0',
374
+ bitrate: formatInfo.bit_rate || '0',
375
+ videoStreams: videoStreams.length,
376
+ audioStreams: audioStreams.length,
377
+ streams: streams.map(stream => ({
378
+ type: stream.codec_type,
379
+ codec: stream.codec_name,
380
+ width: stream.width || null,
381
+ height: stream.height || null,
382
+ duration: stream.duration || null,
383
+ bitrate: stream.bit_rate || null,
384
+ sampleRate: stream.sample_rate || null,
385
+ channels: stream.channels || null
386
+ })),
387
+ rawInfo: info
388
+ };
389
+ return {
390
+ success: true,
391
+ message: '媒体信息获取成功',
392
+ info: mediaInfo
393
+ };
394
+ } catch (error) {
395
+ // 如果ffprobe失败,尝试使用ffmpeg获取基本信息
396
+ try {
397
+ const fallbackCommand = `ffmpeg -i "${mediaPath}"`;
398
+ await this.Tools.executeCommand(fallbackCommand);
399
+ } catch (ffmpegError) {
400
+ // 从错误信息中提取信息
401
+ const errorStr = ffmpegError.toString();
402
+ const durationMatch = errorStr.match(/Duration: (\d{2}:\d{2}:\d{2}\.\d{2})/);
403
+ const videoMatch = errorStr.match(/Video: ([^,]+)/);
404
+ const audioMatch = errorStr.match(/Audio: ([^,]+)/);
405
+ return {
406
+ success: true,
407
+ message: '媒体信息获取成功(从错误信息中提取)',
408
+ info: {
409
+ format: 'unknown',
410
+ duration: durationMatch ? durationMatch[1] : '0',
411
+ size: 'unknown',
412
+ bitrate: 'unknown',
413
+ videoStreams: videoMatch ? 1 : 0,
414
+ audioStreams: audioMatch ? 1 : 0,
415
+ streams: [],
416
+ rawInfo: { note: '信息从ffmpeg错误输出中提取' }
417
+ }
418
+ };
419
+ }
420
+ throw new Error(`无法获取媒体文件信息: ${error.message}`);
421
+ }
422
+ };
423
+
424
+ /**
425
+ * 合并视频和音频
426
+ *
427
+ * @param {Object} params - 参数对象,具体属性见描述
428
+ * @returns {Promise<Object>} 处理结果
429
+ */
430
+ functions.ffmpeg_addWatermark = async (params) => {
431
+ const { inputPath, outputPath, watermarkPath, position = 'bottom-right', opacity = 1 } = params;
432
+ // 检查ffmpeg安装
433
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
434
+ if (!checkResult.installed || !checkResult.versionOk) {
435
+ throw new Error(`FFmpeg检查失败: ${checkResult.message}`);
436
+ }
437
+ // 构建ffmpeg命令
438
+ let command = `ffmpeg -i "${inputPath}" -i "${watermarkPath}"`;
439
+ // 设置水印位置
440
+ let filter = 'overlay=';
441
+ switch (position) {
442
+ case 'top-left':
443
+ filter += '10:10';
444
+ break;
445
+ case 'top-right':
446
+ filter += 'main_w-overlay_w-10:10';
447
+ break;
448
+ case 'bottom-left':
449
+ filter += '10:main_h-overlay_h-10';
450
+ break;
451
+ case 'bottom-right':
452
+ filter += 'main_w-overlay_w-10:main_h-overlay_h-10';
453
+ break;
454
+ case 'center':
455
+ filter += '(main_w-overlay_w)/2:(main_h-overlay_h)/2';
456
+ break;
457
+ default:
458
+ filter += 'main_w-overlay_w-10:main_h-overlay_h-10'; // 默认右下角
459
+ }
460
+ // 添加透明度参数
461
+ let opacityFilter = '';
462
+ if (opacity < 1) {
463
+ opacityFilter = `[1:v]format=rgba,colorchannelmixer=aa=${opacity}[wm];[0:v][wm]${filter}`;
464
+ } else {
465
+ opacityFilter = `[0:v][1:v]${filter}`;
466
+ }
467
+ command += ` -filter_complex "${opacityFilter}"`;
468
+ // 添加输出路径
469
+ command += ` "${outputPath}"`;
470
+ try {
471
+ const result = await this.Tools.executeCommand(command);
472
+ return {
473
+ success: true,
474
+ message: `水印添加完成: ${outputPath}`,
475
+ command,
476
+ outputPath: outputPath
477
+ };
478
+ } catch (error) {
479
+ throw new Error(`水印添加失败: ${error.message}`);
480
+ }
481
+ };
482
+
483
+ /**
484
+ * 提取视频缩略图
485
+ *
486
+ * @param {Object} params - 参数对象,具体属性见描述
487
+ * @returns {Promise<Object>} 处理结果
488
+ */
489
+ functions.ffmpeg_adjustBitrate = async (params) => {
490
+ const { inputPath, outputPath, bitrate, audioBitrate } = params;
491
+ // 检查ffmpeg安装
492
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
493
+ if (!checkResult.installed || !checkResult.versionOk) {
494
+ throw new Error(`FFmpeg检查失败: ${checkResult.message}`);
495
+ }
496
+ // 构建ffmpeg命令
497
+ let command = `ffmpeg -i "${inputPath}" -b:v ${bitrate}`;
498
+ // 添加音频比特率参数
499
+ if (audioBitrate) {
500
+ command += ` -b:a ${audioBitrate}`;
501
+ }
502
+ // 添加输出路径
503
+ command += ` "${outputPath}"`;
504
+ try {
505
+ const result = await this.Tools.executeCommand(command);
506
+ return {
507
+ success: true,
508
+ message: `视频比特率调整完成: ${outputPath}`,
509
+ command,
510
+ outputPath: outputPath
511
+ };
512
+ } catch (error) {
513
+ throw new Error(`视频比特率调整失败: ${error.message}`);
514
+ }
515
+ };
516
+
517
+ /**
518
+ * 获取媒体文件信息
519
+ *
520
+ * @param {Object} params - 参数对象,具体属性见描述
521
+ * @returns {Promise<Object>} 处理结果
522
+ */
523
+ functions.ffmpeg_mergeVideoAudio = async (params) => {
524
+ const { videoPath, audioPath, outputPath, sync = true } = params;
525
+ // 检查ffmpeg安装
526
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
527
+ if (!checkResult.installed || !checkResult.versionOk) {
528
+ throw new Error(`FFmpeg检查失败: ${checkResult.message}`);
529
+ }
530
+ // 构建ffmpeg命令
531
+ let command;
532
+ if (sync) {
533
+ command = `ffmpeg -i "${videoPath}" -i "${audioPath}" -c:v copy -c:a aac -map 0:v:0 -map 1:a:0 -shortest "${outputPath}"`;
534
+ } else {
535
+ command = `ffmpeg -i "${videoPath}" -i "${audioPath}" -c:v copy -c:a aac -map 0:v:0 -map 1:a:0 "${outputPath}"`;
536
+ }
537
+ try {
538
+ const result = await this.Tools.executeCommand(command);
539
+ return {
540
+ success: true,
541
+ message: `视频音频合并完成: ${outputPath}`,
542
+ command,
543
+ outputPath: outputPath
544
+ };
545
+ } catch (error) {
546
+ throw new Error(`视频音频合并失败: ${error.message}`);
547
+ }
548
+ };
549
+
550
+ // ============ 新增常用功能函数 ============
551
+
552
+ /**
553
+ * 将视频转换为GIF动图
554
+ *
555
+ * @param {Object} params.videoPath 输入视频文件路径
556
+ * @param {Object} params.outputPath 输出GIF文件路径
557
+ * @param {Object} params.startTime 开始时间(格式:HH:MM:SS 或 秒数,默认为00:00:00)(可选)
558
+ * @param {Object} params.duration 持续时间(格式:HH:MM:SS 或 秒数,默认为全部)(可选)
559
+ * @param {Object} params.fps 帧率(默认为10)(可选)
560
+ * @param {Object} params.width 输出宽度(保持宽高比,高度自动计算)(可选)
561
+ * @returns {Promise<Object>} 处理结果
562
+ */
563
+ functions.ffmpeg_videoToGif = async (params) => {
564
+ try {
565
+ // 检查ffmpeg安装
566
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
567
+ if (!checkResult.installed || !checkResult.versionOk) {
568
+ throw new Error(`FFmpeg未安装或版本不符合要求: ${checkResult.message}`);
569
+ }
570
+
571
+ // 参数解构和验证
572
+ const { videoPath, outputPath, startTime, duration, fps, width } = params;
573
+ if (!videoPath) throw new Error('缺少必要参数: videoPath');
574
+ if (!outputPath) throw new Error('缺少必要参数: outputPath');
575
+
576
+ // 构建视频转GIF命令
577
+ let command = `ffmpeg -i "${videoPath}"`;
578
+ if (startTime) command += ` -ss ${startTime}`;
579
+ if (duration) command += ` -t ${duration}`;
580
+ command += ` -vf "fps=${fps || 10},scale=${width || -1}:-1:flags=lanczos"`;
581
+ command += ` -gifflags +transdiff`;
582
+ command += ` "${outputPath}"`;
583
+
584
+ // 执行命令
585
+ await this.Tools.executeCommand(command);
586
+
587
+ return {
588
+ success: true,
589
+ message: `${description.split(':')[1] || '操作'}完成: ${outputPath}`,
590
+ command,
591
+ outputPath
592
+ };
593
+ } catch (error) {
594
+ throw new Error(`${name.replace('ffmpeg_', '')}失败: ${error.message}`);
595
+ }
596
+ };
597
+
598
+ /**
599
+ * 裁剪视频区域
600
+ *
601
+ * @param {Object} params.videoPath 输入视频文件路径
602
+ * @param {Object} params.outputPath 输出视频文件路径
603
+ * @param {Object} params.x 起始X坐标(像素)
604
+ * @param {Object} params.y 起始Y坐标(像素)
605
+ * @param {Object} params.width 裁剪宽度(像素)
606
+ * @param {Object} params.height 裁剪高度(像素)
607
+ * @returns {Promise<Object>} 处理结果
608
+ */
609
+ functions.ffmpeg_cropVideo = async (params) => {
610
+ try {
611
+ // 检查ffmpeg安装
612
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
613
+ if (!checkResult.installed || !checkResult.versionOk) {
614
+ throw new Error(`FFmpeg未安装或版本不符合要求: ${checkResult.message}`);
615
+ }
616
+
617
+ // 参数解构和验证
618
+ const { videoPath, outputPath, x, y, width, height } = params;
619
+ if (!videoPath) throw new Error('缺少必要参数: videoPath');
620
+ if (!outputPath) throw new Error('缺少必要参数: outputPath');
621
+ if (!x) throw new Error('缺少必要参数: x');
622
+ if (!y) throw new Error('缺少必要参数: y');
623
+ if (!width) throw new Error('缺少必要参数: width');
624
+ if (!height) throw new Error('缺少必要参数: height');
625
+
626
+ // 构建视频裁剪命令
627
+ const command = `ffmpeg -i "${videoPath}" -vf "crop=${width}:${height}:${x}:${y}" "${outputPath}"`;
628
+
629
+ // 执行命令
630
+ await this.Tools.executeCommand(command);
631
+
632
+ return {
633
+ success: true,
634
+ message: `${description.split(':')[1] || '操作'}完成: ${outputPath}`,
635
+ command,
636
+ outputPath
637
+ };
638
+ } catch (error) {
639
+ throw new Error(`${name.replace('ffmpeg_', '')}失败: ${error.message}`);
640
+ }
641
+ };
642
+
643
+ /**
644
+ * 旋转视频
645
+ *
646
+ * @param {Object} params.videoPath 输入视频文件路径
647
+ * @param {Object} params.outputPath 输出视频文件路径
648
+ * @param {Object} params.angle 旋转角度(90, 180, 270, 或 transpose参数)
649
+ * @returns {Promise<Object>} 处理结果
650
+ */
651
+ functions.ffmpeg_rotateVideo = async (params) => {
652
+ try {
653
+ // 检查ffmpeg安装
654
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
655
+ if (!checkResult.installed || !checkResult.versionOk) {
656
+ throw new Error(`FFmpeg未安装或版本不符合要求: ${checkResult.message}`);
657
+ }
658
+
659
+ // 参数解构和验证
660
+ const { videoPath, outputPath, angle } = params;
661
+ if (!videoPath) throw new Error('缺少必要参数: videoPath');
662
+ if (!outputPath) throw new Error('缺少必要参数: outputPath');
663
+ if (!angle) throw new Error('缺少必要参数: angle');
664
+
665
+ // 构建视频旋转命令
666
+ let filter = '';
667
+ if (angle === '90') filter = 'transpose=1';
668
+ else if (angle === '180') filter = 'transpose=2,transpose=2';
669
+ else if (angle === '270') filter = 'transpose=2';
670
+ else if (angle === 'hflip') filter = 'hflip';
671
+ else if (angle === 'vflip') filter = 'vflip';
672
+ else filter = angle; // 允许直接传递filter表达式
673
+ const command = `ffmpeg -i "${videoPath}" -vf "${filter}" "${outputPath}"`;
674
+
675
+ // 执行命令
676
+ await this.Tools.executeCommand(command);
677
+
678
+ return {
679
+ success: true,
680
+ message: `${description.split(':')[1] || '操作'}完成: ${outputPath}`,
681
+ command,
682
+ outputPath
683
+ };
684
+ } catch (error) {
685
+ throw new Error(`${name.replace('ffmpeg_', '')}失败: ${error.message}`);
686
+ }
687
+ };
688
+
689
+ /**
690
+ * 改变视频播放速度
691
+ *
692
+ * @param {Object} params.videoPath 输入视频文件路径
693
+ * @param {Object} params.outputPath 输出视频文件路径
694
+ * @param {Object} params.speed 速度倍数(0.5表示慢放一半,2.0表示快放一倍)
695
+ * @returns {Promise<Object>} 处理结果
696
+ */
697
+ functions.ffmpeg_changeVideoSpeed = async (params) => {
698
+ try {
699
+ // 检查ffmpeg安装
700
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
701
+ if (!checkResult.installed || !checkResult.versionOk) {
702
+ throw new Error(`FFmpeg未安装或版本不符合要求: ${checkResult.message}`);
703
+ }
704
+
705
+ // 参数解构和验证
706
+ const { videoPath, outputPath, speed } = params;
707
+ if (!videoPath) throw new Error('缺少必要参数: videoPath');
708
+ if (!outputPath) throw new Error('缺少必要参数: outputPath');
709
+ if (!speed) throw new Error('缺少必要参数: speed');
710
+
711
+ // 构建视频变速命令
712
+ const command = `ffmpeg -i "${videoPath}" -filter_complex "[0:v]setpts=${1/speed}*PTS[v];[0:a]atempo=${speed}[a]" -map "[v]" -map "[a]" "${outputPath}"`;
713
+
714
+ // 执行命令
715
+ await this.Tools.executeCommand(command);
716
+
717
+ return {
718
+ success: true,
719
+ message: `${description.split(':')[1] || '操作'}完成: ${outputPath}`,
720
+ command,
721
+ outputPath
722
+ };
723
+ } catch (error) {
724
+ throw new Error(`${name.replace('ffmpeg_', '')}失败: ${error.message}`);
725
+ }
726
+ };
727
+
728
+ /**
729
+ * 添加字幕到视频
730
+ *
731
+ * @param {Object} params.videoPath 输入视频文件路径
732
+ * @param {Object} params.subtitlePath 字幕文件路径(支持srt, ass等格式)
733
+ * @param {Object} params.outputPath 输出视频文件路径
734
+ * @param {Object} params.encoding 字幕编码(默认为UTF-8)(可选)
735
+ * @returns {Promise<Object>} 处理结果
736
+ */
737
+ functions.ffmpeg_addSubtitles = async (params) => {
738
+ try {
739
+ // 检查ffmpeg安装
740
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
741
+ if (!checkResult.installed || !checkResult.versionOk) {
742
+ throw new Error(`FFmpeg未安装或版本不符合要求: ${checkResult.message}`);
743
+ }
744
+
745
+ // 参数解构和验证
746
+ const { videoPath, subtitlePath, outputPath, encoding } = params;
747
+ if (!videoPath) throw new Error('缺少必要参数: videoPath');
748
+ if (!subtitlePath) throw new Error('缺少必要参数: subtitlePath');
749
+ if (!outputPath) throw new Error('缺少必要参数: outputPath');
750
+
751
+ // 构建添加字幕命令
752
+ let command = `ffmpeg -i "${videoPath}"`;
753
+ if (encoding) command += ` -sub_charenc ${encoding}`;
754
+ command += ` -vf "subtitles=${subtitlePath}"`;
755
+ command += ` -c:a copy "${outputPath}"`;
756
+
757
+ // 执行命令
758
+ await this.Tools.executeCommand(command);
759
+
760
+ return {
761
+ success: true,
762
+ message: `${description.split(':')[1] || '操作'}完成: ${outputPath}`,
763
+ command,
764
+ outputPath
765
+ };
766
+ } catch (error) {
767
+ throw new Error(`${name.replace('ffmpeg_', '')}失败: ${error.message}`);
768
+ }
769
+ };
770
+
771
+ /**
772
+ * 提取视频帧为图片序列
773
+ *
774
+ * @param {Object} params.videoPath 输入视频文件路径
775
+ * @param {Object} params.outputDir 输出目录路径
776
+ * @param {Object} params.fps 每秒提取帧数(默认为1)(可选)
777
+ * @param {Object} params.format 输出图片格式(jpg, png等,默认为jpg)(可选)
778
+ * @param {Object} params.startTime 开始时间(格式:HH:MM:SS)(可选)
779
+ * @param {Object} params.duration 持续时间(格式:HH:MM:SS)(可选)
780
+ * @returns {Promise<Object>} 处理结果
781
+ */
782
+ functions.ffmpeg_extractVideoFrames = async (params) => {
783
+ try {
784
+ // 检查ffmpeg安装
785
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
786
+ if (!checkResult.installed || !checkResult.versionOk) {
787
+ throw new Error(`FFmpeg未安装或版本不符合要求: ${checkResult.message}`);
788
+ }
789
+
790
+ // 参数解构和验证
791
+ const { videoPath, outputDir, fps, format, startTime, duration } = params;
792
+ if (!videoPath) throw new Error('缺少必要参数: videoPath');
793
+ if (!outputDir) throw new Error('缺少必要参数: outputDir');
794
+
795
+ // 构建提取视频帧命令
796
+ let command = `ffmpeg -i "${videoPath}"`;
797
+ if (startTime) command += ` -ss ${startTime}`;
798
+ if (duration) command += ` -t ${duration}`;
799
+ command += ` -vf "fps=${fps || 1}"`;
800
+ command += ` "${outputDir}/frame_%04d.${format || 'jpg'}"`;
801
+
802
+ // 执行命令
803
+ await this.Tools.executeCommand(command);
804
+
805
+ return {
806
+ success: true,
807
+ message: `${description.split(':')[1] || '操作'}完成: ${outputPath}`,
808
+ command,
809
+ outputPath
810
+ };
811
+ } catch (error) {
812
+ throw new Error(`${name.replace('ffmpeg_', '')}失败: ${error.message}`);
813
+ }
814
+ };
815
+
816
+ /**
817
+ * 拼接多个视频文件(使用concat demuxer)
818
+ *
819
+ * @param {Object} params.videoPaths 视频文件路径数组
820
+ * @param {Object} params.outputPath 输出视频文件路径
821
+ * @param {Object} params.transition 转场效果(none, fade等,可选)(可选)
822
+ * @returns {Promise<Object>} 处理结果
823
+ */
824
+ functions.ffmpeg_concatVideos = async (params) => {
825
+ try {
826
+ // 检查ffmpeg安装
827
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
828
+ if (!checkResult.installed || !checkResult.versionOk) {
829
+ throw new Error(`FFmpeg未安装或版本不符合要求: ${checkResult.message}`);
830
+ }
831
+
832
+ // 参数解构和验证
833
+ const { videoPaths, outputPath, transition } = params;
834
+ if (!videoPaths) throw new Error('缺少必要参数: videoPaths');
835
+ if (!outputPath) throw new Error('缺少必要参数: outputPath');
836
+
837
+ // 构建视频拼接命令
838
+ // 创建临时文件列表
839
+ const listPath = `${outputPath}.txt`;
840
+ const listContent = videoPaths.map(path => `file '${path}'`).join('\n');
841
+ await this.Tools.fs.writeFile(listPath, listContent);
842
+ const command = `ffmpeg -f concat -safe 0 -i "${listPath}" -c copy "${outputPath}"`;
843
+
844
+ // 执行命令
845
+ await this.Tools.executeCommand(command);
846
+
847
+ return {
848
+ success: true,
849
+ message: `${description.split(':')[1] || '操作'}完成: ${outputPath}`,
850
+ command,
851
+ outputPath
852
+ };
853
+ } catch (error) {
854
+ throw new Error(`${name.replace('ffmpeg_', '')}失败: ${error.message}`);
855
+ }
856
+ };
857
+
858
+ /**
859
+ * 混合多个音频文件
860
+ *
861
+ * @param {Object} params.audioPaths 音频文件路径数组
862
+ * @param {Object} params.outputPath 输出音频文件路径
863
+ * @param {Object} params.volumes 各音频音量数组(如[1.0, 0.5])(可选)
864
+ * @returns {Promise<Object>} 处理结果
865
+ */
866
+ functions.ffmpeg_mixAudios = async (params) => {
867
+ try {
868
+ // 检查ffmpeg安装
869
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
870
+ if (!checkResult.installed || !checkResult.versionOk) {
871
+ throw new Error(`FFmpeg未安装或版本不符合要求: ${checkResult.message}`);
872
+ }
873
+
874
+ // 参数解构和验证
875
+ const { audioPaths, outputPath, volumes } = params;
876
+ if (!audioPaths) throw new Error('缺少必要参数: audioPaths');
877
+ if (!outputPath) throw new Error('缺少必要参数: outputPath');
878
+
879
+ // 构建音频混合命令
880
+ const inputs = audioPaths.map((path, i) => `-i "${path}"`).join(' ');
881
+ const mixFilter = audioPaths.map((_, i) => `[${i}:a]`).join('') + `amix=inputs=${audioPaths.length}:duration=longest`;
882
+ const command = `ffmpeg ${inputs} -filter_complex "${mixFilter}" "${outputPath}"`;
883
+
884
+ // 执行命令
885
+ await this.Tools.executeCommand(command);
886
+
887
+ return {
888
+ success: true,
889
+ message: `${description.split(':')[1] || '操作'}完成: ${outputPath}`,
890
+ command,
891
+ outputPath
892
+ };
893
+ } catch (error) {
894
+ throw new Error(`${name.replace('ffmpeg_', '')}失败: ${error.message}`);
895
+ }
896
+ };
897
+
898
+ /**
899
+ * 压缩视频(调整CRF)
900
+ *
901
+ * @param {Object} params.videoPath 输入视频文件路径
902
+ * @param {Object} params.outputPath 输出视频文件路径
903
+ * @param {Object} params.crf CRF值(0-51,越小质量越高,默认为23)(可选)
904
+ * @param {Object} params.preset 编码预设(ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow,默认为medium)(可选)
905
+ * @returns {Promise<Object>} 处理结果
906
+ */
907
+ functions.ffmpeg_compressVideo = async (params) => {
908
+ try {
909
+ // 检查ffmpeg安装
910
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
911
+ if (!checkResult.installed || !checkResult.versionOk) {
912
+ throw new Error(`FFmpeg未安装或版本不符合要求: ${checkResult.message}`);
913
+ }
914
+
915
+ // 参数解构和验证
916
+ const { videoPath, outputPath, crf, preset } = params;
917
+ if (!videoPath) throw new Error('缺少必要参数: videoPath');
918
+ if (!outputPath) throw new Error('缺少必要参数: outputPath');
919
+
920
+ // 构建视频压缩命令
921
+ const command = `ffmpeg -i "${videoPath}" -c:v libx264 -crf ${crf || 23} -preset ${preset || 'medium'} -c:a copy "${outputPath}"`;
922
+
923
+ // 执行命令
924
+ await this.Tools.executeCommand(command);
925
+
926
+ return {
927
+ success: true,
928
+ message: `${description.split(':')[1] || '操作'}完成: ${outputPath}`,
929
+ command,
930
+ outputPath
931
+ };
932
+ } catch (error) {
933
+ throw new Error(`${name.replace('ffmpeg_', '')}失败: ${error.message}`);
934
+ }
935
+ };
936
+
937
+ /**
938
+ * 添加文本叠加到视频
939
+ *
940
+ * @param {Object} params.videoPath 输入视频文件路径
941
+ * @param {Object} params.outputPath 输出视频文件路径
942
+ * @param {Object} params.text 要叠加的文本
943
+ * @param {Object} params.x 文本X坐标(像素)(可选)
944
+ * @param {Object} params.y 文本Y坐标(像素)(可选)
945
+ * @param {Object} params.fontSize 字体大小(默认为24)(可选)
946
+ * @param {Object} params.fontColor 字体颜色(默认为white)(可选)
947
+ * @param {Object} params.startTime 开始显示时间(格式:HH:MM:SS)(可选)
948
+ * @param {Object} params.duration 显示持续时间(格式:HH:MM:SS)(可选)
949
+ * @returns {Promise<Object>} 处理结果
950
+ */
951
+ functions.ffmpeg_addTextOverlay = async (params) => {
952
+ try {
953
+ // 检查ffmpeg安装
954
+ const checkResult = await functions.ffmpeg_checkFfmpegInstallation({ minVersion: '7.0.0' });
955
+ if (!checkResult.installed || !checkResult.versionOk) {
956
+ throw new Error(`FFmpeg未安装或版本不符合要求: ${checkResult.message}`);
957
+ }
958
+
959
+ // 参数解构和验证
960
+ const { videoPath, outputPath, text, x, y, fontSize, fontColor, startTime, duration } = params;
961
+ if (!videoPath) throw new Error('缺少必要参数: videoPath');
962
+ if (!outputPath) throw new Error('缺少必要参数: outputPath');
963
+ if (!text) throw new Error('缺少必要参数: text');
964
+
965
+ // 构建添加文本叠加命令
966
+ let filter = `drawtext=text='${text}':x=${x || 10}:y=${y || 10}:fontsize=${fontSize || 24}:fontcolor=${fontColor || 'white'}`;
967
+ if (startTime) filter += `:enable='between(t,${startTime},${duration ? `${startTime}+${duration}` : '9999'})'`;
968
+ const command = `ffmpeg -i "${videoPath}" -vf "${filter}" "${outputPath}"`;
969
+
970
+ // 执行命令
971
+ await this.Tools.executeCommand(command);
972
+
973
+ return {
974
+ success: true,
975
+ message: `${description.split(':')[1] || '操作'}完成: ${outputPath}`,
976
+ command,
977
+ outputPath
978
+ };
979
+ } catch (error) {
980
+ throw new Error(`${name.replace('ffmpeg_', '')}失败: ${error.message}`);
981
+ }
982
+ };
983
+
984
+
985
+
986
+ // ============ 模块导出 ============
987
+ module.exports = functions