@pproenca/node-webcodecs 0.1.1-alpha.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 (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/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 +11 -0
  15. package/dist/index.js +3 -1
  16. package/dist/native-types.d.ts +20 -0
  17. package/dist/platform.d.ts +1 -10
  18. package/dist/platform.js +1 -39
  19. package/dist/resource-manager.d.ts +1 -2
  20. package/dist/resource-manager.js +3 -17
  21. package/dist/types.d.ts +12 -0
  22. package/dist/video-decoder.d.ts +21 -0
  23. package/dist/video-decoder.js +74 -2
  24. package/dist/video-encoder.d.ts +22 -0
  25. package/dist/video-encoder.js +83 -8
  26. package/lib/audio-decoder.ts +1 -2
  27. package/lib/audio-encoder.ts +31 -2
  28. package/lib/binding.ts +45 -104
  29. package/lib/control-message-queue.ts +0 -1
  30. package/lib/demuxer.ts +10 -0
  31. package/lib/encoded-chunks.ts +90 -2
  32. package/lib/image-decoder.ts +5 -0
  33. package/lib/index.ts +3 -0
  34. package/lib/native-types.ts +22 -0
  35. package/lib/platform.ts +1 -41
  36. package/lib/resource-manager.ts +3 -19
  37. package/lib/types.ts +13 -0
  38. package/lib/video-decoder.ts +84 -2
  39. package/lib/video-encoder.ts +90 -8
  40. package/package.json +49 -32
  41. package/src/addon.cc +57 -0
  42. package/src/async_decode_worker.cc +241 -33
  43. package/src/async_decode_worker.h +55 -3
  44. package/src/async_encode_worker.cc +103 -35
  45. package/src/async_encode_worker.h +23 -4
  46. package/src/audio_data.cc +38 -15
  47. package/src/audio_data.h +1 -0
  48. package/src/audio_decoder.cc +24 -3
  49. package/src/audio_encoder.cc +55 -4
  50. package/src/common.cc +125 -17
  51. package/src/common.h +34 -4
  52. package/src/demuxer.cc +16 -2
  53. package/src/encoded_audio_chunk.cc +10 -0
  54. package/src/encoded_audio_chunk.h +2 -0
  55. package/src/encoded_video_chunk.h +1 -0
  56. package/src/error_builder.cc +0 -4
  57. package/src/image_decoder.cc +127 -90
  58. package/src/image_decoder.h +11 -4
  59. package/src/muxer.cc +1 -0
  60. package/src/test_video_generator.cc +3 -2
  61. package/src/video_decoder.cc +169 -19
  62. package/src/video_decoder.h +9 -11
  63. package/src/video_encoder.cc +389 -32
  64. package/src/video_encoder.h +15 -0
  65. package/src/video_filter.cc +22 -11
  66. package/src/video_frame.cc +160 -5
  67. package/src/warnings.cc +0 -4
  68. package/dist/audio-data.js.map +0 -1
  69. package/dist/audio-decoder.js.map +0 -1
  70. package/dist/audio-encoder.js.map +0 -1
  71. package/dist/binding.js.map +0 -1
  72. package/dist/codec-base.js.map +0 -1
  73. package/dist/control-message-queue.js.map +0 -1
  74. package/dist/demuxer.js.map +0 -1
  75. package/dist/encoded-chunks.js.map +0 -1
  76. package/dist/errors.js.map +0 -1
  77. package/dist/ffmpeg.d.ts +0 -21
  78. package/dist/ffmpeg.js +0 -112
  79. package/dist/image-decoder.js.map +0 -1
  80. package/dist/image-track-list.js.map +0 -1
  81. package/dist/image-track.js.map +0 -1
  82. package/dist/index.js.map +0 -1
  83. package/dist/is.js.map +0 -1
  84. package/dist/muxer.js.map +0 -1
  85. package/dist/native-types.js.map +0 -1
  86. package/dist/platform.js.map +0 -1
  87. package/dist/resource-manager.js.map +0 -1
  88. package/dist/test-video-generator.js.map +0 -1
  89. package/dist/transfer.js.map +0 -1
  90. package/dist/types.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,13 +3,15 @@
3
3
 
4
4
  #include "src/video_decoder.h"
5
5
 
6
+ #include <chrono>
6
7
  #include <cmath>
7
8
  #include <cstring>
8
9
  #include <memory>
9
10
  #include <string>
11
+ #include <thread>
12
+ #include <utility>
10
13
  #include <vector>
11
14
 
12
- #include "src/async_decode_worker.h"
13
15
  #include "src/common.h"
14
16
  #include "src/encoded_video_chunk.h"
15
17
  #include "src/video_frame.h"
@@ -39,6 +41,8 @@ Napi::Object VideoDecoder::Init(Napi::Env env, Napi::Object exports) {
39
41
  nullptr),
40
42
  InstanceAccessor("codecSaturated", &VideoDecoder::GetCodecSaturated,
41
43
  nullptr),
44
+ InstanceAccessor("pendingFrames", &VideoDecoder::GetPendingFrames,
45
+ nullptr),
42
46
  StaticMethod("isConfigSupported", &VideoDecoder::IsConfigSupported),
43
47
  });
