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.
Files changed (144) hide show
  1. package/README.md +328 -169
  2. package/dist/backends/types.d.ts +2 -0
  3. package/dist/backends/types.d.ts.map +1 -1
  4. package/dist/backends/types.js.map +1 -1
  5. package/dist/canvas/canvas-utils.d.ts +115 -0
  6. package/dist/canvas/canvas-utils.d.ts.map +1 -0
  7. package/dist/canvas/canvas-utils.js +169 -0
  8. package/dist/canvas/canvas-utils.js.map +1 -0
  9. package/dist/canvas/frame-loop.d.ts +113 -0
  10. package/dist/canvas/frame-loop.d.ts.map +1 -0
  11. package/dist/canvas/frame-loop.js +291 -0
  12. package/dist/canvas/frame-loop.js.map +1 -0
  13. package/dist/canvas/gpu-context.d.ts +61 -0
  14. package/dist/canvas/gpu-context.d.ts.map +1 -0
  15. package/dist/canvas/gpu-context.js +134 -0
  16. package/dist/canvas/gpu-context.js.map +1 -0
  17. package/dist/canvas/index.d.ts +22 -0
  18. package/dist/canvas/index.d.ts.map +1 -0
  19. package/dist/canvas/index.js +25 -0
  20. package/dist/canvas/index.js.map +1 -0
  21. package/dist/canvas/types.d.ts +101 -0
  22. package/dist/canvas/types.d.ts.map +1 -0
  23. package/dist/canvas/types.js +7 -0
  24. package/dist/canvas/types.js.map +1 -0
  25. package/dist/codec-utils/formats.d.ts.map +1 -1
  26. package/dist/codec-utils/formats.js +31 -0
  27. package/dist/codec-utils/formats.js.map +1 -1
  28. package/dist/config/ffmpeg-quality.d.ts +14 -0
  29. package/dist/config/ffmpeg-quality.d.ts.map +1 -0
  30. package/dist/config/ffmpeg-quality.js +41 -0
  31. package/dist/config/ffmpeg-quality.js.map +1 -0
  32. package/dist/containers/Muxer.d.ts.map +1 -1
  33. package/dist/containers/Muxer.js +4 -1
  34. package/dist/containers/Muxer.js.map +1 -1
  35. package/dist/core/AudioData.d.ts +2 -1
  36. package/dist/core/AudioData.d.ts.map +1 -1
  37. package/dist/core/AudioData.js.map +1 -1
  38. package/dist/core/VideoFrame.d.ts +2 -2
  39. package/dist/core/VideoFrame.d.ts.map +1 -1
  40. package/dist/core/VideoFrame.js +49 -47
  41. package/dist/core/VideoFrame.js.map +1 -1
  42. package/dist/decoders/AudioDecoder.d.ts +1 -0
  43. package/dist/decoders/AudioDecoder.d.ts.map +1 -1
  44. package/dist/decoders/AudioDecoder.js +39 -17
  45. package/dist/decoders/AudioDecoder.js.map +1 -1
  46. package/dist/decoders/ImageDecoder.d.ts +1 -0
  47. package/dist/decoders/ImageDecoder.d.ts.map +1 -1
  48. package/dist/decoders/ImageDecoder.js +40 -3
  49. package/dist/decoders/ImageDecoder.js.map +1 -1
  50. package/dist/decoders/VideoDecoder.d.ts +12 -0
  51. package/dist/decoders/VideoDecoder.d.ts.map +1 -1
  52. package/dist/decoders/VideoDecoder.js +50 -8
  53. package/dist/decoders/VideoDecoder.js.map +1 -1
  54. package/dist/demos/demo-1080p-transcode.js +8 -2
  55. package/dist/demos/demo-1080p-transcode.js.map +1 -1
  56. package/dist/demos/demo-audio-visualizer.d.ts +11 -0
  57. package/dist/demos/demo-audio-visualizer.d.ts.map +1 -0
  58. package/dist/demos/demo-audio-visualizer.js +281 -0
  59. package/dist/demos/demo-audio-visualizer.js.map +1 -0
  60. package/dist/demos/demo-dvd-logo.d.ts +8 -0
  61. package/dist/demos/demo-dvd-logo.d.ts.map +1 -0
  62. package/dist/demos/demo-dvd-logo.js +196 -0
  63. package/dist/demos/demo-dvd-logo.js.map +1 -0
  64. package/dist/demos/demo-four-corners.js +9 -0
  65. package/dist/demos/demo-four-corners.js.map +1 -1
  66. package/dist/demos/demo-streaming.js +6 -0
  67. package/dist/demos/demo-streaming.js.map +1 -1
  68. package/dist/demos/demo-webcodecs.js +1 -0
  69. package/dist/demos/demo-webcodecs.js.map +1 -1
  70. package/dist/encoders/AudioEncoder.d.ts +3 -0
  71. package/dist/encoders/AudioEncoder.d.ts.map +1 -1
  72. package/dist/encoders/AudioEncoder.js +70 -28
  73. package/dist/encoders/AudioEncoder.js.map +1 -1
  74. package/dist/encoders/ImageEncoder.d.ts +80 -0
  75. package/dist/encoders/ImageEncoder.d.ts.map +1 -0
  76. package/dist/encoders/ImageEncoder.js +156 -0
  77. package/dist/encoders/ImageEncoder.js.map +1 -0
  78. package/dist/encoders/VideoEncoder.d.ts +11 -0
  79. package/dist/encoders/VideoEncoder.d.ts.map +1 -1
  80. package/dist/encoders/VideoEncoder.js +46 -4
  81. package/dist/encoders/VideoEncoder.js.map +1 -1
  82. package/dist/encoders/index.d.ts +1 -0
  83. package/dist/encoders/index.d.ts.map +1 -1
  84. package/dist/encoders/index.js +1 -0
  85. package/dist/encoders/index.js.map +1 -1
  86. package/dist/formats/color-space.d.ts +88 -0
  87. package/dist/formats/color-space.d.ts.map +1 -1
  88. package/dist/formats/color-space.js +55 -1
  89. package/dist/formats/color-space.js.map +1 -1
  90. package/dist/formats/pixel-formats.d.ts +17 -1
  91. package/dist/formats/pixel-formats.d.ts.map +1 -1
  92. package/dist/formats/pixel-formats.js +74 -4
  93. package/dist/formats/pixel-formats.js.map +1 -1
  94. package/dist/hardware/detection.d.ts.map +1 -1
  95. package/dist/hardware/detection.js +5 -2
  96. package/dist/hardware/detection.js.map +1 -1
  97. package/dist/hardware/encoder-args.d.ts.map +1 -1
  98. package/dist/hardware/encoder-args.js +6 -6
  99. package/dist/hardware/encoder-args.js.map +1 -1
  100. package/dist/index.d.ts +11 -3
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +8 -2
  103. package/dist/index.js.map +1 -1
  104. package/dist/node-av/NodeAvAudioDecoder.d.ts.map +1 -1
  105. package/dist/node-av/NodeAvAudioDecoder.js +3 -0
  106. package/dist/node-av/NodeAvAudioDecoder.js.map +1 -1
  107. package/dist/node-av/NodeAvAudioEncoder.d.ts +5 -0
  108. package/dist/node-av/NodeAvAudioEncoder.d.ts.map +1 -1
  109. package/dist/node-av/NodeAvAudioEncoder.js +102 -9
  110. package/dist/node-av/NodeAvAudioEncoder.js.map +1 -1
  111. package/dist/node-av/NodeAvVideoEncoder.d.ts +2 -0
  112. package/dist/node-av/NodeAvVideoEncoder.d.ts.map +1 -1
  113. package/dist/node-av/NodeAvVideoEncoder.js +44 -6
  114. package/dist/node-av/NodeAvVideoEncoder.js.map +1 -1
  115. package/dist/node-av/WebPImageDecoder.d.ts +54 -0
  116. package/dist/node-av/WebPImageDecoder.d.ts.map +1 -0
  117. package/dist/node-av/WebPImageDecoder.js +176 -0
  118. package/dist/node-av/WebPImageDecoder.js.map +1 -0
  119. package/dist/polyfills/OffscreenCanvas.d.ts +141 -7
  120. package/dist/polyfills/OffscreenCanvas.d.ts.map +1 -1
  121. package/dist/polyfills/OffscreenCanvas.js +217 -17
  122. package/dist/polyfills/OffscreenCanvas.js.map +1 -1
  123. package/dist/types/index.d.ts +1 -0
  124. package/dist/types/index.d.ts.map +1 -1
  125. package/dist/types/index.js +2 -0
  126. package/dist/types/index.js.map +1 -1
  127. package/dist/types/native-frame.d.ts +74 -0
  128. package/dist/types/native-frame.d.ts.map +1 -0
  129. package/dist/types/native-frame.js +32 -0
  130. package/dist/types/native-frame.js.map +1 -0
  131. package/dist/utils/index.d.ts +1 -1
  132. package/dist/utils/index.d.ts.map +1 -1
  133. package/dist/utils/index.js +1 -1
  134. package/dist/utils/index.js.map +1 -1
  135. package/dist/utils/type-guards.d.ts +29 -1
  136. package/dist/utils/type-guards.d.ts.map +1 -1
  137. package/dist/utils/type-guards.js +47 -2
  138. package/dist/utils/type-guards.js.map +1 -1
  139. package/docs/api.md +156 -1
  140. package/docs/configuration.md +30 -14
  141. package/examples/README.md +28 -1
  142. package/examples/canvas-encoding.ts +222 -0
  143. package/examples/offscreen-canvas.ts +230 -0
  144. 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 FFmpeg.
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
- - **Mediabunny Integration** - Custom encoders/decoders for file conversion
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
- - FFmpeg with encoding libraries (libx264, libx265, libvpx, etc.)
33
+ - The `node-av` package (automatically installed as a dependency)
32
34
 
