node-av 5.1.0 → 5.2.0-beta.2

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,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
- };
@@ -1,275 +0,0 @@
1
- /**
2
- * Time and Memory Measurement Utilities
3
- *
4
- * Provides utilities for measuring execution time, memory usage,
5
- * and computing statistics for benchmark results.
6
- */
7
-
8
- /**
9
- * Result of a single benchmark run
10
- */
11
- export interface MeasureResult {
12
- /** Duration in milliseconds */
13
- durationMs: number;
14
- /** Peak memory usage in bytes */
15
- peakMemoryBytes: number;
16
- /** Memory samples taken during execution (bytes) */
17
- memorySamples: number[];
18
- /** Frames processed (if applicable) */
19
- framesProcessed?: number;
20
- /** FPS calculated from frames and duration */
21
- fps?: number;
22
- }
23
-
24
- /**
25
- * Statistics computed from multiple runs
26
- */
27
- export interface Stats {
28
- mean: number;
29
- min: number;
30
- max: number;
31
- stdDev: number;
32
- median: number;
33
- }
34
-
35
- /**
36
- * Aggregated benchmark result from multiple iterations
37
- */
38
- export interface AggregatedResult {
39
- iterations: number;
40
- durationMs: Stats;
41
- peakMemoryBytes: Stats;
42
- fps?: Stats;
43
- framesProcessed?: number;
44
- }
45
-
46
- /**
47
- * Memory sampler that records memory usage at regular intervals
48
- * Uses delta from baseline to measure actual memory growth during operation
49
- */
50
- export class MemorySampler {
51
- private samples: number[] = [];
52
- private interval: ReturnType<typeof setInterval> | null = null;
53
- private peakMemory = 0;
54
- private baselineMemory = 0;
55
-
56
- /**
57
- * Start sampling memory at the given interval
58
- * Records baseline before starting to measure delta
59
- */
60
- start(intervalMs = 100): void {
61
- this.samples = [];
62
- this.peakMemory = 0;
63
-
64
- // Force GC before taking baseline for more accurate measurement
65
- if (global.gc) {
66
- global.gc();
67
- }
68
-
69
- // Record baseline memory
70
- this.baselineMemory = process.memoryUsage().rss;
71
-
72
- this.interval = setInterval(() => {
73
- const memUsage = process.memoryUsage();
74
- const rss = memUsage.rss;
75
- this.samples.push(rss);
76
- if (rss > this.peakMemory) {
77
- this.peakMemory = rss;
78
- }
79
- }, intervalMs);
80
- }
81
-
82
- /**
83
- * Stop sampling and return results
84
- * Returns peak memory as delta from baseline
85
- */
86
- stop(): { samples: number[]; peakMemory: number; baselineMemory: number } {
87
- if (this.interval) {
88
- clearInterval(this.interval);
89
- this.interval = null;
90
- }
91
-
92
- // Take one final sample
93
- const finalRss = process.memoryUsage().rss;
94
- this.samples.push(finalRss);
95
- if (finalRss > this.peakMemory) {
96
- this.peakMemory = finalRss;
97
- }
98
-
99
- return {
100
- samples: this.samples,
101
- peakMemory: this.peakMemory,
102
- baselineMemory: this.baselineMemory,
103
- };
104
- }
105
- }
106
-
107
- /**
108
- * High-resolution timer for measuring execution time
109
- */
110
- export class Timer {
111
- private startTime = 0n;
112
- private endTime = 0n;
113
-
114
- /**
115
- * Start the timer
116
- */
117
- start(): void {
118
- this.startTime = process.hrtime.bigint();
119
- }
120
-
121
- /**
122
- * Stop the timer
123
- */
124
- stop(): void {
125
- this.endTime = process.hrtime.bigint();
126
- }
127
-
128
- /**
129
- * Get elapsed time in milliseconds
130
- */
131
- getElapsedMs(): number {
132
- return Number(this.endTime - this.startTime) / 1_000_000;
133
- }
134
-
135
- /**
136
- * Get elapsed time in seconds
137
- */
138
- getElapsedSeconds(): number {
139
- return this.getElapsedMs() / 1000;
140
- }
141
- }
142
-
143
- /**
144
- * Measure execution time and memory usage of an async function
145
- * Memory is measured as delta from baseline for accurate comparison
146
- */
147
- export async function measure(fn: () => Promise<{ framesProcessed?: number }>, options: { memorySampleIntervalMs?: number } = {}): Promise<MeasureResult> {
148
- const { memorySampleIntervalMs = 100 } = options;
149
-
150
- // Force garbage collection if available to get clean baseline
151
- if (global.gc) {
152
- global.gc();
153
- // Wait a bit for GC to complete
154
- await new Promise((resolve) => setTimeout(resolve, 50));
155
- global.gc();
156
- }
157
-
158
- const timer = new Timer();
159
- const memorySampler = new MemorySampler();
160
-
161
- memorySampler.start(memorySampleIntervalMs);
162
- timer.start();
163
-
164
- const result = await fn();
165
-
166
- timer.stop();
167
- const memoryResult = memorySampler.stop();
168
-
169
- const durationMs = timer.getElapsedMs();
170
- const framesProcessed = result.framesProcessed;
171
-
172
- // Calculate peak memory as delta from baseline
173
- // This gives us the actual memory growth during the operation
174
- const peakMemoryDelta = memoryResult.peakMemory - memoryResult.baselineMemory;
175
-
176
- return {
177
- durationMs,
178
- peakMemoryBytes: Math.max(0, peakMemoryDelta), // Ensure non-negative
179
- memorySamples: memoryResult.samples,
180
- framesProcessed,
181
- fps: framesProcessed ? (framesProcessed / durationMs) * 1000 : undefined,
182
- };
183
- }
184
-
185
- /**
186
- * Compute statistics from an array of numbers
187
- */
188
- export function computeStats(values: number[]): Stats {
189
- if (values.length === 0) {
190
- return { mean: 0, min: 0, max: 0, stdDev: 0, median: 0 };
191
- }
192
-
193
- const sorted = [...values].sort((a, b) => a - b);
194
- const sum = values.reduce((a, b) => a + b, 0);
195
- const mean = sum / values.length;
196
- const min = sorted[0];
197
- const max = sorted[sorted.length - 1];
198
-
199
- // Standard deviation
200
- const squaredDiffs = values.map((v) => Math.pow(v - mean, 2));
201
- const avgSquaredDiff = squaredDiffs.reduce((a, b) => a + b, 0) / values.length;
202
- const stdDev = Math.sqrt(avgSquaredDiff);
203
-
204
- // Median
205
- const mid = Math.floor(sorted.length / 2);
206
- const median = sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
207
-
208
- return { mean, min, max, stdDev, median };
209
- }
210
-
211
- /**
212
- * Aggregate multiple measurement results into statistics
213
- */
214
- export function aggregateResults(results: MeasureResult[]): AggregatedResult {
215
- const durations = results.map((r) => r.durationMs);
216
- const peakMemories = results.map((r) => r.peakMemoryBytes);
217
- const fpsValues = results.filter((r) => r.fps !== undefined).map((r) => r.fps!);
218
-
219
- const framesProcessed = results.find((r) => r.framesProcessed)?.framesProcessed;
220
-
221
- return {
222
- iterations: results.length,
223
- durationMs: computeStats(durations),
224
- peakMemoryBytes: computeStats(peakMemories),
225
- fps: fpsValues.length > 0 ? computeStats(fpsValues) : undefined,
226
- framesProcessed,
227
- };
228
- }
229
-
230
- /**
231
- * Format bytes to human-readable string
232
- */
233
- export function formatBytes(bytes: number): string {
234
- const units = ['B', 'KB', 'MB', 'GB'];
235
- let value = bytes;
236
- let unitIndex = 0;
237
-
238
- while (value >= 1024 && unitIndex < units.length - 1) {
239
- value /= 1024;
240
- unitIndex++;
241
- }
242
-
243
- return `${value.toFixed(1)} ${units[unitIndex]}`;
244
- }
245
-
246
- /**
247
- * Format milliseconds to human-readable string
248
- */
249
- export function formatDuration(ms: number): string {
250
- if (ms < 1000) {
251
- return `${ms.toFixed(1)}ms`;
252
- } else if (ms < 60000) {
253
- return `${(ms / 1000).toFixed(2)}s`;
254
- } else {
255
- const minutes = Math.floor(ms / 60000);
256
- const seconds = ((ms % 60000) / 1000).toFixed(1);
257
- return `${minutes}m ${seconds}s`;
258
- }
259
- }
260
-
261
- /**
262
- * Calculate percentage difference between two values
263
- */
264
- export function percentDiff(baseline: number, comparison: number): number {
265
- if (baseline === 0) return 0;
266
- return ((comparison - baseline) / baseline) * 100;
267
- }
268
-
269
- /**
270
- * Format percentage difference with sign
271
- */
272
- export function formatPercentDiff(diff: number): string {
273
- const sign = diff > 0 ? '+' : '';
274
- return `${sign}${diff.toFixed(1)}%`;
275
- }