@pproenca/node-webcodecs 0.1.1-alpha.0 → 0.1.1-alpha.6

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 (97) hide show
  1. package/README.md +75 -233
  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 -125
  8. package/dist/control-message-queue.js +0 -1
  9. package/dist/control-message-queue.js.map +1 -1
  10. package/dist/demuxer.d.ts +7 -0
  11. package/dist/demuxer.js +9 -0
  12. package/dist/encoded-chunks.d.ts +16 -0
  13. package/dist/encoded-chunks.js +82 -2
  14. package/dist/image-decoder.js +4 -0
  15. package/dist/index.d.ts +11 -0
  16. package/dist/index.js +3 -1
  17. package/dist/index.js.map +1 -1
  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/resource-manager.js.map +1 -1
  24. package/dist/types.d.ts +12 -0
  25. package/dist/types.js.map +1 -1
  26. package/dist/video-decoder.d.ts +21 -0
  27. package/dist/video-decoder.js +74 -2
  28. package/dist/video-encoder.d.ts +22 -0
  29. package/dist/video-encoder.js +83 -8
  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 +3 -0
  38. package/lib/native-types.ts +22 -0
  39. package/lib/platform.ts +1 -41
  40. package/lib/resource-manager.ts +3 -19
  41. package/lib/types.ts +13 -0
  42. package/lib/video-decoder.ts +84 -2
  43. package/lib/video-encoder.ts +90 -8
  44. package/package.json +48 -32
  45. package/src/addon.cc +57 -0
  46. package/src/async_decode_worker.cc +241 -33
  47. package/src/async_decode_worker.h +55 -3
  48. package/src/async_encode_worker.cc +103 -35
  49. package/src/async_encode_worker.h +23 -4
  50. package/src/audio_data.cc +38 -15
  51. package/src/audio_data.h +1 -0
  52. package/src/audio_decoder.cc +24 -3
  53. package/src/audio_encoder.cc +55 -4
  54. package/src/common.cc +125 -17
  55. package/src/common.h +34 -4
  56. package/src/demuxer.cc +16 -2
  57. package/src/encoded_audio_chunk.cc +10 -0
  58. package/src/encoded_audio_chunk.h +2 -0
  59. package/src/encoded_video_chunk.h +1 -0
  60. package/src/error_builder.cc +0 -4
  61. package/src/image_decoder.cc +127 -90
  62. package/src/image_decoder.h +11 -4
  63. package/src/muxer.cc +1 -0
  64. package/src/test_video_generator.cc +3 -2
  65. package/src/video_decoder.cc +169 -19
  66. package/src/video_decoder.h +9 -11
  67. package/src/video_encoder.cc +389 -32
  68. package/src/video_encoder.h +15 -0
  69. package/src/video_filter.cc +22 -11
  70. package/src/video_frame.cc +160 -5
  71. package/src/warnings.cc +0 -4
  72. package/dist/audio-data.js.map +0 -1
  73. package/dist/audio-decoder.js.map +0 -1
  74. package/dist/audio-encoder.js.map +0 -1
  75. package/dist/binding.js.map +0 -1
  76. package/dist/codec-base.js.map +0 -1
  77. package/dist/demuxer.js.map +0 -1
  78. package/dist/encoded-chunks.js.map +0 -1
  79. package/dist/errors.js.map +0 -1
  80. package/dist/ffmpeg.d.ts +0 -21
  81. package/dist/ffmpeg.js +0 -112
  82. package/dist/image-decoder.js.map +0 -1
  83. package/dist/image-track-list.js.map +0 -1
  84. package/dist/image-track.js.map +0 -1
  85. package/dist/is.js.map +0 -1
  86. package/dist/muxer.js.map +0 -1
  87. package/dist/native-types.js.map +0 -1
  88. package/dist/platform.js.map +0 -1
  89. package/dist/test-video-generator.js.map +0 -1
  90. package/dist/transfer.js.map +0 -1
  91. package/dist/video-decoder.js.map +0 -1
  92. package/dist/video-encoder.js.map +0 -1
  93. package/dist/video-filter.js.map +0 -1
  94. package/dist/video-frame.js.map +0 -1
  95. package/install/build.js +0 -51
  96. package/install/check.js +0 -192
  97. 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"
@@ -84,6 +87,9 @@ VideoEncoder::VideoEncoder(const Napi::CallbackInfo& info)
84
87
  bitstream_format_("annexb"),
85
88
  frame_count_(0),
86
89
  encode_queue_size_(0) {
90
+ // Track active encoder instance (following sharp pattern)
91
+ webcodecs::counterProcess++;
92
+ webcodecs::counterVideoEncoders++;
87
93
  Napi::Env env = info.Env();
88
94
 
89
95
  if (info.Length() < 1 || !info[0].IsObject()) {
@@ -106,20 +112,59 @@ VideoEncoder::VideoEncoder(const Napi::CallbackInfo& info)
106
112
  error_callback_ = Napi::Persistent(init.Get("error").As<Napi::Function>());
107
113
  }
108
114
 
109
- 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
+ }
110
129
 
