@pproenca/node-webcodecs 0.1.1-alpha.0 → 0.1.1-alpha.6

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 (97) hide show
  1. package/README.md +75 -233
  2. package/binding.gyp +123 -0
  3. package/dist/audio-decoder.js +1 -2
  4. package/dist/audio-encoder.d.ts +4 -0
  5. package/dist/audio-encoder.js +28 -2
  6. package/dist/binding.d.ts +0 -2
  7. package/dist/binding.js +43 -125
  8. package/dist/control-message-queue.js +0 -1
  9. package/dist/control-message-queue.js.map +1 -1
  10. package/dist/demuxer.d.ts +7 -0
  11. package/dist/demuxer.js +9 -0
  12. package/dist/encoded-chunks.d.ts +16 -0
  13. package/dist/encoded-chunks.js +82 -2
  14. package/dist/image-decoder.js +4 -0
  15. package/dist/index.d.ts +11 -0
  16. package/dist/index.js +3 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/native-types.d.ts +20 -0
  19. package/dist/platform.d.ts +1 -10
  20. package/dist/platform.js +1 -39
  21. package/dist/resource-manager.d.ts +1 -2
  22. package/dist/resource-manager.js +3 -17
  23. package/dist/resource-manager.js.map +1 -1
  24. package/dist/types.d.ts +12 -0
  25. package/dist/types.js.map +1 -1
  26. package/dist/video-decoder.d.ts +21 -0
  27. package/dist/video-decoder.js +74 -2
  28. package/dist/video-encoder.d.ts +22 -0
  29. package/dist/video-encoder.js +83 -8
  30. package/lib/audio-decoder.ts +1 -2
  31. package/lib/audio-encoder.ts +31 -2
  32. package/lib/binding.ts +45 -104
  33. package/lib/control-message-queue.ts +0 -1
  34. package/lib/demuxer.ts +10 -0
  35. package/lib/encoded-chunks.ts +90 -2
  36. package/lib/image-decoder.ts +5 -0
  37. package/lib/index.ts +3 -0
  38. package/lib/native-types.ts +22 -0
  39. package/lib/platform.ts +1 -41
  40. package/lib/resource-manager.ts +3 -19
  41. package/lib/types.ts +13 -0
  42. package/lib/video-decoder.ts +84 -2
  43. package/lib/video-encoder.ts +90 -8
  44. package/package.json +48 -32
  45. package/src/addon.cc +57 -0
  46. package/src/async_decode_worker.cc +241 -33
  47. package/src/async_decode_worker.h +55 -3
  48. package/src/async_encode_worker.cc +103 -35
  49. package/src/async_encode_worker.h +23 -4
  50. package/src/audio_data.cc +38 -15
  51. package/src/audio_data.h +1 -0
  52. package/src/audio_decoder.cc +24 -3
  53. package/src/audio_encoder.cc +55 -4
  54. package/src/common.cc +125 -17
  55. package/src/common.h +34 -4
  56. package/src/demuxer.cc +16 -2
  57. package/src/encoded_audio_chunk.cc +10 -0
  58. package/src/encoded_audio_chunk.h +2 -0
  59. package/src/encoded_video_chunk.h +1 -0
  60. package/src/error_builder.cc +0 -4
  61. package/src/image_decoder.cc +127 -90
  62. package/src/image_decoder.h +11 -4
  63. package/src/muxer.cc +1 -0
  64. package/src/test_video_generator.cc +3 -2
  65. package/src/video_decoder.cc +169 -19
  66. package/src/video_decoder.h +9 -11
  67. package/src/video_encoder.cc +389 -32
  68. package/src/video_encoder.h +15 -0
  69. package/src/video_filter.cc +22 -11
  70. package/src/video_frame.cc +160 -5
  71. package/src/warnings.cc +0 -4
  72. package/dist/audio-data.js.map +0 -1
  73. package/dist/audio-decoder.js.map +0 -1
  74. package/dist/audio-encoder.js.map +0 -1
  75. package/dist/binding.js.map +0 -1
  76. package/dist/codec-base.js.map +0 -1
  77. package/dist/demuxer.js.map +0 -1
  78. package/dist/encoded-chunks.js.map +0 -1
  79. package/dist/errors.js.map +0 -1
  80. package/dist/ffmpeg.d.ts +0 -21
  81. package/dist/ffmpeg.js +0 -112
  82. package/dist/image-decoder.js.map +0 -1
  83. package/dist/image-track-list.js.map +0 -1
  84. package/dist/image-track.js.map +0 -1
  85. package/dist/is.js.map +0 -1
  86. package/dist/muxer.js.map +0 -1
  87. package/dist/native-types.js.map +0 -1
  88. package/dist/platform.js.map +0 -1
  89. package/dist/test-video-generator.js.map +0 -1
  90. package/dist/transfer.js.map +0 -1
  91. package/dist/video-decoder.js.map +0 -1
  92. package/dist/video-encoder.js.map +0 -1
  93. package/dist/video-filter.js.map +0 -1
  94. package/dist/video-frame.js.map +0 -1
  95. package/install/build.js +0 -51
  96. package/install/check.js +0 -192
  97. package/lib/ffmpeg.ts +0 -78
