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/docs/api.md
CHANGED
|
@@ -9,7 +9,9 @@ This document provides detailed API documentation for webcodecs-node.
|
|
|
9
9
|
- [AudioEncoder](#audioencoder)
|
|
10
10
|
- [AudioDecoder](#audiodecoder)
|
|
11
11
|
- [ImageDecoder](#imagedecoder)
|
|
12
|
+
- [ImageEncoder](#imageencoder)
|
|
12
13
|
- [VideoFrame](#videoframe)
|
|
14
|
+
- [VideoColorSpace](#videocolorspace)
|
|
13
15
|
- [AudioData](#audiodata)
|
|
14
16
|
- [EncodedVideoChunk](#encodedvideochunk)
|
|
15
17
|
- [EncodedAudioChunk](#encodedaudiochunk)
|
|
@@ -64,6 +66,7 @@ Configure the encoder. Must be called before encoding.
|
|
|
64
66
|
| `alpha` | 'discard' \| 'keep' | No | Alpha channel handling |
|
|
65
67
|
| `latencyMode` | 'quality' \| 'realtime' | No | Latency vs quality tradeoff |
|
|
66
68
|
| `hardwareAcceleration` | 'no-preference' \| 'prefer-hardware' \| 'prefer-software' | No | Hardware acceleration preference |
|
|
69
|
+
| `maxQueueSize` | number | No | Max pending frames before QuotaExceededError. Auto-calculated from resolution if not set (~300MB target memory). |
|
|
67
70
|
|
|
68
71
|
#### `encode(frame: VideoFrame, options?: VideoEncoderEncodeOptions): void`
|
|
69
72
|
|
|
@@ -122,6 +125,7 @@ Check if a configuration is supported.
|
|
|
122
125
|
| `codedWidth` | number | No | Coded frame width |
|
|
123
126
|
| `codedHeight` | number | No | Coded frame height |
|
|
124
127
|
| `description` | BufferSource | No | Codec-specific data (e.g., SPS/PPS for H.264) |
|
|
128
|
+
| `maxQueueSize` | number | No | Max pending chunks before QuotaExceededError. Auto-calculated from resolution if dimensions provided (~300MB target memory). |
|
|
125
129
|
|
|
126
130
|
#### `decode(chunk: EncodedVideoChunk): void`
|
|
127
131
|
|
|
@@ -289,6 +293,63 @@ Close the decoder.
|
|
|
289
293
|
|
|
290
294
|
---
|
|
291
295
|
|
|
296
|
+
## ImageEncoder
|
|
297
|
+
|
|
298
|
+
Encodes VideoFrames to image formats (PNG, JPEG, WebP). This is a utility class (not part of WebCodecs spec) that mirrors ImageDecoder.
|
|
299
|
+
|
|
300
|
+
### Static Methods
|
|
301
|
+
|
|
302
|
+
#### `isTypeSupported(type: string): boolean`
|
|
303
|
+
|
|
304
|
+
Check if an output format is supported.
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
ImageEncoder.isTypeSupported('image/webp'); // true
|
|
308
|
+
ImageEncoder.isTypeSupported('image/gif'); // false
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### `encode(frame: VideoFrame, options?: ImageEncoderOptions): Promise<ImageEncoderResult>`
|
|
312
|
+
|
|
313
|
+
Encode a VideoFrame to an image format asynchronously.
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
const result = await ImageEncoder.encode(frame, {
|
|
317
|
+
type: 'image/jpeg',
|
|
318
|
+
quality: 0.85,
|
|
319
|
+
});
|
|
320
|
+
fs.writeFileSync('output.jpg', Buffer.from(result.data));
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
#### `encodeSync(frame: VideoFrame, options?: ImageEncoderOptions): ImageEncoderResult`
|
|
324
|
+
|
|
325
|
+
Encode a VideoFrame synchronously.
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
const result = ImageEncoder.encodeSync(frame, { type: 'image/png' });
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### `encodeBatch(frames: VideoFrame[], options?: ImageEncoderOptions): Promise<ImageEncoderResult[]>`
|
|
332
|
+
|
|
333
|
+
Encode multiple frames in parallel.
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
const results = await ImageEncoder.encodeBatch(frames, { type: 'image/webp' });
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Types
|
|
340
|
+
|
|
341
|
+
**ImageEncoderOptions:**
|
|
342
|
+
| Property | Type | Default | Description |
|
|
343
|
+
|----------|------|---------|-------------|
|
|
344
|
+
| `type` | `'image/png' \| 'image/jpeg' \| 'image/webp'` | `'image/png'` | Output format |
|
|
345
|
+
| `quality` | number | 0.92 (JPEG), 0.8 (WebP) | Quality for lossy formats (0-1) |
|
|
346
|
+
|
|
347
|
+
**ImageEncoderResult:**
|
|
348
|
+
- `data: ArrayBuffer` - Encoded image data
|
|
349
|
+
- `type: string` - MIME type of the encoded image
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
292
353
|
## VideoFrame
|
|
293
354
|
|
|
294
355
|
Represents a single video frame with raw pixel data.
|
|
@@ -311,6 +372,8 @@ new VideoFrame(data: BufferSource, init: VideoFrameBufferInit)
|
|
|
311
372
|
| `displayHeight` | number | No | Display height |
|
|
312
373
|
|
|
313
374
|
**Supported VideoPixelFormat values:**
|
|
375
|
+
|
|
376
|
+
*8-bit formats:*
|
|
314
377
|
- `'I420'` - YUV 4:2:0 planar
|
|
315
378
|
- `'I420A'` - YUV 4:2:0 planar with alpha
|
|
316
379
|
- `'I422'` - YUV 4:2:2 planar
|
|
@@ -321,6 +384,12 @@ new VideoFrame(data: BufferSource, init: VideoFrameBufferInit)
|
|
|
321
384
|
- `'BGRA'` - 8-bit BGRA
|
|
322
385
|
- `'BGRX'` - 8-bit BGR (alpha ignored)
|
|
323
386
|
|
|
387
|
+
*10-bit formats (HDR):*
|
|
388
|
+
- `'I420P10'` - YUV 4:2:0 planar, 10-bit (16-bit container)
|
|
389
|
+
- `'I422P10'` - YUV 4:2:2 planar, 10-bit (16-bit container)
|
|
390
|
+
- `'I444P10'` - YUV 4:4:4 planar, 10-bit (16-bit container)
|
|
391
|
+
- `'P010'` - YUV 4:2:0 semi-planar, 10-bit (16-bit container)
|
|
392
|
+
|
|
324
393
|
### Instance Methods
|
|
325
394
|
|
|
326
395
|
#### `allocationSize(options?: VideoFrameCopyToOptions): number`
|
|
@@ -348,6 +417,92 @@ Release frame resources. Always call this when done.
|
|
|
348
417
|
- `displayHeight: number`
|
|
349
418
|
- `timestamp: number`
|
|
350
419
|
- `duration: number | null`
|
|
420
|
+
- `colorSpace: VideoColorSpace` - Color space information
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## VideoColorSpace
|
|
425
|
+
|
|
426
|
+
Describes the color space of a video frame, including HDR metadata.
|
|
427
|
+
|
|
428
|
+
### Constructor
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
new VideoColorSpace(init?: VideoColorSpaceInit)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**VideoColorSpaceInit:**
|
|
435
|
+
| Property | Type | Default | Description |
|
|
436
|
+
|----------|------|---------|-------------|
|
|
437
|
+
| `primaries` | string | null | Color primaries ('bt709', 'bt2020', 'smpte432') |
|
|
438
|
+
| `transfer` | string | null | Transfer function ('bt709', 'pq', 'hlg', 'srgb') |
|
|
439
|
+
| `matrix` | string | null | Matrix coefficients ('bt709', 'bt2020-ncl') |
|
|
440
|
+
| `fullRange` | boolean | null | Full vs limited range |
|
|
441
|
+
| `hdrMetadata` | HdrMetadata | null | HDR mastering metadata |
|
|
442
|
+
|
|
443
|
+
### Properties
|
|
444
|
+
|
|
445
|
+
- `primaries: string | null` - Color primaries
|
|
446
|
+
- `transfer: string | null` - Transfer function
|
|
447
|
+
- `matrix: string | null` - Matrix coefficients
|
|
448
|
+
- `fullRange: boolean | null` - Full range flag
|
|
449
|
+
- `hdrMetadata: HdrMetadata | null` - HDR metadata (if present)
|
|
450
|
+
- `isHdr: boolean` - True if PQ or HLG transfer
|
|
451
|
+
- `hasHdrMetadata: boolean` - True if HDR metadata is set
|
|
452
|
+
|
|
453
|
+
### Instance Methods
|
|
454
|
+
|
|
455
|
+
#### `toJSON(): VideoColorSpaceInit`
|
|
456
|
+
|
|
457
|
+
Serialize to JSON-compatible object.
|
|
458
|
+
|
|
459
|
+
### HDR Metadata Types
|
|
460
|
+
|
|
461
|
+
**HdrMetadata:**
|
|
462
|
+
```typescript
|
|
463
|
+
interface HdrMetadata {
|
|
464
|
+
smpteSt2086?: SmpteSt2086Metadata; // Mastering display metadata
|
|
465
|
+
contentLightLevel?: ContentLightLevelInfo; // Content light levels
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
**SmpteSt2086Metadata (Mastering Display):**
|
|
470
|
+
| Property | Type | Description |
|
|
471
|
+
|----------|------|-------------|
|
|
472
|
+
| `primaryRChromaticityX/Y` | number | Red primary chromaticity |
|
|
473
|
+
| `primaryGChromaticityX/Y` | number | Green primary chromaticity |
|
|
474
|
+
| `primaryBChromaticityX/Y` | number | Blue primary chromaticity |
|
|
475
|
+
| `whitePointChromaticityX/Y` | number | White point chromaticity |
|
|
476
|
+
| `maxLuminance` | number | Maximum luminance (nits) |
|
|
477
|
+
| `minLuminance` | number | Minimum luminance (nits) |
|
|
478
|
+
|
|
479
|
+
**ContentLightLevelInfo:**
|
|
480
|
+
| Property | Type | Description |
|
|
481
|
+
|----------|------|-------------|
|
|
482
|
+
| `maxCLL` | number | Maximum Content Light Level (nits) |
|
|
483
|
+
| `maxFALL` | number | Maximum Frame Average Light Level (nits) |
|
|
484
|
+
|
|
485
|
+
### Helper Functions
|
|
486
|
+
|
|
487
|
+
#### `createHdr10MasteringMetadata(maxLuminance, minLuminance?): SmpteSt2086Metadata`
|
|
488
|
+
|
|
489
|
+
Create HDR10 mastering metadata with BT.2020 primaries.
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
const metadata = createHdr10MasteringMetadata(1000, 0.0001);
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
#### `createContentLightLevel(maxCLL, maxFALL): ContentLightLevelInfo`
|
|
496
|
+
|
|
497
|
+
Create content light level info.
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
const cll = createContentLightLevel(800, 400);
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
#### `HDR10_DISPLAY_PRIMARIES`
|
|
504
|
+
|
|
505
|
+
Constant with standard BT.2020 display primaries.
|
|
351
506
|
|
|
352
507
|
---
|
|
353
508
|
|
|
@@ -496,7 +651,7 @@ const info = await mediaCapabilities.encodingInfo({
|
|
|
496
651
|
|
|
497
652
|
### Capability Profiles
|
|
498
653
|
|
|
499
|
-
You can generate a hardware-specific capability profile to make `mediaCapabilities` match your actual
|
|
654
|
+
You can generate a hardware-specific capability profile to make `mediaCapabilities` match your actual system capabilities. Use the provided CLI:
|
|
500
655
|
|
|
501
656
|
```bash
|
|
502
657
|
npm run capabilities:generate -- ./webcodecs-capabilities.json
|
package/docs/configuration.md
CHANGED
|
@@ -5,6 +5,7 @@ This guide covers all configuration options for video and audio encoding in webc
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
7
7
|
- [Bitrate Mode](#bitrate-mode)
|
|
8
|
+
- [Non-standard Quality Overrides](#non-standard-quality-overrides)
|
|
8
9
|
- [Alpha Channel Handling](#alpha-channel-handling)
|
|
9
10
|
- [Latency Mode](#latency-mode)
|
|
10
11
|
- [Hardware Acceleration](#hardware-acceleration)
|
|
@@ -93,16 +94,31 @@ encoder.configure({
|
|
|
93
94
|
- Unpredictable file size
|
|
94
95
|
- Not suitable for streaming
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Non-standard Quality Overrides
|
|
100
|
+
|
|
101
|
+
`crf` and `preset` are **extensions** (not part of the WebCodecs spec). They are loaded globally from a JS file and passed to FFmpeg when supported.
|
|
97
102
|
|
|
98
|
-
|
|
103
|
+
Edit `ffmpeg-quality.js` in the project root:
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
export default {
|
|
107
|
+
// Global overrides:
|
|
108
|
+
// crf: 28,
|
|
109
|
+
// preset: 'veryfast',
|
|
110
|
+
|
|
111
|
+
// Per-codec overrides:
|
|
112
|
+
// perCodec: {
|
|
113
|
+
// h264: { crf: 30, preset: 'veryfast' },
|
|
114
|
+
// hevc: { crf: 28, preset: 'medium' },
|
|
115
|
+
// },
|
|
116
|
+
};
|
|
117
|
+
```
|
|
99
118
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
| H.265 | `-b:v` + `maxrate`/`bufsize`, CBR mode | `-b:v` only | `-crf 28` |
|
|
104
|
-
| VP8/VP9 | `-b:v` + `minrate`/`maxrate` | `-b:v` only | `-crf 31` + `-b:v 0` |
|
|
105
|
-
| AV1 | `-b:v` + `maxrate`/`bufsize` | `-b:v` only | `-crf 30` |
|
|
119
|
+
**Notes:**
|
|
120
|
+
- If the file is missing, no overrides are applied.
|
|
121
|
+
- The file is loaded from `process.cwd()` or from `WEB_CODECS_FFMPEG_QUALITY`.
|
|
106
122
|
|
|
107
123
|
---
|
|
108
124
|
|
|
@@ -131,7 +147,7 @@ The `alpha` option controls how transparent pixels are handled during encoding.
|
|
|
131
147
|
|
|
132
148
|
## Output Bitstream Format
|
|
133
149
|
|
|
134
|
-
By default the encoders emit Annex B (video) and ADTS/OGG (audio)
|
|
150
|
+
By default the encoders emit Annex B (video) and ADTS/OGG (audio) bitstreams. If you need MP4-style payloads (length-prefixed NAL units, raw AAC frames) you can opt-in via the `format` config field.
|
|
135
151
|
|
|
136
152
|
### VideoEncoder `format`
|
|
137
153
|
|
|
@@ -266,11 +282,11 @@ encoder.configure({
|
|
|
266
282
|
|
|
267
283
|
| Codec | Quality Mode | Realtime Mode |
|
|
268
284
|
|-------|--------------|---------------|
|
|
269
|
-
| H.264 |
|
|
270
|
-
| H.265 | Default settings |
|
|
271
|
-
| VP8 | Default |
|
|
272
|
-
| VP9 | Row multithreading, tile columns |
|
|
273
|
-
| AV1 | Default |
|
|
285
|
+
| H.264 | B-frames, lookahead enabled | Zero-latency tuning, no B-frames |
|
|
286
|
+
| H.265 | Default settings | Zero-latency tuning, no B-frames |
|
|
287
|
+
| VP8 | Default | Realtime deadline, fast encoding |
|
|
288
|
+
| VP9 | Row multithreading, tile columns | Realtime deadline, fast encoding |
|
|
289
|
+
| AV1 | Default | Realtime usage, fast encoding |
|
|
274
290
|
|
|
275
291
|
**Example for live streaming:**
|
|
276
292
|
|
package/examples/README.md
CHANGED
|
@@ -22,6 +22,8 @@ npx tsx examples/transparent-video.ts
|
|
|
22
22
|
npx tsx examples/streaming.ts
|
|
23
23
|
npx tsx examples/hardware-encoding.ts
|
|
24
24
|
npx tsx examples/hardware-decoding.ts
|
|
25
|
+
npx tsx examples/canvas-encoding.ts
|
|
26
|
+
npx tsx examples/offscreen-canvas.ts
|
|
25
27
|
```
|
|
26
28
|
|
|
27
29
|
## Examples
|
|
@@ -87,6 +89,27 @@ GPU-accelerated decoding. Demonstrates:
|
|
|
87
89
|
- Benchmarking hardware vs software decoding
|
|
88
90
|
- Real-time decoding capability analysis
|
|
89
91
|
|
|
92
|
+
### canvas-encoding.ts
|
|
93
|
+
|
|
94
|
+
GPU-accelerated canvas rendering with skia-canvas. Demonstrates:
|
|
95
|
+
- GPU acceleration detection (Metal/Vulkan/D3D)
|
|
96
|
+
- Creating GPU-accelerated canvas with `createCanvas()`
|
|
97
|
+
- Using `FrameLoop` with backpressure for smooth encoding
|
|
98
|
+
- Animated canvas drawing with gradients and shapes
|
|
99
|
+
- Proper memory lifecycle (frame closing)
|
|
100
|
+
- Real-time encoding speed measurement
|
|
101
|
+
|
|
102
|
+
### offscreen-canvas.ts
|
|
103
|
+
|
|
104
|
+
Browser-compatible OffscreenCanvas API. Demonstrates:
|
|
105
|
+
- Using `OffscreenCanvasPolyfill` (matches browser API)
|
|
106
|
+
- Installing polyfill globally with `installOffscreenCanvasPolyfill()`
|
|
107
|
+
- `ImageDataPolyfill` with `Uint8ClampedArray`
|
|
108
|
+
- `createPixelBuffer()` utilities for pixel manipulation
|
|
109
|
+
- `convertToBlob()` for PNG/JPEG/WebP export
|
|
110
|
+
- `validateEvenDimensions()` for YUV420 compatibility
|
|
111
|
+
- Direct `VideoFrame` creation from OffscreenCanvas
|
|
112
|
+
|
|
90
113
|
## Additional Demos
|
|
91
114
|
|
|
92
115
|
The `demos/` folder contains more complete demos that can be run via npm scripts:
|
|
@@ -95,6 +118,10 @@ The `demos/` folder contains more complete demos that can be run via npm scripts
|
|
|
95
118
|
npm run demo:webcodecs # Basic WebCodecs demo
|
|
96
119
|
npm run demo:image # Image decoding demo
|
|
97
120
|
npm run demo:streaming # Streaming comparison
|
|
98
|
-
npm run demo:conversion # File conversion with Mediabunny
|
|
99
121
|
npm run demo:hwaccel # Hardware acceleration detection
|
|
122
|
+
npm run demo:fourcorners # Video quadrant compositor
|
|
123
|
+
npm run demo:containers # Container demuxing/muxing
|
|
124
|
+
npm run demo:1080p # 1080p transcoding benchmark
|
|
125
|
+
npm run demo:dvd # Bouncing DVD logo animation
|
|
126
|
+
npm run demo:visualizer # Audio visualizer with waveform
|
|
100
127
|
```
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Encoding Example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates GPU-accelerated canvas rendering with skia-canvas
|
|
5
|
+
* and encoding to H.264 video using the FrameLoop helper.
|
|
6
|
+
*
|
|
7
|
+
* Features shown:
|
|
8
|
+
* - GPU acceleration detection (Metal/Vulkan/D3D)
|
|
9
|
+
* - Creating GPU-accelerated canvas
|
|
10
|
+
* - FrameLoop with backpressure for smooth encoding
|
|
11
|
+
* - Animated canvas drawing
|
|
12
|
+
* - Proper memory lifecycle (frame closing)
|
|
13
|
+
*
|
|
14
|
+
* Run: npx tsx examples/canvas-encoding.ts
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
VideoEncoder,
|
|
19
|
+
EncodedVideoChunk,
|
|
20
|
+
createCanvas,
|
|
21
|
+
createFrameLoop,
|
|
22
|
+
detectGpuAcceleration,
|
|
23
|
+
isGpuAvailable,
|
|
24
|
+
getGpuApi,
|
|
25
|
+
ensureEvenDimensions,
|
|
26
|
+
} from '../src/index.js';
|
|
27
|
+
|
|
28
|
+
async function main() {
|
|
29
|
+
// ============================================
|
|
30
|
+
// 1. Detect GPU Acceleration
|
|
31
|
+
// ============================================
|
|
32
|
+
console.log('=== GPU Detection ===\n');
|
|
33
|
+
|
|
34
|
+
const gpuInfo = detectGpuAcceleration();
|
|
35
|
+
console.log(`Renderer: ${gpuInfo.renderer}`);
|
|
36
|
+
console.log(`GPU Available: ${isGpuAvailable()}`);
|
|
37
|
+
|
|
38
|
+
const gpuApi = getGpuApi();
|
|
39
|
+
if (gpuApi) {
|
|
40
|
+
console.log(`GPU API: ${gpuApi}`);
|
|
41
|
+
if (gpuInfo.device) {
|
|
42
|
+
console.log(`Device: ${gpuInfo.device}`);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
console.log('GPU API: None (CPU fallback)');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ============================================
|
|
49
|
+
// 2. Setup Video Parameters
|
|
50
|
+
// ============================================
|
|
51
|
+
console.log('\n=== Video Setup ===\n');
|
|
52
|
+
|
|
53
|
+
// Ensure even dimensions for YUV420 compatibility
|
|
54
|
+
const { width, height } = ensureEvenDimensions(1280, 720);
|
|
55
|
+
const frameRate = 30;
|
|
56
|
+
const totalFrames = 90; // 3 seconds
|
|
57
|
+
const maxQueueSize = 8; // Backpressure limit
|
|
58
|
+
|
|
59
|
+
console.log(`Resolution: ${width}x${height}`);
|
|
60
|
+
console.log(`Frame Rate: ${frameRate} fps`);
|
|
61
|
+
console.log(`Total Frames: ${totalFrames}`);
|
|
62
|
+
console.log(`Duration: ${(totalFrames / frameRate).toFixed(1)}s`);
|
|
63
|
+
|
|
64
|
+
// ============================================
|
|
65
|
+
// 3. Create Encoder
|
|
66
|
+
// ============================================
|
|
67
|
+
const chunks: EncodedVideoChunk[] = [];
|
|
68
|
+
let encodedCount = 0;
|
|
69
|
+
|
|
70
|
+
const encoder = new VideoEncoder({
|
|
71
|
+
output: (chunk, metadata) => {
|
|
72
|
+
chunks.push(chunk);
|
|
73
|
+
encodedCount++;
|
|
74
|
+
|
|
75
|
+
if (metadata?.decoderConfig) {
|
|
76
|
+
console.log('\nDecoder config received');
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
error: (err) => {
|
|
80
|
+
console.error('Encoding error:', err);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
encoder.configure({
|
|
85
|
+
codec: 'avc1.42001f', // H.264 Baseline Level 3.1
|
|
86
|
+
width,
|
|
87
|
+
height,
|
|
88
|
+
bitrate: 5_000_000, // 5 Mbps
|
|
89
|
+
framerate: frameRate,
|
|
90
|
+
bitrateMode: 'variable',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ============================================
|
|
94
|
+
// 4. Create FrameLoop with Canvas Drawing
|
|
95
|
+
// ============================================
|
|
96
|
+
console.log('\n=== Encoding ===\n');
|
|
97
|
+
|
|
98
|
+
const startTime = Date.now();
|
|
99
|
+
|
|
100
|
+
const loop = createFrameLoop({
|
|
101
|
+
width,
|
|
102
|
+
height,
|
|
103
|
+
frameRate,
|
|
104
|
+
maxQueueSize,
|
|
105
|
+
gpu: isGpuAvailable(), // Use GPU if available
|
|
106
|
+
|
|
107
|
+
// This callback draws each frame
|
|
108
|
+
onFrame: (ctx, timing) => {
|
|
109
|
+
const { frameIndex, timestamp } = timing;
|
|
110
|
+
const t = frameIndex / totalFrames; // Progress 0-1
|
|
111
|
+
|
|
112
|
+
// Background gradient that shifts over time
|
|
113
|
+
const gradient = ctx.createLinearGradient(0, 0, width, height);
|
|
114
|
+
gradient.addColorStop(0, `hsl(${(frameIndex * 3) % 360}, 70%, 20%)`);
|
|
115
|
+
gradient.addColorStop(1, `hsl(${(frameIndex * 3 + 180) % 360}, 70%, 40%)`);
|
|
116
|
+
ctx.fillStyle = gradient;
|
|
117
|
+
ctx.fillRect(0, 0, width, height);
|
|
118
|
+
|
|
119
|
+
// Animated circles
|
|
120
|
+
const numCircles = 5;
|
|
121
|
+
for (let i = 0; i < numCircles; i++) {
|
|
122
|
+
const phase = (i / numCircles) * Math.PI * 2;
|
|
123
|
+
const x = width / 2 + Math.cos(frameIndex * 0.05 + phase) * 200;
|
|
124
|
+
const y = height / 2 + Math.sin(frameIndex * 0.07 + phase) * 150;
|
|
125
|
+
const radius = 30 + Math.sin(frameIndex * 0.1 + i) * 20;
|
|
126
|
+
|
|
127
|
+
ctx.beginPath();
|
|
128
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
129
|
+
ctx.fillStyle = `hsla(${(i * 72 + frameIndex * 2) % 360}, 80%, 60%, 0.7)`;
|
|
130
|
+
ctx.fill();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Central pulsing circle
|
|
134
|
+
const pulseRadius = 80 + Math.sin(frameIndex * 0.15) * 30;
|
|
135
|
+
ctx.beginPath();
|
|
136
|
+
ctx.arc(width / 2, height / 2, pulseRadius, 0, Math.PI * 2);
|
|
137
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
|
138
|
+
ctx.fill();
|
|
139
|
+
|
|
140
|
+
// Frame counter text
|
|
141
|
+
ctx.fillStyle = 'white';
|
|
142
|
+
ctx.font = 'bold 24px sans-serif';
|
|
143
|
+
ctx.textAlign = 'left';
|
|
144
|
+
ctx.textBaseline = 'top';
|
|
145
|
+
ctx.fillText(`Frame: ${frameIndex + 1}/${totalFrames}`, 20, 20);
|
|
146
|
+
|
|
147
|
+
// Timestamp
|
|
148
|
+
ctx.font = '18px sans-serif';
|
|
149
|
+
ctx.fillText(`Time: ${(timestamp / 1_000_000).toFixed(3)}s`, 20, 50);
|
|
150
|
+
|
|
151
|
+
// GPU status
|
|
152
|
+
ctx.textAlign = 'right';
|
|
153
|
+
ctx.fillText(`GPU: ${gpuApi || 'CPU'}`, width - 20, 20);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ============================================
|
|
158
|
+
// 5. Generate and Encode Frames
|
|
159
|
+
// ============================================
|
|
160
|
+
|
|
161
|
+
// Start frame generation
|
|
162
|
+
loop.start(totalFrames);
|
|
163
|
+
|
|
164
|
+
// Process frames as they become available
|
|
165
|
+
let processedCount = 0;
|
|
166
|
+
|
|
167
|
+
while (loop.getState() !== 'stopped' || loop.getQueueSize() > 0) {
|
|
168
|
+
const frame = loop.takeFrame();
|
|
169
|
+
|
|
170
|
+
if (frame) {
|
|
171
|
+
try {
|
|
172
|
+
// Request keyframe every second
|
|
173
|
+
const keyFrame = processedCount % frameRate === 0;
|
|
174
|
+
encoder.encode(frame, { keyFrame });
|
|
175
|
+
processedCount++;
|
|
176
|
+
|
|
177
|
+
// Progress indicator
|
|
178
|
+
if (processedCount % 10 === 0 || processedCount === totalFrames) {
|
|
179
|
+
process.stdout.write(`\rProcessed: ${processedCount}/${totalFrames} frames`);
|
|
180
|
+
}
|
|
181
|
+
} finally {
|
|
182
|
+
// Always close the frame to free memory
|
|
183
|
+
frame.close();
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
// No frame available, wait briefly
|
|
187
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log('\n');
|
|
192
|
+
|
|
193
|
+
// ============================================
|
|
194
|
+
// 6. Flush and Close
|
|
195
|
+
// ============================================
|
|
196
|
+
await encoder.flush();
|
|
197
|
+
encoder.close();
|
|
198
|
+
|
|
199
|
+
const endTime = Date.now();
|
|
200
|
+
const elapsed = (endTime - startTime) / 1000;
|
|
201
|
+
|
|
202
|
+
// ============================================
|
|
203
|
+
// 7. Results
|
|
204
|
+
// ============================================
|
|
205
|
+
console.log('=== Results ===\n');
|
|
206
|
+
|
|
207
|
+
const totalBytes = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
208
|
+
const keyFrames = chunks.filter((c) => c.type === 'key').length;
|
|
209
|
+
const deltaFrames = chunks.filter((c) => c.type === 'delta').length;
|
|
210
|
+
const actualBitrate = (totalBytes * 8) / (totalFrames / frameRate);
|
|
211
|
+
|
|
212
|
+
console.log(`Total chunks: ${chunks.length}`);
|
|
213
|
+
console.log(`Key frames: ${keyFrames}`);
|
|
214
|
+
console.log(`Delta frames: ${deltaFrames}`);
|
|
215
|
+
console.log(`Total size: ${(totalBytes / 1024).toFixed(2)} KB`);
|
|
216
|
+
console.log(`Actual bitrate: ${(actualBitrate / 1000).toFixed(0)} kbps`);
|
|
217
|
+
console.log(`Encoding time: ${elapsed.toFixed(2)}s`);
|
|
218
|
+
console.log(`Speed: ${(totalFrames / elapsed).toFixed(1)} fps`);
|
|
219
|
+
console.log(`Realtime: ${((totalFrames / frameRate) / elapsed).toFixed(2)}x`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
main().catch(console.error);
|