webcodecs-node 0.5.2 → 0.7.1
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 +279 -0
- 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 +58 -48
- 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 +56 -13
- 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 +58 -7
- 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 +94 -1
- package/dist/formats/color-space.d.ts.map +1 -1
- package/dist/formats/color-space.js +51 -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/codec-validation.d.ts +33 -0
- package/dist/utils/codec-validation.d.ts.map +1 -0
- package/dist/utils/codec-validation.js +247 -0
- package/dist/utils/codec-validation.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 +155 -0
- package/docs/configuration.md +27 -0
- package/examples/README.md +28 -1
- package/examples/canvas-encoding.ts +222 -0
- package/examples/offscreen-canvas.ts +230 -0
- package/package.json +9 -3
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)
|
|
@@ -95,6 +96,32 @@ encoder.configure({
|
|
|
95
96
|
|
|
96
97
|
---
|
|
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.
|
|
102
|
+
|
|
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
|
+
```
|
|
118
|
+
|
|
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`.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
98
125
|
## Alpha Channel Handling
|
|
99
126
|
|
|
100
127
|
The `alpha` option controls how transparent pixels are handled during encoding.
|
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);
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OffscreenCanvas Example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates using the OffscreenCanvas polyfill for browser-compatible
|
|
5
|
+
* canvas code in Node.js. This makes porting browser code trivial.
|
|
6
|
+
*
|
|
7
|
+
* Features shown:
|
|
8
|
+
* - OffscreenCanvasPolyfill (browser API compatible)
|
|
9
|
+
* - ImageDataPolyfill with Uint8ClampedArray
|
|
10
|
+
* - createPixelBuffer utilities
|
|
11
|
+
* - convertToBlob for image export
|
|
12
|
+
* - VideoFrame integration
|
|
13
|
+
*
|
|
14
|
+
* Run: npx tsx examples/offscreen-canvas.ts
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
VideoEncoder,
|
|
19
|
+
VideoFrame,
|
|
20
|
+
EncodedVideoChunk,
|
|
21
|
+
} from '../src/index.js';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
OffscreenCanvasPolyfill,
|
|
25
|
+
ImageDataPolyfill,
|
|
26
|
+
installOffscreenCanvasPolyfill,
|
|
27
|
+
} from '../src/polyfills/OffscreenCanvas.js';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
createPixelBuffer,
|
|
31
|
+
createPixelBufferWithColor,
|
|
32
|
+
ensureEvenDimensions,
|
|
33
|
+
validateEvenDimensions,
|
|
34
|
+
} from '../src/canvas/index.js';
|
|
35
|
+
|
|
36
|
+
import { writeFileSync } from 'fs';
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
console.log('=== OffscreenCanvas Polyfill Demo ===\n');
|
|
40
|
+
|
|
41
|
+
// ============================================
|
|
42
|
+
// 1. Install Polyfill Globally (Optional)
|
|
43
|
+
// ============================================
|
|
44
|
+
// This makes OffscreenCanvas available globally like in browsers
|
|
45
|
+
installOffscreenCanvasPolyfill();
|
|
46
|
+
console.log('Polyfill installed globally');
|
|
47
|
+
console.log(` globalThis.OffscreenCanvas: ${typeof (globalThis as any).OffscreenCanvas}`);
|
|
48
|
+
console.log(` globalThis.ImageData: ${typeof (globalThis as any).ImageData}`);
|
|
49
|
+
console.log(` globalThis.VideoFrame: ${typeof (globalThis as any).VideoFrame}`);
|
|
50
|
+
|
|
51
|
+
// ============================================
|
|
52
|
+
// 2. Create OffscreenCanvas (Browser-Style)
|
|
53
|
+
// ============================================
|
|
54
|
+
console.log('\n=== Creating OffscreenCanvas ===\n');
|
|
55
|
+
|
|
56
|
+
const { width, height } = ensureEvenDimensions(640, 480);
|
|
57
|
+
const canvas = new OffscreenCanvasPolyfill(width, height);
|
|
58
|
+
|
|
59
|
+
console.log(`Canvas created: ${canvas.width}x${canvas.height}`);
|
|
60
|
+
console.log(`GPU enabled: ${canvas.gpu}`);
|
|
61
|
+
|
|
62
|
+
// Get 2D context (just like in browser)
|
|
63
|
+
const ctx = canvas.getContext('2d');
|
|
64
|
+
if (!ctx) {
|
|
65
|
+
throw new Error('Failed to get 2D context');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================
|
|
69
|
+
// 3. Draw on Canvas
|
|
70
|
+
// ============================================
|
|
71
|
+
console.log('\n=== Drawing ===\n');
|
|
72
|
+
|
|
73
|
+
// Background
|
|
74
|
+
ctx.fillStyle = '#1a1a2e';
|
|
75
|
+
ctx.fillRect(0, 0, width, height);
|
|
76
|
+
|
|
77
|
+
// Gradient rectangle
|
|
78
|
+
const gradient = ctx.createLinearGradient(50, 50, 250, 250);
|
|
79
|
+
gradient.addColorStop(0, '#e94560');
|
|
80
|
+
gradient.addColorStop(1, '#0f3460');
|
|
81
|
+
ctx.fillStyle = gradient;
|
|
82
|
+
ctx.fillRect(50, 50, 200, 200);
|
|
83
|
+
|
|
84
|
+
// Circle
|
|
85
|
+
ctx.beginPath();
|
|
86
|
+
ctx.arc(450, 200, 100, 0, Math.PI * 2);
|
|
87
|
+
ctx.fillStyle = '#16213e';
|
|
88
|
+
ctx.fill();
|
|
89
|
+
ctx.strokeStyle = '#e94560';
|
|
90
|
+
ctx.lineWidth = 4;
|
|
91
|
+
ctx.stroke();
|
|
92
|
+
|
|
93
|
+
// Text
|
|
94
|
+
ctx.fillStyle = 'white';
|
|
95
|
+
ctx.font = 'bold 32px sans-serif';
|
|
96
|
+
ctx.textAlign = 'center';
|
|
97
|
+
ctx.fillText('OffscreenCanvas', width / 2, height - 80);
|
|
98
|
+
|
|
99
|
+
ctx.font = '20px sans-serif';
|
|
100
|
+
ctx.fillStyle = '#888';
|
|
101
|
+
ctx.fillText('Node.js + skia-canvas', width / 2, height - 50);
|
|
102
|
+
|
|
103
|
+
console.log('Drawing complete');
|
|
104
|
+
|
|
105
|
+
// ============================================
|
|
106
|
+
// 4. Export to Blob (PNG, JPEG, WebP)
|
|
107
|
+
// ============================================
|
|
108
|
+
console.log('\n=== Export to Blob ===\n');
|
|
109
|
+
|
|
110
|
+
// PNG export
|
|
111
|
+
const pngBlob = await canvas.convertToBlob({ type: 'image/png' });
|
|
112
|
+
console.log(`PNG: ${pngBlob.size} bytes, type: ${pngBlob.type}`);
|
|
113
|
+
|
|
114
|
+
// JPEG export with quality
|
|
115
|
+
const jpegBlob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.8 });
|
|
116
|
+
console.log(`JPEG: ${jpegBlob.size} bytes, type: ${jpegBlob.type}`);
|
|
117
|
+
|
|
118
|
+
// WebP export
|
|
119
|
+
const webpBlob = await canvas.convertToBlob({ type: 'image/webp', quality: 0.85 });
|
|
120
|
+
console.log(`WebP: ${webpBlob.size} bytes, type: ${webpBlob.type}`);
|
|
121
|
+
|
|
122
|
+
// ============================================
|
|
123
|
+
// 5. ImageData Operations
|
|
124
|
+
// ============================================
|
|
125
|
+
console.log('\n=== ImageData Operations ===\n');
|
|
126
|
+
|
|
127
|
+
// Create empty ImageData
|
|
128
|
+
const imageData = new ImageDataPolyfill(100, 100);
|
|
129
|
+
console.log(`Empty ImageData: ${imageData.width}x${imageData.height}`);
|
|
130
|
+
console.log(`Data type: ${imageData.data.constructor.name}`);
|
|
131
|
+
console.log(`Data length: ${imageData.data.length} bytes`);
|
|
132
|
+
console.log(`Color type: ${imageData.colorType} (for skia-canvas)`);
|
|
133
|
+
|
|
134
|
+
// Fill with gradient using direct pixel manipulation
|
|
135
|
+
for (let y = 0; y < imageData.height; y++) {
|
|
136
|
+
for (let x = 0; x < imageData.width; x++) {
|
|
137
|
+
const idx = (y * imageData.width + x) * 4;
|
|
138
|
+
imageData.data[idx] = (x * 255) / imageData.width; // R
|
|
139
|
+
imageData.data[idx + 1] = (y * 255) / imageData.height; // G
|
|
140
|
+
imageData.data[idx + 2] = 128; // B
|
|
141
|
+
imageData.data[idx + 3] = 255; // A
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Draw ImageData to canvas
|
|
146
|
+
ctx.putImageData(imageData as any, 270, 280);
|
|
147
|
+
console.log('ImageData drawn to canvas');
|
|
148
|
+
|
|
149
|
+
// ============================================
|
|
150
|
+
// 6. Pixel Buffer Utilities
|
|
151
|
+
// ============================================
|
|
152
|
+
console.log('\n=== Pixel Buffer Utilities ===\n');
|
|
153
|
+
|
|
154
|
+
// Create empty buffer
|
|
155
|
+
const emptyBuffer = createPixelBuffer(64, 64);
|
|
156
|
+
console.log(`createPixelBuffer(64, 64): ${emptyBuffer.length} bytes`);
|
|
157
|
+
console.log(`Type: ${emptyBuffer.constructor.name}`);
|
|
158
|
+
|
|
159
|
+
// Create solid color buffer
|
|
160
|
+
const redBuffer = createPixelBufferWithColor(64, 64, 255, 0, 0);
|
|
161
|
+
console.log(`createPixelBufferWithColor (red): first pixel = [${redBuffer[0]}, ${redBuffer[1]}, ${redBuffer[2]}, ${redBuffer[3]}]`);
|
|
162
|
+
|
|
163
|
+
// Demonstrate Uint8ClampedArray auto-clamping
|
|
164
|
+
const testBuffer = createPixelBuffer(1, 1);
|
|
165
|
+
testBuffer[0] = 300; // Will be clamped to 255
|
|
166
|
+
testBuffer[1] = -50; // Will be clamped to 0
|
|
167
|
+
console.log(`Auto-clamping: 300 -> ${testBuffer[0]}, -50 -> ${testBuffer[1]}`);
|
|
168
|
+
|
|
169
|
+
// ============================================
|
|
170
|
+
// 7. Dimension Validation
|
|
171
|
+
// ============================================
|
|
172
|
+
console.log('\n=== Dimension Validation ===\n');
|
|
173
|
+
|
|
174
|
+
// ensureEvenDimensions rounds up
|
|
175
|
+
const dims = ensureEvenDimensions(1279, 719);
|
|
176
|
+
console.log(`ensureEvenDimensions(1279, 719) = ${dims.width}x${dims.height}`);
|
|
177
|
+
|
|
178
|
+
// validateEvenDimensions throws for odd dimensions
|
|
179
|
+
try {
|
|
180
|
+
validateEvenDimensions(1279, 720);
|
|
181
|
+
} catch (err: any) {
|
|
182
|
+
console.log(`validateEvenDimensions(1279, 720) threw: "${err.message.split('.')[0]}..."`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ============================================
|
|
186
|
+
// 8. VideoFrame Integration
|
|
187
|
+
// ============================================
|
|
188
|
+
console.log('\n=== VideoFrame Integration ===\n');
|
|
189
|
+
|
|
190
|
+
// Create VideoFrame directly from OffscreenCanvas
|
|
191
|
+
const frame = new VideoFrame(canvas as any, {
|
|
192
|
+
timestamp: 0,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
console.log(`VideoFrame from OffscreenCanvas:`);
|
|
196
|
+
console.log(` Size: ${frame.displayWidth}x${frame.displayHeight}`);
|
|
197
|
+
console.log(` Format: ${frame.format}`);
|
|
198
|
+
console.log(` Timestamp: ${frame.timestamp}`);
|
|
199
|
+
|
|
200
|
+
// Use with encoder
|
|
201
|
+
const chunks: EncodedVideoChunk[] = [];
|
|
202
|
+
const encoder = new VideoEncoder({
|
|
203
|
+
output: (chunk) => chunks.push(chunk),
|
|
204
|
+
error: console.error,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
encoder.configure({
|
|
208
|
+
codec: 'avc1.42001f',
|
|
209
|
+
width,
|
|
210
|
+
height,
|
|
211
|
+
bitrate: 2_000_000,
|
|
212
|
+
framerate: 30,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
encoder.encode(frame, { keyFrame: true });
|
|
216
|
+
frame.close();
|
|
217
|
+
|
|
218
|
+
await encoder.flush();
|
|
219
|
+
encoder.close();
|
|
220
|
+
|
|
221
|
+
console.log(`Encoded to ${chunks.length} chunk(s), ${chunks[0].byteLength} bytes`);
|
|
222
|
+
|
|
223
|
+
// ============================================
|
|
224
|
+
// 9. Save Sample Output
|
|
225
|
+
// ============================================
|
|
226
|
+
console.log('\n=== Done ===\n');
|
|
227
|
+
console.log('OffscreenCanvas polyfill provides full browser API compatibility!');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
main().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcodecs-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "WebCodecs API implementation for Node.js using node-av",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -38,7 +38,12 @@
|
|
|
38
38
|
"demo:samples": "node dist/demos/demo-samples.js",
|
|
39
39
|
"demo:fourcorners": "node dist/demos/demo-four-corners.js",
|
|
40
40
|
"demo:containers": "node dist/demos/demo-containers.js",
|
|
41
|
-
"demo:1080p": "node dist/demos/demo-1080p-transcode.js"
|
|
41
|
+
"demo:1080p": "node dist/demos/demo-1080p-transcode.js",
|
|
42
|
+
"demo:dvd": "node dist/demos/demo-dvd-logo.js",
|
|
43
|
+
"demo:visualizer": "node dist/demos/demo-audio-visualizer.js",
|
|
44
|
+
"bench": "node scripts/encoding-benchmark.mjs",
|
|
45
|
+
"bench:quick": "node scripts/encoding-benchmark.mjs --frames 30 --resolution 360p",
|
|
46
|
+
"bench:full": "node scripts/encoding-benchmark.mjs --frames 300 --resolution 1080p"
|
|
42
47
|
},
|
|
43
48
|
"keywords": [
|
|
44
49
|
"webcodecs",
|
|
@@ -73,7 +78,8 @@
|
|
|
73
78
|
"homepage": "",
|
|
74
79
|
"dependencies": {
|
|
75
80
|
"node-av": "^5.0.2",
|
|
76
|
-
"
|
|
81
|
+
"node-webpmux": "^3.2.1",
|
|
82
|
+
"skia-canvas": "^2.0.2"
|
|
77
83
|
},
|
|
78
84
|
"devDependencies": {
|
|
79
85
|
"@types/jest": "^30.0.0",
|