@revizly/node-av 5.2.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BUILD_LINUX.md +61 -0
- package/LICENSE.md +22 -0
- package/README.md +662 -0
- package/build_mac_local.sh +69 -0
- package/dist/api/audio-frame-buffer.d.ts +205 -0
- package/dist/api/audio-frame-buffer.js +287 -0
- package/dist/api/audio-frame-buffer.js.map +1 -0
- package/dist/api/bitstream-filter.d.ts +820 -0
- package/dist/api/bitstream-filter.js +1242 -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 +1088 -0
- package/dist/api/decoder.js +1775 -0
- package/dist/api/decoder.js.map +1 -0
- package/dist/api/demuxer.d.ts +1219 -0
- package/dist/api/demuxer.js +2081 -0
- package/dist/api/demuxer.js.map +1 -0
- package/dist/api/device.d.ts +586 -0
- package/dist/api/device.js +961 -0
- package/dist/api/device.js.map +1 -0
- package/dist/api/encoder.d.ts +1132 -0
- package/dist/api/encoder.js +1988 -0
- package/dist/api/encoder.js.map +1 -0
- package/dist/api/filter-complex.d.ts +821 -0
- package/dist/api/filter-complex.js +1604 -0
- package/dist/api/filter-complex.js.map +1 -0
- package/dist/api/filter-presets.d.ts +1286 -0
- package/dist/api/filter-presets.js +2152 -0
- package/dist/api/filter-presets.js.map +1 -0
- package/dist/api/filter.d.ts +1234 -0
- package/dist/api/filter.js +1976 -0
- package/dist/api/filter.js.map +1 -0
- package/dist/api/fmp4-stream.d.ts +426 -0
- package/dist/api/fmp4-stream.js +739 -0
- package/dist/api/fmp4-stream.js.map +1 -0
- package/dist/api/hardware.d.ts +651 -0
- package/dist/api/hardware.js +1260 -0
- package/dist/api/hardware.js.map +1 -0
- package/dist/api/index.d.ts +17 -0
- package/dist/api/index.js +32 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/io-stream.d.ts +307 -0
- package/dist/api/io-stream.js +282 -0
- package/dist/api/io-stream.js.map +1 -0
- package/dist/api/muxer.d.ts +957 -0
- package/dist/api/muxer.js +2002 -0
- package/dist/api/muxer.js.map +1 -0
- package/dist/api/pipeline.d.ts +607 -0
- package/dist/api/pipeline.js +1145 -0
- package/dist/api/pipeline.js.map +1 -0
- package/dist/api/utilities/async-queue.d.ts +120 -0
- package/dist/api/utilities/async-queue.js +211 -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/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/electron-shared-texture.d.ts +328 -0
- package/dist/api/utilities/electron-shared-texture.js +503 -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 +12 -0
- package/dist/api/utilities/index.js +25 -0
- package/dist/api/utilities/index.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/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/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/whisper-model.d.ts +310 -0
- package/dist/api/utilities/whisper-model.js +528 -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/whisper.d.ts +324 -0
- package/dist/api/whisper.js +362 -0
- package/dist/api/whisper.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 +2325 -0
- package/dist/constants/constants.js +1887 -0
- package/dist/constants/constants.js.map +1 -0
- package/dist/constants/decoders.d.ts +633 -0
- package/dist/constants/decoders.js +641 -0
- package/dist/constants/decoders.js.map +1 -0
- package/dist/constants/encoders.d.ts +295 -0
- package/dist/constants/encoders.js +308 -0
- package/dist/constants/encoders.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 +5 -0
- package/dist/constants/index.js +6 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +99 -0
- package/dist/ffmpeg/index.js +115 -0
- package/dist/ffmpeg/index.js.map +1 -0
- package/dist/ffmpeg/utils.d.ts +31 -0
- package/dist/ffmpeg/utils.js +68 -0
- package/dist/ffmpeg/utils.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 +228 -0
- package/dist/lib/binding.js +60 -0
- package/dist/lib/binding.js.map +1 -0
- package/dist/lib/bitstream-filter-context.d.ts +379 -0
- package/dist/lib/bitstream-filter-context.js +441 -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 +1071 -0
- package/dist/lib/codec-context.js +1354 -0
- package/dist/lib/codec-context.js.map +1 -0
- package/dist/lib/codec-parameters.d.ts +616 -0
- package/dist/lib/codec-parameters.js +761 -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 +586 -0
- package/dist/lib/codec.js +713 -0
- package/dist/lib/codec.js.map +1 -0
- package/dist/lib/device.d.ts +291 -0
- package/dist/lib/device.js +324 -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 +242 -0
- package/dist/lib/error.js +303 -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 +712 -0
- package/dist/lib/filter-context.js +789 -0
- package/dist/lib/filter-context.js.map +1 -0
- package/dist/lib/filter-graph-segment.d.ts +160 -0
- package/dist/lib/filter-graph-segment.js +171 -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 +198 -0
- package/dist/lib/filter-inout.js +257 -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 +1254 -0
- package/dist/lib/format-context.js +1379 -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 +1222 -0
- package/dist/lib/frame.js +1435 -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 +419 -0
- package/dist/lib/hardware-frames-context.js +477 -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 +696 -0
- package/dist/lib/io-context.js +769 -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 +946 -0
- package/dist/lib/native-types.js +2 -0
- package/dist/lib/native-types.js.map +1 -0
- package/dist/lib/option.d.ts +927 -0
- package/dist/lib/option.js +1583 -0
- package/dist/lib/option.js.map +1 -0
- package/dist/lib/output-format.d.ts +180 -0
- package/dist/lib/output-format.js +213 -0
- package/dist/lib/output-format.js.map +1 -0
- package/dist/lib/packet.d.ts +501 -0
- package/dist/lib/packet.js +590 -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 +379 -0
- package/dist/lib/stream.js +526 -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 +1127 -0
- package/dist/lib/utilities.js +1225 -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 +4 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/install/check.js +121 -0
- package/install/ffmpeg.js +66 -0
- package/jellyfin-ffmpeg.patch +181 -0
- package/package.json +129 -0
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Implementation
|
|
3
|
+
// ============================================================================
|
|
4
|
+
/**
|
|
5
|
+
* Pipeline implementation.
|
|
6
|
+
*
|
|
7
|
+
* Creates a processing pipeline from media components.
|
|
8
|
+
* Automatically handles type conversions and proper flushing order.
|
|
9
|
+
*
|
|
10
|
+
* @param args - Variable arguments depending on pipeline type
|
|
11
|
+
*
|
|
12
|
+
* @returns PipelineControl if output is present, AsyncGenerator otherwise
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // Simple pipeline
|
|
17
|
+
* const control = pipeline(
|
|
18
|
+
* input,
|
|
19
|
+
* decoder,
|
|
20
|
+
* filter,
|
|
21
|
+
* encoder,
|
|
22
|
+
* output
|
|
23
|
+
* );
|
|
24
|
+
* await control.completion;
|
|
25
|
+
*
|
|
26
|
+
* // Named pipeline for muxing
|
|
27
|
+
* const control = pipeline(
|
|
28
|
+
* { video: videoInput, audio: audioInput },
|
|
29
|
+
* {
|
|
30
|
+
* video: [videoDecoder, scaleFilter, videoEncoder],
|
|
31
|
+
* audio: [audioDecoder, volumeFilter, audioEncoder]
|
|
32
|
+
* },
|
|
33
|
+
* output
|
|
34
|
+
* );
|
|
35
|
+
* await control.completion;
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function pipeline(...args) {
|
|
39
|
+
// Extract PipelineOptions if last arg is undefined or a PipelineOptions object.
|
|
40
|
+
// PipelineOptions is identified as a plain object whose only known key is 'signal'.
|
|
41
|
+
// This distinguishes it from NamedInputs/NamedStages/NamedOutputs which have stream name keys.
|
|
42
|
+
let pipelineOptions;
|
|
43
|
+
const lastArg = args[args.length - 1];
|
|
44
|
+
if (lastArg === undefined) {
|
|
45
|
+
args.pop();
|
|
46
|
+
}
|
|
47
|
+
else if (args.length > 0 && typeof lastArg === 'object' && lastArg !== null && Object.getPrototypeOf(lastArg) === Object.prototype && 'signal' in lastArg) {
|
|
48
|
+
pipelineOptions = args.pop();
|
|
49
|
+
}
|
|
50
|
+
pipelineOptions?.signal?.throwIfAborted();
|
|
51
|
+
// Detect pipeline type based on first argument
|
|
52
|
+
const firstArg = args[0];
|
|
53
|
+
const secondArg = args[1];
|
|
54
|
+
// Check for shared Demuxer + NamedStages pattern
|
|
55
|
+
if (isDemuxer(firstArg) && isNamedStages(secondArg)) {
|
|
56
|
+
// Convert shared input to NamedInputs based on stages keys
|
|
57
|
+
const sharedInput = firstArg;
|
|
58
|
+
const stages = secondArg;
|
|
59
|
+
const namedInputs = {};
|
|
60
|
+
// Create NamedInputs with shared input for all streams in stages
|
|
61
|
+
for (const streamName of Object.keys(stages)) {
|
|
62
|
+
namedInputs[streamName] = sharedInput;
|
|
63
|
+
}
|
|
64
|
+
if (args.length === 3) {
|
|
65
|
+
// Full named pipeline with output(s)
|
|
66
|
+
return runNamedPipeline(namedInputs, stages, args[2], pipelineOptions);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Partial named pipeline
|
|
70
|
+
return runNamedPartialPipeline(namedInputs, stages);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (isNamedInputs(firstArg)) {
|
|
74
|
+
// Named pipeline (2 or 3 arguments)
|
|
75
|
+
if (args.length === 2) {
|
|
76
|
+
// Partial named pipeline - return generators
|
|
77
|
+
return runNamedPartialPipeline(args[0], args[1]);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Full named pipeline with output
|
|
81
|
+
return runNamedPipeline(args[0], args[1], args[2], pipelineOptions);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (isDemuxer(firstArg)) {
|
|
85
|
+
// Check if this is a stream copy (Demuxer → Muxer)
|
|
86
|
+
if (args.length === 2 && isMuxer(args[1])) {
|
|
87
|
+
// Stream copy all streams
|
|
88
|
+
return runDemuxerPipeline(args[0], args[1], pipelineOptions);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Simple pipeline starting with Demuxer
|
|
92
|
+
return runSimplePipeline(args, pipelineOptions);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// Simple pipeline (variable arguments)
|
|
97
|
+
return runSimplePipeline(args, pipelineOptions);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// PipelineControl Implementation
|
|
102
|
+
// ============================================================================
|
|
103
|
+
/**
|
|
104
|
+
* Pipeline control implementation.
|
|
105
|
+
*
|
|
106
|
+
* @internal
|
|
107
|
+
*/
|
|
108
|
+
class PipelineControlImpl {
|
|
109
|
+
_stopped = false;
|
|
110
|
+
_completion;
|
|
111
|
+
signalCleanup;
|
|
112
|
+
/**
|
|
113
|
+
* @param executionPromise - Promise that resolves when pipeline completes
|
|
114
|
+
*
|
|
115
|
+
* @param signal - Optional AbortSignal for cancellation
|
|
116
|
+
*
|
|
117
|
+
* @internal
|
|
118
|
+
*/
|
|
119
|
+
constructor(executionPromise, signal) {
|
|
120
|
+
// Don't resolve immediately on stop, wait for the actual pipeline to finish
|
|
121
|
+
this._completion = executionPromise;
|
|
122
|
+
if (signal) {
|
|
123
|
+
const handler = () => this.stop();
|
|
124
|
+
signal.addEventListener('abort', handler, { once: true });
|
|
125
|
+
this.signalCleanup = () => signal.removeEventListener('abort', handler);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Stop the pipeline.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* const control = pipeline(input, decoder, filter, encoder, output);
|
|
134
|
+
* control.stop();
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* @see {@link PipelineControl.isStopped}
|
|
138
|
+
*/
|
|
139
|
+
stop() {
|
|
140
|
+
this._stopped = true;
|
|
141
|
+
this.signalCleanup?.();
|
|
142
|
+
this.signalCleanup = undefined;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Check if pipeline is stopped.
|
|
146
|
+
*
|
|
147
|
+
* @returns True if stopped
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* const control = pipeline(input, decoder, filter, encoder, output);
|
|
152
|
+
* const isStopped = control.isStopped();
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
155
|
+
* @see {@link PipelineControl.stop}
|
|
156
|
+
*/
|
|
157
|
+
isStopped() {
|
|
158
|
+
return this._stopped;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get completion promise.
|
|
162
|
+
*/
|
|
163
|
+
get completion() {
|
|
164
|
+
return this._completion;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// Demuxer Pipeline Implementation
|
|
169
|
+
// ============================================================================
|
|
170
|
+
/**
|
|
171
|
+
* Run a demuxer pipeline for stream copy.
|
|
172
|
+
*
|
|
173
|
+
* @param input - Media input source
|
|
174
|
+
*
|
|
175
|
+
* @param output - Media output destination
|
|
176
|
+
*
|
|
177
|
+
* @param options - Pipeline options for cancellation support
|
|
178
|
+
*
|
|
179
|
+
* @returns Pipeline control interface
|
|
180
|
+
*
|
|
181
|
+
* @internal
|
|
182
|
+
*/
|
|
183
|
+
function runDemuxerPipeline(input, output, options) {
|
|
184
|
+
let control;
|
|
185
|
+
// eslint-disable-next-line prefer-const
|
|
186
|
+
control = new PipelineControlImpl(runDemuxerPipelineAsync(input, output, () => control?.isStopped() ?? false), options?.signal);
|
|
187
|
+
return control;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Run demuxer pipeline asynchronously.
|
|
191
|
+
*
|
|
192
|
+
* @param input - Media input source
|
|
193
|
+
*
|
|
194
|
+
* @param output - Media output destination
|
|
195
|
+
*
|
|
196
|
+
* @param shouldStop - Function to check if pipeline should stop
|
|
197
|
+
*
|
|
198
|
+
* @internal
|
|
199
|
+
*/
|
|
200
|
+
async function runDemuxerPipelineAsync(input, output, shouldStop) {
|
|
201
|
+
// Get all streams from input
|
|
202
|
+
const videoStream = input.video();
|
|
203
|
+
const audioStream = input.audio();
|
|
204
|
+
const streams = [];
|
|
205
|
+
// Add video stream if present
|
|
206
|
+
if (videoStream) {
|
|
207
|
+
const outputIndex = output.addStream(videoStream);
|
|
208
|
+
streams.push({ stream: videoStream, index: outputIndex });
|
|
209
|
+
}
|
|
210
|
+
// Add audio stream if present
|
|
211
|
+
if (audioStream) {
|
|
212
|
+
const outputIndex = output.addStream(audioStream);
|
|
213
|
+
streams.push({ stream: audioStream, index: outputIndex });
|
|
214
|
+
}
|
|
215
|
+
// Add any other streams
|
|
216
|
+
const allStreams = input.streams;
|
|
217
|
+
for (const stream of allStreams) {
|
|
218
|
+
// Skip if already added
|
|
219
|
+
if (stream !== videoStream && stream !== audioStream) {
|
|
220
|
+
const outputIndex = output.addStream(stream);
|
|
221
|
+
streams.push({ stream, index: outputIndex });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Get iterator to properly clean up on stop
|
|
225
|
+
const packetsIterable = input.packets();
|
|
226
|
+
const iterator = packetsIterable[Symbol.asyncIterator]();
|
|
227
|
+
try {
|
|
228
|
+
// Copy all packets
|
|
229
|
+
while (true) {
|
|
230
|
+
// Check if we should stop before getting next item
|
|
231
|
+
if (shouldStop()) {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
const { value: packet, done } = await iterator.next();
|
|
235
|
+
if (done)
|
|
236
|
+
break;
|
|
237
|
+
// Handle EOF signal (null packet from input means all streams are done)
|
|
238
|
+
if (packet === null) {
|
|
239
|
+
// Signal EOF for all streams
|
|
240
|
+
for (const mapping of streams) {
|
|
241
|
+
await output.writePacket(null, mapping.index);
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
// Find the corresponding output stream index
|
|
247
|
+
const mapping = streams.find((s) => s.stream.index === packet.streamIndex);
|
|
248
|
+
if (mapping) {
|
|
249
|
+
await output.writePacket(packet, mapping.index);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
// Free the packet after use
|
|
254
|
+
if (packet && typeof packet.free === 'function') {
|
|
255
|
+
packet.free();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
// Always clean up the generator to prevent memory leaks
|
|
262
|
+
if (iterator.return) {
|
|
263
|
+
await iterator.return(undefined);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
await output.close();
|
|
267
|
+
}
|
|
268
|
+
// ============================================================================
|
|
269
|
+
// Simple Pipeline Implementation
|
|
270
|
+
// ============================================================================
|
|
271
|
+
/**
|
|
272
|
+
* Run a simple linear pipeline.
|
|
273
|
+
*
|
|
274
|
+
* @param args - Pipeline arguments
|
|
275
|
+
*
|
|
276
|
+
* @param options - Pipeline options for cancellation support
|
|
277
|
+
*
|
|
278
|
+
* @returns Pipeline control or async generator
|
|
279
|
+
*
|
|
280
|
+
* @internal
|
|
281
|
+
*/
|
|
282
|
+
function runSimplePipeline(args, options) {
|
|
283
|
+
const [source, ...stages] = args;
|
|
284
|
+
// Check if last stage is Muxer (consumes stream)
|
|
285
|
+
const lastStage = stages[stages.length - 1];
|
|
286
|
+
const isOutput = isMuxer(lastStage);
|
|
287
|
+
// Track metadata through pipeline
|
|
288
|
+
const metadata = {};
|
|
289
|
+
// Store Demuxer reference if we have one
|
|
290
|
+
if (isDemuxer(source)) {
|
|
291
|
+
metadata.demuxer = source;
|
|
292
|
+
}
|
|
293
|
+
// Build the pipeline generator
|
|
294
|
+
// If output is present, exclude it from stages for processing
|
|
295
|
+
const processStages = isOutput ? stages.slice(0, -1) : stages;
|
|
296
|
+
// Process metadata first by walking through stages
|
|
297
|
+
for (const stage of processStages) {
|
|
298
|
+
if (isDecoder(stage)) {
|
|
299
|
+
metadata.decoder = stage;
|
|
300
|
+
}
|
|
301
|
+
else if (isEncoder(stage)) {
|
|
302
|
+
metadata.encoder = stage;
|
|
303
|
+
}
|
|
304
|
+
else if (isBitStreamFilterAPI(stage)) {
|
|
305
|
+
metadata.bitStreamFilter = stage;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Convert Demuxer to packet stream if needed
|
|
309
|
+
// If we have a decoder or BSF, filter packets by stream index
|
|
310
|
+
let actualSource;
|
|
311
|
+
if (isDemuxer(source)) {
|
|
312
|
+
if (metadata.decoder) {
|
|
313
|
+
// Filter packets for the decoder's stream
|
|
314
|
+
const streamIndex = metadata.decoder.getStream().index;
|
|
315
|
+
actualSource = source.packets(streamIndex);
|
|
316
|
+
}
|
|
317
|
+
else if (metadata.bitStreamFilter) {
|
|
318
|
+
// Filter packets for the BSF's stream
|
|
319
|
+
const streamIndex = metadata.bitStreamFilter.getStream().index;
|
|
320
|
+
actualSource = source.packets(streamIndex);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
// No decoder or BSF, pass all packets
|
|
324
|
+
actualSource = source.packets();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
actualSource = source;
|
|
329
|
+
}
|
|
330
|
+
const generator = buildSimplePipeline(actualSource, processStages);
|
|
331
|
+
// If output, consume the generator
|
|
332
|
+
if (isOutput) {
|
|
333
|
+
let control;
|
|
334
|
+
// eslint-disable-next-line prefer-const
|
|
335
|
+
control = new PipelineControlImpl(consumeSimplePipeline(generator, lastStage, metadata, () => control?.isStopped() ?? false), options?.signal);
|
|
336
|
+
return control;
|
|
337
|
+
}
|
|
338
|
+
// Otherwise return the generator for further processing
|
|
339
|
+
return generator;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Build a simple pipeline generator.
|
|
343
|
+
*
|
|
344
|
+
* @param source - Source of packets or frames
|
|
345
|
+
*
|
|
346
|
+
* @param stages - Processing stages
|
|
347
|
+
*
|
|
348
|
+
* @yields {Packet | Frame} Processed packets or frames
|
|
349
|
+
*
|
|
350
|
+
* @internal
|
|
351
|
+
*/
|
|
352
|
+
async function* buildSimplePipeline(source, stages) {
|
|
353
|
+
let stream = source;
|
|
354
|
+
for (const stage of stages) {
|
|
355
|
+
if (isDecoder(stage)) {
|
|
356
|
+
stream = stage.frames(stream);
|
|
357
|
+
}
|
|
358
|
+
else if (isEncoder(stage)) {
|
|
359
|
+
stream = stage.packets(stream);
|
|
360
|
+
}
|
|
361
|
+
else if (isFilterAPI(stage)) {
|
|
362
|
+
stream = stage.frames(stream);
|
|
363
|
+
}
|
|
364
|
+
else if (isBitStreamFilterAPI(stage)) {
|
|
365
|
+
stream = stage.packets(stream);
|
|
366
|
+
}
|
|
367
|
+
else if (Array.isArray(stage)) {
|
|
368
|
+
// Chain multiple filters or BSFs
|
|
369
|
+
for (const filter of stage) {
|
|
370
|
+
if (isFilterAPI(filter)) {
|
|
371
|
+
stream = filter.frames(stream);
|
|
372
|
+
}
|
|
373
|
+
else if (isBitStreamFilterAPI(filter)) {
|
|
374
|
+
stream = filter.packets(stream);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
yield* stream;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Consume a simple pipeline stream and write to output.
|
|
383
|
+
*
|
|
384
|
+
* @param stream - Stream of packets or frames
|
|
385
|
+
*
|
|
386
|
+
* @param output - Media output destination
|
|
387
|
+
*
|
|
388
|
+
* @param metadata - Stream metadata
|
|
389
|
+
*
|
|
390
|
+
* @param shouldStop - Function to check if pipeline should stop
|
|
391
|
+
*
|
|
392
|
+
* @internal
|
|
393
|
+
*/
|
|
394
|
+
async function consumeSimplePipeline(stream, output, metadata, shouldStop) {
|
|
395
|
+
// Add stream to output if we have encoder or decoder info
|
|
396
|
+
let streamIndex = 0;
|
|
397
|
+
if (metadata.encoder) {
|
|
398
|
+
// Encoding path
|
|
399
|
+
if (metadata.decoder) {
|
|
400
|
+
// Have decoder - use its stream for metadata/properties
|
|
401
|
+
const originalStream = metadata.decoder.getStream();
|
|
402
|
+
streamIndex = output.addStream(originalStream, { encoder: metadata.encoder });
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
// Encoder-only mode (e.g., frame generator) - no input stream
|
|
406
|
+
streamIndex = output.addStream(metadata.encoder);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else if (metadata.decoder) {
|
|
410
|
+
// Stream copy - use decoder's original stream
|
|
411
|
+
const originalStream = metadata.decoder.getStream();
|
|
412
|
+
streamIndex = output.addStream(originalStream);
|
|
413
|
+
}
|
|
414
|
+
else if (metadata.bitStreamFilter) {
|
|
415
|
+
// BSF without encoder/decoder - use BSF's original stream
|
|
416
|
+
const originalStream = metadata.bitStreamFilter.getStream();
|
|
417
|
+
streamIndex = output.addStream(originalStream);
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
// For direct Demuxer → Muxer, we redirect to runDemuxerPipeline
|
|
421
|
+
// This case shouldn't happen in simple pipeline
|
|
422
|
+
throw new Error('Cannot determine stream configuration. This is likely a bug in the pipeline.');
|
|
423
|
+
}
|
|
424
|
+
// Get iterator to properly clean up on stop
|
|
425
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
426
|
+
try {
|
|
427
|
+
// Process stream
|
|
428
|
+
while (true) {
|
|
429
|
+
// Check if we should stop before getting next item
|
|
430
|
+
if (shouldStop()) {
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
const { value: item, done } = await iterator.next();
|
|
434
|
+
if (done)
|
|
435
|
+
break;
|
|
436
|
+
// Handle EOF signal
|
|
437
|
+
if (item === null) {
|
|
438
|
+
await output.writePacket(null, streamIndex);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
// Use explicit resource management for the item
|
|
442
|
+
try {
|
|
443
|
+
if (isPacket(item) || item === null) {
|
|
444
|
+
await output.writePacket(item, streamIndex);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
throw new Error('Cannot write frames directly to Muxer. Use an encoder first.');
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
// Free the packet/frame after use
|
|
452
|
+
if (item && typeof item.free === 'function') {
|
|
453
|
+
item.free();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
// Always clean up the generator to prevent memory leaks
|
|
460
|
+
if (iterator.return) {
|
|
461
|
+
await iterator.return(undefined);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
await output.close();
|
|
465
|
+
}
|
|
466
|
+
// ============================================================================
|
|
467
|
+
// Named Pipeline Implementation
|
|
468
|
+
// ============================================================================
|
|
469
|
+
/**
|
|
470
|
+
* Run a named partial pipeline.
|
|
471
|
+
*
|
|
472
|
+
* @param inputs - Named input sources
|
|
473
|
+
*
|
|
474
|
+
* @param stages - Named processing stages
|
|
475
|
+
*
|
|
476
|
+
* @returns Record of async generators
|
|
477
|
+
*
|
|
478
|
+
* @internal
|
|
479
|
+
*/
|
|
480
|
+
function runNamedPartialPipeline(inputs, stages) {
|
|
481
|
+
const result = {};
|
|
482
|
+
for (const [streamName, streamStages] of Object.entries(stages)) {
|
|
483
|
+
const input = inputs[streamName];
|
|
484
|
+
if (!input) {
|
|
485
|
+
throw new Error(`No input found for stream: ${streamName}`);
|
|
486
|
+
}
|
|
487
|
+
// Get the appropriate stream based on the stream name
|
|
488
|
+
let stream = null;
|
|
489
|
+
switch (streamName) {
|
|
490
|
+
case 'video':
|
|
491
|
+
stream = input.video() ?? null;
|
|
492
|
+
break;
|
|
493
|
+
case 'audio':
|
|
494
|
+
stream = input.audio() ?? null;
|
|
495
|
+
break;
|
|
496
|
+
default:
|
|
497
|
+
// This should never happen
|
|
498
|
+
throw new Error(`Invalid stream name: ${streamName}. Must be 'video' or 'audio'.`);
|
|
499
|
+
}
|
|
500
|
+
if (!stream) {
|
|
501
|
+
throw new Error(`No ${streamName} stream found in input.`);
|
|
502
|
+
}
|
|
503
|
+
// Normalize stages: if array contains only undefined, treat as passthrough
|
|
504
|
+
// Also filter out undefined entries from the array
|
|
505
|
+
let normalizedStages = streamStages;
|
|
506
|
+
if (Array.isArray(streamStages)) {
|
|
507
|
+
const definedStages = streamStages.filter((stage) => stage !== undefined);
|
|
508
|
+
if (definedStages.length === 0) {
|
|
509
|
+
normalizedStages = 'passthrough';
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
normalizedStages = definedStages;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (normalizedStages === 'passthrough') {
|
|
516
|
+
// Direct passthrough - return input packets for this specific stream
|
|
517
|
+
result[streamName] = (async function* () {
|
|
518
|
+
for await (const packet of input.packets(stream.index)) {
|
|
519
|
+
yield packet;
|
|
520
|
+
}
|
|
521
|
+
})();
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
// Process the stream - pass packets for this specific stream only
|
|
525
|
+
// Build pipeline for this stream (can return frames or packets)
|
|
526
|
+
const metadata = {};
|
|
527
|
+
const stages = normalizedStages;
|
|
528
|
+
result[streamName] = buildFlexibleNamedStreamPipeline(input.packets(stream.index), stages, metadata);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Run a named pipeline with outputs.
|
|
535
|
+
*
|
|
536
|
+
* @param inputs - Named input sources
|
|
537
|
+
*
|
|
538
|
+
* @param stages - Named processing stages
|
|
539
|
+
*
|
|
540
|
+
* @param output - Output destination(s)
|
|
541
|
+
*
|
|
542
|
+
* @param options - Pipeline options for cancellation support
|
|
543
|
+
*
|
|
544
|
+
* @returns Pipeline control interface
|
|
545
|
+
*
|
|
546
|
+
* @internal
|
|
547
|
+
*/
|
|
548
|
+
function runNamedPipeline(inputs, stages, output, options) {
|
|
549
|
+
let control;
|
|
550
|
+
// eslint-disable-next-line prefer-const
|
|
551
|
+
control = new PipelineControlImpl(runNamedPipelineAsync(inputs, stages, output, () => control?.isStopped() ?? false), options?.signal);
|
|
552
|
+
return control;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Run named pipeline asynchronously.
|
|
556
|
+
*
|
|
557
|
+
* @param inputs - Named input sources
|
|
558
|
+
*
|
|
559
|
+
* @param stages - Named processing stages
|
|
560
|
+
*
|
|
561
|
+
* @param output - Output destination(s)
|
|
562
|
+
*
|
|
563
|
+
* @param shouldStop - Function to check if pipeline should stop
|
|
564
|
+
*
|
|
565
|
+
* @internal
|
|
566
|
+
*/
|
|
567
|
+
async function runNamedPipelineAsync(inputs, stages, output, shouldStop) {
|
|
568
|
+
// Check if all inputs reference the same Demuxer instance
|
|
569
|
+
const inputValues = Object.values(inputs);
|
|
570
|
+
const allSameInput = inputValues.length > 1 && inputValues.every((input) => input === inputValues[0]);
|
|
571
|
+
// Track metadata for each stream
|
|
572
|
+
const streamMetadata = {};
|
|
573
|
+
// Process each named stream into generators
|
|
574
|
+
const processedStreams = {};
|
|
575
|
+
// If all inputs are the same instance, use Demuxer's built-in parallel packet generators
|
|
576
|
+
if (allSameInput) {
|
|
577
|
+
const sharedInput = inputValues[0];
|
|
578
|
+
// Single pass: collect metadata and build pipelines directly using input.packets(streamIndex)
|
|
579
|
+
for (const [streamName, streamStages] of Object.entries(stages)) {
|
|
580
|
+
const metadata = {};
|
|
581
|
+
streamMetadata[streamName] = metadata;
|
|
582
|
+
// Normalize stages
|
|
583
|
+
let normalizedStages = streamStages;
|
|
584
|
+
if (Array.isArray(streamStages)) {
|
|
585
|
+
const definedStages = streamStages.filter((stage) => stage !== undefined);
|
|
586
|
+
if (definedStages.length === 0) {
|
|
587
|
+
normalizedStages = 'passthrough';
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
normalizedStages = definedStages;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// Determine stream index and build pipeline
|
|
594
|
+
let streamIndex;
|
|
595
|
+
if (normalizedStages !== 'passthrough') {
|
|
596
|
+
const stages = normalizedStages;
|
|
597
|
+
// Set stream type
|
|
598
|
+
metadata.type = streamName;
|
|
599
|
+
// Populate metadata by walking through ALL stages
|
|
600
|
+
for (const stage of stages) {
|
|
601
|
+
if (isDecoder(stage)) {
|
|
602
|
+
metadata.decoder = stage;
|
|
603
|
+
streamIndex ??= stage.getStream().index;
|
|
604
|
+
}
|
|
605
|
+
else if (isBitStreamFilterAPI(stage)) {
|
|
606
|
+
metadata.bitStreamFilter = stage;
|
|
607
|
+
streamIndex ??= stage.getStream().index;
|
|
608
|
+
}
|
|
609
|
+
else if (isEncoder(stage)) {
|
|
610
|
+
metadata.encoder = stage;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// If no decoder/BSF, use stream name to determine index
|
|
614
|
+
if (streamIndex === undefined) {
|
|
615
|
+
const stream = streamName === 'video' ? sharedInput.video() : sharedInput.audio();
|
|
616
|
+
if (!stream) {
|
|
617
|
+
throw new Error(`No ${streamName} stream found in input.`);
|
|
618
|
+
}
|
|
619
|
+
streamIndex = stream.index;
|
|
620
|
+
}
|
|
621
|
+
// Build pipeline with packets from this specific stream
|
|
622
|
+
processedStreams[streamName] = buildNamedStreamPipeline(sharedInput.packets(streamIndex), stages, metadata);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
// Passthrough - use Demuxer's built-in stream filtering
|
|
626
|
+
metadata.type = streamName;
|
|
627
|
+
metadata.demuxer = sharedInput;
|
|
628
|
+
const stream = streamName === 'video' ? sharedInput.video() : sharedInput.audio();
|
|
629
|
+
if (!stream) {
|
|
630
|
+
throw new Error(`No ${streamName} stream found in input for passthrough.`);
|
|
631
|
+
}
|
|
632
|
+
streamIndex = stream.index;
|
|
633
|
+
// Direct passthrough using input.packets(streamIndex)
|
|
634
|
+
processedStreams[streamName] = sharedInput.packets(streamIndex);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
// Original logic: separate inputs or single input
|
|
640
|
+
for (const [streamName, streamStages] of Object.entries(stages)) {
|
|
641
|
+
const metadata = {};
|
|
642
|
+
streamMetadata[streamName] = metadata;
|
|
643
|
+
const input = inputs[streamName];
|
|
644
|
+
if (!input) {
|
|
645
|
+
throw new Error(`No input found for stream: ${streamName}`);
|
|
646
|
+
}
|
|
647
|
+
// Normalize stages: if array contains only undefined, treat as passthrough
|
|
648
|
+
// Also filter out undefined entries from the array
|
|
649
|
+
let normalizedStages = streamStages;
|
|
650
|
+
if (Array.isArray(streamStages)) {
|
|
651
|
+
const definedStages = streamStages.filter((stage) => stage !== undefined);
|
|
652
|
+
if (definedStages.length === 0) {
|
|
653
|
+
normalizedStages = 'passthrough';
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
normalizedStages = definedStages;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (normalizedStages === 'passthrough') {
|
|
660
|
+
// Direct passthrough - no processing
|
|
661
|
+
let stream = null;
|
|
662
|
+
switch (streamName) {
|
|
663
|
+
case 'video':
|
|
664
|
+
stream = input.video() ?? null;
|
|
665
|
+
metadata.type = 'video';
|
|
666
|
+
break;
|
|
667
|
+
case 'audio':
|
|
668
|
+
stream = input.audio() ?? null;
|
|
669
|
+
metadata.type = 'audio';
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
if (!stream) {
|
|
673
|
+
throw new Error(`No ${streamName} stream found in input for passthrough.`);
|
|
674
|
+
}
|
|
675
|
+
processedStreams[streamName] = input.packets(stream.index);
|
|
676
|
+
metadata.demuxer = input; // Track Demuxer for passthrough
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
// Process the stream - normalizedStages is guaranteed to be an array here
|
|
680
|
+
const stages = normalizedStages;
|
|
681
|
+
// Pre-populate metadata by walking through stages
|
|
682
|
+
for (const stage of stages) {
|
|
683
|
+
if (isDecoder(stage)) {
|
|
684
|
+
metadata.decoder = stage;
|
|
685
|
+
}
|
|
686
|
+
else if (isEncoder(stage)) {
|
|
687
|
+
metadata.encoder = stage;
|
|
688
|
+
}
|
|
689
|
+
else if (isBitStreamFilterAPI(stage)) {
|
|
690
|
+
metadata.bitStreamFilter = stage;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
// Get packets - filter by stream index based on decoder, BSF, or stream type
|
|
694
|
+
let packets;
|
|
695
|
+
if (metadata.decoder) {
|
|
696
|
+
const streamIndex = metadata.decoder.getStream().index;
|
|
697
|
+
packets = input.packets(streamIndex);
|
|
698
|
+
}
|
|
699
|
+
else if (metadata.bitStreamFilter) {
|
|
700
|
+
const streamIndex = metadata.bitStreamFilter.getStream().index;
|
|
701
|
+
packets = input.packets(streamIndex);
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
// No decoder or BSF - determine stream by name
|
|
705
|
+
let stream = null;
|
|
706
|
+
switch (streamName) {
|
|
707
|
+
case 'video':
|
|
708
|
+
stream = input.video() ?? null;
|
|
709
|
+
break;
|
|
710
|
+
case 'audio':
|
|
711
|
+
stream = input.audio() ?? null;
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
if (!stream) {
|
|
715
|
+
throw new Error(`No ${streamName} stream found in input.`);
|
|
716
|
+
}
|
|
717
|
+
packets = input.packets(stream.index);
|
|
718
|
+
}
|
|
719
|
+
// Build pipeline for this stream
|
|
720
|
+
processedStreams[streamName] = buildNamedStreamPipeline(packets, stages, metadata);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
// Write to output(s)
|
|
725
|
+
if (isMuxer(output)) {
|
|
726
|
+
// Always write streams in parallel - Muxer's SyncQueue handles interleaving internally
|
|
727
|
+
const streamIndices = {};
|
|
728
|
+
// Add all streams to output first
|
|
729
|
+
for (const [name, meta] of Object.entries(streamMetadata)) {
|
|
730
|
+
if (meta.encoder) {
|
|
731
|
+
// Encoding path
|
|
732
|
+
if (meta.decoder) {
|
|
733
|
+
// Have decoder - use its stream for metadata/properties
|
|
734
|
+
const originalStream = meta.decoder.getStream();
|
|
735
|
+
streamIndices[name] = output.addStream(originalStream, { encoder: meta.encoder });
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
// Encoder-only mode (e.g., frame generator) - no input stream
|
|
739
|
+
streamIndices[name] = output.addStream(meta.encoder);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
else if (meta.decoder) {
|
|
743
|
+
// Stream copy - use decoder's original stream
|
|
744
|
+
const originalStream = meta.decoder.getStream();
|
|
745
|
+
streamIndices[name] = output.addStream(originalStream);
|
|
746
|
+
}
|
|
747
|
+
else if (meta.bitStreamFilter) {
|
|
748
|
+
// BSF - use BSF's original stream
|
|
749
|
+
const originalStream = meta.bitStreamFilter.getStream();
|
|
750
|
+
streamIndices[name] = output.addStream(originalStream);
|
|
751
|
+
}
|
|
752
|
+
else if (meta.demuxer) {
|
|
753
|
+
// Passthrough from Demuxer
|
|
754
|
+
const stream = name.includes('video') ? meta.demuxer.video() : meta.demuxer.audio();
|
|
755
|
+
if (!stream) {
|
|
756
|
+
throw new Error(`No matching stream found in Demuxer for ${name}`);
|
|
757
|
+
}
|
|
758
|
+
streamIndices[name] = output.addStream(stream);
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
throw new Error(`Cannot determine stream configuration for ${name}. This is likely a bug in the pipeline.`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
// Write all streams in parallel - Muxer's SyncQueue handles interleaving
|
|
765
|
+
const promises = [];
|
|
766
|
+
for (const [name, stream] of Object.entries(processedStreams)) {
|
|
767
|
+
const streamIndex = streamIndices[name];
|
|
768
|
+
if (streamIndex !== undefined) {
|
|
769
|
+
promises.push(consumeStreamInParallel(stream, output, streamIndex, shouldStop));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
await Promise.all(promises);
|
|
773
|
+
await output.close();
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
// Multiple outputs - write each stream to its output
|
|
777
|
+
const outputs = output;
|
|
778
|
+
const promises = [];
|
|
779
|
+
for (const [streamName, stream] of Object.entries(processedStreams)) {
|
|
780
|
+
const streamOutput = outputs[streamName];
|
|
781
|
+
const metadata = streamMetadata[streamName];
|
|
782
|
+
if (streamOutput && metadata) {
|
|
783
|
+
promises.push(consumeNamedStream(stream, streamOutput, metadata, shouldStop));
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
await Promise.all(promises);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Build a flexible named stream pipeline.
|
|
791
|
+
*
|
|
792
|
+
* @param source - Source packets
|
|
793
|
+
*
|
|
794
|
+
* @param stages - Processing stages
|
|
795
|
+
*
|
|
796
|
+
* @param metadata - Stream metadata
|
|
797
|
+
*
|
|
798
|
+
* @yields {Packet | Frame} Processed packets or frames
|
|
799
|
+
*
|
|
800
|
+
* @internal
|
|
801
|
+
*/
|
|
802
|
+
async function* buildFlexibleNamedStreamPipeline(source, stages, metadata) {
|
|
803
|
+
let stream = source;
|
|
804
|
+
for (const stage of stages) {
|
|
805
|
+
if (isDecoder(stage)) {
|
|
806
|
+
metadata.decoder = stage;
|
|
807
|
+
stream = stage.frames(stream);
|
|
808
|
+
}
|
|
809
|
+
else if (isEncoder(stage)) {
|
|
810
|
+
metadata.encoder = stage;
|
|
811
|
+
stream = stage.packets(stream);
|
|
812
|
+
}
|
|
813
|
+
else if (isFilterAPI(stage)) {
|
|
814
|
+
stream = stage.frames(stream);
|
|
815
|
+
}
|
|
816
|
+
else if (isBitStreamFilterAPI(stage)) {
|
|
817
|
+
metadata.bitStreamFilter = stage;
|
|
818
|
+
stream = stage.packets(stream);
|
|
819
|
+
}
|
|
820
|
+
else if (Array.isArray(stage)) {
|
|
821
|
+
// Chain multiple filters or BSFs
|
|
822
|
+
for (const filter of stage) {
|
|
823
|
+
if (isFilterAPI(filter)) {
|
|
824
|
+
stream = filter.frames(stream);
|
|
825
|
+
}
|
|
826
|
+
else if (isBitStreamFilterAPI(filter)) {
|
|
827
|
+
stream = filter.packets(stream);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// Yield whatever the pipeline produces (frames or packets)
|
|
833
|
+
yield* stream;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Build a named stream pipeline.
|
|
837
|
+
*
|
|
838
|
+
* @param source - Source packets
|
|
839
|
+
*
|
|
840
|
+
* @param stages - Processing stages
|
|
841
|
+
*
|
|
842
|
+
* @param metadata - Stream metadata
|
|
843
|
+
*
|
|
844
|
+
* @yields {Packet} Processed packets
|
|
845
|
+
*
|
|
846
|
+
* @internal
|
|
847
|
+
*/
|
|
848
|
+
async function* buildNamedStreamPipeline(source, stages, metadata) {
|
|
849
|
+
let stream = source;
|
|
850
|
+
for (const stage of stages) {
|
|
851
|
+
if (isDecoder(stage)) {
|
|
852
|
+
metadata.decoder = stage;
|
|
853
|
+
stream = stage.frames(stream);
|
|
854
|
+
}
|
|
855
|
+
else if (isEncoder(stage)) {
|
|
856
|
+
metadata.encoder = stage;
|
|
857
|
+
stream = stage.packets(stream);
|
|
858
|
+
}
|
|
859
|
+
else if (isFilterAPI(stage)) {
|
|
860
|
+
stream = stage.frames(stream);
|
|
861
|
+
}
|
|
862
|
+
else if (isBitStreamFilterAPI(stage)) {
|
|
863
|
+
metadata.bitStreamFilter = stage;
|
|
864
|
+
stream = stage.packets(stream);
|
|
865
|
+
}
|
|
866
|
+
else if (Array.isArray(stage)) {
|
|
867
|
+
// Chain multiple filters or BSFs
|
|
868
|
+
for (const filter of stage) {
|
|
869
|
+
if (isFilterAPI(filter)) {
|
|
870
|
+
stream = filter.frames(stream);
|
|
871
|
+
}
|
|
872
|
+
else if (isBitStreamFilterAPI(filter)) {
|
|
873
|
+
stream = filter.packets(stream);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
// Ensure we're yielding packets
|
|
879
|
+
for await (const item of stream) {
|
|
880
|
+
if (isPacket(item) || item === null) {
|
|
881
|
+
yield item;
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
throw new Error('Named pipeline must end with packets (use encoder after filters)');
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Consume a stream in parallel (for passthrough pipelines).
|
|
890
|
+
* Stream index is already added to output.
|
|
891
|
+
*
|
|
892
|
+
* @param stream - Stream of packets
|
|
893
|
+
*
|
|
894
|
+
* @param output - Media output destination
|
|
895
|
+
*
|
|
896
|
+
* @param streamIndex - Pre-allocated stream index in output
|
|
897
|
+
*
|
|
898
|
+
* @param shouldStop - Function to check if pipeline should stop
|
|
899
|
+
*
|
|
900
|
+
* @internal
|
|
901
|
+
*/
|
|
902
|
+
async function consumeStreamInParallel(stream, output, streamIndex, shouldStop) {
|
|
903
|
+
// Get iterator to properly clean up on stop
|
|
904
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
905
|
+
try {
|
|
906
|
+
// Write all packets (including EOF null)
|
|
907
|
+
while (true) {
|
|
908
|
+
// Check if we should stop before getting next item
|
|
909
|
+
if (shouldStop()) {
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
const { value: packet, done } = await iterator.next();
|
|
913
|
+
if (done)
|
|
914
|
+
break;
|
|
915
|
+
try {
|
|
916
|
+
await output.writePacket(packet, streamIndex);
|
|
917
|
+
}
|
|
918
|
+
finally {
|
|
919
|
+
// Free the packet after use (but not null)
|
|
920
|
+
if (packet && typeof packet.free === 'function') {
|
|
921
|
+
packet.free();
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
finally {
|
|
927
|
+
// Always clean up the generator to prevent memory leaks
|
|
928
|
+
if (iterator.return) {
|
|
929
|
+
await iterator.return(undefined);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
// Note: Don't close output here - it will be closed by the caller after all streams finish
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Consume a named stream and write to output.
|
|
936
|
+
*
|
|
937
|
+
* @param stream - Stream of packets
|
|
938
|
+
*
|
|
939
|
+
* @param output - Media output destination
|
|
940
|
+
*
|
|
941
|
+
* @param metadata - Stream metadata
|
|
942
|
+
*
|
|
943
|
+
* @param shouldStop - Function to check if pipeline should stop
|
|
944
|
+
*
|
|
945
|
+
* @internal
|
|
946
|
+
*/
|
|
947
|
+
async function consumeNamedStream(stream, output, metadata, shouldStop) {
|
|
948
|
+
// Add stream to output
|
|
949
|
+
let streamIndex = 0;
|
|
950
|
+
if (metadata.encoder) {
|
|
951
|
+
// Encoding path
|
|
952
|
+
if (metadata.decoder) {
|
|
953
|
+
// Have decoder - use its stream for metadata/properties
|
|
954
|
+
const originalStream = metadata.decoder.getStream();
|
|
955
|
+
streamIndex = output.addStream(originalStream, { encoder: metadata.encoder });
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
// Encoder-only mode (e.g., frame generator) - no input stream
|
|
959
|
+
streamIndex = output.addStream(metadata.encoder);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
else if (metadata.decoder) {
|
|
963
|
+
// Stream copy - use decoder's original stream
|
|
964
|
+
const originalStream = metadata.decoder.getStream();
|
|
965
|
+
streamIndex = output.addStream(originalStream);
|
|
966
|
+
}
|
|
967
|
+
else if (metadata.bitStreamFilter) {
|
|
968
|
+
// BSF - use BSF's original stream
|
|
969
|
+
const originalStream = metadata.bitStreamFilter.getStream();
|
|
970
|
+
streamIndex = output.addStream(originalStream);
|
|
971
|
+
}
|
|
972
|
+
else if (metadata.demuxer) {
|
|
973
|
+
// Passthrough from Demuxer - use type hint from metadata
|
|
974
|
+
const inputStream = metadata.type === 'video' ? metadata.demuxer.video() : metadata.demuxer.audio();
|
|
975
|
+
if (!inputStream) {
|
|
976
|
+
throw new Error(`No ${metadata.type} stream found in Demuxer`);
|
|
977
|
+
}
|
|
978
|
+
streamIndex = output.addStream(inputStream);
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
// This should not happen with the new API
|
|
982
|
+
throw new Error('Cannot determine stream configuration. This is likely a bug in the pipeline.');
|
|
983
|
+
}
|
|
984
|
+
// Store for later use
|
|
985
|
+
metadata.streamIndex = streamIndex;
|
|
986
|
+
// Get iterator to properly clean up on stop
|
|
987
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
988
|
+
try {
|
|
989
|
+
// Write all packets (including EOF null)
|
|
990
|
+
while (true) {
|
|
991
|
+
// Check if we should stop before getting next item
|
|
992
|
+
if (shouldStop()) {
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
995
|
+
const { value: packet, done } = await iterator.next();
|
|
996
|
+
if (done)
|
|
997
|
+
break;
|
|
998
|
+
try {
|
|
999
|
+
await output.writePacket(packet, streamIndex);
|
|
1000
|
+
}
|
|
1001
|
+
finally {
|
|
1002
|
+
// Free the packet after use (but not null)
|
|
1003
|
+
if (packet && typeof packet.free === 'function') {
|
|
1004
|
+
packet.free();
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
finally {
|
|
1010
|
+
// Always clean up the generator to prevent memory leaks
|
|
1011
|
+
if (iterator.return) {
|
|
1012
|
+
await iterator.return(undefined);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
// Note: Output is closed by the caller after all streams finish
|
|
1016
|
+
}
|
|
1017
|
+
// ============================================================================
|
|
1018
|
+
// Type Guards
|
|
1019
|
+
// ============================================================================
|
|
1020
|
+
/**
|
|
1021
|
+
* Check if object is named inputs.
|
|
1022
|
+
*
|
|
1023
|
+
* @param obj - Object to check
|
|
1024
|
+
*
|
|
1025
|
+
* @returns True if object is NamedInputs
|
|
1026
|
+
*
|
|
1027
|
+
* @internal
|
|
1028
|
+
*/
|
|
1029
|
+
function isNamedInputs(obj) {
|
|
1030
|
+
return obj && typeof obj === 'object' && !Array.isArray(obj) && !isAsyncIterable(obj) && !isDemuxer(obj);
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Check if object is named stages.
|
|
1034
|
+
*
|
|
1035
|
+
* @param obj - Object to check
|
|
1036
|
+
*
|
|
1037
|
+
* @returns True if object is NamedStages
|
|
1038
|
+
*
|
|
1039
|
+
* @internal
|
|
1040
|
+
*/
|
|
1041
|
+
function isNamedStages(obj) {
|
|
1042
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
// Check if object has at least one stream name key (video or audio)
|
|
1046
|
+
const keys = Object.keys(obj);
|
|
1047
|
+
return keys.length > 0 && keys.every((key) => key === 'video' || key === 'audio');
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Check if object is async iterable.
|
|
1051
|
+
*
|
|
1052
|
+
* @param obj - Object to check
|
|
1053
|
+
*
|
|
1054
|
+
* @returns True if object is AsyncIterable
|
|
1055
|
+
*
|
|
1056
|
+
* @internal
|
|
1057
|
+
*/
|
|
1058
|
+
function isAsyncIterable(obj) {
|
|
1059
|
+
return obj && typeof obj[Symbol.asyncIterator] === 'function';
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Check if object is Demuxer.
|
|
1063
|
+
*
|
|
1064
|
+
* @param obj - Object to check
|
|
1065
|
+
*
|
|
1066
|
+
* @returns True if object is Demuxer
|
|
1067
|
+
*
|
|
1068
|
+
* @internal
|
|
1069
|
+
*/
|
|
1070
|
+
function isDemuxer(obj) {
|
|
1071
|
+
return obj && typeof obj.packets === 'function' && typeof obj.video === 'function' && typeof obj.audio === 'function';
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Check if object is Decoder.
|
|
1075
|
+
*
|
|
1076
|
+
* @param obj - Object to check
|
|
1077
|
+
*
|
|
1078
|
+
* @returns True if object is Decoder
|
|
1079
|
+
*
|
|
1080
|
+
* @internal
|
|
1081
|
+
*/
|
|
1082
|
+
function isDecoder(obj) {
|
|
1083
|
+
return obj && typeof obj.decode === 'function' && typeof obj.flush === 'function';
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Check if object is Encoder.
|
|
1087
|
+
*
|
|
1088
|
+
* @param obj - Object to check
|
|
1089
|
+
*
|
|
1090
|
+
* @returns True if object is Encoder
|
|
1091
|
+
*
|
|
1092
|
+
* @internal
|
|
1093
|
+
*/
|
|
1094
|
+
function isEncoder(obj) {
|
|
1095
|
+
return obj && typeof obj.encode === 'function' && typeof obj.flush === 'function';
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Check if object is FilterAPI.
|
|
1099
|
+
*
|
|
1100
|
+
* @param obj - Object to check
|
|
1101
|
+
*
|
|
1102
|
+
* @returns True if object is FilterAPI
|
|
1103
|
+
*
|
|
1104
|
+
* @internal
|
|
1105
|
+
*/
|
|
1106
|
+
function isFilterAPI(obj) {
|
|
1107
|
+
return obj && typeof obj.process === 'function' && typeof obj.receive === 'function';
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Check if object is BitStreamFilterAPI.
|
|
1111
|
+
*
|
|
1112
|
+
* @param obj - Object to check
|
|
1113
|
+
*
|
|
1114
|
+
* @returns True if object is BitStreamFilterAPI
|
|
1115
|
+
*
|
|
1116
|
+
* @internal
|
|
1117
|
+
*/
|
|
1118
|
+
function isBitStreamFilterAPI(obj) {
|
|
1119
|
+
return obj && typeof obj.filter === 'function' && typeof obj.flushPackets === 'function' && typeof obj.reset === 'function';
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Check if object is Muxer.
|
|
1123
|
+
*
|
|
1124
|
+
* @param obj - Object to check
|
|
1125
|
+
*
|
|
1126
|
+
* @returns True if object is Muxer
|
|
1127
|
+
*
|
|
1128
|
+
* @internal
|
|
1129
|
+
*/
|
|
1130
|
+
function isMuxer(obj) {
|
|
1131
|
+
return obj && typeof obj.writePacket === 'function' && typeof obj.addStream === 'function';
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Check if object is Packet.
|
|
1135
|
+
*
|
|
1136
|
+
* @param obj - Object to check
|
|
1137
|
+
*
|
|
1138
|
+
* @returns True if object is Packet
|
|
1139
|
+
*
|
|
1140
|
+
* @internal
|
|
1141
|
+
*/
|
|
1142
|
+
function isPacket(obj) {
|
|
1143
|
+
return obj && 'streamIndex' in obj && 'pts' in obj && 'dts' in obj;
|
|
1144
|
+
}
|
|
1145
|
+
//# sourceMappingURL=pipeline.js.map
|