@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
@@ -5,50 +5,79 @@
5
5
 
6
6
  #include "src/async_encode_worker.h"
7
7
 
8
+ #include <chrono>
9
+ #include <cstdio>
10
+ #include <memory>
8
11
  #include <string>
9
12
  #include <utility>
10
13
  #include <vector>
11
14
 
15
+ #include "src/common.h"
12
16
  #include "src/encoded_video_chunk.h"
13
17
  #include "src/video_encoder.h"
14
18
 
15
- AsyncEncodeWorker::AsyncEncodeWorker(VideoEncoder* encoder,
19
+ namespace {
20
+
21
+ // Compute temporal layer ID based on frame position and layer count.
22
+ // Uses standard WebRTC temporal layering pattern.
23
+ // Note: Duplicated from video_encoder.cc to avoid exposing in header.
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
+ }
38
+
39
+ } // namespace
40
+
41
+ AsyncEncodeWorker::AsyncEncodeWorker(VideoEncoder* /* encoder */,
16
42
  Napi::ThreadSafeFunction output_tsfn,
17
43
  Napi::ThreadSafeFunction error_tsfn)
18
- : encoder_(encoder),
19
- output_tsfn_(output_tsfn),
44
+ : output_tsfn_(output_tsfn),
20
45
  error_tsfn_(error_tsfn),
21
46
  codec_context_(nullptr),
22
47
  sws_context_(nullptr) {}
23
48
 
24
49
  void AsyncEncodeWorker::SetCodecContext(AVCodecContext* ctx, SwsContext* sws,
25
50
  int width, int height) {
51
+ std::lock_guard<std::mutex> lock(codec_mutex_);
26
52
  codec_context_ = ctx;
27
53
  sws_context_ = sws;
28
54
  width_ = width;
29
55
  height_ = height;
30
- frame_ = av_frame_alloc();
56
+ frame_ = ffmpeg::make_frame();
31
57
  if (frame_) {
32
58
  frame_->format = AV_PIX_FMT_YUV420P;
33
59
  frame_->width = width;
34
60
  frame_->height = height;
35
- av_frame_get_buffer(frame_, 32);
61
+ int ret = av_frame_get_buffer(frame_.get(), 32);
62
+ if (ret < 0) {
63
+ frame_.reset(); // Clear on allocation failure
64
+ }
36
65
  }
37
- packet_ = av_packet_alloc();
66
+ packet_ = ffmpeg::make_packet();
67
+
68
+ // DARWIN-X64 FIX: Mark codec as valid only after successful initialization.
69
+ // ProcessFrame checks this flag to avoid accessing codec during shutdown.
70
+ codec_valid_.store(true, std::memory_order_release);
38
71
  }
39
72
 
40
73
  void AsyncEncodeWorker::SetMetadataConfig(const EncoderMetadataConfig& config) {
74
+ std::lock_guard<std::mutex> lock(codec_mutex_);
41
75
  metadata_config_ = config;
42
76
  }
43
77
 
44
78
  AsyncEncodeWorker::~AsyncEncodeWorker() {
45
79
  Stop();
46
- if (frame_) {
47
- av_frame_free(&frame_);
48
- }
49
- if (packet_) {
50
- av_packet_free(&packet_);
51
- }
80
+ // frame_ and packet_ are RAII-managed, automatically cleaned up
52
81
  }
53
82
 
