@pproenca/node-webcodecs 0.1.0 → 0.1.1-alpha.5

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 (103) hide show
  1. package/README.md +78 -206
  2. package/binding.gyp +123 -0
  3. package/dist/audio-decoder.js +1 -2
  4. package/dist/audio-encoder.d.ts +4 -0
  5. package/dist/audio-encoder.js +28 -2
  6. package/dist/binding.d.ts +0 -2
  7. package/dist/binding.js +43 -124
  8. package/dist/control-message-queue.js +0 -1
  9. package/dist/demuxer.d.ts +7 -0
  10. package/dist/demuxer.js +9 -0
  11. package/dist/encoded-chunks.d.ts +16 -0
  12. package/dist/encoded-chunks.js +82 -2
  13. package/dist/image-decoder.js +4 -0
  14. package/dist/index.d.ts +17 -3
  15. package/dist/index.js +9 -4
  16. package/dist/is.d.ts +18 -0
  17. package/dist/is.js +14 -0
  18. package/dist/native-types.d.ts +20 -0
  19. package/dist/platform.d.ts +1 -10
  20. package/dist/platform.js +1 -39
  21. package/dist/resource-manager.d.ts +1 -2
  22. package/dist/resource-manager.js +3 -17
  23. package/dist/types.d.ts +46 -0
  24. package/dist/video-decoder.d.ts +21 -0
  25. package/dist/video-decoder.js +74 -2
  26. package/dist/video-encoder.d.ts +22 -0
  27. package/dist/video-encoder.js +83 -8
  28. package/dist/video-frame.d.ts +6 -3
  29. package/dist/video-frame.js +36 -4
  30. package/lib/audio-decoder.ts +1 -2
  31. package/lib/audio-encoder.ts +31 -2
  32. package/lib/binding.ts +45 -104
  33. package/lib/control-message-queue.ts +0 -1
  34. package/lib/demuxer.ts +10 -0
  35. package/lib/encoded-chunks.ts +90 -2
  36. package/lib/image-decoder.ts +5 -0
  37. package/lib/index.ts +9 -3
  38. package/lib/is.ts +32 -0
  39. package/lib/native-types.ts +22 -0
  40. package/lib/platform.ts +1 -41
  41. package/lib/resource-manager.ts +3 -19
  42. package/lib/types.ts +52 -1
  43. package/lib/video-decoder.ts +84 -2
  44. package/lib/video-encoder.ts +90 -8
  45. package/lib/video-frame.ts +52 -7
  46. package/package.json +49 -32
  47. package/src/addon.cc +57 -0
  48. package/src/async_decode_worker.cc +243 -36
  49. package/src/async_decode_worker.h +55 -4
  50. package/src/async_encode_worker.cc +155 -44
  51. package/src/async_encode_worker.h +38 -12
  52. package/src/audio_data.cc +38 -15
  53. package/src/audio_data.h +1 -0
  54. package/src/audio_decoder.cc +24 -3
  55. package/src/audio_encoder.cc +55 -4
  56. package/src/common.cc +125 -17
  57. package/src/common.h +34 -4
  58. package/src/demuxer.cc +16 -2
  59. package/src/encoded_audio_chunk.cc +10 -0
  60. package/src/encoded_audio_chunk.h +2 -0
  61. package/src/encoded_video_chunk.h +1 -0
  62. package/src/error_builder.cc +0 -4
  63. package/src/image_decoder.cc +127 -90
  64. package/src/image_decoder.h +11 -4
  65. package/src/muxer.cc +1 -0
  66. package/src/test_video_generator.cc +3 -2
  67. package/src/video_decoder.cc +169 -19
  68. package/src/video_decoder.h +9 -11
  69. package/src/video_encoder.cc +428 -35
  70. package/src/video_encoder.h +16 -0
  71. package/src/video_filter.cc +22 -11
  72. package/src/video_frame.cc +160 -5
  73. package/src/warnings.cc +0 -4
  74. package/dist/audio-data.js.map +0 -1
  75. package/dist/audio-decoder.js.map +0 -1
  76. package/dist/audio-encoder.js.map +0 -1
  77. package/dist/binding.js.map +0 -1
  78. package/dist/codec-base.js.map +0 -1
  79. package/dist/control-message-queue.js.map +0 -1
  80. package/dist/demuxer.js.map +0 -1
  81. package/dist/encoded-chunks.js.map +0 -1
  82. package/dist/errors.js.map +0 -1
  83. package/dist/ffmpeg.d.ts +0 -21
  84. package/dist/ffmpeg.js +0 -112
  85. package/dist/image-decoder.js.map +0 -1
  86. package/dist/image-track-list.js.map +0 -1
  87. package/dist/image-track.js.map +0 -1
  88. package/dist/index.js.map +0 -1
  89. package/dist/is.js.map +0 -1
  90. package/dist/muxer.js.map +0 -1
  91. package/dist/native-types.js.map +0 -1
  92. package/dist/platform.js.map +0 -1
  93. package/dist/resource-manager.js.map +0 -1
  94. package/dist/test-video-generator.js.map +0 -1
  95. package/dist/transfer.js.map +0 -1
  96. package/dist/types.js.map +0 -1
  97. package/dist/video-decoder.js.map +0 -1
  98. package/dist/video-encoder.js.map +0 -1
  99. package/dist/video-filter.js.map +0 -1
  100. package/dist/video-frame.js.map +0 -1
  101. package/install/build.js +0 -51
  102. package/install/check.js +0 -192
  103. package/lib/ffmpeg.ts +0 -78
