node-av 5.0.4 → 5.1.0
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/BENCHMARK.md +113 -0
- package/README.md +42 -0
- package/benchmarks/cases/latency.ts +361 -0
- package/benchmarks/cases/memory.ts +260 -0
- package/benchmarks/cases/transcode.ts +271 -0
- package/benchmarks/index.ts +264 -0
- package/benchmarks/regen-report.ts +22 -0
- package/benchmarks/results/.gitkeep +2 -0
- package/benchmarks/runner.ts +247 -0
- package/benchmarks/utils/ffmpeg-cli.ts +363 -0
- package/benchmarks/utils/measure.ts +275 -0
- package/benchmarks/utils/report.ts +405 -0
- package/dist/api/bitstream-filter.d.ts +190 -74
- package/dist/api/bitstream-filter.js +276 -222
- package/dist/api/bitstream-filter.js.map +1 -1
- package/dist/api/decoder.d.ts +23 -29
- package/dist/api/decoder.js +37 -47
- package/dist/api/decoder.js.map +1 -1
- package/dist/api/encoder.d.ts +4 -4
- package/dist/api/encoder.js +16 -20
- package/dist/api/encoder.js.map +1 -1
- package/dist/api/filter-complex.js +7 -3
- package/dist/api/filter-complex.js.map +1 -1
- package/dist/api/filter.d.ts +4 -4
- package/dist/api/filter.js +18 -16
- package/dist/api/filter.js.map +1 -1
- package/dist/api/hardware.d.ts +17 -0
- package/dist/api/hardware.js +70 -23
- package/dist/api/hardware.js.map +1 -1
- package/dist/api/muxer.js +8 -4
- package/dist/api/muxer.js.map +1 -1
- package/dist/api/types.d.ts +29 -9
- package/dist/constants/constants.d.ts +6 -3
- package/dist/constants/constants.js +5 -5
- package/dist/constants/constants.js.map +1 -1
- package/dist/lib/codec-context.d.ts +5 -3
- package/dist/lib/codec-context.js +2 -0
- package/dist/lib/codec-context.js.map +1 -1
- package/dist/lib/native-types.d.ts +2 -2
- package/package.json +14 -12
package/BENCHMARK.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# node-av Benchmark Results
|
|
2
|
+
|
|
3
|
+
> Generated: 2026-01-27T18:12:31.045Z
|
|
4
|
+
|
|
5
|
+
## System Information
|
|
6
|
+
|
|
7
|
+
| Property | Value |
|
|
8
|
+
|----------|-------|
|
|
9
|
+
| **OS** | darwin 25.1.0 |
|
|
10
|
+
| **Architecture** | arm64 |
|
|
11
|
+
| **CPU** | Apple M3 Max |
|
|
12
|
+
| **CPU Cores** | 16 |
|
|
13
|
+
| **RAM** | 48.0 GB |
|
|
14
|
+
| **GPU** | Apple M3 Max |
|
|
15
|
+
| **Node.js** | v24.8.0 |
|
|
16
|
+
| **FFmpeg** | 8.0-Jellyfin |
|
|
17
|
+
| **node-av** | 5.0.4 |
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Test Inputs
|
|
21
|
+
|
|
22
|
+
| File | Codec | Resolution | FPS | Duration |
|
|
23
|
+
|------|-------|------------|-----|----------|
|
|
24
|
+
| bbb-4k-av1.mp4 | av1 | 3840x2160 | 60 | 30.0s |
|
|
25
|
+
| bbb-4k-h264.mp4 | h264 | 3840x2160 | 60 | 30.0s |
|
|
26
|
+
| bbb-4k-hevc.mp4 | hevc | 3840x2160 | 60 | 30.0s |
|
|
27
|
+
| bbb-4k-vp9.webm | vp9 | 3840x2160 | 60 | 30.0s |
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Transcode Speed
|
|
31
|
+
|
|
32
|
+
### Input: bbb-4k-av1.mp4
|
|
33
|
+
|
|
34
|
+
| Test | FFmpeg CLI (FPS) | node-av (FPS) | FFmpeg CLI (Time) | node-av (Time) | Diff |
|
|
35
|
+
|------|------------------|---------------|-------------------|----------------|------|
|
|
36
|
+
| SW H.264 Transcode | 95.1 fps | 94.5 fps | 18.93s | 19.06s | -0.6% |
|
|
37
|
+
| SW H.265 Transcode | 38.9 fps | 40.1 fps | 46.31s | 44.93s | +3.2% |
|
|
38
|
+
| HW H.264 Transcode | 54.6 fps | 54.9 fps | 32.98s | 32.79s | +0.6% |
|
|
39
|
+
| Stream Copy (Remux) | 50307.7 fps | 29472.5 fps | 36ms | 109ms | -41.4% |
|
|
40
|
+
|
|
41
|
+
### Input: bbb-4k-h264.mp4
|
|
42
|
+
|
|
43
|
+
| Test | FFmpeg CLI (FPS) | node-av (FPS) | FFmpeg CLI (Time) | node-av (Time) | Diff |
|
|
44
|
+
|------|------------------|---------------|-------------------|----------------|------|
|
|
45
|
+
| SW H.264 Transcode | 96.5 fps | 97.2 fps | 18.65s | 18.52s | +0.7% |
|
|
46
|
+
| SW H.265 Transcode | 40.0 fps | 39.9 fps | 45.00s | 45.12s | -0.2% |
|
|
47
|
+
| HW H.264 Transcode | 54.6 fps | 54.9 fps | 32.98s | 32.83s | +0.5% |
|
|
48
|
+
| Stream Copy (Remux) | 38235.1 fps | 30884.8 fps | 48ms | 104ms | -19.2% |
|
|
49
|
+
|
|
50
|
+
### Input: bbb-4k-hevc.mp4
|
|
51
|
+
|
|
52
|
+
| Test | FFmpeg CLI (FPS) | node-av (FPS) | FFmpeg CLI (Time) | node-av (Time) | Diff |
|
|
53
|
+
|------|------------------|---------------|-------------------|----------------|------|
|
|
54
|
+
| SW H.264 Transcode | 96.3 fps | 95.9 fps | 18.70s | 18.78s | -0.4% |
|
|
55
|
+
| SW H.265 Transcode | 41.4 fps | 41.8 fps | 43.49s | 43.06s | +1.0% |
|
|
56
|
+
| HW H.264 Transcode | 54.5 fps | 54.9 fps | 33.00s | 32.82s | +0.6% |
|
|
57
|
+
| Stream Copy (Remux) | 55110.3 fps | 30395.4 fps | 33ms | 106ms | -44.8% |
|
|
58
|
+
|
|
59
|
+
### Input: bbb-4k-vp9.webm
|
|
60
|
+
|
|
61
|
+
| Test | FFmpeg CLI (FPS) | node-av (FPS) | FFmpeg CLI (Time) | node-av (Time) | Diff |
|
|
62
|
+
|------|------------------|---------------|-------------------|----------------|------|
|
|
63
|
+
| SW H.264 Transcode | 96.5 fps | 96.1 fps | 18.66s | 18.75s | -0.4% |
|
|
64
|
+
| SW H.265 Transcode | 41.6 fps | 42.0 fps | 43.23s | 42.90s | +0.8% |
|
|
65
|
+
| HW H.264 Transcode | 54.6 fps | 54.8 fps | 32.98s | 32.85s | +0.4% |
|
|
66
|
+
| Stream Copy (Remux) | 48635.3 fps | 31617.3 fps | 37ms | 105ms | -35.0% |
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
## Memory Usage
|
|
70
|
+
|
|
71
|
+
### Input: bbb-4k-av1.mp4
|
|
72
|
+
|
|
73
|
+
| Test | FFmpeg CLI Peak | node-av Peak | Difference |
|
|
74
|
+
|------|----------------|--------------|------------|
|
|
75
|
+
| Memory: H.264 Transcode | 3.5 GB | 3.5 GB | -2.0% |
|
|
76
|
+
| Memory: Stream Copy | 19.9 MB | 1.1 MB | -94.6% |
|
|
77
|
+
|
|
78
|
+
### Input: bbb-4k-h264.mp4
|
|
79
|
+
|
|
80
|
+
| Test | FFmpeg CLI Peak | node-av Peak | Difference |
|
|
81
|
+
|------|----------------|--------------|------------|
|
|
82
|
+
| Memory: H.264 Transcode | 3.7 GB | 3.4 GB | -6.0% |
|
|
83
|
+
| Memory: Stream Copy | 40.7 MB | 1.9 MB | -95.2% |
|
|
84
|
+
|
|
85
|
+
### Input: bbb-4k-hevc.mp4
|
|
86
|
+
|
|
87
|
+
| Test | FFmpeg CLI Peak | node-av Peak | Difference |
|
|
88
|
+
|------|----------------|--------------|------------|
|
|
89
|
+
| Memory: H.264 Transcode | 3.8 GB | 3.5 GB | -5.4% |
|
|
90
|
+
| Memory: Stream Copy | 20.5 MB | 320.0 KB | -98.5% |
|
|
91
|
+
|
|
92
|
+
### Input: bbb-4k-vp9.webm
|
|
93
|
+
|
|
94
|
+
| Test | FFmpeg CLI Peak | node-av Peak | Difference |
|
|
95
|
+
|------|----------------|--------------|------------|
|
|
96
|
+
| Memory: H.264 Transcode | 3.5 GB | 3.3 GB | -4.7% |
|
|
97
|
+
| Memory: Stream Copy | 30.6 MB | 378.7 KB | -98.8% |
|
|
98
|
+
|
|
99
|
+
*Note: FFmpeg CLI memory is measured via `/usr/bin/time` (macOS: `-l`, Linux: `-v`).
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Latency
|
|
103
|
+
|
|
104
|
+
| Metric | Mean | Min | Max | StdDev |
|
|
105
|
+
|--------|------|-----|-----|--------|
|
|
106
|
+
| Demuxer Open | 466µs | 430µs | 561µs | 41µs |
|
|
107
|
+
| First Packet | 530µs | 491µs | 575µs | 30µs |
|
|
108
|
+
| First Frame | 11.7ms | 7.2ms | 15.2ms | 2.7ms |
|
|
109
|
+
| First Encoded Packet | 25.2ms | 23.9ms | 26.9ms | 856µs |
|
|
110
|
+
| Pipeline Total | 25.8ms | 22.5ms | 27.3ms | 1.4ms |
|
|
111
|
+
|
|
112
|
+
*Note: Each metric is measured independently. "First Encoded Packet" uses default encoder settings while "Pipeline Total" uses `tune=zerolatency` for low-latency output.*
|
|
113
|
+
|
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ Native Node.js bindings for FFmpeg with full TypeScript support. Provides direct
|
|
|
35
35
|
- [Resource Management](#resource-management)
|
|
36
36
|
- [FFmpeg Binary Access](#ffmpeg-binary-access)
|
|
37
37
|
- [Performance](#performance)
|
|
38
|
+
- [Benchmarks](#benchmarks)
|
|
38
39
|
- [Sync vs Async Operations](#sync-vs-async-operations)
|
|
39
40
|
- [Memory Safety Considerations](#memory-safety-considerations)
|
|
40
41
|
- [Examples](#examples)
|
|
@@ -426,6 +427,25 @@ The FFmpeg binary is automatically downloaded during installation from GitHub re
|
|
|
426
427
|
|
|
427
428
|
NodeAV executes all media operations directly through FFmpeg's native C libraries. The Node.js bindings add minimal overhead - mostly just the JavaScript-to-C boundary crossings. During typical operations like transcoding or filtering, most processing time is spent in FFmpeg's optimized C code.
|
|
428
429
|
|
|
430
|
+
### Benchmarks
|
|
431
|
+
|
|
432
|
+
Performance comparison with FFmpeg CLI (4K 60fps, 30s test files on Apple M3 Max):
|
|
433
|
+
|
|
434
|
+
| Operation | FFmpeg CLI (FPS) | node-av (FPS) | FFmpeg CLI (Time) | node-av (Time) | Diff |
|
|
435
|
+
|-----------|------------------|---------------|-------------------|----------------|------|
|
|
436
|
+
| SW H.264 Transcode | 96 fps | 96 fps | 18.7s | 18.7s | ≈0% |
|
|
437
|
+
| SW H.265 Transcode | 40 fps | 41 fps | 44.5s | 43.7s | **+1.5%** |
|
|
438
|
+
| HW H.264 Transcode | 55 fps | 55 fps | 33.0s | 32.8s | **+0.5%** |
|
|
439
|
+
| Stream Copy (Remux) | 48k fps | 31k fps | 38ms | 106ms | -35% |
|
|
440
|
+
|
|
441
|
+
**Memory Usage:**
|
|
442
|
+
| Operation | FFmpeg CLI | node-av | Difference |
|
|
443
|
+
|-----------|-----------|---------|------------|
|
|
444
|
+
| H.264 Transcode (4K) | 3.6 GB | 3.4 GB | **-5%** |
|
|
445
|
+
| Stream Copy | 28 MB | 1 MB | **-96%** |
|
|
446
|
+
|
|
447
|
+
📊 **[Full benchmark results](https://github.com/seydx/node-av/tree/main/BENCHMARK.md)**
|
|
448
|
+
|
|
429
449
|
### Sync vs Async Operations
|
|
430
450
|
|
|
431
451
|
Every async method in NodeAV has a corresponding synchronous variant with the `Sync` suffix:
|
|
@@ -473,6 +493,7 @@ NodeAV provides direct bindings to FFmpeg's C APIs, which work with raw memory p
|
|
|
473
493
|
| `api-whisper-transcribe` | | | [✓](https://github.com/seydx/node-av/tree/main/examples/api-whisper-transcribe.ts) |
|
|
474
494
|
| `frame-utils` | | [✓](https://github.com/seydx/node-av/tree/main/examples/frame-utils.ts) | |
|
|
475
495
|
| `avio-read-callback` | [✓](https://github.com/FFmpeg/FFmpeg/tree/master/doc/examples/avio_read_callback.c) | [✓](https://github.com/seydx/node-av/tree/main/examples/avio-read-callback.ts) | |
|
|
496
|
+
| `avio-async-read-callback` | | [✓](https://github.com/seydx/node-av/tree/main/examples/avio-async-read-callback.ts) | |
|
|
476
497
|
| `decode-audio` | [✓](https://github.com/FFmpeg/FFmpeg/tree/master/doc/examples/decode_audio.c) | [✓](https://github.com/seydx/node-av/tree/main/examples/decode-audio.ts) | |
|
|
477
498
|
| `decode-filter-audio` | [✓](https://github.com/FFmpeg/FFmpeg/tree/master/doc/examples/decode_filter_audio.c) | [✓](https://github.com/seydx/node-av/tree/main/examples/decode-filter-audio.ts) | |
|
|
478
499
|
| `decode-filter-video` | [✓](https://github.com/FFmpeg/FFmpeg/tree/master/doc/examples/decode_filter_video.c) | [✓](https://github.com/seydx/node-av/tree/main/examples/decode-filter-video.ts) | |
|
|
@@ -562,3 +583,24 @@ For issues and questions, please use the GitHub issue tracker.
|
|
|
562
583
|
- [FFmpeg Doxygen](https://ffmpeg.org/doxygen/trunk/)
|
|
563
584
|
- [Jellyfin FFmpeg](https://github.com/seydx/jellyfin-ffmpeg)
|
|
564
585
|
- [FFmpeg MSVC](https://github.com/seydx/ffmpeg-msvc-prebuilt)
|
|
586
|
+
|
|
587
|
+
## Star History
|
|
588
|
+
|
|
589
|
+
<picture>
|
|
590
|
+
<source
|
|
591
|
+
media="(prefers-color-scheme: dark)"
|
|
592
|
+
srcset="
|
|
593
|
+
https://api.star-history.com/svg?repos=seydx/node-av&type=Date&theme=dark
|
|
594
|
+
"
|
|
595
|
+
/>
|
|
596
|
+
<source
|
|
597
|
+
media="(prefers-color-scheme: light)"
|
|
598
|
+
srcset="
|
|
599
|
+
https://api.star-history.com/svg?repos=seydx/node-av&type=Date
|
|
600
|
+
"
|
|
601
|
+
/>
|
|
602
|
+
<img
|
|
603
|
+
alt="Star History Chart"
|
|
604
|
+
src="https://api.star-history.com/svg?repos=seydx/node-av&type=Date"
|
|
605
|
+
/>
|
|
606
|
+
</picture>
|
|
@@ -0,0 +1,361 @@
|
|
|
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
|
+
}
|