webcodecs-node 0.5.2 → 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 +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 +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 +155 -0
  140. package/docs/configuration.md +27 -0
  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 +9 -3
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
 
@@ -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);