webcodecs-node 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +328 -169
- package/dist/backends/types.d.ts +2 -0
- package/dist/backends/types.d.ts.map +1 -1
- package/dist/backends/types.js.map +1 -1
- package/dist/canvas/canvas-utils.d.ts +115 -0
- package/dist/canvas/canvas-utils.d.ts.map +1 -0
- package/dist/canvas/canvas-utils.js +169 -0
- package/dist/canvas/canvas-utils.js.map +1 -0
- package/dist/canvas/frame-loop.d.ts +113 -0
- package/dist/canvas/frame-loop.d.ts.map +1 -0
- package/dist/canvas/frame-loop.js +291 -0
- package/dist/canvas/frame-loop.js.map +1 -0
- package/dist/canvas/gpu-context.d.ts +61 -0
- package/dist/canvas/gpu-context.d.ts.map +1 -0
- package/dist/canvas/gpu-context.js +134 -0
- package/dist/canvas/gpu-context.js.map +1 -0
- package/dist/canvas/index.d.ts +22 -0
- package/dist/canvas/index.d.ts.map +1 -0
- package/dist/canvas/index.js +25 -0
- package/dist/canvas/index.js.map +1 -0
- package/dist/canvas/types.d.ts +101 -0
- package/dist/canvas/types.d.ts.map +1 -0
- package/dist/canvas/types.js +7 -0
- package/dist/canvas/types.js.map +1 -0
- package/dist/codec-utils/formats.d.ts.map +1 -1
- package/dist/codec-utils/formats.js +31 -0
- package/dist/codec-utils/formats.js.map +1 -1
- package/dist/config/ffmpeg-quality.d.ts +14 -0
- package/dist/config/ffmpeg-quality.d.ts.map +1 -0
- package/dist/config/ffmpeg-quality.js +41 -0
- package/dist/config/ffmpeg-quality.js.map +1 -0
- package/dist/containers/Muxer.d.ts.map +1 -1
- package/dist/containers/Muxer.js +4 -1
- package/dist/containers/Muxer.js.map +1 -1
- package/dist/core/AudioData.d.ts +2 -1
- package/dist/core/AudioData.d.ts.map +1 -1
- package/dist/core/AudioData.js.map +1 -1
- package/dist/core/VideoFrame.d.ts +2 -2
- package/dist/core/VideoFrame.d.ts.map +1 -1
- package/dist/core/VideoFrame.js +49 -47
- package/dist/core/VideoFrame.js.map +1 -1
- package/dist/decoders/AudioDecoder.d.ts +1 -0
- package/dist/decoders/AudioDecoder.d.ts.map +1 -1
- package/dist/decoders/AudioDecoder.js +39 -17
- package/dist/decoders/AudioDecoder.js.map +1 -1
- package/dist/decoders/ImageDecoder.d.ts +1 -0
- package/dist/decoders/ImageDecoder.d.ts.map +1 -1
- package/dist/decoders/ImageDecoder.js +40 -3
- package/dist/decoders/ImageDecoder.js.map +1 -1
- package/dist/decoders/VideoDecoder.d.ts +12 -0
- package/dist/decoders/VideoDecoder.d.ts.map +1 -1
- package/dist/decoders/VideoDecoder.js +50 -8
- package/dist/decoders/VideoDecoder.js.map +1 -1
- package/dist/demos/demo-1080p-transcode.js +8 -2
- package/dist/demos/demo-1080p-transcode.js.map +1 -1
- package/dist/demos/demo-audio-visualizer.d.ts +11 -0
- package/dist/demos/demo-audio-visualizer.d.ts.map +1 -0
- package/dist/demos/demo-audio-visualizer.js +281 -0
- package/dist/demos/demo-audio-visualizer.js.map +1 -0
- package/dist/demos/demo-dvd-logo.d.ts +8 -0
- package/dist/demos/demo-dvd-logo.d.ts.map +1 -0
- package/dist/demos/demo-dvd-logo.js +196 -0
- package/dist/demos/demo-dvd-logo.js.map +1 -0
- package/dist/demos/demo-four-corners.js +9 -0
- package/dist/demos/demo-four-corners.js.map +1 -1
- package/dist/demos/demo-streaming.js +6 -0
- package/dist/demos/demo-streaming.js.map +1 -1
- package/dist/demos/demo-webcodecs.js +1 -0
- package/dist/demos/demo-webcodecs.js.map +1 -1
- package/dist/encoders/AudioEncoder.d.ts +3 -0
- package/dist/encoders/AudioEncoder.d.ts.map +1 -1
- package/dist/encoders/AudioEncoder.js +70 -28
- package/dist/encoders/AudioEncoder.js.map +1 -1
- package/dist/encoders/ImageEncoder.d.ts +80 -0
- package/dist/encoders/ImageEncoder.d.ts.map +1 -0
- package/dist/encoders/ImageEncoder.js +156 -0
- package/dist/encoders/ImageEncoder.js.map +1 -0
- package/dist/encoders/VideoEncoder.d.ts +11 -0
- package/dist/encoders/VideoEncoder.d.ts.map +1 -1
- package/dist/encoders/VideoEncoder.js +46 -4
- package/dist/encoders/VideoEncoder.js.map +1 -1
- package/dist/encoders/index.d.ts +1 -0
- package/dist/encoders/index.d.ts.map +1 -1
- package/dist/encoders/index.js +1 -0
- package/dist/encoders/index.js.map +1 -1
- package/dist/formats/color-space.d.ts +88 -0
- package/dist/formats/color-space.d.ts.map +1 -1
- package/dist/formats/color-space.js +55 -1
- package/dist/formats/color-space.js.map +1 -1
- package/dist/formats/pixel-formats.d.ts +17 -1
- package/dist/formats/pixel-formats.d.ts.map +1 -1
- package/dist/formats/pixel-formats.js +74 -4
- package/dist/formats/pixel-formats.js.map +1 -1
- package/dist/hardware/detection.d.ts.map +1 -1
- package/dist/hardware/detection.js +5 -2
- package/dist/hardware/detection.js.map +1 -1
- package/dist/hardware/encoder-args.d.ts.map +1 -1
- package/dist/hardware/encoder-args.js +6 -6
- package/dist/hardware/encoder-args.js.map +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/node-av/NodeAvAudioDecoder.d.ts.map +1 -1
- package/dist/node-av/NodeAvAudioDecoder.js +3 -0
- package/dist/node-av/NodeAvAudioDecoder.js.map +1 -1
- package/dist/node-av/NodeAvAudioEncoder.d.ts +5 -0
- package/dist/node-av/NodeAvAudioEncoder.d.ts.map +1 -1
- package/dist/node-av/NodeAvAudioEncoder.js +102 -9
- package/dist/node-av/NodeAvAudioEncoder.js.map +1 -1
- package/dist/node-av/NodeAvVideoEncoder.d.ts +2 -0
- package/dist/node-av/NodeAvVideoEncoder.d.ts.map +1 -1
- package/dist/node-av/NodeAvVideoEncoder.js +44 -6
- package/dist/node-av/NodeAvVideoEncoder.js.map +1 -1
- package/dist/node-av/WebPImageDecoder.d.ts +54 -0
- package/dist/node-av/WebPImageDecoder.d.ts.map +1 -0
- package/dist/node-av/WebPImageDecoder.js +176 -0
- package/dist/node-av/WebPImageDecoder.js.map +1 -0
- package/dist/polyfills/OffscreenCanvas.d.ts +141 -7
- package/dist/polyfills/OffscreenCanvas.d.ts.map +1 -1
- package/dist/polyfills/OffscreenCanvas.js +217 -17
- package/dist/polyfills/OffscreenCanvas.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/native-frame.d.ts +74 -0
- package/dist/types/native-frame.d.ts.map +1 -0
- package/dist/types/native-frame.js +32 -0
- package/dist/types/native-frame.js.map +1 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/type-guards.d.ts +29 -1
- package/dist/utils/type-guards.d.ts.map +1 -1
- package/dist/utils/type-guards.js +47 -2
- package/dist/utils/type-guards.js.map +1 -1
- package/docs/api.md +156 -1
- package/docs/configuration.md +30 -14
- package/examples/README.md +28 -1
- package/examples/canvas-encoding.ts +222 -0
- package/examples/offscreen-canvas.ts +230 -0
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# webcodecs-node
|
|
2
2
|
|
|
3
|
-
WebCodecs API implementation for Node.js using
|
|
3
|
+
WebCodecs API implementation for Node.js using node-av.
|
|
4
4
|
|
|
5
5
|
This package provides a Node.js-compatible implementation of the [WebCodecs API](https://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API), enabling video and audio encoding/decoding in server-side JavaScript applications.
|
|
6
6
|
|
|
@@ -9,6 +9,7 @@ This package provides a Node.js-compatible implementation of the [WebCodecs API]
|
|
|
9
9
|
- **VideoEncoder / VideoDecoder** - H.264, HEVC, VP8, VP9, AV1
|
|
10
10
|
- **AudioEncoder / AudioDecoder** - AAC, Opus, MP3, FLAC, Vorbis
|
|
11
11
|
- **ImageDecoder** - PNG, JPEG, WebP, GIF, AVIF, BMP, TIFF (including animated with frame timing)
|
|
12
|
+
- **ImageEncoder** - Encode VideoFrames to PNG, JPEG, WebP
|
|
12
13
|
- **VideoFrame / AudioData** - Frame-level data manipulation
|
|
13
14
|
- **MediaCapabilities** - Query codec support, smooth playback, and power efficiency
|
|
14
15
|
- **Hardware Acceleration** - VAAPI, NVENC, QSV support
|
|
@@ -16,7 +17,8 @@ This package provides a Node.js-compatible implementation of the [WebCodecs API]
|
|
|
16
17
|
- **Latency Modes** - Configure for real-time streaming vs maximum compression
|
|
17
18
|
- **Bitrate Modes** - Constant, variable, and quantizer (CRF) encoding modes
|
|
18
19
|
- **Alpha Channel** - Preserve transparency with VP9 and AV1 codecs
|
|
19
|
-
- **
|
|
20
|
+
- **10-bit & HDR** - I420P10, P010 formats with HDR10 metadata support
|
|
21
|
+
- **Container Support** - MP4, WebM demuxing/muxing utilities
|
|
20
22
|
|
|
21
23
|
## Documentation
|
|
22
24
|
|
|
@@ -28,17 +30,11 @@ This package provides a Node.js-compatible implementation of the [WebCodecs API]
|
|
|
28
30
|
## Requirements
|
|
29
31
|
|
|
30
32
|
- Node.js 18+
|
|
31
|
-
-
|
|
33
|
+
- The `node-av` package (automatically installed as a dependency)
|
|
32
34
|
|
|
33
35
|
```bash
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# macOS
|
|
38
|
-
brew install ffmpeg
|
|
39
|
-
|
|
40
|
-
# Check installation
|
|
41
|
-
ffmpeg -version
|
|
36
|
+
# node-av provides native FFmpeg bindings - no separate FFmpeg installation required
|
|
37
|
+
npm install webcodecs-node
|
|
42
38
|
```
|
|
43
39
|
|
|
44
40
|
## Installation
|
|
@@ -67,7 +63,7 @@ const encoder = new VideoEncoder({
|
|
|
67
63
|
error: (e) => console.error(e),
|
|
68
64
|
});
|
|
69
65
|
|
|
70
|
-
|
|
66
|
+
encoder.configure({
|
|
71
67
|
codec: 'avc1.42001E', // H.264 Baseline
|
|
72
68
|
width: 1280,
|
|
73
69
|
height: 720,
|
|
@@ -100,12 +96,12 @@ Encodes raw video frames to compressed video.
|
|
|
100
96
|
const encoder = new VideoEncoder({
|
|
101
97
|
output: (chunk, metadata) => {
|
|
102
98
|
// chunk is EncodedVideoChunk
|
|
103
|
-
// metadata contains
|
|
99
|
+
// metadata contains decoder config info
|
|
104
100
|
},
|
|
105
101
|
error: (e) => console.error(e),
|
|
106
102
|
});
|
|
107
103
|
|
|
108
|
-
|
|
104
|
+
encoder.configure({
|
|
109
105
|
codec: 'avc1.42001E', // H.264
|
|
110
106
|
width: 1920,
|
|
111
107
|
height: 1080,
|
|
@@ -114,7 +110,6 @@ await encoder.configure({
|
|
|
114
110
|
bitrateMode: 'variable', // Optional: 'constant', 'variable', or 'quantizer'
|
|
115
111
|
latencyMode: 'realtime', // Optional: 'realtime' for streaming, 'quality' for best compression
|
|
116
112
|
hardwareAcceleration: 'prefer-hardware', // Optional: use GPU encoding
|
|
117
|
-
format: 'mp4', // Optional: 'annexb' (default) or 'mp4'
|
|
118
113
|
});
|
|
119
114
|
|
|
120
115
|
// Create a frame from raw RGBA data
|
|
@@ -153,7 +148,7 @@ const decoder = new VideoDecoder({
|
|
|
153
148
|
error: (e) => console.error(e),
|
|
154
149
|
});
|
|
155
150
|
|
|
156
|
-
|
|
151
|
+
decoder.configure({
|
|
157
152
|
codec: 'avc1.42001E',
|
|
158
153
|
codedWidth: 1920,
|
|
159
154
|
codedHeight: 1080,
|
|
@@ -177,12 +172,11 @@ const encoder = new AudioEncoder({
|
|
|
177
172
|
error: (e) => console.error(e),
|
|
178
173
|
});
|
|
179
174
|
|
|
180
|
-
|
|
175
|
+
encoder.configure({
|
|
181
176
|
codec: 'opus',
|
|
182
177
|
sampleRate: 48000,
|
|
183
178
|
numberOfChannels: 2,
|
|
184
179
|
bitrate: 128000,
|
|
185
|
-
format: 'aac', // Optional: 'adts' (default for AAC) or 'aac'
|
|
186
180
|
});
|
|
187
181
|
|
|
188
182
|
// Create audio data from raw samples
|
|
@@ -221,8 +215,6 @@ const imageData = readFileSync('animation.gif');
|
|
|
221
215
|
const decoder = new ImageDecoder({
|
|
222
216
|
type: 'image/gif',
|
|
223
217
|
data: imageData,
|
|
224
|
-
// Optional: transfer ownership for zero-copy
|
|
225
|
-
// transfer: [imageData.buffer],
|
|
226
218
|
});
|
|
227
219
|
|
|
228
220
|
// Wait for parsing to complete
|
|
@@ -247,21 +239,6 @@ for (let i = 0; i < track.frameCount; i++) {
|
|
|
247
239
|
decoder.close();
|
|
248
240
|
```
|
|
249
241
|
|
|
250
|
-
**Constructor options:**
|
|
251
|
-
- `type` - MIME type (required)
|
|
252
|
-
- `data` - ArrayBuffer, TypedArray, or ReadableStream (required)
|
|
253
|
-
- `transfer` - ArrayBuffer[] for zero-copy ownership
|
|
254
|
-
- `colorSpaceConversion` - 'none' | 'default'
|
|
255
|
-
- `desiredWidth` / `desiredHeight` - Target dimensions
|
|
256
|
-
- `preferAnimation` - Prefer animated track if available
|
|
257
|
-
- `premultiplyAlpha` - 'none' | 'premultiply' | 'default'
|
|
258
|
-
|
|
259
|
-
**Properties:**
|
|
260
|
-
- `type` - MIME type string
|
|
261
|
-
- `complete` - Boolean, true when data is buffered
|
|
262
|
-
- `completed` - Promise that resolves when ready
|
|
263
|
-
- `tracks` - ImageTrackList with track information
|
|
264
|
-
|
|
265
242
|
**Supported formats:**
|
|
266
243
|
- `image/png`, `image/apng`
|
|
267
244
|
- `image/jpeg`
|
|
@@ -271,6 +248,36 @@ decoder.close();
|
|
|
271
248
|
- `image/bmp`
|
|
272
249
|
- `image/tiff`
|
|
273
250
|
|
|
251
|
+
### ImageEncoder
|
|
252
|
+
|
|
253
|
+
Encodes VideoFrames to image formats (PNG, JPEG, WebP). This is a utility class that mirrors ImageDecoder.
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import { ImageEncoder, VideoFrame } from 'webcodecs-node';
|
|
257
|
+
|
|
258
|
+
// Check format support
|
|
259
|
+
ImageEncoder.isTypeSupported('image/webp'); // true
|
|
260
|
+
|
|
261
|
+
// Encode a frame to JPEG
|
|
262
|
+
const result = await ImageEncoder.encode(frame, {
|
|
263
|
+
type: 'image/jpeg',
|
|
264
|
+
quality: 0.85,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
fs.writeFileSync('output.jpg', Buffer.from(result.data));
|
|
268
|
+
|
|
269
|
+
// Synchronous encoding
|
|
270
|
+
const pngResult = ImageEncoder.encodeSync(frame, { type: 'image/png' });
|
|
271
|
+
|
|
272
|
+
// Batch encode multiple frames
|
|
273
|
+
const results = await ImageEncoder.encodeBatch(frames, { type: 'image/webp' });
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Supported output formats:**
|
|
277
|
+
- `image/png` - Lossless, supports transparency
|
|
278
|
+
- `image/jpeg` - Lossy, quality 0-1 (default: 0.92)
|
|
279
|
+
- `image/webp` - Lossy/lossless, quality 0-1 (default: 0.8)
|
|
280
|
+
|
|
274
281
|
### MediaCapabilities API
|
|
275
282
|
|
|
276
283
|
Query codec capabilities before encoding/decoding. Implements the standard [MediaCapabilities API](https://developer.mozilla.org/en-US/docs/Web/API/MediaCapabilities).
|
|
@@ -317,35 +324,6 @@ if (encodeInfo.supported && encodeInfo.powerEfficient) {
|
|
|
317
324
|
}
|
|
318
325
|
```
|
|
319
326
|
|
|
320
|
-
**Supported containers & codecs:**
|
|
321
|
-
| Container | Video Codecs | Audio Codecs |
|
|
322
|
-
|-----------|-------------|--------------|
|
|
323
|
-
| video/mp4 | H.264, HEVC, AV1 | AAC |
|
|
324
|
-
| video/webm | VP8, VP9, AV1 | Opus, Vorbis |
|
|
325
|
-
| audio/mp4 | - | AAC |
|
|
326
|
-
| audio/webm | - | Opus, Vorbis |
|
|
327
|
-
| audio/ogg | - | Opus, Vorbis, FLAC |
|
|
328
|
-
| audio/mpeg | - | MP3 |
|
|
329
|
-
|
|
330
|
-
**Result properties:**
|
|
331
|
-
- `supported` - Whether the configuration can be decoded/encoded
|
|
332
|
-
- `smooth` - Whether playback/encoding will be smooth (no dropped frames)
|
|
333
|
-
- `powerEfficient` - Whether hardware acceleration is available
|
|
334
|
-
|
|
335
|
-
### MediaCapabilities Profiles
|
|
336
|
-
|
|
337
|
-
By default, capability queries use heuristics (resolution, bitrate, detected hardware). You can provide a detailed profile generated from the local FFmpeg installation:
|
|
338
|
-
|
|
339
|
-
```bash
|
|
340
|
-
# Generate a JSON profile alongside the repo (builds first)
|
|
341
|
-
npm run capabilities:generate -- ./webcodecs-capabilities.json
|
|
342
|
-
|
|
343
|
-
# Point WebCodecs at the profile
|
|
344
|
-
export WEBCODECS_CAPABILITIES_PROFILE=$(pwd)/webcodecs-capabilities.json
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
`decodingInfo` / `encodingInfo` will load that JSON (schema: `{ video: CapabilityProfileEntry[]; audio: CapabilityProfileEntry[] }`) and match codec/profile/level against those entries for precise limits. Without the env var the library falls back to its built-in heuristics.
|
|
348
|
-
|
|
349
327
|
### Hardware Acceleration
|
|
350
328
|
|
|
351
329
|
Detect and use hardware encoding/decoding:
|
|
@@ -372,7 +350,7 @@ const best = await getBestEncoder('h264', 'prefer-hardware');
|
|
|
372
350
|
console.log(`Using: ${best.encoder} (hardware: ${best.isHardware})`);
|
|
373
351
|
|
|
374
352
|
// Use in VideoEncoder config
|
|
375
|
-
|
|
353
|
+
encoder.configure({
|
|
376
354
|
codec: 'avc1.42001E',
|
|
377
355
|
width: 1920,
|
|
378
356
|
height: 1080,
|
|
@@ -385,23 +363,38 @@ await encoder.configure({
|
|
|
385
363
|
- **VAAPI** - Intel/AMD on Linux
|
|
386
364
|
- **NVENC/NVDEC** - NVIDIA GPUs
|
|
387
365
|
- **QSV** - Intel Quick Sync Video
|
|
388
|
-
- **VideoToolbox** - macOS
|
|
366
|
+
- **VideoToolbox** - macOS
|
|
389
367
|
|
|
390
|
-
###
|
|
368
|
+
### Container Utilities
|
|
391
369
|
|
|
392
|
-
|
|
370
|
+
Import container demuxing/muxing utilities for working with MP4 and WebM files:
|
|
393
371
|
|
|
394
372
|
```typescript
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
373
|
+
import { Mp4Demuxer, WebmMuxer } from 'webcodecs-node/containers';
|
|
374
|
+
|
|
375
|
+
// Demux an MP4 file
|
|
376
|
+
const demuxer = new Mp4Demuxer(mp4Data);
|
|
377
|
+
await demuxer.initialize();
|
|
378
|
+
|
|
379
|
+
for await (const sample of demuxer.videoSamples()) {
|
|
380
|
+
// sample contains encoded video chunks
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Mux encoded chunks to WebM
|
|
384
|
+
const muxer = new WebmMuxer({
|
|
385
|
+
video: { codec: 'vp9', width: 1920, height: 1080 },
|
|
402
386
|
});
|
|
403
387
|
|
|
404
|
-
|
|
388
|
+
muxer.addVideoChunk(encodedChunk, metadata);
|
|
389
|
+
const webmData = muxer.finalize();
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Streaming & Latency Modes
|
|
393
|
+
|
|
394
|
+
For real-time streaming applications, use `latencyMode: 'realtime'` to minimize encoding latency:
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
encoder.configure({
|
|
405
398
|
codec: 'avc1.42001E',
|
|
406
399
|
width: 1280,
|
|
407
400
|
height: 720,
|
|
@@ -409,40 +402,18 @@ await encoder.configure({
|
|
|
409
402
|
framerate: 30,
|
|
410
403
|
latencyMode: 'realtime', // Prioritize low latency
|
|
411
404
|
});
|
|
412
|
-
|
|
413
|
-
// Process frames as they arrive
|
|
414
|
-
camera.on('frame', (frameData) => {
|
|
415
|
-
const frame = new VideoFrame(frameData, {
|
|
416
|
-
format: 'RGBA',
|
|
417
|
-
codedWidth: 1280,
|
|
418
|
-
codedHeight: 720,
|
|
419
|
-
timestamp: Date.now() * 1000,
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
encoder.encode(frame);
|
|
423
|
-
frame.close();
|
|
424
|
-
});
|
|
425
405
|
```
|
|
426
406
|
|
|
427
407
|
**Latency mode options:**
|
|
428
408
|
- `'quality'` (default) - Best compression, higher latency (uses B-frames, lookahead)
|
|
429
409
|
- `'realtime'` - Minimum latency for live streaming (no B-frames, zero-delay)
|
|
430
410
|
|
|
431
|
-
**Codec-specific optimizations in realtime mode:**
|
|
432
|
-
| Codec | Quality Mode | Realtime Mode |
|
|
433
|
-
|-------|-------------|---------------|
|
|
434
|
-
| H.264 | B-frames, rc-lookahead | zerolatency tune, no B-frames |
|
|
435
|
-
| H.265 | B-frames, lookahead | zerolatency tune, no B-frames |
|
|
436
|
-
| VP8 | Default settings | deadline=realtime, cpu-used=8 |
|
|
437
|
-
| VP9 | row-mt, tile-columns | deadline=realtime, cpu-used=8 |
|
|
438
|
-
| AV1 | Default settings | usage=realtime, cpu-used=8 |
|
|
439
|
-
|
|
440
411
|
### Bitrate Modes
|
|
441
412
|
|
|
442
413
|
Control how bitrate is managed during encoding:
|
|
443
414
|
|
|
444
415
|
```typescript
|
|
445
|
-
|
|
416
|
+
encoder.configure({
|
|
446
417
|
codec: 'avc1.42001E',
|
|
447
418
|
width: 1920,
|
|
448
419
|
height: 1080,
|
|
@@ -451,7 +422,6 @@ await encoder.configure({
|
|
|
451
422
|
});
|
|
452
423
|
```
|
|
453
424
|
|
|
454
|
-
**Bitrate mode options:**
|
|
455
425
|
| Mode | Description | Use Case |
|
|
456
426
|
|------|-------------|----------|
|
|
457
427
|
| `'variable'` | VBR - varies bitrate for quality (default) | General purpose |
|
|
@@ -463,8 +433,7 @@ await encoder.configure({
|
|
|
463
433
|
Preserve transparency when encoding with VP9 or AV1:
|
|
464
434
|
|
|
465
435
|
```typescript
|
|
466
|
-
|
|
467
|
-
await encoder.configure({
|
|
436
|
+
encoder.configure({
|
|
468
437
|
codec: 'vp9',
|
|
469
438
|
width: 1920,
|
|
470
439
|
height: 1080,
|
|
@@ -482,72 +451,207 @@ const frame = new VideoFrame(rgbaWithAlpha, {
|
|
|
482
451
|
encoder.encode(frame);
|
|
483
452
|
```
|
|
484
453
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
454
|
+
### 10-bit Pixel Formats & HDR
|
|
455
|
+
|
|
456
|
+
Support for high bit-depth content and HDR metadata:
|
|
488
457
|
|
|
489
|
-
|
|
458
|
+
```typescript
|
|
459
|
+
import {
|
|
460
|
+
VideoFrame,
|
|
461
|
+
VideoColorSpace,
|
|
462
|
+
createHdr10MasteringMetadata,
|
|
463
|
+
createContentLightLevel,
|
|
464
|
+
is10BitFormat,
|
|
465
|
+
getBitDepth,
|
|
466
|
+
} from 'webcodecs-node';
|
|
467
|
+
|
|
468
|
+
// Create a 10-bit frame
|
|
469
|
+
const frame = new VideoFrame(yuv10bitData, {
|
|
470
|
+
format: 'I420P10', // 10-bit YUV 4:2:0
|
|
471
|
+
codedWidth: 3840,
|
|
472
|
+
codedHeight: 2160,
|
|
473
|
+
timestamp: 0,
|
|
474
|
+
colorSpace: new VideoColorSpace({
|
|
475
|
+
primaries: 'bt2020',
|
|
476
|
+
transfer: 'pq', // HDR10 PQ transfer
|
|
477
|
+
matrix: 'bt2020-ncl',
|
|
478
|
+
}),
|
|
479
|
+
});
|
|
490
480
|
|
|
491
|
-
|
|
481
|
+
// Check format properties
|
|
482
|
+
console.log(is10BitFormat('I420P10')); // true
|
|
483
|
+
console.log(getBitDepth('I420P10')); // 10
|
|
484
|
+
|
|
485
|
+
// HDR metadata for mastering display
|
|
486
|
+
const hdrMetadata = {
|
|
487
|
+
smpteSt2086: createHdr10MasteringMetadata(1000, 0.0001), // max/min luminance
|
|
488
|
+
contentLightLevel: createContentLightLevel(800, 400), // MaxCLL, MaxFALL
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const colorSpace = new VideoColorSpace({
|
|
492
|
+
primaries: 'bt2020',
|
|
493
|
+
transfer: 'pq',
|
|
494
|
+
hdrMetadata,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
console.log(colorSpace.isHdr); // true
|
|
498
|
+
console.log(colorSpace.hasHdrMetadata); // true
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**10-bit pixel formats:**
|
|
502
|
+
- `I420P10` - YUV 4:2:0 planar, 10-bit
|
|
503
|
+
- `I422P10` - YUV 4:2:2 planar, 10-bit
|
|
504
|
+
- `I444P10` - YUV 4:4:4 planar, 10-bit
|
|
505
|
+
- `P010` - YUV 4:2:0 semi-planar, 10-bit
|
|
506
|
+
|
|
507
|
+
**Pixel format utilities:**
|
|
508
|
+
- `is10BitFormat(format)` - Check if format is 10-bit
|
|
509
|
+
- `getBitDepth(format)` - Get bit depth (8 or 10)
|
|
510
|
+
- `get8BitEquivalent(format)` - Get 8-bit version of a 10-bit format
|
|
511
|
+
- `get10BitEquivalent(format)` - Get 10-bit version of an 8-bit format
|
|
512
|
+
|
|
513
|
+
### Canvas Rendering (skia-canvas)
|
|
514
|
+
|
|
515
|
+
GPU-accelerated 2D canvas rendering with automatic hardware detection:
|
|
492
516
|
|
|
493
517
|
```typescript
|
|
494
|
-
import {
|
|
495
|
-
|
|
518
|
+
import {
|
|
519
|
+
createCanvas,
|
|
520
|
+
createFrameLoop,
|
|
521
|
+
detectGpuAcceleration,
|
|
522
|
+
isGpuAvailable,
|
|
523
|
+
getGpuApi,
|
|
524
|
+
ensureEvenDimensions,
|
|
525
|
+
VideoEncoder,
|
|
526
|
+
} from 'webcodecs-node';
|
|
496
527
|
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
|
|
528
|
+
// Check GPU availability
|
|
529
|
+
const gpuInfo = detectGpuAcceleration();
|
|
530
|
+
console.log(`Renderer: ${gpuInfo.renderer}`); // 'GPU' or 'CPU'
|
|
531
|
+
console.log(`API: ${getGpuApi()}`); // 'Metal', 'Vulkan', 'D3D', or null
|
|
532
|
+
|
|
533
|
+
// Create GPU-accelerated canvas
|
|
534
|
+
const canvas = createCanvas({
|
|
535
|
+
width: 1920,
|
|
536
|
+
height: 1080,
|
|
537
|
+
gpu: true, // or omit for auto-detection
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const ctx = canvas.getContext('2d');
|
|
541
|
+
ctx.fillStyle = 'red';
|
|
542
|
+
ctx.fillRect(0, 0, 1920, 1080);
|
|
543
|
+
|
|
544
|
+
// Create VideoFrame directly from canvas
|
|
545
|
+
const frame = new VideoFrame(canvas, { timestamp: 0 });
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**FrameLoop helper** for animation with backpressure:
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
const loop = createFrameLoop({
|
|
552
|
+
width: 1920,
|
|
553
|
+
height: 1080,
|
|
554
|
+
frameRate: 30,
|
|
555
|
+
maxQueueSize: 8, // Backpressure limit
|
|
556
|
+
onFrame: (ctx, timing) => {
|
|
557
|
+
// Draw each frame
|
|
558
|
+
ctx.fillStyle = `hsl(${timing.frameIndex % 360}, 100%, 50%)`;
|
|
559
|
+
ctx.fillRect(0, 0, 1920, 1080);
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
loop.start(300); // Generate 300 frames
|
|
564
|
+
|
|
565
|
+
while (loop.getState() !== 'stopped' || loop.getQueueSize() > 0) {
|
|
566
|
+
const frame = loop.takeFrame();
|
|
567
|
+
if (frame) {
|
|
568
|
+
encoder.encode(frame);
|
|
569
|
+
frame.close(); // Always close frames!
|
|
570
|
+
}
|
|
503
571
|
}
|
|
504
|
-
|
|
505
|
-
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
**OffscreenCanvas polyfill** for browser-compatible code:
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
import { installOffscreenCanvasPolyfill } from 'webcodecs-node';
|
|
578
|
+
|
|
579
|
+
installOffscreenCanvasPolyfill();
|
|
580
|
+
|
|
581
|
+
// Now use standard OffscreenCanvas API
|
|
582
|
+
const canvas = new OffscreenCanvas(1920, 1080);
|
|
583
|
+
const ctx = canvas.getContext('2d');
|
|
584
|
+
const blob = await canvas.convertToBlob({ type: 'image/png' });
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
## Performance Tuning
|
|
588
|
+
|
|
589
|
+
### Memory Management
|
|
590
|
+
|
|
591
|
+
Always close VideoFrames and AudioData when done:
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
const frame = new VideoFrame(buffer, { ... });
|
|
595
|
+
try {
|
|
596
|
+
encoder.encode(frame);
|
|
597
|
+
} finally {
|
|
598
|
+
frame.close(); // Prevent memory leaks
|
|
506
599
|
}
|
|
600
|
+
```
|
|
507
601
|
|
|
508
|
-
|
|
509
|
-
installWebCodecsPolyfill();
|
|
602
|
+
### Even Dimensions
|
|
510
603
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
import { FFmpegVideoDecoder } from 'webcodecs-node/mediabunny/FFmpegVideoDecoder';
|
|
525
|
-
import { FFmpegAudioEncoder } from 'webcodecs-node/mediabunny/FFmpegAudioEncoder';
|
|
526
|
-
import { FFmpegAudioDecoder } from 'webcodecs-node/mediabunny/FFmpegAudioDecoder';
|
|
527
|
-
|
|
528
|
-
// Register FFmpeg-backed encoders/decoders
|
|
529
|
-
registerEncoder(FFmpegVideoEncoder);
|
|
530
|
-
registerEncoder(FFmpegAudioEncoder);
|
|
531
|
-
registerDecoder(FFmpegVideoDecoder);
|
|
532
|
-
registerDecoder(FFmpegAudioDecoder);
|
|
533
|
-
|
|
534
|
-
// Convert video
|
|
535
|
-
const input = new Input({
|
|
536
|
-
formats: ALL_FORMATS,
|
|
537
|
-
source: new FilePathSource('input.mkv'),
|
|
538
|
-
});
|
|
604
|
+
Video codecs require even dimensions for YUV420 chroma subsampling:
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
import { ensureEvenDimensions, validateEvenDimensions } from 'webcodecs-node';
|
|
608
|
+
|
|
609
|
+
// Auto-fix odd dimensions (rounds up)
|
|
610
|
+
const { width, height } = ensureEvenDimensions(1279, 719);
|
|
611
|
+
// Returns { width: 1280, height: 720 }
|
|
612
|
+
|
|
613
|
+
// Strict validation (throws if odd)
|
|
614
|
+
validateEvenDimensions(1280, 720); // OK
|
|
615
|
+
validateEvenDimensions(1279, 720); // Throws TypeError
|
|
616
|
+
```
|
|
539
617
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
618
|
+
### Backpressure Handling
|
|
619
|
+
|
|
620
|
+
Monitor encoder queue to prevent memory exhaustion:
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
encoder.addEventListener('dequeue', () => {
|
|
624
|
+
// Queue size decreased, safe to encode more
|
|
625
|
+
if (encoder.encodeQueueSize < 10) {
|
|
626
|
+
encodeNextFrame();
|
|
627
|
+
}
|
|
543
628
|
});
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### Raw Buffer Export
|
|
544
632
|
|
|
545
|
-
|
|
546
|
-
await conversion.execute();
|
|
633
|
+
For maximum performance, use raw RGBA buffers instead of PNG/JPEG:
|
|
547
634
|
|
|
548
|
-
|
|
635
|
+
```typescript
|
|
636
|
+
import { getRawPixels } from 'webcodecs-node';
|
|
637
|
+
|
|
638
|
+
// Fast: raw RGBA buffer (no compression)
|
|
639
|
+
const pixels = getRawPixels(canvas); // Returns Buffer
|
|
640
|
+
|
|
641
|
+
// Slow: PNG encoding (avoid in hot paths)
|
|
642
|
+
const png = await canvas.toBuffer('png');
|
|
549
643
|
```
|
|
550
644
|
|
|
645
|
+
### GPU vs CPU Tradeoffs
|
|
646
|
+
|
|
647
|
+
| Scenario | Recommendation |
|
|
648
|
+
|----------|----------------|
|
|
649
|
+
| HD/4K encoding | `hardwareAcceleration: 'prefer-hardware'` |
|
|
650
|
+
| Real-time streaming | Hardware + `latencyMode: 'realtime'` |
|
|
651
|
+
| Maximum quality | Software + `bitrateMode: 'quantizer'` |
|
|
652
|
+
| Batch processing | Hardware for throughput |
|
|
653
|
+
| Low-end systems | Software (more compatible) |
|
|
654
|
+
|
|
551
655
|
## Demos
|
|
552
656
|
|
|
553
657
|
Run the included demos to test functionality:
|
|
@@ -555,27 +659,78 @@ Run the included demos to test functionality:
|
|
|
555
659
|
```bash
|
|
556
660
|
npm run build
|
|
557
661
|
|
|
558
|
-
# Basic
|
|
662
|
+
# Basic demo
|
|
663
|
+
npm run demo
|
|
664
|
+
|
|
665
|
+
# WebCodecs API demo
|
|
559
666
|
npm run demo:webcodecs
|
|
560
667
|
|
|
561
668
|
# Image decoding demo (animated GIF/PNG/WebP with frame timing)
|
|
562
669
|
npm run demo:image
|
|
563
670
|
|
|
564
|
-
# Streaming demo (real-time encoding with latency comparison)
|
|
565
|
-
npm run demo:streaming
|
|
566
|
-
|
|
567
|
-
# File conversion with Mediabunny
|
|
568
|
-
npm run demo:conversion
|
|
569
|
-
|
|
570
671
|
# Hardware acceleration detection
|
|
571
672
|
npm run demo:hwaccel
|
|
572
673
|
|
|
573
|
-
#
|
|
574
|
-
npm run demo:
|
|
674
|
+
# Streaming demo (real-time encoding)
|
|
675
|
+
npm run demo:streaming
|
|
676
|
+
|
|
677
|
+
# Sample-based encoding demo
|
|
678
|
+
npm run demo:samples
|
|
679
|
+
|
|
680
|
+
# Container demuxing/muxing demo
|
|
681
|
+
npm run demo:containers
|
|
575
682
|
|
|
576
|
-
# Video quadrant compositor demo (
|
|
577
|
-
# Requires Node 20+; set hardwareAcceleration in src/demos/demo-four-corners.ts to 'prefer-hardware' if VAAPI/NVENC/QSV are available
|
|
683
|
+
# Video quadrant compositor demo (four-up render)
|
|
578
684
|
npm run demo:fourcorners
|
|
685
|
+
|
|
686
|
+
# 1080p transcoding demo
|
|
687
|
+
npm run demo:1080p
|
|
688
|
+
|
|
689
|
+
# DVD bouncing logo animation
|
|
690
|
+
npm run demo:dvd
|
|
691
|
+
|
|
692
|
+
# Audio visualizer with waveform and spectrum
|
|
693
|
+
npm run demo:visualizer
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
## Benchmarking
|
|
697
|
+
|
|
698
|
+
Compare software vs hardware encoding performance:
|
|
699
|
+
|
|
700
|
+
```bash
|
|
701
|
+
# Quick benchmark (30 frames, 360p)
|
|
702
|
+
npm run bench:quick
|
|
703
|
+
|
|
704
|
+
# Default benchmark (120 frames, 720p)
|
|
705
|
+
npm run bench
|
|
706
|
+
|
|
707
|
+
# Full benchmark (300 frames, 1080p)
|
|
708
|
+
npm run bench:full
|
|
709
|
+
|
|
710
|
+
# Custom options
|
|
711
|
+
node scripts/encoding-benchmark.mjs --frames 100 --resolution 1080p --codecs h264,hevc
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
**Options:**
|
|
715
|
+
- `--frames <n>` - Number of frames to encode (default: 120)
|
|
716
|
+
- `--resolution <res>` - 360p, 480p, 720p, 1080p, 4k (default: 720p)
|
|
717
|
+
- `--bitrate <bps>` - Target bitrate in bps
|
|
718
|
+
- `--framerate <fps>` - Target framerate (default: 30)
|
|
719
|
+
- `--codecs <list>` - Comma-separated: h264,hevc,vp9,av1
|
|
720
|
+
- `--skip-software` - Only test hardware encoding
|
|
721
|
+
- `--verbose` - Show detailed output
|
|
722
|
+
|
|
723
|
+
**Example output:**
|
|
724
|
+
```
|
|
725
|
+
════════════════════════════════════════════════════════════════════════════════
|
|
726
|
+
ENCODING BENCHMARK RESULTS (720p)
|
|
727
|
+
════════════════════════════════════════════════════════════════════════════════
|
|
728
|
+
Codec Mode FPS Time Latency Size Bitrate
|
|
729
|
+
────────────────────────────────────────────────────────────────────────────────
|
|
730
|
+
H.264/AVC SW 213.6 562ms 391ms 2.00 MB 4.20 Mbps
|
|
731
|
+
H.264/AVC HW 370.4 324ms 187ms 2.11 MB 4.43 Mbps
|
|
732
|
+
H.265/HEVC SW 141.4 848ms 106ms 1.94 MB 4.06 Mbps
|
|
733
|
+
H.265/HEVC HW 589.0 204ms 61ms 2.16 MB 4.54 Mbps
|
|
579
734
|
```
|
|
580
735
|
|
|
581
736
|
## API Compatibility
|
|
@@ -600,12 +755,16 @@ This implementation follows the [WebCodecs specification](https://www.w3.org/TR/
|
|
|
600
755
|
| bitrateMode | ✓ | ✓ |
|
|
601
756
|
| alpha (transparency) | ✓ | ✓ (VP9, AV1) |
|
|
602
757
|
| isConfigSupported() | ✓ | ✓ |
|
|
603
|
-
| isTypeSupported() | ✓ | ✓ |
|
|
604
758
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
-
|
|
759
|
+
## Architecture
|
|
760
|
+
|
|
761
|
+
This library uses **node-av** as its backend, which provides native bindings to FFmpeg's libav* libraries. This approach offers:
|
|
762
|
+
|
|
763
|
+
- **Native performance** - Direct library calls instead of subprocess spawning
|
|
764
|
+
- **Lower latency** - No IPC overhead between Node.js and FFmpeg
|
|
765
|
+
- **Better resource management** - Native memory handling and cleanup
|
|
766
|
+
- **Simplified deployment** - No need for separate FFmpeg installation
|
|
608
767
|
|
|
609
768
|
## License
|
|
610
769
|
|
|
611
|
-
webcodecs-node is distributed under the GNU Affero General Public License v3.0.
|
|
770
|
+
webcodecs-node is distributed under the GNU Affero General Public License v3.0. See `LICENSE` for full terms.
|
package/dist/backends/types.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface VideoEncoderBackendConfig {
|
|
|
40
40
|
latencyMode?: 'quality' | 'realtime';
|
|
41
41
|
alpha?: 'discard' | 'keep';
|
|
42
42
|
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
|
|
43
|
+
/** Output format: 'annexb' for raw Annex B, 'mp4' for length-prefixed (AVCC/HVCC) */
|
|
44
|
+
format?: 'annexb' | 'mp4';
|
|
43
45
|
}
|
|
44
46
|
/**
|
|
45
47
|
* Video decoder configuration
|