node-av 3.1.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +65 -52
  2. package/binding.gyp +4 -0
  3. package/dist/api/audio-frame-buffer.d.ts +201 -0
  4. package/dist/api/audio-frame-buffer.js +275 -0
  5. package/dist/api/audio-frame-buffer.js.map +1 -0
  6. package/dist/api/bitstream-filter.d.ts +319 -78
  7. package/dist/api/bitstream-filter.js +680 -151
  8. package/dist/api/bitstream-filter.js.map +1 -1
  9. package/dist/api/constants.d.ts +44 -0
  10. package/dist/api/constants.js +45 -0
  11. package/dist/api/constants.js.map +1 -0
  12. package/dist/api/data/test_av1.ivf +0 -0
  13. package/dist/api/data/test_mjpeg.mjpeg +0 -0
  14. package/dist/api/data/test_vp8.ivf +0 -0
  15. package/dist/api/data/test_vp9.ivf +0 -0
  16. package/dist/api/decoder.d.ts +279 -17
  17. package/dist/api/decoder.js +998 -209
  18. package/dist/api/decoder.js.map +1 -1
  19. package/dist/api/{media-input.d.ts → demuxer.d.ts} +294 -44
  20. package/dist/api/demuxer.js +1968 -0
  21. package/dist/api/demuxer.js.map +1 -0
  22. package/dist/api/encoder.d.ts +308 -50
  23. package/dist/api/encoder.js +1133 -111
  24. package/dist/api/encoder.js.map +1 -1
  25. package/dist/api/filter-presets.d.ts +12 -5
  26. package/dist/api/filter-presets.js +21 -7
  27. package/dist/api/filter-presets.js.map +1 -1
  28. package/dist/api/filter.d.ts +406 -40
  29. package/dist/api/filter.js +966 -139
  30. package/dist/api/filter.js.map +1 -1
  31. package/dist/api/{fmp4.d.ts → fmp4-stream.d.ts} +141 -140
  32. package/dist/api/fmp4-stream.js +539 -0
  33. package/dist/api/fmp4-stream.js.map +1 -0
  34. package/dist/api/hardware.d.ts +58 -6
  35. package/dist/api/hardware.js +127 -11
  36. package/dist/api/hardware.js.map +1 -1
  37. package/dist/api/index.d.ts +6 -4
  38. package/dist/api/index.js +14 -8
  39. package/dist/api/index.js.map +1 -1
  40. package/dist/api/io-stream.d.ts +3 -3
  41. package/dist/api/io-stream.js +5 -4
  42. package/dist/api/io-stream.js.map +1 -1
  43. package/dist/api/{media-output.d.ts → muxer.d.ts} +274 -60
  44. package/dist/api/muxer.js +1934 -0
  45. package/dist/api/muxer.js.map +1 -0
  46. package/dist/api/pipeline.d.ts +77 -29
  47. package/dist/api/pipeline.js +435 -425
  48. package/dist/api/pipeline.js.map +1 -1
  49. package/dist/api/rtp-stream.d.ts +312 -0
  50. package/dist/api/rtp-stream.js +630 -0
  51. package/dist/api/rtp-stream.js.map +1 -0
  52. package/dist/api/types.d.ts +476 -55
  53. package/dist/api/utilities/async-queue.d.ts +91 -0
  54. package/dist/api/utilities/async-queue.js +162 -0
  55. package/dist/api/utilities/async-queue.js.map +1 -0
  56. package/dist/api/utilities/audio-sample.d.ts +1 -1
  57. package/dist/api/utilities/image.d.ts +1 -1
  58. package/dist/api/utilities/index.d.ts +2 -0
  59. package/dist/api/utilities/index.js +4 -0
  60. package/dist/api/utilities/index.js.map +1 -1
  61. package/dist/api/utilities/media-type.d.ts +1 -1
  62. package/dist/api/utilities/pixel-format.d.ts +1 -1
  63. package/dist/api/utilities/sample-format.d.ts +1 -1
  64. package/dist/api/utilities/scheduler.d.ts +169 -0
  65. package/dist/api/utilities/scheduler.js +136 -0
  66. package/dist/api/utilities/scheduler.js.map +1 -0
  67. package/dist/api/utilities/streaming.d.ts +74 -15
  68. package/dist/api/utilities/streaming.js +170 -12
  69. package/dist/api/utilities/streaming.js.map +1 -1
  70. package/dist/api/utilities/timestamp.d.ts +1 -1
  71. package/dist/api/webrtc-stream.d.ts +288 -0
  72. package/dist/api/webrtc-stream.js +440 -0
  73. package/dist/api/webrtc-stream.js.map +1 -0
  74. package/dist/constants/constants.d.ts +51 -1
  75. package/dist/constants/constants.js +47 -1
  76. package/dist/constants/constants.js.map +1 -1
  77. package/dist/constants/encoders.d.ts +2 -1
  78. package/dist/constants/encoders.js +4 -3
  79. package/dist/constants/encoders.js.map +1 -1
  80. package/dist/constants/hardware.d.ts +26 -0
  81. package/dist/constants/hardware.js +27 -0
  82. package/dist/constants/hardware.js.map +1 -0
  83. package/dist/constants/index.d.ts +1 -0
  84. package/dist/constants/index.js +1 -0
  85. package/dist/constants/index.js.map +1 -1
  86. package/dist/lib/binding.d.ts +19 -8
  87. package/dist/lib/binding.js.map +1 -1
  88. package/dist/lib/codec-context.d.ts +87 -0
  89. package/dist/lib/codec-context.js +125 -4
  90. package/dist/lib/codec-context.js.map +1 -1
  91. package/dist/lib/codec-parameters.d.ts +183 -1
  92. package/dist/lib/codec-parameters.js +209 -0
  93. package/dist/lib/codec-parameters.js.map +1 -1
  94. package/dist/lib/codec-parser.d.ts +23 -0
  95. package/dist/lib/codec-parser.js +25 -0
  96. package/dist/lib/codec-parser.js.map +1 -1
  97. package/dist/lib/codec.d.ts +26 -4
  98. package/dist/lib/codec.js +35 -0
  99. package/dist/lib/codec.js.map +1 -1
  100. package/dist/lib/dictionary.js +1 -0
  101. package/dist/lib/dictionary.js.map +1 -1
  102. package/dist/lib/error.js +1 -1
  103. package/dist/lib/error.js.map +1 -1
  104. package/dist/lib/filter-context.d.ts +52 -11
  105. package/dist/lib/filter-context.js +56 -12
  106. package/dist/lib/filter-context.js.map +1 -1
  107. package/dist/lib/filter-graph.d.ts +9 -0
  108. package/dist/lib/filter-graph.js +13 -0
  109. package/dist/lib/filter-graph.js.map +1 -1
  110. package/dist/lib/filter.d.ts +21 -0
  111. package/dist/lib/filter.js +28 -0
  112. package/dist/lib/filter.js.map +1 -1
  113. package/dist/lib/format-context.d.ts +48 -14
  114. package/dist/lib/format-context.js +76 -7
  115. package/dist/lib/format-context.js.map +1 -1
  116. package/dist/lib/frame.d.ts +168 -0
  117. package/dist/lib/frame.js +212 -0
  118. package/dist/lib/frame.js.map +1 -1
  119. package/dist/lib/hardware-device-context.d.ts +3 -2
  120. package/dist/lib/hardware-device-context.js.map +1 -1
  121. package/dist/lib/index.d.ts +1 -0
  122. package/dist/lib/index.js +2 -0
  123. package/dist/lib/index.js.map +1 -1
  124. package/dist/lib/input-format.d.ts +21 -0
  125. package/dist/lib/input-format.js +42 -2
  126. package/dist/lib/input-format.js.map +1 -1
  127. package/dist/lib/native-types.d.ts +48 -26
  128. package/dist/lib/option.d.ts +25 -13
  129. package/dist/lib/option.js +28 -0
  130. package/dist/lib/option.js.map +1 -1
  131. package/dist/lib/output-format.d.ts +22 -1
  132. package/dist/lib/output-format.js +28 -0
  133. package/dist/lib/output-format.js.map +1 -1
  134. package/dist/lib/packet.d.ts +35 -0
  135. package/dist/lib/packet.js +52 -2
  136. package/dist/lib/packet.js.map +1 -1
  137. package/dist/lib/stream.d.ts +126 -0
  138. package/dist/lib/stream.js +188 -5
  139. package/dist/lib/stream.js.map +1 -1
  140. package/dist/lib/sync-queue.d.ts +179 -0
  141. package/dist/lib/sync-queue.js +197 -0
  142. package/dist/lib/sync-queue.js.map +1 -0
  143. package/dist/lib/types.d.ts +27 -1
  144. package/dist/lib/utilities.d.ts +281 -53
  145. package/dist/lib/utilities.js +298 -55
  146. package/dist/lib/utilities.js.map +1 -1
  147. package/install/check.js +18 -7
  148. package/package.json +20 -19
  149. package/dist/api/fmp4.js +0 -710
  150. package/dist/api/fmp4.js.map +0 -1
  151. package/dist/api/media-input.js +0 -1075
  152. package/dist/api/media-input.js.map +0 -1
  153. package/dist/api/media-output.js +0 -1040
  154. package/dist/api/media-output.js.map +0 -1
  155. package/dist/api/webrtc.d.ts +0 -664
  156. package/dist/api/webrtc.js +0 -1132
  157. package/dist/api/webrtc.js.map +0 -1
