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.
Files changed (148) hide show
  1. package/README.md +279 -0
  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 +58 -48
  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 +56 -13
  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 +58 -7
  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 +94 -1
  87. package/dist/formats/color-space.d.ts.map +1 -1
  88. package/dist/formats/color-space.js +51 -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/codec-validation.d.ts +33 -0
  132. package/dist/utils/codec-validation.d.ts.map +1 -0
  133. package/dist/utils/codec-validation.js +247 -0
  134. package/dist/utils/codec-validation.js.map +1 -0
  135. package/dist/utils/index.d.ts +1 -1
  136. package/dist/utils/index.d.ts.map +1 -1
  137. package/dist/utils/index.js +1 -1
  138. package/dist/utils/index.js.map +1 -1
  139. package/dist/utils/type-guards.d.ts +29 -1
  140. package/dist/utils/type-guards.d.ts.map +1 -1
  141. package/dist/utils/type-guards.js +47 -2
  142. package/dist/utils/type-guards.js.map +1 -1
  143. package/docs/api.md +155 -0
  144. package/docs/configuration.md +27 -0
  145. package/examples/README.md +28 -1
  146. package/examples/canvas-encoding.ts +222 -0
  147. package/examples/offscreen-canvas.ts +230 -0
  148. package/package.json +9 -3
@@ -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.
@@ -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.5.2",
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
- "sharp": "^0.32.6"
81
+ "node-webpmux": "^3.2.1",
82
+ "skia-canvas": "^2.0.2"
77
83
  },
78
84
  "devDependencies": {
79
85
  "@types/jest": "^30.0.0",