@xiping/node-utils 1.0.53 → 1.0.61

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,186 @@
1
+ import shell from "shelljs";
2
+ import { spawn } from "node:child_process";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import * as os from "node:os";
6
+ import { checkFFmpegAvailability } from "./check.js";
7
+ import { getVideoInfo } from "./getVideoInfo.js";
8
+ const log = (message, data) => {
9
+ console.log(`[ExtractAudio] ${message}`, data ? JSON.stringify(data, null, 2) : "");
10
+ };
11
+ function validateAndSanitizePath(inputPath) {
12
+ if (!inputPath || typeof inputPath !== "string") {
13
+ throw new Error("Invalid path provided");
14
+ }
15
+ if (!path.isAbsolute(inputPath)) {
16
+ throw new Error("Path must be absolute");
17
+ }
18
+ return inputPath;
19
+ }
20
+ function getDurationInSeconds(videoPath) {
21
+ const result = shell.exec(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${videoPath}"`, { silent: true });
22
+ if (result.code !== 0) {
23
+ throw new Error(`FFprobe failed to get duration: ${result.stderr}`);
24
+ }
25
+ const duration = parseFloat(result.stdout);
26
+ if (isNaN(duration) || duration <= 0) {
27
+ throw new Error("Invalid video duration");
28
+ }
29
+ return duration;
30
+ }
31
+ /** 解析 ffmpeg stderr 中的 time=HH:MM:SS.xx 为秒数 */
32
+ function parseTimeFromStderr(line) {
33
+ const match = line.match(/time=(\d{2}):(\d{2}):(\d{2}\.\d+)/);
34
+ if (!match)
35
+ return null;
36
+ const [, h, m, s] = match;
37
+ return parseInt(h, 10) * 3600 + parseInt(m, 10) * 60 + parseFloat(s);
38
+ }
39
+ /** 根据格式和是否流复制,构建 ffmpeg 参数(不含输入输出路径) */
40
+ function buildFfmpegArgs(outputPath, outputFormat, copyStream, audioCodec) {
41
+ const args = ["-i", ""]; // input 占位,调用方填入
42
+ args.push("-vn"); // 不要视频
43
+ if (outputFormat === "mp3") {
44
+ args.push("-acodec", "libmp3lame");
45
+ }
46
+ else if (outputFormat === "m4a") {
47
+ if (copyStream && (audioCodec === "aac" || audioCodec === "aac_latm")) {
48
+ args.push("-acodec", "copy");
49
+ }
50
+ else {
51
+ args.push("-acodec", "aac");
52
+ }
53
+ }
54
+ else if (outputFormat === "wav") {
55
+ args.push("-acodec", "pcm_s16le");
56
+ }
57
+ args.push("-y", outputPath);
58
+ return args;
59
+ }
60
+ /** 执行 ffmpeg(shell.exec),无进度 */
61
+ function runFfmpegSync(inputPath, outputPath, outputFormat, copyStream, audioCodec) {
62
+ const args = buildFfmpegArgs(outputPath, outputFormat, copyStream, audioCodec);
63
+ args[1] = inputPath;
64
+ const command = `ffmpeg ${args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}`;
65
+ log("Executing FFmpeg command", { command });
66
+ const result = shell.exec(command, { silent: true });
67
+ if (result.code !== 0) {
68
+ throw new Error(`FFmpeg failed: ${result.stderr}`);
69
+ }
70
+ }
71
+ /** 执行 ffmpeg(spawn),解析 stderr 并回调 onProgress */
72
+ function runFfmpegWithProgress(inputPath, outputPath, outputFormat, copyStream, audioCodec, totalDuration, onProgress) {
73
+ return new Promise((resolve, reject) => {
74
+ const args = buildFfmpegArgs(outputPath, outputFormat, copyStream, audioCodec);
75
+ args[1] = inputPath;
76
+ const child = spawn("ffmpeg", args, { stdio: ["ignore", "pipe", "pipe"] });
77
+ let lastPercent = -1;
78
+ const report = (progress) => {
79
+ if (progress.percent !== lastPercent) {
80
+ lastPercent = progress.percent;
81
+ onProgress(progress);
82
+ }
83
+ };
84
+ if (child.stderr) {
85
+ let buffer = "";
86
+ child.stderr.setEncoding("utf8");
87
+ child.stderr.on("data", (chunk) => {
88
+ buffer += chunk;
89
+ const lines = buffer.split(/\r?\n/);
90
+ buffer = lines.pop() ?? "";
91
+ for (const line of lines) {
92
+ const currentTime = parseTimeFromStderr(line);
93
+ if (currentTime != null && totalDuration > 0) {
94
+ const percent = Math.min(99, Math.round((currentTime / totalDuration) * 100));
95
+ report({
96
+ phase: "encoding",
97
+ percent,
98
+ message: "正在提取音频...",
99
+ currentTime,
100
+ duration: totalDuration,
101
+ });
102
+ }
103
+ }
104
+ });
105
+ }
106
+ child.on("error", (err) => reject(err));
107
+ child.on("close", (code, signal) => {
108
+ if (signal) {
109
+ reject(new Error(`FFmpeg killed by signal: ${signal}`));
110
+ return;
111
+ }
112
+ if (code !== 0) {
113
+ reject(new Error(`FFmpeg exited with code ${code}`));
114
+ return;
115
+ }
116
+ report({
117
+ phase: "done",
118
+ percent: 100,
119
+ message: "完成",
120
+ currentTime: totalDuration,
121
+ duration: totalDuration,
122
+ });
123
+ resolve();
124
+ });
125
+ });
126
+ }
127
+ export async function extractAudio(videoPath, options = {}) {
128
+ const startTime = Date.now();
129
+ const validatedPath = validateAndSanitizePath(videoPath);
130
+ if (!fs.existsSync(validatedPath)) {
131
+ throw new Error(`Video file not found: ${validatedPath}`);
132
+ }
133
+ if (!checkFFmpegAvailability()) {
134
+ throw new Error("FFmpeg is not available. Please install FFmpeg first.");
135
+ }
136
+ const { outputFileName, outputFormat = "mp3", overwrite = false, tempDir: customTempDir, copyStream = true, onProgress, } = options;
137
+ const videoInfo = getVideoInfo(validatedPath);
138
+ if (!videoInfo.audioCodec) {
139
+ throw new Error("Video has no audio stream.");
140
+ }
141
+ const duration = getDurationInSeconds(validatedPath);
142
+ const videoDir = path.dirname(validatedPath);
143
+ const baseName = path.basename(validatedPath, path.extname(validatedPath));
144
+ const defaultOutputName = `${baseName}.${outputFormat}`;
145
+ const outputFileNameFinal = outputFileName ?? defaultOutputName;
146
+ const outputPath = path.join(videoDir, outputFileNameFinal);
147
+ if (fs.existsSync(outputPath) && !overwrite) {
148
+ throw new Error(`Output file already exists: ${outputPath}. Use overwrite option to force overwrite.`);
149
+ }
150
+ const reportProgress = (progress) => {
151
+ onProgress?.(progress);
152
+ };
153
+ reportProgress({
154
+ phase: "preparing",
155
+ percent: 0,
156
+ message: "准备提取音频...",
157
+ duration,
158
+ });
159
+ const tempDir = customTempDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "extract-audio-"));
160
+ const tempOutputPath = path.join(tempDir, `temp_audio.${outputFormat}`);
161
+ try {
162
+ if (onProgress) {
163
+ await runFfmpegWithProgress(validatedPath, tempOutputPath, outputFormat, copyStream, videoInfo.audioCodec, duration, reportProgress);
164
+ }
165
+ else {
166
+ runFfmpegSync(validatedPath, tempOutputPath, outputFormat, copyStream, videoInfo.audioCodec);
167
+ }
168
+ fs.copyFileSync(tempOutputPath, outputPath);
169
+ log("Extract audio complete", { outputPath });
170
+ }
171
+ finally {
172
+ if (!customTempDir && fs.existsSync(tempDir)) {
173
+ fs.rmSync(tempDir, { recursive: true, force: true });
174
+ }
175
+ }
176
+ const outputStats = fs.statSync(outputPath);
177
+ const processingTime = Date.now() - startTime;
178
+ return {
179
+ outputPath,
180
+ metadata: {
181
+ duration,
182
+ fileSize: outputStats.size,
183
+ processingTime,
184
+ },
185
+ };
186
+ }
@@ -2,6 +2,19 @@
2
2
  * 视频缩略图生成器
