@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.
- package/README.md +75 -233
- package/binding.gyp +123 -0
- package/dist/audio-decoder.js +1 -2
- package/dist/audio-encoder.d.ts +4 -0
- package/dist/audio-encoder.js +28 -2
- package/dist/binding.d.ts +0 -2
- package/dist/binding.js +43 -125
- package/dist/control-message-queue.js +0 -1
- package/dist/demuxer.d.ts +7 -0
- package/dist/demuxer.js +9 -0
- package/dist/encoded-chunks.d.ts +16 -0
- package/dist/encoded-chunks.js +82 -2
- package/dist/image-decoder.js +4 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +3 -1
- package/dist/native-types.d.ts +20 -0
- package/dist/platform.d.ts +1 -10
- package/dist/platform.js +1 -39
- package/dist/resource-manager.d.ts +1 -2
- package/dist/resource-manager.js +3 -17
- package/dist/types.d.ts +12 -0
- package/dist/video-decoder.d.ts +21 -0
- package/dist/video-decoder.js +74 -2
- package/dist/video-encoder.d.ts +22 -0
- package/dist/video-encoder.js +83 -8
- package/lib/audio-decoder.ts +1 -2
- package/lib/audio-encoder.ts +31 -2
- package/lib/binding.ts +45 -104
- package/lib/control-message-queue.ts +0 -1
- package/lib/demuxer.ts +10 -0
- package/lib/encoded-chunks.ts +90 -2
- package/lib/image-decoder.ts +5 -0
- package/lib/index.ts +3 -0
- package/lib/native-types.ts +22 -0
- package/lib/platform.ts +1 -41
- package/lib/resource-manager.ts +3 -19
- package/lib/types.ts +13 -0
- package/lib/video-decoder.ts +84 -2
- package/lib/video-encoder.ts +90 -8
- package/package.json +49 -32
- package/src/addon.cc +57 -0
- package/src/async_decode_worker.cc +241 -33
- package/src/async_decode_worker.h +55 -3
- package/src/async_encode_worker.cc +103 -35
- package/src/async_encode_worker.h +23 -4
- package/src/audio_data.cc +38 -15
- package/src/audio_data.h +1 -0
- package/src/audio_decoder.cc +24 -3
- package/src/audio_encoder.cc +55 -4
- package/src/common.cc +125 -17
- package/src/common.h +34 -4
- package/src/demuxer.cc +16 -2
- package/src/encoded_audio_chunk.cc +10 -0
- package/src/encoded_audio_chunk.h +2 -0
- package/src/encoded_video_chunk.h +1 -0
- package/src/error_builder.cc +0 -4
- package/src/image_decoder.cc +127 -90
- package/src/image_decoder.h +11 -4
- package/src/muxer.cc +1 -0
- package/src/test_video_generator.cc +3 -2
- package/src/video_decoder.cc +169 -19
- package/src/video_decoder.h +9 -11
- package/src/video_encoder.cc +389 -32
- package/src/video_encoder.h +15 -0
- package/src/video_filter.cc +22 -11
- package/src/video_frame.cc +160 -5
- package/src/warnings.cc +0 -4
- package/dist/audio-data.js.map +0 -1
- package/dist/audio-decoder.js.map +0 -1
- package/dist/audio-encoder.js.map +0 -1
- package/dist/binding.js.map +0 -1
- package/dist/codec-base.js.map +0 -1
- package/dist/control-message-queue.js.map +0 -1
- package/dist/demuxer.js.map +0 -1
- package/dist/encoded-chunks.js.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/ffmpeg.d.ts +0 -21
- package/dist/ffmpeg.js +0 -112
- package/dist/image-decoder.js.map +0 -1
- package/dist/image-track-list.js.map +0 -1
- package/dist/image-track.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/is.js.map +0 -1
- package/dist/muxer.js.map +0 -1
- package/dist/native-types.js.map +0 -1
- package/dist/platform.js.map +0 -1
- package/dist/resource-manager.js.map +0 -1
- package/dist/test-video-generator.js.map +0 -1
- package/dist/transfer.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/video-decoder.js.map +0 -1
- package/dist/video-encoder.js.map +0 -1
- package/dist/video-filter.js.map +0 -1
- package/dist/video-frame.js.map +0 -1
- package/install/build.js +0 -51
- package/install/check.js +0 -192
- package/lib/ffmpeg.ts +0 -78
package/src/video_decoder.cc
CHANGED
|
@@ -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() {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
268
|
-
[](Napi::Env) {});
|
|
322
|
+
"VideoDecoderError", 0, 1);
|
|
269
323
|
|
|
270
|
-
//
|
|
324
|
+
// Create and start the async worker
|
|
271
325
|
async_worker_ =
|
|
272
326
|
std::make_unique<AsyncDecodeWorker>(this, output_tsfn_, error_tsfn_);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
383
|
-
|
|
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
|
-
|
|
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";
|
package/src/video_decoder.h
CHANGED
|
@@ -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
|
-
//
|
|
106
|
-
|
|
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_
|