@xiping/node-utils 1.0.60 → 1.0.62

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.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @xiping/node-utils
2
+
3
+ Node.js 通用工具库,提供目录树、路径、SRT→VTT 字幕转换、FFmpeg 视频处理、文件信息与图片处理等能力。
4
+
5
+ ```bash
6
+ npm install @xiping/node-utils
7
+ ```
8
+
9
+ ## 使用案例
10
+
11
+ ```typescript
12
+ import {
13
+ buildDirectoryTree,
14
+ getParentDirectory,
15
+ convertSrtFileToVttFile,
16
+ getVideoInfo,
17
+ getThumbnail,
18
+ getFileConfig,
19
+ convertImage,
20
+ checkFFmpegAvailability,
21
+ } from '@xiping/node-utils';
22
+
23
+ // 目录树
24
+ const tree = buildDirectoryTree('/path/to/dir');
25
+
26
+ // 路径
27
+ const parent = getParentDirectory('/Users/documents/folder');
28
+
29
+ // SRT 转 VTT
30
+ convertSrtFileToVttFile('/path/to/subtitle.srt');
31
+
32
+ // 视频信息与缩略图(需系统安装 ffmpeg)
33
+ if (checkFFmpegAvailability()) {
34
+ const info = getVideoInfo('/path/to/video.mp4');
35
+ const thumb = await getThumbnail('/path/to/video.mp4', { frames: 30 });
36
+ }
37
+
38
+ // 文件配置(含视频元数据)
39
+ const config = getFileConfig('/path/to/file.mp4');
40
+
41
+ // 图片格式转换
42
+ await convertImage(inputPath, outputPath, 'webp', { quality: 80 });
43
+ ```
44
+
45
+ 更多说明见 [src/ffmpeg/README.md](./src/ffmpeg/README.md)、[src/image/README.md](./src/image/README.md)、[src/srt-to-vtt/README.md](./src/srt-to-vtt/README.md)。
@@ -0,0 +1,370 @@
1
+ # FFmpeg 工具模块
2
+
3
+ 基于 FFmpeg/FFprobe 的 Node.js 视频处理工具集,提供视频信息获取、缩略图生成、视频截取、音频提取及运行环境检查等功能。
4
+
5
+ 本模块由 `@xiping/node-utils` 包统一导出,使用时从包根路径导入即可:`import { ... } from '@xiping/node-utils'`。
6
+
7
+ ## 功能概览
8
+
9
+ | 功能 | 说明 | 依赖 |
10
+ |------------|--------------------------|-------------|
11
+ | 视频信息 | 时长、分辨率、编码、比特率等 | ffprobe |
12
+ | 缩略图生成 | 多帧合成预览图(AVIF/WebP/JPEG/PNG) | ffmpeg + sharp |
13
+ | 视频截取 | 按时间范围截取,流复制不重编码 | ffmpeg |
14
+ | 提取音频 | 从视频中提取音轨为 mp3/m4a/wav | ffmpeg |
15
+ | 环境检查 | 检测 ffmpeg/ffprobe 是否可用 | - |
16
+
17
+ ## 安装要求
18
+
19
+ ### 依赖包
20
+
21
+ - Node.js(使用内置 `child_process.spawnSync` 调用 ffmpeg/ffprobe,兼容 Electron、跨平台)
22
+ - [sharp](https://www.npmjs.com/package/sharp)(仅缩略图功能需要)
23
+
24
+ ### 系统要求
25
+
26
+ - **ffprobe**:视频信息相关接口需要(通常随 ffmpeg 一起安装)
27
+ - **ffmpeg**:缩略图、视频截取需要
28
+
29
+ ### 安装 FFmpeg / FFprobe
30
+
31
+ **macOS:**
32
+
33
+ ```bash
34
+ brew install ffmpeg
35
+ ```
36
+
37
+ **Ubuntu/Debian:**
38
+
39
+ ```bash
40
+ sudo apt update
41
+ sudo apt install ffmpeg
42
+ ```
43
+
44
+ **CentOS/RHEL:**
45
+
46
+ ```bash
47
+ sudo yum install ffmpeg
48
+ ```
49
+
50
+ **Windows:**
51
+
52
+ 从 [FFmpeg 官网](https://ffmpeg.org/download.html) 下载并将可执行文件加入 PATH,或使用 Chocolatey:
53
+
54
+ ```bash
55
+ choco install ffmpeg
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 1. 视频信息(getVideoInfo)
61
+
62
+ 使用 ffprobe 获取视频元数据:时长、分辨率、编码、比特率、帧率、文件大小等。
63
+
64
+ ### 基本使用
65
+
66
+ ```typescript
67
+ import { getVideoInfo } from '@xiping/node-utils';
68
+
69
+ const videoInfo = getVideoInfo('/path/to/video.mp4');
70
+ console.log(videoInfo.durationFormatted, videoInfo.width, videoInfo.height);
71
+ ```
72
+
73
+ ### 检查 ffprobe 是否可用
74
+
75
+ ```typescript
76
+ import { isFfprobeAvailable } from '@xiping/node-utils';
77
+
78
+ if (isFfprobeAvailable()) {
79
+ console.log('ffprobe 可用');
80
+ }
81
+ ```
82
+
83
+ ### 批量获取
84
+
85
+ ```typescript
86
+ import { getMultipleVideoInfo } from '@xiping/node-utils';
87
+
88
+ const infos = getMultipleVideoInfo(['/path/video1.mp4', '/path/video2.mp4']);
89
+ ```
90
+
91
+ ### 获取详细元数据(含章节等)
92
+
93
+ ```typescript
94
+ import { getDetailedVideoInfo } from '@xiping/node-utils';
95
+
96
+ const detailed = getDetailedVideoInfo('/path/to/video.mp4');
97
+ ```
98
+
99
+ ### VideoInfo 结构
100
+
101
+ ```typescript
102
+ interface VideoInfo {
103
+ path: string;
104
+ duration: number; // 秒
105
+ durationFormatted: string;
106
+ width: number;
107
+ height: number;
108
+ videoCodec: string;
109
+ audioCodec: string;
110
+ bitrate: number;
111
+ fps: number;
112
+ fileSize: number; // 字节
113
+ fileSizeFormatted: string;
114
+ rawInfo: string; // 原始 ffprobe JSON
115
+ }
116
+ ```
117
+
118
+ ---
119
+
120
+ ## 2. 缩略图生成(getThumbnail)
121
+
122
+ 从视频中等间隔提取多帧,用 sharp 合成为一张缩略图,支持 AVIF/WebP/JPEG/PNG。
123
+
124
+ ### 基本使用
125
+
126
+ ```typescript
127
+ import { getThumbnail } from '@xiping/node-utils';
128
+
129
+ const result = await getThumbnail('/path/to/video.mp4', {
130
+ frames: 60, // 提取帧数(默认 60)
131
+ columns: 4, // 每行列数(默认 4)
132
+ outputWidth: 3840, // 输出宽度(默认 3840)
133
+ format: 'avif', // 'avif' | 'webp' | 'jpeg' | 'png'
134
+ quality: 80, // 1–100
135
+ outputFileName: 'thumbnail.avif',
136
+ });
137
+
138
+ console.log(result.outputPath);
139
+ console.log(result.metadata);
140
+ ```
141
+
142
+ ### 进度回调
143
+
144
+ ```typescript
145
+ const result = await getThumbnail('/path/to/video.mp4', {
146
+ frames: 30,
147
+ onProgress(progress) {
148
+ console.log(progress.phase, progress.percent, progress.message);
149
+ // phase: 'analyzing' | 'extracting' | 'composing' | 'encoding' | 'done'
150
+ },
151
+ });
152
+ ```
153
+
154
+ ### 选项说明
155
+
156
+ | 选项 | 类型 | 默认值 | 说明 |
157
+ |-----------------|----------|----------------|----------------|
158
+ | frames | number | 60 | 提取的帧数 |
159
+ | columns | number | 4 | 缩略图列数 |
160
+ | outputWidth | number | 3840 | 输出图宽度 |
161
+ | outputFileName | string | thumbnail.avif | 输出文件名 |
162
+ | format | string | 'avif' | avif/webp/jpeg/png |
163
+ | quality | number | 80 | 1–100 |
164
+ | batchSize | number | 10 | 合成时每批帧数 |
165
+ | maxConcurrency | number | 4 | 提取帧并发数 |
166
+ | tempDir | string | - | 自定义临时目录 |
167
+ | onProgress | function | - | 进度回调 |
168
+
169
+ ### 返回值
170
+
171
+ ```typescript
172
+ {
173
+ buffer: Buffer;
174
+ outputPath: string;
175
+ metadata: {
176
+ frames: number;
177
+ duration: number;
178
+ outputSize: { width: number; height: number };
179
+ };
180
+ }
181
+ ```
182
+
183
+ **注意**:输入路径需为**绝对路径**;使用前请确保已安装 ffmpeg(可用下方「环境检查」接口检测)。
184
+
185
+ ---
186
+
187
+ ## 3. 视频截取(cutVideo)
188
+
189
+ 按开始时间、时长或结束时间截取片段,使用流复制(`-c copy`),不重新编码。
190
+
191
+ ### 基本使用
192
+
193
+ ```typescript
194
+ import { cutVideo, cutVideoByTimeRange, cutVideoByDuration, cutVideoFromStart } from '@xiping/node-utils';
195
+
196
+ // 从 30 秒开始截取 60 秒
197
+ const result = await cutVideo('/path/to/video.mp4', {
198
+ startTime: '00:00:30',
199
+ duration: '00:01:00',
200
+ outputFormat: 'mp4',
201
+ overwrite: true,
202
+ });
203
+
204
+ // 按时间范围:1:30 到 3:45
205
+ const r2 = await cutVideoByTimeRange('/path/to/video.mp4', '00:01:30', '00:03:45');
206
+
207
+ // 从 2 分钟开始截取 90 秒
208
+ const r3 = await cutVideoByDuration('/path/to/video.mp4', '00:02:00', '00:01:30');
209
+
210
+ // 只取前 30 秒
211
+ const r4 = await cutVideoFromStart('/path/to/video.mp4', 30);
212
+ ```
213
+
214
+ ### CutVideoOptions
215
+
216
+ ```typescript
217
+ {
218
+ startTime?: string; // 开始时间,如 '00:00:30' 或秒数
219
+ duration?: string; // 持续时间
220
+ endTime?: string; // 结束时间(与 duration 二选一)
221
+ outputFileName?: string;
222
+ outputFormat?: string; // 默认 'mp4'
223
+ tempDir?: string;
224
+ overwrite?: boolean; // 是否覆盖已存在文件
225
+ }
226
+ ```
227
+
228
+ ### CutVideoResult
229
+
230
+ ```typescript
231
+ {
232
+ outputPath: string;
233
+ metadata: {
234
+ originalDuration: number;
235
+ cutDuration: number;
236
+ startTime: string;
237
+ endTime: string;
238
+ fileSize: number;
239
+ processingTime: number;
240
+ };
241
+ }
242
+ ```
243
+
244
+ 时间格式支持:`HH:MM:SS`、`MM:SS` 或纯秒数。输入路径需为**绝对路径**。更多说明见 [README_cutVideo.md](./README_cutVideo.md)。
245
+
246
+ ---
247
+
248
+ ## 4. 提取音频(extractAudio)
249
+
250
+ 从视频文件中仅提取音频轨,输出为独立音频文件(mp3、m4a、wav)。
251
+
252
+ ### 基本使用
253
+
254
+ ```typescript
255
+ import { extractAudio } from '@xiping/node-utils';
256
+
257
+ const result = await extractAudio('/path/to/video.mp4', {
258
+ outputFormat: 'mp3', // 'mp3' | 'm4a' | 'wav',默认 'mp3'
259
+ outputFileName: 'audio.mp3',
260
+ overwrite: true,
261
+ });
262
+
263
+ console.log(result.outputPath);
264
+ console.log(result.metadata.duration, result.metadata.fileSize);
265
+ ```
266
+
267
+ ### 进度回调
268
+
269
+ ```typescript
270
+ const result = await extractAudio('/path/to/video.mp4', {
271
+ outputFormat: 'mp3',
272
+ onProgress(progress) {
273
+ console.log(progress.phase, progress.percent, progress.message);
274
+ // phase: 'preparing' | 'encoding' | 'done'
275
+ if (progress.currentTime != null && progress.duration != null) {
276
+ console.log(`已处理 ${progress.currentTime}/${progress.duration} 秒`);
277
+ }
278
+ },
279
+ });
280
+ ```
281
+
282
+ ### 选项说明
283
+
284
+ | 选项 | 类型 | 默认值 | 说明 |
285
+ |-----------------|----------|----------|------------------------------|
286
+ | outputFileName | string | 源文件名.格式 | 输出文件名 |
287
+ | outputFormat | string | 'mp3' | mp3 / m4a / wav |
288
+ | overwrite | boolean | false | 是否覆盖已存在文件 |
289
+ | tempDir | string | - | 自定义临时目录 |
290
+ | copyStream | boolean | true | 尽量流复制不重编码(格式兼容时) |
291
+ | onProgress | function | - | 进度回调 |
292
+
293
+ ### 返回值
294
+
295
+ ```typescript
296
+ {
297
+ outputPath: string;
298
+ metadata: {
299
+ duration: number; // 音频时长(秒)
300
+ fileSize: number; // 输出文件大小(字节)
301
+ processingTime: number; // 处理耗时(毫秒)
302
+ };
303
+ }
304
+ ```
305
+
306
+ 输入路径需为**绝对路径**;若视频无音轨将抛错。使用前请确保已安装 ffmpeg。
307
+
308
+ ---
309
+
310
+ ## 5. 环境检查(check)
311
+
312
+ ```typescript
313
+ import { checkFFmpegAvailability, isFfprobeAvailable } from '@xiping/node-utils';
314
+
315
+ if (checkFFmpegAvailability()) {
316
+ console.log('ffmpeg 可用');
317
+ }
318
+ if (isFfprobeAvailable()) {
319
+ console.log('ffprobe 可用');
320
+ }
321
+ ```
322
+
323
+ - **checkFFmpegAvailability()**:用于缩略图、视频截取、提取音频前检查。
324
+ - **isFfprobeAvailable()**:用于视频信息接口前检查。
325
+
326
+ ---
327
+
328
+ ## 错误处理
329
+
330
+ 各函数在以下情况会抛出错误,建议用 try/catch 包裹:
331
+
332
+ - 文件不存在或路径无效
333
+ - 未安装 ffmpeg/ffprobe 或不可用
334
+ - 格式/参数不支持(如时间超出视频时长、视频无音轨)
335
+ - 权限或磁盘空间不足
336
+
337
+ ```typescript
338
+ try {
339
+ const info = getVideoInfo('/path/to/video.mp4');
340
+ } catch (err) {
341
+ console.error(err.message);
342
+ }
343
+ ```
344
+
345
+ ---
346
+
347
+ ## 支持格式
348
+
349
+ ffprobe/ffmpeg 支持常见容器与编码,例如:
350
+
351
+ - 容器:MP4, AVI, MOV, MKV, FLV, WMV, WebM, OGV, 3GP 等
352
+ - 具体支持以系统安装的 FFmpeg 版本为准
353
+
354
+ ---
355
+
356
+ ## API 速览
357
+
358
+ | 接口 | 说明 |
359
+ |--------------------------|------|
360
+ | `getVideoInfo(path)` | 获取视频基本信息 |
361
+ | `getMultipleVideoInfo(paths)` | 批量获取视频信息 |
362
+ | `getDetailedVideoInfo(path)` | 获取详细元数据(含章节等) |
363
+ | `getThumbnail(path, options)` | 生成多帧缩略图(异步) |
364
+ | `cutVideo(path, options)` | 按选项截取视频(异步) |
365
+ | `cutVideoByTimeRange(path, start, end, options)` | 按起止时间截取 |
366
+ | `cutVideoByDuration(path, start, duration, options)` | 按起始+时长截取 |
367
+ | `cutVideoFromStart(path, durationSeconds, options)` | 从开头截取 N 秒 |
368
+ | `extractAudio(path, options)` | 从视频提取音频(异步) |
369
+ | `isFfprobeAvailable()` | 检测 ffprobe 是否可用 |
370
+ | `checkFFmpegAvailability()` | 检测 ffmpeg 是否可用 |
@@ -0,0 +1,220 @@
1
+ # Video Cutter (视频截取器)
2
+
3
+ 一个基于 FFmpeg 的 Node.js 视频截取工具,支持精确的时间控制和高性能的视频处理。
4
+
5
+ ## 功能特性
6
+
7
+ - 🎯 **精确时间控制** - 支持多种时间格式 (HH:MM:SS, 秒数)
8
+ - ⚡ **高性能处理** - 使用 FFmpeg 的 copy 模式,避免重新编码
9
+ - 🛡️ **输入验证** - 完整的参数验证和错误处理
10
+ - 📁 **灵活输出** - 支持自定义输出格式和文件名
11
+ - 🔄 **临时文件管理** - 自动清理临时文件
12
+ - 📊 **详细元数据** - 返回处理结果和文件信息
13
+
14
+ ## 安装依赖
15
+
16
+ 确保系统已安装 FFmpeg:
17
+
18
+ ```bash
19
+ # macOS
20
+ brew install ffmpeg
21
+
22
+ # Ubuntu/Debian
23
+ sudo apt update
24
+ sudo apt install ffmpeg
25
+
26
+ # Windows
27
+ # 下载并安装 FFmpeg,或使用 chocolatey: choco install ffmpeg
28
+ ```
29
+
30
+ ## API 参考
31
+
32
+ ### 主要函数
33
+
34
+ #### `cutVideo(videoPath, options)`
35
+
36
+ 主要的视频截取函数。
37
+
38
+ **参数:**
39
+ - `videoPath` (string): 输入视频文件的绝对路径
40
+ - `options` (CutVideoOptions): 截取选项
41
+
42
+ **返回值:**
43
+ - `Promise<CutVideoResult>`: 包含输出路径和元数据的对象
44
+
45
+ #### `cutVideoByTimeRange(videoPath, startTime, endTime, options)`
46
+
47
+ 按时间范围截取视频的便捷函数。
48
+
49
+ #### `cutVideoByDuration(videoPath, startTime, duration, options)`
50
+
51
+ 按持续时间截取视频的便捷函数。
52
+
53
+ #### `cutVideoFromStart(videoPath, durationSeconds, options)`
54
+
55
+ 从视频开头截取指定秒数的便捷函数。
56
+
57
+ ### 类型定义
58
+
59
+ #### CutVideoOptions
60
+
61
+ ```typescript
62
+ interface CutVideoOptions {
63
+ startTime?: string; // 开始时间 (格式: HH:MM:SS 或秒数)
64
+ duration?: string; // 持续时间 (格式: HH:MM:SS 或秒数)
65
+ endTime?: string; // 结束时间 (格式: HH:MM:SS 或秒数)
66
+ outputFileName?: string; // 输出文件名
67
+ outputFormat?: string; // 输出格式 (mp4, avi, mov, etc.)
68
+ tempDir?: string; // 临时目录
69
+ overwrite?: boolean; // 是否覆盖已存在的文件
70
+ }
71
+ ```
72
+
73
+ #### CutVideoResult
74
+
75
+ ```typescript
76
+ interface CutVideoResult {
77
+ outputPath: string;
78
+ metadata: {
79
+ originalDuration: number; // 原视频时长(秒)
80
+ cutDuration: number; // 截取时长(秒)
81
+ startTime: string; // 开始时间
82
+ endTime: string; // 结束时间
83
+ fileSize: number; // 输出文件大小(字节)
84
+ processingTime: number; // 处理时间(毫秒)
85
+ };
86
+ }
87
+ ```
88
+
89
+ ## 使用示例
90
+
91
+ ### 基本用法
92
+
93
+ ```typescript
94
+ import { cutVideo } from './cutVideo.js';
95
+
96
+ // 从第30秒开始截取60秒
97
+ const result = await cutVideo('/path/to/video.mp4', {
98
+ startTime: '00:00:30',
99
+ duration: '00:01:00'
100
+ });
101
+
102
+ console.log('输出文件:', result.outputPath);
103
+ console.log('文件大小:', result.metadata.fileSize);
104
+ ```
105
+
106
+ ### 按时间范围截取
107
+
108
+ ```typescript
109
+ import { cutVideoByTimeRange } from './cutVideo.js';
110
+
111
+ // 截取 1:30 到 3:45 之间的片段
112
+ const result = await cutVideoByTimeRange(
113
+ '/path/to/video.mp4',
114
+ '00:01:30',
115
+ '00:03:45'
116
+ );
117
+ ```
118
+
119
+ ### 按持续时间截取
120
+
121
+ ```typescript
122
+ import { cutVideoByDuration } from './cutVideo.js';
123
+
124
+ // 从第2分钟开始截取90秒
125
+ const result = await cutVideoByDuration(
126
+ '/path/to/video.mp4',
127
+ '00:02:00',
128
+ '00:01:30'
129
+ );
130
+ ```
131
+
132
+ ### 从头开始截取
133
+
134
+ ```typescript
135
+ import { cutVideoFromStart } from './cutVideo.js';
136
+
137
+ // 截取视频前30秒
138
+ const result = await cutVideoFromStart(
139
+ '/path/to/video.mp4',
140
+ 30
141
+ );
142
+ ```
143
+
144
+ ### 自定义输出选项
145
+
146
+ ```typescript
147
+ const result = await cutVideo('/path/to/video.mp4', {
148
+ startTime: '00:01:00',
149
+ duration: '00:02:00',
150
+ outputFileName: 'my_cut_video.mp4',
151
+ outputFormat: 'mp4',
152
+ overwrite: true
153
+ });
154
+ ```
155
+
156
+ ### 使用自定义临时目录
157
+
158
+ ```typescript
159
+ const result = await cutVideo('/path/to/video.mp4', {
160
+ startTime: '00:00:30',
161
+ duration: '00:01:00',
162
+ tempDir: '/custom/temp/directory'
163
+ });
164
+ ```
165
+
166
+ ## 时间格式支持
167
+
168
+ 支持多种时间格式:
169
+
170
+ - `HH:MM:SS` - 标准时间格式 (如: `01:30:45`)
171
+ - `MM:SS` - 分钟:秒格式 (如: `30:45`)
172
+ - 秒数 - 纯数字格式 (如: `90.5`)
173
+
174
+ ## 错误处理
175
+
176
+ 函数会抛出以下类型的错误:
177
+
178
+ - **文件不存在**: 输入视频文件不存在
179
+ - **FFmpeg 未安装**: 系统未安装 FFmpeg
180
+ - **时间参数无效**: 开始时间大于等于视频时长
181
+ - **输出文件已存在**: 输出文件存在且未设置覆盖选项
182
+ - **FFmpeg 执行失败**: 视频处理过程中出现错误
183
+
184
+ ```typescript
185
+ try {
186
+ const result = await cutVideo('/path/to/video.mp4', {
187
+ startTime: '00:00:30',
188
+ duration: '00:01:00'
189
+ });
190
+ } catch (error) {
191
+ console.error('视频截取失败:', error.message);
192
+ }
193
+ ```
194
+
195
+ ## 性能优化
196
+
197
+ - 使用 FFmpeg 的 `-c copy` 参数避免重新编码
198
+ - 自动管理临时文件,处理完成后自动清理
199
+ - 支持自定义临时目录以优化 I/O 性能
200
+
201
+ ## 注意事项
202
+
203
+ 1. **路径要求**: 输入路径必须是绝对路径
204
+ 2. **文件权限**: 确保对输入和输出目录有读写权限
205
+ 3. **磁盘空间**: 确保有足够的磁盘空间存储输出文件
206
+ 4. **FFmpeg 依赖**: 必须安装 FFmpeg 才能使用此模块
207
+
208
+ ## 日志输出
209
+
210
+ 模块会输出详细的处理日志,包括:
211
+ - 参数验证结果
212
+ - FFmpeg 命令执行
213
+ - 处理进度和结果
214
+ - 错误信息
215
+
216
+ 日志格式:`[VideoCutter] 消息内容`
217
+
218
+ ## 许可证
219
+
220
+ MIT License
@@ -1,7 +1,7 @@
1
- import shell from "shelljs";
1
+ import { runSync } from "./runSync.js";
2
2
  // 检查 ffmpeg 可用性
3
3
  export function checkFFmpegAvailability() {
4
- const result = shell.exec('ffmpeg -version', { silent: true });
4
+ const result = runSync("ffmpeg", ["-version"]);
5
5
  return result.code === 0;
6
6
  }
7
7
  /**
@@ -9,6 +9,6 @@ export function checkFFmpegAvailability() {
9
9
  * @returns 是否可用
10
10
  */
11
11
  export function isFfprobeAvailable() {
12
- const result = shell.exec("ffprobe -version", { silent: true });
12
+ const result = runSync("ffprobe", ["-version"]);
13
13
  return result.code === 0;
14
14
  }