package/dist/platform.js CHANGED
@@ -41,36 +41,12 @@ exports.prebuiltPlatforms = void 0;
41
41
  exports.runtimePlatformArch = runtimePlatformArch;
42
42
  exports.buildPlatformArch = buildPlatformArch;
43
43
  exports.isPrebuiltAvailable = isPrebuiltAvailable;
44
- exports.getPrebuiltPackageName = getPrebuiltPackageName;
45
- exports.getFFmpegPackageName = getFFmpegPackageName;
46
44
  const os = __importStar(require("node:os"));
47
- // Try to detect musl vs glibc on Linux
48
- function detectLibc() {
49
- if (os.platform() !== 'linux')
50
- return null;
51
- try {
52
- const { familySync } = require('detect-libc');
53
- return familySync() === 'musl' ? 'musl' : 'glibc';
54
- }
55
- catch {
56
- // detect-libc not available, assume glibc
57
- return 'glibc';
58
- }
59
- }
60
45
  /**
61
46
  * Get the runtime platform-architecture string.
62
- * Handles musl vs glibc distinction on Linux.
63
47
  */
64
48
  function runtimePlatformArch() {
65
- const platform = os.platform();
66
- const arch = os.arch();
67
- if (platform === 'linux') {
68
- const libc = detectLibc();
69
- if (libc === 'musl') {
70
- return `linuxmusl-${arch}`;
71
- }
72
- }
73
- return `${platform}-${arch}`;
49
+ return `${os.platform()}-${os.arch()}`;
74
50
  }
75
51
  /**
76
52
  * Get the build-time platform-architecture string.
@@ -92,8 +68,6 @@ exports.prebuiltPlatforms = [
92
68
  'darwin-arm64',
93
69
  'darwin-x64',
94
70
  'linux-x64',
95
- 'linuxmusl-x64',
96
- 'win32-x64',
97
71
  ];
98
72
  /**
99
73
  * Check if a prebuilt binary is available for the current platform.
@@ -102,15 +76,3 @@ function isPrebuiltAvailable() {
102
76
  const platform = runtimePlatformArch();
103
77
  return exports.prebuiltPlatforms.includes(platform);
104
78
  }
105
- /**
106
- * Get the npm package name for the prebuilt binary.
107
- */
108
- function getPrebuiltPackageName() {
109
- return `@pproenca/node-webcodecs-${runtimePlatformArch()}`;
110
- }
111
- /**
112
- * Get the npm package name for prebuilt FFmpeg.
113
- */
114
- function getFFmpegPackageName() {
115
- return `@pproenca/ffmpeg-${runtimePlatformArch()}`;
116
- }
@@ -15,7 +15,6 @@ export declare class ResourceManager {
15
15
  private static instance;
16
16
  private codecs;
17
17
  private inactivityTimeout;
18
- private checkInterval;
19
18
  private constructor();
20
19
  static getInstance(): ResourceManager;
21
20
  /**
@@ -51,9 +50,9 @@ export declare class ResourceManager {
51
50
  * Set inactivity timeout (for testing).
52
51
  */
53
52
  setInactivityTimeout(ms: number): void;
54
- private startMonitoring;
55
53
  /**
56
54
  * Stop monitoring (for cleanup).
55
+ * @deprecated No longer needed - monitoring happens on-demand via getReclaimableCodecs()
57
56
  */
58
57
  stopMonitoring(): void;
59
58
  }