33
35
  ```bash
34
- # Ubuntu/Debian
35
- sudo apt install ffmpeg
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
- await encoder.configure({
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 timing info
99
+ // metadata contains decoder config info
104
100
  },
105
101
  error: (e) => console.error(e),
106
102
  });
107
103
 
108
- await encoder.configure({
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
- await decoder.configure({
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
- await encoder.configure({
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
- await encoder.configure({
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 (planned)
366
+ - **VideoToolbox** - macOS
389
367
 
390
- ### Streaming & Latency Modes
368
+ ### Container Utilities
391
369
 
392
- For real-time streaming applications, use `latencyMode: 'realtime'` to minimize encoding latency:
370
+ Import container demuxing/muxing utilities for working with MP4 and WebM files:
393
371
 
394
372
  ```typescript
395
- // Real-time streaming encoder
396
- const encoder = new VideoEncoder({
397
- output: (chunk) => {
398
- // Send chunk immediately over network
399
- streamToClient(chunk);
400
- },
401
- error: console.error,
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
- await encoder.configure({
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
- await encoder.configure({
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
- // Encode video with alpha channel
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
- **Alpha options:**
486
- - `'discard'` (default) - Strip alpha channel (works with all codecs)
487
- - `'keep'` - Preserve transparency (VP9 and AV1 only)
454
+ ### 10-bit Pixel Formats & HDR
455
+
456
+ Support for high bit-depth content and HDR metadata:
488
457
 
489
- ## Mediabunny Integration
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
- For file-to-file conversion, use with [Mediabunny](https://mediabunny.dev):
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 { ReadableStream, WritableStream, TransformStream } from 'stream/web';
495
- import { installWebCodecsPolyfill } from 'webcodecs-node';
518
+ import {
519
+ createCanvas,
520
+ createFrameLoop,
521
+ detectGpuAcceleration,
522
+ isGpuAvailable,
523
+ getGpuApi,
524
+ ensureEvenDimensions,
525
+ VideoEncoder,
526
+ } from 'webcodecs-node';
496
527
 
497
- // Polyfill Web Streams
498
- if (typeof globalThis.WritableStream === 'undefined') {
499
- globalThis.WritableStream = WritableStream;
500
- }
501
- if (typeof globalThis.ReadableStream === 'undefined') {
502
- globalThis.ReadableStream = ReadableStream;
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
- if (typeof globalThis.TransformStream === 'undefined') {
505
- globalThis.TransformStream = TransformStream;
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
- // Install WebCodecs
509
- installWebCodecsPolyfill();
602
+ ### Even Dimensions
510
603
 
511
- import {
512
- Input,
513
- Output,
514
- Conversion,
515
- FilePathSource,
516
- FilePathTarget,
517
- Mp4OutputFormat,
518
- ALL_FORMATS,
519
- registerEncoder,
520
- registerDecoder,
521
- } from 'mediabunny';
522
-
523
- import { FFmpegVideoEncoder } from 'webcodecs-node/mediabunny/FFmpegVideoEncoder';
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
- const output = new Output({
541
- format: new Mp4OutputFormat(),
542
- target: new FilePathTarget('output.mp4'),
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
- const conversion = await Conversion.init({ input, output });
546
- await conversion.execute();
633
+ For maximum performance, use raw RGBA buffers instead of PNG/JPEG:
547
634
 
548
- console.log('Conversion complete!');
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 WebCodecs demo
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
- # Hardware vs software encoding comparison
574
- npm run demo:hwaccel-conversion
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 (WebGPU four-up render → MP4)
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
- **Notes:**
606
- - Hardware acceleration defaults to software encoding for reliability. Use `hardwareAcceleration: 'prefer-hardware'` to enable GPU acceleration.
607
- - ImageDecoder supports animated image frame timing (duration, timestamp) and loop count (repetitionCount).
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. Files located under `src/mediabunny/` remain available under the MIT License to preserve compatibility with Mediabunny integrations. See `LICENSE` for full terms.
770
+ webcodecs-node is distributed under the GNU Affero General Public License v3.0. See `LICENSE` for full terms.
@@ -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