@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.
- package/README.md +78 -206
- 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 -124
- 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 +17 -3
- package/dist/index.js +9 -4
- package/dist/is.d.ts +18 -0
- package/dist/is.js +14 -0
- 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 +46 -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/dist/video-frame.d.ts +6 -3
- package/dist/video-frame.js +36 -4
- 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 +9 -3
- package/lib/is.ts +32 -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 +52 -1
- package/lib/video-decoder.ts +84 -2
- package/lib/video-encoder.ts +90 -8
- package/lib/video-frame.ts +52 -7
- package/package.json +49 -32
- package/src/addon.cc +57 -0
- package/src/async_decode_worker.cc +243 -36
- package/src/async_decode_worker.h +55 -4
- package/src/async_encode_worker.cc +155 -44
- package/src/async_encode_worker.h +38 -12
- 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 +428 -35
- package/src/video_encoder.h +16 -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
|
@@ -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
|
-
|
|
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
|
-
:
|
|
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_ =
|
|
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_ =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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_
|
|
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 =
|
|
206
|
-
cb_data->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 =
|
|
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
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
//
|
|
232
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
281
|
-
|
|
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;
|
|
35
|
-
int quantizer = -1;
|
|
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_
|
|
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>
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/src/audio_decoder.cc
CHANGED
|
@@ -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() {
|
|
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
|
|