@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
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"
|
|
@@ -14,6 +17,24 @@ namespace {
|
|
|
14
17
|
|
|
15
18
|
// Encoder configuration constants.
|
|
16
19
|
constexpr int kDefaultBitrate = 1000000; // 1 Mbps
|
|
20
|
+
constexpr int kDefaultTemporalLayers = 1;
|
|
21
|
+
|
|
22
|
+
// Compute temporal layer ID based on frame position and layer count.
|
|
23
|
+
// Uses standard WebRTC temporal layering pattern.
|
|
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
|
+
}
|
|
17
38
|
constexpr int kDefaultFramerate = 30; // 30 fps
|
|
18
39
|
constexpr int kDefaultGopSize = 30; // Keyframe interval
|
|
19
40
|
constexpr int kDefaultMaxBFrames = 2;
|
|
@@ -66,6 +87,9 @@ VideoEncoder::VideoEncoder(const Napi::CallbackInfo& info)
|
|
|
66
87
|
bitstream_format_("annexb"),
|
|
67
88
|
frame_count_(0),
|
|
68
89
|
encode_queue_size_(0) {
|
|
90
|
+
// Track active encoder instance (following sharp pattern)
|
|
91
|
+
webcodecs::counterProcess++;
|
|
92
|
+
webcodecs::counterVideoEncoders++;
|
|
69
93
|
Napi::Env env = info.Env();
|
|
70
94
|
|
|
71
95
|
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
@@ -88,20 +112,59 @@ VideoEncoder::VideoEncoder(const Napi::CallbackInfo& info)
|
|
|
88
112
|
error_callback_ = Napi::Persistent(init.Get("error").As<Napi::Function>());
|
|
89
113
|
}
|
|
90
114
|
|
|
91
|
-
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
|
+
}
|
|
92
129
|
|
|
93
130
|
void VideoEncoder::Cleanup() {
|
|
94
131
|
if (async_worker_) {
|
|
132
|
+
// Stop() joins the worker thread - after this, no new TSFN calls will be made
|
|
95
133
|
async_worker_->Stop();
|
|
96
|
-
|
|
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
|
+
}
|
|
97
145
|
}
|
|
98
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.
|
|
99
150
|
if (async_mode_) {
|
|
100
151
|
output_tsfn_.Release();
|
|
101
152
|
error_tsfn_.Release();
|
|
102
153
|
async_mode_ = false;
|
|
103
154
|
}
|
|
104
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
|
+
|
|
105
168
|
frame_.reset();
|
|
106
169
|
packet_.reset();
|
|
107
170
|
sws_context_.reset();
|
|
@@ -130,6 +193,12 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
|
|
|
130
193
|
int framerate =
|
|
131
194
|
webcodecs::AttrAsInt32(config, "framerate", kDefaultFramerate);
|
|
132
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
|
+
|
|
133
202
|
// Parse codec string
|
|
134
203
|
std::string codec_str = webcodecs::AttrAsStr(config, "codec", "h264");
|
|
135
204
|
codec_string_ = codec_str; // Store for metadata
|
|
@@ -148,6 +217,35 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
|
|
|
148
217
|
color_full_range_ = webcodecs::AttrAsBool(cs, "fullRange", false);
|
|
149
218
|
}
|
|
150
219
|
|
|
220
|
+
// Parse scalabilityMode to determine temporal layer count.
|
|
221
|
+
// Format: L{spatial}T{temporal}, e.g., "L1T2", "L1T3", "L2T2"
|
|
222
|
+
temporal_layer_count_ = kDefaultTemporalLayers;
|
|
223
|
+
std::string scalability_mode =
|
|
224
|
+
webcodecs::AttrAsStr(config, "scalabilityMode", "");
|
|
225
|
+
if (!scalability_mode.empty()) {
|
|
226
|
+
size_t t_pos = scalability_mode.find('T');
|
|
227
|
+
if (t_pos != std::string::npos && t_pos + 1 < scalability_mode.size()) {
|
|
228
|
+
int t_count = scalability_mode[t_pos + 1] - '0';
|
|
229
|
+
if (t_count >= 1 && t_count <= 3) {
|
|
230
|
+
temporal_layer_count_ = t_count;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
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
|
+
|
|
151
249
|
// Parse codec-specific bitstream format per W3C codec registration.
|
|
152
250
|
// Default to "annexb" for backwards compatibility (FFmpeg's native format).
|
|
153
251
|
// Per W3C spec, the default should be "avc"/"hevc" when explicit config
|
|
@@ -182,7 +280,46 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
|
|
|
182
280
|
throw Napi::Error::New(env, "Unsupported codec: " + codec_str);
|
|
183
281
|
}
|
|
184
282
|
|
|
185
|
-
|
|
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
|
+
|
|
186
323
|
if (!codec_) {
|
|
187
324
|
throw Napi::Error::New(env, "Encoder not found for codec: " + codec_str);
|
|
188
325
|
}
|
|
@@ -198,9 +335,24 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
|
|
|
198
335
|
codec_context_->time_base = {1, framerate};
|
|
199
336
|
codec_context_->framerate = {framerate, 1};
|
|
200
337
|
codec_context_->pix_fmt = AV_PIX_FMT_YUV420P;
|
|
201
|
-
|
|
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
|
+
}
|
|
202
346
|
codec_context_->gop_size = kDefaultGopSize;
|
|
203
|
-
|
|
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
|
+
}
|
|
204
356
|
|
|
205
357
|
// Set global header flag for non-annexb bitstream formats.
|
|
206
358
|
// This puts SPS/PPS/VPS in codec_context_->extradata instead of in the
|
|
@@ -210,29 +362,130 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
|
|
|
210
362
|
codec_context_->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
|
|
211
363
|
}
|
|
212
364
|
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
}
|
|
233
432
|
}
|
|
234
433
|
|
|
235
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
|
+
|
|
236
489
|
if (ret < 0) {
|
|
237
490
|
char errbuf[256];
|
|
238
491
|
av_strerror(ret, errbuf, sizeof(errbuf));
|
|
@@ -245,7 +498,11 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
|
|
|
245
498
|
frame_->format = codec_context_->pix_fmt;
|
|
246
499
|
frame_->width = width_;
|
|
247
500
|
frame_->height = height_;
|
|
248
|
-
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
|
+
}
|
|
249
506
|
|
|
250
507
|
packet_ = ffmpeg::make_packet();
|
|
251
508
|
|
|
@@ -286,6 +543,7 @@ Napi::Value VideoEncoder::Configure(const Napi::CallbackInfo& info) {
|
|
|
286
543
|
metadata_config.color_transfer = color_transfer_;
|
|
287
544
|
metadata_config.color_matrix = color_matrix_;
|
|
288
545
|
metadata_config.color_full_range = color_full_range_;
|
|
546
|
+
metadata_config.temporal_layer_count = temporal_layer_count_;
|
|
289
547
|
async_worker_->SetMetadataConfig(metadata_config);
|
|
290
548
|
|
|
291
549
|
async_worker_->Start();
|
|
@@ -319,6 +577,24 @@ Napi::Value VideoEncoder::Encode(const Napi::CallbackInfo& info) {
|
|
|
319
577
|
throw Napi::Error::New(env, "Encoder not configured");
|
|
320
578
|
}
|
|
321
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
|
+
|
|
322
598
|
if (info.Length() < 1 || !info[0].IsObject()) {
|
|
323
599
|
throw Napi::Error::New(env, "encode requires VideoFrame");
|
|
324
600
|
}
|
|
@@ -389,13 +665,16 @@ Napi::Value VideoEncoder::Encode(const Napi::CallbackInfo& info) {
|
|
|
389
665
|
task.duration = video_frame->GetDurationValue();
|
|
390
666
|
task.key_frame = force_key_frame;
|
|
391
667
|
task.quantizer = quantizer;
|
|
668
|
+
task.frame_index = frame_count_++;
|
|
392
669
|
|
|
393
670
|
// Get RGBA data from frame
|
|
394
671
|
size_t data_size = task.width * task.height * 4;
|
|
395
672
|
task.rgba_data.resize(data_size);
|
|
396
673
|
std::memcpy(task.rgba_data.data(), video_frame->GetData(), data_size);
|
|
397
674
|
|
|
398
|
-
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
|
|
399
678
|
async_worker_->Enqueue(std::move(task));
|
|
400
679
|
|
|
401
680
|
return env.Undefined();
|
|
@@ -478,6 +757,89 @@ Napi::Value VideoEncoder::Encode(const Napi::CallbackInfo& info) {
|
|
|
478
757
|
return env.Undefined();
|
|
479
758
|
}
|
|
480
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
|
+
|
|
481
843
|
Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
|
|
482
844
|
Napi::Env env = info.Env();
|
|
483
845
|
|
|
@@ -488,6 +850,13 @@ Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
|
|
|
488
850
|
if (async_mode_ && async_worker_) {
|
|
489
851
|
// Wait for async worker to drain its queue
|
|
490
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
|
+
}
|
|
491
860
|
// Reset queue after async flush completes
|
|
492
861
|
encode_queue_size_ = 0;
|
|
493
862
|
codec_saturated_.store(false);
|
|
@@ -500,6 +869,9 @@ Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
|
|
|
500
869
|
// Get remaining packets.
|
|
501
870
|
EmitChunks(env);
|
|
502
871
|
|
|
872
|
+
// Reinitialize codec after drain (FFmpeg enters EOF mode after NULL frame)
|
|
873
|
+
ReinitializeCodec();
|
|
874
|
+
|
|
503
875
|
// Reset queue after flush
|
|
504
876
|
encode_queue_size_ = 0;
|
|
505
877
|
codec_saturated_.store(false);
|
|
@@ -510,13 +882,29 @@ Napi::Value VideoEncoder::Flush(const Napi::CallbackInfo& info) {
|
|
|
510
882
|
Napi::Value VideoEncoder::Reset(const Napi::CallbackInfo& info) {
|
|
511
883
|
Napi::Env env = info.Env();
|
|
512
884
|
|
|
885
|
+
// W3C spec: reset() is a no-op when closed (don't throw)
|
|
513
886
|
if (state_ == "closed") {
|
|
514
|
-
|
|
515
|
-
|
|
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();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Release ThreadSafeFunctions after worker is stopped.
|
|
899
|
+
if (async_mode_) {
|
|
900
|
+
output_tsfn_.Release();
|
|
901
|
+
error_tsfn_.Release();
|
|
902
|
+
async_mode_ = false;
|
|
516
903
|
}
|
|
517
904
|
|
|
518
|
-
//
|
|
519
|
-
|
|
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_) {
|
|
520
908
|
avcodec_send_frame(codec_context_.get(), nullptr);
|
|
521
909
|
while (avcodec_receive_packet(codec_context_.get(), packet_.get()) == 0) {
|
|
522
910
|
av_packet_unref(packet_.get());
|
|
@@ -524,7 +912,11 @@ Napi::Value VideoEncoder::Reset(const Napi::CallbackInfo& info) {
|
|
|
524
912
|
}
|
|
525
913
|
|
|
526
914
|
// Clean up FFmpeg resources.
|
|
527
|
-
|
|
915
|
+
frame_.reset();
|
|
916
|
+
packet_.reset();
|
|
917
|
+
sws_context_.reset();
|
|
918
|
+
codec_context_.reset();
|
|
919
|
+
codec_ = nullptr;
|
|
528
920
|
|
|
529
921
|
// Reset state.
|
|
530
922
|
state_ = "unconfigured";
|
|
@@ -568,10 +960,11 @@ void VideoEncoder::EmitChunks(Napi::Env env) {
|
|
|
568
960
|
Napi::Object metadata = Napi::Object::New(env);
|
|
569
961
|
|
|
570
962
|
// Add SVC metadata per W3C spec.
|
|
571
|
-
//
|
|
572
|
-
// now, always report layer 0 (base layer).
|
|
963
|
+
// Compute temporal layer ID based on frame position and scalabilityMode.
|
|
573
964
|
Napi::Object svc = Napi::Object::New(env);
|
|
574
|
-
|
|
965
|
+
int temporal_layer =
|
|
966
|
+
ComputeTemporalLayerId(packet_->pts, temporal_layer_count_);
|
|
967
|
+
svc.Set("temporalLayerId", Napi::Number::New(env, temporal_layer));
|
|
575
968
|
metadata.Set("svc", svc);
|
|
576
969
|
|
|
577
970
|
// Add decoderConfig for keyframes per W3C spec.
|
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*
|
|
@@ -75,15 +76,30 @@ class VideoEncoder : public Napi::ObjectWrap<VideoEncoder> {
|
|
|
75
76
|
std::string color_transfer_;
|
|
76
77
|
std::string color_matrix_;
|
|
77
78
|
bool color_full_range_;
|
|
79
|
+
int temporal_layer_count_;
|
|
78
80
|
// Bitstream format for AVC/HEVC (per W3C codec registration).
|
|
79
81
|
// "avc"/"hevc": Description (SPS/PPS) provided separately
|
|
80
82
|
// "annexb": Description embedded in bitstream (default for backwards compat)
|
|
81
83
|
std::string bitstream_format_;
|
|
82
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_;
|
|
83
93
|
int encode_queue_size_;
|
|
84
94
|
std::atomic<bool> codec_saturated_{false};
|
|
85
95
|
static constexpr size_t kMaxQueueSize = 16; // Saturation threshold
|
|
86
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
|
+
|
|
87
103
|
// Saturation status accessor
|
|
88
104
|
bool IsCodecSaturated() const { return codec_saturated_.load(); }
|
|
89
105
|
|