node-av-lgpl 0.0.0 → 7.1.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.
- package/LICENSE.md +22 -0
- package/README.md +711 -0
- package/dist/api/audio-frame-buffer.d.ts +636 -0
- package/dist/api/audio-frame-buffer.js +1027 -0
- package/dist/api/audio-frame-buffer.js.map +1 -0
- package/dist/api/bitstream-filter.d.ts +938 -0
- package/dist/api/bitstream-filter.js +1427 -0
- package/dist/api/bitstream-filter.js.map +1 -0
- package/dist/api/constants.d.ts +44 -0
- package/dist/api/constants.js +45 -0
- package/dist/api/constants.js.map +1 -0
- package/dist/api/data/test_av1.ivf +0 -0
- package/dist/api/data/test_h264.h264 +0 -0
- package/dist/api/data/test_hevc.h265 +0 -0
- package/dist/api/data/test_mjpeg.mjpeg +0 -0
- package/dist/api/data/test_vp8.ivf +0 -0
- package/dist/api/data/test_vp9.ivf +0 -0
- package/dist/api/decoder.d.ts +1413 -0
- package/dist/api/decoder.js +2367 -0
- package/dist/api/decoder.js.map +1 -0
- package/dist/api/demuxer.d.ts +1295 -0
- package/dist/api/demuxer.js +2344 -0
- package/dist/api/demuxer.js.map +1 -0
- package/dist/api/device.d.ts +589 -0
- package/dist/api/device.js +964 -0
- package/dist/api/device.js.map +1 -0
- package/dist/api/encoder-pool.d.ts +262 -0
- package/dist/api/encoder-pool.js +397 -0
- package/dist/api/encoder-pool.js.map +1 -0
- package/dist/api/encoder.d.ts +1509 -0
- package/dist/api/encoder.js +2619 -0
- package/dist/api/encoder.js.map +1 -0
- package/dist/api/filter-complex.d.ts +828 -0
- package/dist/api/filter-complex.js +1661 -0
- package/dist/api/filter-complex.js.map +1 -0
- package/dist/api/filter-presets.d.ts +800 -0
- package/dist/api/filter-presets.js +1554 -0
- package/dist/api/filter-presets.js.map +1 -0
- package/dist/api/filter.d.ts +1339 -0
- package/dist/api/filter.js +2075 -0
- package/dist/api/filter.js.map +1 -0
- package/dist/api/fmp4-stream.d.ts +797 -0
- package/dist/api/fmp4-stream.js +1428 -0
- package/dist/api/fmp4-stream.js.map +1 -0
- package/dist/api/hardware.d.ts +677 -0
- package/dist/api/hardware.js +1300 -0
- package/dist/api/hardware.js.map +1 -0
- package/dist/api/index.d.ts +21 -0
- package/dist/api/index.js +38 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/io-stream.d.ts +308 -0
- package/dist/api/io-stream.js +358 -0
- package/dist/api/io-stream.js.map +1 -0
- package/dist/api/muxer.d.ts +1311 -0
- package/dist/api/muxer.js +2672 -0
- package/dist/api/muxer.js.map +1 -0
- package/dist/api/pipeline.d.ts +801 -0
- package/dist/api/pipeline.js +1344 -0
- package/dist/api/pipeline.js.map +1 -0
- package/dist/api/probe.d.ts +128 -0
- package/dist/api/probe.js +227 -0
- package/dist/api/probe.js.map +1 -0
- package/dist/api/rtp-stream.d.ts +497 -0
- package/dist/api/rtp-stream.js +1131 -0
- package/dist/api/rtp-stream.js.map +1 -0
- package/dist/api/scaler.d.ts +561 -0
- package/dist/api/scaler.js +918 -0
- package/dist/api/scaler.js.map +1 -0
- package/dist/api/types.d.ts +17 -0
- package/dist/api/types.js +2 -0
- package/dist/api/types.js.map +1 -0
- package/dist/api/utilities/async-queue.d.ts +149 -0
- package/dist/api/utilities/async-queue.js +254 -0
- package/dist/api/utilities/async-queue.js.map +1 -0
- package/dist/api/utilities/audio-sample.d.ts +117 -0
- package/dist/api/utilities/audio-sample.js +112 -0
- package/dist/api/utilities/audio-sample.js.map +1 -0
- package/dist/api/utilities/audio-transcode.d.ts +20 -0
- package/dist/api/utilities/audio-transcode.js +27 -0
- package/dist/api/utilities/audio-transcode.js.map +1 -0
- package/dist/api/utilities/channel-layout.d.ts +76 -0
- package/dist/api/utilities/channel-layout.js +80 -0
- package/dist/api/utilities/channel-layout.js.map +1 -0
- package/dist/api/utilities/codec-format.d.ts +87 -0
- package/dist/api/utilities/codec-format.js +117 -0
- package/dist/api/utilities/codec-format.js.map +1 -0
- package/dist/api/utilities/context-options.d.ts +25 -0
- package/dist/api/utilities/context-options.js +23 -0
- package/dist/api/utilities/context-options.js.map +1 -0
- package/dist/api/utilities/electron-shared-texture.d.ts +368 -0
- package/dist/api/utilities/electron-shared-texture.js +540 -0
- package/dist/api/utilities/electron-shared-texture.js.map +1 -0
- package/dist/api/utilities/image.d.ts +207 -0
- package/dist/api/utilities/image.js +213 -0
- package/dist/api/utilities/image.js.map +1 -0
- package/dist/api/utilities/index.d.ts +14 -0
- package/dist/api/utilities/index.js +27 -0
- package/dist/api/utilities/index.js.map +1 -0
- package/dist/api/utilities/live-decoding.d.ts +15 -0
- package/dist/api/utilities/live-decoding.js +32 -0
- package/dist/api/utilities/live-decoding.js.map +1 -0
- package/dist/api/utilities/media-type.d.ts +49 -0
- package/dist/api/utilities/media-type.js +53 -0
- package/dist/api/utilities/media-type.js.map +1 -0
- package/dist/api/utilities/packet-age-watchdog.d.ts +198 -0
- package/dist/api/utilities/packet-age-watchdog.js +594 -0
- package/dist/api/utilities/packet-age-watchdog.js.map +1 -0
- package/dist/api/utilities/pixel-format.d.ts +89 -0
- package/dist/api/utilities/pixel-format.js +97 -0
- package/dist/api/utilities/pixel-format.js.map +1 -0
- package/dist/api/utilities/sample-format.d.ts +129 -0
- package/dist/api/utilities/sample-format.js +141 -0
- package/dist/api/utilities/sample-format.js.map +1 -0
- package/dist/api/utilities/scale-size.d.ts +51 -0
- package/dist/api/utilities/scale-size.js +55 -0
- package/dist/api/utilities/scale-size.js.map +1 -0
- package/dist/api/utilities/scheduler.d.ts +138 -0
- package/dist/api/utilities/scheduler.js +98 -0
- package/dist/api/utilities/scheduler.js.map +1 -0
- package/dist/api/utilities/streaming.d.ts +186 -0
- package/dist/api/utilities/streaming.js +309 -0
- package/dist/api/utilities/streaming.js.map +1 -0
- package/dist/api/utilities/timestamp.d.ts +193 -0
- package/dist/api/utilities/timestamp.js +206 -0
- package/dist/api/utilities/timestamp.js.map +1 -0
- package/dist/api/utilities/video-transcode.d.ts +218 -0
- package/dist/api/utilities/video-transcode.js +624 -0
- package/dist/api/utilities/video-transcode.js.map +1 -0
- package/dist/api/utilities/whisper-model.d.ts +330 -0
- package/dist/api/utilities/whisper-model.js +587 -0
- package/dist/api/utilities/whisper-model.js.map +1 -0
- package/dist/api/utils.d.ts +19 -0
- package/dist/api/utils.js +39 -0
- package/dist/api/utils.js.map +1 -0
- package/dist/api/webrtc-stream.d.ts +294 -0
- package/dist/api/webrtc-stream.js +546 -0
- package/dist/api/webrtc-stream.js.map +1 -0
- package/dist/api/whisper.d.ts +326 -0
- package/dist/api/whisper.js +402 -0
- package/dist/api/whisper.js.map +1 -0
- package/dist/constants/bsf-options.d.ts +364 -0
- package/dist/constants/bsf-options.js +7 -0
- package/dist/constants/bsf-options.js.map +1 -0
- package/dist/constants/channel-layouts.d.ts +53 -0
- package/dist/constants/channel-layouts.js +57 -0
- package/dist/constants/channel-layouts.js.map +1 -0
- package/dist/constants/constants.d.ts +2435 -0
- package/dist/constants/constants.js +1989 -0
- package/dist/constants/constants.js.map +1 -0
- package/dist/constants/decoders.d.ts +651 -0
- package/dist/constants/decoders.js +641 -0
- package/dist/constants/decoders.js.map +1 -0
- package/dist/constants/encoders.d.ts +314 -0
- package/dist/constants/encoders.js +309 -0
- package/dist/constants/encoders.js.map +1 -0
- package/dist/constants/filter-options.d.ts +12099 -0
- package/dist/constants/filter-options.js +7 -0
- package/dist/constants/filter-options.js.map +1 -0
- package/dist/constants/format-options.d.ts +4319 -0
- package/dist/constants/format-options.js +7 -0
- package/dist/constants/format-options.js.map +1 -0
- package/dist/constants/formats.d.ts +18 -0
- package/dist/constants/formats.js +7 -0
- package/dist/constants/formats.js.map +1 -0
- package/dist/constants/hardware.d.ts +26 -0
- package/dist/constants/hardware.js +27 -0
- package/dist/constants/hardware.js.map +1 -0
- package/dist/constants/index.d.ts +10 -0
- package/dist/constants/index.js +6 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/constants/options.d.ts +6217 -0
- package/dist/constants/options.js +7 -0
- package/dist/constants/options.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +101 -0
- package/dist/ffmpeg/index.js +161 -0
- package/dist/ffmpeg/index.js.map +1 -0
- package/dist/ffmpeg/version.d.ts +6 -0
- package/dist/ffmpeg/version.js +7 -0
- package/dist/ffmpeg/version.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/audio-fifo.d.ts +399 -0
- package/dist/lib/audio-fifo.js +431 -0
- package/dist/lib/audio-fifo.js.map +1 -0
- package/dist/lib/binding.d.ts +242 -0
- package/dist/lib/binding.js +102 -0
- package/dist/lib/binding.js.map +1 -0
- package/dist/lib/bitstream-filter-context.d.ts +381 -0
- package/dist/lib/bitstream-filter-context.js +443 -0
- package/dist/lib/bitstream-filter-context.js.map +1 -0
- package/dist/lib/bitstream-filter.d.ts +140 -0
- package/dist/lib/bitstream-filter.js +154 -0
- package/dist/lib/bitstream-filter.js.map +1 -0
- package/dist/lib/codec-context.d.ts +1069 -0
- package/dist/lib/codec-context.js +1336 -0
- package/dist/lib/codec-context.js.map +1 -0
- package/dist/lib/codec-parameters.d.ts +664 -0
- package/dist/lib/codec-parameters.js +813 -0
- package/dist/lib/codec-parameters.js.map +1 -0
- package/dist/lib/codec-parser.d.ts +201 -0
- package/dist/lib/codec-parser.js +213 -0
- package/dist/lib/codec-parser.js.map +1 -0
- package/dist/lib/codec.d.ts +617 -0
- package/dist/lib/codec.js +746 -0
- package/dist/lib/codec.js.map +1 -0
- package/dist/lib/device.d.ts +294 -0
- package/dist/lib/device.js +326 -0
- package/dist/lib/device.js.map +1 -0
- package/dist/lib/dictionary.d.ts +333 -0
- package/dist/lib/dictionary.js +372 -0
- package/dist/lib/dictionary.js.map +1 -0
- package/dist/lib/error.d.ts +311 -0
- package/dist/lib/error.js +395 -0
- package/dist/lib/error.js.map +1 -0
- package/dist/lib/fifo.d.ts +416 -0
- package/dist/lib/fifo.js +453 -0
- package/dist/lib/fifo.js.map +1 -0
- package/dist/lib/filter-context.d.ts +714 -0
- package/dist/lib/filter-context.js +783 -0
- package/dist/lib/filter-context.js.map +1 -0
- package/dist/lib/filter-graph-segment.d.ts +161 -0
- package/dist/lib/filter-graph-segment.js +172 -0
- package/dist/lib/filter-graph-segment.js.map +1 -0
- package/dist/lib/filter-graph.d.ts +641 -0
- package/dist/lib/filter-graph.js +704 -0
- package/dist/lib/filter-graph.js.map +1 -0
- package/dist/lib/filter-inout.d.ts +202 -0
- package/dist/lib/filter-inout.js +261 -0
- package/dist/lib/filter-inout.js.map +1 -0
- package/dist/lib/filter.d.ts +243 -0
- package/dist/lib/filter.js +272 -0
- package/dist/lib/filter.js.map +1 -0
- package/dist/lib/format-context.d.ts +1335 -0
- package/dist/lib/format-context.js +1455 -0
- package/dist/lib/format-context.js.map +1 -0
- package/dist/lib/frame-utils.d.ts +116 -0
- package/dist/lib/frame-utils.js +98 -0
- package/dist/lib/frame-utils.js.map +1 -0
- package/dist/lib/frame.d.ts +1315 -0
- package/dist/lib/frame.js +1535 -0
- package/dist/lib/frame.js.map +1 -0
- package/dist/lib/hardware-device-context.d.ts +362 -0
- package/dist/lib/hardware-device-context.js +383 -0
- package/dist/lib/hardware-device-context.js.map +1 -0
- package/dist/lib/hardware-frames-context.d.ts +418 -0
- package/dist/lib/hardware-frames-context.js +471 -0
- package/dist/lib/hardware-frames-context.js.map +1 -0
- package/dist/lib/index.d.ts +35 -0
- package/dist/lib/index.js +60 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/input-format.d.ts +249 -0
- package/dist/lib/input-format.js +306 -0
- package/dist/lib/input-format.js.map +1 -0
- package/dist/lib/io-context.d.ts +698 -0
- package/dist/lib/io-context.js +771 -0
- package/dist/lib/io-context.js.map +1 -0
- package/dist/lib/log.d.ts +174 -0
- package/dist/lib/log.js +184 -0
- package/dist/lib/log.js.map +1 -0
- package/dist/lib/native-types.d.ts +1024 -0
- package/dist/lib/native-types.js +2 -0
- package/dist/lib/native-types.js.map +1 -0
- package/dist/lib/option.d.ts +953 -0
- package/dist/lib/option.js +1607 -0
- package/dist/lib/option.js.map +1 -0
- package/dist/lib/output-format.d.ts +222 -0
- package/dist/lib/output-format.js +259 -0
- package/dist/lib/output-format.js.map +1 -0
- package/dist/lib/packet.d.ts +521 -0
- package/dist/lib/packet.js +613 -0
- package/dist/lib/packet.js.map +1 -0
- package/dist/lib/rational.d.ts +251 -0
- package/dist/lib/rational.js +278 -0
- package/dist/lib/rational.js.map +1 -0
- package/dist/lib/software-resample-context.d.ts +552 -0
- package/dist/lib/software-resample-context.js +592 -0
- package/dist/lib/software-resample-context.js.map +1 -0
- package/dist/lib/software-scale-context.d.ts +344 -0
- package/dist/lib/software-scale-context.js +366 -0
- package/dist/lib/software-scale-context.js.map +1 -0
- package/dist/lib/stream.d.ts +384 -0
- package/dist/lib/stream.js +525 -0
- package/dist/lib/stream.js.map +1 -0
- package/dist/lib/sync-queue.d.ts +179 -0
- package/dist/lib/sync-queue.js +197 -0
- package/dist/lib/sync-queue.js.map +1 -0
- package/dist/lib/types.d.ts +34 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/utilities.d.ts +1198 -0
- package/dist/lib/utilities.js +1302 -0
- package/dist/lib/utilities.js.map +1 -0
- 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 +6 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/libc.d.ts +11 -0
- package/dist/utils/libc.js +35 -0
- package/dist/utils/libc.js.map +1 -0
- package/dist/utils/variant.d.ts +26 -0
- package/dist/utils/variant.js +31 -0
- package/dist/utils/variant.js.map +1 -0
- package/dist/webrtc/index.d.ts +4 -0
- package/dist/webrtc/index.js +7 -0
- package/dist/webrtc/index.js.map +1 -0
- package/install/check.js +124 -0
- package/install/ffmpeg.js +66 -0
- package/package.json +165 -3
|
@@ -0,0 +1,1344 @@
|
|
|
1
|
+
import { AV_NOPTS_VALUE } from '../constants/constants.js';
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// Implementation
|
|
4
|
+
// ============================================================================
|
|
5
|
+
/**
|
|
6
|
+
* Pipeline implementation.
|
|
7
|
+
*
|
|
8
|
+
* Creates a processing pipeline from media components.
|
|
9
|
+
* Automatically handles type conversions and proper flushing order.
|
|
10
|
+
*
|
|
11
|
+
* @param args - Variable arguments depending on pipeline type
|
|
12
|
+
*
|
|
13
|
+
* @returns PipelineControl if output is present, AsyncGenerator otherwise
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* // Simple pipeline
|
|
18
|
+
* const control = pipeline(
|
|
19
|
+
* input,
|
|
20
|
+
* decoder,
|
|
21
|
+
* filter,
|
|
22
|
+
* encoder,
|
|
23
|
+
* output
|
|
24
|
+
* );
|
|
25
|
+
* await control.completion;
|
|
26
|
+
*
|
|
27
|
+
* // Named pipeline for muxing
|
|
28
|
+
* const control = pipeline(
|
|
29
|
+
* { video: videoInput, audio: audioInput },
|
|
30
|
+
* {
|
|
31
|
+
* video: [videoDecoder, scaleFilter, videoEncoder],
|
|
32
|
+
* audio: [audioDecoder, volumeFilter, audioEncoder]
|
|
33
|
+
* },
|
|
34
|
+
* output
|
|
35
|
+
* );
|
|
36
|
+
* await control.completion;
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function pipeline(...args) {
|
|
40
|
+
// Extract PipelineOptions if last arg is undefined or a PipelineOptions object.
|
|
41
|
+
// PipelineOptions is identified as a plain object carrying a known option key
|
|
42
|
+
// ('signal', 'onProgress', or 'progressInterval'). This distinguishes it from
|
|
43
|
+
// NamedInputs/NamedStages/NamedOutputs which have stream name keys.
|
|
44
|
+
let pipelineOptions;
|
|
45
|
+
const lastArg = args[args.length - 1];
|
|
46
|
+
if (lastArg === undefined) {
|
|
47
|
+
args.pop();
|
|
48
|
+
}
|
|
49
|
+
else if (args.length > 0 &&
|
|
50
|
+
typeof lastArg === 'object' &&
|
|
51
|
+
lastArg !== null &&
|
|
52
|
+
Object.getPrototypeOf(lastArg) === Object.prototype &&
|
|
53
|
+
('signal' in lastArg || 'onProgress' in lastArg || 'progressInterval' in lastArg)) {
|
|
54
|
+
pipelineOptions = args.pop();
|
|
55
|
+
}
|
|
56
|
+
pipelineOptions?.signal?.throwIfAborted();
|
|
57
|
+
// Detect pipeline type based on first argument
|
|
58
|
+
const firstArg = args[0];
|
|
59
|
+
const secondArg = args[1];
|
|
60
|
+
// Check for shared Demuxer + NamedStages pattern
|
|
61
|
+
if (isDemuxer(firstArg) && isNamedStages(secondArg)) {
|
|
62
|
+
// Convert shared input to NamedInputs based on stages keys
|
|
63
|
+
const sharedInput = firstArg;
|
|
64
|
+
const stages = secondArg;
|
|
65
|
+
const namedInputs = {};
|
|
66
|
+
// Create NamedInputs with shared input for all streams in stages
|
|
67
|
+
for (const streamName of Object.keys(stages)) {
|
|
68
|
+
namedInputs[streamName] = sharedInput;
|
|
69
|
+
}
|
|
70
|
+
if (args.length === 3) {
|
|
71
|
+
// Full named pipeline with output(s)
|
|
72
|
+
return runNamedPipeline(namedInputs, stages, args[2], pipelineOptions);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// Partial named pipeline
|
|
76
|
+
return runNamedPartialPipeline(namedInputs, stages);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (isNamedInputs(firstArg)) {
|
|
80
|
+
// Named pipeline (2 or 3 arguments)
|
|
81
|
+
if (args.length === 2) {
|
|
82
|
+
// Partial named pipeline - return generators
|
|
83
|
+
return runNamedPartialPipeline(args[0], args[1]);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Full named pipeline with output
|
|
87
|
+
return runNamedPipeline(args[0], args[1], args[2], pipelineOptions);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (isDemuxer(firstArg)) {
|
|
91
|
+
// Check if this is a stream copy (Demuxer → Muxer)
|
|
92
|
+
if (args.length === 2 && isMuxer(args[1])) {
|
|
93
|
+
// Stream copy all streams
|
|
94
|
+
return runDemuxerPipeline(args[0], args[1], pipelineOptions);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Simple pipeline starting with Demuxer
|
|
98
|
+
return runSimplePipeline(args, pipelineOptions);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Simple pipeline (variable arguments)
|
|
103
|
+
return runSimplePipeline(args, pipelineOptions);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// PipelineControl Implementation
|
|
108
|
+
// ============================================================================
|
|
109
|
+
/**
|
|
110
|
+
* Accumulates output statistics and emits throttled progress updates.
|
|
111
|
+
*
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
class ProgressTracker {
|
|
115
|
+
onProgress;
|
|
116
|
+
throttleMs;
|
|
117
|
+
frames = 0;
|
|
118
|
+
bytes = 0;
|
|
119
|
+
maxTime = 0;
|
|
120
|
+
startTime = Date.now();
|
|
121
|
+
lastEmit = 0;
|
|
122
|
+
/**
|
|
123
|
+
* @param onProgress - Optional callback for push updates
|
|
124
|
+
*
|
|
125
|
+
* @param throttleMs - Minimum interval between callbacks
|
|
126
|
+
*
|
|
127
|
+
* @internal
|
|
128
|
+
*/
|
|
129
|
+
constructor(onProgress, throttleMs = 250) {
|
|
130
|
+
this.onProgress = onProgress;
|
|
131
|
+
this.throttleMs = throttleMs;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Record a written output packet.
|
|
135
|
+
*
|
|
136
|
+
* @param packet - The packet written to the output
|
|
137
|
+
*
|
|
138
|
+
* @internal
|
|
139
|
+
*/
|
|
140
|
+
record(packet) {
|
|
141
|
+
this.frames++;
|
|
142
|
+
this.bytes += packet.size;
|
|
143
|
+
const tb = packet.timeBase;
|
|
144
|
+
if (packet.pts !== AV_NOPTS_VALUE && tb && tb.den > 0) {
|
|
145
|
+
const t = (Number(packet.pts) * tb.num) / tb.den;
|
|
146
|
+
if (t > this.maxTime) {
|
|
147
|
+
this.maxTime = t;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
if (this.onProgress && now - this.lastEmit >= this.throttleMs) {
|
|
152
|
+
this.lastEmit = now;
|
|
153
|
+
this.onProgress(this.snapshot());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Build a progress snapshot from the current counters.
|
|
158
|
+
*
|
|
159
|
+
* @returns The current progress statistics
|
|
160
|
+
*
|
|
161
|
+
* @internal
|
|
162
|
+
*/
|
|
163
|
+
snapshot() {
|
|
164
|
+
const elapsed = (Date.now() - this.startTime) / 1000;
|
|
165
|
+
return {
|
|
166
|
+
frames: this.frames,
|
|
167
|
+
bytes: this.bytes,
|
|
168
|
+
time: this.maxTime,
|
|
169
|
+
fps: elapsed > 0 ? this.frames / elapsed : 0,
|
|
170
|
+
bitrate: this.maxTime > 0 ? (this.bytes * 8) / this.maxTime : 0,
|
|
171
|
+
speed: elapsed > 0 ? this.maxTime / elapsed : 0,
|
|
172
|
+
elapsed,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Emit a final progress update.
|
|
177
|
+
*
|
|
178
|
+
* @internal
|
|
179
|
+
*/
|
|
180
|
+
finish() {
|
|
181
|
+
this.onProgress?.(this.snapshot());
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const EMPTY_PROGRESS = { frames: 0, bytes: 0, time: 0, fps: 0, bitrate: 0, speed: 0, elapsed: 0 };
|
|
185
|
+
/**
|
|
186
|
+
* Pipeline control implementation.
|
|
187
|
+
*
|
|
188
|
+
* @internal
|
|
189
|
+
*/
|
|
190
|
+
export class PipelineControlImpl {
|
|
191
|
+
_stopped = false;
|
|
192
|
+
_completion;
|
|
193
|
+
signalCleanup;
|
|
194
|
+
tracker;
|
|
195
|
+
sources;
|
|
196
|
+
/**
|
|
197
|
+
* @param executionPromise - Promise that resolves when pipeline completes
|
|
198
|
+
*
|
|
199
|
+
* @param signal - Optional AbortSignal for cancellation
|
|
200
|
+
*
|
|
201
|
+
* @param tracker - Optional progress tracker shared with the runner
|
|
202
|
+
*
|
|
203
|
+
* @param sources - Demuxer sources to interrupt on stop (unblocks a stalled read)
|
|
204
|
+
*
|
|
205
|
+
* @internal
|
|
206
|
+
*/
|
|
207
|
+
constructor(executionPromise, signal, tracker, sources = []) {
|
|
208
|
+
this.tracker = tracker;
|
|
209
|
+
this.sources = sources;
|
|
210
|
+
// Don't resolve immediately on stop, wait for the actual pipeline to finish.
|
|
211
|
+
// Emit a final progress update once the pipeline settles.
|
|
212
|
+
this._completion = tracker ? executionPromise.finally(() => tracker.finish()) : executionPromise;
|
|
213
|
+
if (signal) {
|
|
214
|
+
const handler = () => this.stop();
|
|
215
|
+
signal.addEventListener('abort', handler, { once: true });
|
|
216
|
+
this.signalCleanup = () => signal.removeEventListener('abort', handler);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Stop the pipeline.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```typescript
|
|
224
|
+
* const control = pipeline(input, decoder, filter, encoder, output);
|
|
225
|
+
* control.stop();
|
|
226
|
+
* ```
|
|
227
|
+
*
|
|
228
|
+
* @see {@link PipelineControl.isStopped}
|
|
229
|
+
*/
|
|
230
|
+
stop() {
|
|
231
|
+
this._stopped = true;
|
|
232
|
+
this.signalCleanup?.();
|
|
233
|
+
this.signalCleanup = undefined;
|
|
234
|
+
// Interrupt the source demuxer(s) so a blocking read (e.g. a quiet RTSP
|
|
235
|
+
// source) returns immediately. Without this, completion would wait for the
|
|
236
|
+
// read loop while the read waits to be unblocked - a teardown deadlock.
|
|
237
|
+
for (const source of this.sources) {
|
|
238
|
+
source.interrupt();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Check if pipeline is stopped.
|
|
243
|
+
*
|
|
244
|
+
* @returns True if stopped
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```typescript
|
|
248
|
+
* const control = pipeline(input, decoder, filter, encoder, output);
|
|
249
|
+
* const isStopped = control.isStopped();
|
|
250
|
+
* ```
|
|
251
|
+
*
|
|
252
|
+
* @see {@link PipelineControl.stop}
|
|
253
|
+
*/
|
|
254
|
+
isStopped() {
|
|
255
|
+
return this._stopped;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Get completion promise.
|
|
259
|
+
*/
|
|
260
|
+
get completion() {
|
|
261
|
+
return this._completion;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get the latest progress snapshot.
|
|
265
|
+
*/
|
|
266
|
+
get progress() {
|
|
267
|
+
return this.tracker?.snapshot() ?? EMPTY_PROGRESS;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Demuxer Pipeline Implementation
|
|
272
|
+
// ============================================================================
|
|
273
|
+
/**
|
|
274
|
+
* Run a demuxer pipeline for stream copy.
|
|
275
|
+
*
|
|
276
|
+
* @param input - Media input source
|
|
277
|
+
*
|
|
278
|
+
* @param output - Media output destination
|
|
279
|
+
*
|
|
280
|
+
* @param options - Pipeline options for cancellation support
|
|
281
|
+
*
|
|
282
|
+
* @returns Pipeline control interface
|
|
283
|
+
*
|
|
284
|
+
* @internal
|
|
285
|
+
*/
|
|
286
|
+
function runDemuxerPipeline(input, output, options) {
|
|
287
|
+
const tracker = new ProgressTracker(options?.onProgress, options?.progressInterval);
|
|
288
|
+
let control;
|
|
289
|
+
// eslint-disable-next-line prefer-const
|
|
290
|
+
control = new PipelineControlImpl(runDemuxerPipelineAsync(input, output, () => control?.isStopped() ?? false, tracker), options?.signal, tracker, [input]);
|
|
291
|
+
return control;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Run demuxer pipeline asynchronously.
|
|
295
|
+
*
|
|
296
|
+
* @param input - Media input source
|
|
297
|
+
*
|
|
298
|
+
* @param output - Media output destination
|
|
299
|
+
*
|
|
300
|
+
* @param shouldStop - Function to check if pipeline should stop
|
|
301
|
+
*
|
|
302
|
+
* @param tracker - Optional progress tracker for output statistics
|
|
303
|
+
*
|
|
304
|
+
* @internal
|
|
305
|
+
*/
|
|
306
|
+
async function runDemuxerPipelineAsync(input, output, shouldStop, tracker) {
|
|
307
|
+
// Get all streams from input
|
|
308
|
+
const videoStream = input.video();
|
|
309
|
+
const audioStream = input.audio();
|
|
310
|
+
const streams = [];
|
|
311
|
+
// Add video stream if present
|
|
312
|
+
if (videoStream) {
|
|
313
|
+
const outputIndex = output.addStream(videoStream);
|
|
314
|
+
streams.push({ stream: videoStream, index: outputIndex });
|
|
315
|
+
}
|
|
316
|
+
// Add audio stream if present
|
|
317
|
+
if (audioStream) {
|
|
318
|
+
const outputIndex = output.addStream(audioStream);
|
|
319
|
+
streams.push({ stream: audioStream, index: outputIndex });
|
|
320
|
+
}
|
|
321
|
+
// Add any other streams
|
|
322
|
+
const allStreams = input.streams;
|
|
323
|
+
for (const stream of allStreams) {
|
|
324
|
+
// Skip if already added
|
|
325
|
+
if (stream !== videoStream && stream !== audioStream) {
|
|
326
|
+
const outputIndex = output.addStream(stream);
|
|
327
|
+
streams.push({ stream, index: outputIndex });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Get iterator to properly clean up on stop
|
|
331
|
+
const packetsIterable = input.packets();
|
|
332
|
+
const iterator = packetsIterable[Symbol.asyncIterator]();
|
|
333
|
+
try {
|
|
334
|
+
// Copy all packets
|
|
335
|
+
while (true) {
|
|
336
|
+
// Check if we should stop before getting next item
|
|
337
|
+
if (shouldStop()) {
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
const { value: packet, done } = await iterator.next();
|
|
341
|
+
if (done)
|
|
342
|
+
break;
|
|
343
|
+
// Handle EOF signal (null packet from input means all streams are done)
|
|
344
|
+
if (packet === null) {
|
|
345
|
+
// Signal EOF for all streams
|
|
346
|
+
for (const mapping of streams) {
|
|
347
|
+
await output.writePacket(null, mapping.index);
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
// Find the corresponding output stream index
|
|
353
|
+
const mapping = streams.find((s) => s.stream.index === packet.streamIndex);
|
|
354
|
+
if (mapping) {
|
|
355
|
+
await output.writePacket(packet, mapping.index);
|
|
356
|
+
tracker?.record(packet);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
// Free the packet after use
|
|
361
|
+
if (packet && typeof packet.free === 'function') {
|
|
362
|
+
packet.free();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
// Always clean up the generator to prevent memory leaks
|
|
369
|
+
if (iterator.return) {
|
|
370
|
+
await iterator.return(undefined);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
await output.close();
|
|
374
|
+
}
|
|
375
|
+
// ============================================================================
|
|
376
|
+
// Simple Pipeline Implementation
|
|
377
|
+
// ============================================================================
|
|
378
|
+
/**
|
|
379
|
+
* Run a simple linear pipeline.
|
|
380
|
+
*
|
|
381
|
+
* @param args - Pipeline arguments
|
|
382
|
+
*
|
|
383
|
+
* @param options - Pipeline options for cancellation support
|
|
384
|
+
*
|
|
385
|
+
* @returns Pipeline control or async generator
|
|
386
|
+
*
|
|
387
|
+
* @internal
|
|
388
|
+
*/
|
|
389
|
+
function runSimplePipeline(args, options) {
|
|
390
|
+
const [source, ...stages] = args;
|
|
391
|
+
// Check if last stage is Muxer (consumes stream)
|
|
392
|
+
const lastStage = stages[stages.length - 1];
|
|
393
|
+
const isOutput = isMuxer(lastStage);
|
|
394
|
+
// Track metadata through pipeline
|
|
395
|
+
const metadata = {};
|
|
396
|
+
// Store Demuxer reference if we have one
|
|
397
|
+
if (isDemuxer(source)) {
|
|
398
|
+
metadata.demuxer = source;
|
|
399
|
+
}
|
|
400
|
+
// Build the pipeline generator
|
|
401
|
+
// If output is present, exclude it from stages for processing
|
|
402
|
+
const processStages = isOutput ? stages.slice(0, -1) : stages;
|
|
403
|
+
// Process metadata first by walking through stages
|
|
404
|
+
for (const stage of processStages) {
|
|
405
|
+
if (isDecoder(stage)) {
|
|
406
|
+
metadata.decoder = stage;
|
|
407
|
+
}
|
|
408
|
+
else if (isEncoder(stage)) {
|
|
409
|
+
metadata.encoder = stage;
|
|
410
|
+
}
|
|
411
|
+
else if (isBitStreamFilterAPI(stage)) {
|
|
412
|
+
metadata.bitStreamFilter = stage;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Convert Demuxer to packet stream if needed
|
|
416
|
+
// If we have a decoder or BSF, filter packets by stream index
|
|
417
|
+
let actualSource;
|
|
418
|
+
if (isDemuxer(source)) {
|
|
419
|
+
if (metadata.decoder) {
|
|
420
|
+
// Filter packets for the decoder's stream
|
|
421
|
+
const streamIndex = metadata.decoder.getStream().index;
|
|
422
|
+
actualSource = source.packets(streamIndex);
|
|
423
|
+
}
|
|
424
|
+
else if (metadata.bitStreamFilter) {
|
|
425
|
+
// Filter packets for the BSF's stream
|
|
426
|
+
const streamIndex = metadata.bitStreamFilter.getStream().index;
|
|
427
|
+
actualSource = source.packets(streamIndex);
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
// No decoder or BSF, pass all packets
|
|
431
|
+
actualSource = source.packets();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
actualSource = source;
|
|
436
|
+
}
|
|
437
|
+
const generator = buildSimplePipeline(actualSource, processStages);
|
|
438
|
+
// If output, consume the generator
|
|
439
|
+
if (isOutput) {
|
|
440
|
+
const tracker = new ProgressTracker(options?.onProgress, options?.progressInterval);
|
|
441
|
+
let control;
|
|
442
|
+
// eslint-disable-next-line prefer-const
|
|
443
|
+
control = new PipelineControlImpl(consumeSimplePipeline(generator, lastStage, metadata, () => control?.isStopped() ?? false, tracker), options?.signal, tracker, isDemuxer(source) ? [source] : []);
|
|
444
|
+
return control;
|
|
445
|
+
}
|
|
446
|
+
// Otherwise return the generator for further processing
|
|
447
|
+
return generator;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Build a simple pipeline generator.
|
|
451
|
+
*
|
|
452
|
+
* @param source - Source of packets or frames
|
|
453
|
+
*
|
|
454
|
+
* @param stages - Processing stages
|
|
455
|
+
*
|
|
456
|
+
* @yields {Packet | Frame} Processed packets or frames
|
|
457
|
+
*
|
|
458
|
+
* @internal
|
|
459
|
+
*/
|
|
460
|
+
async function* buildSimplePipeline(source, stages) {
|
|
461
|
+
let stream = source;
|
|
462
|
+
for (const stage of stages) {
|
|
463
|
+
if (isDecoder(stage)) {
|
|
464
|
+
stream = stage.frames(stream);
|
|
465
|
+
}
|
|
466
|
+
else if (isEncoder(stage)) {
|
|
467
|
+
stream = stage.packets(stream);
|
|
468
|
+
}
|
|
469
|
+
else if (isFilterAPI(stage)) {
|
|
470
|
+
stream = stage.frames(stream);
|
|
471
|
+
}
|
|
472
|
+
else if (isBitStreamFilterAPI(stage)) {
|
|
473
|
+
stream = stage.packets(stream);
|
|
474
|
+
}
|
|
475
|
+
else if (Array.isArray(stage)) {
|
|
476
|
+
// Chain multiple filters or BSFs
|
|
477
|
+
for (const filter of stage) {
|
|
478
|
+
if (isFilterAPI(filter)) {
|
|
479
|
+
stream = filter.frames(stream);
|
|
480
|
+
}
|
|
481
|
+
else if (isBitStreamFilterAPI(filter)) {
|
|
482
|
+
stream = filter.packets(stream);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
yield* stream;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Consume a simple pipeline stream and write to output.
|
|
491
|
+
*
|
|
492
|
+
* @param stream - Stream of packets or frames
|
|
493
|
+
*
|
|
494
|
+
* @param output - Media output destination
|
|
495
|
+
*
|
|
496
|
+
* @param metadata - Stream metadata
|
|
497
|
+
*
|
|
498
|
+
* @param shouldStop - Function to check if pipeline should stop
|
|
499
|
+
*
|
|
500
|
+
* @param tracker - Optional progress tracker for output statistics
|
|
501
|
+
*
|
|
502
|
+
* @internal
|
|
503
|
+
*/
|
|
504
|
+
async function consumeSimplePipeline(stream, output, metadata, shouldStop, tracker) {
|
|
505
|
+
// Add stream to output if we have encoder or decoder info
|
|
506
|
+
let streamIndex = 0;
|
|
507
|
+
if (metadata.encoder) {
|
|
508
|
+
// Encoding path
|
|
509
|
+
if (metadata.decoder) {
|
|
510
|
+
// Have decoder - use its stream for metadata/properties
|
|
511
|
+
const originalStream = metadata.decoder.getStream();
|
|
512
|
+
streamIndex = output.addStream(originalStream, { encoder: metadata.encoder, bsf: metadata.bitStreamFilter });
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
// Encoder-only mode (e.g., frame generator) - no input stream
|
|
516
|
+
streamIndex = output.addStream(metadata.encoder, { bsf: metadata.bitStreamFilter });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
else if (metadata.decoder) {
|
|
520
|
+
// Stream copy - use decoder's original stream
|
|
521
|
+
const originalStream = metadata.decoder.getStream();
|
|
522
|
+
streamIndex = output.addStream(originalStream);
|
|
523
|
+
}
|
|
524
|
+
else if (metadata.bitStreamFilter) {
|
|
525
|
+
// BSF without encoder/decoder - use BSF's original stream
|
|
526
|
+
const originalStream = metadata.bitStreamFilter.getStream();
|
|
527
|
+
streamIndex = output.addStream(originalStream);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
// For direct Demuxer → Muxer, we redirect to runDemuxerPipeline
|
|
531
|
+
// This case shouldn't happen in simple pipeline
|
|
532
|
+
throw new Error('Cannot determine stream configuration. This is likely a bug in the pipeline.');
|
|
533
|
+
}
|
|
534
|
+
// Get iterator to properly clean up on stop
|
|
535
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
536
|
+
try {
|
|
537
|
+
// Process stream
|
|
538
|
+
while (true) {
|
|
539
|
+
// Check if we should stop before getting next item
|
|
540
|
+
if (shouldStop()) {
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
const { value: item, done } = await iterator.next();
|
|
544
|
+
if (done)
|
|
545
|
+
break;
|
|
546
|
+
// Handle EOF signal
|
|
547
|
+
if (item === null) {
|
|
548
|
+
await output.writePacket(null, streamIndex);
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
// Use explicit resource management for the item
|
|
552
|
+
try {
|
|
553
|
+
if (isPacket(item) || item === null) {
|
|
554
|
+
await output.writePacket(item, streamIndex);
|
|
555
|
+
if (item !== null) {
|
|
556
|
+
tracker?.record(item);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
throw new Error('Cannot write frames directly to Muxer. Use an encoder first.');
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
finally {
|
|
564
|
+
// Free the packet/frame after use
|
|
565
|
+
if (item && typeof item.free === 'function') {
|
|
566
|
+
item.free();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
finally {
|
|
572
|
+
// Always clean up the generator to prevent memory leaks
|
|
573
|
+
if (iterator.return) {
|
|
574
|
+
await iterator.return(undefined);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
await output.close();
|
|
578
|
+
}
|
|
579
|
+
// ============================================================================
|
|
580
|
+
// Named Pipeline Implementation
|
|
581
|
+
// ============================================================================
|
|
582
|
+
/**
|
|
583
|
+
* Run a named partial pipeline.
|
|
584
|
+
*
|
|
585
|
+
* @param inputs - Named input sources
|
|
586
|
+
*
|
|
587
|
+
* @param stages - Named processing stages
|
|
588
|
+
*
|
|
589
|
+
* @returns Record of async generators
|
|
590
|
+
*
|
|
591
|
+
* @internal
|
|
592
|
+
*/
|
|
593
|
+
function runNamedPartialPipeline(inputs, stages) {
|
|
594
|
+
const result = {};
|
|
595
|
+
for (const [streamName, streamStages] of Object.entries(stages)) {
|
|
596
|
+
const input = inputs[streamName];
|
|
597
|
+
if (!input) {
|
|
598
|
+
throw new Error(`No input found for stream: ${streamName}`);
|
|
599
|
+
}
|
|
600
|
+
// Get the appropriate stream based on the stream name
|
|
601
|
+
let stream = null;
|
|
602
|
+
switch (streamName) {
|
|
603
|
+
case 'video':
|
|
604
|
+
stream = input.video() ?? null;
|
|
605
|
+
break;
|
|
606
|
+
case 'audio':
|
|
607
|
+
stream = input.audio() ?? null;
|
|
608
|
+
break;
|
|
609
|
+
default:
|
|
610
|
+
// This should never happen
|
|
611
|
+
throw new Error(`Invalid stream name: ${streamName}. Must be 'video' or 'audio'.`);
|
|
612
|
+
}
|
|
613
|
+
if (!stream) {
|
|
614
|
+
throw new Error(`No ${streamName} stream found in input.`);
|
|
615
|
+
}
|
|
616
|
+
// Normalize stages: if array contains only undefined, treat as passthrough
|
|
617
|
+
// Also filter out undefined entries from the array
|
|
618
|
+
let normalizedStages = streamStages;
|
|
619
|
+
if (Array.isArray(streamStages)) {
|
|
620
|
+
const definedStages = streamStages.filter((stage) => stage !== undefined);
|
|
621
|
+
if (definedStages.length === 0) {
|
|
622
|
+
normalizedStages = 'passthrough';
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
normalizedStages = definedStages;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (normalizedStages === 'passthrough') {
|
|
629
|
+
// Direct passthrough - return input packets for this specific stream
|
|
630
|
+
result[streamName] = (async function* () {
|
|
631
|
+
for await (const packet of input.packets(stream.index)) {
|
|
632
|
+
yield packet;
|
|
633
|
+
}
|
|
634
|
+
})();
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
// Process the stream - pass packets for this specific stream only
|
|
638
|
+
// Build pipeline for this stream (can return frames or packets)
|
|
639
|
+
const metadata = {};
|
|
640
|
+
const stages = normalizedStages;
|
|
641
|
+
result[streamName] = buildFlexibleNamedStreamPipeline(input.packets(stream.index), stages, metadata);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return result;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Run a named pipeline with outputs.
|
|
648
|
+
*
|
|
649
|
+
* @param inputs - Named input sources
|
|
650
|
+
*
|
|
651
|
+
* @param stages - Named processing stages
|
|
652
|
+
*
|
|
653
|
+
* @param output - Output destination(s)
|
|
654
|
+
*
|
|
655
|
+
* @param options - Pipeline options for cancellation support
|
|
656
|
+
*
|
|
657
|
+
* @returns Pipeline control interface
|
|
658
|
+
*
|
|
659
|
+
* @internal
|
|
660
|
+
*/
|
|
661
|
+
function runNamedPipeline(inputs, stages, output, options) {
|
|
662
|
+
const tracker = new ProgressTracker(options?.onProgress, options?.progressInterval);
|
|
663
|
+
// Collect the unique Demuxer sources so stop() can interrupt their reads.
|
|
664
|
+
const sources = [...new Set(Object.values(inputs).filter(isDemuxer))];
|
|
665
|
+
let control;
|
|
666
|
+
// eslint-disable-next-line prefer-const
|
|
667
|
+
control = new PipelineControlImpl(runNamedPipelineAsync(inputs, stages, output, () => control?.isStopped() ?? false, tracker), options?.signal, tracker, sources);
|
|
668
|
+
return control;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Run named pipeline asynchronously.
|
|
672
|
+
*
|
|
673
|
+
* @param inputs - Named input sources
|
|
674
|
+
*
|
|
675
|
+
* @param stages - Named processing stages
|
|
676
|
+
*
|
|
677
|
+
* @param output - Output destination(s)
|
|
678
|
+
*
|
|
679
|
+
* @param shouldStop - Function to check if pipeline should stop
|
|
680
|
+
*
|
|
681
|
+
* @param tracker - Optional progress tracker for output statistics
|
|
682
|
+
*
|
|
683
|
+
* @internal
|
|
684
|
+
*/
|
|
685
|
+
async function runNamedPipelineAsync(inputs, stages, output, shouldStop, tracker) {
|
|
686
|
+
// Check if all inputs reference the same Demuxer instance
|
|
687
|
+
const inputValues = Object.values(inputs);
|
|
688
|
+
const allSameInput = inputValues.length > 1 && inputValues.every((input) => input === inputValues[0]);
|
|
689
|
+
// Track metadata for each stream
|
|
690
|
+
const streamMetadata = {};
|
|
691
|
+
// Process each named stream into generators
|
|
692
|
+
const processedStreams = {};
|
|
693
|
+
// If all inputs are the same instance, use Demuxer's built-in parallel packet generators
|
|
694
|
+
if (allSameInput) {
|
|
695
|
+
const sharedInput = inputValues[0];
|
|
696
|
+
// Single pass: collect metadata and build pipelines directly using input.packets(streamIndex)
|
|
697
|
+
for (const [streamName, streamStages] of Object.entries(stages)) {
|
|
698
|
+
const metadata = {};
|
|
699
|
+
streamMetadata[streamName] = metadata;
|
|
700
|
+
// Normalize stages
|
|
701
|
+
let normalizedStages = streamStages;
|
|
702
|
+
if (Array.isArray(streamStages)) {
|
|
703
|
+
const definedStages = streamStages.filter((stage) => stage !== undefined);
|
|
704
|
+
if (definedStages.length === 0) {
|
|
705
|
+
normalizedStages = 'passthrough';
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
normalizedStages = definedStages;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// Determine stream index and build pipeline
|
|
712
|
+
let streamIndex;
|
|
713
|
+
if (normalizedStages !== 'passthrough') {
|
|
714
|
+
const stages = normalizedStages;
|
|
715
|
+
// Set stream type
|
|
716
|
+
metadata.type = streamName;
|
|
717
|
+
// Populate metadata by walking through ALL stages
|
|
718
|
+
for (const stage of stages) {
|
|
719
|
+
if (isDecoder(stage)) {
|
|
720
|
+
metadata.decoder = stage;
|
|
721
|
+
streamIndex ??= stage.getStream().index;
|
|
722
|
+
}
|
|
723
|
+
else if (isBitStreamFilterAPI(stage)) {
|
|
724
|
+
metadata.bitStreamFilter = stage;
|
|
725
|
+
streamIndex ??= stage.getStream().index;
|
|
726
|
+
}
|
|
727
|
+
else if (isEncoder(stage)) {
|
|
728
|
+
metadata.encoder = stage;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
// If no decoder/BSF, use stream name to determine index
|
|
732
|
+
if (streamIndex === undefined) {
|
|
733
|
+
const stream = streamName === 'video' ? sharedInput.video() : sharedInput.audio();
|
|
734
|
+
if (!stream) {
|
|
735
|
+
throw new Error(`No ${streamName} stream found in input.`);
|
|
736
|
+
}
|
|
737
|
+
streamIndex = stream.index;
|
|
738
|
+
}
|
|
739
|
+
// Build pipeline with packets from this specific stream
|
|
740
|
+
processedStreams[streamName] = buildNamedStreamPipeline(sharedInput.packets(streamIndex), stages, metadata);
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
// Passthrough - use Demuxer's built-in stream filtering
|
|
744
|
+
metadata.type = streamName;
|
|
745
|
+
metadata.demuxer = sharedInput;
|
|
746
|
+
const stream = streamName === 'video' ? sharedInput.video() : sharedInput.audio();
|
|
747
|
+
if (!stream) {
|
|
748
|
+
throw new Error(`No ${streamName} stream found in input for passthrough.`);
|
|
749
|
+
}
|
|
750
|
+
streamIndex = stream.index;
|
|
751
|
+
// Direct passthrough using input.packets(streamIndex)
|
|
752
|
+
processedStreams[streamName] = sharedInput.packets(streamIndex);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
// Original logic: separate inputs or single input
|
|
758
|
+
for (const [streamName, streamStages] of Object.entries(stages)) {
|
|
759
|
+
const metadata = {};
|
|
760
|
+
streamMetadata[streamName] = metadata;
|
|
761
|
+
const input = inputs[streamName];
|
|
762
|
+
if (!input) {
|
|
763
|
+
throw new Error(`No input found for stream: ${streamName}`);
|
|
764
|
+
}
|
|
765
|
+
// Normalize stages: if array contains only undefined, treat as passthrough
|
|
766
|
+
// Also filter out undefined entries from the array
|
|
767
|
+
let normalizedStages = streamStages;
|
|
768
|
+
if (Array.isArray(streamStages)) {
|
|
769
|
+
const definedStages = streamStages.filter((stage) => stage !== undefined);
|
|
770
|
+
if (definedStages.length === 0) {
|
|
771
|
+
normalizedStages = 'passthrough';
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
normalizedStages = definedStages;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (normalizedStages === 'passthrough') {
|
|
778
|
+
// Direct passthrough - no processing
|
|
779
|
+
let stream = null;
|
|
780
|
+
switch (streamName) {
|
|
781
|
+
case 'video':
|
|
782
|
+
stream = input.video() ?? null;
|
|
783
|
+
metadata.type = 'video';
|
|
784
|
+
break;
|
|
785
|
+
case 'audio':
|
|
786
|
+
stream = input.audio() ?? null;
|
|
787
|
+
metadata.type = 'audio';
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
if (!stream) {
|
|
791
|
+
throw new Error(`No ${streamName} stream found in input for passthrough.`);
|
|
792
|
+
}
|
|
793
|
+
processedStreams[streamName] = input.packets(stream.index);
|
|
794
|
+
metadata.demuxer = input; // Track Demuxer for passthrough
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
// Process the stream - normalizedStages is guaranteed to be an array here
|
|
798
|
+
const stages = normalizedStages;
|
|
799
|
+
// Pre-populate metadata by walking through stages
|
|
800
|
+
for (const stage of stages) {
|
|
801
|
+
if (isDecoder(stage)) {
|
|
802
|
+
metadata.decoder = stage;
|
|
803
|
+
}
|
|
804
|
+
else if (isEncoder(stage)) {
|
|
805
|
+
metadata.encoder = stage;
|
|
806
|
+
}
|
|
807
|
+
else if (isBitStreamFilterAPI(stage)) {
|
|
808
|
+
metadata.bitStreamFilter = stage;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
// Get packets - filter by stream index based on decoder, BSF, or stream type
|
|
812
|
+
let packets;
|
|
813
|
+
if (metadata.decoder) {
|
|
814
|
+
const streamIndex = metadata.decoder.getStream().index;
|
|
815
|
+
packets = input.packets(streamIndex);
|
|
816
|
+
}
|
|
817
|
+
else if (metadata.bitStreamFilter) {
|
|
818
|
+
const streamIndex = metadata.bitStreamFilter.getStream().index;
|
|
819
|
+
packets = input.packets(streamIndex);
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
// No decoder or BSF - determine stream by name
|
|
823
|
+
let stream = null;
|
|
824
|
+
switch (streamName) {
|
|
825
|
+
case 'video':
|
|
826
|
+
stream = input.video() ?? null;
|
|
827
|
+
break;
|
|
828
|
+
case 'audio':
|
|
829
|
+
stream = input.audio() ?? null;
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
if (!stream) {
|
|
833
|
+
throw new Error(`No ${streamName} stream found in input.`);
|
|
834
|
+
}
|
|
835
|
+
packets = input.packets(stream.index);
|
|
836
|
+
}
|
|
837
|
+
// Build pipeline for this stream
|
|
838
|
+
processedStreams[streamName] = buildNamedStreamPipeline(packets, stages, metadata);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
// Write to output(s)
|
|
843
|
+
if (isMuxer(output)) {
|
|
844
|
+
// Always write streams in parallel - Muxer's SyncQueue handles interleaving internally
|
|
845
|
+
const streamIndices = {};
|
|
846
|
+
// Add all streams to output first
|
|
847
|
+
for (const [name, meta] of Object.entries(streamMetadata)) {
|
|
848
|
+
if (meta.encoder) {
|
|
849
|
+
// Encoding path
|
|
850
|
+
if (meta.decoder) {
|
|
851
|
+
// Have decoder - use its stream for metadata/properties
|
|
852
|
+
const originalStream = meta.decoder.getStream();
|
|
853
|
+
streamIndices[name] = output.addStream(originalStream, { encoder: meta.encoder, bsf: meta.bitStreamFilter });
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
// Encoder-only mode (e.g., frame generator) - no input stream
|
|
857
|
+
streamIndices[name] = output.addStream(meta.encoder, { bsf: meta.bitStreamFilter });
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
else if (meta.decoder) {
|
|
861
|
+
// Stream copy - use decoder's original stream
|
|
862
|
+
const originalStream = meta.decoder.getStream();
|
|
863
|
+
streamIndices[name] = output.addStream(originalStream);
|
|
864
|
+
}
|
|
865
|
+
else if (meta.bitStreamFilter) {
|
|
866
|
+
// BSF - use BSF's original stream
|
|
867
|
+
const originalStream = meta.bitStreamFilter.getStream();
|
|
868
|
+
streamIndices[name] = output.addStream(originalStream);
|
|
869
|
+
}
|
|
870
|
+
else if (meta.demuxer) {
|
|
871
|
+
// Passthrough from Demuxer
|
|
872
|
+
const stream = name.includes('video') ? meta.demuxer.video() : meta.demuxer.audio();
|
|
873
|
+
if (!stream) {
|
|
874
|
+
throw new Error(`No matching stream found in Demuxer for ${name}`);
|
|
875
|
+
}
|
|
876
|
+
streamIndices[name] = output.addStream(stream);
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
throw new Error(`Cannot determine stream configuration for ${name}. This is likely a bug in the pipeline.`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
// Write all streams in parallel - Muxer's SyncQueue handles interleaving
|
|
883
|
+
const promises = [];
|
|
884
|
+
for (const [name, stream] of Object.entries(processedStreams)) {
|
|
885
|
+
const streamIndex = streamIndices[name];
|
|
886
|
+
if (streamIndex !== undefined) {
|
|
887
|
+
promises.push(consumeStreamInParallel(stream, output, streamIndex, shouldStop, tracker));
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
await Promise.all(promises);
|
|
891
|
+
await output.close();
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
// Multiple outputs - write each stream to its output. Group consumers by
|
|
895
|
+
// Muxer so each output is closed exactly once, after ALL streams feeding
|
|
896
|
+
// it are done - closing writes the trailer, without which containers like
|
|
897
|
+
// MP4 are left unreadable. Shared outputs (two streams into one Muxer)
|
|
898
|
+
// therefore must not be closed per stream.
|
|
899
|
+
const outputs = output;
|
|
900
|
+
const outputConsumers = new Map();
|
|
901
|
+
for (const [streamName, stream] of Object.entries(processedStreams)) {
|
|
902
|
+
const streamOutput = outputs[streamName];
|
|
903
|
+
const metadata = streamMetadata[streamName];
|
|
904
|
+
if (streamOutput && metadata) {
|
|
905
|
+
const consumer = consumeNamedStream(stream, streamOutput, metadata, shouldStop, tracker);
|
|
906
|
+
const consumers = outputConsumers.get(streamOutput);
|
|
907
|
+
if (consumers) {
|
|
908
|
+
consumers.push(consumer);
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
outputConsumers.set(streamOutput, [consumer]);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
// Finalize each output once its streams complete. A failing consumer
|
|
916
|
+
// rejects its output group and propagates to the completion promise
|
|
917
|
+
// (matching the single-output branch, which also skips close() on error).
|
|
918
|
+
await Promise.all([...outputConsumers].map(async ([muxer, consumers]) => {
|
|
919
|
+
await Promise.all(consumers);
|
|
920
|
+
await muxer.close();
|
|
921
|
+
}));
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Build a flexible named stream pipeline.
|
|
926
|
+
*
|
|
927
|
+
* @param source - Source packets
|
|
928
|
+
*
|
|
929
|
+
* @param stages - Processing stages
|
|
930
|
+
*
|
|
931
|
+
* @param metadata - Stream metadata
|
|
932
|
+
*
|
|
933
|
+
* @yields {Packet | Frame} Processed packets or frames
|
|
934
|
+
*
|
|
935
|
+
* @internal
|
|
936
|
+
*/
|
|
937
|
+
async function* buildFlexibleNamedStreamPipeline(source, stages, metadata) {
|
|
938
|
+
let stream = source;
|
|
939
|
+
for (const stage of stages) {
|
|
940
|
+
if (isDecoder(stage)) {
|
|
941
|
+
metadata.decoder = stage;
|
|
942
|
+
stream = stage.frames(stream);
|
|
943
|
+
}
|
|
944
|
+
else if (isEncoder(stage)) {
|
|
945
|
+
metadata.encoder = stage;
|
|
946
|
+
stream = stage.packets(stream);
|
|
947
|
+
}
|
|
948
|
+
else if (isFilterAPI(stage)) {
|
|
949
|
+
stream = stage.frames(stream);
|
|
950
|
+
}
|
|
951
|
+
else if (isBitStreamFilterAPI(stage)) {
|
|
952
|
+
metadata.bitStreamFilter = stage;
|
|
953
|
+
stream = stage.packets(stream);
|
|
954
|
+
}
|
|
955
|
+
else if (Array.isArray(stage)) {
|
|
956
|
+
// Chain multiple filters or BSFs
|
|
957
|
+
for (const filter of stage) {
|
|
958
|
+
if (isFilterAPI(filter)) {
|
|
959
|
+
stream = filter.frames(stream);
|
|
960
|
+
}
|
|
961
|
+
else if (isBitStreamFilterAPI(filter)) {
|
|
962
|
+
stream = filter.packets(stream);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
// Yield whatever the pipeline produces (frames or packets)
|
|
968
|
+
yield* stream;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Build a named stream pipeline.
|
|
972
|
+
*
|
|
973
|
+
* @param source - Source packets
|
|
974
|
+
*
|
|
975
|
+
* @param stages - Processing stages
|
|
976
|
+
*
|
|
977
|
+
* @param metadata - Stream metadata
|
|
978
|
+
*
|
|
979
|
+
* @yields {Packet} Processed packets
|
|
980
|
+
*
|
|
981
|
+
* @internal
|
|
982
|
+
*/
|
|
983
|
+
async function* buildNamedStreamPipeline(source, stages, metadata) {
|
|
984
|
+
let stream = source;
|
|
985
|
+
for (const stage of stages) {
|
|
986
|
+
if (isDecoder(stage)) {
|
|
987
|
+
metadata.decoder = stage;
|
|
988
|
+
stream = stage.frames(stream);
|
|
989
|
+
}
|
|
990
|
+
else if (isEncoder(stage)) {
|
|
991
|
+
metadata.encoder = stage;
|
|
992
|
+
stream = stage.packets(stream);
|
|
993
|
+
}
|
|
994
|
+
else if (isFilterAPI(stage)) {
|
|
995
|
+
stream = stage.frames(stream);
|
|
996
|
+
}
|
|
997
|
+
else if (isBitStreamFilterAPI(stage)) {
|
|
998
|
+
metadata.bitStreamFilter = stage;
|
|
999
|
+
stream = stage.packets(stream);
|
|
1000
|
+
}
|
|
1001
|
+
else if (Array.isArray(stage)) {
|
|
1002
|
+
// Chain multiple filters or BSFs
|
|
1003
|
+
for (const filter of stage) {
|
|
1004
|
+
if (isFilterAPI(filter)) {
|
|
1005
|
+
stream = filter.frames(stream);
|
|
1006
|
+
}
|
|
1007
|
+
else if (isBitStreamFilterAPI(filter)) {
|
|
1008
|
+
stream = filter.packets(stream);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
// Ensure we're yielding packets
|
|
1014
|
+
for await (const item of stream) {
|
|
1015
|
+
if (isPacket(item) || item === null) {
|
|
1016
|
+
yield item;
|
|
1017
|
+
}
|
|
1018
|
+
else {
|
|
1019
|
+
throw new Error('Named pipeline must end with packets (use encoder after filters)');
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Consume a stream in parallel (for passthrough pipelines).
|
|
1025
|
+
* Stream index is already added to output.
|
|
1026
|
+
*
|
|
1027
|
+
* @param stream - Stream of packets
|
|
1028
|
+
*
|
|
1029
|
+
* @param output - Media output destination
|
|
1030
|
+
*
|
|
1031
|
+
* @param streamIndex - Pre-allocated stream index in output
|
|
1032
|
+
*
|
|
1033
|
+
* @param shouldStop - Function to check if pipeline should stop
|
|
1034
|
+
*
|
|
1035
|
+
* @param tracker - Optional progress tracker for output statistics
|
|
1036
|
+
*
|
|
1037
|
+
* @internal
|
|
1038
|
+
*/
|
|
1039
|
+
export async function consumeStreamInParallel(stream, output, streamIndex, shouldStop, tracker) {
|
|
1040
|
+
// Get iterator to properly clean up on stop
|
|
1041
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
1042
|
+
try {
|
|
1043
|
+
// Write all packets (including EOF null)
|
|
1044
|
+
while (true) {
|
|
1045
|
+
// Check if we should stop before getting next item
|
|
1046
|
+
if (shouldStop()) {
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
const { value: packet, done } = await iterator.next();
|
|
1050
|
+
if (done)
|
|
1051
|
+
break;
|
|
1052
|
+
try {
|
|
1053
|
+
await output.writePacket(packet, streamIndex);
|
|
1054
|
+
if (packet !== null) {
|
|
1055
|
+
tracker?.record(packet);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
finally {
|
|
1059
|
+
// Free the packet after use (but not null)
|
|
1060
|
+
if (packet && typeof packet.free === 'function') {
|
|
1061
|
+
packet.free();
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
finally {
|
|
1067
|
+
// Always clean up the generator to prevent memory leaks
|
|
1068
|
+
if (iterator.return) {
|
|
1069
|
+
await iterator.return(undefined);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
// Note: Don't close output here - it will be closed by the caller after all streams finish
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Marks a pull that lost the race against the stop request.
|
|
1076
|
+
*
|
|
1077
|
+
* @internal
|
|
1078
|
+
*/
|
|
1079
|
+
const FRAME_SOURCE_STOPPED = Symbol('frameSourceStopped');
|
|
1080
|
+
/**
|
|
1081
|
+
* Wrap a caller-supplied frame source so a stop request unblocks a stalled pull.
|
|
1082
|
+
*
|
|
1083
|
+
* A demuxer-backed pipeline unwinds promptly because stop() interrupts the
|
|
1084
|
+
* blocking read. A frame source offers no such lever: the consuming loop only
|
|
1085
|
+
* notices the stop request once the source yields again, so a source that goes
|
|
1086
|
+
* silent (a wedged capture device, a generator awaiting an event that never
|
|
1087
|
+
* fires) keeps the pipeline - and with it teardown - waiting forever. Calling
|
|
1088
|
+
* `return()` on the source does not help, because an async generator queues
|
|
1089
|
+
* that request behind the pending `next()` that is already stuck.
|
|
1090
|
+
*
|
|
1091
|
+
* Racing every pull against `stopped` supplies the missing lever.
|
|
1092
|
+
*
|
|
1093
|
+
* @param source - Caller-supplied frame source
|
|
1094
|
+
*
|
|
1095
|
+
* @param stopped - Resolves once teardown has been requested
|
|
1096
|
+
*
|
|
1097
|
+
* @yields {Frame | null} Frames from the source until it ends or teardown starts
|
|
1098
|
+
*
|
|
1099
|
+
* @internal
|
|
1100
|
+
*/
|
|
1101
|
+
export async function* interruptibleFrameSource(source, stopped) {
|
|
1102
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
1103
|
+
const stopRequested = stopped.then(() => FRAME_SOURCE_STOPPED);
|
|
1104
|
+
try {
|
|
1105
|
+
for (;;) {
|
|
1106
|
+
const result = await Promise.race([iterator.next(), stopRequested]);
|
|
1107
|
+
if (result === FRAME_SOURCE_STOPPED || result.done) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
yield result.value;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
finally {
|
|
1114
|
+
// Deliberately not awaited: a stalled source never settles the pending
|
|
1115
|
+
// next(), and return() would queue behind it - reintroducing the very hang
|
|
1116
|
+
// this wrapper exists to prevent.
|
|
1117
|
+
void (async () => {
|
|
1118
|
+
try {
|
|
1119
|
+
await iterator.return?.(undefined);
|
|
1120
|
+
}
|
|
1121
|
+
catch {
|
|
1122
|
+
// The source failed while winding down; teardown proceeds regardless.
|
|
1123
|
+
}
|
|
1124
|
+
})();
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Consume a named stream and write to output.
|
|
1129
|
+
*
|
|
1130
|
+
* @param stream - Stream of packets
|
|
1131
|
+
*
|
|
1132
|
+
* @param output - Media output destination
|
|
1133
|
+
*
|
|
1134
|
+
* @param metadata - Stream metadata
|
|
1135
|
+
*
|
|
1136
|
+
* @param shouldStop - Function to check if pipeline should stop
|
|
1137
|
+
*
|
|
1138
|
+
* @param tracker - Optional progress tracker for output statistics
|
|
1139
|
+
*
|
|
1140
|
+
* @internal
|
|
1141
|
+
*/
|
|
1142
|
+
async function consumeNamedStream(stream, output, metadata, shouldStop, tracker) {
|
|
1143
|
+
// Add stream to output
|
|
1144
|
+
let streamIndex = 0;
|
|
1145
|
+
if (metadata.encoder) {
|
|
1146
|
+
// Encoding path
|
|
1147
|
+
if (metadata.decoder) {
|
|
1148
|
+
// Have decoder - use its stream for metadata/properties
|
|
1149
|
+
const originalStream = metadata.decoder.getStream();
|
|
1150
|
+
streamIndex = output.addStream(originalStream, { encoder: metadata.encoder, bsf: metadata.bitStreamFilter });
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
// Encoder-only mode (e.g., frame generator) - no input stream
|
|
1154
|
+
streamIndex = output.addStream(metadata.encoder, { bsf: metadata.bitStreamFilter });
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
else if (metadata.decoder) {
|
|
1158
|
+
// Stream copy - use decoder's original stream
|
|
1159
|
+
const originalStream = metadata.decoder.getStream();
|
|
1160
|
+
streamIndex = output.addStream(originalStream);
|
|
1161
|
+
}
|
|
1162
|
+
else if (metadata.bitStreamFilter) {
|
|
1163
|
+
// BSF - use BSF's original stream
|
|
1164
|
+
const originalStream = metadata.bitStreamFilter.getStream();
|
|
1165
|
+
streamIndex = output.addStream(originalStream);
|
|
1166
|
+
}
|
|
1167
|
+
else if (metadata.demuxer) {
|
|
1168
|
+
// Passthrough from Demuxer - use type hint from metadata
|
|
1169
|
+
const inputStream = metadata.type === 'video' ? metadata.demuxer.video() : metadata.demuxer.audio();
|
|
1170
|
+
if (!inputStream) {
|
|
1171
|
+
throw new Error(`No ${metadata.type} stream found in Demuxer`);
|
|
1172
|
+
}
|
|
1173
|
+
streamIndex = output.addStream(inputStream);
|
|
1174
|
+
}
|
|
1175
|
+
else {
|
|
1176
|
+
// This should not happen with the new API
|
|
1177
|
+
throw new Error('Cannot determine stream configuration. This is likely a bug in the pipeline.');
|
|
1178
|
+
}
|
|
1179
|
+
// Store for later use
|
|
1180
|
+
metadata.streamIndex = streamIndex;
|
|
1181
|
+
// Get iterator to properly clean up on stop
|
|
1182
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
1183
|
+
try {
|
|
1184
|
+
// Write all packets (including EOF null)
|
|
1185
|
+
while (true) {
|
|
1186
|
+
// Check if we should stop before getting next item
|
|
1187
|
+
if (shouldStop()) {
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
const { value: packet, done } = await iterator.next();
|
|
1191
|
+
if (done)
|
|
1192
|
+
break;
|
|
1193
|
+
try {
|
|
1194
|
+
await output.writePacket(packet, streamIndex);
|
|
1195
|
+
if (packet !== null) {
|
|
1196
|
+
tracker?.record(packet);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
finally {
|
|
1200
|
+
// Free the packet after use (but not null)
|
|
1201
|
+
if (packet && typeof packet.free === 'function') {
|
|
1202
|
+
packet.free();
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
finally {
|
|
1208
|
+
// Always clean up the generator to prevent memory leaks
|
|
1209
|
+
if (iterator.return) {
|
|
1210
|
+
await iterator.return(undefined);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
// Note: Output is closed by runNamedPipelineAsync once every stream feeding
|
|
1214
|
+
// it has finished (shared outputs are closed exactly once).
|
|
1215
|
+
}
|
|
1216
|
+
// ============================================================================
|
|
1217
|
+
// Type Guards
|
|
1218
|
+
// ============================================================================
|
|
1219
|
+
/**
|
|
1220
|
+
* Check if object is named inputs.
|
|
1221
|
+
*
|
|
1222
|
+
* @param obj - Object to check
|
|
1223
|
+
*
|
|
1224
|
+
* @returns True if object is NamedInputs
|
|
1225
|
+
*
|
|
1226
|
+
* @internal
|
|
1227
|
+
*/
|
|
1228
|
+
function isNamedInputs(obj) {
|
|
1229
|
+
return obj && typeof obj === 'object' && !Array.isArray(obj) && !isAsyncIterable(obj) && !isDemuxer(obj);
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Check if object is named stages.
|
|
1233
|
+
*
|
|
1234
|
+
* @param obj - Object to check
|
|
1235
|
+
*
|
|
1236
|
+
* @returns True if object is NamedStages
|
|
1237
|
+
*
|
|
1238
|
+
* @internal
|
|
1239
|
+
*/
|
|
1240
|
+
function isNamedStages(obj) {
|
|
1241
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
1242
|
+
return false;
|
|
1243
|
+
}
|
|
1244
|
+
// Check if object has at least one stream name key (video or audio)
|
|
1245
|
+
const keys = Object.keys(obj);
|
|
1246
|
+
return keys.length > 0 && keys.every((key) => key === 'video' || key === 'audio');
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Check if object is async iterable.
|
|
1250
|
+
*
|
|
1251
|
+
* @param obj - Object to check
|
|
1252
|
+
*
|
|
1253
|
+
* @returns True if object is AsyncIterable
|
|
1254
|
+
*
|
|
1255
|
+
* @internal
|
|
1256
|
+
*/
|
|
1257
|
+
function isAsyncIterable(obj) {
|
|
1258
|
+
return obj && typeof obj[Symbol.asyncIterator] === 'function';
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Check if object is Demuxer.
|
|
1262
|
+
*
|
|
1263
|
+
* @param obj - Object to check
|
|
1264
|
+
*
|
|
1265
|
+
* @returns True if object is Demuxer
|
|
1266
|
+
*
|
|
1267
|
+
* @internal
|
|
1268
|
+
*/
|
|
1269
|
+
function isDemuxer(obj) {
|
|
1270
|
+
return obj && typeof obj.packets === 'function' && typeof obj.video === 'function' && typeof obj.audio === 'function';
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Check if object is Decoder.
|
|
1274
|
+
*
|
|
1275
|
+
* @param obj - Object to check
|
|
1276
|
+
*
|
|
1277
|
+
* @returns True if object is Decoder
|
|
1278
|
+
*
|
|
1279
|
+
* @internal
|
|
1280
|
+
*/
|
|
1281
|
+
function isDecoder(obj) {
|
|
1282
|
+
return obj && typeof obj.decode === 'function' && typeof obj.flush === 'function';
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Check if object is Encoder.
|
|
1286
|
+
*
|
|
1287
|
+
* @param obj - Object to check
|
|
1288
|
+
*
|
|
1289
|
+
* @returns True if object is Encoder
|
|
1290
|
+
*
|
|
1291
|
+
* @internal
|
|
1292
|
+
*/
|
|
1293
|
+
function isEncoder(obj) {
|
|
1294
|
+
return obj && typeof obj.encode === 'function' && typeof obj.flush === 'function';
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Check if object is FilterAPI.
|
|
1298
|
+
*
|
|
1299
|
+
* @param obj - Object to check
|
|
1300
|
+
*
|
|
1301
|
+
* @returns True if object is FilterAPI
|
|
1302
|
+
*
|
|
1303
|
+
* @internal
|
|
1304
|
+
*/
|
|
1305
|
+
function isFilterAPI(obj) {
|
|
1306
|
+
return obj && typeof obj.process === 'function' && typeof obj.receive === 'function';
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Check if object is BitStreamFilterAPI.
|
|
1310
|
+
*
|
|
1311
|
+
* @param obj - Object to check
|
|
1312
|
+
*
|
|
1313
|
+
* @returns True if object is BitStreamFilterAPI
|
|
1314
|
+
*
|
|
1315
|
+
* @internal
|
|
1316
|
+
*/
|
|
1317
|
+
function isBitStreamFilterAPI(obj) {
|
|
1318
|
+
return obj && typeof obj.filter === 'function' && typeof obj.flushPackets === 'function' && typeof obj.reset === 'function';
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Check if object is Muxer.
|
|
1322
|
+
*
|
|
1323
|
+
* @param obj - Object to check
|
|
1324
|
+
*
|
|
1325
|
+
* @returns True if object is Muxer
|
|
1326
|
+
*
|
|
1327
|
+
* @internal
|
|
1328
|
+
*/
|
|
1329
|
+
function isMuxer(obj) {
|
|
1330
|
+
return obj && typeof obj.writePacket === 'function' && typeof obj.addStream === 'function';
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Check if object is Packet.
|
|
1334
|
+
*
|
|
1335
|
+
* @param obj - Object to check
|
|
1336
|
+
*
|
|
1337
|
+
* @returns True if object is Packet
|
|
1338
|
+
*
|
|
1339
|
+
* @internal
|
|
1340
|
+
*/
|
|
1341
|
+
function isPacket(obj) {
|
|
1342
|
+
return obj && 'streamIndex' in obj && 'pts' in obj && 'dts' in obj;
|
|
1343
|
+
}
|
|
1344
|
+
//# sourceMappingURL=pipeline.js.map
|