44
48
 
@@ -52,6 +56,9 @@ VideoDecoder::VideoDecoder(const Napi::CallbackInfo& info)
52
56
  state_("unconfigured"),
53
57
  coded_width_(0),
54
58
  coded_height_(0) {
59
+ // Track active decoder instance (following sharp pattern)
60
+ webcodecs::counterProcess++;
61
+ webcodecs::counterVideoDecoders++;
55
62
  Napi::Env env = info.Env();
56
63
 
57
64
  if (info.Length() < 1 || !info[0].IsObject()) {
@@ -74,22 +81,64 @@ VideoDecoder::VideoDecoder(const Napi::CallbackInfo& info)
74
81
  error_callback_ = Napi::Persistent(init.Get("error").As<Napi::Function>());
75
82
  }
76
83
 
77
- VideoDecoder::~VideoDecoder() { Cleanup(); }
84
+ VideoDecoder::~VideoDecoder() {
85
+ // CRITICAL: Call Cleanup() first to stop the async worker thread and wait
86
+ // for pending TSFN callbacks. The worker may still be processing frames,
87
+ // and we must ensure it exits cleanly before any further cleanup.
88
+ Cleanup();
89
+
90
+ // Now safe to disable FFmpeg logging. The worker thread has exited and all
91
+ // pending callbacks have been processed or aborted.
92
+ webcodecs::ShutdownFFmpegLogging();
93
+
94
+ // Track active decoder instance (following sharp pattern)
95
+ webcodecs::counterProcess--;
96
+ webcodecs::counterVideoDecoders--;
97
+ }
78
98
 
79
99
  void VideoDecoder::Cleanup() {
80
- // Stop async worker first.
100
+ // Stop async worker before cleaning up codec context
81
101
  if (async_worker_) {
102
+ // Stop() joins the worker thread - after this, no new TSFN calls will be made
82
103
  async_worker_->Stop();
83
- async_worker_.reset();
104
+
105
+ // DARWIN-X64 FIX: Wait for pending TSFN callbacks to complete.
106
+ // After Stop() joins the thread, there may still be queued TSFN callbacks
107
+ // that haven't been processed yet. These callbacks reference memory that
108
+ // will be freed below.
109
+ auto deadline =
110
+ std::chrono::steady_clock::now() + std::chrono::milliseconds(100);
111
+ while (async_worker_->GetPendingFrames() > 0 &&
112
+ std::chrono::steady_clock::now() < deadline) {
113
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
114
+ }
84
115
  }
85
116
 
86
- // Release ThreadSafeFunctions if they were created.
117
+ // DARWIN-X64 FIX: Release ThreadSafeFunctions to ensure proper cleanup.
118
+ // Release() signals no more calls will be made and decrements the reference
119
+ // count. Without explicit Release(), TSFN cleanup during environment teardown
120
+ // may race with other shutdown operations on Intel Mac (slower timing).
121
+ // The shared_ptr<atomic<int>> pending_frames_ captured by callbacks ensures
122
+ // thread-safety even if callbacks are cancelled mid-flight.
87
123
  if (async_mode_) {
88
124
  output_tsfn_.Release();
89
125
  error_tsfn_.Release();
126
+ async_mode_ = false;
90
127
  }
91
128
 
92
- async_mode_ = false;
129
+ // Safe to destroy async_worker_ - worker thread has exited and TSFN aborted
130
+ if (async_worker_) {
131
+ async_worker_.reset();
132
+ }
133
+
134
+ // DARWIN-X64 FIX: Flush codec internal buffers before destruction.
135
+ // Some decoders may have internal queued frames. Flushing ensures they're
136
+ // drained before context destruction, preventing use-after-free.
137
+ // CRITICAL: Only flush if codec was successfully opened. avcodec_flush_buffers
138
+ // crashes on an unopened codec context (the internal codec pointer is NULL).
139
+ if (codec_context_ && avcodec_is_open(codec_context_.get())) {
140
+ avcodec_flush_buffers(codec_context_.get());
141
+ }
93
142
 
94
143
  frame_.reset();
95
144
  packet_.reset();
@@ -258,21 +307,41 @@ Napi::Value VideoDecoder::Configure(const Napi::CallbackInfo& info) {
258
307
  throw Napi::Error::New(env, "Could not allocate packet");
259
308
  }
260
309
 
261
- // Create ThreadSafeFunctions for async worker.
262
- output_tsfn_ = Napi::ThreadSafeFunction::New(env, output_callback_.Value(),
263
- "VideoDecoder::output", 0, 1,
264
- [](Napi::Env) {});
310
+ state_ = "configured";
265
311
 
312
+ // Enable async decoding via worker thread.
313
+ // Flush semantics use pendingFrames counter - TypeScript polls with
314
+ // setTimeout to wait for all TSFN callbacks to complete without blocking
315
+ // the event loop.
316
+ async_mode_ = true;
317
+
318
+ // Create ThreadSafeFunctions for async callbacks
319
+ output_tsfn_ = Napi::ThreadSafeFunction::New(env, output_callback_.Value(),
320
+ "VideoDecoderOutput", 0, 1);
266
321
  error_tsfn_ = Napi::ThreadSafeFunction::New(env, error_callback_.Value(),
267
- "VideoDecoder::error", 0, 1,
268
- [](Napi::Env) {});
322
+ "VideoDecoderError", 0, 1);
269
323
 
270
- // Initialize async worker.
324
+ // Create and start the async worker
271
325
  async_worker_ =
272
326
  std::make_unique<AsyncDecodeWorker>(this, output_tsfn_, error_tsfn_);
273
- async_mode_ = true;
274
-
275
- state_ = "configured";
327
+ // Pass nullptr for sws_context - AsyncDecodeWorker creates it lazily
328
+ async_worker_->SetCodecContext(codec_context_.get(), nullptr, coded_width_,
329
+ coded_height_);
330
+
331
+ // Set metadata config for async output frames (matching encoder pattern)
332
+ DecoderMetadataConfig metadata_config;
333
+ metadata_config.rotation = rotation_;
334
+ metadata_config.flip = flip_;
335
+ metadata_config.display_width = display_aspect_width_;
336
+ metadata_config.display_height = display_aspect_height_;
337
+ metadata_config.color_primaries = color_primaries_;
338
+ metadata_config.color_transfer = color_transfer_;
339
+ metadata_config.color_matrix = color_matrix_;
340
+ metadata_config.color_full_range = color_full_range_;
341
+ metadata_config.has_color_space = has_color_space_;
342
+ async_worker_->SetMetadataConfig(metadata_config);
343
+
344
+ async_worker_->Start();
276
345
 
277
346
  return env.Undefined();
278
347
  }