111
130
  void VideoEncoder::Cleanup() {
112
131
  if (async_worker_) {
132
+ // Stop() joins the worker thread - after this, no new TSFN calls will be made
113
133
  async_worker_->Stop();
114
- 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
+ }
115
145
  }
116
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.
117
150
  if (async_mode_) {
118
151
  output_tsfn_.Release();
119
152
  error_tsfn_.Release();
120
153
  async_mode_ = false;
121
154
  }
122
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
+
123
168
  frame_.reset();
124
169
  packet_.reset();
125
170
  sws_context_.reset();
@@ -148,6 +193,12 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
148
193
  int framerate =
149
194
  webcodecs::AttrAsInt32(config, "framerate", kDefaultFramerate);
150
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
+
151
202
  // Parse codec string
152
203
  std::string codec_str = webcodecs::AttrAsStr(config, "codec", "h264");
153
204
  codec_string_ = codec_str; // Store for metadata
@@ -181,6 +232,20 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
181
232
  }
182
233
  }
183
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
+
184
249
  // Parse codec-specific bitstream format per W3C codec registration.
185
250
  // Default to "annexb" for backwards compatibility (FFmpeg's native format).
186
251
  // Per W3C spec, the default should be "avc"/"hevc" when explicit config
@@ -215,7 +280,46 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
215
280
  throw Napi::Error::New(env, "Unsupported codec: " + codec_str);
216
281
  }
217
282
 
218
- 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
+
219
323
  if (!codec_) {
220
324
  throw Napi::Error::New(env, "Encoder not found for codec: " + codec_str);
221
325
  }
@@ -231,9 +335,24 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
231
335
  codec_context_->time_base = {1, framerate};
232
336
  codec_context_->framerate = {framerate, 1};
233
337
  codec_context_->pix_fmt = AV_PIX_FMT_YUV420P;
234
- 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
+ }
235
346
  codec_context_->gop_size = kDefaultGopSize;
236
- 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
+ }
237
356
 
238
357
  // Set global header flag for non-annexb bitstream formats.
239
358
  // This puts SPS/PPS/VPS in codec_context_->extradata instead of in the
@@ -243,29 +362,130 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
243
362
  codec_context_->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
244
363
  }
245
364
 
246
- // Codec-specific options.
247
- if (codec_id == AV_CODEC_ID_H264) {
248
- av_opt_set(codec_context_->priv_data, "preset", "fast", 0);
249
- av_opt_set(codec_context_->priv_data, "tune", "zerolatency", 0);
250
- } else if (codec_id == AV_CODEC_ID_VP8 || codec_id == AV_CODEC_ID_VP9) {
251
- // VP8/VP9 specific: set quality (crf) and speed
252
- av_opt_set(codec_context_->priv_data, "quality", "realtime", 0);
253
- av_opt_set(codec_context_->priv_data, "speed", "6", 0);
254
- // VP8/VP9 don't support B-frames
255
- codec_context_->max_b_frames = 0;
256
- } else if (codec_id == AV_CODEC_ID_AV1) {
257
- // AV1 specific options
258
- av_opt_set(codec_context_->priv_data, "preset", "8", 0);
259
- } else if (codec_id == AV_CODEC_ID_HEVC) {
260
- // libx265 specific options
261
- av_opt_set(codec_context_->priv_data, "preset", "fast", 0);
262
- // Note: libx265 tune options are different from libx264 (grain, animation,
263
- // psnr, ssim) "zerolatency" is not valid for x265, using x265-params
264
- // instead
265
- 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
+ }
266
432
  }
267
433
 
268
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
+
269
489
  if (ret < 0) {
270
490
  char errbuf[256];
271
491
  av_strerror(ret, errbuf, sizeof(errbuf));
@@ -278,7 +498,11 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
278
498
  frame_->format = codec_context_->pix_fmt;
279
499
  frame_->width = width_;
280
500
  frame_->height = height_;
281
- 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
+ }
282
506
 
283
507
  packet_ = ffmpeg::make_packet();
284
508
 
@@ -353,6 +577,24 @@ Napi::Value VideoEncoder::Encode(const Napi::CallbackInfo& info) {
353
577
  throw Napi::Error::New(env, "Encoder not configured");
354
578
  }
355
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
+
356
598
  if (info.Length() < 1 || !info[0].IsObject()) {
357
599
  throw Napi::Error::New(env, "encode requires VideoFrame");
358
600
  }