@@ -11,8 +11,7 @@ class ResourceManager {
11
11
  constructor() {
12
12
  this.codecs = new Map();
13
13
  this.inactivityTimeout = 10000; // 10 seconds per spec
14
- this.checkInterval = null;
15
- this.startMonitoring();
14
+ // Monitoring happens on-demand via getReclaimableCodecs()
16
15
  }
17
16
  static getInstance() {
18
17
  if (!ResourceManager.instance) {
@@ -104,25 +103,12 @@ class ResourceManager {
104
103
  setInactivityTimeout(ms) {
105
104
  this.inactivityTimeout = ms;
106
105
  }
107
- startMonitoring() {
108
- // Check every 5 seconds
109
- this.checkInterval = setInterval(() => {
110
- // Just track, don't auto-reclaim
111
- // Actual reclamation would be triggered by memory pressure
112
- }, 5000);
113
- // Don't keep process alive
114
- if (this.checkInterval.unref) {
115
- this.checkInterval.unref();
116
- }
117
- }
118
106
  /**
119
107
  * Stop monitoring (for cleanup).
108
+ * @deprecated No longer needed - monitoring happens on-demand via getReclaimableCodecs()
120
109
  */
121
110
  stopMonitoring() {
122
- if (this.checkInterval) {
123
- clearInterval(this.checkInterval);
124
- this.checkInterval = null;
125
- }
111
+ // No-op: monitoring now happens on-demand
126
112
  }
127
113
  }
128
114
  exports.ResourceManager = ResourceManager;
@@ -1 +1 @@
1
- {"version":3,"file":"resource-manager.js","sourceRoot":"","sources":["../lib/resource-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"resource-manager.js","sourceRoot":"","sources":["../lib/resource-manager.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -54,6 +54,16 @@ export type HardwareAcceleration = 'no-preference' | 'prefer-hardware' | 'prefer
54
54
  * enum AlphaOption { "keep", "discard" };
55
55
  */
56
56
  export type AlphaOption = 'keep' | 'discard';
57
+ /**
58
+ * WebIDL:
59
+ * enum PremultiplyAlpha { "none", "premultiply", "default" };
60
+ *
61
+ * Per W3C WebCodecs spec:
62
+ * - "none": Do not premultiply alpha
63
+ * - "premultiply": Premultiply RGB values by alpha
64
+ * - "default": Use default behavior (typically none)
65
+ */
66
+ export type PremultiplyAlpha = 'none' | 'premultiply' | 'default';
57
67
  /**
58
68
  * WebIDL:
59
69
  * enum LatencyMode { "quality", "realtime" };
@@ -561,6 +571,7 @@ export type ImageBufferSource = AllowSharedBufferSource | ReadableStream<Uint8Ar
561
571
  * required DOMString type;
562
572
  * required ImageBufferSource data;
563
573
  * ColorSpaceConversion colorSpaceConversion = "default";
574
+ * PremultiplyAlpha premultiplyAlpha = "default";
564
575
  * [EnforceRange] unsigned long desiredWidth;
565
576
  * [EnforceRange] unsigned long desiredHeight;
566
577
  * boolean preferAnimation;
@@ -571,6 +582,7 @@ export interface ImageDecoderInit {
571
582
  type: string;
572
583
  data: ImageBufferSource;
573
584
  colorSpaceConversion?: ColorSpaceConversion;
585
+ premultiplyAlpha?: PremultiplyAlpha;
574
586
  desiredWidth?: number;
575
587
  desiredHeight?: number;
576
588
  preferAnimation?: boolean;
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":""}
@@ -13,10 +13,31 @@ export declare class VideoDecoder extends CodecBase {
13
13
  private _needsKeyFrame;
14
14
  private _errorCallback;
15
15
  private _resourceId;
16
+ private _maxQueueDepth;
16
17
  constructor(init: VideoDecoderInit);
17
18
  get state(): CodecState;
18
19
  get decodeQueueSize(): number;
19
20
  get codecSaturated(): boolean;
21
+ /**
22
+ * Returns a Promise that resolves when the decoder has capacity for more chunks.
23
+ * Use this to implement backpressure in high-throughput decoding pipelines.
24
+ *
25
+ * When the internal queue is full (decodeQueueSize >= maxQueueDepth), calling
26
+ * `await decoder.ready` will pause until capacity is available.
27
+ *
28
+ * @example
29
+ * for (const chunk of chunks) {
30
+ * await decoder.ready; // Wait for capacity
31
+ * decoder.decode(chunk);
32
+ * }
33
+ */
34
+ get ready(): Promise<void>;
35
+ /**
36
+ * The maximum queue depth before backpressure is applied.
37
+ * Default is 16. Adjust based on memory constraints and frame size.
38
+ */
39
+ get maxQueueDepth(): number;
40
+ set maxQueueDepth(value: number);
20
41
  configure(config: VideoDecoderConfig): void;
21
42
  decode(chunk: EncodedVideoChunk): void;
22
43
  flush(): Promise<void>;
@@ -47,11 +47,15 @@ const resource_manager_1 = require("./resource-manager");
47
47
  const video_frame_1 = require("./video-frame");
48
48
  // Load native addon with type assertion
49
49
  const native = binding_1.binding;
50
+ // Default backpressure threshold for limiting in-flight chunks
51
+ const DEFAULT_MAX_QUEUE_DEPTH = 16;
50
52
  class VideoDecoder extends codec_base_1.CodecBase {
51
53
  constructor(init) {
52
54
  super();
53
55
  this._decodeQueueSize = 0;
54
56
  this._needsKeyFrame = true;
57
+ // Backpressure support
58
+ this._maxQueueDepth = DEFAULT_MAX_QUEUE_DEPTH;
55
59
  // W3C spec: output and error callbacks are required
56
60
  is.assertPlainObject(init, 'init');
57
61
  is.assertFunction(init.output, 'init.output');
@@ -61,7 +65,6 @@ class VideoDecoder extends codec_base_1.CodecBase {
61
65
  this._controlQueue.setErrorHandler(init.error);
62
66
  this._resourceId = resource_manager_1.ResourceManager.getInstance().register(this);
63
67
  const outputCallback = (nativeFrame) => {
64
- // Decrement queue size when output received
65
68
  this._decodeQueueSize = Math.max(0, this._decodeQueueSize - 1);
66
69
  // Wrap the native frame as a VideoFrame
67
70
  // biome-ignore lint/suspicious/noExplicitAny: Object.create wrapper pattern requires any for property assignment
@@ -87,11 +90,72 @@ class VideoDecoder extends codec_base_1.CodecBase {
87
90
  get codecSaturated() {
88
91
  return this._native.codecSaturated;
89
92
  }
93
+ /**
94
+ * Returns a Promise that resolves when the decoder has capacity for more chunks.
95
+ * Use this to implement backpressure in high-throughput decoding pipelines.
96
+ *
97
+ * When the internal queue is full (decodeQueueSize >= maxQueueDepth), calling
98
+ * `await decoder.ready` will pause until capacity is available.
99
+ *
100
+ * @example
101
+ * for (const chunk of chunks) {
102
+ * await decoder.ready; // Wait for capacity
103
+ * decoder.decode(chunk);
104
+ * }
105
+ */
106
+ get ready() {
107
+ // If we have capacity, resolve immediately
108
+ if (this._decodeQueueSize < this._maxQueueDepth) {
109
+ return Promise.resolve();
110
+ }
111
+ // Otherwise, poll until capacity is available.
112
+ // We use setTimeout(1ms) polling to allow TSFN output callbacks to execute.
113
+ // setTimeout ensures we yield through the full event loop cycle, including
114
+ // the I/O phase where TSFN callbacks are delivered.
115
+ return new Promise((resolve) => {
116
+ const checkCapacity = () => {
117
+ if (this._decodeQueueSize < this._maxQueueDepth) {
118
+ resolve();
119
+ }
120
+ else {
121
+ // Yield full event loop cycle to allow output callbacks to run
122
+ setTimeout(checkCapacity, 1);
123
+ }
124
+ };
125
+ // Initial yield to allow any pending callbacks to run
126
+ setTimeout(checkCapacity, 1);
127
+ });
128
+ }
129
+ /**
130
+ * The maximum queue depth before backpressure is applied.
131
+ * Default is 16. Adjust based on memory constraints and frame size.
132
+ */
133
+ get maxQueueDepth() {
134
+ return this._maxQueueDepth;
135
+ }
136
+ set maxQueueDepth(value) {
137
+ if (value < 1) {
138
+ throw new RangeError('maxQueueDepth must be at least 1');
139
+ }
140
+ this._maxQueueDepth = value;
141
+ }
90
142
  configure(config) {
91
143
  // W3C spec: throw if closed
92
144
  if (this.state === 'closed') {
93
145
  throw new DOMException('Decoder is closed', 'InvalidStateError');
94
146
  }
147
+ // Validate rotation (node-webcodecs extension)
148
+ if ('rotation' in config && config.rotation !== undefined) {
149
+ if (![0, 90, 180, 270].includes(config.rotation)) {
150
+ throw new TypeError(`rotation must be 0, 90, 180, or 270, got ${config.rotation}`);
151
+ }
152
+ }
153
+ // Validate flip (node-webcodecs extension)
154
+ if ('flip' in config && config.flip !== undefined) {
155
+ if (typeof config.flip !== 'boolean') {
156
+ throw new TypeError('flip must be a boolean');
157
+ }
158
+ }
95
159
  this._needsKeyFrame = true;
96
160
  // Configure synchronously to set state immediately per W3C spec
97
161
  this._native.configure(config);
@@ -121,7 +185,15 @@ class VideoDecoder extends codec_base_1.CodecBase {
121
185
  return Promise.reject(new DOMException('Decoder is closed', 'InvalidStateError'));
122
186
  }
123
187
  await this._controlQueue.flush();
124
- return this._native.flush();
188
+ // Flush the native decoder (waits for worker queue to drain)
189
+ this._native.flush();
190
+ // Poll for pending TSFN callbacks to complete.
191
+ // This allows the event loop to run (delivering callbacks) while we wait.
192
+ // Using setTimeout(1ms) instead of setImmediate to ensure other event loop
193
+ // phases (timers, I/O) can run, preventing event loop starvation.
194
+ while (this._native.pendingFrames > 0) {
195
+ await new Promise((resolve) => setTimeout(resolve, 1)); // 1ms poll
196
+ }
125
197
  }
126
198
  reset() {
127
199
  // W3C spec: throw InvalidStateError if closed
@@ -11,10 +11,32 @@ export declare class VideoEncoder extends CodecBase {
11
11
  private _controlQueue;
12
12
  private _encodeQueueSize;
13
13
  private _resourceId;
14
+ private _maxQueueDepth;
14
15
  constructor(init: VideoEncoderInit);
15
16
  get state(): CodecState;
16
17
  get encodeQueueSize(): number;
17
18
  get codecSaturated(): boolean;
19
+ /**
20
+ * Returns a Promise that resolves when the encoder has capacity for more frames.
21
+ * Use this to implement backpressure in high-throughput encoding pipelines.
22
+ *
23
+ * When the internal queue is full (encodeQueueSize >= maxQueueDepth), calling
24
+ * `await encoder.ready` will pause until capacity is available.
25
+ *
26
+ * @example
27
+ * for (const frame of frames) {
28
+ * await encoder.ready; // Wait for capacity
29
+ * encoder.encode(frame);
30
+ * frame.close();
31
+ * }
32
+ */
33
+ get ready(): Promise<void>;
34
+ /**
35
+ * The maximum queue depth before backpressure is applied.
36
+ * Default is 16. Adjust based on memory constraints and frame size.
37
+ */
38
+ get maxQueueDepth(): number;
39
+ set maxQueueDepth(value: number);
18
40
  configure(config: VideoEncoderConfig): void;
19
41
  encode(frame: VideoFrame, options?: {
20
42
  keyFrame?: boolean;
@@ -47,10 +47,14 @@ const is = __importStar(require("./is"));
47
47
  const resource_manager_1 = require("./resource-manager");
48
48
  // Load native addon with type assertion
49
49
  const native = binding_1.binding;
50
+ // Default backpressure threshold for limiting in-flight frames
51
+ const DEFAULT_MAX_QUEUE_DEPTH = 16;
50
52
  class VideoEncoder extends codec_base_1.CodecBase {
51
53
  constructor(init) {
52
54
  super();
53
55
  this._encodeQueueSize = 0;
56
+ // Backpressure support
57
+ this._maxQueueDepth = DEFAULT_MAX_QUEUE_DEPTH;
54
58
  // W3C spec: output and error callbacks are required
55
59
  is.assertPlainObject(init, 'init');
56
60
  is.assertFunction(init.output, 'init.output');
@@ -59,14 +63,25 @@ class VideoEncoder extends codec_base_1.CodecBase {
59
63
  this._controlQueue.setErrorHandler(init.error);
60
64
  this._resourceId = resource_manager_1.ResourceManager.getInstance().register(this);
61
65
  const outputCallback = (chunk, metadata) => {
62
- // Decrement queue size when output received
63
66
  this._encodeQueueSize = Math.max(0, this._encodeQueueSize - 1);
64
- const wrappedChunk = new encoded_chunks_1.EncodedVideoChunk({
65
- type: chunk.type,
66
- timestamp: chunk.timestamp,
67
- duration: chunk.duration ?? undefined,
68
- data: chunk.data,
69
- });
67
+ // The native layer now returns an EncodedVideoChunk directly (not a plain object).
68
+ // Check if it's already a native chunk (has close method) vs plain object (has data buffer).
69
+ // Native chunks have close() but no 'data' property; plain objects have 'data' buffer.
70
+ let wrappedChunk;
71
+ if ('data' in chunk && chunk.data instanceof Buffer) {
72
+ // Legacy path: plain object from sync encoder - wrap it
73
+ wrappedChunk = new encoded_chunks_1.EncodedVideoChunk({
74
+ type: chunk.type,
75
+ timestamp: chunk.timestamp,
76
+ duration: chunk.duration ?? undefined,
77
+ data: chunk.data,
78
+ });
79
+ }
80
+ else {
81
+ // New path: already a native EncodedVideoChunk from async encoder
82
+ // Wrap without copying data
83
+ wrappedChunk = encoded_chunks_1.EncodedVideoChunk._fromNative(chunk);
84
+ }
70
85
  init.output(wrappedChunk, metadata);
71
86
  // Fire ondequeue after output
72
87
  this._triggerDequeue();
@@ -85,6 +100,56 @@ class VideoEncoder extends codec_base_1.CodecBase {
85
100
  get codecSaturated() {
86
101
  return this._native.codecSaturated;
87
102
  }
103
+ /**
104
+ * Returns a Promise that resolves when the encoder has capacity for more frames.
105
+ * Use this to implement backpressure in high-throughput encoding pipelines.
106
+ *
107
+ * When the internal queue is full (encodeQueueSize >= maxQueueDepth), calling
108
+ * `await encoder.ready` will pause until capacity is available.
109
+ *
110
+ * @example
111
+ * for (const frame of frames) {
112
+ * await encoder.ready; // Wait for capacity
113
+ * encoder.encode(frame);
114
+ * frame.close();
115
+ * }
116
+ */
117
+ get ready() {
118
+ // If we have capacity, resolve immediately
119
+ if (this._encodeQueueSize < this._maxQueueDepth) {
120
+ return Promise.resolve();
121
+ }
122
+ // Otherwise, poll until capacity is available.
123
+ // We use setTimeout(1ms) polling to allow TSFN output callbacks to execute.
124
+ // setTimeout ensures we yield through the full event loop cycle, including
125
+ // the I/O phase where TSFN callbacks are delivered.
126
+ return new Promise((resolve) => {
127
+ const checkCapacity = () => {
128
+ if (this._encodeQueueSize < this._maxQueueDepth) {
129
+ resolve();
130
+ }
131
+ else {
132
+ // Yield full event loop cycle to allow output callbacks to run
133
+ setTimeout(checkCapacity, 1);
134
+ }
135
+ };
136
+ // Initial yield to allow any pending callbacks to run
137
+ setTimeout(checkCapacity, 1);
138
+ });
139
+ }
140
+ /**
141
+ * The maximum queue depth before backpressure is applied.
142
+ * Default is 16. Adjust based on memory constraints and frame size.
143
+ */
144
+ get maxQueueDepth() {
145
+ return this._maxQueueDepth;
146
+ }
147
+ set maxQueueDepth(value) {
148
+ if (value < 1) {
149
+ throw new RangeError('maxQueueDepth must be at least 1');
150
+ }
151
+ this._maxQueueDepth = value;
152
+ }
88
153
  configure(config) {
89
154
  // W3C spec: throw if closed
90
155
  if (this.state === 'closed') {
@@ -98,6 +163,10 @@ class VideoEncoder extends codec_base_1.CodecBase {
98
163
  this._native.configure(config);
99
164
  }
100
165
  encode(frame, options) {
166
+ // W3C spec: throw if not configured
167
+ if (this.state !== 'configured') {
168
+ throw new DOMException(`Encoder is ${this.state}`, 'InvalidStateError');
169
+ }
101
170
  resource_manager_1.ResourceManager.getInstance().recordActivity(this._resourceId);
102
171
  this._encodeQueueSize++;
103
172
  // Call native encode directly - frame must be valid at call time
@@ -116,11 +185,17 @@ class VideoEncoder extends codec_base_1.CodecBase {
116
185
  this._native.flush();
117
186
  // Poll for pending TSFN callbacks to complete.
118
187
  // This allows the event loop to run (delivering callbacks) while we wait.
188
+ // Using setTimeout(1ms) instead of setImmediate to ensure other event loop
189
+ // phases (timers, I/O) can run, preventing event loop starvation.
119
190
  while (this._native.pendingChunks > 0) {
120
- await new Promise((resolve) => setImmediate(resolve));
191
+ await new Promise((resolve) => setTimeout(resolve, 1)); // 1ms poll
121
192
  }
122
193
  }
123
194
  reset() {
195
+ // W3C spec: throw if closed
196
+ if (this.state === 'closed') {
197
+ throw new DOMException('Encoder is closed', 'InvalidStateError');
198
+ }
124
199
  this._controlQueue.clear();
125
200
  this._encodeQueueSize = 0;
126
201
  this._native.reset();
@@ -30,7 +30,6 @@ export class AudioDecoder extends CodecBase {
30
30
  this._controlQueue.setErrorHandler(init.error);
31
31
 
32
32
  const outputCallback: AudioDecoderOutputCallback = (nativeData) => {
33
- // Decrement queue size when output received
34
33
  this._decodeQueueSize = Math.max(0, this._decodeQueueSize - 1);
35
34
 
36
35
  // biome-ignore lint/suspicious/noExplicitAny: Object.create wrapper pattern requires any for property assignment
@@ -76,7 +75,7 @@ export class AudioDecoder extends CodecBase {
76
75
  decode(chunk: EncodedAudioChunk): void {
77
76
  // W3C spec: throw InvalidStateError if not configured
78
77
  if (this.state === 'unconfigured') {
79
- throw new DOMException('Decoder is not configured', 'InvalidStateError');
78
+ throw new DOMException('Decoder is unconfigured', 'InvalidStateError');
80
79
  }
81
80
  if (this.state === 'closed') {
82
81
  throw new DOMException('Decoder is closed', 'InvalidStateError');
@@ -21,10 +21,13 @@ import type { AudioEncoderConfig, AudioEncoderInit, CodecState } from './types';
21
21
  // Load native addon with type assertion
22
22
  const native = binding as NativeModule;
23
23
 
24
+ const DEFAULT_MAX_QUEUE_DEPTH = 16;
25
+
24
26
  export class AudioEncoder extends CodecBase {
25
27
  private _native: NativeAudioEncoder;
26
28
  private _controlQueue: ControlMessageQueue;
27
29
  private _encodeQueueSize: number = 0;
30
+ private _maxQueueDepth: number = DEFAULT_MAX_QUEUE_DEPTH;
28
31
 
29
32
  constructor(init: AudioEncoderInit) {
30
33
  super();
@@ -38,7 +41,6 @@ export class AudioEncoder extends CodecBase {
38
41
  this._controlQueue.setErrorHandler(init.error);
39
42
 
40
43
  const outputCallback: AudioEncoderOutputCallback = (chunk, metadata) => {
41
- // Decrement queue size when output received
42
44
  this._encodeQueueSize = Math.max(0, this._encodeQueueSize - 1);
43
45
 
44
46
  // biome-ignore lint/suspicious/noExplicitAny: Object.create wrapper pattern requires any for property assignment
@@ -68,6 +70,33 @@ export class AudioEncoder extends CodecBase {
68
70
  return this._native.codecSaturated;
69
71
  }
70
72
 
73
+ get maxQueueDepth(): number {
74
+ return this._maxQueueDepth;
75
+ }
76
+
77
+ set maxQueueDepth(value: number) {
78
+ if (value < 1) {
79
+ throw new RangeError('maxQueueDepth must be at least 1');
80
+ }
81
+ this._maxQueueDepth = value;
82
+ }
83
+
84
+ get ready(): Promise<void> {
85
+ if (this._encodeQueueSize < this._maxQueueDepth) {
86
+ return Promise.resolve();
87
+ }
88
+ return new Promise<void>((resolve) => {
89
+ const checkCapacity = () => {
90
+ if (this._encodeQueueSize < this._maxQueueDepth) {
91
+ resolve();
92
+ } else {
93
+ setTimeout(checkCapacity, 1);
94
+ }
95
+ };
96
+ setTimeout(checkCapacity, 1);
97
+ });
98
+ }
99
+
71
100
  configure(config: AudioEncoderConfig): void {
72
101
  // W3C spec: throw if closed
73
102
  if (this.state === 'closed') {
@@ -86,7 +115,7 @@ export class AudioEncoder extends CodecBase {
86
115
  encode(data: AudioData): void {
87
116
  // W3C spec: throw InvalidStateError if not configured
88
117
  if (this.state === 'unconfigured') {
89
- throw new DOMException('Encoder is not configured', 'InvalidStateError');
118
+ throw new DOMException('Encoder is unconfigured', 'InvalidStateError');
90
119
  }
91
120
  if (this.state === 'closed') {
92
121
  throw new DOMException('Encoder is closed', 'InvalidStateError');