54
83
  void AsyncEncodeWorker::Start() {
@@ -59,9 +88,26 @@ void AsyncEncodeWorker::Start() {
59
88
  }
60
89
 
61
90
  void AsyncEncodeWorker::Stop() {
91
+ // DARWIN-X64 FIX: Use stop_mutex_ to prevent double-stop race.
92
+ // Cleanup() and destructor may both call Stop().
93
+ std::lock_guard<std::mutex> stop_lock(stop_mutex_);
94
+
62
95
  if (!running_.load()) return;
63
96
 
64
- running_.store(false);
97
+ // DARWIN-X64 FIX: Invalidate codec FIRST, before signaling shutdown.
98
+ // This prevents ProcessFrame from accessing codec_context_ during the
99
+ // race window between setting running_=false and the worker thread exiting.
100
+ codec_valid_.store(false, std::memory_order_release);
101
+
102
+ {
103
+ // CRITICAL: Hold mutex while modifying condition predicate to prevent
104
+ // lost wakeup race on x86_64. Without mutex, there's a window where:
105
+ // 1. Worker checks predicate (running_==true), starts entering wait()
106
+ // 2. Main thread sets running_=false, calls notify_all()
107
+ // 3. Worker enters wait() after notification - blocked forever
108
+ std::lock_guard<std::mutex> lock(queue_mutex_);
109
+ running_.store(false, std::memory_order_release);
110
+ }
65
111
  queue_cv_.notify_all();
66
112
 
67
113
  if (worker_thread_.joinable()) {
@@ -89,11 +135,13 @@ void AsyncEncodeWorker::Flush() {
89
135
 
90
136
  flushing_.store(true);
91
137
 
92
- // Wait for queue to drain (including flush task)
138
+ // Wait for queue to drain AND all in-flight processing to complete
93
139
  {
94
140
  std::unique_lock<std::mutex> lock(queue_mutex_);
95
- queue_cv_.wait(lock,
96
- [this] { return task_queue_.empty() || !running_.load(); });
141
+ queue_cv_.wait(lock, [this] {
142
+ return (task_queue_.empty() && processing_.load() == 0) ||
143
+ !running_.load();
144
+ });
97
145
  }
98
146
 
99
147
  flushing_.store(false);
@@ -124,29 +172,49 @@ void AsyncEncodeWorker::WorkerThread() {
124
172
 
125
173
  task = std::move(task_queue_.front());
126
174
  task_queue_.pop();
175
+ processing_++; // Track that we're processing this task
127
176
  }
128
177
 
129
178
  ProcessFrame(task);
130
179
 
131
- if (task_queue_.empty()) {
132
- queue_cv_.notify_all();
180
+ // Decrement counter and notify under lock (fixes race condition).
181
+ {
182
+ std::lock_guard<std::mutex> lock(queue_mutex_);
183
+ processing_--;
184
+ if (task_queue_.empty() && processing_.load() == 0) {
185
+ queue_cv_.notify_all();
186
+ }
133
187
  }
134
188
  }
135
189
  }
136
190
 
137
191
  void AsyncEncodeWorker::ProcessFrame(const EncodeTask& task) {
192
+ // DARWIN-X64 FIX: Check codec_valid_ BEFORE acquiring mutex.
193
+ // During shutdown, Stop() sets codec_valid_=false before running_=false.
194
+ // This creates a window where the worker thread could still be running
195
+ // but the codec is being destroyed. Early exit prevents the race.
196
+ if (!codec_valid_.load(std::memory_order_acquire)) {
197
+ return;
198
+ }
199
+
200
+ std::lock_guard<std::mutex> lock(codec_mutex_);
138
201
  if (!codec_context_ || !sws_context_ || !frame_ || !packet_) {
139
202
  return;
140
203
  }
141
204
 
142
- // Handle flush task - send NULL frame to drain encoder
205
+ // Handle flush task - send NULL frame to drain encoder.
206
+ // Note: After this, the codec enters EOF mode and won't accept new frames.
207
+ // The VideoEncoder::Flush() method handles codec reinitialization after
208
+ // the worker drains to allow continued encoding per W3C WebCodecs spec.
143
209
  if (task.is_flush) {
144
210
  avcodec_send_frame(codec_context_, nullptr);
145
211
  // Drain all remaining packets
146
- while (avcodec_receive_packet(codec_context_, packet_) == 0) {
147
- EmitChunk(packet_);
148
- av_packet_unref(packet_);
212
+ while (avcodec_receive_packet(codec_context_, packet_.get()) == 0) {
213
+ EmitChunk(packet_.get());
214
+ av_packet_unref(packet_.get());
149
215
  }
216
+ // Clear frame info map after flush
217
+ frame_info_.clear();
150
218
  return;
151
219
  }
152
220
 
@@ -157,7 +225,11 @@ void AsyncEncodeWorker::ProcessFrame(const EncodeTask& task) {
157
225
  sws_scale(sws_context_, src_data, src_linesize, 0, height_, frame_->data,
158
226
  frame_->linesize);
159
227
 
160
- frame_->pts = task.timestamp;
228
+ // Use frame_index as pts for consistent SVC layer computation
229
+ // Store original timestamp/duration for lookup when emitting packets
230
+ frame_->pts = task.frame_index;
231
+ frame_info_[task.frame_index] =
232
+ std::make_pair(task.timestamp, task.duration);
161
233
 
162
234
  // Apply per-frame quantizer if specified (matches sync path)
163
235
  if (task.quantizer >= 0) {
@@ -166,21 +238,26 @@ void AsyncEncodeWorker::ProcessFrame(const EncodeTask& task) {
166
238
  frame_->quality = 0; // Let encoder decide
167
239
  }
168
240
 
169
- int ret = avcodec_send_frame(codec_context_, frame_);
241
+ int ret = avcodec_send_frame(codec_context_, frame_.get());
170
242
  if (ret < 0 && ret != AVERROR(EAGAIN)) {
171
243
  std::string error_msg = "Encode error: " + std::to_string(ret);
172
244
  error_tsfn_.NonBlockingCall(
173
245
  new std::string(error_msg),
174
246
  [](Napi::Env env, Napi::Function fn, std::string* msg) {
247
+ // If env is null, TSFN is closing during teardown. Just cleanup.
248
+ if (env == nullptr) {
249
+ delete msg;
250
+ return;
251
+ }
175
252
  fn.Call({Napi::Error::New(env, *msg).Value()});
176
253
  delete msg;
177
254
  });
178
255
  return;
179
256
  }
180
257
 
181
- while (avcodec_receive_packet(codec_context_, packet_) == 0) {
182
- EmitChunk(packet_);
183
- av_packet_unref(packet_);
258
+ while (avcodec_receive_packet(codec_context_, packet_.get()) == 0) {
259
+ EmitChunk(packet_.get());
260
+ av_packet_unref(packet_.get());
184
261
  }
185
262
  }
186
263
 
@@ -190,21 +267,38 @@ struct ChunkCallbackData {
190
267
  int64_t pts;
191
268
  int64_t duration;
192
269
  bool is_key;
270
+ int64_t frame_index; // For SVC layer computation
193
271
  EncoderMetadataConfig metadata;
194
272
  std::vector<uint8_t> extradata; // Copy from codec_context at emit time
195
- std::atomic<int>* pending;
273
+ // Use shared_ptr to pending counter so it remains valid even if worker is
274
+ // destroyed before callback executes on main thread.
275
+ std::shared_ptr<std::atomic<int>> pending;
196
276
  };
197
277
 
198
278
  void AsyncEncodeWorker::EmitChunk(AVPacket* pkt) {
199
279
  // Increment pending count before async operation
200
- pending_chunks_.fetch_add(1);
280
+ pending_chunks_->fetch_add(1);
281
+
282
+ // pkt->pts is the frame_index (set in ProcessFrame)
283
+ int64_t frame_index = pkt->pts;
284
+
285
+ // Look up original timestamp/duration from the map
286
+ int64_t timestamp = 0;
287
+ int64_t duration = 0;
288
+ auto it = frame_info_.find(frame_index);
289
+ if (it != frame_info_.end()) {
290
+ timestamp = it->second.first;
291
+ duration = it->second.second;
292
+ frame_info_.erase(it); // Clean up after use
293
+ }
201
294
 
202
295
  // Create callback data with all info needed on main thread
203
296
  auto* cb_data = new ChunkCallbackData();
204
297
  cb_data->data.assign(pkt->data, pkt->data + pkt->size);
205
- cb_data->pts = pkt->pts;
206
- cb_data->duration = pkt->duration;
298
+ cb_data->pts = timestamp; // Use original timestamp, not frame_index
299
+ cb_data->duration = duration;
207
300
  cb_data->is_key = (pkt->flags & AV_PKT_FLAG_KEY) != 0;
301
+ cb_data->frame_index = frame_index; // For SVC layer computation
208
302
  cb_data->metadata = metadata_config_;
209
303
  // Copy extradata from codec_context at emit time (may be set after configure)
210
304
  if (codec_context_ && codec_context_->extradata &&
@@ -213,25 +307,42 @@ void AsyncEncodeWorker::EmitChunk(AVPacket* pkt) {
213
307
  codec_context_->extradata,
214
308
  codec_context_->extradata + codec_context_->extradata_size);
215
309
  }
216
- cb_data->pending = &pending_chunks_;
310
+ cb_data->pending = pending_chunks_;
217
311
 
218
312
  output_tsfn_.NonBlockingCall(cb_data, [](Napi::Env env, Napi::Function fn,
219
313
  ChunkCallbackData* info) {
220
- // Create EncodedVideoChunk-like object (matches synchronous path)
221
- Napi::Object chunk = Napi::Object::New(env);
222
- chunk.Set("type", info->is_key ? "key" : "delta");
223
- chunk.Set("timestamp", Napi::Number::New(env, info->pts));
224
- chunk.Set("duration", Napi::Number::New(env, info->duration));
225
- chunk.Set("data", Napi::Buffer<uint8_t>::Copy(env, info->data.data(),
226
- info->data.size()));
314
+ // CRITICAL: If env is null, the TSFN is being destroyed (environment teardown).
315
+ // Must still clean up data and counters, then return to avoid crashing.
316
+ // NOTE: Do NOT access static variables (like counterQueue) here - they may
317
+ // already be destroyed due to static destruction order during process exit.
318
+ if (env == nullptr) {
319
+ info->pending->fetch_sub(1);
320
+ // Skip counterQueue-- : static may be destroyed during process exit
321
+ delete info;
322
+ return;
323
+ }
324
+
325
+ // Decrement pending count before any operations
326
+ info->pending->fetch_sub(1);
327
+ webcodecs::counterQueue--;
328
+
329
+ // Create native EncodedVideoChunk directly to avoid double-copy.
330
+ // The data is copied once into the chunk's internal buffer.
331
+ // Previously we created a plain JS object here, which the TS layer
332
+ // would wrap in a new EncodedVideoChunk, causing a second copy.
333
+ Napi::Object chunk = EncodedVideoChunk::CreateInstance(
334
+ env, info->is_key ? "key" : "delta", info->pts, info->duration,
335
+ info->data.data(), info->data.size());
227
336
 
228
337
  // Create metadata object matching sync path
229
338
  Napi::Object metadata = Napi::Object::New(env);
230
339
 
231
- // TODO(pproenca): Implement actual temporal/spatial layer tracking.
232
- // See video_encoder.cc for related TODO. For now, always report layer 0.
340
+ // Add SVC metadata per W3C spec.
341
+ // Compute temporal layer ID based on frame_index and scalabilityMode.
233
342
  Napi::Object svc = Napi::Object::New(env);
234
- svc.Set("temporalLayerId", Napi::Number::New(env, 0));
343
+ int temporal_layer = ComputeTemporalLayerId(
344
+ info->frame_index, info->metadata.temporal_layer_count);
345
+ svc.Set("temporalLayerId", Napi::Number::New(env, temporal_layer));
235
346
  metadata.Set("svc", svc);
236
347
 
237
348
  // Add decoderConfig for keyframes per W3C spec
@@ -277,8 +388,8 @@ void AsyncEncodeWorker::EmitChunk(AVPacket* pkt) {
277
388
 
278
389
  fn.Call({chunk, metadata});
279
390
 
280
- // Decrement pending count after callback completes
281
- info->pending->fetch_sub(1);
391
+ // ChunkCallbackData is no longer tied to the buffer lifetime.
392
+ // Delete it now that the data has been copied into the EncodedVideoChunk.
282
393
  delete info;
283
394
  });
284
395
  }
@@ -12,27 +12,33 @@ extern "C" {
12
12
  #include <libswscale/swscale.h>
13
13
  }
14
14
 
15
+ #include "src/ffmpeg_raii.h"
16
+
15
17
  #include <napi.h>
16
18
 
17
19
  #include <atomic>
18
20
  #include <condition_variable>
21
+ #include <map>
22
+ #include <memory>
19
23
  #include <mutex>
20
24
  #include <queue>
21
25
  #include <string>
22
26
  #include <thread>
27
+ #include <utility>
23
28
  #include <vector>
24
29
 
25
30
  class VideoEncoder;
26
31
 
27
32
  struct EncodeTask {
28
33
  std::vector<uint8_t> rgba_data;
29
- uint32_t width;
30
- uint32_t height;
31
- int64_t timestamp;
32
- int64_t duration;
33
- bool key_frame;
34
- bool is_flush = false; // When true, flush the encoder instead of encoding
35
- int quantizer = -1; // -1 means not specified, otherwise 0-63 range
34
+ uint32_t width = 0;
35
+ uint32_t height = 0;
36
+ int64_t timestamp = 0;
37
+ int64_t duration = 0;
38
+ bool key_frame = false;
39
+ bool is_flush = false; // When true, flush the encoder instead of encoding
40
+ int quantizer = -1; // -1 means not specified, otherwise 0-63 range
41
+ int64_t frame_index = 0; // Sequential frame index for SVC layer computation
36
42
  };
37
43
 
38
44
  struct EncodedChunk {
@@ -53,6 +59,7 @@ struct EncoderMetadataConfig {
53
59
  std::string color_transfer;
54
60
  std::string color_matrix;
55
61
  bool color_full_range = false;
62
+ int temporal_layer_count = 1; // From scalabilityMode (L1T1=1, L1T2=2, L1T3=3)
56
63
  // Note: extradata is copied from codec_context at emit time (may be set after
57
64
  // configure)
58
65
  };
@@ -74,7 +81,11 @@ class AsyncEncodeWorker {
74
81
  void Flush();
75
82
  bool IsRunning() const { return running_.load(); }
76
83
  size_t QueueSize() const;
77
- int GetPendingChunks() const { return pending_chunks_.load(); }
84
+ int GetPendingChunks() const { return pending_chunks_->load(); }
85
+ // Get shared pending counter for TSFN callbacks to capture
86
+ std::shared_ptr<std::atomic<int>> GetPendingChunksPtr() const {
87
+ return pending_chunks_;
88
+ }
78
89
  void SetCodecContext(AVCodecContext* ctx, SwsContext* sws, int width,
79
90
  int height);
80
91
  void SetMetadataConfig(const EncoderMetadataConfig& config);
@@ -84,28 +95,43 @@ class AsyncEncodeWorker {
84
95
  void ProcessFrame(const EncodeTask& task);
85
96
  void EmitChunk(AVPacket* packet);
86
97
 
87
- VideoEncoder* encoder_;
88
98
  Napi::ThreadSafeFunction output_tsfn_;
89
99
  Napi::ThreadSafeFunction error_tsfn_;
90
100
 
91
101
  std::thread worker_thread_;
92
102
  std::queue<EncodeTask> task_queue_;
93
103
  mutable std::mutex queue_mutex_; // mutable for const QueueSize()
104
+ std::mutex codec_mutex_; // Protects codec_context_, sws_context_, frame_, packet_, metadata_config_
94
105
  std::condition_variable queue_cv_;
95
106
  std::atomic<bool> running_{false};
96
107
  std::atomic<bool> flushing_{false};
97
- std::atomic<int> pending_chunks_{0};
108
+ std::atomic<int> processing_{0}; // Track tasks currently being processed
109
+ // DARWIN-X64 FIX: Guard against codec access during shutdown race window.
110
+ // Set to true after SetCodecContext, false at START of Stop().
111
+ // ProcessFrame checks this before accessing codec_context_.
112
+ std::atomic<bool> codec_valid_{false};
113
+ // Mutex to synchronize Stop() calls from Cleanup() and destructor
114
+ std::mutex stop_mutex_;
115
+ // Use shared_ptr for pending counter so TSFN callbacks can safely access it
116
+ // even after the worker object is destroyed. The shared_ptr is captured by
117
+ // the callback lambda, ensuring the atomic counter remains valid.
118
+ std::shared_ptr<std::atomic<int>> pending_chunks_ =
119
+ std::make_shared<std::atomic<int>>(0);
98
120
 
99
121
  // FFmpeg contexts (owned by VideoEncoder, just references here)
100
122
  AVCodecContext* codec_context_;
101
123
  SwsContext* sws_context_;
102
- AVFrame* frame_;
103
- AVPacket* packet_;
124
+ ffmpeg::AVFramePtr frame_; // RAII-managed, owned by this worker
125
+ ffmpeg::AVPacketPtr packet_; // RAII-managed, owned by this worker
104
126
  int width_;
105
127
  int height_;
106
128
 
107
129
  // Encoder metadata for output chunks
108
130
  EncoderMetadataConfig metadata_config_;
131
+
132
+ // Map from frame_index (used as pts) to original timestamp/duration
133
+ // Needed because packets may come out in different order due to B-frames
134
+ std::map<int64_t, std::pair<int64_t, int64_t>> frame_info_; // frame_index -> (timestamp, duration)
109
135
  };
110
136
 
111
137
  #endif // SRC_ASYNC_ENCODE_WORKER_H_
package/src/audio_data.cc CHANGED
@@ -8,6 +8,7 @@
8
8
  #include <vector>
9
9
 
10
10
  #include "src/common.h"
11
+ #include "src/ffmpeg_raii.h"
11
12
 
12
13
  extern "C" {
13
14
  #include <libavutil/channel_layout.h>
@@ -98,6 +99,7 @@ AudioData::AudioData(const Napi::CallbackInfo& info)
98
99
  number_of_channels_(0),
99
100
  timestamp_(0),
100
101
  closed_(false) {
102
+ webcodecs::counterAudioData++;
101
103
  Napi::Env env = info.Env();
102
104
 
103
105
  if (info.Length() < 1 || !info[0].IsObject()) {
@@ -197,6 +199,26 @@ AudioData::AudioData(const Napi::CallbackInfo& info)
197
199
  .ThrowAsJavaScriptException();
198
200
  return;
199
201
  }
202
+
203
+ // Inform V8 of external memory allocation for GC pressure calculation.
204
+ Napi::MemoryManagement::AdjustExternalMemory(
205
+ env, static_cast<int64_t>(data_.size()));
206
+ }
207
+
208
+ AudioData::~AudioData() {
209
+ webcodecs::counterAudioData--;
210
+ // Note: We intentionally DO NOT call AdjustExternalMemory here.
211
+ //
212
+ // Calling NAPI functions (including AdjustExternalMemory) from destructors
213
+ // during V8 shutdown is unsafe and causes crashes on Node.js 24+ due to
214
+ // race conditions with V8's ArrayBufferSweeper during Heap::TearDown().
215
+ // See: https://github.com/nodejs/node-addon-api/issues/1153
216
+ //
217
+ // The WebCodecs spec mandates that close() must be called for proper
218
+ // resource management. External memory tracking is handled exclusively
219
+ // in Close() to avoid shutdown crashes.
220
+ data_.clear();
221
+ data_.shrink_to_fit();
200
222
  }
201
223
 
202
224
  size_t AudioData::GetBytesPerSample() const {
@@ -503,8 +525,8 @@ void AudioData::CopyTo(const Napi::CallbackInfo& info) {
503
525
  return;
504
526
  }
505
527
 
506
- // Create resampler context.
507
- SwrContext* swr = swr_alloc();
528
+ // Create resampler context (RAII managed).
529
+ ffmpeg::SwrContextPtr swr(swr_alloc());
508
530
  if (!swr) {
509
531
  Napi::Error::New(env, "Failed to allocate SwrContext")
510
532
  .ThrowAsJavaScriptException();
@@ -517,18 +539,17 @@ void AudioData::CopyTo(const Napi::CallbackInfo& info) {
517
539
  av_channel_layout_default(&ch_layout, number_of_channels_);
518
540
 
519
541
  // Set input parameters.
520
- av_opt_set_chlayout(swr, "in_chlayout", &ch_layout, 0);
521
- av_opt_set_int(swr, "in_sample_rate", sample_rate_, 0);
522
- av_opt_set_sample_fmt(swr, "in_sample_fmt", src_fmt, 0);
542
+ av_opt_set_chlayout(swr.get(), "in_chlayout", &ch_layout, 0);
543
+ av_opt_set_int(swr.get(), "in_sample_rate", sample_rate_, 0);
544
+ av_opt_set_sample_fmt(swr.get(), "in_sample_fmt", src_fmt, 0);
523
545
 
524
546
  // Set output parameters.
525
- av_opt_set_chlayout(swr, "out_chlayout", &ch_layout, 0);
526
- av_opt_set_int(swr, "out_sample_rate", sample_rate_, 0);
527
- av_opt_set_sample_fmt(swr, "out_sample_fmt", dst_fmt, 0);
547
+ av_opt_set_chlayout(swr.get(), "out_chlayout", &ch_layout, 0);
548
+ av_opt_set_int(swr.get(), "out_sample_rate", sample_rate_, 0);
549
+ av_opt_set_sample_fmt(swr.get(), "out_sample_fmt", dst_fmt, 0);
528
550
 
529
- int ret = swr_init(swr);
551
+ int ret = swr_init(swr.get());
530
552
  if (ret < 0) {
531
- swr_free(&swr);
532
553
  av_channel_layout_uninit(&ch_layout);
533
554
  Napi::Error::New(env, "Failed to initialize SwrContext")
534
555
  .ThrowAsJavaScriptException();
@@ -566,9 +587,8 @@ void AudioData::CopyTo(const Napi::CallbackInfo& info) {
566
587
  temp_buffer.data() + c * frame_count * target_bytes_per_sample;
567
588
  }
568
589
 
569
- ret = swr_convert(swr, dst_data, frame_count, src_data, frame_count);
590
+ ret = swr_convert(swr.get(), dst_data, frame_count, src_data, frame_count);
570
591
  if (ret < 0) {
571
- swr_free(&swr);
572
592
  av_channel_layout_uninit(&ch_layout);
573
593
  Napi::Error::New(env, "swr_convert failed").ThrowAsJavaScriptException();
574
594
  return;
@@ -581,16 +601,15 @@ void AudioData::CopyTo(const Napi::CallbackInfo& info) {
581
601
  // Interleaved output: write directly to destination.
582
602
  dst_data[0] = dest_data;
583
603
 
584
- ret = swr_convert(swr, dst_data, frame_count, src_data, frame_count);
604
+ ret = swr_convert(swr.get(), dst_data, frame_count, src_data, frame_count);
585
605
  if (ret < 0) {
586
- swr_free(&swr);
587
606
  av_channel_layout_uninit(&ch_layout);
588
607
  Napi::Error::New(env, "swr_convert failed").ThrowAsJavaScriptException();
589
608
  return;
590
609
  }
591
610
  }
592
611
 
593
- swr_free(&swr);
612
+ // RAII handles swr cleanup
594
613
  av_channel_layout_uninit(&ch_layout);
595
614
  }
596
615
 
@@ -610,6 +629,10 @@ Napi::Value AudioData::Clone(const Napi::CallbackInfo& info) {
610
629
 
611
630
  void AudioData::Close(const Napi::CallbackInfo& info) {
612
631
  if (!closed_) {
632
+ if (!data_.empty()) {
633
+ Napi::MemoryManagement::AdjustExternalMemory(
634
+ info.Env(), -static_cast<int64_t>(data_.size()));
635
+ }
613
636
  data_.clear();
614
637
  data_.shrink_to_fit();
615
638
  closed_ = true;
package/src/audio_data.h CHANGED
@@ -20,6 +20,7 @@ class AudioData : public Napi::ObjectWrap<AudioData> {
20
20
  int64_t timestamp, const uint8_t* data,
21
21
  size_t data_size);
22
22
  explicit AudioData(const Napi::CallbackInfo& info);
23
+ ~AudioData();
23
24
 
24
25
  // Prevent copy and assignment.
25
26
  AudioData(const AudioData&) = delete;
@@ -50,6 +50,8 @@ AudioDecoder::AudioDecoder(const Napi::CallbackInfo& info)
50
50
  state_("unconfigured"),
51
51
  sample_rate_(0),
52
52
  number_of_channels_(0) {
53
+ // Track active decoder instance
54
+ webcodecs::counterAudioDecoders++;
53
55
  Napi::Env env = info.Env();
54
56
 
55
57
  if (info.Length() < 1 || !info[0].IsObject()) {
@@ -75,9 +77,29 @@ AudioDecoder::AudioDecoder(const Napi::CallbackInfo& info)
75
77
  error_callback_ = Napi::Persistent(init.Get("error").As<Napi::Function>());
76
78
  }
77
79
 
78
- AudioDecoder::~AudioDecoder() { Cleanup(); }
80
+ AudioDecoder::~AudioDecoder() {
81
+ // CRITICAL: Call Cleanup() first to ensure codec context is properly
82
+ // flushed before any further cleanup.
83
+ Cleanup();
84
+
85
+ // Now safe to disable FFmpeg logging.
86
+ webcodecs::ShutdownFFmpegLogging();
87
+
88
+ webcodecs::counterAudioDecoders--;
89
+ }
79
90
 
80
91
  void AudioDecoder::Cleanup() {
92
+ // DARWIN-X64 FIX: Flush codec internal buffers BEFORE destroying resources.
93
+ // Audio decoders may have internal queued frames. Flushing ensures they're
94
+ // drained before context destruction, preventing use-after-free.
95
+ // CRITICAL: Only flush if codec was successfully opened. avcodec_flush_buffers
96
+ // crashes on an unopened codec context (the internal codec pointer is NULL).
97
+ // NOTE: Order matters - flush must happen before resetting frame_/packet_/swr_
98
+ // to match VideoDecoder pattern and ensure codec internal state is consistent.
99
+ if (codec_context_ && avcodec_is_open(codec_context_.get())) {
100
+ avcodec_flush_buffers(codec_context_.get());
101
+ }
102
+
81
103
  frame_.reset();
82
104
  packet_.reset();
83
105
  swr_context_.reset();
@@ -236,9 +258,8 @@ void AudioDecoder::Close(const Napi::CallbackInfo& info) {
236
258
  Napi::Value AudioDecoder::Reset(const Napi::CallbackInfo& info) {
237
259
  Napi::Env env = info.Env();
238
260
 
261
+ // W3C spec: reset() is a no-op when closed (don't throw)
239
262
  if (state_ == "closed") {
240
- Napi::Error::New(env, "InvalidStateError: Cannot reset closed decoder")
241
- .ThrowAsJavaScriptException();
242
263
  return env.Undefined();
243
264
  }
244
265