3
3
  * 使用 ffmpeg 提取视频帧,使用 sharp 合成缩略图
4
4
  */
5
+ /** 进度信息,用于 onProgress 回调 */
6
+ export interface ThumbnailProgress {
7
+ /** 当前阶段 */
8
+ phase: "analyzing" | "extracting" | "composing" | "encoding" | "done";
9
+ /** 总进度 0–100 */
10
+ percent: number;
11
+ /** 当前阶段已完成数(如已提取帧数) */
12
+ current?: number;
13
+ /** 当前阶段总数 */
14
+ total?: number;
15
+ /** 可读描述 */
16
+ message?: string;
17
+ }
5
18
  interface ThumbnailOptions {
6
19
  frames?: number;
7
20
  outputWidth?: number;
@@ -12,6 +25,8 @@ interface ThumbnailOptions {
12
25
  batchSize?: number;
13
26
  maxConcurrency?: number;
14
27
  tempDir?: string;
28
+ /** 进度回调,按阶段与完成比例调用 */
29
+ onProgress?: (progress: ThumbnailProgress) => void;
15
30
  }
16
31
  interface ProcessingResult {
17
32
  buffer: Buffer;
@@ -33,15 +33,16 @@ function getDurationInSeconds(videoPath) {
33
33
  return duration;
34
34
  }
35
35
  // 并行提取视频帧
36
- async function extractFrames(videoPath, tempDir, frames, interval, maxConcurrency = 4) {
36
+ async function extractFrames(videoPath, tempDir, frames, interval, maxConcurrency = 4, onProgress) {
37
+ const total = frames - 1;
37
38
  log('Starting frame extraction', { frames, interval, maxConcurrency });
38
- const framePromises = Array.from({ length: frames - 1 }, (_, i) => {
39
+ const framePromises = Array.from({ length: total }, (_, i) => {
39
40
  const t = interval * i;
40
41
  const framePath = path.join(tempDir, `frame-${t}.jpg`);
41
42
  return new Promise((resolve, reject) => {
42
43
  const result = shell.exec(`ffmpeg -ss ${t} -i "${videoPath}" -vframes 1 -q:v 10 -an -threads 4 "${framePath}"`, { silent: true });
43
44
  if (result.code === 0) {
44
- log(`Frame ${i + 1}/${frames - 1} extracted`, { time: t });
45
+ log(`Frame ${i + 1}/${total} extracted`, { time: t });
45
46
  resolve();
46
47
  }
47
48
  else {
@@ -49,14 +50,16 @@ async function extractFrames(videoPath, tempDir, frames, interval, maxConcurrenc
49
50
  }
50
51
  });
51
52
  });
52
- // 分批执行以控制并发数
53
+ // 分批执行以控制并发数,每批完成后上报进度
53
54
  for (let i = 0; i < framePromises.length; i += maxConcurrency) {
54
55
  const batch = framePromises.slice(i, i + maxConcurrency);
55
56
  await Promise.all(batch);
57
+ const current = Math.min(i + maxConcurrency, total);
58
+ onProgress?.(current, total);
56
59
  }
57
60
  }
58
61
  // 创建缩略图
59
- async function createThumbnail(frameFiles, tempDir, options) {
62
+ async function createThumbnail(frameFiles, tempDir, options, progressCallbacks) {
60
63
  const { columns = 4, outputWidth = 3840, format = 'avif', quality = 80 } = options;
61
64
  log('Creating thumbnail', { frameCount: frameFiles.length, columns, outputWidth });
62
65
  // 计算尺寸
@@ -84,6 +87,7 @@ async function createThumbnail(frameFiles, tempDir, options) {
84
87
  // 分批处理帧以避免内存问题
85
88
  const batchSize = options.batchSize || 10;
86
89
  const overlays = [];
90
+ const totalBatches = Math.ceil(frameFiles.length / batchSize);
87
91
  for (let i = 0; i < frameFiles.length; i += batchSize) {
88
92
  const batch = frameFiles.slice(i, i + batchSize);
89
93
  const batchOverlays = await Promise.all(batch.map(async (file, batchIndex) => {
@@ -100,7 +104,9 @@ async function createThumbnail(frameFiles, tempDir, options) {
100
104
  };
101
105
  }));
102
106
  overlays.push(...batchOverlays);
103
- log(`Processed batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(frameFiles.length / batchSize)}`);
107
+ const currentBatch = Math.floor(i / batchSize) + 1;
108
+ progressCallbacks?.onComposingProgress?.(currentBatch, totalBatches);
109
+ log(`Processed batch ${currentBatch}/${totalBatches}`);
104
110
  }
105
111
  // 生成最终图像
106
112
  let finalImage = composite.composite(overlays);
@@ -121,7 +127,10 @@ async function createThumbnail(frameFiles, tempDir, options) {
121
127
  default:
122
128
  finalImage = finalImage.avif({ quality });
123
129
  }
124
- return await finalImage.toBuffer();
130
+ progressCallbacks?.onEncodingStart?.();
131
+ const buffer = await finalImage.toBuffer();
132
+ progressCallbacks?.onEncodingEnd?.();
133
+ return buffer;
125
134
  }
126
135
  // 临时目录管理
127
136
  async function withTempDir(fn, customTempDir) {
@@ -150,7 +159,7 @@ export const getThumbnail = async (videoPath, options = {}) => {
150
159
  throw new Error('FFmpeg is not available. Please install FFmpeg first.');
151
160
  }
152
161
  // 参数设置和验证
153
- const { frames = 60, outputWidth = 3840, columns = 4, outputFileName = "thumbnail.avif", quality = 80, format = 'avif', batchSize = 10, maxConcurrency = 4, tempDir: customTempDir } = options;
162
+ const { frames = 60, outputWidth = 3840, columns = 4, outputFileName = "thumbnail.avif", quality = 80, format = 'avif', batchSize = 10, maxConcurrency = 4, tempDir: customTempDir, onProgress } = options;
154
163
  // 参数验证
155
164
  if (frames <= 0 || outputWidth <= 0 || columns <= 0) {
156
165
  throw new Error('Invalid parameters: frames, outputWidth, and columns must be positive');
@@ -158,14 +167,26 @@ export const getThumbnail = async (videoPath, options = {}) => {
158
167
  if (quality < 1 || quality > 100) {
159
168
  throw new Error('Quality must be between 1 and 100');
160
169
  }
170
+ const report = (progress) => {
171
+ onProgress?.(progress);
172
+ };
161
173
  log('Starting thumbnail generation', {
162
174
  videoPath: validatedPath,
163
175
  options: { frames, outputWidth, columns, format, quality }
164
176
  });
177
+ report({ phase: 'analyzing', percent: 0, message: '分析视频...' });
165
178
  // 获取视频信息
166
179
  const durationInSeconds = getDurationInSeconds(validatedPath);
167
180
  const interval = durationInSeconds / frames;
168
181
  log('Video analysis complete', { durationInSeconds, interval });
182
+ const extractTotal = frames - 1;
183
+ report({
184
+ phase: 'extracting',
185
+ percent: 5,
186
+ current: 0,
187
+ total: extractTotal,
188
+ message: `提取帧 0/${extractTotal}`
189
+ });
169
190
  // 准备输出路径
170
191
  const videoDir = path.dirname(validatedPath);
171
192
  const fileNameWithoutExt = path.basename(validatedPath, path.extname(validatedPath));
@@ -173,7 +194,16 @@ export const getThumbnail = async (videoPath, options = {}) => {
173
194
  // 使用临时目录处理
174
195
  const result = await withTempDir(async (tempDir) => {
175
196
  // 提取帧
176
- await extractFrames(validatedPath, tempDir, frames, interval, maxConcurrency);
197
+ await extractFrames(validatedPath, tempDir, frames, interval, maxConcurrency, (current, total) => {
198
+ const percent = total > 0 ? 5 + 45 * (current / total) : 5;
199
+ report({
200
+ phase: 'extracting',
201
+ percent: Math.round(percent),
202
+ current,
203
+ total,
204
+ message: `提取帧 ${current}/${total}`
205
+ });
206
+ });
177
207
  // 获取所有帧文件并排序
178
208
  const frameFiles = fs
179
209
  .readdirSync(tempDir)
@@ -187,12 +217,36 @@ export const getThumbnail = async (videoPath, options = {}) => {
187
217
  throw new Error('No frames were extracted from the video');
188
218
  }
189
219
  log('Frame extraction complete', { extractedFrames: frameFiles.length });
220
+ report({
221
+ phase: 'composing',
222
+ percent: 50,
223
+ message: '合成缩略图...'
224
+ });
190
225
  // 创建缩略图
191
- const buffer = await createThumbnail(frameFiles, tempDir, options);
226
+ const totalComposeBatches = Math.ceil(frameFiles.length / (batchSize || 10));
227
+ const buffer = await createThumbnail(frameFiles, tempDir, options, {
228
+ onComposingProgress: (current, total) => {
229
+ const percent = total > 0 ? 50 + 45 * (current / total) : 50;
230
+ report({
231
+ phase: 'composing',
232
+ percent: Math.round(percent),
233
+ current,
234
+ total,
235
+ message: `合成 ${current}/${total}`
236
+ });
237
+ },
238
+ onEncodingStart: () => {
239
+ report({ phase: 'encoding', percent: 95, message: '编码输出...' });
240
+ },
241
+ onEncodingEnd: () => {
242
+ report({ phase: 'encoding', percent: 100, message: '编码完成' });
243
+ }
244
+ });
192
245
  return buffer;
193
246
  }, customTempDir);
194
247
  // 保存文件
195
248
  fs.writeFileSync(outputPath, result);
249
+ report({ phase: 'done', percent: 100, message: '完成' });
196
250
  const processingTime = Date.now() - startTime;
197
251
  log('Thumbnail generation complete', {
198
252
  outputPath,
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 获取视频信息的接口定义
3
+ */
4
+ export interface VideoInfo {
5
+ /** 视频文件路径 */
6
+ path: string;
7
+ /** 视频时长(秒) */
8
+ duration: number;
9
+ /** 视频时长格式化字符串 */
10
+ durationFormatted: string;
11
+ /** 视频宽度 */
12
+ width: number;
13
+ /** 视频高度 */
14
+ height: number;
15
+ /** 视频编码格式 */
16
+ videoCodec: string;
17
+ /** 音频编码格式 */
18
+ audioCodec: string;
19
+ /** 视频比特率 */
20
+ bitrate: number;
21
+ /** 帧率 */
22
+ fps: number;
23
+ /** 文件大小(字节) */
24
+ fileSize: number;
25
+ /** 文件大小格式化字符串 */
26
+ fileSizeFormatted: string;
27
+ /** 原始 ffprobe 输出信息 */
28
+ rawInfo: string;
29
+ }
30
+ /**
31
+ * 获取视频信息(使用 ffprobe)
32
+ * @param videoPath 视频文件路径
33
+ * @returns 视频信息对象
34
+ */
35
+ export declare function getVideoInfo(videoPath: string): VideoInfo;
36
+ /**
37
+ * 批量获取视频信息
38
+ * @param videoPaths 视频文件路径数组
39
+ * @returns 视频信息对象数组
40
+ */
41
+ export declare function getMultipleVideoInfo(videoPaths: string[]): VideoInfo[];
42
+ /**
43
+ * 获取详细的视频信息(包含更多元数据)
44
+ * @param videoPath 视频文件路径
45
+ * @returns 详细的视频信息
46
+ */
47
+ export declare function getDetailedVideoInfo(videoPath: string): any;
@@ -0,0 +1,109 @@
1
+ import shell from "shelljs";
2
+ /**
3
+ * 格式化文件大小
4
+ * @param bytes 字节数
5
+ * @returns 格式化后的文件大小字符串
6
+ */
7
+ function formatFileSize(bytes) {
8
+ const units = ["B", "KB", "MB", "GB", "TB"];
9
+ let size = bytes;
10
+ let unitIndex = 0;
11
+ while (size >= 1024 && unitIndex < units.length - 1) {
12
+ size /= 1024;
13
+ unitIndex++;
14
+ }
15
+ return `${size.toFixed(2)} ${units[unitIndex]}`;
16
+ }
17
+ /**
18
+ * 格式化时长
19
+ * @param seconds 秒数
20
+ * @returns 格式化后的时长字符串
21
+ */
22
+ function formatDuration(seconds) {
23
+ const hours = Math.floor(seconds / 3600);
24
+ const minutes = Math.floor((seconds % 3600) / 60);
25
+ const secs = Math.floor(seconds % 60);
26
+ const ms = Math.floor((seconds % 1) * 100);
27
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}.${ms.toString().padStart(2, "0")}`;
28
+ }
29
+ /**
30
+ * 获取视频信息(使用 ffprobe)
31
+ * @param videoPath 视频文件路径
32
+ * @returns 视频信息对象
33
+ */
34
+ export function getVideoInfo(videoPath) {
35
+ // 检查文件是否存在
36
+ if (!shell.test("-f", videoPath)) {
37
+ throw new Error(`视频文件不存在: ${videoPath}`);
38
+ }
39
+ // 使用 ffprobe 获取 JSON 格式的视频信息
40
+ const result = shell.exec(`ffprobe -v quiet -print_format json -show_format -show_streams "${videoPath}"`, { silent: true });
41
+ if (result.code !== 0) {
42
+ throw new Error(`获取视频信息失败: ${result.stderr}`);
43
+ }
44
+ let probeData;
45
+ try {
46
+ probeData = JSON.parse(result.stdout);
47
+ }
48
+ catch (error) {
49
+ throw new Error(`解析 ffprobe 输出失败: ${error}`);
50
+ }
51
+ const info = {
52
+ path: videoPath,
53
+ rawInfo: JSON.stringify(probeData, null, 2),
54
+ };
55
+ // 从 format 信息中获取时长和文件大小
56
+ if (probeData.format) {
57
+ if (probeData.format.duration) {
58
+ info.duration = parseFloat(probeData.format.duration);
59
+ info.durationFormatted = formatDuration(info.duration);
60
+ }
61
+ if (probeData.format.size) {
62
+ info.fileSize = parseInt(probeData.format.size);
63
+ info.fileSizeFormatted = formatFileSize(info.fileSize);
64
+ }
65
+ }
66
+ // 从视频流中获取信息
67
+ const videoStream = probeData.streams?.find((stream) => stream.codec_type === "video");
68
+ if (videoStream) {
69
+ info.videoCodec = videoStream.codec_name || "unknown";
70
+ info.width = videoStream.width || 0;
71
+ info.height = videoStream.height || 0;
72
+ if (videoStream.r_frame_rate) {
73
+ const [num, den] = videoStream.r_frame_rate.split("/");
74
+ info.fps = parseInt(num) / parseInt(den);
75
+ }
76
+ if (videoStream.bit_rate) {
77
+ info.bitrate = parseInt(videoStream.bit_rate);
78
+ }
79
+ }
80
+ // 从音频流中获取信息
81
+ const audioStream = probeData.streams?.find((stream) => stream.codec_type === "audio");
82
+ if (audioStream) {
83
+ info.audioCodec = audioStream.codec_name || "unknown";
84
+ }
85
+ return info;
86
+ }
87
+ /**
88
+ * 批量获取视频信息
89
+ * @param videoPaths 视频文件路径数组
90
+ * @returns 视频信息对象数组
91
+ */
92
+ export function getMultipleVideoInfo(videoPaths) {
93
+ return videoPaths.map((path) => getVideoInfo(path));
94
+ }
95
+ /**
96
+ * 获取详细的视频信息(包含更多元数据)
97
+ * @param videoPath 视频文件路径
98
+ * @returns 详细的视频信息
99
+ */
100
+ export function getDetailedVideoInfo(videoPath) {
101
+ if (!shell.test("-f", videoPath)) {
102
+ throw new Error(`视频文件不存在: ${videoPath}`);
103
+ }
104
+ const result = shell.exec(`ffprobe -v quiet -print_format json -show_format -show_streams -show_chapters -show_private_data "${videoPath}"`, { silent: true });
105
+ if (result.code !== 0) {
106
+ throw new Error(`获取详细视频信息失败: ${result.stderr}`);
107
+ }
108
+ return JSON.parse(result.stdout);
109
+ }
@@ -1,50 +1,5 @@
1
1
  export * from "./getThumbnail.js";
2
2
  export * from "./check.js";
3
3
  export * from "./cutVideo.js";
4
- /**
5
- * 获取视频信息的接口定义
6
- */
7
- export interface VideoInfo {
8
- /** 视频文件路径 */
9
- path: string;
10
- /** 视频时长(秒) */
11
- duration: number;
12
- /** 视频时长格式化字符串 */
13
- durationFormatted: string;
14
- /** 视频宽度 */
15
- width: number;
16
- /** 视频高度 */
17
- height: number;
18
- /** 视频编码格式 */
19
- videoCodec: string;
20
- /** 音频编码格式 */
21
- audioCodec: string;
22
- /** 视频比特率 */
23
- bitrate: number;
24
- /** 帧率 */
25
- fps: number;
26
- /** 文件大小(字节) */
27
- fileSize: number;
28
- /** 文件大小格式化字符串 */
29
- fileSizeFormatted: string;
30
- /** 原始 ffprobe 输出信息 */
31
- rawInfo: string;
32
- }
33
- /**
34
- * 获取视频信息(使用 ffprobe)
35
- * @param videoPath 视频文件路径
36
- * @returns 视频信息对象
37
- */
38
- export declare function getVideoInfo(videoPath: string): VideoInfo;
39
- /**
40
- * 批量获取视频信息
41
- * @param videoPaths 视频文件路径数组
42
- * @returns 视频信息对象数组
43
- */
44
- export declare function getMultipleVideoInfo(videoPaths: string[]): VideoInfo[];
45
- /**
46
- * 获取详细的视频信息(包含更多元数据)
47
- * @param videoPath 视频文件路径
48
- * @returns 详细的视频信息
49
- */
50
- export declare function getDetailedVideoInfo(videoPath: string): any;
4
+ export * from "./getVideoInfo.js";
5
+ export * from "./extractAudio.js";