@@ -1,5 +1,66 @@
1
- import { AVERROR_EAGAIN, AVERROR_EOF } from '../constants/constants.js';
2
- import { Codec, CodecContext, Dictionary, FFmpegError, Frame } from '../lib/index.js';
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { AV_CODEC_FLAG_COPY_OPAQUE, AV_FRAME_FLAG_CORRUPT, AV_NOPTS_VALUE, AV_ROUND_UP, AVERROR_EAGAIN, AVERROR_EOF, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_VIDEO, INT_MAX, } from '../constants/constants.js';
54
+ import { CodecContext } from '../lib/codec-context.js';
55
+ import { Codec } from '../lib/codec.js';
56
+ import { Dictionary } from '../lib/dictionary.js';
57
+ import { FFmpegError } from '../lib/error.js';
58
+ import { Frame } from '../lib/frame.js';
59
+ import { Rational } from '../lib/rational.js';
60
+ import { avGcd, avInvQ, avMulQ, avRescaleDelta, avRescaleQ, avRescaleQRnd } from '../lib/utilities.js';
61
+ import { FRAME_THREAD_QUEUE_SIZE, PACKET_THREAD_QUEUE_SIZE } from './constants.js';
62
+ import { AsyncQueue } from './utilities/async-queue.js';
63
+ import { Scheduler } from './utilities/scheduler.js';
3
64
  /**
4
65
  * High-level decoder for audio and video streams.
5
66
  *
@@ -10,10 +71,10 @@ import { Codec, CodecContext, Dictionary, FFmpegError, Frame } from '../lib/inde
10
71
  *
11
72
  * @example
12
73
  * ```typescript
13
- * import { MediaInput, Decoder } from 'node-av/api';
74
+ * import { Demuxer, Decoder } from 'node-av/api';
14
75
  *
15
76
  * // Open media and create decoder
16
- * await using input = await MediaInput.open('video.mp4');
77
+ * await using input = await Demuxer.open('video.mp4');
17
78
  * using decoder = await Decoder.create(input.video());
18
79
  *
19
80
  * // Decode frames
@@ -39,7 +100,7 @@ import { Codec, CodecContext, Dictionary, FFmpegError, Frame } from '../lib/inde
39
100
  * ```
40
101
  *
41
102
  * @see {@link Encoder} For encoding frames to packets
42
- * @see {@link MediaInput} For reading media files
103
+ * @see {@link Demuxer} For reading media files
43
104
  * @see {@link HardwareContext} For GPU acceleration
44
105
  */