@@ -3,8 +3,11 @@
3
3
 
4
4
  #include "src/video_encoder.h"
5
5
 
6
+ #include <chrono>
7
+ #include <cstdio>
6
8
  #include <memory>
7
9
  #include <string>
10
+ #include <thread>
8
11
  #include <utility>
9
12
 
10
13
  #include "src/common.h"
@@ -14,6 +17,24 @@ namespace {
14
17
 
15
18
  // Encoder configuration constants.
16
19
  constexpr int kDefaultBitrate = 1000000; // 1 Mbps
20
+ constexpr int kDefaultTemporalLayers = 1;
21
+
22
+ // Compute temporal layer ID based on frame position and layer count.
23
+ // Uses standard WebRTC temporal layering pattern.
24
+ int ComputeTemporalLayerId(int64_t frame_index, int temporal_layer_count) {
25
+ if (temporal_layer_count <= 1) return 0;
26
+
27
+ if (temporal_layer_count == 2) {
28
+ // L1T2: alternating pattern [0, 1, 0, 1, ...]
29
+ return (frame_index % 2 == 0) ? 0 : 1;
30
+ }
31
+
32
+ // L1T3: pyramid pattern [0, 2, 1, 2, 0, 2, 1, 2, ...]
33
+ int pos = frame_index % 4;
34
+ if (pos == 0) return 0; // Base layer
35
+ if (pos == 2) return 1; // Middle layer
36
+ return 2; // Enhancement layer (pos 1, 3)
37
+ }
17
38
  constexpr int kDefaultFramerate = 30; // 30 fps
18
39
  constexpr int kDefaultGopSize = 30; // Keyframe interval
19
40
  constexpr int kDefaultMaxBFrames = 2;
@@ -66,6 +87,9 @@ VideoEncoder::VideoEncoder(const Napi::CallbackInfo& info)
66
87
  bitstream_format_("annexb"),
67
88
  frame_count_(0),
68
89
  encode_queue_size_(0) {
90
+ // Track active encoder instance (following sharp pattern)
91
+ webcodecs::counterProcess++;
92
+ webcodecs::counterVideoEncoders++;
69
93
  Napi::Env env = info.Env();
70
94
 
71
95
  if (info.Length() < 1 || !info[0].IsObject()) {
@@ -88,20 +112,59 @@ VideoEncoder::VideoEncoder(const Napi::CallbackInfo& info)
88
112
  error_callback_ = Napi::Persistent(init.Get("error").As<Napi::Function>());
89
113
  }
90
114
 
91
- VideoEncoder::~VideoEncoder() { Cleanup(); }
115
+ VideoEncoder::~VideoEncoder() {
116
+ // CRITICAL: Call Cleanup() first to stop the async worker thread and wait
117
+ // for pending TSFN callbacks. The worker may still be processing frames,
118
+ // and we must ensure it exits cleanly before any further cleanup.
119
+ Cleanup();
120
+
121
+ // Now safe to disable FFmpeg logging. The worker thread has exited and all
122
+ // pending callbacks have been processed or aborted.
123
+ webcodecs::ShutdownFFmpegLogging();
124
+
125
+ // Track active encoder instance (following sharp pattern)
126
+ webcodecs::counterProcess--;
127
+ webcodecs::counterVideoEncoders--;
128
+ }
92
129
 
93
130
  void VideoEncoder::Cleanup() {
94
131
  if (async_worker_) {
132
+ // Stop() joins the worker thread - after this, no new TSFN calls will be made
95
133
  async_worker_->Stop();
96
- async_worker_.reset();
134
+
135
+ // Wait for pending TSFN callbacks to complete.
136
+ // After Stop() joins the thread, there may still be queued TSFN callbacks
137
+ // that haven't been processed yet. These callbacks reference memory that
138
+ // will be freed below.
139
+ auto deadline =
140
+ std::chrono::steady_clock::now() + std::chrono::milliseconds(100);
141
+ while (async_worker_->GetPendingChunks() > 0 &&
142
+ std::chrono::steady_clock::now() < deadline) {
143
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
144
+ }
97
145
  }
98
146
 
147
+ // Release ThreadSafeFunctions to ensure proper cleanup.
148
+ // Release() signals no more calls will be made and decrements the reference
149
+ // count. Pending callbacks will receive null env.
99
150
  if (async_mode_) {
100
151
  output_tsfn_.Release();
101
152
  error_tsfn_.Release();
102
153
  async_mode_ = false;
103
154
  }
104
155
 
156
+ // Safe to destroy async_worker_ - worker thread has exited and TSFN released
157
+ if (async_worker_) {
158
+ async_worker_.reset();
159
+ }
160
+
161
+ // Flush codec internal buffers before destruction.
162
+ // CRITICAL: Only flush if codec was successfully opened. avcodec_flush_buffers
163
+ // crashes on an unopened codec context (the internal codec pointer is NULL).
164
+ if (codec_context_ && avcodec_is_open(codec_context_.get())) {
165
+ avcodec_flush_buffers(codec_context_.get());
166
+ }
167
+
105
168
  frame_.reset();
106
169
  packet_.reset();
107
170
  sws_context_.reset();
@@ -130,6 +193,12 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
130
193
  int framerate =
131
194
  webcodecs::AttrAsInt32(config, "framerate", kDefaultFramerate);
132
195
 
196
+ // Parse bitrateMode per W3C WebCodecs spec.
197
+ // "quantizer" = use CQP mode where frame->quality controls encoding quality.
198
+ // "variable" or "constant" = use bitrate-based encoding (default).
199
+ std::string bitrate_mode =
200
+ webcodecs::AttrAsStr(config, "bitrateMode", "variable");
201
+
133
202
  // Parse codec string
134
203
  std::string codec_str = webcodecs::AttrAsStr(config, "codec", "h264");
135
204
  codec_string_ = codec_str; // Store for metadata
@@ -148,6 +217,35 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
148
217
  color_full_range_ = webcodecs::AttrAsBool(cs, "fullRange", false);
149
218
  }
150
219
 
220
+ // Parse scalabilityMode to determine temporal layer count.
221
+ // Format: L{spatial}T{temporal}, e.g., "L1T2", "L1T3", "L2T2"
222
+ temporal_layer_count_ = kDefaultTemporalLayers;
223
+ std::string scalability_mode =
224
+ webcodecs::AttrAsStr(config, "scalabilityMode", "");
225
+ if (!scalability_mode.empty()) {
226
+ size_t t_pos = scalability_mode.find('T');
227
+ if (t_pos != std::string::npos && t_pos + 1 < scalability_mode.size()) {
228
+ int t_count = scalability_mode[t_pos + 1] - '0';
229
+ if (t_count >= 1 && t_count <= 3) {
230
+ temporal_layer_count_ = t_count;
231
+ }
232
+ }
233
+ }
234
+
235
+ // Parse latencyMode per W3C WebCodecs spec.
236
+ // "realtime" = disable B-frames for low latency (no reordering)
237
+ // "quality" = allow B-frames for better compression (default)
238
+ std::string latency_mode =
239
+ webcodecs::AttrAsStr(config, "latencyMode", "quality");
240
+
241
+ // Store config for codec reinitialization after flush.
242
+ // FFmpeg encoders enter EOF mode after sending NULL frame, requiring
243
+ // full reinitialization to continue encoding per W3C WebCodecs spec.
244
+ bitrate_ = bitrate;
245
+ framerate_ = framerate;
246
+ max_b_frames_ = (latency_mode == "realtime") ? 0 : kDefaultMaxBFrames;
247
+ use_qscale_ = (bitrate_mode == "quantizer");
248
+
151
249
  // Parse codec-specific bitstream format per W3C codec registration.
152
250
  // Default to "annexb" for backwards compatibility (FFmpeg's native format).
153
251
  // Per W3C spec, the default should be "avc"/"hevc" when explicit config
@@ -182,7 +280,46 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
182
280
  throw Napi::Error::New(env, "Unsupported codec: " + codec_str);
183
281
  }
184
282
 
185
- codec_ = avcodec_find_encoder(codec_id);
283
+ // Try hardware encoders first based on platform and hardwareAcceleration
284
+ // setting
285
+ codec_ = nullptr;
286
+ std::string hw_accel =
287
+ webcodecs::AttrAsStr(config, "hardwareAcceleration", "no-preference");
288
+
289
+ if (hw_accel != "prefer-software") {
290
+ #ifdef __APPLE__
291
+ if (codec_id == AV_CODEC_ID_H264) {
292
+ codec_ = avcodec_find_encoder_by_name("h264_videotoolbox");
293
+ } else if (codec_id == AV_CODEC_ID_HEVC) {
294
+ codec_ = avcodec_find_encoder_by_name("hevc_videotoolbox");
295
+ }
296
+ #endif
297
+ #ifdef _WIN32
298
+ if (codec_id == AV_CODEC_ID_H264) {
299
+ codec_ = avcodec_find_encoder_by_name("h264_nvenc");
300
+ if (!codec_) codec_ = avcodec_find_encoder_by_name("h264_qsv");
301
+ if (!codec_) codec_ = avcodec_find_encoder_by_name("h264_amf");
302
+ } else if (codec_id == AV_CODEC_ID_HEVC) {
303
+ codec_ = avcodec_find_encoder_by_name("hevc_nvenc");
304
+ if (!codec_) codec_ = avcodec_find_encoder_by_name("hevc_qsv");
305
+ }
306
+ #endif
307
+ #ifdef __linux__
308
+ if (codec_id == AV_CODEC_ID_H264) {
309
+ codec_ = avcodec_find_encoder_by_name("h264_vaapi");
310
+ if (!codec_) codec_ = avcodec_find_encoder_by_name("h264_nvenc");
311
+ } else if (codec_id == AV_CODEC_ID_HEVC) {
312
+ codec_ = avcodec_find_encoder_by_name("hevc_vaapi");
313
+ if (!codec_) codec_ = avcodec_find_encoder_by_name("hevc_nvenc");
314
+ }
315
+ #endif
316
+ }
317
+
318
+ // Fallback to software encoder
319
+ if (!codec_) {
320
+ codec_ = avcodec_find_encoder(codec_id);
321
+ }
322
+
186
323
  if (!codec_) {
187
324
  throw Napi::Error::New(env, "Encoder not found for codec: " + codec_str);
188
325
  }
@@ -198,9 +335,24 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
198
335
  codec_context_->time_base = {1, framerate};
199
336
  codec_context_->framerate = {framerate, 1};
200
337
  codec_context_->pix_fmt = AV_PIX_FMT_YUV420P;
201
- codec_context_->bit_rate = bitrate;
338
+ // When bitrateMode = "quantizer", enable CQP mode so frame->quality is
339
+ // respected. Don't set bit_rate - let quality control encoding.
340
+ if (bitrate_mode == "quantizer") {
341
+ codec_context_->flags |= AV_CODEC_FLAG_QSCALE;
342
+ codec_context_->global_quality = FF_QP2LAMBDA * 23; // Default QP if none specified
343
+ } else {
344
+ codec_context_->bit_rate = bitrate;
345
+ }
202
346
  codec_context_->gop_size = kDefaultGopSize;
203
- codec_context_->max_b_frames = kDefaultMaxBFrames;
347
+ // Per W3C WebCodecs spec: latencyMode "realtime" disables B-frames for low
348
+ // latency encoding (no frame reordering). This is critical for correct MP4
349
+ // muxing as B-frames require proper DTS calculation which isn't available
350
+ // from WebCodecs chunk timestamps.
351
+ if (latency_mode == "realtime") {
352
+ codec_context_->max_b_frames = 0;
353
+ } else {
354
+ codec_context_->max_b_frames = kDefaultMaxBFrames;
355
+ }
204
356
 
205
357
  // Set global header flag for non-annexb bitstream formats.
206
358
  // This puts SPS/PPS/VPS in codec_context_->extradata instead of in the
@@ -210,29 +362,130 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
210
362
  codec_context_->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
211
363
  }
