@pproenca/node-webcodecs 0.1.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 +78 -206
- 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 -124
- 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 +17 -3
- package/dist/index.js +9 -4
- package/dist/is.d.ts +18 -0
- package/dist/is.js +14 -0
- 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 +46 -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/dist/video-frame.d.ts +6 -3
- package/dist/video-frame.js +36 -4
- 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 +9 -3
- package/lib/is.ts +32 -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 +52 -1
- package/lib/video-decoder.ts +84 -2
- package/lib/video-encoder.ts +90 -8
- package/lib/video-frame.ts +52 -7
- package/package.json +49 -32
- package/src/addon.cc +57 -0
- package/src/async_decode_worker.cc +243 -36
- package/src/async_decode_worker.h +55 -4
- package/src/async_encode_worker.cc +155 -44
- package/src/async_encode_worker.h +38 -12
- 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 +428 -35
- package/src/video_encoder.h +16 -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/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
|
|
29
|
-
() => {
|
|
30
|
-
const pkg = getPrebuiltPackageName();
|
|
31
|
-
|
|
32
|
-
return require(pkg);
|
|
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
|
}
|
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
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
* - VideoEncoder, VideoDecoder, AudioEncoder, AudioDecoder extend EventTarget via CodecBase
|
|
6
6
|
* - VideoFrame visibleRect cropping implemented in native layer
|
|
7
7
|
* - ArrayBuffer transfer semantics implemented (uses structuredClone with transfer)
|
|
8
|
-
* - High bit-depth pixel formats for VideoFrame (I420P10, I420P12,
|
|
8
|
+
* - High bit-depth pixel formats for VideoFrame (I420P10, I420P12, etc.)
|
|
9
9
|
* Note: VideoEncoder input format conversion does not yet support high bit-depth formats
|
|
10
10
|
* - NV21 pixel format supported (8-bit YUV420 semi-planar with VU ordering)
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
11
|
+
* - VideoFrame constructor accepts ImageData (from canvas.getImageData()) - Node.js extension
|
|
12
|
+
* Usage: const frame = new VideoFrame(ctx.getImageData(0, 0, w, h), { timestamp: 0 })
|
|
13
|
+
* - ImageDecoder decodes JPEG/PNG/WebP/GIF directly to VideoFrame
|
|
14
|
+
* - 10-bit alpha formats (I420AP10, I422AP10, I444AP10) supported
|
|
15
|
+
* - SVC temporal layer tracking via scalabilityMode (L1T1, L1T2, L1T3)
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
import { binding, platformInfo } from './binding';
|
|
@@ -44,6 +47,9 @@ export const ErrorBuilder = native.ErrorBuilder;
|
|
|
44
47
|
export const testAttrAsEnum = native.testAttrAsEnum;
|
|
45
48
|
export const createEncoderConfigDescriptor = native.createEncoderConfigDescriptor;
|
|
46
49
|
|
|
50
|
+
// Export instance counters for monitoring and leak detection
|
|
51
|
+
export const getCounters = native.getCounters;
|
|
52
|
+
|
|
47
53
|
export type { ErrorCodeType } from './errors';
|
|
48
54
|
// Re-export error classes and codes
|
|
49
55
|
export {
|
package/lib/is.ts
CHANGED
|
@@ -135,6 +135,38 @@ export function inArray<T>(val: T, list: readonly T[]): boolean {
|
|
|
135
135
|
return list.includes(val);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
//==============================================================================
|
|
139
|
+
// ImageData Support (for canvas integration)
|
|
140
|
+
//==============================================================================
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* ImageData-like interface (from canvas.getContext('2d').getImageData()).
|
|
144
|
+
* Self-describing: contains width, height, and RGBA pixel data.
|
|
145
|
+
*
|
|
146
|
+
* Note: This is a Node.js extension to WebCodecs, providing equivalent
|
|
147
|
+
* functionality to CanvasImageSource for server-side canvas integration.
|
|
148
|
+
*/
|
|
149
|
+
export interface ImageDataLike {
|
|
150
|
+
readonly width: number;
|
|
151
|
+
readonly height: number;
|
|
152
|
+
readonly data: Uint8ClampedArray;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Is this value an ImageData object (from canvas.getImageData)?
|
|
157
|
+
* ImageData is self-describing: contains width, height, and Uint8ClampedArray data.
|
|
158
|
+
* This follows sharp's pattern of accepting self-describing pixel buffers.
|
|
159
|
+
*/
|
|
160
|
+
export function isImageData(val: unknown): val is ImageDataLike {
|
|
161
|
+
if (!object(val)) return false;
|
|
162
|
+
const img = val as Record<string, unknown>;
|
|
163
|
+
return (
|
|
164
|
+
positiveInteger(img.width) &&
|
|
165
|
+
positiveInteger(img.height) &&
|
|
166
|
+
img.data instanceof Uint8ClampedArray
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
138
170
|
//==============================================================================
|
|
139
171
|
// Domain-Specific Guards
|
|
140
172
|
//==============================================================================
|
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
|
}
|