@pproenca/node-webcodecs 0.1.1-alpha.0 → 0.1.1-alpha.5
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/README.md +75 -233
- package/binding.gyp +123 -0
- package/dist/audio-decoder.js +1 -2
- package/dist/audio-encoder.d.ts +4 -0
- package/dist/audio-encoder.js +28 -2
- package/dist/binding.d.ts +0 -2
- package/dist/binding.js +43 -125
- package/dist/control-message-queue.js +0 -1
- package/dist/demuxer.d.ts +7 -0
- package/dist/demuxer.js +9 -0
- package/dist/encoded-chunks.d.ts +16 -0
- package/dist/encoded-chunks.js +82 -2
- package/dist/image-decoder.js +4 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +3 -1
- package/dist/native-types.d.ts +20 -0
- package/dist/platform.d.ts +1 -10
- package/dist/platform.js +1 -39
- package/dist/resource-manager.d.ts +1 -2
- package/dist/resource-manager.js +3 -17
- package/dist/types.d.ts +12 -0
- package/dist/video-decoder.d.ts +21 -0
- package/dist/video-decoder.js +74 -2
- package/dist/video-encoder.d.ts +22 -0
- package/dist/video-encoder.js +83 -8
- package/lib/audio-decoder.ts +1 -2
- package/lib/audio-encoder.ts +31 -2
- package/lib/binding.ts +45 -104
- package/lib/control-message-queue.ts +0 -1
- package/lib/demuxer.ts +10 -0
- package/lib/encoded-chunks.ts +90 -2
- package/lib/image-decoder.ts +5 -0
- package/lib/index.ts +3 -0
- package/lib/native-types.ts +22 -0
- package/lib/platform.ts +1 -41
- package/lib/resource-manager.ts +3 -19
- package/lib/types.ts +13 -0
- package/lib/video-decoder.ts +84 -2
- package/lib/video-encoder.ts +90 -8
- package/package.json +49 -32
- package/src/addon.cc +57 -0
- package/src/async_decode_worker.cc +241 -33
- package/src/async_decode_worker.h +55 -3
- package/src/async_encode_worker.cc +103 -35
- package/src/async_encode_worker.h +23 -4
- package/src/audio_data.cc +38 -15
- package/src/audio_data.h +1 -0
- package/src/audio_decoder.cc +24 -3
- package/src/audio_encoder.cc +55 -4
- package/src/common.cc +125 -17
- package/src/common.h +34 -4
- package/src/demuxer.cc +16 -2
- package/src/encoded_audio_chunk.cc +10 -0
- package/src/encoded_audio_chunk.h +2 -0
- package/src/encoded_video_chunk.h +1 -0
- package/src/error_builder.cc +0 -4
- package/src/image_decoder.cc +127 -90
- package/src/image_decoder.h +11 -4
- package/src/muxer.cc +1 -0
- package/src/test_video_generator.cc +3 -2
- package/src/video_decoder.cc +169 -19
- package/src/video_decoder.h +9 -11
- package/src/video_encoder.cc +389 -32
- package/src/video_encoder.h +15 -0
- package/src/video_filter.cc +22 -11
- package/src/video_frame.cc +160 -5
- package/src/warnings.cc +0 -4
- package/dist/audio-data.js.map +0 -1
- package/dist/audio-decoder.js.map +0 -1
- package/dist/audio-encoder.js.map +0 -1
- package/dist/binding.js.map +0 -1
- package/dist/codec-base.js.map +0 -1
- package/dist/control-message-queue.js.map +0 -1
- package/dist/demuxer.js.map +0 -1
- package/dist/encoded-chunks.js.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/ffmpeg.d.ts +0 -21
- package/dist/ffmpeg.js +0 -112
- package/dist/image-decoder.js.map +0 -1
- package/dist/image-track-list.js.map +0 -1
- package/dist/image-track.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/is.js.map +0 -1
- package/dist/muxer.js.map +0 -1
- package/dist/native-types.js.map +0 -1
- package/dist/platform.js.map +0 -1
- package/dist/resource-manager.js.map +0 -1
- package/dist/test-video-generator.js.map +0 -1
- package/dist/transfer.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/video-decoder.js.map +0 -1
- package/dist/video-encoder.js.map +0 -1
- package/dist/video-filter.js.map +0 -1
- package/dist/video-frame.js.map +0 -1
- package/install/build.js +0 -51
- package/install/check.js +0 -192
- package/lib/ffmpeg.ts +0 -78
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/video-decoder.d.ts
CHANGED
|
@@ -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>;
|
package/dist/video-decoder.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/video-encoder.d.ts
CHANGED
|
@@ -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;
|
package/dist/video-encoder.js
CHANGED
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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) =>
|
|
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();
|
package/lib/audio-decoder.ts
CHANGED
|
@@ -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
|
|
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');
|
package/lib/audio-encoder.ts
CHANGED
|
@@ -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
|
|
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');
|
package/lib/binding.ts
CHANGED
|
@@ -1,116 +1,59 @@
|
|
|
1
1
|
// Copyright 2024 The node-webcodecs Authors
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
//
|
|
4
|
-
// Native binding loader
|
|
4
|
+
// Native binding loader using esbuild-style platform resolution.
|
|
5
|
+
// Tries platform-specific package first, falls back to node-gyp-build for local dev.
|
|
5
6
|
|
|
6
|
-
import
|
|
7
|
-
import * as path from 'node:path';
|
|
8
|
-
import { getPrebuiltPackageName, isPrebuiltAvailable, runtimePlatformArch } from './platform';
|
|
7
|
+
import { resolve, dirname, join } from 'node:path';
|
|
9
8
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// Development build (node-gyp output)
|
|
16
|
-
path.join(rootDir, 'build', 'Release', 'node_webcodecs.node'),
|
|
17
|
-
path.join(rootDir, 'build', 'Debug', 'node_webcodecs.node'),
|
|
18
|
-
|
|
19
|
-
// node-gyp-build compatible
|
|
20
|
-
() => {
|
|
21
|
-
try {
|
|
22
|
-
return require('node-gyp-build')(rootDir);
|
|
23
|
-
} catch {
|
|
24
|
-
throw new Error('node-gyp-build not available');
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
// Prebuilt from platform package (sharp pattern: @scope/pkg-platform/binding.node)
|
|
29
|
-
() => {
|
|
30
|
-
const pkg = getPrebuiltPackageName();
|
|
31
|
-
const bindingPath = `${pkg}/node-webcodecs.node`;
|
|
32
|
-
return require(bindingPath);
|
|
33
|
-
},
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
function getPlatformBuildInstructions(): string {
|
|
37
|
-
const platform = process.platform;
|
|
38
|
-
|
|
39
|
-
if (platform === 'darwin') {
|
|
40
|
-
return ` brew install ffmpeg pkg-config
|
|
41
|
-
npm run build:native`;
|
|
42
|
-
}
|
|
43
|
-
if (platform === 'linux') {
|
|
44
|
-
return ` sudo apt-get install libavcodec-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev pkg-config
|
|
45
|
-
npm run build:native`;
|
|
46
|
-
}
|
|
47
|
-
if (platform === 'win32') {
|
|
48
|
-
return ` Download FFmpeg from https://github.com/BtbN/FFmpeg-Builds/releases
|
|
49
|
-
Set FFMPEG_PATH environment variable
|
|
50
|
-
npm run build:native`;
|
|
51
|
-
}
|
|
52
|
-
return ` Install FFmpeg development libraries
|
|
53
|
-
npm run build:native`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function buildHelpMessage(errors: Array<{ path: string; error: Error }>): string {
|
|
57
|
-
const platform = runtimePlatformArch();
|
|
58
|
-
const hasPrebuilt = isPrebuiltAvailable();
|
|
59
|
-
|
|
60
|
-
let msg = `Could not load native binding for ${platform}.\n\n`;
|
|
61
|
-
msg += `Node.js: ${process.version}\n\n`;
|
|
9
|
+
const PLATFORMS: Record<string, string> = {
|
|
10
|
+
'darwin-arm64': '@pproenca/node-webcodecs-darwin-arm64',
|
|
11
|
+
'darwin-x64': '@pproenca/node-webcodecs-darwin-x64',
|
|
12
|
+
'linux-x64': '@pproenca/node-webcodecs-linux-x64',
|
|
13
|
+
};
|
|
62
14
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Load the native binding.
|
|
17
|
+
*
|
|
18
|
+
* Resolution order:
|
|
19
|
+
* 1. Platform-specific npm package (production path via optionalDependencies)
|
|
20
|
+
* 2. node-gyp-build (local development fallback)
|
|
21
|
+
*/
|
|
22
|
+
function loadBinding(): unknown {
|
|
23
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
24
|
+
const pkg = PLATFORMS[platform];
|
|
25
|
+
|
|
26
|
+
if (!pkg) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Unsupported platform: ${platform}. ` +
|
|
29
|
+
`Supported platforms: ${Object.keys(PLATFORMS).join(', ')}`
|
|
30
|
+
);
|
|
66
31
|
}
|
|
67
32
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
msg += ' 1. Build from source:\n';
|
|
33
|
+
// Try platform-specific package first (production path)
|
|
34
|
+
try {
|
|
35
|
+
const pkgPath = require.resolve(`${pkg}/package.json`);
|
|
36
|
+
const binPath = join(dirname(pkgPath), 'bin', 'node.napi.node');
|
|
37
|
+
return require(binPath);
|
|
38
|
+
} catch {
|
|
39
|
+
// Platform package not installed - fallback to local build
|
|
76
40
|
}
|
|
77
41
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
throw new Error('Invalid binding: missing VideoEncoder');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (!fs.existsSync(candidate)) {
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const binding = require(candidate);
|
|
101
|
-
if (typeof binding.VideoEncoder !== 'function') {
|
|
102
|
-
throw new Error('Invalid binding: missing VideoEncoder');
|
|
103
|
-
}
|
|
104
|
-
return binding;
|
|
105
|
-
} catch (err) {
|
|
106
|
-
errors.push({
|
|
107
|
-
path: typeof candidate === 'string' ? candidate : 'dynamic loader',
|
|
108
|
-
error: err instanceof Error ? err : new Error(String(err)),
|
|
109
|
-
});
|
|
110
|
-
}
|
|
42
|
+
// Fallback to node-gyp-build for local development
|
|
43
|
+
try {
|
|
44
|
+
const nodeGypBuild = require('node-gyp-build');
|
|
45
|
+
const rootDir = resolve(__dirname, '..');
|
|
46
|
+
return nodeGypBuild(rootDir);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Could not load the node-webcodecs native binding for ${platform}.\n` +
|
|
50
|
+
`Error: ${err instanceof Error ? err.message : String(err)}\n\n` +
|
|
51
|
+
`Solutions:\n` +
|
|
52
|
+
` 1. Install the main package: npm install @pproenca/node-webcodecs\n` +
|
|
53
|
+
` 2. Build from source: npm rebuild --build-from-source\n` +
|
|
54
|
+
` 3. Ensure FFmpeg dev libs: pkg-config --exists libavcodec\n`
|
|
55
|
+
);
|
|
111
56
|
}
|
|
112
|
-
|
|
113
|
-
throw new Error(buildHelpMessage(errors));
|
|
114
57
|
}
|
|
115
58
|
|
|
116
59
|
export const binding = loadBinding();
|
|
@@ -118,8 +61,6 @@ export const binding = loadBinding();
|
|
|
118
61
|
export const platformInfo = {
|
|
119
62
|
platform: process.platform,
|
|
120
63
|
arch: process.arch,
|
|
121
|
-
runtimePlatform: runtimePlatformArch(),
|
|
122
64
|
nodeVersion: process.version,
|
|
123
65
|
napiVersion: (process.versions as Record<string, string>).napi ?? 'unknown',
|
|
124
|
-
prebuiltAvailable: isPrebuiltAvailable(),
|
|
125
66
|
};
|
package/lib/demuxer.ts
CHANGED
|
@@ -45,6 +45,16 @@ export class Demuxer {
|
|
|
45
45
|
return this._native.demux();
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Read packets from the file in chunks.
|
|
50
|
+
* This is useful for yielding to the event loop during demuxing.
|
|
51
|
+
* @param maxPackets - Maximum number of packets to read. 0 = unlimited (reads all).
|
|
52
|
+
* @returns The number of packets actually read.
|
|
53
|
+
*/
|
|
54
|
+
demuxPackets(maxPackets?: number): number {
|
|
55
|
+
return this._native.demuxPackets(maxPackets ?? 0);
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
close(): void {
|
|
49
59
|
this._native.close();
|
|
50
60
|
}
|