@pproenca/node-webcodecs 0.1.1-alpha.0 → 0.1.1-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -233
- package/binding.gyp +123 -0
- package/dist/audio-decoder.js +1 -2
- package/dist/audio-encoder.d.ts +4 -0
- package/dist/audio-encoder.js +28 -2
- package/dist/binding.d.ts +0 -2
- package/dist/binding.js +43 -125
- package/dist/control-message-queue.js +0 -1
- package/dist/demuxer.d.ts +7 -0
- package/dist/demuxer.js +9 -0
- package/dist/encoded-chunks.d.ts +16 -0
- package/dist/encoded-chunks.js +82 -2
- package/dist/image-decoder.js +4 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +3 -1
- package/dist/native-types.d.ts +20 -0
- package/dist/platform.d.ts +1 -10
- package/dist/platform.js +1 -39
- package/dist/resource-manager.d.ts +1 -2
- package/dist/resource-manager.js +3 -17
- package/dist/types.d.ts +12 -0
- package/dist/video-decoder.d.ts +21 -0
- package/dist/video-decoder.js +74 -2
- package/dist/video-encoder.d.ts +22 -0
- package/dist/video-encoder.js +83 -8
- package/lib/audio-decoder.ts +1 -2
- package/lib/audio-encoder.ts +31 -2
- package/lib/binding.ts +45 -104
- package/lib/control-message-queue.ts +0 -1
- package/lib/demuxer.ts +10 -0
- package/lib/encoded-chunks.ts +90 -2
- package/lib/image-decoder.ts +5 -0
- package/lib/index.ts +3 -0
- package/lib/native-types.ts +22 -0
- package/lib/platform.ts +1 -41
- package/lib/resource-manager.ts +3 -19
- package/lib/types.ts +13 -0
- package/lib/video-decoder.ts +84 -2
- package/lib/video-encoder.ts +90 -8
- package/package.json +49 -32
- package/src/addon.cc +57 -0
- package/src/async_decode_worker.cc +241 -33
- package/src/async_decode_worker.h +55 -3
- package/src/async_encode_worker.cc +103 -35
- package/src/async_encode_worker.h +23 -4
- package/src/audio_data.cc +38 -15
- package/src/audio_data.h +1 -0
- package/src/audio_decoder.cc +24 -3
- package/src/audio_encoder.cc +55 -4
- package/src/common.cc +125 -17
- package/src/common.h +34 -4
- package/src/demuxer.cc +16 -2
- package/src/encoded_audio_chunk.cc +10 -0
- package/src/encoded_audio_chunk.h +2 -0
- package/src/encoded_video_chunk.h +1 -0
- package/src/error_builder.cc +0 -4
- package/src/image_decoder.cc +127 -90
- package/src/image_decoder.h +11 -4
- package/src/muxer.cc +1 -0
- package/src/test_video_generator.cc +3 -2
- package/src/video_decoder.cc +169 -19
- package/src/video_decoder.h +9 -11
- package/src/video_encoder.cc +389 -32
- package/src/video_encoder.h +15 -0
- package/src/video_filter.cc +22 -11
- package/src/video_frame.cc +160 -5
- package/src/warnings.cc +0 -4
- package/dist/audio-data.js.map +0 -1
- package/dist/audio-decoder.js.map +0 -1
- package/dist/audio-encoder.js.map +0 -1
- package/dist/binding.js.map +0 -1
- package/dist/codec-base.js.map +0 -1
- package/dist/control-message-queue.js.map +0 -1
- package/dist/demuxer.js.map +0 -1
- package/dist/encoded-chunks.js.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/ffmpeg.d.ts +0 -21
- package/dist/ffmpeg.js +0 -112
- package/dist/image-decoder.js.map +0 -1
- package/dist/image-track-list.js.map +0 -1
- package/dist/image-track.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/is.js.map +0 -1
- package/dist/muxer.js.map +0 -1
- package/dist/native-types.js.map +0 -1
- package/dist/platform.js.map +0 -1
- package/dist/resource-manager.js.map +0 -1
- package/dist/test-video-generator.js.map +0 -1
- package/dist/transfer.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/video-decoder.js.map +0 -1
- package/dist/video-encoder.js.map +0 -1
- package/dist/video-filter.js.map +0 -1
- package/dist/video-frame.js.map +0 -1
- package/install/build.js +0 -51
- package/install/check.js +0 -192
- package/lib/ffmpeg.ts +0 -78
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pproenca/node-webcodecs",
|
|
3
3
|
"description": "WebCodecs API implementation for Node.js using FFmpeg",
|
|
4
|
-
"version": "0.1.1-alpha.
|
|
4
|
+
"version": "0.1.1-alpha.5",
|
|
5
5
|
"author": "Pedro Proenca",
|
|
6
6
|
"homepage": "https://github.com/pproenca/node-webcodecs",
|
|
7
7
|
"repository": {
|
|
@@ -12,29 +12,48 @@
|
|
|
12
12
|
"url": "https://github.com/pproenca/node-webcodecs/issues"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "
|
|
16
|
-
"build:ts": "tsc",
|
|
15
|
+
"build": "node-gyp rebuild && tsc",
|
|
17
16
|
"build:native": "node-gyp rebuild",
|
|
18
|
-
"build:
|
|
19
|
-
"
|
|
17
|
+
"build:ts": "tsc",
|
|
18
|
+
"build:debug": "node-gyp rebuild --debug && tsc",
|
|
19
|
+
"rebuild": "npm run clean && npm run build",
|
|
20
|
+
"install": "node-gyp-build",
|
|
20
21
|
"clean": "rm -rf src/build/ .nyc_output/ coverage/ test/fixtures/output.*",
|
|
21
|
-
"
|
|
22
|
-
"test
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
22
|
+
"check": "npm run lint && npm test",
|
|
23
|
+
"test": "npm run test:fast && npm run test:guardrails",
|
|
24
|
+
"test:fast": "tsx --test --test-concurrency=1 --import ./test/setup.ts --test-timeout=30000 test/golden/*.test.ts test/unit/*.test.ts",
|
|
25
|
+
"test:golden": "tsx --test --test-concurrency=1 --import ./test/setup.ts --test-timeout=30000 test/golden/*.test.ts",
|
|
26
|
+
"test:unit": "tsx --test --test-concurrency=1 --import ./test/setup.ts --test-timeout=30000 test/unit/*.test.ts",
|
|
27
|
+
"test:stress": "tsx --test --test-concurrency=1 --import ./test/setup.ts --test-timeout=60000 test/stress/*.test.ts",
|
|
28
|
+
"test:contracts": "node test/contracts/video_encoder/state_machine.js && node test/contracts/audio_encoder/state_machine.js && node test/contracts/video_decoder/state_machine.js && node test/contracts/audio_decoder/state_machine.js",
|
|
29
|
+
"test:guardrails": "node test/guardrails/fuzzer.js && node test/guardrails/event_loop_lag.js",
|
|
30
|
+
"test:coverage": "c8 --check-coverage --lines 70 --branches 60 --functions 70 --statements 70 --include lib/**/*.ts --exclude lib/**/*.d.ts --exclude lib/types.ts npm run test:fast",
|
|
31
|
+
"lint": "npm run lint:cpp && npm run lint:ts && npm run lint:types && npm run lint:md",
|
|
32
|
+
"lint:cpp": "cpplint --quiet src/*.h src/*.cc",
|
|
33
|
+
"lint:ts": "biome lint",
|
|
34
|
+
"lint:types": "tsd",
|
|
35
|
+
"lint:md": "prettier --check \"**/*.md\"",
|
|
36
|
+
"format": "npm run format:md",
|
|
37
|
+
"format:md": "prettier --write \"**/*.md\"",
|
|
38
|
+
"version:bump": "node scripts/bump-version.js",
|
|
39
|
+
"create-platform-packages": "node scripts/create-platform-packages.mjs"
|
|
29
40
|
},
|
|
30
41
|
"type": "commonjs",
|
|
31
42
|
"main": "dist/index.js",
|
|
32
43
|
"types": "dist/index.d.ts",
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"require": "./dist/index.js",
|
|
48
|
+
"import": "./dist/index.js",
|
|
49
|
+
"default": "./dist/index.js"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
33
52
|
"files": [
|
|
34
|
-
"install",
|
|
35
53
|
"lib",
|
|
36
54
|
"dist",
|
|
37
|
-
"src/*.{cc,h,gyp}"
|
|
55
|
+
"src/*.{cc,h,gyp}",
|
|
56
|
+
"binding.gyp"
|
|
38
57
|
],
|
|
39
58
|
"keywords": [
|
|
40
59
|
"webcodecs",
|
|
@@ -46,35 +65,33 @@
|
|
|
46
65
|
"tsd": {
|
|
47
66
|
"directory": "test/types"
|
|
48
67
|
},
|
|
68
|
+
"optionalDependencies": {
|
|
69
|
+
"@pproenca/node-webcodecs-darwin-arm64": "0.1.1-alpha.5",
|
|
70
|
+
"@pproenca/node-webcodecs-darwin-x64": "0.1.1-alpha.5",
|
|
71
|
+
"@pproenca/node-webcodecs-linux-x64": "0.1.1-alpha.5"
|
|
72
|
+
},
|
|
49
73
|
"dependencies": {
|
|
50
|
-
"detect-libc": "^2.0.3",
|
|
51
74
|
"node-gyp-build": "^4.8.0"
|
|
52
75
|
},
|
|
53
|
-
"optionalDependencies": {
|
|
54
|
-
"@pproenca/node-webcodecs-darwin-arm64": "0.1.0",
|
|
55
|
-
"@pproenca/node-webcodecs-darwin-x64": "0.1.0",
|
|
56
|
-
"@pproenca/node-webcodecs-linux-x64": "0.1.0",
|
|
57
|
-
"@pproenca/node-webcodecs-linuxmusl-x64": "0.1.0",
|
|
58
|
-
"@pproenca/node-webcodecs-win32-x64": "0.1.0"
|
|
59
|
-
},
|
|
60
76
|
"devDependencies": {
|
|
61
77
|
"@biomejs/biome": "^2.3.10",
|
|
62
78
|
"@cpplint/cli": "^0.1.0",
|
|
63
|
-
"@types/node": "
|
|
64
|
-
"c8": "^10.1.
|
|
79
|
+
"@types/node": "^25.0.3",
|
|
80
|
+
"c8": "^10.1.3",
|
|
65
81
|
"express": "^5.2.1",
|
|
66
|
-
"mediabunny": "^1.
|
|
67
|
-
"node-addon-api": "^8.
|
|
68
|
-
"node-gyp": "^
|
|
82
|
+
"mediabunny": "^1.27.3",
|
|
83
|
+
"node-addon-api": "^8.5.0",
|
|
84
|
+
"node-gyp": "^12.1.0",
|
|
85
|
+
"prebuildify": "^6.0.1",
|
|
86
|
+
"prettier": "^3.7.4",
|
|
69
87
|
"tsd": "^0.33.0",
|
|
70
|
-
"tsx": "^4.
|
|
88
|
+
"tsx": "^4.21.0",
|
|
71
89
|
"typedoc": "^0.28.15",
|
|
72
|
-
"typescript": "^5.
|
|
73
|
-
"vitest": "^4.0.15"
|
|
90
|
+
"typescript": "^5.9.3"
|
|
74
91
|
},
|
|
75
92
|
"license": "MIT",
|
|
76
93
|
"engines": {
|
|
77
|
-
"node": "^
|
|
94
|
+
"node": "^20.17.0 || ^22.9.0 || >=24"
|
|
78
95
|
},
|
|
79
96
|
"publishConfig": {
|
|
80
97
|
"access": "public",
|
package/src/addon.cc
CHANGED
|
@@ -41,6 +41,45 @@ void ClearFFmpegWarningsJS(const Napi::CallbackInfo& info) {
|
|
|
41
41
|
webcodecs::ClearFFmpegWarnings();
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Counter accessor functions (following sharp pattern for observability)
|
|
45
|
+
Napi::Value GetCounterQueueJS(const Napi::CallbackInfo& info) {
|
|
46
|
+
return Napi::Number::New(info.Env(), webcodecs::counterQueue.load());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Napi::Value GetCounterProcessJS(const Napi::CallbackInfo& info) {
|
|
50
|
+
return Napi::Number::New(info.Env(), webcodecs::counterProcess.load());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Napi::Value GetCounterFramesJS(const Napi::CallbackInfo& info) {
|
|
54
|
+
return Napi::Number::New(info.Env(), webcodecs::counterFrames.load());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
Napi::Value GetCountersJS(const Napi::CallbackInfo& info) {
|
|
58
|
+
Napi::Env env = info.Env();
|
|
59
|
+
Napi::Object counters = Napi::Object::New(env);
|
|
60
|
+
|
|
61
|
+
// New per-class counters
|
|
62
|
+
counters.Set("videoFrames",
|
|
63
|
+
static_cast<double>(webcodecs::counterVideoFrames.load()));
|
|
64
|
+
counters.Set("audioData",
|
|
65
|
+
static_cast<double>(webcodecs::counterAudioData.load()));
|
|
66
|
+
counters.Set("videoEncoders",
|
|
67
|
+
static_cast<double>(webcodecs::counterVideoEncoders.load()));
|
|
68
|
+
counters.Set("videoDecoders",
|
|
69
|
+
static_cast<double>(webcodecs::counterVideoDecoders.load()));
|
|
70
|
+
counters.Set("audioEncoders",
|
|
71
|
+
static_cast<double>(webcodecs::counterAudioEncoders.load()));
|
|
72
|
+
counters.Set("audioDecoders",
|
|
73
|
+
static_cast<double>(webcodecs::counterAudioDecoders.load()));
|
|
74
|
+
|
|
75
|
+
// Legacy counters (for backwards compatibility)
|
|
76
|
+
counters.Set("queue", webcodecs::counterQueue.load());
|
|
77
|
+
counters.Set("process", webcodecs::counterProcess.load());
|
|
78
|
+
counters.Set("frames", webcodecs::counterFrames.load());
|
|
79
|
+
|
|
80
|
+
return counters;
|
|
81
|
+
}
|
|
82
|
+
|
|
44
83
|
// Test helper for AttrAsEnum template
|
|
45
84
|
Napi::Value TestAttrAsEnum(const Napi::CallbackInfo& info) {
|
|
46
85
|
Napi::Env env = info.Env();
|
|
@@ -54,11 +93,22 @@ Napi::Value TestAttrAsEnum(const Napi::CallbackInfo& info) {
|
|
|
54
93
|
return Napi::String::New(env, webcodecs::ColorPrimariesToString(primaries));
|
|
55
94
|
}
|
|
56
95
|
|
|
96
|
+
// Cleanup hook called when the Node.js environment is being torn down.
|
|
97
|
+
// This prevents the static destruction order fiasco where FFmpeg's log
|
|
98
|
+
// callback might access destroyed static objects during process exit.
|
|
99
|
+
static void CleanupCallback(void* arg) {
|
|
100
|
+
webcodecs::ShutdownFFmpegLogging();
|
|
101
|
+
}
|
|
102
|
+
|
|
57
103
|
Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
|
|
58
104
|
// Thread-safe FFmpeg initialization
|
|
59
105
|
webcodecs::InitFFmpeg();
|
|
60
106
|
webcodecs::InitFFmpegLogging();
|
|
61
107
|
|
|
108
|
+
// Register cleanup hook to disable FFmpeg logging before static destructors.
|
|
109
|
+
// This fixes crashes on macOS x64 where FFmpeg logs during process exit.
|
|
110
|
+
napi_add_env_cleanup_hook(env, CleanupCallback, nullptr);
|
|
111
|
+
|
|
62
112
|
InitVideoEncoder(env, exports);
|
|
63
113
|
InitVideoDecoder(env, exports);
|
|
64
114
|
InitVideoFrame(env, exports);
|
|
@@ -82,6 +132,13 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
|
|
|
82
132
|
exports.Set("clearFFmpegWarnings",
|
|
83
133
|
Napi::Function::New(env, ClearFFmpegWarningsJS));
|
|
84
134
|
|
|
135
|
+
// Export global counter functions (following sharp pattern for observability)
|
|
136
|
+
exports.Set("getCounterQueue", Napi::Function::New(env, GetCounterQueueJS));
|
|
137
|
+
exports.Set("getCounterProcess",
|
|
138
|
+
Napi::Function::New(env, GetCounterProcessJS));
|
|
139
|
+
exports.Set("getCounterFrames", Napi::Function::New(env, GetCounterFramesJS));
|
|
140
|
+
exports.Set("getCounters", Napi::Function::New(env, GetCountersJS));
|
|
141
|
+
|
|
85
142
|
// Export test helpers
|
|
86
143
|
exports.Set("testAttrAsEnum", Napi::Function::New(env, TestAttrAsEnum));
|
|
87
144
|
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
#include "src/async_decode_worker.h"
|
|
7
7
|
|
|
8
|
+
#include <chrono>
|
|
9
|
+
#include <cmath>
|
|
10
|
+
#include <cstdio>
|
|
8
11
|
#include <string>
|
|
9
12
|
#include <utility>
|
|
10
13
|
#include <vector>
|
|
@@ -14,6 +17,7 @@ extern "C" {
|
|
|
14
17
|
#include <libswscale/swscale.h>
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
#include "src/common.h"
|
|
17
21
|
#include "src/video_decoder.h"
|
|
18
22
|
#include "src/video_frame.h"
|
|
19
23
|
|
|
@@ -23,28 +27,44 @@ AsyncDecodeWorker::AsyncDecodeWorker(VideoDecoder* /* decoder */,
|
|
|
23
27
|
: output_tsfn_(output_tsfn),
|
|
24
28
|
error_tsfn_(error_tsfn),
|
|
25
29
|
codec_context_(nullptr),
|
|
26
|
-
sws_context_(nullptr)
|
|
30
|
+
sws_context_(nullptr),
|
|
31
|
+
frame_(nullptr),
|
|
32
|
+
packet_(nullptr),
|
|
33
|
+
output_width_(0),
|
|
34
|
+
output_height_(0) {}
|
|
27
35
|
|
|
28
36
|
AsyncDecodeWorker::~AsyncDecodeWorker() {
|
|
29
37
|
Stop();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
// frame_, packet_, and sws_context_ are RAII-managed, automatically cleaned up
|
|
39
|
+
// Note: codec_context_ is owned by VideoDecoder
|
|
40
|
+
|
|
41
|
+
// Clean up buffer pool
|
|
42
|
+
for (auto* buffer : buffer_pool_) {
|
|
43
|
+
delete buffer;
|
|
35
44
|
}
|
|
36
|
-
|
|
37
|
-
// They are cleaned up there, not here
|
|
45
|
+
buffer_pool_.clear();
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
void AsyncDecodeWorker::SetCodecContext(AVCodecContext* ctx,
|
|
48
|
+
void AsyncDecodeWorker::SetCodecContext(AVCodecContext* ctx,
|
|
49
|
+
SwsContext* /* sws_unused */,
|
|
41
50
|
int width, int height) {
|
|
51
|
+
std::lock_guard<std::mutex> lock(codec_mutex_);
|
|
42
52
|
codec_context_ = ctx;
|
|
43
|
-
sws_context_
|
|
53
|
+
// sws_context_ is created lazily in EmitFrame when we know the frame format
|
|
54
|
+
sws_context_.reset();
|
|
44
55
|
output_width_ = width;
|
|
45
56
|
output_height_ = height;
|
|
46
|
-
frame_ =
|
|
47
|
-
packet_ =
|
|
57
|
+
frame_ = ffmpeg::make_frame();
|
|
58
|
+
packet_ = ffmpeg::make_packet();
|
|
59
|
+
|
|
60
|
+
// DARWIN-X64 FIX: Mark codec as valid only after successful initialization.
|
|
61
|
+
// ProcessPacket checks this flag to avoid accessing codec during shutdown.
|
|
62
|
+
codec_valid_.store(true, std::memory_order_release);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
void AsyncDecodeWorker::SetMetadataConfig(const DecoderMetadataConfig& config) {
|
|
66
|
+
std::lock_guard<std::mutex> lock(codec_mutex_);
|
|
67
|
+
metadata_config_ = config;
|
|
48
68
|
}
|
|
49
69
|
|
|
50
70
|
void AsyncDecodeWorker::Start() {
|
|
@@ -55,9 +75,26 @@ void AsyncDecodeWorker::Start() {
|
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
void AsyncDecodeWorker::Stop() {
|
|
78
|
+
// DARWIN-X64 FIX: Use stop_mutex_ to prevent double-stop race.
|
|
79
|
+
// Cleanup() and destructor may both call Stop().
|
|
80
|
+
std::lock_guard<std::mutex> stop_lock(stop_mutex_);
|
|
81
|
+
|
|
58
82
|
if (!running_.load()) return;
|
|
59
83
|
|
|
60
|
-
|
|
84
|
+
// DARWIN-X64 FIX: Invalidate codec FIRST, before signaling shutdown.
|
|
85
|
+
// This prevents ProcessPacket from accessing codec_context_ during the
|
|
86
|
+
// race window between setting running_=false and the worker thread exiting.
|
|
87
|
+
codec_valid_.store(false, std::memory_order_release);
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
// CRITICAL: Hold mutex while modifying condition predicate to prevent
|
|
91
|
+
// lost wakeup race on x86_64. Without mutex, there's a window where:
|
|
92
|
+
// 1. Worker checks predicate (running_==true), starts entering wait()
|
|
93
|
+
// 2. Main thread sets running_=false, calls notify_all()
|
|
94
|
+
// 3. Worker enters wait() after notification - blocked forever
|
|
95
|
+
std::lock_guard<std::mutex> lock(queue_mutex_);
|
|
96
|
+
running_.store(false, std::memory_order_release);
|
|
97
|
+
}
|
|
61
98
|
queue_cv_.notify_all();
|
|
62
99
|
|
|
63
100
|
if (worker_thread_.joinable()) {
|
|
@@ -74,13 +111,22 @@ void AsyncDecodeWorker::Enqueue(DecodeTask task) {
|
|
|
74
111
|
}
|
|
75
112
|
|
|
76
113
|
void AsyncDecodeWorker::Flush() {
|
|
77
|
-
|
|
114
|
+
// Enqueue a flush task to drain FFmpeg's internal frame buffers
|
|
115
|
+
DecodeTask flush_task;
|
|
116
|
+
flush_task.is_flush = true;
|
|
117
|
+
{
|
|
118
|
+
std::lock_guard<std::mutex> lock(queue_mutex_);
|
|
119
|
+
task_queue_.push(std::move(flush_task));
|
|
120
|
+
}
|
|
78
121
|
queue_cv_.notify_one();
|
|
79
122
|
|
|
80
|
-
|
|
123
|
+
flushing_.store(true);
|
|
124
|
+
|
|
125
|
+
// Wait for queue to drain AND all in-flight processing to complete
|
|
81
126
|
std::unique_lock<std::mutex> lock(queue_mutex_);
|
|
82
|
-
queue_cv_.wait(lock,
|
|
83
|
-
|
|
127
|
+
queue_cv_.wait(lock, [this] {
|
|
128
|
+
return (task_queue_.empty() && processing_.load() == 0) || !running_.load();
|
|
129
|
+
});
|
|
84
130
|
|
|
85
131
|
flushing_.store(false);
|
|
86
132
|
}
|
|
@@ -90,6 +136,28 @@ size_t AsyncDecodeWorker::QueueSize() const {
|
|
|
90
136
|
return task_queue_.size();
|
|
91
137
|
}
|
|
92
138
|
|
|
139
|
+
std::vector<uint8_t>* AsyncDecodeWorker::AcquireBuffer(size_t size) {
|
|
140
|
+
std::lock_guard<std::mutex> lock(pool_mutex_);
|
|
141
|
+
for (auto it = buffer_pool_.begin(); it != buffer_pool_.end(); ++it) {
|
|
142
|
+
if ((*it)->capacity() >= size) {
|
|
143
|
+
auto* buffer = *it;
|
|
144
|
+
buffer_pool_.erase(it);
|
|
145
|
+
buffer->resize(size);
|
|
146
|
+
return buffer;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return new std::vector<uint8_t>(size);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
void AsyncDecodeWorker::ReleaseBuffer(std::vector<uint8_t>* buffer) {
|
|
153
|
+
std::lock_guard<std::mutex> lock(pool_mutex_);
|
|
154
|
+
if (buffer_pool_.size() < 4) { // Keep up to 4 buffers
|
|
155
|
+
buffer_pool_.push_back(buffer);
|
|
156
|
+
} else {
|
|
157
|
+
delete buffer;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
93
161
|
void AsyncDecodeWorker::WorkerThread() {
|
|
94
162
|
while (running_.load()) {
|
|
95
163
|
DecodeTask task;
|
|
@@ -110,71 +178,211 @@ void AsyncDecodeWorker::WorkerThread() {
|
|
|
110
178
|
|
|
111
179
|
task = std::move(task_queue_.front());
|
|
112
180
|
task_queue_.pop();
|
|
181
|
+
processing_++; // Track that we're processing this task
|
|
113
182
|
}
|
|
114
183
|
|
|
115
184
|
ProcessPacket(task);
|
|
116
185
|
|
|
117
|
-
|
|
118
|
-
|
|
186
|
+
// Decrement counter and notify under lock (fixes race condition).
|
|
187
|
+
{
|
|
188
|
+
std::lock_guard<std::mutex> lock(queue_mutex_);
|
|
189
|
+
processing_--;
|
|
190
|
+
if (task_queue_.empty() && processing_.load() == 0) {
|
|
191
|
+
queue_cv_.notify_all();
|
|
192
|
+
}
|
|
119
193
|
}
|
|
120
194
|
}
|
|
121
195
|
}
|
|
122
196
|
|
|
123
197
|
void AsyncDecodeWorker::ProcessPacket(const DecodeTask& task) {
|
|
198
|
+
// DARWIN-X64 FIX: Check codec_valid_ BEFORE acquiring mutex.
|
|
199
|
+
// During shutdown, Stop() sets codec_valid_=false before running_=false.
|
|
200
|
+
// This creates a window where the worker thread could still be running
|
|
201
|
+
// but the codec is being destroyed. Early exit prevents the race.
|
|
202
|
+
if (!codec_valid_.load(std::memory_order_acquire)) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
std::lock_guard<std::mutex> lock(codec_mutex_);
|
|
124
207
|
if (!codec_context_ || !packet_ || !frame_) {
|
|
125
208
|
return;
|
|
126
209
|
}
|
|
127
210
|
|
|
211
|
+
// Handle flush task - send NULL packet to drain decoder
|
|
212
|
+
if (task.is_flush) {
|
|
213
|
+
avcodec_send_packet(codec_context_, nullptr);
|
|
214
|
+
// Drain all remaining frames from the decoder
|
|
215
|
+
while (avcodec_receive_frame(codec_context_, frame_.get()) == 0) {
|
|
216
|
+
EmitFrame(frame_.get());
|
|
217
|
+
av_frame_unref(frame_.get());
|
|
218
|
+
}
|
|
219
|
+
// Reset decoder to accept new packets after drain.
|
|
220
|
+
// Without this, decoder stays in drain mode and rejects further input.
|
|
221
|
+
avcodec_flush_buffers(codec_context_);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
128
225
|
// Set up packet from task data
|
|
129
|
-
av_packet_unref(packet_);
|
|
226
|
+
av_packet_unref(packet_.get());
|
|
130
227
|
packet_->data = const_cast<uint8_t*>(task.data.data());
|
|
131
228
|
packet_->size = static_cast<int>(task.data.size());
|
|
132
229
|
packet_->pts = task.timestamp;
|
|
133
230
|
|
|
134
|
-
int ret = avcodec_send_packet(codec_context_, packet_);
|
|
231
|
+
int ret = avcodec_send_packet(codec_context_, packet_.get());
|
|
135
232
|
if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) {
|
|
136
233
|
// Post error to main thread
|
|
137
234
|
std::string error_msg = "Decode error: " + std::to_string(ret);
|
|
138
235
|
error_tsfn_.NonBlockingCall(
|
|
139
236
|
new std::string(error_msg),
|
|
140
237
|
[](Napi::Env env, Napi::Function fn, std::string* msg) {
|
|
238
|
+
// If env is null, TSFN is closing during teardown. Just cleanup.
|
|
239
|
+
if (env == nullptr) {
|
|
240
|
+
delete msg;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
141
243
|
fn.Call({Napi::Error::New(env, *msg).Value()});
|
|
142
244
|
delete msg;
|
|
143
245
|
});
|
|
144
246
|
return;
|
|
145
247
|
}
|
|
146
248
|
|
|
147
|
-
while (avcodec_receive_frame(codec_context_, frame_) == 0) {
|
|
148
|
-
EmitFrame(frame_);
|
|
149
|
-
av_frame_unref(frame_);
|
|
249
|
+
while (avcodec_receive_frame(codec_context_, frame_.get()) == 0) {
|
|
250
|
+
EmitFrame(frame_.get());
|
|
251
|
+
av_frame_unref(frame_.get());
|
|
150
252
|
}
|
|
151
253
|
}
|
|
152
254
|
|
|
153
255
|
void AsyncDecodeWorker::EmitFrame(AVFrame* frame) {
|
|
154
|
-
if
|
|
155
|
-
|
|
256
|
+
// Initialize or recreate SwsContext if frame format/dimensions change
|
|
257
|
+
// (convert from decoder's pixel format to RGBA). RAII managed.
|
|
258
|
+
AVPixelFormat frame_format = static_cast<AVPixelFormat>(frame->format);
|
|
259
|
+
|
|
260
|
+
if (!sws_context_ || last_frame_format_ != frame_format ||
|
|
261
|
+
last_frame_width_ != frame->width ||
|
|
262
|
+
last_frame_height_ != frame->height) {
|
|
263
|
+
// RAII handles cleanup of old context automatically via reset()
|
|
264
|
+
sws_context_.reset(
|
|
265
|
+
sws_getContext(frame->width, frame->height, frame_format, frame->width,
|
|
266
|
+
frame->height, AV_PIX_FMT_RGBA, SWS_BILINEAR, nullptr,
|
|
267
|
+
nullptr, nullptr));
|
|
268
|
+
|
|
269
|
+
if (!sws_context_) {
|
|
270
|
+
std::string error_msg = "Could not create sws context";
|
|
271
|
+
error_tsfn_.NonBlockingCall(
|
|
272
|
+
new std::string(error_msg),
|
|
273
|
+
[](Napi::Env env, Napi::Function fn, std::string* msg) {
|
|
274
|
+
// If env is null, TSFN is closing during teardown. Just cleanup.
|
|
275
|
+
if (env == nullptr) {
|
|
276
|
+
delete msg;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
fn.Call({Napi::Error::New(env, *msg).Value()});
|
|
280
|
+
delete msg;
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
last_frame_format_ = frame_format;
|
|
286
|
+
last_frame_width_ = frame->width;
|
|
287
|
+
last_frame_height_ = frame->height;
|
|
288
|
+
// Update output dimensions based on actual frame
|
|
289
|
+
output_width_ = frame->width;
|
|
290
|
+
output_height_ = frame->height;
|
|
156
291
|
}
|
|
157
292
|
|
|
293
|
+
// Copy metadata under lock to prevent torn reads
|
|
294
|
+
// Note: codec_mutex_ is already held by ProcessPacket caller
|
|
295
|
+
DecoderMetadataConfig metadata_copy = metadata_config_;
|
|
296
|
+
|
|
158
297
|
// Convert YUV to RGBA
|
|
159
298
|
size_t rgba_size = output_width_ * output_height_ * 4;
|
|
160
|
-
auto* rgba_data =
|
|
299
|
+
auto* rgba_data = AcquireBuffer(rgba_size);
|
|
161
300
|
|
|
162
301
|
uint8_t* dst_data[1] = {rgba_data->data()};
|
|
163
302
|
int dst_linesize[1] = {output_width_ * 4};
|
|
164
303
|
|
|
165
|
-
sws_scale(sws_context_, frame->data, frame->linesize, 0, frame->height,
|
|
304
|
+
sws_scale(sws_context_.get(), frame->data, frame->linesize, 0, frame->height,
|
|
166
305
|
dst_data, dst_linesize);
|
|
167
306
|
|
|
168
307
|
int64_t timestamp = frame->pts;
|
|
169
308
|
int width = output_width_;
|
|
170
309
|
int height = output_height_;
|
|
171
310
|
|
|
311
|
+
// Capture metadata for lambda
|
|
312
|
+
int rotation = metadata_copy.rotation;
|
|
313
|
+
bool flip = metadata_copy.flip;
|
|
314
|
+
|
|
315
|
+
// Calculate display dimensions based on aspect ratio (per W3C spec).
|
|
316
|
+
// If displayAspectWidth/displayAspectHeight are set, compute display
|
|
317
|
+
// dimensions maintaining the height and adjusting width to match ratio.
|
|
318
|
+
int disp_width = width;
|
|
319
|
+
int disp_height = height;
|
|
320
|
+
if (metadata_copy.display_width > 0 && metadata_copy.display_height > 0) {
|
|
321
|
+
// Per W3C spec: displayWidth = codedHeight * aspectWidth / aspectHeight
|
|
322
|
+
disp_width = static_cast<int>(
|
|
323
|
+
std::round(static_cast<double>(height) *
|
|
324
|
+
static_cast<double>(metadata_copy.display_width) /
|
|
325
|
+
static_cast<double>(metadata_copy.display_height)));
|
|
326
|
+
disp_height = height;
|
|
327
|
+
}
|
|
328
|
+
std::string color_primaries = metadata_copy.color_primaries;
|
|
329
|
+
std::string color_transfer = metadata_copy.color_transfer;
|
|
330
|
+
std::string color_matrix = metadata_copy.color_matrix;
|
|
331
|
+
bool color_full_range = metadata_copy.color_full_range;
|
|
332
|
+
bool has_color_space = metadata_copy.has_color_space;
|
|
333
|
+
|
|
334
|
+
// Increment pending BEFORE queueing callback for accurate tracking
|
|
335
|
+
(*pending_frames_)++;
|
|
336
|
+
|
|
337
|
+
// Capture shared_ptr to pending counter, NOT raw worker pointer.
|
|
338
|
+
// This ensures the counter remains valid even if the worker is destroyed
|
|
339
|
+
// before the TSFN callback executes on the main thread.
|
|
340
|
+
// Note: Buffer is managed via raw delete since buffer pool access is unsafe
|
|
341
|
+
// after worker destruction.
|
|
342
|
+
auto pending_counter = pending_frames_;
|
|
172
343
|
output_tsfn_.NonBlockingCall(
|
|
173
|
-
rgba_data,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
344
|
+
rgba_data,
|
|
345
|
+
[pending_counter, width, height, timestamp, rotation, flip, disp_width,
|
|
346
|
+
disp_height, color_primaries, color_transfer, color_matrix,
|
|
347
|
+
color_full_range,
|
|
348
|
+
has_color_space](Napi::Env env, Napi::Function fn,
|
|
349
|
+
std::vector<uint8_t>* data) {
|
|
350
|
+
// CRITICAL: If env is null, TSFN is closing during teardown.
|
|
351
|
+
// Must still clean up data and counters, then return.
|
|
352
|
+
// NOTE: Do NOT access static variables (like counterQueue) here - they may
|
|
353
|
+
// already be destroyed due to static destruction order during process exit.
|
|
354
|
+
if (env == nullptr) {
|
|
355
|
+
delete data;
|
|
356
|
+
(*pending_counter)--;
|
|
357
|
+
// Skip counterQueue-- : static may be destroyed during process exit
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Always clean up, even if callback throws
|
|
362
|
+
try {
|
|
363
|
+
Napi::Object frame_obj;
|
|
364
|
+
if (has_color_space) {
|
|
365
|
+
frame_obj = VideoFrame::CreateInstance(
|
|
366
|
+
env, data->data(), data->size(), width, height, timestamp,
|
|
367
|
+
"RGBA", rotation, flip, disp_width, disp_height, color_primaries,
|
|
368
|
+
color_transfer, color_matrix, color_full_range);
|
|
369
|
+
} else {
|
|
370
|
+
frame_obj = VideoFrame::CreateInstance(
|
|
371
|
+
env, data->data(), data->size(), width, height, timestamp,
|
|
372
|
+
"RGBA", rotation, flip, disp_width, disp_height);
|
|
373
|
+
}
|
|
374
|
+
fn.Call({frame_obj});
|
|
375
|
+
} catch (const std::exception& e) {
|
|
376
|
+
// Log but don't propagate - cleanup must happen
|
|
377
|
+
fprintf(stderr, "AsyncDecodeWorker callback error: %s\n", e.what());
|
|
378
|
+
} catch (...) {
|
|
379
|
+
fprintf(stderr,
|
|
380
|
+
"AsyncDecodeWorker callback error: unknown exception\n");
|
|
381
|
+
}
|
|
382
|
+
// Delete buffer directly (can't use pool after worker destruction)
|
|
178
383
|
delete data;
|
|
384
|
+
// Decrement pending counter via shared_ptr (safe after worker destruction)
|
|
385
|
+
(*pending_counter)--;
|
|
386
|
+
webcodecs::counterQueue--; // Decrement global queue counter
|
|
179
387
|
});
|
|
180
388
|
}
|