@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,6 +5,9 @@
5
5
 
6
6
  #include "src/async_decode_worker.h"
7
7
 
8
+ #include <chrono>
9
+ #include <cmath>
10
+ #include <cstdio>
8
11
  #include <string>
9
12
  #include <utility>
10
13
  #include <vector>
@@ -14,38 +17,54 @@ extern "C" {
14
17
  #include <libswscale/swscale.h>
15
18
  }
16
19
 
20
+ #include "src/common.h"
17
21
  #include "src/video_decoder.h"
18
22
  #include "src/video_frame.h"
19
23
 
20
- AsyncDecodeWorker::AsyncDecodeWorker(VideoDecoder* decoder,
24
+ AsyncDecodeWorker::AsyncDecodeWorker(VideoDecoder* /* decoder */,
21
25
  Napi::ThreadSafeFunction output_tsfn,
22
26
  Napi::ThreadSafeFunction error_tsfn)
23
- : decoder_(decoder),
24
- output_tsfn_(output_tsfn),
27
+ : output_tsfn_(output_tsfn),
25
28
  error_tsfn_(error_tsfn),
26
29
  codec_context_(nullptr),
27
- sws_context_(nullptr) {}
30
+ sws_context_(nullptr),
31
+ frame_(nullptr),
32
+ packet_(nullptr),
33
+ output_width_(0),
34
+ output_height_(0) {}
28
35
 
29
36
  AsyncDecodeWorker::~AsyncDecodeWorker() {
30
37
  Stop();
31
- if (frame_) {
32
- av_frame_free(&frame_);
33
- }
34
- if (packet_) {
35
- av_packet_free(&packet_);
38
+ // frame_, packet_, and sws_context_ are RAII-managed, automatically cleaned up
39
+ // Note: codec_context_ is owned by VideoDecoder
40
+
41
+ // Clean up buffer pool
42
+ for (auto* buffer : buffer_pool_) {
43
+ delete buffer;
36
44
  }
37
- // Note: codec_context_ and sws_context_ are owned by VideoDecoder
38
- // They are cleaned up there, not here
45
+ buffer_pool_.clear();
39
46
  }
40
47
 
41
- void AsyncDecodeWorker::SetCodecContext(AVCodecContext* ctx, SwsContext* sws,
48
+ void AsyncDecodeWorker::SetCodecContext(AVCodecContext* ctx,
49
+ SwsContext* /* sws_unused */,
42
50
  int width, int height) {
51
+ std::lock_guard<std::mutex> lock(codec_mutex_);
43
52
  codec_context_ = ctx;
44
- sws_context_ = sws;
53
+ // sws_context_ is created lazily in EmitFrame when we know the frame format
54
+ sws_context_.reset();
45
55
  output_width_ = width;
46
56
  output_height_ = height;
47
- frame_ = av_frame_alloc();
48
- packet_ = av_packet_alloc();
57
+ frame_ = ffmpeg::make_frame();
58
+ packet_ = ffmpeg::make_packet();
59
+
60
+ // DARWIN-X64 FIX: Mark codec as valid only after successful initialization.
61
+ // ProcessPacket checks this flag to avoid accessing codec during shutdown.
62
+ codec_valid_.store(true, std::memory_order_release);
63
+ }
64
+
65
+ void AsyncDecodeWorker::SetMetadataConfig(const DecoderMetadataConfig& config) {
66
+ std::lock_guard<std::mutex> lock(codec_mutex_);
67
+ metadata_config_ = config;
49
68
  }
50
69
 
51
70
  void AsyncDecodeWorker::Start() {
@@ -56,9 +75,26 @@ void AsyncDecodeWorker::Start() {
56
75
  }
57
76
 
58
77
  void AsyncDecodeWorker::Stop() {
78
+ // DARWIN-X64 FIX: Use stop_mutex_ to prevent double-stop race.
79
+ // Cleanup() and destructor may both call Stop().
80
+ std::lock_guard<std::mutex> stop_lock(stop_mutex_);
81
+
59
82
  if (!running_.load()) return;
60
83
 
61
- running_.store(false);
84
+ // DARWIN-X64 FIX: Invalidate codec FIRST, before signaling shutdown.
85
+ // This prevents ProcessPacket from accessing codec_context_ during the
86
+ // race window between setting running_=false and the worker thread exiting.
87
+ codec_valid_.store(false, std::memory_order_release);
88
+
89
+ {
90
+ // CRITICAL: Hold mutex while modifying condition predicate to prevent
91
+ // lost wakeup race on x86_64. Without mutex, there's a window where:
92
+ // 1. Worker checks predicate (running_==true), starts entering wait()
93
+ // 2. Main thread sets running_=false, calls notify_all()
94
+ // 3. Worker enters wait() after notification - blocked forever
95
+ std::lock_guard<std::mutex> lock(queue_mutex_);
96
+ running_.store(false, std::memory_order_release);
97
+ }
62
98
  queue_cv_.notify_all();
63
99
 
64
100
  if (worker_thread_.joinable()) {
@@ -75,13 +111,22 @@ void AsyncDecodeWorker::Enqueue(DecodeTask task) {
75
111
  }
76
112
 
77
113
  void AsyncDecodeWorker::Flush() {
78
- flushing_.store(true);
114
+ // Enqueue a flush task to drain FFmpeg's internal frame buffers
115
+ DecodeTask flush_task;
116
+ flush_task.is_flush = true;
117
+ {
118
+ std::lock_guard<std::mutex> lock(queue_mutex_);
119
+ task_queue_.push(std::move(flush_task));
120
+ }
79
121
  queue_cv_.notify_one();
80
122
 
81
- // Wait for queue to drain
123
+ flushing_.store(true);
124
+
125
+ // Wait for queue to drain AND all in-flight processing to complete
82
126
  std::unique_lock<std::mutex> lock(queue_mutex_);
83
- queue_cv_.wait(lock,
84
- [this] { return task_queue_.empty() || !running_.load(); });
127
+ queue_cv_.wait(lock, [this] {
128
+ return (task_queue_.empty() && processing_.load() == 0) || !running_.load();
129
+ });
85
130
 
86
131
  flushing_.store(false);
87
132
  }
@@ -91,6 +136,28 @@ size_t AsyncDecodeWorker::QueueSize() const {
91
136
  return task_queue_.size();
92
137
  }
93
138
 
139
+ std::vector<uint8_t>* AsyncDecodeWorker::AcquireBuffer(size_t size) {
140
+ std::lock_guard<std::mutex> lock(pool_mutex_);
141
+ for (auto it = buffer_pool_.begin(); it != buffer_pool_.end(); ++it) {
142
+ if ((*it)->capacity() >= size) {
143
+ auto* buffer = *it;
144
+ buffer_pool_.erase(it);
145
+ buffer->resize(size);
146
+ return buffer;
147
+ }
148
+ }
149
+ return new std::vector<uint8_t>(size);
150
+ }
151
+
152
+ void AsyncDecodeWorker::ReleaseBuffer(std::vector<uint8_t>* buffer) {
153
+ std::lock_guard<std::mutex> lock(pool_mutex_);
154
+ if (buffer_pool_.size() < 4) { // Keep up to 4 buffers
155
+ buffer_pool_.push_back(buffer);
156
+ } else {
157
+ delete buffer;
158
+ }
159
+ }
160
+
94
161
  void AsyncDecodeWorker::WorkerThread() {
95
162
  while (running_.load()) {
96
163
  DecodeTask task;
@@ -111,71 +178,211 @@ void AsyncDecodeWorker::WorkerThread() {
111
178
 
112
179
  task = std::move(task_queue_.front());
113
180
  task_queue_.pop();
181
+ processing_++; // Track that we're processing this task
114
182
  }
115
183
 
116
184
  ProcessPacket(task);
117
185
 
118
- if (task_queue_.empty()) {
119
- queue_cv_.notify_all();
186
+ // Decrement counter and notify under lock (fixes race condition).
187
+ {
188
+ std::lock_guard<std::mutex> lock(queue_mutex_);
189
+ processing_--;
190
+ if (task_queue_.empty() && processing_.load() == 0) {
191
+ queue_cv_.notify_all();
192
+ }
120
193
  }
121
194
  }
122
195
  }
123
196
 
124
197
  void AsyncDecodeWorker::ProcessPacket(const DecodeTask& task) {
198
+ // DARWIN-X64 FIX: Check codec_valid_ BEFORE acquiring mutex.
199
+ // During shutdown, Stop() sets codec_valid_=false before running_=false.
200
+ // This creates a window where the worker thread could still be running
201
+ // but the codec is being destroyed. Early exit prevents the race.
202
+ if (!codec_valid_.load(std::memory_order_acquire)) {
203
+ return;
204
+ }
205
+
206
+ std::lock_guard<std::mutex> lock(codec_mutex_);
125
207
  if (!codec_context_ || !packet_ || !frame_) {
126
208
  return;
127
209
  }
128
210
 
211
+ // Handle flush task - send NULL packet to drain decoder
212
+ if (task.is_flush) {
213
+ avcodec_send_packet(codec_context_, nullptr);
214
+ // Drain all remaining frames from the decoder
215
+ while (avcodec_receive_frame(codec_context_, frame_.get()) == 0) {
216
+ EmitFrame(frame_.get());
217
+ av_frame_unref(frame_.get());
218
+ }
219
+ // Reset decoder to accept new packets after drain.
220
+ // Without this, decoder stays in drain mode and rejects further input.
221
+ avcodec_flush_buffers(codec_context_);
222
+ return;
223
+ }
224
+
129
225
  // Set up packet from task data
130
- av_packet_unref(packet_);
226
+ av_packet_unref(packet_.get());
131
227
  packet_->data = const_cast<uint8_t*>(task.data.data());
132
228
  packet_->size = static_cast<int>(task.data.size());
133
229
  packet_->pts = task.timestamp;
134
230
 
135
- int ret = avcodec_send_packet(codec_context_, packet_);
231
+ int ret = avcodec_send_packet(codec_context_, packet_.get());
136
232
  if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) {
137
233
  // Post error to main thread
138
234
  std::string error_msg = "Decode error: " + std::to_string(ret);
139
235
  error_tsfn_.NonBlockingCall(
140
236
  new std::string(error_msg),
141
237
  [](Napi::Env env, Napi::Function fn, std::string* msg) {
238
+ // If env is null, TSFN is closing during teardown. Just cleanup.
239
+ if (env == nullptr) {
240
+ delete msg;
241
+ return;
242
+ }
142
243
  fn.Call({Napi::Error::New(env, *msg).Value()});
143
244
  delete msg;
144
245
  });
145
246
  return;
146
247
  }
147
248
 
148
- while (avcodec_receive_frame(codec_context_, frame_) == 0) {
149
- EmitFrame(frame_);
150
- av_frame_unref(frame_);
249
+ while (avcodec_receive_frame(codec_context_, frame_.get()) == 0) {
250
+ EmitFrame(frame_.get());
251
+ av_frame_unref(frame_.get());
151
252
  }
152
253
  }
153
254
 
154
255
  void AsyncDecodeWorker::EmitFrame(AVFrame* frame) {
155
- if (!sws_context_) {
156
- return;
256
+ // Initialize or recreate SwsContext if frame format/dimensions change
257
+ // (convert from decoder's pixel format to RGBA). RAII managed.
258
+ AVPixelFormat frame_format = static_cast<AVPixelFormat>(frame->format);
259
+
260
+ if (!sws_context_ || last_frame_format_ != frame_format ||
261
+ last_frame_width_ != frame->width ||
262
+ last_frame_height_ != frame->height) {
263
+ // RAII handles cleanup of old context automatically via reset()
264
+ sws_context_.reset(
265
+ sws_getContext(frame->width, frame->height, frame_format, frame->width,
266
+ frame->height, AV_PIX_FMT_RGBA, SWS_BILINEAR, nullptr,
267
+ nullptr, nullptr));
268
+
269
+ if (!sws_context_) {
270
+ std::string error_msg = "Could not create sws context";
271
+ error_tsfn_.NonBlockingCall(
272
+ new std::string(error_msg),
273
+ [](Napi::Env env, Napi::Function fn, std::string* msg) {
274
+ // If env is null, TSFN is closing during teardown. Just cleanup.
275
+ if (env == nullptr) {
276
+ delete msg;
277
+ return;
278
+ }
279
+ fn.Call({Napi::Error::New(env, *msg).Value()});
280
+ delete msg;
281
+ });
282
+ return;
283
+ }
284
+
285
+ last_frame_format_ = frame_format;
286
+ last_frame_width_ = frame->width;
287
+ last_frame_height_ = frame->height;
288
+ // Update output dimensions based on actual frame
289
+ output_width_ = frame->width;
290
+ output_height_ = frame->height;
157
291
  }
158
292
 
293
+ // Copy metadata under lock to prevent torn reads
294
+ // Note: codec_mutex_ is already held by ProcessPacket caller
295
+ DecoderMetadataConfig metadata_copy = metadata_config_;
296
+
159
297
  // Convert YUV to RGBA
160
298
  size_t rgba_size = output_width_ * output_height_ * 4;
161
- auto* rgba_data = new std::vector<uint8_t>(rgba_size);
299
+ auto* rgba_data = AcquireBuffer(rgba_size);
162
300
 
163
301
  uint8_t* dst_data[1] = {rgba_data->data()};
164
302
  int dst_linesize[1] = {output_width_ * 4};
165
303
 
166
- sws_scale(sws_context_, frame->data, frame->linesize, 0, frame->height,
304
+ sws_scale(sws_context_.get(), frame->data, frame->linesize, 0, frame->height,
167
305
  dst_data, dst_linesize);
168
306
 
169
307
  int64_t timestamp = frame->pts;
170
308
  int width = output_width_;
171
309
  int height = output_height_;
172
310
 
311
+ // Capture metadata for lambda
312
+ int rotation = metadata_copy.rotation;
313
+ bool flip = metadata_copy.flip;
314
+
315
+ // Calculate display dimensions based on aspect ratio (per W3C spec).
316
+ // If displayAspectWidth/displayAspectHeight are set, compute display
317
+ // dimensions maintaining the height and adjusting width to match ratio.
318
+ int disp_width = width;
319
+ int disp_height = height;
320
+ if (metadata_copy.display_width > 0 && metadata_copy.display_height > 0) {
321
+ // Per W3C spec: displayWidth = codedHeight * aspectWidth / aspectHeight
322
+ disp_width = static_cast<int>(
323
+ std::round(static_cast<double>(height) *
324
+ static_cast<double>(metadata_copy.display_width) /
325
+ static_cast<double>(metadata_copy.display_height)));
326
+ disp_height = height;
327
+ }
328
+ std::string color_primaries = metadata_copy.color_primaries;
329
+ std::string color_transfer = metadata_copy.color_transfer;
330
+ std::string color_matrix = metadata_copy.color_matrix;
331
+ bool color_full_range = metadata_copy.color_full_range;
332
+ bool has_color_space = metadata_copy.has_color_space;
333
+
334
+ // Increment pending BEFORE queueing callback for accurate tracking
335
+ (*pending_frames_)++;
336
+
337
+ // Capture shared_ptr to pending counter, NOT raw worker pointer.
338
+ // This ensures the counter remains valid even if the worker is destroyed
339
+ // before the TSFN callback executes on the main thread.
340
+ // Note: Buffer is managed via raw delete since buffer pool access is unsafe
341
+ // after worker destruction.
342
+ auto pending_counter = pending_frames_;
173
343
  output_tsfn_.NonBlockingCall(
174
- rgba_data, [width, height, timestamp](Napi::Env env, Napi::Function fn,
175
- std::vector<uint8_t>* data) {
176
- Napi::Object frame_obj = VideoFrame::CreateInstance(
177
- env, data->data(), data->size(), width, height, timestamp, "RGBA");
178
- fn.Call({frame_obj});
344
+ rgba_data,
345
+ [pending_counter, width, height, timestamp, rotation, flip, disp_width,
346
+ disp_height, color_primaries, color_transfer, color_matrix,
347
+ color_full_range,
348
+ has_color_space](Napi::Env env, Napi::Function fn,
349
+ std::vector<uint8_t>* data) {
350
+ // CRITICAL: If env is null, TSFN is closing during teardown.
351
+ // Must still clean up data and counters, then return.
352
+ // NOTE: Do NOT access static variables (like counterQueue) here - they may
353
+ // already be destroyed due to static destruction order during process exit.
354
+ if (env == nullptr) {
355
+ delete data;
356
+ (*pending_counter)--;
357
+ // Skip counterQueue-- : static may be destroyed during process exit
358
+ return;
359
+ }
360
+
361
+ // Always clean up, even if callback throws
362
+ try {
363
+ Napi::Object frame_obj;
364
+ if (has_color_space) {
365
+ frame_obj = VideoFrame::CreateInstance(
366
+ env, data->data(), data->size(), width, height, timestamp,
367
+ "RGBA", rotation, flip, disp_width, disp_height, color_primaries,
368
+ color_transfer, color_matrix, color_full_range);
369
+ } else {
370
+ frame_obj = VideoFrame::CreateInstance(
371
+ env, data->data(), data->size(), width, height, timestamp,
372
+ "RGBA", rotation, flip, disp_width, disp_height);
373
+ }
374
+ fn.Call({frame_obj});
375
+ } catch (const std::exception& e) {
376
+ // Log but don't propagate - cleanup must happen
377
+ fprintf(stderr, "AsyncDecodeWorker callback error: %s\n", e.what());
378
+ } catch (...) {
379
+ fprintf(stderr,
380
+ "AsyncDecodeWorker callback error: unknown exception\n");
381
+ }
382
+ // Delete buffer directly (can't use pool after worker destruction)
179
383
  delete data;
384
+ // Decrement pending counter via shared_ptr (safe after worker destruction)
385
+ (*pending_counter)--;
386
+ webcodecs::counterQueue--; // Decrement global queue counter
180
387
  });
181
388
  }
@@ -12,22 +12,40 @@ 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 <memory>
19
22
  #include <mutex>
20
23
  #include <queue>
24
+ #include <string>
21
25
  #include <thread>
22
26
  #include <vector>
23
27
 
24
28
  class VideoDecoder;
25
29
 
30
+ // Metadata config for decoded video frames (mirrors EncoderMetadataConfig pattern)
31
+ struct DecoderMetadataConfig {
32
+ int rotation = 0;
33
+ bool flip = false;
34
+ int display_width = 0;
35
+ int display_height = 0;
36
+ std::string color_primaries;
37
+ std::string color_transfer;
38
+ std::string color_matrix;
39
+ bool color_full_range = false;
40
+ bool has_color_space = false;
41
+ };
42
+
26
43
  struct DecodeTask {
27
44
  std::vector<uint8_t> data;
28
45
  int64_t timestamp;
29
46
  int64_t duration;
30
47
  bool is_key;
48
+ bool is_flush = false; // When true, flush the decoder instead of decoding
31
49
  };
32
50
 
33
51
  struct DecodedFrame {
@@ -55,15 +73,20 @@ class AsyncDecodeWorker {
55
73
  void Flush();
56
74
  void SetCodecContext(AVCodecContext* ctx, SwsContext* sws, int width,
57
75
  int height);
76
+ void SetMetadataConfig(const DecoderMetadataConfig& config);
58
77
  bool IsRunning() const { return running_.load(); }
59
78
  size_t QueueSize() const;
79
+ int GetPendingFrames() const { return pending_frames_->load(); }
80
+ // Get shared pending counter for TSFN callbacks to capture
81
+ std::shared_ptr<std::atomic<int>> GetPendingFramesPtr() const {
82
+ return pending_frames_;
83
+ }
60
84
 
61
85
  private:
62
86
  void WorkerThread();
63
87
  void ProcessPacket(const DecodeTask& task);
64
88
  void EmitFrame(AVFrame* frame);
65
89
 
66
- VideoDecoder* decoder_;
67
90
  Napi::ThreadSafeFunction output_tsfn_;
68
91
  Napi::ThreadSafeFunction error_tsfn_;
69
92
 
@@ -71,16 +94,44 @@ class AsyncDecodeWorker {
71
94
  std::queue<DecodeTask> task_queue_;
72
95
  mutable std::mutex queue_mutex_; // mutable for const QueueSize()
73
96
  std::condition_variable queue_cv_;
97
+ std::mutex codec_mutex_; // Protects codec_context_, sws_context_, frame_, packet_, metadata_config_
74
98
  std::atomic<bool> running_{false};
75
99
  std::atomic<bool> flushing_{false};
100
+ std::atomic<int> processing_{0}; // Track tasks currently being processed
101
+ // DARWIN-X64 FIX: Guard against codec access during shutdown race window.
102
+ // Set to true after SetCodecContext, false at START of Stop().
103
+ // ProcessPacket checks this before accessing codec_context_.
104
+ std::atomic<bool> codec_valid_{false};
105
+ // Mutex to synchronize Stop() calls from Cleanup() and destructor
106
+ std::mutex stop_mutex_;
107
+ // Use shared_ptr for pending counter so TSFN callbacks can safely access it
108
+ // even after the worker object is destroyed. The shared_ptr is captured by
109
+ // the callback lambda, ensuring the atomic counter remains valid.
110
+ std::shared_ptr<std::atomic<int>> pending_frames_ =
111
+ std::make_shared<std::atomic<int>>(0);
76
112
 
77
113
  // FFmpeg contexts (owned by VideoDecoder, just references here)
78
114
  AVCodecContext* codec_context_;
79
- SwsContext* sws_context_;
80
- AVFrame* frame_;
81
- AVPacket* packet_;
115
+ ffmpeg::SwsContextPtr sws_context_; // RAII-managed, created lazily on first frame
116
+ ffmpeg::AVFramePtr frame_; // RAII-managed, owned by this worker
117
+ ffmpeg::AVPacketPtr packet_; // RAII-managed, owned by this worker
82
118
  int output_width_;
83
119
  int output_height_;
120
+
121
+ // Track last frame format/dimensions for sws_context recreation
122
+ AVPixelFormat last_frame_format_ = AV_PIX_FMT_NONE;
123
+ int last_frame_width_ = 0;
124
+ int last_frame_height_ = 0;
125
+
126
+ // Buffer pool for decoded frame data to reduce allocations
127
+ std::vector<std::vector<uint8_t>*> buffer_pool_;
128
+ std::mutex pool_mutex_;
129
+
130
+ // Decoder metadata for output frames
131
+ DecoderMetadataConfig metadata_config_;
132
+
133
+ std::vector<uint8_t>* AcquireBuffer(size_t size);
134
+ void ReleaseBuffer(std::vector<uint8_t>* buffer);
84
135
  };
85
136
 
86
137
  #endif // SRC_ASYNC_DECODE_WORKER_H_