212
364
 
213
- // Codec-specific options.
214
- if (codec_id == AV_CODEC_ID_H264) {
215
- av_opt_set(codec_context_->priv_data, "preset", "fast", 0);
216
- av_opt_set(codec_context_->priv_data, "tune", "zerolatency", 0);
217
- } else if (codec_id == AV_CODEC_ID_VP8 || codec_id == AV_CODEC_ID_VP9) {
218
- // VP8/VP9 specific: set quality (crf) and speed
219
- av_opt_set(codec_context_->priv_data, "quality", "realtime", 0);
220
- av_opt_set(codec_context_->priv_data, "speed", "6", 0);
221
- // VP8/VP9 don't support B-frames
222
- codec_context_->max_b_frames = 0;
223
- } else if (codec_id == AV_CODEC_ID_AV1) {
224
- // AV1 specific options
225
- av_opt_set(codec_context_->priv_data, "preset", "8", 0);
226
- } else if (codec_id == AV_CODEC_ID_HEVC) {
227
- // libx265 specific options
228
- av_opt_set(codec_context_->priv_data, "preset", "fast", 0);
229
- // Note: libx265 tune options are different from libx264 (grain, animation,
230
- // psnr, ssim) "zerolatency" is not valid for x265, using x265-params
231
- // instead
232
- av_opt_set(codec_context_->priv_data, "x265-params", "bframes=0", 0);
365
+ // Detect if this is a hardware encoder (for skipping software-specific
366
+ // options)
367
+ bool is_hw_encoder =
368
+ codec_ && (strstr(codec_->name, "videotoolbox") != nullptr ||
369
+ strstr(codec_->name, "nvenc") != nullptr ||
370
+ strstr(codec_->name, "qsv") != nullptr ||
371
+ strstr(codec_->name, "vaapi") != nullptr ||
372
+ strstr(codec_->name, "amf") != nullptr);
373
+
374
+ // Detect specific software encoder libraries.
375
+ // Different encoders support different options, so we must check the encoder
376
+ // name before setting library-specific options.
377
+ bool is_libx264 = codec_ && strcmp(codec_->name, "libx264") == 0;
378
+ bool is_libx265 = codec_ && strcmp(codec_->name, "libx265") == 0;
379
+ bool is_libvpx =
380
+ codec_ && (strcmp(codec_->name, "libvpx") == 0 ||
381
+ strcmp(codec_->name, "libvpx-vp9") == 0);
382
+ bool is_libaom = codec_ && strcmp(codec_->name, "libaom-av1") == 0;
383
+ bool is_libsvtav1 = codec_ && strcmp(codec_->name, "libsvtav1") == 0;
384
+
385
+ // Codec-specific options (only for software encoders).
386
+ // Hardware encoders have their own internal quality/speed settings.
387
+ // Only set options when the specific encoder library is detected.
388
+ if (!is_hw_encoder) {
389
+ if (codec_id == AV_CODEC_ID_H264 && is_libx264) {
390
+ // libx264-specific options
391
+ av_opt_set(codec_context_->priv_data, "preset", "fast", 0);
392
+ av_opt_set(codec_context_->priv_data, "tune", "zerolatency", 0);
393
+ // For bitrateMode=quantizer, enable CQP mode in libx264.
394
+ // libx264 ignores AV_CODEC_FLAG_QSCALE; it needs the "qp" option set.
395
+ // We set a default QP here; per-frame quality will be applied via
396
+ // frame->quality which libx264 reads when in CQP mode.
397
+ if (bitrate_mode == "quantizer") {
398
+ av_opt_set_int(codec_context_->priv_data, "qp", 23, 0);
399
+ }
400
+ } else if ((codec_id == AV_CODEC_ID_VP8 || codec_id == AV_CODEC_ID_VP9) &&
401
+ is_libvpx) {
402
+ // libvpx-specific options
403
+ av_opt_set(codec_context_->priv_data, "quality", "realtime", 0);
404
+ av_opt_set(codec_context_->priv_data, "speed", "6", 0);
405
+ // VP8/VP9 don't support B-frames
406
+ codec_context_->max_b_frames = 0;
407
+ } else if (codec_id == AV_CODEC_ID_AV1 && is_libaom) {
408
+ // libaom-av1 uses "cpu-used" for speed preset (0-8, higher = faster)
409
+ av_opt_set(codec_context_->priv_data, "cpu-used", "8", 0);
410
+ } else if (codec_id == AV_CODEC_ID_AV1 && is_libsvtav1) {
411
+ // SVT-AV1 uses "preset" for speed (0-13, higher = faster)
412
+ av_opt_set(codec_context_->priv_data, "preset", "8", 0);
413
+ } else if (codec_id == AV_CODEC_ID_HEVC && is_libx265) {
414
+ // libx265-specific options
415
+ av_opt_set(codec_context_->priv_data, "preset", "fast", 0);
416
+ // Note: libx265 tune options are different from libx264 (grain,
417
+ // animation, psnr, ssim) "zerolatency" is not valid for x265, using
418
+ // x265-params instead
419
+ av_opt_set(codec_context_->priv_data, "x265-params", "bframes=0", 0);
420
+ }
421
+ // For unrecognized encoders, skip library-specific options entirely.
422
+ // The encoder will use its default settings.
423
+ }
424
+
425
+ // Hardware encoder-specific options.
426
+ if (is_hw_encoder) {
427
+ // VideoToolbox: allow software fallback when hardware encoding is unavailable
428
+ // (e.g., in CI/headless environments without GPU access)
429
+ if (strstr(codec_->name, "videotoolbox") != nullptr) {
430
+ av_opt_set(codec_context_->priv_data, "allow_sw", "1", 0);
431
+ }
233
432
  }
234
433
 
235
434
  int ret = avcodec_open2(codec_context_.get(), codec_, nullptr);
435
+
436
+ // If hardware encoder failed, fall back to software encoder.
437
+ // Per W3C spec, "prefer-hardware" still allows software fallback.
438
+ if (ret < 0 && is_hw_encoder) {
439
+ // Reset the failed hardware encoder context
440
+ codec_context_.reset();
441
+
442
+ // Find software encoder as fallback
443
+ codec_ = avcodec_find_encoder(codec_id);
444
+ if (codec_) {
445
+ codec_context_ = ffmpeg::make_codec_context(codec_);
446
+ if (codec_context_) {
447
+ // Reconfigure encoder for software
448
+ codec_context_->width = width_;
449
+ codec_context_->height = height_;
450
+ codec_context_->time_base = {1, framerate};
451
+ codec_context_->framerate = {framerate, 1};
452
+ codec_context_->pix_fmt = AV_PIX_FMT_YUV420P;
453
+ if (bitrate_mode == "quantizer") {
454
+ codec_context_->flags |= AV_CODEC_FLAG_QSCALE;
455
+ codec_context_->global_quality = FF_QP2LAMBDA * 23;
456
+ } else {
457
+ codec_context_->bit_rate = bitrate;
458
+ }
459
+ codec_context_->gop_size = kDefaultGopSize;
460
+ if (latency_mode == "realtime") {
461
+ codec_context_->max_b_frames = 0;
462
+ } else {
463
+ codec_context_->max_b_frames = kDefaultMaxBFrames;
464
+ }
465
+ if (bitstream_format_ != "annexb") {
466
+ codec_context_->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
467
+ }
468
+
469
+ // Set software encoder-specific options
470
+ if (codec_id == AV_CODEC_ID_H264 &&
471
+ strcmp(codec_->name, "libx264") == 0) {
472
+ av_opt_set(codec_context_->priv_data, "preset", "fast", 0);
473
+ av_opt_set(codec_context_->priv_data, "tune", "zerolatency", 0);
474
+ if (bitrate_mode == "quantizer") {
475
+ av_opt_set_int(codec_context_->priv_data, "qp", 23, 0);
476
+ }
477
+ } else if (codec_id == AV_CODEC_ID_HEVC &&
478
+ strcmp(codec_->name, "libx265") == 0) {
479
+ av_opt_set(codec_context_->priv_data, "preset", "fast", 0);
480
+ av_opt_set(codec_context_->priv_data, "x265-params", "bframes=0", 0);
481
+ }
482
+
483
+ // Try opening software encoder
484
+ ret = avcodec_open2(codec_context_.get(), codec_, nullptr);
485
+ }
486
+ }
487
+ }
488
+
236
489
  if (ret < 0) {
237
490
  char errbuf[256];
238
491
  av_strerror(ret, errbuf, sizeof(errbuf));
@@ -245,7 +498,11 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
245
498
  frame_->format = codec_context_->pix_fmt;
246
499
  frame_->width = width_;
247
500
  frame_->height = height_;
248
- av_frame_get_buffer(frame_.get(), kFrameBufferAlignment);
501
+ ret = av_frame_get_buffer(frame_.get(), kFrameBufferAlignment);
502
+ if (ret < 0) {
503
+ Cleanup();
504
+ throw Napi::Error::New(env, "Failed to allocate frame buffer");
505
+ }
249
506
 
250
507
  packet_ = ffmpeg::make_packet();
251
508
 
@@ -286,6 +543,7 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
286
543
  metadata_config.color_transfer = color_transfer_;
287
544
  metadata_config.color_matrix = color_matrix_;
288
545
  metadata_config.color_full_range = color_full_range_;
546
+ metadata_config.temporal_layer_count = temporal_layer_count_;
289
547
  async_worker_->SetMetadataConfig(metadata_config);
290
548
 
291
549
  async_worker_->Start();
@@ -319,6 +577,24 @@ Napi::Value VideoEncoder::Encode(const Napi::CallbackInfo& info) {
319
577
  throw Napi::Error::New(env, "Encoder not configured");
320
578
  }
321
579
 
580
+ // SAFETY VALVE: Reject if queue is too large.
581
+ // This prevents OOM if the consumer ignores backpressure.
582
+ // For async mode, check the async worker's queue; for sync mode, use encode_queue_size_.
583
+ if (async_mode_ && async_worker_) {
584
+ size_t async_queue = async_worker_->QueueSize() + async_worker_->GetPendingChunks();
585
+ if (async_queue >= kMaxHardQueueSize) {
586
+ throw Napi::Error::New(
587
+ env,
588
+ "QuotaExceededError: Encode queue is full. You must handle backpressure "
589
+ "by waiting for encodeQueueSize to decrease.");
590
+ }
591
+ } else if (encode_queue_size_ >= static_cast<int>(kMaxHardQueueSize)) {
592
+ throw Napi::Error::New(
593
+ env,
594
+ "QuotaExceededError: Encode queue is full. You must handle backpressure "
595
+ "by waiting for encodeQueueSize to decrease.");
596
+ }
597
+
322
598
  if (info.Length() < 1 || !info[0].IsObject()) {
323
599
  throw Napi::Error::New(env, "encode requires VideoFrame");
324
600
  }
@@ -389,13 +665,16 @@ Napi::Value VideoEncoder::Encode(const Napi::CallbackInfo& info) {
389
665
  task.duration = video_frame->GetDurationValue();
390
666
  task.key_frame = force_key_frame;
391
667
  task.quantizer = quantizer;
668
+ task.frame_index = frame_count_++;
392
669
 
393
670
  // Get RGBA data from frame
394
671
  size_t data_size = task.width * task.height * 4;
395
672
  task.rgba_data.resize(data_size);
396
673
  std::memcpy(task.rgba_data.data(), video_frame->GetData(), data_size);
397
674
 
398
- encode_queue_size_++;
675
+ // Note: Don't increment encode_queue_size_ for async path.
676
+ // The hard limit check uses async_worker_->QueueSize() + GetPendingChunks() instead.
677
+ webcodecs::counterQueue++; // Global queue tracking
399
678
  async_worker_->Enqueue(std::move(task));
400
679
 
401
680
  return env.Undefined();
@@ -478,6 +757,89 @@ Napi::Value VideoEncoder::Encode(const Napi::CallbackInfo& info) {
478
757
  return env.Undefined();
479
758
  }
480
759
 
760
+ void VideoEncoder::ReinitializeCodec() {
761
+ // Reinitialize codec context after flush.
762
+ // FFmpeg encoders enter EOF mode after sending NULL frame and cannot
763
+ // accept new input without full reinitialization.
764
+ // This is required for W3C WebCodecs compliance where flush() should
765
+ // allow continued encoding.
766
+
767
+ if (!codec_) {
768
+ return; // No codec configured
769
+ }
770
+
771
+ // Close old codec context (RAII handles cleanup via reset)
772
+ codec_context_.reset();
773
+ sws_context_.reset();
774
+
775
+ // Recreate codec context with same settings
776
+ codec_context_ = ffmpeg::make_codec_context(codec_);
777
+ if (!codec_context_) {
778
+ return; // Failed to allocate - will be detected on next encode
779
+ }
780
+
781
+ // Reconfigure encoder with stored settings
782
+ codec_context_->width = width_;
783
+ codec_context_->height = height_;
784
+ codec_context_->time_base = {1, framerate_};
785
+ codec_context_->framerate = {framerate_, 1};
786
+ codec_context_->pix_fmt = AV_PIX_FMT_YUV420P;
787
+ codec_context_->gop_size = kDefaultGopSize;
788
+ codec_context_->max_b_frames = max_b_frames_;
789
+
790
+ if (use_qscale_) {
791
+ codec_context_->flags |= AV_CODEC_FLAG_QSCALE;
792
+ codec_context_->global_quality = FF_QP2LAMBDA * 23;
793
+ } else {
794
+ codec_context_->bit_rate = bitrate_;
795
+ }
796
+
797
+ if (bitstream_format_ != "annexb") {
798
+ codec_context_->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
799
+ }
800
+
801
+ // Hardware encoder-specific options
802
+ if (codec_ && strstr(codec_->name, "videotoolbox") != nullptr) {
803
+ av_opt_set(codec_context_->priv_data, "allow_sw", "1", 0);
804
+ }
805
+
806
+ // Software encoder-specific options
807
+ bool is_libx264 = codec_ && strcmp(codec_->name, "libx264") == 0;
808
+ if (is_libx264) {
809
+ av_opt_set(codec_context_->priv_data, "preset", "fast", 0);
810
+ av_opt_set(codec_context_->priv_data, "tune", "zerolatency", 0);
811
+ if (use_qscale_) {
812
+ av_opt_set_int(codec_context_->priv_data, "qp", 23, 0);
813
+ }
814
+ }
815
+
816
+ // Open codec
817
+ int ret = avcodec_open2(codec_context_.get(), codec_, nullptr);
818
+ if (ret < 0) {
819
+ codec_context_.reset();
820
+ return; // Failed to open - will be detected on next encode
821
+ }
822
+
823
+ // Recreate sws context for RGBA to YUV conversion
824
+ sws_context_.reset(sws_getContext(width_, height_, AV_PIX_FMT_RGBA, width_,
825
+ height_, AV_PIX_FMT_YUV420P, SWS_BILINEAR,
826
+ nullptr, nullptr, nullptr));
827
+
828
+ // Recreate frame and packet for sync mode
829
+ if (!async_mode_) {
830
+ frame_ = ffmpeg::make_frame();
831
+ if (frame_) {
832
+ frame_->format = AV_PIX_FMT_YUV420P;
833
+ frame_->width = width_;
834
+ frame_->height = height_;
835
+ av_frame_get_buffer(frame_.get(), kFrameBufferAlignment);
836
+ }
837
+ packet_ = ffmpeg::make_packet();
838
+ }
839
+
840
+ // Note: frame_count_ is NOT reset - continue numbering from before flush
841
+ }
842
+
481
843
  Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
482
844
  Napi::Env env = info.Env();
483
845
 
@@ -488,6 +850,13 @@ Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
488
850
  if (async_mode_ && async_worker_) {
489
851
  // Wait for async worker to drain its queue
490
852
  async_worker_->Flush();
853
+ // Reinitialize codec after drain (FFmpeg enters EOF mode after NULL frame)
854
+ ReinitializeCodec();
855
+ if (async_worker_) {
856
+ // Update worker with new codec context and sws context
857
+ async_worker_->SetCodecContext(codec_context_.get(), sws_context_.get(),
858
+ width_, height_);
859
+ }
491
860
  // Reset queue after async flush completes
492
861
  encode_queue_size_ = 0;
493
862
  codec_saturated_.store(false);
@@ -500,6 +869,9 @@ Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
500
869
  // Get remaining packets.
501
870
  EmitChunks(env);
502
871
 
872
+ // Reinitialize codec after drain (FFmpeg enters EOF mode after NULL frame)
873
+ ReinitializeCodec();
874
+
503
875
  // Reset queue after flush
504
876
  encode_queue_size_ = 0;
505
877
  codec_saturated_.store(false);
@@ -510,13 +882,29 @@ Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
510
882
  Napi::Value VideoEncoder::Reset(const Napi::CallbackInfo& info) {
511
883
  Napi::Env env = info.Env();
512
884
 
885
+ // W3C spec: reset() is a no-op when closed (don't throw)
513
886
  if (state_ == "closed") {
514
- throw Napi::Error::New(env,
515
- "InvalidStateError: Cannot reset a closed encoder");
887
+ return env.Undefined();
888
+ }
889
+
890
+ // CRITICAL: Stop async worker FIRST before accessing codec_context_.
891
+ // The worker thread may be calling avcodec_send_frame() or avcodec_receive_packet()
892
+ // concurrently. AVCodecContext is NOT thread-safe - concurrent access causes SIGSEGV.
893
+ if (async_worker_) {
894
+ async_worker_->Stop();
895
+ async_worker_.reset();
896
+ }
897
+
898
+ // Release ThreadSafeFunctions after worker is stopped.
899
+ if (async_mode_) {
900
+ output_tsfn_.Release();
901
+ error_tsfn_.Release();
902
+ async_mode_ = false;
516
903
  }
517
904
 
518
- // Flush any pending frames (don't emit - discard).
519
- if (codec_context_) {
905
+ // Now safe to flush codec - worker is stopped.
906
+ // Drain encoder's internal buffers, discard any remaining encoded packets.
907
+ if (codec_context_ && packet_) {
520
908
  avcodec_send_frame(codec_context_.get(), nullptr);
521
909
  while (avcodec_receive_packet(codec_context_.get(), packet_.get()) == 0) {
522
910
  av_packet_unref(packet_.get());
@@ -524,7 +912,11 @@ Napi::Value VideoEncoder::Reset(const Napi::CallbackInfo& info) {
524
912
  }
525
913
 
526
914
  // Clean up FFmpeg resources.
527
- Cleanup();
915
+ frame_.reset();
916
+ packet_.reset();
917
+ sws_context_.reset();
918
+ codec_context_.reset();
919
+ codec_ = nullptr;
528
920
 
529
921
  // Reset state.
530
922
  state_ = "unconfigured";
@@ -568,10 +960,11 @@ void VideoEncoder::EmitChunks(Napi::Env env) {
568
960
  Napi::Object metadata = Napi::Object::New(env);
569
961
 
570
962
  // Add SVC metadata per W3C spec.
571
- // TODO(pproenca): Implement actual temporal/spatial layer tracking. For
572
- // now, always report layer 0 (base layer).
963
+ // Compute temporal layer ID based on frame position and scalabilityMode.
573
964
  Napi::Object svc = Napi::Object::New(env);
574
- svc.Set("temporalLayerId", Napi::Number::New(env, 0));
965
+ int temporal_layer =
966
+ ComputeTemporalLayerId(packet_->pts, temporal_layer_count_);
967
+ svc.Set("temporalLayerId", Napi::Number::New(env, temporal_layer));
575
968
  metadata.Set("svc", svc);
576
969
 
577
970
  // Add decoderConfig for keyframes per W3C spec.
@@ -51,6 +51,7 @@ class VideoEncoder : public Napi::ObjectWrap<VideoEncoder> {
51
51
  // Internal helpers.
52
52
  void Cleanup();
53
53
  void EmitChunks(Napi::Env env);
54
+ void ReinitializeCodec(); // Recreates codec context after flush
54
55
 
55
56
  // FFmpeg state.
56
57
  const AVCodec*
@@ -75,15 +76,30 @@ class VideoEncoder : public Napi::ObjectWrap<VideoEncoder> {
75
76
  std::string color_transfer_;
76
77
  std::string color_matrix_;
77
78
  bool color_full_range_;
79
+ int temporal_layer_count_;
78
80
  // Bitstream format for AVC/HEVC (per W3C codec registration).
79
81
  // "avc"/"hevc": Description (SPS/PPS) provided separately
80
82
  // "annexb": Description embedded in bitstream (default for backwards compat)
81
83
  std::string bitstream_format_;
82
84
  int64_t frame_count_;
85
+
86
+ // Stored configuration for codec reinitialization after flush.
87
+ // FFmpeg encoders enter EOF mode after flush (sending NULL frame),
88
+ // requiring full reinitialization to accept new frames per W3C spec.
89
+ int bitrate_;
90
+ int framerate_;
91
+ int max_b_frames_;
92
+ bool use_qscale_;
83
93
  int encode_queue_size_;
84
94
  std::atomic<bool> codec_saturated_{false};
85
95
  static constexpr size_t kMaxQueueSize = 16; // Saturation threshold
86
96
 
97
+ // HARD LIMIT: The "Circuit Breaker".
98
+ // If the user ignores backpressure signals and keeps pushing frames,
99
+ // we reject requests to prevent OOM.
100
+ // 64 frames @ 4K RGBA (3840x2160x4) is ~2GB of RAM.
101
+ static constexpr size_t kMaxHardQueueSize = 64;
102
+
87
103
  // Saturation status accessor
88
104
  bool IsCodecSaturated() const { return codec_saturated_.load(); }
89
105