@@ -430,7 +672,9 @@ Napi::Value VideoEncoder::Encode(const Napi::CallbackInfo& info) {
430
672
  task.rgba_data.resize(data_size);
431
673
  std::memcpy(task.rgba_data.data(), video_frame->GetData(), data_size);
432
674
 
433
- 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
434
678
  async_worker_->Enqueue(std::move(task));
435
679
 
436
680
  return env.Undefined();
@@ -513,6 +757,89 @@ Napi::Value VideoEncoder::Encode(const Napi::CallbackInfo& info) {
513
757
  return env.Undefined();
514
758
  }
515
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
+
516
843
  Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
517
844
  Napi::Env env = info.Env();
518
845
 
@@ -523,6 +850,13 @@ Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
523
850
  if (async_mode_ && async_worker_) {
524
851
  // Wait for async worker to drain its queue
525
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
+ }
526
860
  // Reset queue after async flush completes
527
861
  encode_queue_size_ = 0;
528
862
  codec_saturated_.store(false);
@@ -535,6 +869,9 @@ Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
535
869
  // Get remaining packets.
536
870
  EmitChunks(env);
537
871
 
872
+ // Reinitialize codec after drain (FFmpeg enters EOF mode after NULL frame)
873
+ ReinitializeCodec();
874
+
538
875
  // Reset queue after flush
539
876
  encode_queue_size_ = 0;
540
877
  codec_saturated_.store(false);
@@ -545,13 +882,29 @@ Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
545
882
  Napi::Value VideoEncoder::Reset(const Napi::CallbackInfo& info) {
546
883
  Napi::Env env = info.Env();
547
884
 
885
+ // W3C spec: reset() is a no-op when closed (don't throw)
548
886
  if (state_ == "closed") {
549
- throw Napi::Error::New(env,
550
- "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();
551
896
  }
552
897
 
553
- // Flush any pending frames (don't emit - discard).
554
- if (codec_context_) {
898
+ // Release ThreadSafeFunctions after worker is stopped.
899
+ if (async_mode_) {
900
+ output_tsfn_.Release();
901
+ error_tsfn_.Release();
902
+ async_mode_ = false;
903
+ }
904
+
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_) {
555
908
  avcodec_send_frame(codec_context_.get(), nullptr);
556
909
  while (avcodec_receive_packet(codec_context_.get(), packet_.get()) == 0) {
557
910
  av_packet_unref(packet_.get());
@@ -559,7 +912,11 @@ Napi::Value VideoEncoder::Reset(const Napi::CallbackInfo& info) {
559
912
  }
560
913
 
561
914
  // Clean up FFmpeg resources.
562
- Cleanup();
915
+ frame_.reset();
916
+ packet_.reset();
917
+ sws_context_.reset();
918
+ codec_context_.reset();
919
+ codec_ = nullptr;
563
920
 
564
921
  // Reset state.
565
922
  state_ = "unconfigured";
@@ -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*
@@ -81,10 +82,24 @@ class VideoEncoder : public Napi::ObjectWrap<VideoEncoder> {
81
82
  // "annexb": Description embedded in bitstream (default for backwards compat)
82
83
  std::string bitstream_format_;
83
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_;
84
93
  int encode_queue_size_;
85
94
  std::atomic<bool> codec_saturated_{false};
86
95
  static constexpr size_t kMaxQueueSize = 16; // Saturation threshold
87
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
+
88
103
  // Saturation status accessor
89
104
  bool IsCodecSaturated() const { return codec_saturated_.load(); }
90
105
 
@@ -295,22 +295,33 @@ Napi::Value VideoFilter::ApplyBlur(const Napi::CallbackInfo& info) {
295
295
  return env.Undefined();
296
296
  }
297
297
 
298
- // Parse and link filter graph
299
- AVFilterInOut* outputs = avfilter_inout_alloc();
300
- AVFilterInOut* inputs = avfilter_inout_alloc();
298
+ // Parse and link filter graph (RAII for initial allocation safety)
299
+ ffmpeg::AVFilterInOutPtr outputs_raii = ffmpeg::make_filter_inout();
300
+ ffmpeg::AVFilterInOutPtr inputs_raii = ffmpeg::make_filter_inout();
301
301
 
302
- outputs->name = av_strdup("in");
303
- outputs->filter_ctx = buffersrc_ctx_;
304
- outputs->pad_idx = 0;
305
- outputs->next = nullptr;
302
+ if (!outputs_raii || !inputs_raii) {
303
+ Napi::Error::New(env, "Failed to allocate filter inout")
304
+ .ThrowAsJavaScriptException();
305
+ return env.Undefined();
306
+ }
307
+
308
+ outputs_raii->name = av_strdup("in");
309
+ outputs_raii->filter_ctx = buffersrc_ctx_;
310
+ outputs_raii->pad_idx = 0;
311
+ outputs_raii->next = nullptr;
312
+
313
+ inputs_raii->name = av_strdup("out");
314
+ inputs_raii->filter_ctx = buffersink_ctx_;
315
+ inputs_raii->pad_idx = 0;
316
+ inputs_raii->next = nullptr;
306
317
 
307
- inputs->name = av_strdup("out");
308
- inputs->filter_ctx = buffersink_ctx_;
309
- inputs->pad_idx = 0;
310
- inputs->next = nullptr;
318
+ // Release ownership before parse_ptr (it may modify/consume the pointers)
319
+ AVFilterInOut* outputs = outputs_raii.release();
320
+ AVFilterInOut* inputs = inputs_raii.release();
311
321
 
312
322
  ret = avfilter_graph_parse_ptr(filter_graph_.get(), filter_str.c_str(),
313
323
  &inputs, &outputs, nullptr);
324
+ // parse_ptr may have modified pointers, free remaining
314
325
  avfilter_inout_free(&inputs);
315
326
  avfilter_inout_free(&outputs);
316
327