@pproenca/node-webcodecs 0.1.1-alpha.0 → 0.1.1-alpha.6
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/control-message-queue.js.map +1 -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/index.js.map +1 -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/resource-manager.js.map +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/types.js.map +1 -1
- 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 +48 -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/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/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/test-video-generator.js.map +0 -1
- package/dist/transfer.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_encoder.cc
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
#include "src/video_encoder.h"
|
|
5
5
|
|
|
6
|
+
#include <chrono>
|
|
7
|
+
#include <cstdio>
|
|
6
8
|
#include <memory>
|
|
7
9
|
#include <string>
|
|
10
|
+
#include <thread>
|
|
8
11
|
#include <utility>
|
|
9
12
|
|
|
10
13
|
#include "src/common.h"
|
|
@@ -84,6 +87,9 @@ VideoEncoder::VideoEncoder(const Napi::CallbackInfo& info)
|
|
|
84
87
|
bitstream_format_("annexb"),
|
|
85
88
|
frame_count_(0),
|
|
86
89
|
encode_queue_size_(0) {
|
|
90
|
+
// Track active encoder instance (following sharp pattern)
|
|
91
|
+
webcodecs::counterProcess++;
|
|
92
|
+
webcodecs::counterVideoEncoders++;
|
|
87
93
|
Napi::Env env = info.Env();
|
|
88
94
|
|
|
89
95
|
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
@@ -106,20 +112,59 @@ VideoEncoder::VideoEncoder(const Napi::CallbackInfo& info)
|
|
|
106
112
|
error_callback_ = Napi::Persistent(init.Get("error").As<Napi::Function>());
|
|
107
113
|
}
|
|
108
114
|
|
|
109
|
-
VideoEncoder::~VideoEncoder() {
|
|
115
|
+
VideoEncoder::~VideoEncoder() {
|
|
116
|
+
// CRITICAL: Call Cleanup() first to stop the async worker thread and wait
|
|
117
|
+
// for pending TSFN callbacks. The worker may still be processing frames,
|
|
118
|
+
// and we must ensure it exits cleanly before any further cleanup.
|
|
119
|
+
Cleanup();
|
|
120
|
+
|
|
121
|
+
// Now safe to disable FFmpeg logging. The worker thread has exited and all
|
|
122
|
+
// pending callbacks have been processed or aborted.
|
|
123
|
+
webcodecs::ShutdownFFmpegLogging();
|
|
124
|
+
|
|
125
|
+
// Track active encoder instance (following sharp pattern)
|
|
126
|
+
webcodecs::counterProcess--;
|
|
127
|
+
webcodecs::counterVideoEncoders--;
|
|
128
|
+
}
|
|
110
129
|
|
|
111
130
|
void VideoEncoder::Cleanup() {
|
|
112
131
|
if (async_worker_) {
|
|
132
|
+
// Stop() joins the worker thread - after this, no new TSFN calls will be made
|
|
113
133
|
async_worker_->Stop();
|
|
114
|
-
|
|
134
|
+
|
|
135
|
+
// Wait for pending TSFN callbacks to complete.
|
|
136
|
+
// After Stop() joins the thread, there may still be queued TSFN callbacks
|
|
137
|
+
// that haven't been processed yet. These callbacks reference memory that
|
|
138
|
+
// will be freed below.
|
|
139
|
+
auto deadline =
|
|
140
|
+
std::chrono::steady_clock::now() + std::chrono::milliseconds(100);
|
|
141
|
+
while (async_worker_->GetPendingChunks() > 0 &&
|
|
142
|
+
std::chrono::steady_clock::now() < deadline) {
|
|
143
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
144
|
+
}
|
|
115
145
|
}
|
|
116
146
|
|
|
147
|
+
// Release ThreadSafeFunctions to ensure proper cleanup.
|
|
148
|
+
// Release() signals no more calls will be made and decrements the reference
|
|
149
|
+
// count. Pending callbacks will receive null env.
|
|
117
150
|
if (async_mode_) {
|
|
118
151
|
output_tsfn_.Release();
|
|
119
152
|
error_tsfn_.Release();
|
|
120
153
|
async_mode_ = false;
|
|
121
154
|
}
|
|
122
155
|
|
|
156
|
+
// Safe to destroy async_worker_ - worker thread has exited and TSFN released
|
|
157
|
+
if (async_worker_) {
|
|
158
|
+
async_worker_.reset();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Flush codec internal buffers before destruction.
|
|
162
|
+
// CRITICAL: Only flush if codec was successfully opened. avcodec_flush_buffers
|
|
163
|
+
// crashes on an unopened codec context (the internal codec pointer is NULL).
|
|
164
|
+
if (codec_context_ && avcodec_is_open(codec_context_.get())) {
|
|
165
|
+
avcodec_flush_buffers(codec_context_.get());
|
|
166
|
+
}
|
|
167
|
+
|
|
123
168
|
frame_.reset();
|
|
124
169
|
packet_.reset();
|
|
125
170
|
sws_context_.reset();
|
|
@@ -148,6 +193,12 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
|
|
|
148
193
|
int framerate =
|
|
149
194
|
webcodecs::AttrAsInt32(config, "framerate", kDefaultFramerate);
|
|
150
195
|
|
|
196
|
+
// Parse bitrateMode per W3C WebCodecs spec.
|
|
197
|
+
// "quantizer" = use CQP mode where frame->quality controls encoding quality.
|
|
198
|
+
// "variable" or "constant" = use bitrate-based encoding (default).
|
|
199
|
+
std::string bitrate_mode =
|
|
200
|
+
webcodecs::AttrAsStr(config, "bitrateMode", "variable");
|
|
201
|
+
|
|
151
202
|
// Parse codec string
|
|
152
203
|
std::string codec_str = webcodecs::AttrAsStr(config, "codec", "h264");
|
|
153
204
|
codec_string_ = codec_str; // Store for metadata
|
|
@@ -181,6 +232,20 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
|
|
|
181
232
|
}
|
|
182
233
|
}
|
|
183
234
|
|
|
235
|
+
// Parse latencyMode per W3C WebCodecs spec.
|
|
236
|
+
// "realtime" = disable B-frames for low latency (no reordering)
|
|
237
|
+
// "quality" = allow B-frames for better compression (default)
|
|
238
|
+
std::string latency_mode =
|
|
239
|
+
webcodecs::AttrAsStr(config, "latencyMode", "quality");
|
|
240
|
+
|
|
241
|
+
// Store config for codec reinitialization after flush.
|
|
242
|
+
// FFmpeg encoders enter EOF mode after sending NULL frame, requiring
|
|
243
|
+
// full reinitialization to continue encoding per W3C WebCodecs spec.
|
|
244
|
+
bitrate_ = bitrate;
|
|
245
|
+
framerate_ = framerate;
|
|
246
|
+
max_b_frames_ = (latency_mode == "realtime") ? 0 : kDefaultMaxBFrames;
|
|
247
|
+
use_qscale_ = (bitrate_mode == "quantizer");
|
|
248
|
+
|
|
184
249
|
// Parse codec-specific bitstream format per W3C codec registration.
|
|
185
250
|
// Default to "annexb" for backwards compatibility (FFmpeg's native format).
|
|
186
251
|
// Per W3C spec, the default should be "avc"/"hevc" when explicit config
|
|
@@ -215,7 +280,46 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
|
|
|
215
280
|
throw Napi::Error::New(env, "Unsupported codec: " + codec_str);
|
|
216
281
|
}
|
|
217
282
|
|
|
218
|
-
|
|
283
|
+
// Try hardware encoders first based on platform and hardwareAcceleration
|
|
284
|
+
// setting
|
|
285
|
+
codec_ = nullptr;
|
|
286
|
+
std::string hw_accel =
|
|
287
|
+
webcodecs::AttrAsStr(config, "hardwareAcceleration", "no-preference");
|
|
288
|
+
|
|
289
|
+
if (hw_accel != "prefer-software") {
|
|
290
|
+
#ifdef __APPLE__
|
|
291
|
+
if (codec_id == AV_CODEC_ID_H264) {
|
|
292
|
+
codec_ = avcodec_find_encoder_by_name("h264_videotoolbox");
|
|
293
|
+
} else if (codec_id == AV_CODEC_ID_HEVC) {
|
|
294
|
+
codec_ = avcodec_find_encoder_by_name("hevc_videotoolbox");
|
|
295
|
+
}
|
|
296
|
+
#endif
|
|
297
|
+
#ifdef _WIN32
|
|
298
|
+
if (codec_id == AV_CODEC_ID_H264) {
|
|
299
|
+
codec_ = avcodec_find_encoder_by_name("h264_nvenc");
|
|
300
|
+
if (!codec_) codec_ = avcodec_find_encoder_by_name("h264_qsv");
|
|
301
|
+
if (!codec_) codec_ = avcodec_find_encoder_by_name("h264_amf");
|
|
302
|
+
} else if (codec_id == AV_CODEC_ID_HEVC) {
|
|
303
|
+
codec_ = avcodec_find_encoder_by_name("hevc_nvenc");
|
|
304
|
+
if (!codec_) codec_ = avcodec_find_encoder_by_name("hevc_qsv");
|
|
305
|
+
}
|
|
306
|
+
#endif
|
|
307
|
+
#ifdef __linux__
|
|
308
|
+
if (codec_id == AV_CODEC_ID_H264) {
|
|
309
|
+
codec_ = avcodec_find_encoder_by_name("h264_vaapi");
|
|
310
|
+
if (!codec_) codec_ = avcodec_find_encoder_by_name("h264_nvenc");
|
|
311
|
+
} else if (codec_id == AV_CODEC_ID_HEVC) {
|
|
312
|
+
codec_ = avcodec_find_encoder_by_name("hevc_vaapi");
|
|
313
|
+
if (!codec_) codec_ = avcodec_find_encoder_by_name("hevc_nvenc");
|
|
314
|
+
}
|
|
315
|
+
#endif
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Fallback to software encoder
|
|
319
|
+
if (!codec_) {
|
|
320
|
+
codec_ = avcodec_find_encoder(codec_id);
|
|
321
|
+
}
|
|
322
|
+
|
|
219
323
|
if (!codec_) {
|
|
220
324
|
throw Napi::Error::New(env, "Encoder not found for codec: " + codec_str);
|
|
221
325
|
}
|
|
@@ -231,9 +335,24 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
|
|
|
231
335
|
codec_context_->time_base = {1, framerate};
|
|
232
336
|
codec_context_->framerate = {framerate, 1};
|
|
233
337
|
codec_context_->pix_fmt = AV_PIX_FMT_YUV420P;
|
|
234
|
-
|
|
338
|
+
// When bitrateMode = "quantizer", enable CQP mode so frame->quality is
|
|
339
|
+
// respected. Don't set bit_rate - let quality control encoding.
|
|
340
|
+
if (bitrate_mode == "quantizer") {
|
|
341
|
+
codec_context_->flags |= AV_CODEC_FLAG_QSCALE;
|
|
342
|
+
codec_context_->global_quality = FF_QP2LAMBDA * 23; // Default QP if none specified
|
|
343
|
+
} else {
|
|
344
|
+
codec_context_->bit_rate = bitrate;
|
|
345
|
+
}
|
|
235
346
|
codec_context_->gop_size = kDefaultGopSize;
|
|
236
|
-
|
|
347
|
+
// Per W3C WebCodecs spec: latencyMode "realtime" disables B-frames for low
|
|
348
|
+
// latency encoding (no frame reordering). This is critical for correct MP4
|
|
349
|
+
// muxing as B-frames require proper DTS calculation which isn't available
|
|
350
|
+
// from WebCodecs chunk timestamps.
|
|
351
|
+
if (latency_mode == "realtime") {
|
|
352
|
+
codec_context_->max_b_frames = 0;
|
|
353
|
+
} else {
|
|
354
|
+
codec_context_->max_b_frames = kDefaultMaxBFrames;
|
|
355
|
+
}
|
|
237
356
|
|
|
238
357
|
// Set global header flag for non-annexb bitstream formats.
|
|
239
358
|
// This puts SPS/PPS/VPS in codec_context_->extradata instead of in the
|
|
@@ -243,29 +362,130 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
|
|
|
243
362
|
codec_context_->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
|
|
244
363
|
}
|
|
245
364
|
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
365
|
+
// Detect if this is a hardware encoder (for skipping software-specific
|
|
366
|
+
// options)
|
|
367
|
+
bool is_hw_encoder =
|
|
368
|
+
codec_ && (strstr(codec_->name, "videotoolbox") != nullptr ||
|
|
369
|
+
strstr(codec_->name, "nvenc") != nullptr ||
|
|
370
|
+
strstr(codec_->name, "qsv") != nullptr ||
|
|
371
|
+
strstr(codec_->name, "vaapi") != nullptr ||
|
|
372
|
+
strstr(codec_->name, "amf") != nullptr);
|
|
373
|
+
|
|
374
|
+
// Detect specific software encoder libraries.
|
|
375
|
+
// Different encoders support different options, so we must check the encoder
|
|
376
|
+
// name before setting library-specific options.
|
|
377
|
+
bool is_libx264 = codec_ && strcmp(codec_->name, "libx264") == 0;
|
|
378
|
+
bool is_libx265 = codec_ && strcmp(codec_->name, "libx265") == 0;
|
|
379
|
+
bool is_libvpx =
|
|
380
|
+
codec_ && (strcmp(codec_->name, "libvpx") == 0 ||
|
|
381
|
+
strcmp(codec_->name, "libvpx-vp9") == 0);
|
|
382
|
+
bool is_libaom = codec_ && strcmp(codec_->name, "libaom-av1") == 0;
|
|
383
|
+
bool is_libsvtav1 = codec_ && strcmp(codec_->name, "libsvtav1") == 0;
|
|
384
|
+
|
|
385
|
+
// Codec-specific options (only for software encoders).
|
|
386
|
+
// Hardware encoders have their own internal quality/speed settings.
|
|
387
|
+
// Only set options when the specific encoder library is detected.
|
|
388
|
+
if (!is_hw_encoder) {
|
|
389
|
+
if (codec_id == AV_CODEC_ID_H264 && is_libx264) {
|
|
390
|
+
// libx264-specific options
|
|
391
|
+
av_opt_set(codec_context_->priv_data, "preset", "fast", 0);
|
|
392
|
+
av_opt_set(codec_context_->priv_data, "tune", "zerolatency", 0);
|
|
393
|
+
// For bitrateMode=quantizer, enable CQP mode in libx264.
|
|
394
|
+
// libx264 ignores AV_CODEC_FLAG_QSCALE; it needs the "qp" option set.
|
|
395
|
+
// We set a default QP here; per-frame quality will be applied via
|
|
396
|
+
// frame->quality which libx264 reads when in CQP mode.
|
|
397
|
+
if (bitrate_mode == "quantizer") {
|
|
398
|
+
av_opt_set_int(codec_context_->priv_data, "qp", 23, 0);
|
|
399
|
+
}
|
|
400
|
+
} else if ((codec_id == AV_CODEC_ID_VP8 || codec_id == AV_CODEC_ID_VP9) &&
|
|
401
|
+
is_libvpx) {
|
|
402
|
+
// libvpx-specific options
|
|
403
|
+
av_opt_set(codec_context_->priv_data, "quality", "realtime", 0);
|
|
404
|
+
av_opt_set(codec_context_->priv_data, "speed", "6", 0);
|
|
405
|
+
// VP8/VP9 don't support B-frames
|
|
406
|
+
codec_context_->max_b_frames = 0;
|
|
407
|
+
} else if (codec_id == AV_CODEC_ID_AV1 && is_libaom) {
|
|
408
|
+
// libaom-av1 uses "cpu-used" for speed preset (0-8, higher = faster)
|
|
409
|
+
av_opt_set(codec_context_->priv_data, "cpu-used", "8", 0);
|
|
410
|
+
} else if (codec_id == AV_CODEC_ID_AV1 && is_libsvtav1) {
|
|
411
|
+
// SVT-AV1 uses "preset" for speed (0-13, higher = faster)
|
|
412
|
+
av_opt_set(codec_context_->priv_data, "preset", "8", 0);
|
|
413
|
+
} else if (codec_id == AV_CODEC_ID_HEVC && is_libx265) {
|
|
414
|
+
// libx265-specific options
|
|
415
|
+
av_opt_set(codec_context_->priv_data, "preset", "fast", 0);
|
|
416
|
+
// Note: libx265 tune options are different from libx264 (grain,
|
|
417
|
+
// animation, psnr, ssim) "zerolatency" is not valid for x265, using
|
|
418
|
+
// x265-params instead
|
|
419
|
+
av_opt_set(codec_context_->priv_data, "x265-params", "bframes=0", 0);
|
|
420
|
+
}
|
|
421
|
+
// For unrecognized encoders, skip library-specific options entirely.
|
|
422
|
+
// The encoder will use its default settings.
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Hardware encoder-specific options.
|
|
426
|
+
if (is_hw_encoder) {
|
|
427
|
+
// VideoToolbox: allow software fallback when hardware encoding is unavailable
|
|
428
|
+
// (e.g., in CI/headless environments without GPU access)
|
|
429
|
+
if (strstr(codec_->name, "videotoolbox") != nullptr) {
|
|
430
|
+
av_opt_set(codec_context_->priv_data, "allow_sw", "1", 0);
|
|
431
|
+
}
|
|
266
432
|
}
|
|
267
433
|
|
|
268
434
|
int ret = avcodec_open2(codec_context_.get(), codec_, nullptr);
|
|
435
|
+
|
|
436
|
+
// If hardware encoder failed, fall back to software encoder.
|
|
437
|
+
// Per W3C spec, "prefer-hardware" still allows software fallback.
|
|
438
|
+
if (ret < 0 && is_hw_encoder) {
|
|
439
|
+
// Reset the failed hardware encoder context
|
|
440
|
+
codec_context_.reset();
|
|
441
|
+
|
|
442
|
+
// Find software encoder as fallback
|
|
443
|
+
codec_ = avcodec_find_encoder(codec_id);
|
|
444
|
+
if (codec_) {
|
|
445
|
+
codec_context_ = ffmpeg::make_codec_context(codec_);
|
|
446
|
+
if (codec_context_) {
|
|
447
|
+
// Reconfigure encoder for software
|
|
448
|
+
codec_context_->width = width_;
|
|
449
|
+
codec_context_->height = height_;
|
|
450
|
+
codec_context_->time_base = {1, framerate};
|
|
451
|
+
codec_context_->framerate = {framerate, 1};
|
|
452
|
+
codec_context_->pix_fmt = AV_PIX_FMT_YUV420P;
|
|
453
|
+
if (bitrate_mode == "quantizer") {
|
|
454
|
+
codec_context_->flags |= AV_CODEC_FLAG_QSCALE;
|
|
455
|
+
codec_context_->global_quality = FF_QP2LAMBDA * 23;
|
|
456
|
+
} else {
|
|
457
|
+
codec_context_->bit_rate = bitrate;
|
|
458
|
+
}
|
|
459
|
+
codec_context_->gop_size = kDefaultGopSize;
|
|
460
|
+
if (latency_mode == "realtime") {
|
|
461
|
+
codec_context_->max_b_frames = 0;
|
|
462
|
+
} else {
|
|
463
|
+
codec_context_->max_b_frames = kDefaultMaxBFrames;
|
|
464
|
+
}
|
|
465
|
+
if (bitstream_format_ != "annexb") {
|
|
466
|
+
codec_context_->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Set software encoder-specific options
|
|
470
|
+
if (codec_id == AV_CODEC_ID_H264 &&
|
|
471
|
+
strcmp(codec_->name, "libx264") == 0) {
|
|
472
|
+
av_opt_set(codec_context_->priv_data, "preset", "fast", 0);
|
|
473
|
+
av_opt_set(codec_context_->priv_data, "tune", "zerolatency", 0);
|
|
474
|
+
if (bitrate_mode == "quantizer") {
|
|
475
|
+
av_opt_set_int(codec_context_->priv_data, "qp", 23, 0);
|
|
476
|
+
}
|
|
477
|
+
} else if (codec_id == AV_CODEC_ID_HEVC &&
|
|
478
|
+
strcmp(codec_->name, "libx265") == 0) {
|
|
479
|
+
av_opt_set(codec_context_->priv_data, "preset", "fast", 0);
|
|
480
|
+
av_opt_set(codec_context_->priv_data, "x265-params", "bframes=0", 0);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Try opening software encoder
|
|
484
|
+
ret = avcodec_open2(codec_context_.get(), codec_, nullptr);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
269
489
|
if (ret < 0) {
|
|
270
490
|
char errbuf[256];
|
|
271
491
|
av_strerror(ret, errbuf, sizeof(errbuf));
|
|
@@ -278,7 +498,11 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
|
|
|
278
498
|
frame_->format = codec_context_->pix_fmt;
|
|
279
499
|
frame_->width = width_;
|
|
280
500
|
frame_->height = height_;
|
|
281
|
-
av_frame_get_buffer(frame_.get(), kFrameBufferAlignment);
|
|
501
|
+
ret = av_frame_get_buffer(frame_.get(), kFrameBufferAlignment);
|
|
502
|
+
if (ret < 0) {
|
|
503
|
+
Cleanup();
|
|
504
|
+
throw Napi::Error::New(env, "Failed to allocate frame buffer");
|
|
505
|
+
}
|
|
282
506
|
|
|
283
507
|
packet_ = ffmpeg::make_packet();
|
|
284
508
|
|
|
@@ -353,6 +577,24 @@ Napi::Value VideoEncoder::Encode(const Napi::CallbackInfo& info) {
|
|
|
353
577
|
throw Napi::Error::New(env, "Encoder not configured");
|
|
354
578
|
}
|
|
355
579
|
|
|
580
|
+
// SAFETY VALVE: Reject if queue is too large.
|
|
581
|
+
// This prevents OOM if the consumer ignores backpressure.
|
|
582
|
+
// For async mode, check the async worker's queue; for sync mode, use encode_queue_size_.
|
|
583
|
+
if (async_mode_ && async_worker_) {
|
|
584
|
+
size_t async_queue = async_worker_->QueueSize() + async_worker_->GetPendingChunks();
|
|
585
|
+
if (async_queue >= kMaxHardQueueSize) {
|
|
586
|
+
throw Napi::Error::New(
|
|
587
|
+
env,
|
|
588
|
+
"QuotaExceededError: Encode queue is full. You must handle backpressure "
|
|
589
|
+
"by waiting for encodeQueueSize to decrease.");
|
|
590
|
+
}
|
|
591
|
+
} else if (encode_queue_size_ >= static_cast<int>(kMaxHardQueueSize)) {
|
|
592
|
+
throw Napi::Error::New(
|
|
593
|
+
env,
|
|
594
|
+
"QuotaExceededError: Encode queue is full. You must handle backpressure "
|
|
595
|
+
"by waiting for encodeQueueSize to decrease.");
|
|
596
|
+
}
|
|
597
|
+
|
|
356
598
|
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
357
599
|
throw Napi::Error::New(env, "encode requires VideoFrame");
|
|
358
600
|
}
|
|
@@ -430,7 +672,9 @@ Napi::Value VideoEncoder::Encode(const Napi::CallbackInfo& info) {
|
|
|
430
672
|
task.rgba_data.resize(data_size);
|
|
431
673
|
std::memcpy(task.rgba_data.data(), video_frame->GetData(), data_size);
|
|
432
674
|
|
|
433
|
-
encode_queue_size_
|
|
675
|
+
// Note: Don't increment encode_queue_size_ for async path.
|
|
676
|
+
// The hard limit check uses async_worker_->QueueSize() + GetPendingChunks() instead.
|
|
677
|
+
webcodecs::counterQueue++; // Global queue tracking
|
|
434
678
|
async_worker_->Enqueue(std::move(task));
|
|
435
679
|
|
|
436
680
|
return env.Undefined();
|
|
@@ -513,6 +757,89 @@ Napi::Value VideoEncoder::Encode(const Napi::CallbackInfo& info) {
|
|
|
513
757
|
return env.Undefined();
|
|
514
758
|
}
|
|
515
759
|
|
|
760
|
+
void VideoEncoder::ReinitializeCodec() {
|
|
761
|
+
// Reinitialize codec context after flush.
|
|
762
|
+
// FFmpeg encoders enter EOF mode after sending NULL frame and cannot
|
|
763
|
+
// accept new input without full reinitialization.
|
|
764
|
+
// This is required for W3C WebCodecs compliance where flush() should
|
|
765
|
+
// allow continued encoding.
|
|
766
|
+
|
|
767
|
+
if (!codec_) {
|
|
768
|
+
return; // No codec configured
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Close old codec context (RAII handles cleanup via reset)
|
|
772
|
+
codec_context_.reset();
|
|
773
|
+
sws_context_.reset();
|
|
774
|
+
|
|
775
|
+
// Recreate codec context with same settings
|
|
776
|
+
codec_context_ = ffmpeg::make_codec_context(codec_);
|
|
777
|
+
if (!codec_context_) {
|
|
778
|
+
return; // Failed to allocate - will be detected on next encode
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Reconfigure encoder with stored settings
|
|
782
|
+
codec_context_->width = width_;
|
|
783
|
+
codec_context_->height = height_;
|
|
784
|
+
codec_context_->time_base = {1, framerate_};
|
|
785
|
+
codec_context_->framerate = {framerate_, 1};
|
|
786
|
+
codec_context_->pix_fmt = AV_PIX_FMT_YUV420P;
|
|
787
|
+
codec_context_->gop_size = kDefaultGopSize;
|
|
788
|
+
codec_context_->max_b_frames = max_b_frames_;
|
|
789
|
+
|
|
790
|
+
if (use_qscale_) {
|
|
791
|
+
codec_context_->flags |= AV_CODEC_FLAG_QSCALE;
|
|
792
|
+
codec_context_->global_quality = FF_QP2LAMBDA * 23;
|
|
793
|
+
} else {
|
|
794
|
+
codec_context_->bit_rate = bitrate_;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (bitstream_format_ != "annexb") {
|
|
798
|
+
codec_context_->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Hardware encoder-specific options
|
|
802
|
+
if (codec_ && strstr(codec_->name, "videotoolbox") != nullptr) {
|
|
803
|
+
av_opt_set(codec_context_->priv_data, "allow_sw", "1", 0);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Software encoder-specific options
|
|
807
|
+
bool is_libx264 = codec_ && strcmp(codec_->name, "libx264") == 0;
|
|
808
|
+
if (is_libx264) {
|
|
809
|
+
av_opt_set(codec_context_->priv_data, "preset", "fast", 0);
|
|
810
|
+
av_opt_set(codec_context_->priv_data, "tune", "zerolatency", 0);
|
|
811
|
+
if (use_qscale_) {
|
|
812
|
+
av_opt_set_int(codec_context_->priv_data, "qp", 23, 0);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Open codec
|
|
817
|
+
int ret = avcodec_open2(codec_context_.get(), codec_, nullptr);
|
|
818
|
+
if (ret < 0) {
|
|
819
|
+
codec_context_.reset();
|
|
820
|
+
return; // Failed to open - will be detected on next encode
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Recreate sws context for RGBA to YUV conversion
|
|
824
|
+
sws_context_.reset(sws_getContext(width_, height_, AV_PIX_FMT_RGBA, width_,
|
|
825
|
+
height_, AV_PIX_FMT_YUV420P, SWS_BILINEAR,
|
|
826
|
+
nullptr, nullptr, nullptr));
|
|
827
|
+
|
|
828
|
+
// Recreate frame and packet for sync mode
|
|
829
|
+
if (!async_mode_) {
|
|
830
|
+
frame_ = ffmpeg::make_frame();
|
|
831
|
+
if (frame_) {
|
|
832
|
+
frame_->format = AV_PIX_FMT_YUV420P;
|
|
833
|
+
frame_->width = width_;
|
|
834
|
+
frame_->height = height_;
|
|
835
|
+
av_frame_get_buffer(frame_.get(), kFrameBufferAlignment);
|
|
836
|
+
}
|
|
837
|
+
packet_ = ffmpeg::make_packet();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Note: frame_count_ is NOT reset - continue numbering from before flush
|
|
841
|
+
}
|
|
842
|
+
|
|
516
843
|
Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
|
|
517
844
|
Napi::Env env = info.Env();
|
|
518
845
|
|
|
@@ -523,6 +850,13 @@ Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
|
|
|
523
850
|
if (async_mode_ && async_worker_) {
|
|
524
851
|
// Wait for async worker to drain its queue
|
|
525
852
|
async_worker_->Flush();
|
|
853
|
+
// Reinitialize codec after drain (FFmpeg enters EOF mode after NULL frame)
|
|
854
|
+
ReinitializeCodec();
|
|
855
|
+
if (async_worker_) {
|
|
856
|
+
// Update worker with new codec context and sws context
|
|
857
|
+
async_worker_->SetCodecContext(codec_context_.get(), sws_context_.get(),
|
|
858
|
+
width_, height_);
|
|
859
|
+
}
|
|
526
860
|
// Reset queue after async flush completes
|
|
527
861
|
encode_queue_size_ = 0;
|
|
528
862
|
codec_saturated_.store(false);
|
|
@@ -535,6 +869,9 @@ Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
|
|
|
535
869
|
// Get remaining packets.
|
|
536
870
|
EmitChunks(env);
|
|
537
871
|
|
|
872
|
+
// Reinitialize codec after drain (FFmpeg enters EOF mode after NULL frame)
|
|
873
|
+
ReinitializeCodec();
|
|
874
|
+
|
|
538
875
|
// Reset queue after flush
|
|
539
876
|
encode_queue_size_ = 0;
|
|
540
877
|
codec_saturated_.store(false);
|
|
@@ -545,13 +882,29 @@ Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
|
|
|
545
882
|
Napi::Value VideoEncoder::Reset(const Napi::CallbackInfo& info) {
|
|
546
883
|
Napi::Env env = info.Env();
|
|
547
884
|
|
|
885
|
+
// W3C spec: reset() is a no-op when closed (don't throw)
|
|
548
886
|
if (state_ == "closed") {
|
|
549
|
-
|
|
550
|
-
|
|
887
|
+
return env.Undefined();
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// CRITICAL: Stop async worker FIRST before accessing codec_context_.
|
|
891
|
+
// The worker thread may be calling avcodec_send_frame() or avcodec_receive_packet()
|
|
892
|
+
// concurrently. AVCodecContext is NOT thread-safe - concurrent access causes SIGSEGV.
|
|
893
|
+
if (async_worker_) {
|
|
894
|
+
async_worker_->Stop();
|
|
895
|
+
async_worker_.reset();
|
|
551
896
|
}
|
|
552
897
|
|
|
553
|
-
//
|
|
554
|
-
if (
|
|
898
|
+
// Release ThreadSafeFunctions after worker is stopped.
|
|
899
|
+
if (async_mode_) {
|
|
900
|
+
output_tsfn_.Release();
|
|
901
|
+
error_tsfn_.Release();
|
|
902
|
+
async_mode_ = false;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Now safe to flush codec - worker is stopped.
|
|
906
|
+
// Drain encoder's internal buffers, discard any remaining encoded packets.
|
|
907
|
+
if (codec_context_ && packet_) {
|
|
555
908
|
avcodec_send_frame(codec_context_.get(), nullptr);
|
|
556
909
|
while (avcodec_receive_packet(codec_context_.get(), packet_.get()) == 0) {
|
|
557
910
|
av_packet_unref(packet_.get());
|
|
@@ -559,7 +912,11 @@ Napi::Value VideoEncoder::Reset(const Napi::CallbackInfo& info) {
|
|
|
559
912
|
}
|
|
560
913
|
|
|
561
914
|
// Clean up FFmpeg resources.
|
|
562
|
-
|
|
915
|
+
frame_.reset();
|
|
916
|
+
packet_.reset();
|
|
917
|
+
sws_context_.reset();
|
|
918
|
+
codec_context_.reset();
|
|
919
|
+
codec_ = nullptr;
|
|
563
920
|
|
|
564
921
|
// Reset state.
|
|
565
922
|
state_ = "unconfigured";
|
package/src/video_encoder.h
CHANGED
|
@@ -51,6 +51,7 @@ class VideoEncoder : public Napi::ObjectWrap<VideoEncoder> {
|
|
|
51
51
|
// Internal helpers.
|
|
52
52
|
void Cleanup();
|
|
53
53
|
void EmitChunks(Napi::Env env);
|
|
54
|
+
void ReinitializeCodec(); // Recreates codec context after flush
|
|
54
55
|
|
|
55
56
|
// FFmpeg state.
|
|
56
57
|
const AVCodec*
|
|
@@ -81,10 +82,24 @@ class VideoEncoder : public Napi::ObjectWrap<VideoEncoder> {
|
|
|
81
82
|
// "annexb": Description embedded in bitstream (default for backwards compat)
|
|
82
83
|
std::string bitstream_format_;
|
|
83
84
|
int64_t frame_count_;
|
|
85
|
+
|
|
86
|
+
// Stored configuration for codec reinitialization after flush.
|
|
87
|
+
// FFmpeg encoders enter EOF mode after flush (sending NULL frame),
|
|
88
|
+
// requiring full reinitialization to accept new frames per W3C spec.
|
|
89
|
+
int bitrate_;
|
|
90
|
+
int framerate_;
|
|
91
|
+
int max_b_frames_;
|
|
92
|
+
bool use_qscale_;
|
|
84
93
|
int encode_queue_size_;
|
|
85
94
|
std::atomic<bool> codec_saturated_{false};
|
|
86
95
|
static constexpr size_t kMaxQueueSize = 16; // Saturation threshold
|
|
87
96
|
|
|
97
|
+
// HARD LIMIT: The "Circuit Breaker".
|
|
98
|
+
// If the user ignores backpressure signals and keeps pushing frames,
|
|
99
|
+
// we reject requests to prevent OOM.
|
|
100
|
+
// 64 frames @ 4K RGBA (3840x2160x4) is ~2GB of RAM.
|
|
101
|
+
static constexpr size_t kMaxHardQueueSize = 64;
|
|
102
|
+
|
|
88
103
|
// Saturation status accessor
|
|
89
104
|
bool IsCodecSaturated() const { return codec_saturated_.load(); }
|
|
90
105
|
|
package/src/video_filter.cc
CHANGED
|
@@ -295,22 +295,33 @@ Napi::Value VideoFilter::ApplyBlur(const Napi::CallbackInfo& info) {
|
|
|
295
295
|
return env.Undefined();
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
// Parse and link filter graph
|
|
299
|
-
|
|
300
|
-
|
|
298
|
+
// Parse and link filter graph (RAII for initial allocation safety)
|
|
299
|
+
ffmpeg::AVFilterInOutPtr outputs_raii = ffmpeg::make_filter_inout();
|
|
300
|
+
ffmpeg::AVFilterInOutPtr inputs_raii = ffmpeg::make_filter_inout();
|
|
301
301
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
if (!outputs_raii || !inputs_raii) {
|
|
303
|
+
Napi::Error::New(env, "Failed to allocate filter inout")
|
|
304
|
+
.ThrowAsJavaScriptException();
|
|
305
|
+
return env.Undefined();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
outputs_raii->name = av_strdup("in");
|
|
309
|
+
outputs_raii->filter_ctx = buffersrc_ctx_;
|
|
310
|
+
outputs_raii->pad_idx = 0;
|
|
311
|
+
outputs_raii->next = nullptr;
|
|
312
|
+
|
|
313
|
+
inputs_raii->name = av_strdup("out");
|
|
314
|
+
inputs_raii->filter_ctx = buffersink_ctx_;
|
|
315
|
+
inputs_raii->pad_idx = 0;
|
|
316
|
+
inputs_raii->next = nullptr;
|
|
306
317
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
inputs
|
|
310
|
-
inputs->next = nullptr;
|
|
318
|
+
// Release ownership before parse_ptr (it may modify/consume the pointers)
|
|
319
|
+
AVFilterInOut* outputs = outputs_raii.release();
|
|
320
|
+
AVFilterInOut* inputs = inputs_raii.release();
|
|
311
321
|
|
|
312
322
|
ret = avfilter_graph_parse_ptr(filter_graph_.get(), filter_str.c_str(),
|
|
313
323
|
&inputs, &outputs, nullptr);
|
|
324
|
+
// parse_ptr may have modified pointers, free remaining
|
|
314
325
|
avfilter_inout_free(&inputs);
|
|
315
326
|
avfilter_inout_free(&outputs);
|
|
316
327
|
|