@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/lib/encoded-chunks.ts
CHANGED
|
@@ -10,11 +10,42 @@ import type {
|
|
|
10
10
|
NativeEncodedVideoChunk,
|
|
11
11
|
NativeModule,
|
|
12
12
|
} from './native-types';
|
|
13
|
+
import { detachArrayBuffers } from './transfer';
|
|
13
14
|
import type { EncodedAudioChunkInit, EncodedVideoChunkInit } from './types';
|
|
14
15
|
|
|
15
16
|
// Load native addon with type assertion
|
|
16
17
|
const native = binding as NativeModule;
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* FinalizationRegistry for automatic cleanup of native EncodedVideoChunk objects.
|
|
21
|
+
* When a JS EncodedVideoChunk wrapper becomes unreachable, the registry callback
|
|
22
|
+
* fires and releases the native memory via close().
|
|
23
|
+
*
|
|
24
|
+
* This provides a safety net for users who forget to call close(), preventing
|
|
25
|
+
* memory leaks in high-throughput scenarios where GC may be delayed.
|
|
26
|
+
*/
|
|
27
|
+
const videoChunkRegistry = new FinalizationRegistry<NativeEncodedVideoChunk>((native) => {
|
|
28
|
+
// The weak reference to the JS wrapper is now dead, but the native object
|
|
29
|
+
// may still be valid. Call close() to release its internal data buffer.
|
|
30
|
+
// close() is idempotent - safe to call even if already closed.
|
|
31
|
+
try {
|
|
32
|
+
native.close();
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore errors - native object may already be destroyed
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* FinalizationRegistry for automatic cleanup of native EncodedAudioChunk objects.
|
|
40
|
+
*/
|
|
41
|
+
const audioChunkRegistry = new FinalizationRegistry<NativeEncodedAudioChunk>((native) => {
|
|
42
|
+
try {
|
|
43
|
+
native.close();
|
|
44
|
+
} catch {
|
|
45
|
+
// Ignore errors - native object may already be destroyed
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
18
49
|
export class EncodedVideoChunk {
|
|
19
50
|
/** @internal */
|
|
20
51
|
_native: NativeEncodedVideoChunk;
|
|
@@ -24,7 +55,6 @@ export class EncodedVideoChunk {
|
|
|
24
55
|
if (init.type !== 'key' && init.type !== 'delta') {
|
|
25
56
|
throw new TypeError(`Invalid type: ${init.type}`);
|
|
26
57
|
}
|
|
27
|
-
// Convert BufferSource to Buffer for native
|
|
28
58
|
let dataBuffer: Buffer;
|
|
29
59
|
if (init.data instanceof ArrayBuffer) {
|
|
30
60
|
dataBuffer = Buffer.from(init.data);
|
|
@@ -39,6 +69,29 @@ export class EncodedVideoChunk {
|
|
|
39
69
|
duration: init.duration,
|
|
40
70
|
data: dataBuffer,
|
|
41
71
|
});
|
|
72
|
+
|
|
73
|
+
// Register with FinalizationRegistry for automatic cleanup.
|
|
74
|
+
// When this JS wrapper is GC'd, the registry callback will call close()
|
|
75
|
+
// on the native object to release memory.
|
|
76
|
+
videoChunkRegistry.register(this, this._native, this);
|
|
77
|
+
|
|
78
|
+
// Handle ArrayBuffer transfer semantics per W3C spec
|
|
79
|
+
if (init.transfer && Array.isArray(init.transfer)) {
|
|
80
|
+
detachArrayBuffers(init.transfer.filter((b): b is ArrayBuffer => b instanceof ArrayBuffer));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @internal
|
|
86
|
+
* Wrap an existing native EncodedVideoChunk without copying data.
|
|
87
|
+
* Used by the encoder's async output path to avoid double-copying.
|
|
88
|
+
*/
|
|
89
|
+
static _fromNative(nativeChunk: NativeEncodedVideoChunk): EncodedVideoChunk {
|
|
90
|
+
const chunk = Object.create(EncodedVideoChunk.prototype) as EncodedVideoChunk;
|
|
91
|
+
chunk._native = nativeChunk;
|
|
92
|
+
// Register with FinalizationRegistry for automatic cleanup
|
|
93
|
+
videoChunkRegistry.register(chunk, nativeChunk, chunk);
|
|
94
|
+
return chunk;
|
|
42
95
|
}
|
|
43
96
|
|
|
44
97
|
get type(): 'key' | 'delta' {
|
|
@@ -75,13 +128,28 @@ export class EncodedVideoChunk {
|
|
|
75
128
|
throw new TypeError('Destination must be ArrayBuffer or ArrayBufferView');
|
|
76
129
|
}
|
|
77
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Releases the internal data buffer.
|
|
134
|
+
* Per W3C WebCodecs spec, this allows early release of memory.
|
|
135
|
+
*/
|
|
136
|
+
close(): void {
|
|
137
|
+
// Unregister from FinalizationRegistry to prevent double-close.
|
|
138
|
+
// If close() is called explicitly, we don't need the registry callback.
|
|
139
|
+
videoChunkRegistry.unregister(this);
|
|
140
|
+
this._native.close();
|
|
141
|
+
}
|
|
78
142
|
}
|
|
79
143
|
|
|
80
144
|
export class EncodedAudioChunk {
|
|
81
145
|
private _native: NativeEncodedAudioChunk;
|
|
82
146
|
|
|
83
147
|
constructor(init: EncodedAudioChunkInit) {
|
|
84
|
-
//
|
|
148
|
+
// W3C spec: type must be 'key' or 'delta'
|
|
149
|
+
if (init.type !== 'key' && init.type !== 'delta') {
|
|
150
|
+
throw new TypeError(`Invalid type: ${init.type}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
85
153
|
let dataBuffer: Buffer;
|
|
86
154
|
if (init.data instanceof ArrayBuffer) {
|
|
87
155
|
dataBuffer = Buffer.from(init.data);
|
|
@@ -96,6 +164,16 @@ export class EncodedAudioChunk {
|
|
|
96
164
|
duration: init.duration,
|
|
97
165
|
data: dataBuffer,
|
|
98
166
|
});
|
|
167
|
+
|
|
168
|
+
// Register with FinalizationRegistry for automatic cleanup.
|
|
169
|
+
// When this JS wrapper is GC'd, the registry callback will call close()
|
|
170
|
+
// on the native object to release memory.
|
|
171
|
+
audioChunkRegistry.register(this, this._native, this);
|
|
172
|
+
|
|
173
|
+
// Handle ArrayBuffer transfer semantics per W3C spec
|
|
174
|
+
if (init.transfer && Array.isArray(init.transfer)) {
|
|
175
|
+
detachArrayBuffers(init.transfer.filter((b): b is ArrayBuffer => b instanceof ArrayBuffer));
|
|
176
|
+
}
|
|
99
177
|
}
|
|
100
178
|
|
|
101
179
|
get type(): 'key' | 'delta' {
|
|
@@ -128,6 +206,16 @@ export class EncodedAudioChunk {
|
|
|
128
206
|
}
|
|
129
207
|
}
|
|
130
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Releases the internal data buffer.
|
|
211
|
+
* Per W3C WebCodecs spec, this allows early release of memory.
|
|
212
|
+
*/
|
|
213
|
+
close(): void {
|
|
214
|
+
// Unregister from FinalizationRegistry to prevent double-close.
|
|
215
|
+
audioChunkRegistry.unregister(this);
|
|
216
|
+
this._native.close();
|
|
217
|
+
}
|
|
218
|
+
|
|
131
219
|
get _nativeChunk(): NativeEncodedAudioChunk {
|
|
132
220
|
return this._native;
|
|
133
221
|
}
|
package/lib/image-decoder.ts
CHANGED
|
@@ -49,6 +49,7 @@ export class ImageDecoder {
|
|
|
49
49
|
this._initOptions = {
|
|
50
50
|
type: init.type,
|
|
51
51
|
colorSpaceConversion: init.colorSpaceConversion,
|
|
52
|
+
premultiplyAlpha: init.premultiplyAlpha,
|
|
52
53
|
desiredWidth: init.desiredWidth,
|
|
53
54
|
desiredHeight: init.desiredHeight,
|
|
54
55
|
preferAnimation: init.preferAnimation,
|
|
@@ -86,6 +87,7 @@ export class ImageDecoder {
|
|
|
86
87
|
type: string;
|
|
87
88
|
data: Buffer;
|
|
88
89
|
colorSpaceConversion?: string;
|
|
90
|
+
premultiplyAlpha?: string;
|
|
89
91
|
desiredWidth?: number;
|
|
90
92
|
desiredHeight?: number;
|
|
91
93
|
preferAnimation?: boolean;
|
|
@@ -98,6 +100,9 @@ export class ImageDecoder {
|
|
|
98
100
|
if ('colorSpaceConversion' in init && init.colorSpaceConversion) {
|
|
99
101
|
nativeInit.colorSpaceConversion = init.colorSpaceConversion;
|
|
100
102
|
}
|
|
103
|
+
if ('premultiplyAlpha' in init && init.premultiplyAlpha !== undefined) {
|
|
104
|
+
nativeInit.premultiplyAlpha = init.premultiplyAlpha;
|
|
105
|
+
}
|
|
101
106
|
if ('desiredWidth' in init && init.desiredWidth !== undefined) {
|
|
102
107
|
nativeInit.desiredWidth = init.desiredWidth;
|
|
103
108
|
}
|
package/lib/index.ts
CHANGED
|
@@ -47,6 +47,9 @@ export const ErrorBuilder = native.ErrorBuilder;
|
|
|
47
47
|
export const testAttrAsEnum = native.testAttrAsEnum;
|
|
48
48
|
export const createEncoderConfigDescriptor = native.createEncoderConfigDescriptor;
|
|
49
49
|
|
|
50
|
+
// Export instance counters for monitoring and leak detection
|
|
51
|
+
export const getCounters = native.getCounters;
|
|
52
|
+
|
|
50
53
|
export type { ErrorCodeType } from './errors';
|
|
51
54
|
// Re-export error classes and codes
|
|
52
55
|
export {
|
package/lib/native-types.ts
CHANGED
|
@@ -80,6 +80,7 @@ export interface NativeVideoDecoder {
|
|
|
80
80
|
readonly state: CodecState;
|
|
81
81
|
readonly decodeQueueSize: number;
|
|
82
82
|
readonly codecSaturated: boolean;
|
|
83
|
+
readonly pendingFrames: number;
|
|
83
84
|
|
|
84
85
|
configure(config: VideoDecoderConfig): void;
|
|
85
86
|
decode(chunk: NativeEncodedVideoChunk): void;
|
|
@@ -98,6 +99,7 @@ export interface NativeEncodedVideoChunk {
|
|
|
98
99
|
readonly byteLength: number;
|
|
99
100
|
|
|
100
101
|
copyTo(dest: Uint8Array | ArrayBuffer): void;
|
|
102
|
+
close(): void;
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
/**
|
|
@@ -127,6 +129,7 @@ export interface NativeEncodedAudioChunk {
|
|
|
127
129
|
readonly byteLength: number;
|
|
128
130
|
|
|
129
131
|
copyTo(dest: Uint8Array | ArrayBuffer): void;
|
|
132
|
+
close(): void;
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
/**
|
|
@@ -173,6 +176,12 @@ export interface NativeVideoFilter {
|
|
|
173
176
|
export interface NativeDemuxer {
|
|
174
177
|
open(path: string): void;
|
|
175
178
|
demux(): void;
|
|
179
|
+
/**
|
|
180
|
+
* Read packets from the file in chunks.
|
|
181
|
+
* @param maxPackets - Maximum number of packets to read. 0 = unlimited.
|
|
182
|
+
* @returns The number of packets actually read.
|
|
183
|
+
*/
|
|
184
|
+
demuxPackets(maxPackets: number): number;
|
|
176
185
|
close(): void;
|
|
177
186
|
getVideoTrack(): TrackInfo | null;
|
|
178
187
|
getAudioTrack(): TrackInfo | null;
|
|
@@ -509,6 +518,19 @@ export interface NativeModule {
|
|
|
509
518
|
// Test helpers
|
|
510
519
|
testAttrAsEnum: (obj: object, attr: string) => string;
|
|
511
520
|
|
|
521
|
+
// Instance counters for monitoring and leak detection
|
|
522
|
+
getCounters: () => {
|
|
523
|
+
videoFrames: number;
|
|
524
|
+
audioData: number;
|
|
525
|
+
videoEncoders: number;
|
|
526
|
+
videoDecoders: number;
|
|
527
|
+
audioEncoders: number;
|
|
528
|
+
audioDecoders: number;
|
|
529
|
+
queue: number;
|
|
530
|
+
process: number;
|
|
531
|
+
frames: number;
|
|
532
|
+
};
|
|
533
|
+
|
|
512
534
|
// Descriptor factories
|
|
513
535
|
createEncoderConfigDescriptor: (config: object) => {
|
|
514
536
|
codec: string;
|
package/lib/platform.ts
CHANGED
|
@@ -5,35 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import * as os from 'node:os';
|
|
7
7
|
|
|
8
|
-
// Try to detect musl vs glibc on Linux
|
|
9
|
-
function detectLibc(): 'glibc' | 'musl' | null {
|
|
10
|
-
if (os.platform() !== 'linux') return null;
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
const { familySync } = require('detect-libc');
|
|
14
|
-
return familySync() === 'musl' ? 'musl' : 'glibc';
|
|
15
|
-
} catch {
|
|
16
|
-
// detect-libc not available, assume glibc
|
|
17
|
-
return 'glibc';
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
8
|
/**
|
|
22
9
|
* Get the runtime platform-architecture string.
|
|
23
|
-
* Handles musl vs glibc distinction on Linux.
|
|
24
10
|
*/
|
|
25
11
|
export function runtimePlatformArch(): string {
|
|
26
|
-
|
|
27
|
-
const arch = os.arch();
|
|
28
|
-
|
|
29
|
-
if (platform === 'linux') {
|
|
30
|
-
const libc = detectLibc();
|
|
31
|
-
if (libc === 'musl') {
|
|
32
|
-
return `linuxmusl-${arch}`;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return `${platform}-${arch}`;
|
|
12
|
+
return `${os.platform()}-${os.arch()}`;
|
|
37
13
|
}
|
|
38
14
|
|
|
39
15
|
/**
|
|
@@ -57,8 +33,6 @@ export const prebuiltPlatforms = [
|
|
|
57
33
|
'darwin-arm64',
|
|
58
34
|
'darwin-x64',
|
|
59
35
|
'linux-x64',
|
|
60
|
-
'linuxmusl-x64',
|
|
61
|
-
'win32-x64',
|
|
62
36
|
] as const;
|
|
63
37
|
|
|
64
38
|
export type PrebuiltPlatform = (typeof prebuiltPlatforms)[number];
|
|
@@ -70,17 +44,3 @@ export function isPrebuiltAvailable(): boolean {
|
|
|
70
44
|
const platform = runtimePlatformArch();
|
|
71
45
|
return prebuiltPlatforms.includes(platform as PrebuiltPlatform);
|
|
72
46
|
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Get the npm package name for the prebuilt binary.
|
|
76
|
-
*/
|
|
77
|
-
export function getPrebuiltPackageName(): string {
|
|
78
|
-
return `@pproenca/node-webcodecs-${runtimePlatformArch()}`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Get the npm package name for prebuilt FFmpeg.
|
|
83
|
-
*/
|
|
84
|
-
export function getFFmpegPackageName(): string {
|
|
85
|
-
return `@pproenca/ffmpeg-${runtimePlatformArch()}`;
|
|
86
|
-
}
|
package/lib/resource-manager.ts
CHANGED
|
@@ -23,10 +23,9 @@ export class ResourceManager {
|
|
|
23
23
|
private static instance: ResourceManager | null = null;
|
|
24
24
|
private codecs: Map<symbol, CodecEntry> = new Map();
|
|
25
25
|
private inactivityTimeout: number = 10000; // 10 seconds per spec
|
|
26
|
-
private checkInterval: ReturnType<typeof setInterval> | null = null;
|
|
27
26
|
|
|
28
27
|
private constructor() {
|
|
29
|
-
|
|
28
|
+
// Monitoring happens on-demand via getReclaimableCodecs()
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
static getInstance(): ResourceManager {
|
|
@@ -132,26 +131,11 @@ export class ResourceManager {
|
|
|
132
131
|
this.inactivityTimeout = ms;
|
|
133
132
|
}
|
|
134
133
|
|
|
135
|
-
private startMonitoring(): void {
|
|
136
|
-
// Check every 5 seconds
|
|
137
|
-
this.checkInterval = setInterval(() => {
|
|
138
|
-
// Just track, don't auto-reclaim
|
|
139
|
-
// Actual reclamation would be triggered by memory pressure
|
|
140
|
-
}, 5000);
|
|
141
|
-
|
|
142
|
-
// Don't keep process alive
|
|
143
|
-
if (this.checkInterval.unref) {
|
|
144
|
-
this.checkInterval.unref();
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
134
|
/**
|
|
149
135
|
* Stop monitoring (for cleanup).
|
|
136
|
+
* @deprecated No longer needed - monitoring happens on-demand via getReclaimableCodecs()
|
|
150
137
|
*/
|
|
151
138
|
stopMonitoring(): void {
|
|
152
|
-
|
|
153
|
-
clearInterval(this.checkInterval);
|
|
154
|
-
this.checkInterval = null;
|
|
155
|
-
}
|
|
139
|
+
// No-op: monitoring now happens on-demand
|
|
156
140
|
}
|
|
157
141
|
}
|
package/lib/types.ts
CHANGED
|
@@ -82,6 +82,17 @@ export type HardwareAcceleration = 'no-preference' | 'prefer-hardware' | 'prefer
|
|
|
82
82
|
*/
|
|
83
83
|
export type AlphaOption = 'keep' | 'discard';
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* WebIDL:
|
|
87
|
+
* enum PremultiplyAlpha { "none", "premultiply", "default" };
|
|
88
|
+
*
|
|
89
|
+
* Per W3C WebCodecs spec:
|
|
90
|
+
* - "none": Do not premultiply alpha
|
|
91
|
+
* - "premultiply": Premultiply RGB values by alpha
|
|
92
|
+
* - "default": Use default behavior (typically none)
|
|
93
|
+
*/
|
|
94
|
+
export type PremultiplyAlpha = 'none' | 'premultiply' | 'default';
|
|
95
|
+
|
|
85
96
|
// =============================================================================
|
|
86
97
|
// LATENCY MODE
|
|
87
98
|
// =============================================================================
|
|
@@ -816,6 +827,7 @@ export type ImageBufferSource = AllowSharedBufferSource | ReadableStream<Uint8Ar
|
|
|
816
827
|
* required DOMString type;
|
|
817
828
|
* required ImageBufferSource data;
|
|
818
829
|
* ColorSpaceConversion colorSpaceConversion = "default";
|
|
830
|
+
* PremultiplyAlpha premultiplyAlpha = "default";
|
|
819
831
|
* [EnforceRange] unsigned long desiredWidth;
|
|
820
832
|
* [EnforceRange] unsigned long desiredHeight;
|
|
821
833
|
* boolean preferAnimation;
|
|
@@ -826,6 +838,7 @@ export interface ImageDecoderInit {
|
|
|
826
838
|
type: string;
|
|
827
839
|
data: ImageBufferSource;
|
|
828
840
|
colorSpaceConversion?: ColorSpaceConversion;
|
|
841
|
+
premultiplyAlpha?: PremultiplyAlpha;
|
|
829
842
|
desiredWidth?: number;
|
|
830
843
|
desiredHeight?: number;
|
|
831
844
|
preferAnimation?: boolean;
|
package/lib/video-decoder.ts
CHANGED
|
@@ -17,6 +17,9 @@ import { VideoFrame } from './video-frame';
|
|
|
17
17
|
// Load native addon with type assertion
|
|
18
18
|
const native = binding as NativeModule;
|
|
19
19
|
|
|
20
|
+
// Default backpressure threshold for limiting in-flight chunks
|
|
21
|
+
const DEFAULT_MAX_QUEUE_DEPTH = 16;
|
|
22
|
+
|
|
20
23
|
export class VideoDecoder extends CodecBase {
|
|
21
24
|
private _native: NativeVideoDecoder;
|
|
22
25
|
private _controlQueue: ControlMessageQueue;
|
|
@@ -25,6 +28,9 @@ export class VideoDecoder extends CodecBase {
|
|
|
25
28
|
private _errorCallback: (error: DOMException) => void;
|
|
26
29
|
private _resourceId: symbol;
|
|
27
30
|
|
|
31
|
+
// Backpressure support
|
|
32
|
+
private _maxQueueDepth: number = DEFAULT_MAX_QUEUE_DEPTH;
|
|
33
|
+
|
|
28
34
|
constructor(init: VideoDecoderInit) {
|
|
29
35
|
super();
|
|
30
36
|
// W3C spec: output and error callbacks are required
|
|
@@ -38,7 +44,6 @@ export class VideoDecoder extends CodecBase {
|
|
|
38
44
|
this._resourceId = ResourceManager.getInstance().register(this);
|
|
39
45
|
|
|
40
46
|
const outputCallback: VideoDecoderOutputCallback = (nativeFrame) => {
|
|
41
|
-
// Decrement queue size when output received
|
|
42
47
|
this._decodeQueueSize = Math.max(0, this._decodeQueueSize - 1);
|
|
43
48
|
|
|
44
49
|
// Wrap the native frame as a VideoFrame
|
|
@@ -71,11 +76,78 @@ export class VideoDecoder extends CodecBase {
|
|
|
71
76
|
return this._native.codecSaturated;
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Returns a Promise that resolves when the decoder has capacity for more chunks.
|
|
81
|
+
* Use this to implement backpressure in high-throughput decoding pipelines.
|
|
82
|
+
*
|
|
83
|
+
* When the internal queue is full (decodeQueueSize >= maxQueueDepth), calling
|
|
84
|
+
* `await decoder.ready` will pause until capacity is available.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* for (const chunk of chunks) {
|
|
88
|
+
* await decoder.ready; // Wait for capacity
|
|
89
|
+
* decoder.decode(chunk);
|
|
90
|
+
* }
|
|
91
|
+
*/
|
|
92
|
+
get ready(): Promise<void> {
|
|
93
|
+
// If we have capacity, resolve immediately
|
|
94
|
+
if (this._decodeQueueSize < this._maxQueueDepth) {
|
|
95
|
+
return Promise.resolve();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Otherwise, poll until capacity is available.
|
|
99
|
+
// We use setTimeout(1ms) polling to allow TSFN output callbacks to execute.
|
|
100
|
+
// setTimeout ensures we yield through the full event loop cycle, including
|
|
101
|
+
// the I/O phase where TSFN callbacks are delivered.
|
|
102
|
+
return new Promise<void>((resolve) => {
|
|
103
|
+
const checkCapacity = () => {
|
|
104
|
+
if (this._decodeQueueSize < this._maxQueueDepth) {
|
|
105
|
+
resolve();
|
|
106
|
+
} else {
|
|
107
|
+
// Yield full event loop cycle to allow output callbacks to run
|
|
108
|
+
setTimeout(checkCapacity, 1);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
// Initial yield to allow any pending callbacks to run
|
|
112
|
+
setTimeout(checkCapacity, 1);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The maximum queue depth before backpressure is applied.
|
|
118
|
+
* Default is 16. Adjust based on memory constraints and frame size.
|
|
119
|
+
*/
|
|
120
|
+
get maxQueueDepth(): number {
|
|
121
|
+
return this._maxQueueDepth;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
set maxQueueDepth(value: number) {
|
|
125
|
+
if (value < 1) {
|
|
126
|
+
throw new RangeError('maxQueueDepth must be at least 1');
|
|
127
|
+
}
|
|
128
|
+
this._maxQueueDepth = value;
|
|
129
|
+
}
|
|
130
|
+
|
|
74
131
|
configure(config: VideoDecoderConfig): void {
|
|
75
132
|
// W3C spec: throw if closed
|
|
76
133
|
if (this.state === 'closed') {
|
|
77
134
|
throw new DOMException('Decoder is closed', 'InvalidStateError');
|
|
78
135
|
}
|
|
136
|
+
|
|
137
|
+
// Validate rotation (node-webcodecs extension)
|
|
138
|
+
if ('rotation' in config && config.rotation !== undefined) {
|
|
139
|
+
if (![0, 90, 180, 270].includes(config.rotation)) {
|
|
140
|
+
throw new TypeError(`rotation must be 0, 90, 180, or 270, got ${config.rotation}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Validate flip (node-webcodecs extension)
|
|
145
|
+
if ('flip' in config && config.flip !== undefined) {
|
|
146
|
+
if (typeof config.flip !== 'boolean') {
|
|
147
|
+
throw new TypeError('flip must be a boolean');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
79
151
|
this._needsKeyFrame = true;
|
|
80
152
|
// Configure synchronously to set state immediately per W3C spec
|
|
81
153
|
this._native.configure(config);
|
|
@@ -111,7 +183,17 @@ export class VideoDecoder extends CodecBase {
|
|
|
111
183
|
return Promise.reject(new DOMException('Decoder is closed', 'InvalidStateError'));
|
|
112
184
|
}
|
|
113
185
|
await this._controlQueue.flush();
|
|
114
|
-
|
|
186
|
+
|
|
187
|
+
// Flush the native decoder (waits for worker queue to drain)
|
|
188
|
+
this._native.flush();
|
|
189
|
+
|
|
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
|
+
}
|
|
115
197
|
}
|
|
116
198
|
|
|
117
199
|
reset(): void {
|
package/lib/video-encoder.ts
CHANGED
|
@@ -17,12 +17,18 @@ import type { VideoFrame } from './video-frame';
|
|
|
17
17
|
// Load native addon with type assertion
|
|
18
18
|
const native = binding as NativeModule;
|
|
19
19
|
|
|
20
|
+
// Default backpressure threshold for limiting in-flight frames
|
|
21
|
+
const DEFAULT_MAX_QUEUE_DEPTH = 16;
|
|
22
|
+
|
|
20
23
|
export class VideoEncoder extends CodecBase {
|
|
21
24
|
private _native: NativeVideoEncoder;
|
|
22
25
|
private _controlQueue: ControlMessageQueue;
|
|
23
26
|
private _encodeQueueSize: number = 0;
|
|
24
27
|
private _resourceId: symbol;
|
|
25
28
|
|
|
29
|
+
// Backpressure support
|
|
30
|
+
private _maxQueueDepth: number = DEFAULT_MAX_QUEUE_DEPTH;
|
|
31
|
+
|
|
26
32
|
constructor(init: VideoEncoderInit) {
|
|
27
33
|
super();
|
|
28
34
|
// W3C spec: output and error callbacks are required
|
|
@@ -35,19 +41,32 @@ export class VideoEncoder extends CodecBase {
|
|
|
35
41
|
this._resourceId = ResourceManager.getInstance().register(this);
|
|
36
42
|
|
|
37
43
|
const outputCallback: VideoEncoderOutputCallback = (chunk, metadata) => {
|
|
38
|
-
// Decrement queue size when output received
|
|
39
44
|
this._encodeQueueSize = Math.max(0, this._encodeQueueSize - 1);
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
// The native layer now returns an EncodedVideoChunk directly (not a plain object).
|
|
47
|
+
// Check if it's already a native chunk (has close method) vs plain object (has data buffer).
|
|
48
|
+
// Native chunks have close() but no 'data' property; plain objects have 'data' buffer.
|
|
49
|
+
let wrappedChunk: EncodedVideoChunk;
|
|
50
|
+
if ('data' in chunk && chunk.data instanceof Buffer) {
|
|
51
|
+
// Legacy path: plain object from sync encoder - wrap it
|
|
52
|
+
wrappedChunk = new EncodedVideoChunk({
|
|
53
|
+
type: chunk.type as 'key' | 'delta',
|
|
54
|
+
timestamp: chunk.timestamp,
|
|
55
|
+
duration: chunk.duration ?? undefined,
|
|
56
|
+
data: chunk.data,
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
// New path: already a native EncodedVideoChunk from async encoder
|
|
60
|
+
// Wrap without copying data
|
|
61
|
+
wrappedChunk = EncodedVideoChunk._fromNative(
|
|
62
|
+
chunk as unknown as import('./native-types').NativeEncodedVideoChunk,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
47
65
|
init.output(wrappedChunk, metadata);
|
|
48
66
|
|
|
49
67
|
// Fire ondequeue after output
|
|
50
68
|
this._triggerDequeue();
|
|
69
|
+
|
|
51
70
|
};
|
|
52
71
|
|
|
53
72
|
this._native = new native.VideoEncoder({
|
|
@@ -68,6 +87,59 @@ export class VideoEncoder extends CodecBase {
|
|
|
68
87
|
return this._native.codecSaturated;
|
|
69
88
|
}
|
|
70
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Returns a Promise that resolves when the encoder has capacity for more frames.
|
|
92
|
+
* Use this to implement backpressure in high-throughput encoding pipelines.
|
|
93
|
+
*
|
|
94
|
+
* When the internal queue is full (encodeQueueSize >= maxQueueDepth), calling
|
|
95
|
+
* `await encoder.ready` will pause until capacity is available.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* for (const frame of frames) {
|
|
99
|
+
* await encoder.ready; // Wait for capacity
|
|
100
|
+
* encoder.encode(frame);
|
|
101
|
+
* frame.close();
|
|
102
|
+
* }
|
|
103
|
+
*/
|
|
104
|
+
get ready(): Promise<void> {
|
|
105
|
+
// If we have capacity, resolve immediately
|
|
106
|
+
if (this._encodeQueueSize < this._maxQueueDepth) {
|
|
107
|
+
return Promise.resolve();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Otherwise, poll until capacity is available.
|
|
111
|
+
// We use setTimeout(1ms) polling to allow TSFN output callbacks to execute.
|
|
112
|
+
// setTimeout ensures we yield through the full event loop cycle, including
|
|
113
|
+
// the I/O phase where TSFN callbacks are delivered.
|
|
114
|
+
return new Promise<void>((resolve) => {
|
|
115
|
+
const checkCapacity = () => {
|
|
116
|
+
if (this._encodeQueueSize < this._maxQueueDepth) {
|
|
117
|
+
resolve();
|
|
118
|
+
} else {
|
|
119
|
+
// Yield full event loop cycle to allow output callbacks to run
|
|
120
|
+
setTimeout(checkCapacity, 1);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
// Initial yield to allow any pending callbacks to run
|
|
124
|
+
setTimeout(checkCapacity, 1);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The maximum queue depth before backpressure is applied.
|
|
130
|
+
* Default is 16. Adjust based on memory constraints and frame size.
|
|
131
|
+
*/
|
|
132
|
+
get maxQueueDepth(): number {
|
|
133
|
+
return this._maxQueueDepth;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
set maxQueueDepth(value: number) {
|
|
137
|
+
if (value < 1) {
|
|
138
|
+
throw new RangeError('maxQueueDepth must be at least 1');
|
|
139
|
+
}
|
|
140
|
+
this._maxQueueDepth = value;
|
|
141
|
+
}
|
|
142
|
+
|
|
71
143
|
configure(config: VideoEncoderConfig): void {
|
|
72
144
|
// W3C spec: throw if closed
|
|
73
145
|
if (this.state === 'closed') {
|
|
@@ -84,6 +156,10 @@ export class VideoEncoder extends CodecBase {
|
|
|
84
156
|
}
|
|
85
157
|
|
|
86
158
|
encode(frame: VideoFrame, options?: { keyFrame?: boolean }): void {
|
|
159
|
+
// W3C spec: throw if not configured
|
|
160
|
+
if (this.state !== 'configured') {
|
|
161
|
+
throw new DOMException(`Encoder is ${this.state}`, 'InvalidStateError');
|
|
162
|
+
}
|
|
87
163
|
ResourceManager.getInstance().recordActivity(this._resourceId);
|
|
88
164
|
this._encodeQueueSize++;
|
|
89
165
|
// Call native encode directly - frame must be valid at call time
|
|
@@ -105,12 +181,18 @@ export class VideoEncoder extends CodecBase {
|
|
|
105
181
|
|
|
106
182
|
// Poll for pending TSFN callbacks to complete.
|
|
107
183
|
// This allows the event loop to run (delivering callbacks) while we wait.
|
|
184
|
+
// Using setTimeout(1ms) instead of setImmediate to ensure other event loop
|
|
185
|
+
// phases (timers, I/O) can run, preventing event loop starvation.
|
|
108
186
|
while (this._native.pendingChunks > 0) {
|
|
109
|
-
await new Promise((resolve) =>
|
|
187
|
+
await new Promise((resolve) => setTimeout(resolve, 1)); // 1ms poll
|
|
110
188
|
}
|
|
111
189
|
}
|
|
112
190
|
|
|
113
191
|
reset(): void {
|
|
192
|
+
// W3C spec: throw if closed
|
|
193
|
+
if (this.state === 'closed') {
|
|
194
|
+
throw new DOMException('Encoder is closed', 'InvalidStateError');
|
|
195
|
+
}
|
|
114
196
|
this._controlQueue.clear();
|
|
115
197
|
this._encodeQueueSize = 0;
|
|
116
198
|
this._native.reset();
|