@@ -289,6 +358,13 @@ Napi::Value VideoDecoder::GetCodecSaturated(const Napi::CallbackInfo& info) {
289
358
  return Napi::Boolean::New(info.Env(), codec_saturated_.load());
290
359
  }
291
360
 
361
+ Napi::Value VideoDecoder::GetPendingFrames(const Napi::CallbackInfo& info) {
362
+ if (async_worker_) {
363
+ return Napi::Number::New(info.Env(), async_worker_->GetPendingFrames());
364
+ }
365
+ return Napi::Number::New(info.Env(), 0);
366
+ }
367
+
292
368
  Napi::Value VideoDecoder::Decode(const Napi::CallbackInfo& info) {
293
369
  Napi::Env env = info.Env();
294
370
 
@@ -296,6 +372,22 @@ Napi::Value VideoDecoder::Decode(const Napi::CallbackInfo& info) {
296
372
  throw Napi::Error::New(env, "InvalidStateError: Decoder not configured");
297
373
  }
298
374
 
375
+ // Reject if queue is too large (prevents OOM).
376
+ if (async_mode_ && async_worker_) {
377
+ size_t queue = async_worker_->QueueSize() + async_worker_->GetPendingFrames();
378
+ if (queue >= kMaxHardQueueSize) {
379
+ throw Napi::Error::New(
380
+ env,
381
+ "QuotaExceededError: Decode queue is full. You must handle "
382
+ "backpressure by waiting for decodeQueueSize to decrease.");
383
+ }
384
+ } else if (decode_queue_size_ >= static_cast<int>(kMaxHardQueueSize)) {
385
+ throw Napi::Error::New(
386
+ env,
387
+ "QuotaExceededError: Decode queue is full. You must handle "
388
+ "backpressure by waiting for decodeQueueSize to decrease.");
389
+ }
390
+
299
391
  if (info.Length() < 1 || !info[0].IsObject()) {
300
392
  throw Napi::Error::New(env, "decode requires EncodedVideoChunk");
301
393
  }
@@ -308,8 +400,31 @@ Napi::Value VideoDecoder::Decode(const Napi::CallbackInfo& info) {
308
400
  const uint8_t* data = chunk->GetData();
309
401
  size_t data_size = chunk->GetDataSize();
310
402
  int64_t timestamp = chunk->GetTimestampValue();
403
+ int64_t duration = chunk->GetDurationValue();
311
404
  bool is_key_frame = (chunk->GetTypeValue() == "key");
312
405
 
406
+ // Use async decoding path
407
+ if (async_mode_ && async_worker_) {
408
+ // Create decode task with copy of chunk data
409
+ DecodeTask task;
410
+ task.data.assign(data, data + data_size);
411
+ task.timestamp = timestamp;
412
+ task.duration = duration;
413
+ task.is_key = is_key_frame;
414
+
415
+ // Enqueue to async worker (non-blocking)
416
+ async_worker_->Enqueue(std::move(task));
417
+
418
+ // Update queue size tracking
419
+ decode_queue_size_++;
420
+ webcodecs::counterQueue++; // Global queue tracking
421
+ bool saturated = decode_queue_size_ >= static_cast<int>(kMaxQueueSize);
422
+ codec_saturated_.store(saturated);
423
+
424
+ return env.Undefined();
425
+ }
426
+
427
+ // Fallback to synchronous decoding (shouldn't normally reach here)
313
428
  // Setup packet.
314
429
  av_packet_unref(packet_.get());
315
430
  packet_->data = const_cast<uint8_t*>(data);
@@ -353,6 +468,24 @@ Napi::Value VideoDecoder::Flush(const Napi::CallbackInfo& info) {
353
468
  return deferred.Promise();
354
469
  }
355
470
 
471
+ // Use async flush path
472
+ if (async_mode_ && async_worker_) {
473
+ // Wait for async worker's task queue to drain
474
+ async_worker_->Flush();
475
+
476
+ // Reset queue after flush
477
+ decode_queue_size_ = 0;
478
+ codec_saturated_.store(false);
479
+
480
+ // Return resolved promise.
481
+ // TypeScript layer will poll pendingFrames to wait for all TSFN
482
+ // callbacks to complete.
483
+ Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
484
+ deferred.Resolve(env.Undefined());
485
+ return deferred.Promise();
486
+ }
487
+
488
+ // Fallback to synchronous flush (shouldn't normally reach here)
356
489
  // Send NULL packet to flush decoder.
357
490
  int ret = avcodec_send_packet(codec_context_.get(), nullptr);
358
491
  if (ret < 0 && ret != AVERROR_EOF) {
@@ -378,9 +511,22 @@ Napi::Value VideoDecoder::Flush(const Napi::CallbackInfo& info) {
378
511
  Napi::Value VideoDecoder::Reset(const Napi::CallbackInfo& info) {
379
512
  Napi::Env env = info.Env();
380
513
 
514
+ // W3C spec: reset() is a no-op when closed (don't throw)
381
515
  if (state_ == "closed") {
382
- throw Napi::Error::New(env,
383
- "InvalidStateError: Cannot reset a closed decoder");
516
+ return env.Undefined();
517
+ }
518
+
519
+ // Stop async worker first before accessing codec
520
+ if (async_worker_) {
521
+ async_worker_->Stop();
522
+ async_worker_.reset();
523
+ }
524
+
525
+ // Release ThreadSafeFunctions
526
+ if (async_mode_) {
527
+ output_tsfn_.Release();
528
+ error_tsfn_.Release();
529
+ async_mode_ = false;
384
530
  }
385
531
 
386
532
  // Flush any pending frames (discard them).
@@ -392,7 +538,11 @@ Napi::Value VideoDecoder::Reset(const Napi::CallbackInfo& info) {
392
538
  }
393
539
 
394
540
  // Clean up FFmpeg resources.
395
- Cleanup();
541
+ frame_.reset();
542
+ packet_.reset();
543
+ sws_context_.reset();
544
+ codec_context_.reset();
545
+ codec_ = nullptr;
396
546
 
397
547
  // Reset state.
398
548
  state_ = "unconfigured";
@@ -18,11 +18,9 @@ extern "C" {
18
18
  #include <memory>
19
19
  #include <string>
20
20
 
21
+ #include "src/async_decode_worker.h"
21
22
  #include "src/ffmpeg_raii.h"
22
23
 
23
- // Forward declaration
24
- class AsyncDecodeWorker;
25
-
26
24
  class VideoDecoder : public Napi::ObjectWrap<VideoDecoder> {
27
25
  public:
28
26
  static Napi::Object Init(Napi::Env env, Napi::Object exports);
@@ -44,6 +42,7 @@ class VideoDecoder : public Napi::ObjectWrap<VideoDecoder> {
44
42
  Napi::Value GetState(const Napi::CallbackInfo& info);
45
43
  Napi::Value GetDecodeQueueSize(const Napi::CallbackInfo& info);
46
44
  Napi::Value GetCodecSaturated(const Napi::CallbackInfo& info);
45
+ Napi::Value GetPendingFrames(const Napi::CallbackInfo& info);
47
46
 
48
47
  // Internal helpers.
49
48
  void Cleanup();
@@ -61,12 +60,6 @@ class VideoDecoder : public Napi::ObjectWrap<VideoDecoder> {
61
60
  Napi::FunctionReference output_callback_;
62
61
  Napi::FunctionReference error_callback_;
63
62
 
64
- // Async worker for non-blocking decode.
65
- std::unique_ptr<AsyncDecodeWorker> async_worker_;
66
- Napi::ThreadSafeFunction output_tsfn_;
67
- Napi::ThreadSafeFunction error_tsfn_;
68
- bool async_mode_ = false;
69
-
70
63
  // State.
71
64
  std::string state_;
72
65
  int coded_width_;
@@ -74,6 +67,7 @@ class VideoDecoder : public Napi::ObjectWrap<VideoDecoder> {
74
67
  int decode_queue_size_ = 0;
75
68
  std::atomic<bool> codec_saturated_{false};
76
69
  static constexpr size_t kMaxQueueSize = 16;
70
+ static constexpr size_t kMaxHardQueueSize = 64;
77
71
 
78
72
  // Rotation and flip config (per W3C spec).
79
73
  int rotation_ = 0; // 0, 90, 180, 270
@@ -102,8 +96,12 @@ class VideoDecoder : public Napi::ObjectWrap<VideoDecoder> {
102
96
  int last_frame_width_ = 0;
103
97
  int last_frame_height_ = 0;
104
98
 
105
- // Friend declaration
106
- friend class AsyncDecodeWorker;
99
+ // Async decoding via worker thread (non-blocking).
100
+ bool async_mode_ = false;
101
+ Napi::ThreadSafeFunction output_tsfn_;
102
+ Napi::ThreadSafeFunction error_tsfn_;
103
+ std::unique_ptr<AsyncDecodeWorker> async_worker_;
104
+ std::atomic<int> pending_frames_{0}; // Track frames in flight for flush
107
105
  };
108
106
 
109
107
  #endif // SRC_VIDEO_DECODER_H_