45
106
  export class Decoder {
@@ -50,6 +111,19 @@ export class Decoder {
50
111
  initialized = true;
51
112
  isClosed = false;
52
113
  options;
114
+ // Frame tracking for PTS/duration estimation
115
+ lastFramePts = AV_NOPTS_VALUE;
116
+ lastFrameDurationEst = 0n;
117
+ lastFrameTb;
118
+ // Audio-specific frame tracking
119
+ lastFrameSampleRate = 0;
120
+ lastFilterInRescaleDelta = AV_NOPTS_VALUE;
121
+ // Worker pattern for push-based processing
122
+ inputQueue;
123
+ outputQueue;
124
+ workerPromise = null;
125
+ nextComponent = null;
126
+ pipeToPromise = null;
53
127
  /**
54
128
  * @param codecContext - Configured codec context
55
129
  *
@@ -70,71 +144,67 @@ export class Decoder {
70
144
  this.options = options;
71
145
  this.frame = new Frame();
72
146
  this.frame.alloc();
147
+ this.lastFrameTb = new Rational(0, 1);
148
+ this.inputQueue = new AsyncQueue(PACKET_THREAD_QUEUE_SIZE);
149
+ this.outputQueue = new AsyncQueue(FRAME_THREAD_QUEUE_SIZE);
73
150
  }
74
- /**
75
- * Create a decoder for a media stream.
76
- *
77
- * Initializes a decoder with the appropriate codec and configuration.
78
- * Automatically detects and configures hardware acceleration if provided.
79
- * Applies custom codec options and threading configuration.
80
- *
81
- * @param stream - Media stream to decode
82
- *
83
- * @param options - Decoder configuration options
84
- *
85
- * @returns Configured decoder instance
86
- *
87
- * @throws {Error} If decoder not found for codec
88
- *
89
- * @throws {FFmpegError} If codec initialization fails
90
- *
91
- * @example
92
- * ```typescript
93
- * import { MediaInput, Decoder } from 'node-av/api';
94
- *
95
- * await using input = await MediaInput.open('video.mp4');
96
- * using decoder = await Decoder.create(input.video());
97
- * ```
98
- *
99
- * @example
100
- * ```typescript
101
- * using decoder = await Decoder.create(stream, {
102
- * threads: 4,
103
- * options: {
104
- * 'refcounted_frames': '1',
105
- * 'skip_frame': 'nonkey' // Only decode keyframes
106
- * }
107
- * });
108
- * ```
109
- *
110
- * @example
111
- * ```typescript
112
- * const hw = HardwareContext.auto();
113
- * using decoder = await Decoder.create(stream, {
114
- * hardware: hw,
115
- * threads: 0 // Auto-detect thread count
116
- * exitOnError: false // Continue on decode errors (default: true)
117
- * });
118
- * ```
119
- *
120
- * @see {@link HardwareContext} For GPU acceleration setup
121
- * @see {@link DecoderOptions} For configuration options
122
- */
123
- static async create(stream, options = {}) {
151
+ static async create(stream, optionsOrCodec, maybeOptions) {
152
+ // Parse arguments
153
+ let options = {};
154
+ let explicitCodec;
155
+ if (optionsOrCodec !== undefined) {
156
+ // Check if first argument is a codec or options
157
+ if (typeof optionsOrCodec === 'string' || // FFDecoderCodec
158
+ typeof optionsOrCodec === 'number' || // AVCodecID
159
+ optionsOrCodec instanceof Codec // Codec instance
160
+ ) {
161
+ // First argument is a codec
162
+ explicitCodec = optionsOrCodec;
163
+ options = maybeOptions ?? {};
164
+ }
165
+ else {
166
+ // First argument is options
167
+ options = optionsOrCodec;
168
+ }
169
+ }
124
170
  let codec = null;
125
- // If hardware acceleration requested, try to find hardware decoder first
126
- if (options.hardware) {
127
- codec = options.hardware.getDecoderCodec(stream.codecpar.codecId);
128
- if (!codec) {
129
- // No hardware decoder available, fall back to software
130
- options.hardware = undefined;
171
+ // If explicit codec provided, use it
172
+ if (explicitCodec !== undefined) {
173
+ if (typeof explicitCodec === 'object' && 'id' in explicitCodec) {
174
+ // Already a Codec instance
175
+ codec = explicitCodec;
176
+ }
177
+ else if (typeof explicitCodec === 'string') {
178
+ // FFDecoderCodec string
179
+ codec = Codec.findDecoderByName(explicitCodec);
180
+ if (!codec) {
181
+ throw new Error(`Decoder '${explicitCodec}' not found`);
182
+ }
183
+ }
184
+ else {
185
+ // AVCodecID number
186
+ codec = Codec.findDecoder(explicitCodec);
187
+ if (!codec) {
188
+ throw new Error(`Decoder not found for codec ID ${explicitCodec}`);
189
+ }
131
190
  }
132
191
  }
133
- // If no hardware decoder or no hardware requested, use software decoder
134
- if (!codec) {
135
- codec = Codec.findDecoder(stream.codecpar.codecId);
192
+ else {
193
+ // No explicit codec - use auto-detection logic
194
+ // If hardware acceleration requested, try to find hardware decoder first
195
+ if (options.hardware) {
196
+ codec = options.hardware.getDecoderCodec(stream.codecpar.codecId);
197
+ if (!codec) {
198
+ // No hardware decoder available, fall back to software
199
+ options.hardware = undefined;
200
+ }
201
+ }
202
+ // If no hardware decoder or no hardware requested, use software decoder
136
203
  if (!codec) {
137
- throw new Error(`Decoder not found for codec ${stream.codecpar.codecId}`);
204
+ codec = Codec.findDecoder(stream.codecpar.codecId);
205
+ if (!codec) {
206
+ throw new Error(`Decoder not found for codec ${stream.codecpar.codecId}`);
207
+ }
138
208
  }
139
209
  }
140
210
  // Allocate and configure codec context
@@ -148,16 +218,14 @@ export class Decoder {
148
218
  }
149
219
  // Set packet time base
150
220
  codecContext.pktTimebase = stream.timeBase;
151
- // Apply options
152
- if (options.threads !== undefined) {
153
- codecContext.threadCount = options.threads;
154
- }
155
221
  // Check if this decoder supports hardware acceleration
156
222
  // Only apply hardware acceleration if the decoder supports it
157
223
  // Silently ignore hardware for software decoders
158
224
  const isHWDecoder = codec.isHardwareAcceleratedDecoder();
159
225
  if (isHWDecoder && options.hardware) {
160
226
  codecContext.hwDeviceCtx = options.hardware.deviceContext;
227
+ // Set hardware pixel format
228
+ codecContext.setHardwarePixelFormat(options.hardware.devicePixelFormat);
161
229
  // Set extra_hw_frames if specified
162
230
  if (options.extraHWFrames !== undefined && options.extraHWFrames > 0) {
163
231
  codecContext.extraHWFrames = options.extraHWFrames;
@@ -167,6 +235,8 @@ export class Decoder {
167
235
  options.hardware = undefined;
168
236
  }
169
237
  options.exitOnError = options.exitOnError ?? true;
238
+ // Enable COPY_OPAQUE flag to copy packet.opaque to frame.opaque
239
+ codecContext.setFlags(AV_CODEC_FLAG_COPY_OPAQUE);
170
240
  const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
171
241
  // Open codec
172
242
  const openRet = await codecContext.open2(codec, opts);
@@ -174,71 +244,76 @@ export class Decoder {
174
244
  codecContext.freeContext();
175
245
  FFmpegError.throwIfError(openRet, 'Failed to open codec');
176
246
  }
247
+ // Adjust extra_hw_frames for queuing
248
+ // This is done AFTER open2 because the decoder validates extra_hw_frames during open
249
+ if (isHWDecoder && options.hardware) {
250
+ const currentExtraFrames = codecContext.extraHWFrames;
251
+ if (currentExtraFrames >= 0) {
252
+ codecContext.extraHWFrames = currentExtraFrames + FRAME_THREAD_QUEUE_SIZE;
253
+ }
254
+ else {
255
+ codecContext.extraHWFrames = 1;
256
+ }
257
+ }
177
258
  return new Decoder(codecContext, codec, stream, options);
178
259
  }
179
- /**
180
- * Create a decoder for a media stream synchronously.
181
- * Synchronous version of create.
182
- *
183
- * Initializes a decoder with the appropriate codec and configuration.
184
- * Automatically detects and configures hardware acceleration if provided.
185
- * Applies custom codec options and threading configuration.
186
- *
187
- * @param stream - Media stream to decode
188
- *
189
- * @param options - Decoder configuration options
190
- *
191
- * @returns Configured decoder instance
192
- *
193
- * @throws {Error} If decoder not found for codec
194
- *
195
- * @throws {FFmpegError} If codec initialization fails
196
- *
197
- * @example
198
- * ```typescript
199
- * import { MediaInput, Decoder } from 'node-av/api';
200
- *
201
- * await using input = await MediaInput.open('video.mp4');
202
- * using decoder = await Decoder.create(input.video());
203
- * ```
204
- *
205
- * @example
206
- * ```typescript
207
- * using decoder = await Decoder.create(stream, {
208
- * threads: 4,
209
- * options: {
210
- * 'refcounted_frames': '1',
211
- * 'skip_frame': 'nonkey' // Only decode keyframes
212
- * }
213
- * });
214
- * ```
215
- *
216
- * @example
217
- * ```typescript
218
- * const hw = HardwareContext.auto();
219
- * using decoder = await Decoder.create(stream, {
220
- * hardware: hw,
221
- * threads: 0 // Auto-detect thread count
222
- * });
223
- * ```
224
- *
225
- * @see {@link create} For async version
226
- */
227
- static createSync(stream, options = {}) {
260
+ static createSync(stream, optionsOrCodec, maybeOptions) {
261
+ // Parse arguments
262
+ let options = {};
263
+ let explicitCodec;
264
+ if (optionsOrCodec !== undefined) {
265
+ // Check if first argument is a codec or options
266
+ if (typeof optionsOrCodec === 'string' || // FFDecoderCodec
267
+ typeof optionsOrCodec === 'number' || // AVCodecID
268
+ optionsOrCodec instanceof Codec // Codec instance
269
+ ) {
270
+ // First argument is a codec
271
+ explicitCodec = optionsOrCodec;
272
+ options = maybeOptions ?? {};
273
+ }
274
+ else {
275
+ // First argument is options
276
+ options = optionsOrCodec;
277
+ }
278
+ }
228
279
  let codec = null;
229
- // If hardware acceleration requested, try to find hardware decoder first
230
- if (options.hardware) {
231
- codec = options.hardware.getDecoderCodec(stream.codecpar.codecId);
232
- if (!codec) {
233
- // No hardware decoder available, fall back to software
234
- options.hardware = undefined;
280
+ // If explicit codec provided, use it
281
+ if (explicitCodec !== undefined) {
282
+ if (typeof explicitCodec === 'object' && 'id' in explicitCodec) {
283
+ // Already a Codec instance
284
+ codec = explicitCodec;
285
+ }
286
+ else if (typeof explicitCodec === 'string') {
287
+ // FFDecoderCodec string
288
+ codec = Codec.findDecoderByName(explicitCodec);
289
+ if (!codec) {
290
+ throw new Error(`Decoder '${explicitCodec}' not found`);
291
+ }
292
+ }
293
+ else {
294
+ // AVCodecID number
295
+ codec = Codec.findDecoder(explicitCodec);
296
+ if (!codec) {
297
+ throw new Error(`Decoder not found for codec ID ${explicitCodec}`);
298
+ }
235
299
  }
236
300
  }
237
- // If no hardware decoder or no hardware requested, use software decoder
238
- if (!codec) {
239
- codec = Codec.findDecoder(stream.codecpar.codecId);
301
+ else {
302
+ // No explicit codec - use auto-detection logic
303
+ // If hardware acceleration requested, try to find hardware decoder first
304
+ if (options.hardware) {
305
+ codec = options.hardware.getDecoderCodec(stream.codecpar.codecId);
306
+ if (!codec) {
307
+ // No hardware decoder available, fall back to software
308
+ options.hardware = undefined;
309
+ }
310
+ }
311
+ // If no hardware decoder or no hardware requested, use software decoder
240
312
  if (!codec) {
241
- throw new Error(`Decoder not found for codec ${stream.codecpar.codecId}`);
313
+ codec = Codec.findDecoder(stream.codecpar.codecId);
314
+ if (!codec) {
315
+ throw new Error(`Decoder not found for codec ${stream.codecpar.codecId}`);
316
+ }
242
317
  }
243
318
  }
244
319
  // Allocate and configure codec context
@@ -252,16 +327,14 @@ export class Decoder {
252
327
  }
253
328
  // Set packet time base
254
329
  codecContext.pktTimebase = stream.timeBase;
255
- // Apply options
256
- if (options.threads !== undefined) {
257
- codecContext.threadCount = options.threads;
258
- }
259
330
  // Check if this decoder supports hardware acceleration
260
331
  // Only apply hardware acceleration if the decoder supports it
261
332
  // Silently ignore hardware for software decoders
262
333
  const isHWDecoder = codec.isHardwareAcceleratedDecoder();
263
334
  if (isHWDecoder && options.hardware) {
264
335
  codecContext.hwDeviceCtx = options.hardware.deviceContext;
336
+ // Set hardware pixel format and get_format callback
337
+ codecContext.setHardwarePixelFormat(options.hardware.devicePixelFormat);
265
338
  // Set extra_hw_frames if specified
266
339
  if (options.extraHWFrames !== undefined && options.extraHWFrames > 0) {
267
340
  codecContext.extraHWFrames = options.extraHWFrames;
@@ -270,6 +343,9 @@ export class Decoder {
270
343
  else {
271
344
  options.hardware = undefined;
272
345
  }
346
+ options.exitOnError = options.exitOnError ?? true;
347
+ // Enable COPY_OPAQUE flag to copy packet.opaque to frame.opaque
348
+ // codecContext.setFlags(AV_CODEC_FLAG_COPY_OPAQUE);
273
349
  const opts = options.options ? Dictionary.fromObject(options.options) : undefined;
274
350
  // Open codec synchronously
275
351
  const openRet = codecContext.open2Sync(codec, opts);
@@ -277,6 +353,17 @@ export class Decoder {
277
353
  codecContext.freeContext();
278
354
  FFmpegError.throwIfError(openRet, 'Failed to open codec');
279
355
  }
356
+ // Adjust extra_hw_frames for queuing
357
+ // This is done AFTER open2 because the decoder validates extra_hw_frames during open
358
+ if (isHWDecoder && options.hardware) {
359
+ const currentExtraFrames = codecContext.extraHWFrames;
360
+ if (currentExtraFrames >= 0) {
361
+ codecContext.extraHWFrames = currentExtraFrames + FRAME_THREAD_QUEUE_SIZE;
362
+ }
363
+ else {
364
+ codecContext.extraHWFrames = 1;
365
+ }
366
+ }
280
367
  return new Decoder(codecContext, codec, stream, options);
281
368
  }
282
369
  /**
@@ -351,6 +438,10 @@ export class Decoder {
351
438
  * Handles internal buffering - may return null if more packets needed.
352
439
  * Automatically manages decoder state and error recovery.
353
440
  *
441
+ * **Note**: This method receives only ONE frame per call.
442
+ * A single packet can produce multiple frames (e.g., packed B-frames, codec buffering).
443
+ * To receive all frames from a packet, use {@link decodeAll} or {@link frames} instead.
444
+ *
354
445
  * Direct mapping to avcodec_send_packet() and avcodec_receive_frame().
355
446
  *
356
447
  * @param packet - Compressed packet to decode
@@ -382,29 +473,42 @@ export class Decoder {
382
473
  * }
383
474
  * ```
384
475
  *
476
+ * @see {@link decodeAll} For multiple frame decoding
385
477
  * @see {@link frames} For automatic packet iteration
386
478
  * @see {@link flush} For end-of-stream handling
479
+ * @see {@link decodeSync} For synchronous version
387
480
  */
388
481
  async decode(packet) {
389
482
  if (this.isClosed) {
390
483
  return null;
391
484
  }
485
+ // Skip 0-sized packets
486
+ if (packet.size === 0) {
487
+ return null;
488
+ }
392
489
  // Send packet to decoder
393
490
  const sendRet = await this.codecContext.sendPacket(packet);
394
- if (sendRet < 0 && sendRet !== AVERROR_EOF) {
395
- // Decoder might be full, try to receive first
491
+ // Handle EAGAIN: decoder buffer is full, need to read frames first
492
+ // Unlike FFmpeg CLI which reads ALL frames in a loop, our decode() returns
493
+ // only one frame at a time. This means the decoder can still have frames
494
+ // from previous packets when we try to send a new packet.
495
+ if (sendRet === AVERROR_EAGAIN) {
496
+ // Decoder buffer full, receive a frame first
396
497
  const frame = await this.receive();
397
498
  if (frame) {
398
499
  return frame;
399
500
  }
400
- // If still failing, it's an error
401
- if (sendRet !== AVERROR_EAGAIN && this.options.exitOnError) {
402
- FFmpegError.throwIfError(sendRet, 'Failed to send packet');
501
+ // If receive() returned null, this is unexpected - treat as decoder bug
502
+ throw new Error('Decoder returned EAGAIN on send but no frame available - decoder bug');
503
+ }
504
+ // Handle other send errors (matches FFmpeg's error handling)
505
+ if (sendRet < 0 && sendRet !== AVERROR_EOF) {
506
+ if (this.options.exitOnError) {
507
+ FFmpegError.throwIfError(sendRet, 'Failed to send packet to decoder');
403
508
  }
404
509
  }
405
510
  // Try to receive frame
406
- const frame = await this.receive();
407
- return frame;
511
+ return await this.receive();
408
512
  }
409
513
  /**
410
514
  * Decode a packet to frame synchronously.
@@ -414,6 +518,10 @@ export class Decoder {
414
518
  * Handles decoder buffering and error conditions.
415
519
  * May return null if decoder needs more data.
416
520
  *
521
+ * **Note**: This method receives only ONE frame per call.
522
+ * A single packet can produce multiple frames (e.g., packed B-frames, codec buffering).
523
+ * To receive all frames from a packet, use {@link decodeAllSync} or {@link framesSync} instead.
524
+ *
417
525
  * @param packet - Compressed packet to decode
418
526
  *
419
527
  * @returns Decoded frame or null if more data needed or decoder is closed
@@ -428,28 +536,179 @@ export class Decoder {
428
536
  * }
429
537
  * ```
430
538
  *
539
+ * @see {@link decodeAllSync} For multiple frame decoding
540
+ * @see {@link framesSync} For automatic packet iteration
541
+ * @see {@link flushSync} For end-of-stream handling
431
542
  * @see {@link decode} For async version
432
543
  */
433
544
  decodeSync(packet) {
434
545
  if (this.isClosed) {
435
546
  return null;
436
547
  }
548
+ // Skip 0-sized packets
549
+ if (packet.size === 0) {
550
+ return null;
551
+ }
437
552
  // Send packet to decoder
438
553
  const sendRet = this.codecContext.sendPacketSync(packet);
439
- if (sendRet < 0 && sendRet !== AVERROR_EOF) {
440
- // Decoder might be full, try to receive first
554
+ // Handle EAGAIN: decoder buffer is full, need to read frames first
555
+ // Unlike FFmpeg CLI which reads ALL frames in a loop, our decode() returns
556
+ // only one frame at a time. This means the decoder can still have frames
557
+ // from previous packets when we try to send a new packet.
558
+ if (sendRet === AVERROR_EAGAIN) {
559
+ // Decoder buffer full, receive a frame first
441
560
  const frame = this.receiveSync();
442
561
  if (frame) {
443
562
  return frame;
444
563
  }
445
- // If still failing, it's an error
446
- if (sendRet !== AVERROR_EAGAIN) {
447
- FFmpegError.throwIfError(sendRet, 'Failed to send packet');
564
+ // If receive() returned null, this is unexpected - treat as decoder bug
565
+ throw new Error('Decoder returned EAGAIN on send but no frame available - decoder bug');
566
+ }
567
+ // Handle other send errors
568
+ if (sendRet < 0 && sendRet !== AVERROR_EOF) {
569
+ if (this.options.exitOnError) {
570
+ FFmpegError.throwIfError(sendRet, 'Failed to send packet to decoder');
448
571
  }
572
+ // exitOnError=false: Continue to receive
449
573
  }
450
574
  // Try to receive frame
451
- const frame = this.receiveSync();
452
- return frame;
575
+ return this.receiveSync();
576
+ }
577
+ /**
578
+ * Decode a packet to frames.
579
+ *
580
+ * Sends a packet to the decoder and receives all available decoded frames.
581
+ * Returns array of frames - may be empty if decoder needs more data.
582
+ * One packet can produce zero, one, or multiple frames depending on codec.
583
+ * Automatically manages decoder state and error recovery.
584
+ *
585
+ * Direct mapping to avcodec_send_packet() and avcodec_receive_frame().
586
+ *
587
+ * @param packet - Compressed packet to decode
588
+ *
589
+ * @returns Array of decoded frames (empty if more data needed or decoder is closed)
590
+ *
591
+ * @throws {FFmpegError} If decoding fails
592
+ *
593
+ * @example
594
+ * ```typescript
595
+ * const frames = await decoder.decodeAll(packet);
596
+ * for (const frame of frames) {
597
+ * console.log(`Decoded frame with PTS: ${frame.pts}`);
598
+ * frame.free();
599
+ * }
600
+ * ```
601
+ *
602
+ * @example
603
+ * ```typescript
604
+ * for await (const packet of input.packets()) {
605
+ * if (packet.streamIndex === decoder.getStream().index) {
606
+ * const frames = await decoder.decodeAll(packet);
607
+ * for (const frame of frames) {
608
+ * await processFrame(frame);
609
+ * frame.free();
610
+ * }
611
+ * }
612
+ * packet.free();
613
+ * }
614
+ * ```
615
+ *
616
+ * @see {@link decode} For single packet decoding
617
+ * @see {@link frames} For automatic packet iteration
618
+ * @see {@link flush} For end-of-stream handling
619
+ * @see {@link decodeAllSync} For synchronous version
620
+ */
621
+ async decodeAll(packet) {
622
+ if (this.isClosed) {
623
+ return [];
624
+ }
625
+ // Skip 0-sized packets
626
+ if (packet.size === 0) {
627
+ return [];
628
+ }
629
+ // Send packet to decoder
630
+ const sendRet = await this.codecContext.sendPacket(packet);
631
+ // EAGAIN during send_packet is a decoder bug (FFmpeg treats this as AVERROR_BUG)
632
+ // We read all decoded frames with receive() until done, so decoder should never be full
633
+ if (sendRet === AVERROR_EAGAIN) {
634
+ throw new Error('Decoder returned EAGAIN on send - this is a decoder bug');
635
+ }
636
+ // Handle send errors
637
+ if (sendRet < 0 && sendRet !== AVERROR_EOF) {
638
+ if (this.options.exitOnError) {
639
+ FFmpegError.throwIfError(sendRet, 'Failed to send packet to decoder');
640
+ }
641
+ // exitOnError=false: Continue to receive loop to drain any buffered frames
642
+ }
643
+ // Receive all available frames
644
+ const frames = [];
645
+ while (true) {
646
+ const remaining = await this.receive();
647
+ if (!remaining)
648
+ break;
649
+ frames.push(remaining);
650
+ }
651
+ return frames;
652
+ }
653
+ /**
654
+ * Decode a packet to frames synchronously.
655
+ * Synchronous version of decodeAll.
656
+ *
657
+ * Sends packet to decoder and receives all available decoded frames.
658
+ * Returns array of frames - may be empty if decoder needs more data.
659
+ * One packet can produce zero, one, or multiple frames depending on codec.
660
+ *
661
+ * @param packet - Compressed packet to decode
662
+ *
663
+ * @returns Array of decoded frames (empty if more data needed or decoder is closed)
664
+ *
665
+ * @throws {FFmpegError} If decoding fails
666
+ *
667
+ * @example
668
+ * ```typescript
669
+ * const frames = decoder.decodeAllSync(packet);
670
+ * for (const frame of frames) {
671
+ * console.log(`Decoded: ${frame.width}x${frame.height}`);
672
+ * frame.free();
673
+ * }
674
+ * ```
675
+ *
676
+ * @see {@link decodeSync} For single packet decoding
677
+ * @see {@link framesSync} For automatic packet iteration
678
+ * @see {@link flushSync} For end-of-stream handling
679
+ * @see {@link decodeAll} For async version
680
+ */
681
+ decodeAllSync(packet) {
682
+ if (this.isClosed) {
683
+ return [];
684
+ }
685
+ // Skip 0-sized packets
686
+ if (packet.size === 0) {
687
+ return [];
688
+ }
689
+ // Send packet to decoder
690
+ const sendRet = this.codecContext.sendPacketSync(packet);
691
+ // EAGAIN during send_packet is a decoder bug (FFmpeg treats this as AVERROR_BUG)
692
+ // We read all decoded frames with receive() until done, so decoder should never be full
693
+ if (sendRet === AVERROR_EAGAIN) {
694
+ throw new Error('Decoder returned EAGAIN on send - this is a decoder bug');
695
+ }
696
+ // Handle send errors
697
+ if (sendRet < 0 && sendRet !== AVERROR_EOF) {
698
+ if (this.options.exitOnError) {
699
+ FFmpegError.throwIfError(sendRet, 'Failed to send packet to decoder');
700
+ }
701
+ // exitOnError=false: Continue to receive loop to drain any buffered frames
702
+ }
703
+ // Receive all available frames
704
+ const frames = [];
705
+ while (true) {
706
+ const remaining = this.receiveSync();
707
+ if (!remaining)
708
+ break;
709
+ frames.push(remaining);
710
+ }
711
+ return frames;
453
712
  }
454
713
  /**
455
714
  * Decode packet stream to frame stream.
@@ -469,7 +728,7 @@ export class Decoder {
469
728
  *
470
729
  * @example
471
730
  * ```typescript
472
- * await using input = await MediaInput.open('video.mp4');
731
+ * await using input = await Demuxer.open('video.mp4');
473
732
  * using decoder = await Decoder.create(input.video());
474
733
  *
475
734
  * for await (const frame of decoder.frames(input.packets())) {
@@ -503,33 +762,77 @@ export class Decoder {
503
762
  * ```
504
763
  *
505
764
  * @see {@link decode} For single packet decoding
506
- * @see {@link MediaInput.packets} For packet source
765
+ * @see {@link Demuxer.packets} For packet source
766
+ * @see {@link framesSync} For sync version
507
767
  */
508
768
  async *frames(packets) {
509
- // Process packets
510
- for await (const packet of packets) {
769
+ for await (const packet_1 of packets) {
770
+ const env_1 = { stack: [], error: void 0, hasError: false };
511
771
  try {
772
+ const packet = __addDisposableResource(env_1, packet_1, false);
773
+ // Handle EOF signal
774
+ if (packet === null) {
775
+ // Flush decoder
776
+ await this.flush();
777
+ while (true) {
778
+ const remaining = await this.receive();
779
+ if (!remaining)
780
+ break;
781
+ yield remaining;
782
+ }
783
+ // Signal EOF and stop processing
784
+ yield null;
785
+ return;
786
+ }
512
787
  // Only process packets for our stream
513
788
  if (packet.streamIndex === this.stream.index) {
514
- const frame = await this.decode(packet);
515
- if (frame) {
789
+ if (this.isClosed) {
790
+ break;
791
+ }
792
+ // Skip 0-sized packets
793
+ if (packet.size === 0) {
794
+ continue;
795
+ }
796
+ // Send packet to decoder
797
+ const sendRet = await this.codecContext.sendPacket(packet);
798
+ // EAGAIN during send_packet is a decoder bug
799
+ // We read all decoded frames with receive() until done, so decoder should never be full
800
+ if (sendRet === AVERROR_EAGAIN) {
801
+ throw new Error('Decoder returned EAGAIN but no frame available');
802
+ }
803
+ if (sendRet < 0 && sendRet !== AVERROR_EOF) {
804
+ if (this.options.exitOnError) {
805
+ FFmpegError.throwIfError(sendRet, 'Failed to send packet');
806
+ }
807
+ }
808
+ // Receive ALL available frames immediately
809
+ // This ensures frames are yielded ASAP without latency
810
+ while (true) {
811
+ const frame = await this.receive();
812
+ if (!frame)
813
+ break; // EAGAIN or EOF
516
814
  yield frame;
517
815
  }
518
816
  }
519
817
  }
818
+ catch (e_1) {
819
+ env_1.error = e_1;
820
+ env_1.hasError = true;
821
+ }
520
822
  finally {
521
- // Free the input packet after processing
522
- packet.free();
823
+ __disposeResources(env_1);
523
824
  }
524
825
  }
525
- // Flush decoder after all packets
826
+ // Flush decoder after all packets (fallback if no null was sent)
526
827
  await this.flush();
527
- while (!this.isClosed) {
828
+ while (true) {
528
829
  const remaining = await this.receive();
529
830
  if (!remaining)
530
831
  break;
531
832
  yield remaining;
532
833
  }
834
+ // Signal EOF
835
+ yield null;
533
836
  }
534
837
  /**
535
838
  * Decode packet stream to frame stream synchronously.
@@ -555,33 +858,78 @@ export class Decoder {
555
858
  * }
556
859
  * ```
557
860
  *
861
+ * @see {@link decodeSync} For single packet decoding
862
+ * @see {@link Demuxer.packetsSync} For packet source
558
863
  * @see {@link frames} For async version
559
864
  */
560
865
  *framesSync(packets) {
561
- // Process packets
562
- for (const packet of packets) {
866
+ for (const packet_2 of packets) {
867
+ const env_2 = { stack: [], error: void 0, hasError: false };
563
868
  try {
869
+ const packet = __addDisposableResource(env_2, packet_2, false);
870
+ // Handle EOF signal
871
+ if (packet === null) {
872
+ // Flush decoder
873
+ this.flushSync();
874
+ while (true) {
875
+ const remaining = this.receiveSync();
876
+ if (!remaining)
877
+ break;
878
+ yield remaining;
879
+ }
880
+ // Signal EOF and stop processing
881
+ yield null;
882
+ return;
883
+ }
564
884
  // Only process packets for our stream
565
885
  if (packet.streamIndex === this.stream.index) {
566
- const frame = this.decodeSync(packet);
567
- if (frame) {
886
+ if (this.isClosed) {
887
+ break;
888
+ }
889
+ // Skip 0-sized packets
890
+ if (packet.size === 0) {
891
+ continue;
892
+ }
893
+ // Send packet to decoder
894
+ const sendRet = this.codecContext.sendPacketSync(packet);
895
+ // EAGAIN during send_packet is a decoder bug
896
+ // We read all decoded frames with receive() until done, so decoder should never be full
897
+ if (sendRet === AVERROR_EAGAIN) {
898
+ throw new Error('Decoder returned EAGAIN but no frame available');
899
+ }
900
+ if (sendRet < 0 && sendRet !== AVERROR_EOF) {
901
+ if (this.options.exitOnError) {
902
+ FFmpegError.throwIfError(sendRet, 'Failed to send packet');
903
+ }
904
+ }
905
+ // Receive ALL available frames immediately
906
+ // This ensures frames are yielded ASAP without latency
907
+ while (true) {
908
+ const frame = this.receiveSync();
909
+ if (!frame)
910
+ break; // EAGAIN or EOF
568
911
  yield frame;
569
912
  }
570
913
  }
571
914
  }
915
+ catch (e_2) {
916
+ env_2.error = e_2;
917
+ env_2.hasError = true;
918
+ }
572
919
  finally {
573
- // Free the input packet after processing
574
- packet.free();
920
+ __disposeResources(env_2);
575
921
  }
576
922
  }
577
- // Flush decoder after all packets
923
+ // Flush decoder after all packets (fallback if no null was sent)
578
924
  this.flushSync();
579
- while (!this.isClosed) {
925
+ while (true) {
580
926
  const remaining = this.receiveSync();
581
927
  if (!remaining)
582
928
  break;
583
929
  yield remaining;
584
930
  }
931
+ // Signal EOF
932
+ yield null;
585
933
  }
586
934
  /**
587
935
  * Flush decoder and signal end-of-stream.
@@ -609,6 +957,7 @@ export class Decoder {
609
957
  *
610
958
  * @see {@link flushFrames} For convenient async iteration
611
959
  * @see {@link receive} For getting buffered frames
960
+ * @see {@link flushSync} For synchronous version
612
961
  */
613
962
  async flush() {
614
963
  if (this.isClosed) {
@@ -642,6 +991,8 @@ export class Decoder {
642
991
  * }
643
992
  * ```
644
993
  *
994
+ * @see {@link flushFramesSync} For convenient sync iteration
995
+ * @see {@link receiveSync} For getting buffered frames
645
996
  * @see {@link flush} For async version
646
997
  */
647
998
  flushSync() {
@@ -675,15 +1026,18 @@ export class Decoder {
675
1026
  * }
676
1027
  * ```
677
1028
  *
1029
+ * @see {@link decode} For sending packets and receiving frames
678
1030
  * @see {@link flush} For signaling end-of-stream
679
- * @see {@link frames} For complete pipeline
1031
+ * @see {@link flushFramesSync} For synchronous version
680
1032
  */
681
1033
  async *flushFrames() {
682
1034
  // Send flush signal
683
1035
  await this.flush();
684
- let frame;
685
- while ((frame = await this.receive()) !== null) {
686
- yield frame;
1036
+ while (true) {
1037
+ const remaining = await this.receive();
1038
+ if (!remaining)
1039
+ break;
1040
+ yield remaining;
687
1041
  }
688
1042
  }
689
1043
  /**
@@ -706,14 +1060,18 @@ export class Decoder {
706
1060
  * }
707
1061
  * ```
708
1062
  *
1063
+ * @see {@link decodeSync} For sending packets and receiving frames
1064
+ * @see {@link flushSync} For signaling end-of-stream
709
1065
  * @see {@link flushFrames} For async version
710
1066
  */
711
1067
  *flushFramesSync() {
712
1068
  // Send flush signal
713
1069
  this.flushSync();
714
- let frame;
715
- while ((frame = this.receiveSync()) !== null) {
716
- yield frame;
1070
+ while (true) {
1071
+ const remaining = this.receiveSync();
1072
+ if (!remaining)
1073
+ break;
1074
+ yield remaining;
717
1075
  }
718
1076
  }
719
1077
  /**
@@ -751,29 +1109,50 @@ export class Decoder {
751
1109
  *
752
1110
  * @see {@link decode} For sending packets and receiving frames
753
1111
  * @see {@link flush} For signaling end-of-stream
1112
+ * @see {@link receiveSync} For synchronous version
754
1113
  */
755
1114
  async receive() {
756
- // Clear previous frame data
757
- this.frame.unref();
758
- if (this.isClosed) {
759
- return null;
760
- }
761
- const ret = await this.codecContext.receiveFrame(this.frame);
762
- if (ret === 0) {
763
- // Got a frame, clone it for the user
764
- return this.frame.clone();
765
- }
766
- else if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
767
- // Need more data or end of stream
768
- return null;
769
- }
770
- else {
771
- // Error
772
- if (this.options.exitOnError) {
773
- FFmpegError.throwIfError(ret, 'Failed to receive frame');
1115
+ // When exitOnError=false, continue on errors until we get a frame or EAGAIN/EOF
1116
+ while (!this.isClosed) {
1117
+ // Clear previous frame data
1118
+ this.frame.unref();
1119
+ const ret = await this.codecContext.receiveFrame(this.frame);
1120
+ if (ret === 0) {
1121
+ // Set frame time_base to decoder's packet timebase
1122
+ this.frame.timeBase = this.codecContext.pktTimebase;
1123
+ // Check for corrupt frame
1124
+ if (this.frame.decodeErrorFlags || this.frame.hasFlags(AV_FRAME_FLAG_CORRUPT)) {
1125
+ if (this.options.exitOnError) {
1126
+ throw new Error('Corrupt decoded frame detected');
1127
+ }
1128
+ // exitOnError=false: skip corrupt frame, continue to next
1129
+ continue;
1130
+ }
1131
+ // Handles PTS assignment, duration estimation, and frame tracking
1132
+ if (this.codecContext.codecType === AVMEDIA_TYPE_VIDEO) {
1133
+ this.processVideoFrame(this.frame);
1134
+ }
1135
+ // Handles timestamp extrapolation, sample rate changes, and duration calculation
1136
+ if (this.codecContext.codecType === AVMEDIA_TYPE_AUDIO) {
1137
+ this.processAudioFrame(this.frame);
1138
+ }
1139
+ // Got a frame, clone it for the user
1140
+ return this.frame.clone();
1141
+ }
1142
+ else if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
1143
+ // Need more data or end of stream
1144
+ return null;
1145
+ }
1146
+ else {
1147
+ // Error during receive
1148
+ if (this.options.exitOnError) {
1149
+ FFmpegError.throwIfError(ret, 'Failed to receive frame');
1150
+ }
1151
+ // exitOnError=false: continue to next frame
1152
+ continue;
774
1153
  }
775
- return null;
776
1154
  }
1155
+ return null;
777
1156
  }
778
1157
  /**
779
1158
  * Receive frame from decoder synchronously.
@@ -809,30 +1188,72 @@ export class Decoder {
809
1188
  * }
810
1189
  * ```
811
1190
  *
1191
+ * @see {@link decodeSync} For sending packets and receiving frames
1192
+ * @see {@link flushSync} For signaling end-of-stream
812
1193
  * @see {@link receive} For async version
813
1194
  */
814
1195
  receiveSync() {
815
- // Clear previous frame data
816
- this.frame.unref();
817
- if (this.isClosed) {
818
- return null;
819
- }
820
- const ret = this.codecContext.receiveFrameSync(this.frame);
821
- if (ret === 0) {
822
- // Got a frame, clone it for the user
823
- return this.frame.clone();
824
- }
825
- else if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
826
- // Need more data or end of stream
827
- return null;
828
- }
829
- else {
830
- // Error
831
- if (this.options.exitOnError) {
832
- FFmpegError.throwIfError(ret, 'Failed to receive frame');
1196
+ // When exitOnError=false, continue on errors until we get a frame or EAGAIN/EOF
1197
+ while (!this.isClosed) {
1198
+ // Clear previous frame data
1199
+ this.frame.unref();
1200
+ const ret = this.codecContext.receiveFrameSync(this.frame);
1201
+ if (ret === 0) {
1202
+ // Set frame time_base to decoder's packet timebase
1203
+ this.frame.timeBase = this.codecContext.pktTimebase;
1204
+ // Check for corrupt frame
1205
+ if (this.frame.decodeErrorFlags || this.frame.hasFlags(AV_FRAME_FLAG_CORRUPT)) {
1206
+ if (this.options.exitOnError) {
1207
+ throw new Error('Corrupt decoded frame detected');
1208
+ }
1209
+ // exitOnError=false: skip corrupt frame, continue to next
1210
+ continue;
1211
+ }
1212
+ // Process video frame
1213
+ // Handles PTS assignment, duration estimation, and frame tracking
1214
+ if (this.codecContext.codecType === AVMEDIA_TYPE_VIDEO) {
1215
+ this.processVideoFrame(this.frame);
1216
+ }
1217
+ // Process audio frame
1218
+ // Handles timestamp extrapolation, sample rate changes, and duration calculation
1219
+ if (this.codecContext.codecType === AVMEDIA_TYPE_AUDIO) {
1220
+ this.processAudioFrame(this.frame);
1221
+ }
1222
+ // Got a frame, clone it for the user
1223
+ return this.frame.clone();
1224
+ }
1225
+ else if (ret === AVERROR_EAGAIN || ret === AVERROR_EOF) {
1226
+ // Need more data or end of stream
1227
+ return null;
1228
+ }
1229
+ else {
1230
+ // Error during receive
1231
+ if (this.options.exitOnError) {
1232
+ FFmpegError.throwIfError(ret, 'Failed to receive frame');
1233
+ }
1234
+ // exitOnError=false: continue to next frame
1235
+ continue;
833
1236
  }
834
- return null;
835
1237
  }
1238
+ return null;
1239
+ }
1240
+ pipeTo(target) {
1241
+ const t = target;
1242
+ // Store reference to next component for flush propagation
1243
+ this.nextComponent = t;
1244
+ // Start worker if not already running
1245
+ this.workerPromise ??= this.runWorker();
1246
+ // Start pipe task: decoder.outputQueue -> target.inputQueue (via target.send)
1247
+ this.pipeToPromise = (async () => {
1248
+ while (true) {
1249
+ const frame = await this.receiveFromQueue();
1250
+ if (!frame)
1251
+ break;
1252
+ await t.sendToQueue(frame);
1253
+ }
1254
+ })();
1255
+ // Return scheduler for chaining (target is now the last component)
1256
+ return new Scheduler(this, t);
836
1257
  }
837
1258
  /**
838
1259
  * Close decoder and free resources.
@@ -858,6 +1279,8 @@ export class Decoder {
858
1279
  return;
859
1280
  }
860
1281
  this.isClosed = true;
1282
+ this.inputQueue?.close();
1283
+ this.outputQueue?.close();
861
1284
  this.frame.free();
862
1285
  this.codecContext.freeContext();
863
1286
  this.initialized = false;
@@ -908,6 +1331,372 @@ export class Decoder {
908
1331
  getCodecContext() {
909
1332
  return !this.isClosed && this.initialized ? this.codecContext : null;
910
1333
  }
1334
+ /**
1335
+ * Worker loop for push-based processing.
1336
+ *
1337
+ * @internal
1338
+ */
1339
+ async runWorker() {
1340
+ try {
1341
+ // Outer loop - receive packets
1342
+ while (!this.inputQueue.isClosed) {
1343
+ const env_3 = { stack: [], error: void 0, hasError: false };
1344
+ try {
1345
+ const packet = __addDisposableResource(env_3, await this.inputQueue.receive(), false);
1346
+ if (!packet)
1347
+ break;
1348
+ // Skip packets for other streams
1349
+ if (packet.streamIndex !== this.stream.index) {
1350
+ continue;
1351
+ }
1352
+ if (packet.size === 0) {
1353
+ continue;
1354
+ }
1355
+ // Send packet to decoder
1356
+ const sendRet = await this.codecContext.sendPacket(packet);
1357
+ // EAGAIN during send_packet is a decoder bug
1358
+ // We read all decoded frames with receive() until done, so decoder should never be full
1359
+ if (sendRet === AVERROR_EAGAIN) {
1360
+ throw new Error('Decoder returned EAGAIN but no frame available');
1361
+ }
1362
+ if (sendRet < 0 && sendRet !== AVERROR_EOF) {
1363
+ if (this.options.exitOnError) {
1364
+ FFmpegError.throwIfError(sendRet, 'Failed to send packet');
1365
+ }
1366
+ }
1367
+ // Receive ALL available frames immediately
1368
+ // This ensures frames are yielded ASAP without latency
1369
+ while (!this.outputQueue.isClosed) {
1370
+ const frame = await this.receive();
1371
+ if (!frame)
1372
+ break; // EAGAIN or EOF
1373
+ await this.outputQueue.send(frame);
1374
+ }
1375
+ }
1376
+ catch (e_3) {
1377
+ env_3.error = e_3;
1378
+ env_3.hasError = true;
1379
+ }
1380
+ finally {
1381
+ __disposeResources(env_3);
1382
+ }
1383
+ }
1384
+ // Flush decoder at end
1385
+ await this.flush();
1386
+ while (!this.outputQueue.isClosed) {
1387
+ const frame = await this.receive();
1388
+ if (!frame)
1389
+ break;
1390
+ await this.outputQueue.send(frame);
1391
+ }
1392
+ }
1393
+ catch {
1394
+ // Ignore ?
1395
+ }
1396
+ finally {
1397
+ // Close output queue when done
1398
+ this.outputQueue?.close();
1399
+ }
1400
+ }
1401
+ /**
1402
+ * Send packet to input queue.
1403
+ *
1404
+ * @param packet - Packet to send
1405
+ *
1406
+ * @internal
1407
+ */
1408
+ async sendToQueue(packet) {
1409
+ await this.inputQueue.send(packet);
1410
+ }
1411
+ /**
1412
+ * Receive frame from output queue.
1413
+ *
1414
+ * @returns Frame from output queue or null if closed
1415
+ *
1416
+ * @internal
1417
+ */
1418
+ async receiveFromQueue() {
1419
+ return await this.outputQueue.receive();
1420
+ }
1421
+ /**
1422
+ * Flush the entire filter pipeline.
1423
+ *
1424
+ * Propagates flush through worker, output queue, and next component.
1425
+ *
1426
+ * @internal
1427
+ */
1428
+ async flushPipeline() {
1429
+ // Close input queue to signal end of stream to worker
1430
+ this.inputQueue.close();
1431
+ // Wait for worker to finish processing all packets (if exists)
1432
+ if (this.workerPromise) {
1433
+ await this.workerPromise;
1434
+ }
1435
+ // Flush decoder at end
1436
+ await this.flush();
1437
+ // Send all flushed frames to output queue
1438
+ while (true) {
1439
+ const frame = await this.receive();
1440
+ if (!frame)
1441
+ break;
1442
+ await this.outputQueue.send(frame);
1443
+ }
1444
+ // Close output queue to signal end of stream to pipeTo() task
1445
+ this.outputQueue.close();
1446
+ // Wait for pipeTo() task to finish processing all frames (if exists)
1447
+ if (this.pipeToPromise) {
1448
+ await this.pipeToPromise;
1449
+ }
1450
+ // Then propagate flush to next component
1451
+ if (this.nextComponent) {
1452
+ await this.nextComponent.flushPipeline();
1453
+ }
1454
+ }
1455
+ /**
1456
+ * Estimate video frame duration.
1457
+ *
1458
+ * Implements FFmpeg CLI's video_duration_estimate() logic.
1459
+ * Uses multiple heuristics to determine frame duration when not explicitly available:
1460
+ * 1. Frame duration from container (if reliable)
1461
+ * 2. Duration from codec framerate
1462
+ * 3. PTS difference between frames
1463
+ * 4. Stream framerate
1464
+ * 5. Last frame's estimated duration
1465
+ *
1466
+ * @param frame - Frame to estimate duration for
1467
+ *
1468
+ * @returns Estimated duration in frame's timebase units
1469
+ *
1470
+ * @internal
1471
+ */
1472
+ estimateVideoDuration(frame) {
1473
+ // Difference between this and last frame's timestamps
1474
+ const tsDiff = frame.pts !== AV_NOPTS_VALUE && this.lastFramePts !== AV_NOPTS_VALUE ? frame.pts - this.lastFramePts : -1n;
1475
+ // Frame duration is unreliable (typically guessed by lavf) when it is equal
1476
+ // to 1 and the actual duration of the last frame is more than 2x larger
1477
+ const durationUnreliable = frame.duration === 1n && tsDiff > 2n * frame.duration;
1478
+ // Prefer frame duration for containers with timestamps
1479
+ if (frame.duration > 0n && !durationUnreliable) {
1480
+ return frame.duration;
1481
+ }
1482
+ // Calculate codec duration from framerate
1483
+ let codecDuration = 0n;
1484
+ const framerate = this.codecContext.framerate;
1485
+ if (framerate && framerate.den > 0 && framerate.num > 0) {
1486
+ const fields = (frame.repeatPict ?? 0) + 2;
1487
+ const fieldRate = avMulQ(framerate, { num: 2, den: 1 });
1488
+ codecDuration = avRescaleQ(fields, avInvQ(fieldRate), frame.timeBase);
1489
+ }
1490
+ // When timestamps are available, repeat last frame's actual duration
1491
+ if (tsDiff > 0n) {
1492
+ return tsDiff;
1493
+ }
1494
+ // Try frame/codec duration
1495
+ if (frame.duration > 0n) {
1496
+ return frame.duration;
1497
+ }
1498
+ if (codecDuration > 0n) {
1499
+ return codecDuration;
1500
+ }
1501
+ // Try stream framerate
1502
+ const streamFramerate = this.stream.avgFrameRate ?? this.stream.rFrameRate;
1503
+ if (streamFramerate && streamFramerate.num > 0 && streamFramerate.den > 0) {
1504
+ const d = avRescaleQ(1, avInvQ(streamFramerate), frame.timeBase);
1505
+ if (d > 0n) {
1506
+ return d;
1507
+ }
1508
+ }
1509
+ // Last resort is last frame's estimated duration, and 1
1510
+ return this.lastFrameDurationEst > 0n ? this.lastFrameDurationEst : 1n;
1511
+ }
1512
+ /**
1513
+ * Process video frame after decoding.
1514
+ *
1515
+ * Implements FFmpeg CLI's video_frame_process() logic.
1516
+ * Handles:
1517
+ * - Hardware frame transfer to software format
1518
+ * - PTS assignment from best_effort_timestamp
1519
+ * - PTS extrapolation when missing
1520
+ * - Duration estimation
1521
+ * - Frame tracking for next frame
1522
+ *
1523
+ * @param frame - Decoded frame to process
1524
+ *
1525
+ * @internal
1526
+ */
1527
+ processVideoFrame(frame) {
1528
+ // Hardware acceleration retrieve
1529
+ // If hwaccel_output_format is set and frame is in hardware format, transfer to software format
1530
+ if (this.options.hwaccelOutputFormat !== undefined && frame.isHwFrame()) {
1531
+ const swFrame = new Frame();
1532
+ swFrame.alloc();
1533
+ swFrame.format = this.options.hwaccelOutputFormat;
1534
+ // Transfer data from hardware to software frame
1535
+ const ret = frame.hwframeTransferDataSync(swFrame, 0);
1536
+ if (ret < 0) {
1537
+ swFrame.free();
1538
+ if (this.options.exitOnError) {
1539
+ FFmpegError.throwIfError(ret, 'Failed to transfer hardware frame data');
1540
+ }
1541
+ return;
1542
+ }
1543
+ // Copy properties from hw frame to sw frame
1544
+ swFrame.copyProps(frame);
1545
+ // Replace frame with software version (unref old, move ref)
1546
+ frame.unref();
1547
+ const refRet = frame.ref(swFrame);
1548
+ swFrame.free();
1549
+ if (refRet < 0) {
1550
+ if (this.options.exitOnError) {
1551
+ FFmpegError.throwIfError(refRet, 'Failed to reference software frame');
1552
+ }
1553
+ return;
1554
+ }
1555
+ }
1556
+ // Set PTS from best_effort_timestamp
1557
+ frame.pts = frame.bestEffortTimestamp;
1558
+ // DECODER_FLAG_FRAMERATE_FORCED: Ignores all timestamps and generates constant framerate
1559
+ if (this.options.forcedFramerate) {
1560
+ frame.pts = AV_NOPTS_VALUE;
1561
+ frame.duration = 1n;
1562
+ const invFramerate = avInvQ(this.options.forcedFramerate);
1563
+ frame.timeBase = new Rational(invFramerate.num, invFramerate.den);
1564
+ }
1565
+ // No timestamp available - extrapolate from previous frame duration
1566
+ if (frame.pts === AV_NOPTS_VALUE) {
1567
+ frame.pts = this.lastFramePts === AV_NOPTS_VALUE ? 0n : this.lastFramePts + this.lastFrameDurationEst;
1568
+ }
1569
+ // Update timestamp history
1570
+ this.lastFrameDurationEst = this.estimateVideoDuration(frame);
1571
+ this.lastFramePts = frame.pts;
1572
+ this.lastFrameTb = new Rational(frame.timeBase.num, frame.timeBase.den);
1573
+ // SAR override
1574
+ if (this.options.sarOverride) {
1575
+ frame.sampleAspectRatio = new Rational(this.options.sarOverride.num, this.options.sarOverride.den);
1576
+ }
1577
+ // Apply cropping
1578
+ if (this.options.applyCropping) {
1579
+ const ret = frame.applyCropping(1); // AV_FRAME_CROP_UNALIGNED = 1
1580
+ if (ret < 0) {
1581
+ if (this.options.exitOnError) {
1582
+ FFmpegError.throwIfError(ret, 'Error applying decoder cropping');
1583
+ }
1584
+ }
1585
+ }
1586
+ }
1587
+ /**
1588
+ * Audio samplerate update - handles sample rate changes.
1589
+ *
1590
+ * Based on FFmpeg's audio_samplerate_update().
1591
+ *
1592
+ * On sample rate change, chooses a new internal timebase that can represent
1593
+ * timestamps from all sample rates seen so far. Uses GCD to find minimal
1594
+ * common timebase, with fallback to LCM of common sample rates (28224000).
1595
+ *
1596
+ * Handles:
1597
+ * - Sample rate change detection
1598
+ * - Timebase calculation via GCD
1599
+ * - Overflow detection and fallback
1600
+ * - Frame timebase optimization
1601
+ * - Rescaling existing timestamps
1602
+ *
1603
+ * @param frame - Audio frame to process
1604
+ *
1605
+ * @returns Timebase to use for this frame
1606
+ *
1607
+ * @internal
1608
+ */
1609
+ audioSamplerateUpdate(frame) {
1610
+ const prev = this.lastFrameTb.den;
1611
+ const sr = frame.sampleRate;
1612
+ // No change - return existing timebase
1613
+ if (frame.sampleRate === this.lastFrameSampleRate) {
1614
+ return this.lastFrameTb;
1615
+ }
1616
+ // Calculate GCD to find minimal common timebase
1617
+ const gcd = avGcd(prev, sr);
1618
+ let tbNew;
1619
+ // Check for overflow
1620
+ if (Number(prev) / Number(gcd) >= INT_MAX / sr) {
1621
+ // LCM of 192000, 44100 - represents all common sample rates
1622
+ tbNew = { num: 1, den: 28224000 };
1623
+ }
1624
+ else {
1625
+ // Normal case
1626
+ tbNew = { num: 1, den: (Number(prev) / Number(gcd)) * sr };
1627
+ }
1628
+ // Keep frame's timebase if strictly better
1629
+ // "Strictly better" means: num=1, den > tbNew.den, and tbNew.den divides den evenly
1630
+ if (frame.timeBase.num === 1 && frame.timeBase.den > tbNew.den && frame.timeBase.den % tbNew.den === 0) {
1631
+ tbNew = { num: frame.timeBase.num, den: frame.timeBase.den };
1632
+ }
1633
+ // Rescale existing timestamps to new timebase
1634
+ if (this.lastFramePts !== AV_NOPTS_VALUE) {
1635
+ this.lastFramePts = avRescaleQ(this.lastFramePts, this.lastFrameTb, tbNew);
1636
+ }
1637
+ this.lastFrameDurationEst = avRescaleQ(this.lastFrameDurationEst, this.lastFrameTb, tbNew);
1638
+ this.lastFrameTb = new Rational(tbNew.num, tbNew.den);
1639
+ this.lastFrameSampleRate = frame.sampleRate;
1640
+ return this.lastFrameTb;
1641
+ }
1642
+ /**
1643
+ * Audio timestamp processing - handles audio frame timestamps.
1644
+ *
1645
+ * Based on FFmpeg's audio_ts_process().
1646
+ *
1647
+ * Processes audio frame timestamps with:
1648
+ * - Sample rate change handling via audioSamplerateUpdate()
1649
+ * - PTS extrapolation when missing (pts_pred)
1650
+ * - Gap detection (resets av_rescale_delta state)
1651
+ * - Smooth timestamp conversion via av_rescale_delta
1652
+ * - Duration calculation from nb_samples
1653
+ * - Conversion to filtering timebase {1, sample_rate}
1654
+ *
1655
+ * Handles:
1656
+ * - Dynamic sample rate changes
1657
+ * - Missing timestamps (AV_NOPTS_VALUE)
1658
+ * - Timestamp gaps/discontinuities
1659
+ * - Sample-accurate timestamp generation
1660
+ * - Frame duration calculation
1661
+ *
1662
+ * @param frame - Decoded audio frame to process
1663
+ *
1664
+ * @internal
1665
+ */
1666
+ processAudioFrame(frame) {
1667
+ // Filtering timebase is always {1, sample_rate} for audio
1668
+ const tbFilter = { num: 1, den: frame.sampleRate };
1669
+ // Handle sample rate change - updates internal timebase
1670
+ const tb = this.audioSamplerateUpdate(frame);
1671
+ // Predict next PTS based on last frame + duration
1672
+ const ptsPred = this.lastFramePts === AV_NOPTS_VALUE ? 0n : this.lastFramePts + this.lastFrameDurationEst;
1673
+ // No timestamp - use predicted value
1674
+ if (frame.pts === AV_NOPTS_VALUE) {
1675
+ frame.pts = ptsPred;
1676
+ frame.timeBase = new Rational(tb.num, tb.den);
1677
+ }
1678
+ else if (this.lastFramePts !== AV_NOPTS_VALUE) {
1679
+ // Detect timestamp gap - compare with predicted timestamp
1680
+ const ptsPredInFrameTb = avRescaleQRnd(ptsPred, tb, frame.timeBase, AV_ROUND_UP);
1681
+ if (frame.pts > ptsPredInFrameTb) {
1682
+ // Gap detected - reset rescale_delta state for smooth conversion
1683
+ this.lastFilterInRescaleDelta = AV_NOPTS_VALUE;
1684
+ }
1685
+ }
1686
+ // Smooth timestamp conversion with av_rescale_delta
1687
+ // This maintains fractional sample accuracy across timebase conversions
1688
+ // avRescaleDelta modifies lastRef in place (simulates C's &last_filter_in_rescale_delta)
1689
+ const lastRef = { value: this.lastFilterInRescaleDelta };
1690
+ frame.pts = avRescaleDelta(frame.timeBase, frame.pts, tb, frame.nbSamples, lastRef, tb);
1691
+ this.lastFilterInRescaleDelta = lastRef.value;
1692
+ // Update frame tracking
1693
+ this.lastFramePts = frame.pts;
1694
+ this.lastFrameDurationEst = avRescaleQ(BigInt(frame.nbSamples), tbFilter, tb);
1695
+ // Convert to filtering timebase
1696
+ frame.pts = avRescaleQ(frame.pts, tb, tbFilter);
1697
+ frame.duration = BigInt(frame.nbSamples);
1698
+ frame.timeBase = new Rational(tbFilter.num, tbFilter.den);
1699
+ }
911
1700
  /**
912
1701
  * Dispose of decoder.
913
1702
  *