node-av 5.1.0 → 5.1.1
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 +17 -1
- package/dist/api/hardware.js +3 -4
- package/dist/api/hardware.js.map +1 -1
- package/dist/api/rtp-stream.js +0 -6
- package/dist/api/rtp-stream.js.map +1 -1
- package/dist/ffmpeg/index.js +3 -4
- package/dist/ffmpeg/index.js.map +1 -1
- package/dist/lib/binding.js +3 -4
- package/dist/lib/binding.js.map +1 -1
- package/dist/utils/electron.d.ts +49 -0
- package/dist/utils/electron.js +63 -0
- package/dist/utils/electron.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +27 -12
- package/benchmarks/cases/latency.ts +0 -361
- package/benchmarks/cases/memory.ts +0 -260
- package/benchmarks/cases/transcode.ts +0 -271
- package/benchmarks/index.ts +0 -264
- package/benchmarks/regen-report.ts +0 -22
- package/benchmarks/results/.gitkeep +0 -2
- package/benchmarks/runner.ts +0 -247
- package/benchmarks/utils/ffmpeg-cli.ts +0 -363
- package/benchmarks/utils/measure.ts +0 -275
- package/benchmarks/utils/report.ts +0 -405
- package/binding.gyp +0 -166
package/benchmarks/runner.ts
DELETED
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Benchmark Runner
|
|
3
|
-
*
|
|
4
|
-
* Core class for running benchmarks and collecting results.
|
|
5
|
-
* Supports both FFmpeg CLI and node-av implementations.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync, unlinkSync } from 'node:fs';
|
|
9
|
-
import { mkdir } from 'node:fs/promises';
|
|
10
|
-
import { dirname } from 'node:path';
|
|
11
|
-
|
|
12
|
-
import { measureFFmpegCLI } from './utils/ffmpeg-cli.js';
|
|
13
|
-
import { aggregateResults, measure } from './utils/measure.js';
|
|
14
|
-
|
|
15
|
-
import type { FFmpegRunOptions } from './utils/ffmpeg-cli.js';
|
|
16
|
-
import type { AggregatedResult, MeasureResult } from './utils/measure.js';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Configuration for a benchmark test case
|
|
20
|
-
*/
|
|
21
|
-
export interface BenchmarkConfig {
|
|
22
|
-
/** Name of the benchmark */
|
|
23
|
-
name: string;
|
|
24
|
-
/** Description of what this benchmark tests */
|
|
25
|
-
description: string;
|
|
26
|
-
/** Category: transcode, memory, or latency */
|
|
27
|
-
category: 'transcode' | 'memory' | 'latency';
|
|
28
|
-
/** Number of iterations to run */
|
|
29
|
-
iterations: number;
|
|
30
|
-
/** Number of warmup iterations (not counted in results) */
|
|
31
|
-
warmupIterations: number;
|
|
32
|
-
/** Input file path */
|
|
33
|
-
inputFile: string;
|
|
34
|
-
/** Output file path (will be deleted after each run) */
|
|
35
|
-
outputFile: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Result of a benchmark comparison between FFmpeg CLI and node-av
|
|
40
|
-
*/
|
|
41
|
-
export interface BenchmarkComparison {
|
|
42
|
-
config: BenchmarkConfig;
|
|
43
|
-
ffmpegCLI: AggregatedResult;
|
|
44
|
-
nodeAV: AggregatedResult;
|
|
45
|
-
/** Difference metrics */
|
|
46
|
-
comparison: {
|
|
47
|
-
/** Duration difference (positive means node-av is slower) */
|
|
48
|
-
durationDiffPercent: number;
|
|
49
|
-
/** Memory difference (positive means node-av uses more memory) */
|
|
50
|
-
memoryDiffPercent: number;
|
|
51
|
-
/** FPS difference (positive means node-av is faster) */
|
|
52
|
-
fpsDiffPercent?: number;
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Callback type for node-av benchmark function
|
|
58
|
-
*/
|
|
59
|
-
export type NodeAVBenchmarkFn = () => Promise<{ framesProcessed?: number }>;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Runner for executing and comparing benchmarks
|
|
63
|
-
*/
|
|
64
|
-
export class BenchmarkRunner {
|
|
65
|
-
private results: BenchmarkComparison[] = [];
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Run a single iteration and measure results
|
|
69
|
-
*/
|
|
70
|
-
private async runSingleIteration(
|
|
71
|
-
config: BenchmarkConfig,
|
|
72
|
-
ffmpegOptions: FFmpegRunOptions,
|
|
73
|
-
nodeAVFn: NodeAVBenchmarkFn,
|
|
74
|
-
): Promise<{ ffmpeg: MeasureResult; nodeAV: MeasureResult }> {
|
|
75
|
-
// Ensure output directory exists
|
|
76
|
-
const outputDir = dirname(config.outputFile);
|
|
77
|
-
if (!existsSync(outputDir)) {
|
|
78
|
-
await mkdir(outputDir, { recursive: true });
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Run FFmpeg CLI
|
|
82
|
-
const ffmpegResult = await measureFFmpegCLI(ffmpegOptions);
|
|
83
|
-
|
|
84
|
-
// Clean up output file
|
|
85
|
-
if (existsSync(config.outputFile)) {
|
|
86
|
-
unlinkSync(config.outputFile);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Run node-av
|
|
90
|
-
const nodeAVResult = await measure(nodeAVFn);
|
|
91
|
-
|
|
92
|
-
// Clean up output file
|
|
93
|
-
if (existsSync(config.outputFile)) {
|
|
94
|
-
unlinkSync(config.outputFile);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return { ffmpeg: ffmpegResult, nodeAV: nodeAVResult };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Run a complete benchmark with warmup and multiple iterations
|
|
102
|
-
*/
|
|
103
|
-
async runBenchmark(config: BenchmarkConfig, ffmpegOptions: FFmpegRunOptions, nodeAVFn: NodeAVBenchmarkFn): Promise<BenchmarkComparison> {
|
|
104
|
-
console.log(`\n📊 Running benchmark: ${config.name}`);
|
|
105
|
-
console.log(` ${config.description}`);
|
|
106
|
-
console.log(` Iterations: ${config.iterations} (+ ${config.warmupIterations} warmup)\n`);
|
|
107
|
-
|
|
108
|
-
const ffmpegResults: MeasureResult[] = [];
|
|
109
|
-
const nodeAVResults: MeasureResult[] = [];
|
|
110
|
-
|
|
111
|
-
// Warmup iterations
|
|
112
|
-
for (let i = 0; i < config.warmupIterations; i++) {
|
|
113
|
-
process.stdout.write(` Warmup ${i + 1}/${config.warmupIterations}...\r`);
|
|
114
|
-
await this.runSingleIteration(config, ffmpegOptions, nodeAVFn);
|
|
115
|
-
// GC between iterations to prevent memory accumulation
|
|
116
|
-
if (global.gc) {
|
|
117
|
-
global.gc();
|
|
118
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Actual benchmark iterations
|
|
123
|
-
for (let i = 0; i < config.iterations; i++) {
|
|
124
|
-
process.stdout.write(` Iteration ${i + 1}/${config.iterations}...\r`);
|
|
125
|
-
const { ffmpeg, nodeAV } = await this.runSingleIteration(config, ffmpegOptions, nodeAVFn);
|
|
126
|
-
ffmpegResults.push(ffmpeg);
|
|
127
|
-
nodeAVResults.push(nodeAV);
|
|
128
|
-
// GC between iterations to prevent memory accumulation
|
|
129
|
-
if (global.gc) {
|
|
130
|
-
global.gc();
|
|
131
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
console.log(''); // New line after progress
|
|
136
|
-
|
|
137
|
-
// Aggregate results
|
|
138
|
-
const ffmpegAggregated = aggregateResults(ffmpegResults);
|
|
139
|
-
const nodeAVAggregated = aggregateResults(nodeAVResults);
|
|
140
|
-
|
|
141
|
-
// Calculate comparison metrics
|
|
142
|
-
const durationDiffPercent = this.percentDiff(ffmpegAggregated.durationMs.mean, nodeAVAggregated.durationMs.mean);
|
|
143
|
-
|
|
144
|
-
const memoryDiffPercent =
|
|
145
|
-
ffmpegAggregated.peakMemoryBytes.mean > 0 ? this.percentDiff(ffmpegAggregated.peakMemoryBytes.mean, nodeAVAggregated.peakMemoryBytes.mean) : 0;
|
|
146
|
-
|
|
147
|
-
const fpsDiffPercent = ffmpegAggregated.fps && nodeAVAggregated.fps ? this.percentDiff(ffmpegAggregated.fps.mean, nodeAVAggregated.fps.mean) : undefined;
|
|
148
|
-
|
|
149
|
-
const result: BenchmarkComparison = {
|
|
150
|
-
config,
|
|
151
|
-
ffmpegCLI: ffmpegAggregated,
|
|
152
|
-
nodeAV: nodeAVAggregated,
|
|
153
|
-
comparison: {
|
|
154
|
-
durationDiffPercent,
|
|
155
|
-
memoryDiffPercent,
|
|
156
|
-
fpsDiffPercent,
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
this.results.push(result);
|
|
161
|
-
|
|
162
|
-
// Print summary
|
|
163
|
-
this.printBenchmarkSummary(result);
|
|
164
|
-
|
|
165
|
-
return result;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Calculate percentage difference
|
|
170
|
-
*/
|
|
171
|
-
private percentDiff(baseline: number, comparison: number): number {
|
|
172
|
-
if (baseline === 0) return 0;
|
|
173
|
-
return ((comparison - baseline) / baseline) * 100;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Print summary of a single benchmark result
|
|
178
|
-
*/
|
|
179
|
-
private printBenchmarkSummary(result: BenchmarkComparison): void {
|
|
180
|
-
const { ffmpegCLI, nodeAV, comparison } = result;
|
|
181
|
-
|
|
182
|
-
console.log(' Results:');
|
|
183
|
-
console.log(' ┌──────────────────┬──────────────────┬──────────────────┐');
|
|
184
|
-
console.log(' │ Metric │ FFmpeg CLI │ node-av │');
|
|
185
|
-
console.log(' ├──────────────────┼──────────────────┼──────────────────┤');
|
|
186
|
-
|
|
187
|
-
const durationStr = `${ffmpegCLI.durationMs.mean.toFixed(0)}ms`;
|
|
188
|
-
const nodeAVDurationStr = `${nodeAV.durationMs.mean.toFixed(0)}ms`;
|
|
189
|
-
console.log(` │ Duration │ ${durationStr.padEnd(16)} │ ${nodeAVDurationStr.padEnd(16)} │`);
|
|
190
|
-
|
|
191
|
-
if (ffmpegCLI.fps && nodeAV.fps) {
|
|
192
|
-
const fpsStr = `${ffmpegCLI.fps.mean.toFixed(1)} fps`;
|
|
193
|
-
const nodeAVFpsStr = `${nodeAV.fps.mean.toFixed(1)} fps`;
|
|
194
|
-
console.log(` │ FPS │ ${fpsStr.padEnd(16)} │ ${nodeAVFpsStr.padEnd(16)} │`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const memStr = this.formatBytes(ffmpegCLI.peakMemoryBytes.mean);
|
|
198
|
-
const nodeAVMemStr = this.formatBytes(nodeAV.peakMemoryBytes.mean);
|
|
199
|
-
console.log(` │ Peak Memory │ ${memStr.padEnd(16)} │ ${nodeAVMemStr.padEnd(16)} │`);
|
|
200
|
-
|
|
201
|
-
console.log(' └──────────────────┴──────────────────┴──────────────────┘');
|
|
202
|
-
|
|
203
|
-
const diffSign = comparison.durationDiffPercent > 0 ? '+' : '';
|
|
204
|
-
console.log(` Difference: ${diffSign}${comparison.durationDiffPercent.toFixed(1)}% duration`);
|
|
205
|
-
|
|
206
|
-
if (comparison.fpsDiffPercent !== undefined) {
|
|
207
|
-
const fpsSign = comparison.fpsDiffPercent > 0 ? '+' : '';
|
|
208
|
-
console.log(` ${fpsSign}${comparison.fpsDiffPercent.toFixed(1)}% FPS`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Format bytes to human readable
|
|
214
|
-
*/
|
|
215
|
-
private formatBytes(bytes: number): string {
|
|
216
|
-
if (bytes === 0) return 'N/A';
|
|
217
|
-
const units = ['B', 'KB', 'MB', 'GB'];
|
|
218
|
-
let value = bytes;
|
|
219
|
-
let unitIndex = 0;
|
|
220
|
-
|
|
221
|
-
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
222
|
-
value /= 1024;
|
|
223
|
-
unitIndex++;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return `${value.toFixed(1)} ${units[unitIndex]}`;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Get all collected results
|
|
231
|
-
*/
|
|
232
|
-
getResults(): BenchmarkComparison[] {
|
|
233
|
-
return this.results;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Clear all results
|
|
238
|
-
*/
|
|
239
|
-
clearResults(): void {
|
|
240
|
-
this.results = [];
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Global benchmark runner instance
|
|
246
|
-
*/
|
|
247
|
-
export const runner = new BenchmarkRunner();
|
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FFmpeg CLI Wrapper
|
|
3
|
-
*
|
|
4
|
-
* Provides utilities for running FFmpeg CLI commands and measuring
|
|
5
|
-
* their performance for comparison with node-av.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { spawn } from 'node:child_process';
|
|
9
|
-
import { existsSync } from 'node:fs';
|
|
10
|
-
import { platform } from 'node:os';
|
|
11
|
-
|
|
12
|
-
import { ffmpegPath, isFfmpegAvailable } from '../../src/ffmpeg/index.js';
|
|
13
|
-
|
|
14
|
-
import type { MeasureResult } from './measure.js';
|
|
15
|
-
|
|
16
|
-
export interface FFmpegRunOptions {
|
|
17
|
-
/** Input file path */
|
|
18
|
-
input: string;
|
|
19
|
-
/** Output file path */
|
|
20
|
-
output: string;
|
|
21
|
-
/** Additional FFmpeg arguments */
|
|
22
|
-
args?: string[];
|
|
23
|
-
/** Timeout in milliseconds */
|
|
24
|
-
timeout?: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface FFmpegRunResult {
|
|
28
|
-
/** Exit code */
|
|
29
|
-
exitCode: number;
|
|
30
|
-
/** Standard output */
|
|
31
|
-
stdout: string;
|
|
32
|
-
/** Standard error (contains progress) */
|
|
33
|
-
stderr: string;
|
|
34
|
-
/** Duration in milliseconds */
|
|
35
|
-
durationMs: number;
|
|
36
|
-
/** Parsed frame count if available */
|
|
37
|
-
framesProcessed?: number;
|
|
38
|
-
/** Parsed FPS if available */
|
|
39
|
-
fps?: number;
|
|
40
|
-
/** Peak memory (estimated from /usr/bin/time if available) */
|
|
41
|
-
peakMemoryBytes?: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Get the FFmpeg binary path, checking if it exists
|
|
46
|
-
*/
|
|
47
|
-
export function getFFmpegPath(): string {
|
|
48
|
-
if (isFfmpegAvailable()) {
|
|
49
|
-
return ffmpegPath();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Try system ffmpeg
|
|
53
|
-
const systemPaths = ['/usr/bin/ffmpeg', '/usr/local/bin/ffmpeg', '/opt/homebrew/bin/ffmpeg'];
|
|
54
|
-
|
|
55
|
-
for (const path of systemPaths) {
|
|
56
|
-
if (existsSync(path)) {
|
|
57
|
-
return path;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
throw new Error('FFmpeg binary not found. Please install FFmpeg or run npm install to download the bundled binary.');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Run FFmpeg CLI with the given arguments
|
|
66
|
-
*/
|
|
67
|
-
export async function runFFmpegCLI(options: FFmpegRunOptions): Promise<FFmpegRunResult> {
|
|
68
|
-
const { input, output, args = [], timeout = 300000 } = options;
|
|
69
|
-
|
|
70
|
-
const ffmpeg = getFFmpegPath();
|
|
71
|
-
|
|
72
|
-
const ffmpegArgs = ['-hide_banner', '-y', '-i', input, ...args, output];
|
|
73
|
-
|
|
74
|
-
// Use /usr/bin/time to measure memory usage
|
|
75
|
-
// macOS: /usr/bin/time -l outputs "maximum resident set size" in bytes
|
|
76
|
-
// Linux: /usr/bin/time -v outputs "Maximum resident set size (kbytes)"
|
|
77
|
-
const os = platform();
|
|
78
|
-
const useTime = os === 'darwin' || os === 'linux';
|
|
79
|
-
const timePath = '/usr/bin/time';
|
|
80
|
-
const timeExists = useTime && existsSync(timePath);
|
|
81
|
-
|
|
82
|
-
let command: string;
|
|
83
|
-
let fullArgs: string[];
|
|
84
|
-
|
|
85
|
-
if (timeExists) {
|
|
86
|
-
command = timePath;
|
|
87
|
-
if (os === 'darwin') {
|
|
88
|
-
// macOS: -l for resource usage
|
|
89
|
-
fullArgs = ['-l', ffmpeg, ...ffmpegArgs];
|
|
90
|
-
} else {
|
|
91
|
-
// Linux: -v for verbose (includes memory)
|
|
92
|
-
fullArgs = ['-v', ffmpeg, ...ffmpegArgs];
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
command = ffmpeg;
|
|
96
|
-
fullArgs = ffmpegArgs;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return new Promise((resolve, reject) => {
|
|
100
|
-
const startTime = process.hrtime.bigint();
|
|
101
|
-
|
|
102
|
-
const proc = spawn(command, fullArgs, {
|
|
103
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
let stdout = '';
|
|
107
|
-
let stderr = '';
|
|
108
|
-
|
|
109
|
-
proc.stdout.on('data', (data) => {
|
|
110
|
-
stdout += data.toString();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
proc.stderr.on('data', (data) => {
|
|
114
|
-
stderr += data.toString();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const timeoutId = setTimeout(() => {
|
|
118
|
-
proc.kill('SIGKILL');
|
|
119
|
-
reject(new Error(`FFmpeg timed out after ${timeout}ms`));
|
|
120
|
-
}, timeout);
|
|
121
|
-
|
|
122
|
-
proc.on('close', (code) => {
|
|
123
|
-
clearTimeout(timeoutId);
|
|
124
|
-
const endTime = process.hrtime.bigint();
|
|
125
|
-
const durationMs = Number(endTime - startTime) / 1_000_000;
|
|
126
|
-
|
|
127
|
-
// Parse frame count from stderr (look for the last occurrence)
|
|
128
|
-
const frameMatches = [...stderr.matchAll(/frame=\s*(\d+)/g)];
|
|
129
|
-
const frameCount = frameMatches.length > 0 ? parseInt(frameMatches[frameMatches.length - 1][1], 10) : undefined;
|
|
130
|
-
|
|
131
|
-
// Parse FPS - try multiple methods:
|
|
132
|
-
// 1. Direct fps= value (if > 0)
|
|
133
|
-
// 2. Calculate from speed= multiplier and video duration
|
|
134
|
-
// 3. Calculate from frame count and our measured duration
|
|
135
|
-
let fps: number | undefined;
|
|
136
|
-
|
|
137
|
-
const fpsMatch = /fps=\s*([\d.]+)/.exec(stderr);
|
|
138
|
-
if (fpsMatch && parseFloat(fpsMatch[1]) > 0) {
|
|
139
|
-
fps = parseFloat(fpsMatch[1]);
|
|
140
|
-
} else if (frameCount && durationMs > 0) {
|
|
141
|
-
// Calculate FPS from frames processed and actual duration
|
|
142
|
-
fps = (frameCount / durationMs) * 1000;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Parse peak memory from /usr/bin/time output
|
|
146
|
-
let peakMemoryBytes: number | undefined;
|
|
147
|
-
if (timeExists) {
|
|
148
|
-
if (os === 'darwin') {
|
|
149
|
-
// macOS: "maximum resident set size" is in bytes
|
|
150
|
-
// Format: " 123456789 maximum resident set size"
|
|
151
|
-
const memMatch = /(\d+)\s+maximum resident set size/.exec(stderr);
|
|
152
|
-
if (memMatch) {
|
|
153
|
-
peakMemoryBytes = parseInt(memMatch[1], 10);
|
|
154
|
-
}
|
|
155
|
-
} else {
|
|
156
|
-
// Linux: "Maximum resident set size (kbytes): 123456"
|
|
157
|
-
const memMatch = /Maximum resident set size.*?:\s*(\d+)/.exec(stderr);
|
|
158
|
-
if (memMatch) {
|
|
159
|
-
peakMemoryBytes = parseInt(memMatch[1], 10) * 1024; // Convert KB to bytes
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
resolve({
|
|
165
|
-
exitCode: code ?? 0,
|
|
166
|
-
stdout,
|
|
167
|
-
stderr,
|
|
168
|
-
durationMs,
|
|
169
|
-
framesProcessed: frameCount,
|
|
170
|
-
fps,
|
|
171
|
-
peakMemoryBytes,
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
proc.on('error', (err) => {
|
|
176
|
-
clearTimeout(timeoutId);
|
|
177
|
-
reject(err);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Run FFmpeg CLI and return a MeasureResult for comparison
|
|
184
|
-
*/
|
|
185
|
-
export async function measureFFmpegCLI(options: FFmpegRunOptions): Promise<MeasureResult> {
|
|
186
|
-
const result = await runFFmpegCLI(options);
|
|
187
|
-
|
|
188
|
-
if (result.exitCode !== 0) {
|
|
189
|
-
throw new Error(`FFmpeg failed with exit code ${result.exitCode}: ${result.stderr}`);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Calculate FPS from frames and duration if not available from output
|
|
193
|
-
const fps = result.fps ?? (result.framesProcessed ? (result.framesProcessed / result.durationMs) * 1000 : undefined);
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
durationMs: result.durationMs,
|
|
197
|
-
peakMemoryBytes: result.peakMemoryBytes ?? 0,
|
|
198
|
-
memorySamples: [],
|
|
199
|
-
framesProcessed: result.framesProcessed,
|
|
200
|
-
fps,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Get FFmpeg version string
|
|
206
|
-
*/
|
|
207
|
-
export async function getFFmpegVersion(): Promise<string> {
|
|
208
|
-
const ffmpeg = getFFmpegPath();
|
|
209
|
-
|
|
210
|
-
return new Promise((resolve, reject) => {
|
|
211
|
-
const proc = spawn(ffmpeg, ['-version'], {
|
|
212
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
let stdout = '';
|
|
216
|
-
|
|
217
|
-
proc.stdout.on('data', (data) => {
|
|
218
|
-
stdout += data.toString();
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
proc.on('close', (code) => {
|
|
222
|
-
if (code !== 0) {
|
|
223
|
-
reject(new Error('Failed to get FFmpeg version'));
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Parse version from first line
|
|
228
|
-
const match = /ffmpeg version (\S+)/.exec(stdout);
|
|
229
|
-
resolve(match ? match[1] : 'unknown');
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
proc.on('error', reject);
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Probe a media file using ffprobe
|
|
238
|
-
*/
|
|
239
|
-
export async function probeMediaFile(
|
|
240
|
-
filePath: string,
|
|
241
|
-
): Promise<{ duration: number; videoCodec?: string; audioCodec?: string; width?: number; height?: number; fps?: number }> {
|
|
242
|
-
const ffmpeg = getFFmpegPath();
|
|
243
|
-
const ffprobe = ffmpeg.replace('ffmpeg', 'ffprobe');
|
|
244
|
-
|
|
245
|
-
// Check if ffprobe exists, otherwise use ffmpeg -i
|
|
246
|
-
const probeExists = existsSync(ffprobe);
|
|
247
|
-
|
|
248
|
-
return new Promise((resolve, reject) => {
|
|
249
|
-
if (probeExists) {
|
|
250
|
-
const proc = spawn(ffprobe, ['-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', filePath], {
|
|
251
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
let stdout = '';
|
|
255
|
-
|
|
256
|
-
proc.stdout.on('data', (data) => {
|
|
257
|
-
stdout += data.toString();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
proc.on('close', (code) => {
|
|
261
|
-
if (code !== 0) {
|
|
262
|
-
reject(new Error('Failed to probe media file'));
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
const info = JSON.parse(stdout);
|
|
268
|
-
const videoStream = info.streams?.find((s: { codec_type: string }) => s.codec_type === 'video');
|
|
269
|
-
const audioStream = info.streams?.find((s: { codec_type: string }) => s.codec_type === 'audio');
|
|
270
|
-
|
|
271
|
-
let fps: number | undefined;
|
|
272
|
-
if (videoStream?.r_frame_rate) {
|
|
273
|
-
const [num, den] = videoStream.r_frame_rate.split('/').map(Number);
|
|
274
|
-
fps = num / den;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
resolve({
|
|
278
|
-
duration: parseFloat(info.format?.duration ?? '0'),
|
|
279
|
-
videoCodec: videoStream?.codec_name,
|
|
280
|
-
audioCodec: audioStream?.codec_name,
|
|
281
|
-
width: videoStream?.width,
|
|
282
|
-
height: videoStream?.height,
|
|
283
|
-
fps,
|
|
284
|
-
});
|
|
285
|
-
} catch (e) {
|
|
286
|
-
reject(e);
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
proc.on('error', reject);
|
|
291
|
-
} else {
|
|
292
|
-
// Fallback: use ffmpeg -i and parse stderr
|
|
293
|
-
const proc = spawn(ffmpeg, ['-i', filePath], {
|
|
294
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
let stderr = '';
|
|
298
|
-
|
|
299
|
-
proc.stderr.on('data', (data) => {
|
|
300
|
-
stderr += data.toString();
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
proc.on('close', () => {
|
|
304
|
-
// Parse duration
|
|
305
|
-
const durationMatch = /Duration: (\d+):(\d+):(\d+\.\d+)/.exec(stderr);
|
|
306
|
-
let duration = 0;
|
|
307
|
-
if (durationMatch) {
|
|
308
|
-
duration = parseInt(durationMatch[1]) * 3600 + parseInt(durationMatch[2]) * 60 + parseFloat(durationMatch[3]);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Parse video stream info
|
|
312
|
-
const videoMatch = /Stream #\d+:\d+.*Video: (\w+).*, (\d+)x(\d+).*, ([\d.]+) fps/.exec(stderr);
|
|
313
|
-
|
|
314
|
-
// Parse audio stream info
|
|
315
|
-
const audioMatch = /Stream #\d+:\d+.*Audio: (\w+)/.exec(stderr);
|
|
316
|
-
|
|
317
|
-
resolve({
|
|
318
|
-
duration,
|
|
319
|
-
videoCodec: videoMatch?.[1],
|
|
320
|
-
width: videoMatch ? parseInt(videoMatch[2]) : undefined,
|
|
321
|
-
height: videoMatch ? parseInt(videoMatch[3]) : undefined,
|
|
322
|
-
fps: videoMatch ? parseFloat(videoMatch[4]) : undefined,
|
|
323
|
-
audioCodec: audioMatch?.[1],
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
proc.on('error', reject);
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Build common FFmpeg arguments for different test cases
|
|
334
|
-
* Note: -threads 0 enables auto-detection (same as node-av default)
|
|
335
|
-
*/
|
|
336
|
-
export const FFmpegArgs = {
|
|
337
|
-
/** Software H.264 encoding */
|
|
338
|
-
swH264: (crf = 23): string[] => ['-threads', '0', '-c:v', 'libx264', '-preset', 'medium', '-crf', crf.toString(), '-c:a', 'copy'],
|
|
339
|
-
|
|
340
|
-
/** Software H.265/HEVC encoding */
|
|
341
|
-
swH265: (crf = 28): string[] => ['-threads', '0', '-c:v', 'libx265', '-preset', 'medium', '-crf', crf.toString(), '-c:a', 'copy'],
|
|
342
|
-
|
|
343
|
-
/** Hardware H.264 encoding (VideoToolbox) */
|
|
344
|
-
hwH264VideoToolbox: (): string[] => ['-threads', '0', '-c:v', 'h264_videotoolbox', '-b:v', '2M', '-c:a', 'copy'],
|
|
345
|
-
|
|
346
|
-
/** Hardware H.264 encoding (NVENC) */
|
|
347
|
-
hwH264Nvenc: (): string[] => ['-threads', '0', '-c:v', 'h264_nvenc', '-preset', 'p4', '-b:v', '2M', '-c:a', 'copy'],
|
|
348
|
-
|
|
349
|
-
/** Hardware H.264 encoding (VAAPI) */
|
|
350
|
-
hwH264Vaapi: (): string[] => ['-threads', '0', '-vaapi_device', '/dev/dri/renderD128', '-c:v', 'h264_vaapi', '-b:v', '2M', '-c:a', 'copy'],
|
|
351
|
-
|
|
352
|
-
/** Hardware H.264 encoding (QSV) */
|
|
353
|
-
hwH264Qsv: (): string[] => ['-threads', '0', '-c:v', 'h264_qsv', '-preset', 'medium', '-b:v', '2M', '-c:a', 'copy'],
|
|
354
|
-
|
|
355
|
-
/** Stream copy (no re-encoding) */
|
|
356
|
-
streamCopy: (): string[] => ['-c', 'copy'],
|
|
357
|
-
|
|
358
|
-
/** Video only (no audio) */
|
|
359
|
-
videoOnly: (videoArgs: string[]): string[] => [...videoArgs, '-an'],
|
|
360
|
-
|
|
361
|
-
/** First N seconds only */
|
|
362
|
-
duration: (seconds: number, args: string[]): string[] => ['-t', seconds.toString(), ...args],
|
|
363
|
-
};
|