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.
@@ -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
- };