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,361 +0,0 @@
1
- /**
2
- * Latency Benchmark Tests
3
- *
4
- * Measures various latency metrics for node-av:
5
- * - Time to open a file and get first frame
6
- * - Time to decode first frame
7
- * - Time to encode first frame
8
- * - End-to-end pipeline latency
9
- */
10
-
11
- import { existsSync, unlinkSync } from 'node:fs';
12
- import { dirname, join, resolve } from 'node:path';
13
- import { fileURLToPath } from 'node:url';
14
-
15
- import { Decoder, Demuxer, Encoder, Muxer } from '../../src/api/index.js';
16
- import { FF_ENCODER_LIBX264 } from '../../src/constants/encoders.js';
17
- import { Timer, computeStats } from '../utils/measure.js';
18
-
19
- import type { Stats } from '../utils/measure.js';
20
-
21
- const __filename = fileURLToPath(import.meta.url);
22
- const __dirname = dirname(__filename);
23
-
24
- // Default paths
25
- const testDataDir = resolve(__dirname, '../../testdata');
26
- const resultsDir = resolve(__dirname, '../results');
27
-
28
- /**
29
- * Latency measurement result
30
- */
31
- export interface LatencyResult {
32
- name: string;
33
- description: string;
34
- stats: Stats;
35
- samples: number[];
36
- }
37
-
38
- /**
39
- * All latency metrics collected
40
- */
41
- export interface LatencyMetrics {
42
- demuxerOpen: LatencyResult;
43
- firstPacket: LatencyResult;
44
- firstFrame: LatencyResult;
45
- firstEncodedPacket: LatencyResult;
46
- pipelineTotal: LatencyResult;
47
- }
48
-
49
- /**
50
- * Measure latency with multiple iterations
51
- */
52
- async function measureLatency(name: string, description: string, fn: () => Promise<void>, iterations = 10): Promise<LatencyResult> {
53
- const samples: number[] = [];
54
-
55
- // Warmup
56
- await fn();
57
-
58
- // Actual measurements
59
- for (let i = 0; i < iterations; i++) {
60
- const timer = new Timer();
61
- timer.start();
62
- await fn();
63
- timer.stop();
64
- samples.push(timer.getElapsedMs());
65
- }
66
-
67
- return {
68
- name,
69
- description,
70
- stats: computeStats(samples),
71
- samples,
72
- };
73
- }
74
-
75
- /**
76
- * Measure Demuxer.open() latency
77
- */
78
- export async function measureDemuxerOpenLatency(inputFile?: string, iterations = 10): Promise<LatencyResult> {
79
- const input = inputFile ?? join(testDataDir, 'video.mp4');
80
-
81
- return measureLatency(
82
- 'Demuxer.open()',
83
- 'Time to open input file and parse headers',
84
- async () => {
85
- const demuxer = await Demuxer.open(input);
86
- await demuxer.close();
87
- },
88
- iterations,
89
- );
90
- }
91
-
92
- /**
93
- * Measure time to first packet
94
- */
95
- export async function measureFirstPacketLatency(inputFile?: string, iterations = 10): Promise<LatencyResult> {
96
- const input = inputFile ?? join(testDataDir, 'video.mp4');
97
-
98
- return measureLatency(
99
- 'First Packet',
100
- 'Time from open to receiving first packet',
101
- async () => {
102
- const demuxer = await Demuxer.open(input);
103
- const videoStream = demuxer.video();
104
- if (!videoStream) {
105
- await demuxer.close();
106
- throw new Error('No video stream found');
107
- }
108
-
109
- // Get first packet
110
- for await (const packet of demuxer.packets(videoStream.index)) {
111
- if (packet) break;
112
- }
113
-
114
- await demuxer.close();
115
- },
116
- iterations,
117
- );
118
- }
119
-
120
- /**
121
- * Measure time to first decoded frame
122
- */
123
- export async function measureFirstFrameLatency(inputFile?: string, iterations = 10): Promise<LatencyResult> {
124
- const input = inputFile ?? join(testDataDir, 'video.mp4');
125
-
126
- return measureLatency(
127
- 'First Frame',
128
- 'Time from open to first decoded frame',
129
- async () => {
130
- const demuxer = await Demuxer.open(input);
131
- const videoStream = demuxer.video();
132
- if (!videoStream) {
133
- await demuxer.close();
134
- throw new Error('No video stream found');
135
- }
136
-
137
- const decoder = await Decoder.create(videoStream);
138
-
139
- // Get first frame
140
- let gotFrame = false;
141
- for await (const packet of demuxer.packets(videoStream.index)) {
142
- if (!packet) continue;
143
-
144
- for await (const frame of decoder.frames(packet)) {
145
- if (frame) {
146
- gotFrame = true;
147
- break;
148
- }
149
- }
150
-
151
- if (gotFrame) break;
152
- }
153
-
154
- await demuxer.close();
155
- },
156
- iterations,
157
- );
158
- }
159
-
160
- /**
161
- * Measure time to first encoded packet
162
- */
163
- export async function measureFirstEncodedPacketLatency(inputFile?: string, iterations = 10): Promise<LatencyResult> {
164
- const input = inputFile ?? join(testDataDir, 'video.mp4');
165
-
166
- return measureLatency(
167
- 'First Encoded Packet',
168
- 'Time from open to first re-encoded packet',
169
- async () => {
170
- const demuxer = await Demuxer.open(input);
171
- const videoStream = demuxer.video();
172
- if (!videoStream) {
173
- await demuxer.close();
174
- throw new Error('No video stream found');
175
- }
176
-
177
- const decoder = await Decoder.create(videoStream);
178
- const encoder = await Encoder.create(FF_ENCODER_LIBX264, {
179
- decoder,
180
- options: { preset: 'ultrafast', tune: 'zerolatency' },
181
- });
182
-
183
- // Get first encoded packet
184
- let gotPacket = false;
185
- for await (const packet of demuxer.packets(videoStream.index)) {
186
- if (!packet) continue;
187
-
188
- for await (const frame of decoder.frames(packet)) {
189
- if (!frame) continue;
190
-
191
- for await (const encodedPacket of encoder.packets(frame)) {
192
- if (encodedPacket) {
193
- gotPacket = true;
194
- break;
195
- }
196
- }
197
-
198
- if (gotPacket) break;
199
- }
200
-
201
- if (gotPacket) break;
202
- }
203
-
204
- await demuxer.close();
205
- },
206
- iterations,
207
- );
208
- }
209
-
210
- /**
211
- * Measure end-to-end pipeline latency (first packet written to output)
212
- */
213
- export async function measurePipelineLatency(inputFile?: string, iterations = 10): Promise<LatencyResult> {
214
- const input = inputFile ?? join(testDataDir, 'video.mp4');
215
- const output = join(resultsDir, 'latency-output.mp4');
216
-
217
- return measureLatency(
218
- 'Pipeline Total',
219
- 'Time from open to first packet written to output',
220
- async () => {
221
- const demuxer = await Demuxer.open(input);
222
- const videoStream = demuxer.video();
223
- if (!videoStream) {
224
- await demuxer.close();
225
- throw new Error('No video stream found');
226
- }
227
-
228
- const decoder = await Decoder.create(videoStream);
229
- const encoder = await Encoder.create(FF_ENCODER_LIBX264, {
230
- decoder,
231
- options: { preset: 'ultrafast', tune: 'zerolatency' },
232
- });
233
-
234
- const muxer = await Muxer.open(output);
235
- const streamIndex = muxer.addStream(encoder);
236
-
237
- // Get first packet written to output
238
- let wrotePacket = false;
239
- for await (const packet of demuxer.packets(videoStream.index)) {
240
- if (!packet) continue;
241
-
242
- for await (const frame of decoder.frames(packet)) {
243
- if (!frame) continue;
244
-
245
- for await (const encodedPacket of encoder.packets(frame)) {
246
- if (encodedPacket) {
247
- await muxer.writePacket(encodedPacket, streamIndex);
248
- wrotePacket = true;
249
- break;
250
- }
251
- }
252
-
253
- if (wrotePacket) break;
254
- }
255
-
256
- if (wrotePacket) break;
257
- }
258
-
259
- await demuxer.close();
260
- await muxer.close();
261
-
262
- // Clean up the incomplete output file
263
- if (existsSync(output)) {
264
- unlinkSync(output);
265
- }
266
- },
267
- iterations,
268
- );
269
- }
270
-
271
- /**
272
- * Run all latency measurements and collect metrics
273
- */
274
- export async function measureAllLatencies(inputFile?: string, iterations = 10): Promise<LatencyMetrics> {
275
- console.log('\nā±ļø Measuring Latency Metrics\n');
276
- console.log('='.repeat(60));
277
-
278
- console.log('Measuring Demuxer.open() latency...');
279
- const demuxerOpen = await measureDemuxerOpenLatency(inputFile, iterations);
280
-
281
- console.log('Measuring First Packet latency...');
282
- const firstPacket = await measureFirstPacketLatency(inputFile, iterations);
283
-
284
- console.log('Measuring First Frame latency...');
285
- const firstFrame = await measureFirstFrameLatency(inputFile, iterations);
286
-
287
- console.log('Measuring First Encoded Packet latency...');
288
- const firstEncodedPacket = await measureFirstEncodedPacketLatency(inputFile, iterations);
289
-
290
- console.log('Measuring Pipeline Total latency...');
291
- const pipelineTotal = await measurePipelineLatency(inputFile, iterations);
292
-
293
- const metrics = {
294
- demuxerOpen,
295
- firstPacket,
296
- firstFrame,
297
- firstEncodedPacket,
298
- pipelineTotal,
299
- };
300
-
301
- // Print results
302
- printLatencyResults(metrics);
303
-
304
- return metrics;
305
- }
306
-
307
- /**
308
- * Print latency results in a formatted table
309
- */
310
- function printLatencyResults(metrics: LatencyMetrics): void {
311
- console.log('\nšŸ“Š Latency Results:\n');
312
- console.log('ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”');
313
- console.log('│ Metric │ Mean │ Min │ Max │ StdDev │');
314
- console.log('ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤');
315
-
316
- const results = [metrics.demuxerOpen, metrics.firstPacket, metrics.firstFrame, metrics.firstEncodedPacket, metrics.pipelineTotal];
317
-
318
- for (const result of results) {
319
- const name = result.name.padEnd(24);
320
- const mean = formatMs(result.stats.mean).padStart(8);
321
- const min = formatMs(result.stats.min).padStart(8);
322
- const max = formatMs(result.stats.max).padStart(8);
323
- const stdDev = formatMs(result.stats.stdDev).padStart(8);
324
-
325
- console.log(`│ ${name} │ ${mean} │ ${min} │ ${max} │ ${stdDev} │`);
326
- }
327
-
328
- console.log('ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜');
329
- console.log('');
330
-
331
- // Show breakdown
332
- console.log('Latency Breakdown:');
333
- console.log(` Open → First Packet: ${formatMs(metrics.firstPacket.stats.mean - metrics.demuxerOpen.stats.mean)}`);
334
- console.log(` First Packet → First Frame: ${formatMs(metrics.firstFrame.stats.mean - metrics.firstPacket.stats.mean)}`);
335
- console.log(` First Frame → First Encoded: ${formatMs(metrics.firstEncodedPacket.stats.mean - metrics.firstFrame.stats.mean)}`);
336
- console.log(` First Encoded → Written: ${formatMs(metrics.pipelineTotal.stats.mean - metrics.firstEncodedPacket.stats.mean)}`);
337
-
338
- console.log('\n' + '='.repeat(60));
339
- }
340
-
341
- /**
342
- * Format milliseconds
343
- */
344
- function formatMs(ms: number): string {
345
- if (ms < 1) {
346
- return `${(ms * 1000).toFixed(0)}µs`;
347
- }
348
- return `${ms.toFixed(1)}ms`;
349
- }
350
-
351
- /**
352
- * Run all latency benchmarks
353
- */
354
- export async function runAllLatencyBenchmarks(inputFile?: string): Promise<void> {
355
- console.log('\n⚔ Running Latency Benchmarks\n');
356
- console.log('='.repeat(60));
357
-
358
- await measureAllLatencies(inputFile, 10);
359
-
360
- console.log('\nLatency benchmarks completed\n');
361
- }
@@ -1,260 +0,0 @@
1
- /**
2
- * Memory Usage Benchmark Tests
3
- *
4
- * Compares memory consumption between FFmpeg CLI and node-av
5
- * during media processing operations.
6
- */
7
-
8
- import { dirname, join, resolve } from 'node:path';
9
- import { fileURLToPath } from 'node:url';
10
-
11
- import { Decoder, Demuxer, Encoder, Muxer, pipeline } from '../../src/api/index.js';
12
- import { FF_ENCODER_LIBX264 } from '../../src/constants/encoders.js';
13
- import { runner } from '../runner.js';
14
- import { FFmpegArgs } from '../utils/ffmpeg-cli.js';
15
- import { MemorySampler } from '../utils/measure.js';
16
-
17
- import type { BenchmarkConfig } from '../runner.js';
18
-
19
- const __filename = fileURLToPath(import.meta.url);
20
- const __dirname = dirname(__filename);
21
-
22
- // Default paths
23
- const testDataDir = resolve(__dirname, '../../testdata');
24
- const resultsDir = resolve(__dirname, '../results');
25
-
26
- /**
27
- * Configuration for memory-focused benchmarks
28
- */
29
- const memoryConfig = {
30
- iterations: 3,
31
- warmupIterations: 1,
32
- };
33
-
34
- /**
35
- * Memory usage during H.264 transcode
36
- */
37
- export async function benchmarkMemoryH264Transcode(inputFile?: string, outputFile?: string): Promise<void> {
38
- const input = inputFile ?? join(testDataDir, 'video.mp4');
39
- const output = outputFile ?? join(resultsDir, 'memory-h264-output.mp4');
40
-
41
- const config: BenchmarkConfig = {
42
- name: 'Memory: H.264 Transcode',
43
- description: 'Peak memory usage during H.264 software transcoding',
44
- category: 'memory',
45
- inputFile: input,
46
- outputFile: output,
47
- ...memoryConfig,
48
- };
49
-
50
- const ffmpegOptions = {
51
- input,
52
- output,
53
- args: FFmpegArgs.swH264(23),
54
- };
55
-
56
- const nodeAVFn = async () => {
57
- await using demuxer = await Demuxer.open(input);
58
- const videoStream = demuxer.video();
59
- if (!videoStream) throw new Error('No video stream found');
60
-
61
- const decoder = await Decoder.create(videoStream);
62
- const encoder = await Encoder.create(FF_ENCODER_LIBX264, {
63
- decoder,
64
- options: { preset: 'medium', crf: 23 },
65
- });
66
-
67
- await using muxer = await Muxer.open(output);
68
-
69
- const control = pipeline(demuxer, decoder, encoder, muxer);
70
- await control.completion;
71
-
72
- // Get frame count from codec context
73
- const ctx = encoder.getCodecContext();
74
- return { framesProcessed: ctx?.frameNumber };
75
- };
76
-
77
- await runner.runBenchmark(config, ffmpegOptions, nodeAVFn);
78
- }
79
-
80
- /**
81
- * Memory usage during stream copy
82
- */
83
- export async function benchmarkMemoryStreamCopy(inputFile?: string, outputFile?: string): Promise<void> {
84
- const input = inputFile ?? join(testDataDir, 'video.mp4');
85
- const output = outputFile ?? join(resultsDir, 'memory-copy-output.mp4');
86
-
87
- const config: BenchmarkConfig = {
88
- name: 'Memory: Stream Copy',
89
- description: 'Peak memory usage during stream copy (remux)',
90
- category: 'memory',
91
- inputFile: input,
92
- outputFile: output,
93
- ...memoryConfig,
94
- };
95
-
96
- const ffmpegOptions = {
97
- input,
98
- output,
99
- args: FFmpegArgs.streamCopy(),
100
- };
101
-
102
- const nodeAVFn = async () => {
103
- await using demuxer = await Demuxer.open(input);
104
- await using muxer = await Muxer.open(output);
105
-
106
- const control = pipeline(demuxer, muxer);
107
- await control.completion;
108
-
109
- return {};
110
- };
111
-
112
- await runner.runBenchmark(config, ffmpegOptions, nodeAVFn);
113
- }
114
-
115
- /**
116
- * Detailed memory profiling for node-av
117
- * This provides more granular memory analysis beyond peak usage
118
- */
119
- export async function profileNodeAVMemory(inputFile?: string): Promise<void> {
120
- const input = inputFile ?? join(testDataDir, 'video.mp4');
121
-
122
- console.log('\nšŸ“Š Detailed Memory Profile for node-av\n');
123
- console.log('='.repeat(60));
124
-
125
- // Force GC before starting
126
- if (global.gc) {
127
- global.gc();
128
- }
129
-
130
- const sampler = new MemorySampler();
131
- sampler.start(50); // Sample every 50ms
132
-
133
- const baselineMemory = process.memoryUsage();
134
- console.log('Baseline Memory:');
135
- console.log(` RSS: ${formatBytes(baselineMemory.rss)}`);
136
- console.log(` Heap Used: ${formatBytes(baselineMemory.heapUsed)}`);
137
- console.log(` Heap Total: ${formatBytes(baselineMemory.heapTotal)}`);
138
- console.log(` External: ${formatBytes(baselineMemory.external)}`);
139
- console.log('');
140
-
141
- // Open demuxer
142
- console.log('Opening demuxer...');
143
- const demuxer = await Demuxer.open(input);
144
- const afterDemuxer = process.memoryUsage();
145
- console.log(` After Demuxer: +${formatBytes(afterDemuxer.rss - baselineMemory.rss)} RSS`);
146
-
147
- // Create decoder
148
- const videoStream = demuxer.video();
149
- if (!videoStream) {
150
- await demuxer.close();
151
- throw new Error('No video stream found');
152
- }
153
-
154
- console.log('Creating decoder...');
155
- const decoder = await Decoder.create(videoStream);
156
- const afterDecoder = process.memoryUsage();
157
- console.log(` After Decoder: +${formatBytes(afterDecoder.rss - afterDemuxer.rss)} RSS`);
158
-
159
- // Create encoder
160
- console.log('Creating encoder...');
161
- const encoder = await Encoder.create(FF_ENCODER_LIBX264, {
162
- decoder,
163
- options: { preset: 'ultrafast', crf: 23 },
164
- });
165
- const afterEncoder = process.memoryUsage();
166
- console.log(` After Encoder: +${formatBytes(afterEncoder.rss - afterDecoder.rss)} RSS`);
167
-
168
- // Process frames
169
- console.log('Processing frames...');
170
- let frameCount = 0;
171
- let maxMemoryDuringProcess = process.memoryUsage().rss;
172
-
173
- for await (const packet of demuxer.packets(videoStream.index)) {
174
- if (!packet) continue;
175
-
176
- for await (const frame of decoder.frames(packet)) {
177
- if (!frame) continue;
178
- frameCount++;
179
-
180
- for await (const _ of encoder.packets(frame)) {
181
- // Just encode and discard
182
- }
183
-
184
- // Sample memory periodically
185
- if (frameCount % 30 === 0) {
186
- const currentMem = process.memoryUsage().rss;
187
- if (currentMem > maxMemoryDuringProcess) {
188
- maxMemoryDuringProcess = currentMem;
189
- }
190
- }
191
- }
192
- }
193
-
194
- // Flush
195
- for await (const frame of decoder.frames(null)) {
196
- if (!frame) continue;
197
- for await (const _ of encoder.packets(frame)) {
198
- // Just encode and discard
199
- }
200
- }
201
- for await (const _ of encoder.packets(null)) {
202
- // Just encode and discard
203
- }
204
-
205
- const { samples, peakMemory } = sampler.stop();
206
-
207
- console.log('\nProcessing Complete:');
208
- console.log(` Frames: ${frameCount}`);
209
- console.log(` Peak Memory: ${formatBytes(peakMemory)}`);
210
- console.log(` Memory Samples: ${samples.length}`);
211
- console.log(` Memory Growth: ${formatBytes(peakMemory - baselineMemory.rss)}`);
212
-
213
- // Cleanup
214
- await demuxer.close();
215
-
216
- // Force GC and check final memory
217
- if (global.gc) {
218
- global.gc();
219
- await new Promise((resolve) => setTimeout(resolve, 100));
220
- global.gc();
221
- }
222
-
223
- const finalMemory = process.memoryUsage();
224
- console.log('\nAfter Cleanup:');
225
- console.log(` RSS: ${formatBytes(finalMemory.rss)}`);
226
- console.log(` Retained: ${formatBytes(finalMemory.rss - baselineMemory.rss)}`);
227
-
228
- console.log('\n' + '='.repeat(60));
229
- }
230
-
231
- /**
232
- * Format bytes to human readable string
233
- */
234
- function formatBytes(bytes: number): string {
235
- const units = ['B', 'KB', 'MB', 'GB'];
236
- let value = bytes;
237
- let unitIndex = 0;
238
-
239
- while (value >= 1024 && unitIndex < units.length - 1) {
240
- value /= 1024;
241
- unitIndex++;
242
- }
243
-
244
- return `${value.toFixed(1)} ${units[unitIndex]}`;
245
- }
246
-
247
- /**
248
- * Run all memory benchmarks
249
- */
250
- export async function runAllMemoryBenchmarks(inputFile?: string): Promise<void> {
251
- console.log('\nšŸ’¾ Running Memory Usage Benchmarks\n');
252
- console.log('='.repeat(60));
253
-
254
- await benchmarkMemoryH264Transcode(inputFile);
255
- await benchmarkMemoryStreamCopy(inputFile);
256
- await profileNodeAVMemory(inputFile);
257
-
258
- console.log('\n' + '='.repeat(60));
259
- console.log('Memory benchmarks completed\n');
260
- }