@camstack/addon-pipeline 1.1.19 → 1.1.21
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/dist/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/audio-codec-ffmpeg/index.js +1125 -0
- package/dist/audio-codec-ffmpeg/index.mjs +1107 -0
- package/dist/decoder-ffmpeg/index.js +543 -288
- package/dist/decoder-ffmpeg/index.mjs +542 -287
- package/dist/detection-pipeline/index.js +1 -1
- package/dist/detection-pipeline/index.mjs +1 -1
- package/dist/{dist-BgoBMCez.js → dist-BiP1gPeY.js} +20 -1
- package/dist/{dist-7Yx2dmuV.mjs → dist-tzhTTRq4.mjs} +20 -1
- package/dist/{ffmpeg-args-common-c3p-nWtU.mjs → frame-dropper-CwkBTPGV.mjs} +22 -22
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +1 -1
- package/dist/pipeline-runner/index.mjs +1 -1
- package/dist/recorder/index.js +24 -4
- package/dist/recorder/index.mjs +24 -4
- package/dist/stream-broker/_stub.js +35 -35
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-w2pP0MYO.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D08qJTw_.mjs} +3 -3
- package/dist/stream-broker/{hostInit-BUEbBinp.mjs → hostInit-CGnMNtPl.mjs} +3 -3
- package/dist/stream-broker/index.js +487 -458
- package/dist/stream-broker/index.mjs +477 -448
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CYmHXacX.js → MaskShapeCanvas-DI4BY7W2-CW4Qu4V6.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-B-BkPQvX.js → MotionZonesSettings-NcxxQN8r-CHJ39tfi.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DC3Jw85p.js → PrivacyMaskSettings-APgPLF7p-dInJ7kcz.js} +1 -1
- package/embed-dist/assets/{index-B0IQgci0.js → index-CKpVWKSG.js} +4 -4
- package/embed-dist/index.html +1 -1
- package/package.json +10 -10
- package/dist/audio-codec-nodeav/index.js +0 -310
- package/dist/audio-codec-nodeav/index.mjs +0 -305
- package/dist/codec-runtime-BOk-13PN.js +0 -202
- package/dist/codec-runtime-BsqlEjPi.mjs +0 -197
- package/dist/{ffmpeg-args-common-CASoNt42.js → frame-dropper-7RTo_YyG.js} +21 -21
package/embed-dist/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>CamStack Embed</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-CKpVWKSG.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="./assets/index-C7SSikPl.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.21",
|
|
4
4
|
"description": "CamStack Pipeline bundle — runner, detection, motion, decoders, audio + stream broker. Multi-entry npm package shipping 7 addons under a single bundle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"import": "./dist/decoder-ffmpeg/index.mjs",
|
|
37
37
|
"require": "./dist/decoder-ffmpeg/index.js"
|
|
38
38
|
},
|
|
39
|
-
"./audio-codec-
|
|
40
|
-
"import": "./dist/audio-codec-
|
|
41
|
-
"require": "./dist/audio-codec-
|
|
39
|
+
"./audio-codec-ffmpeg": {
|
|
40
|
+
"import": "./dist/audio-codec-ffmpeg/index.mjs",
|
|
41
|
+
"require": "./dist/audio-codec-ffmpeg/index.js"
|
|
42
42
|
},
|
|
43
43
|
"./audio-analyzer": {
|
|
44
44
|
"import": "./dist/audio-analyzer/index.mjs",
|
|
@@ -166,12 +166,12 @@
|
|
|
166
166
|
"color": "#0ea5e9"
|
|
167
167
|
},
|
|
168
168
|
{
|
|
169
|
-
"id": "audio-codec-
|
|
169
|
+
"id": "audio-codec-ffmpeg",
|
|
170
170
|
"category": "pipeline",
|
|
171
|
-
"name": "Audio Codec (
|
|
171
|
+
"name": "Audio Codec (ffmpeg)",
|
|
172
172
|
"version": "0.1.0",
|
|
173
|
-
"description": "Bidirectional audio codec I/O — decodes camera audio (AAC, Opus, PCMU/PCMA) into per-consumer PCM sessions and encodes intercom PCM back into camera-supported codecs",
|
|
174
|
-
"entry": "./dist/audio-codec-
|
|
173
|
+
"description": "Bidirectional audio codec I/O in an ffmpeg subprocess — decodes camera audio (AAC, Opus, PCMU/PCMA, G.722) into per-consumer PCM sessions and encodes intercom PCM back into camera-supported codecs. A codec crash is isolated to the child; ffmpeg is provisioned per node via ctx.deps.ensureFfmpeg().",
|
|
174
|
+
"entry": "./dist/audio-codec-ffmpeg/index.js",
|
|
175
175
|
"execution": {
|
|
176
176
|
"placement": "any-node",
|
|
177
177
|
"heapProfile": "heavy"
|
|
@@ -260,7 +260,8 @@
|
|
|
260
260
|
{
|
|
261
261
|
"id": "recordings",
|
|
262
262
|
"displayName": "Recordings",
|
|
263
|
-
"cardinality": "multi"
|
|
263
|
+
"cardinality": "multi",
|
|
264
|
+
"defaultRoot": "media"
|
|
264
265
|
},
|
|
265
266
|
{
|
|
266
267
|
"id": "recordingsLow",
|
|
@@ -298,7 +299,6 @@
|
|
|
298
299
|
"@camstack/system": "*",
|
|
299
300
|
"lucide-react": "^0.511.0",
|
|
300
301
|
"mp4box": "0.5.4",
|
|
301
|
-
"node-av": "^6.0.0",
|
|
302
302
|
"sharp": "^0.35.2",
|
|
303
303
|
"zod": "^4.3.6"
|
|
304
304
|
},
|
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
Object.defineProperties(exports, {
|
|
2
|
-
__esModule: { value: true },
|
|
3
|
-
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
-
});
|
|
5
|
-
const require_dist = require("../dist-BgoBMCez.js");
|
|
6
|
-
const require_codec_runtime = require("../codec-runtime-BOk-13PN.js");
|
|
7
|
-
let node_crypto = require("node:crypto");
|
|
8
|
-
//#region src/audio-codec-nodeav/addon/index.ts
|
|
9
|
-
var CODEC_CATALOG = [
|
|
10
|
-
{
|
|
11
|
-
codec: "pcm_mulaw",
|
|
12
|
-
canDecode: true,
|
|
13
|
-
canEncode: true,
|
|
14
|
-
label: "PCM µ-law (G.711)"
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
codec: "pcm_alaw",
|
|
18
|
-
canDecode: true,
|
|
19
|
-
canEncode: true,
|
|
20
|
-
label: "PCM A-law (G.711)"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
codec: "aac",
|
|
24
|
-
canDecode: true,
|
|
25
|
-
canEncode: true,
|
|
26
|
-
label: "AAC"
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
codec: "aac_latm",
|
|
30
|
-
canDecode: true,
|
|
31
|
-
canEncode: false,
|
|
32
|
-
label: "AAC LATM"
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
codec: "mpeg4-generic",
|
|
36
|
-
canDecode: true,
|
|
37
|
-
canEncode: false,
|
|
38
|
-
label: "AAC (MPEG4-GENERIC)"
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
codec: "opus",
|
|
42
|
-
canDecode: true,
|
|
43
|
-
canEncode: true,
|
|
44
|
-
label: "Opus"
|
|
45
|
-
}
|
|
46
|
-
];
|
|
47
|
-
function resolveCodecAlias(codec) {
|
|
48
|
-
const c = codec.toLowerCase();
|
|
49
|
-
if (c === "mpeg4-generic") return "aac";
|
|
50
|
-
if (c === "l16") return "pcm_s16be";
|
|
51
|
-
return c;
|
|
52
|
-
}
|
|
53
|
-
var DEFAULT_IDLE_MS = 3e4;
|
|
54
|
-
var REAPER_INTERVAL_MS = 5e3;
|
|
55
|
-
var DEFAULT_GLOBAL_CONFIG = { defaultIdleMs: DEFAULT_IDLE_MS };
|
|
56
|
-
/**
|
|
57
|
-
* Audio codec I/O box backed by node-av (libavcodec + libswresample).
|
|
58
|
-
*
|
|
59
|
-
* Each `createDecodeSession` / `createEncodeSession` allocates an
|
|
60
|
-
* independent libav codec context + resampler so consumers don't share
|
|
61
|
-
* resamplers — a 16kHz mono ASA subscriber and a 48kHz stereo WebRTC
|
|
62
|
-
* subscriber on the same source codec each get their own session.
|
|
63
|
-
*
|
|
64
|
-
* Phase 1 scaffolds the cap surface + session bookkeeping. The actual
|
|
65
|
-
* libav decode/encode wiring is filled in by follow-up commits — the
|
|
66
|
-
* stub keeps the cap registered so wiring on the broker side can land
|
|
67
|
-
* before the audio backend matures.
|
|
68
|
-
*/
|
|
69
|
-
var AudioCodecNodeAvAddon = class extends require_dist.BaseAddon {
|
|
70
|
-
sessions = /* @__PURE__ */ new Map();
|
|
71
|
-
reaperTimer = null;
|
|
72
|
-
logger = null;
|
|
73
|
-
constructor() {
|
|
74
|
-
super(DEFAULT_GLOBAL_CONFIG);
|
|
75
|
-
}
|
|
76
|
-
async onInitialize() {
|
|
77
|
-
this.logger = this.ctx.logger;
|
|
78
|
-
this.reaperTimer = setInterval(() => this.reapIdleSessions(), REAPER_INTERVAL_MS);
|
|
79
|
-
if (typeof this.reaperTimer.unref === "function") this.reaperTimer.unref();
|
|
80
|
-
return [{
|
|
81
|
-
capability: require_dist.audioCodecCapability,
|
|
82
|
-
provider: this
|
|
83
|
-
}];
|
|
84
|
-
}
|
|
85
|
-
async onShutdown() {
|
|
86
|
-
if (this.reaperTimer) {
|
|
87
|
-
clearInterval(this.reaperTimer);
|
|
88
|
-
this.reaperTimer = null;
|
|
89
|
-
}
|
|
90
|
-
for (const s of this.sessions.values()) this.disposeSession(s);
|
|
91
|
-
this.sessions.clear();
|
|
92
|
-
}
|
|
93
|
-
async listSupportedCodecs() {
|
|
94
|
-
return CODEC_CATALOG.map((e) => ({
|
|
95
|
-
codec: e.codec,
|
|
96
|
-
canDecode: e.canDecode,
|
|
97
|
-
canEncode: e.canEncode,
|
|
98
|
-
...e.label ? { label: e.label } : {}
|
|
99
|
-
}));
|
|
100
|
-
}
|
|
101
|
-
async canHandle(input) {
|
|
102
|
-
const resolved = resolveCodecAlias(input.codec);
|
|
103
|
-
const entry = CODEC_CATALOG.find((e) => e.codec === resolved);
|
|
104
|
-
if (!entry) return false;
|
|
105
|
-
return input.kind === "decode" ? entry.canDecode : entry.canEncode;
|
|
106
|
-
}
|
|
107
|
-
async createDecodeSession(input) {
|
|
108
|
-
const codec = resolveCodecAlias(input.codec);
|
|
109
|
-
const entry = CODEC_CATALOG.find((e) => e.codec === codec);
|
|
110
|
-
if (!entry || !entry.canDecode) throw new Error(`audio-codec: decode unsupported for codec '${input.codec}'`);
|
|
111
|
-
const sessionId = `dec-${(0, node_crypto.randomUUID)()}`;
|
|
112
|
-
const state = {
|
|
113
|
-
sessionId,
|
|
114
|
-
kind: "decode",
|
|
115
|
-
config: {
|
|
116
|
-
...input,
|
|
117
|
-
codec
|
|
118
|
-
},
|
|
119
|
-
...input.tag ? { tag: input.tag } : {},
|
|
120
|
-
createdAtMs: Date.now(),
|
|
121
|
-
lastActivityMs: Date.now(),
|
|
122
|
-
framesIn: 0,
|
|
123
|
-
framesOut: 0,
|
|
124
|
-
pcmQueue: [],
|
|
125
|
-
runtime: null
|
|
126
|
-
};
|
|
127
|
-
this.sessions.set(sessionId, state);
|
|
128
|
-
this.logger?.info("audio-codec: decode session created", {
|
|
129
|
-
tags: { sessionId },
|
|
130
|
-
meta: {
|
|
131
|
-
codec,
|
|
132
|
-
target: `${input.targetSampleRate}Hz×${input.targetChannels}`
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
return {
|
|
136
|
-
sessionId,
|
|
137
|
-
nodeId: this.ctx.kernel.localNodeId ?? "local"
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
async createEncodeSession(input) {
|
|
141
|
-
const codec = resolveCodecAlias(input.codec);
|
|
142
|
-
const entry = CODEC_CATALOG.find((e) => e.codec === codec);
|
|
143
|
-
if (!entry || !entry.canEncode) throw new Error(`audio-codec: encode unsupported for codec '${input.codec}'`);
|
|
144
|
-
const sessionId = `enc-${(0, node_crypto.randomUUID)()}`;
|
|
145
|
-
const state = {
|
|
146
|
-
sessionId,
|
|
147
|
-
kind: "encode",
|
|
148
|
-
config: {
|
|
149
|
-
...input,
|
|
150
|
-
codec
|
|
151
|
-
},
|
|
152
|
-
...input.tag ? { tag: input.tag } : {},
|
|
153
|
-
createdAtMs: Date.now(),
|
|
154
|
-
lastActivityMs: Date.now(),
|
|
155
|
-
framesIn: 0,
|
|
156
|
-
framesOut: 0,
|
|
157
|
-
encodedQueue: []
|
|
158
|
-
};
|
|
159
|
-
this.sessions.set(sessionId, state);
|
|
160
|
-
this.logger?.info("audio-codec: encode session created", {
|
|
161
|
-
tags: { sessionId },
|
|
162
|
-
meta: {
|
|
163
|
-
codec,
|
|
164
|
-
target: `${input.targetSampleRate}Hz×${input.targetChannels}`
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
return {
|
|
168
|
-
sessionId,
|
|
169
|
-
nodeId: this.ctx.kernel.localNodeId ?? "local"
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
async closeSession(input) {
|
|
173
|
-
const s = this.sessions.get(input.sessionId);
|
|
174
|
-
if (!s) return;
|
|
175
|
-
this.disposeSession(s);
|
|
176
|
-
this.sessions.delete(input.sessionId);
|
|
177
|
-
}
|
|
178
|
-
async pushEncodedFrame(input) {
|
|
179
|
-
const s = this.sessions.get(input.sessionId);
|
|
180
|
-
if (!s || s.kind !== "decode") throw new Error(`audio-codec: decode session '${input.sessionId}' not found`);
|
|
181
|
-
s.lastActivityMs = Date.now();
|
|
182
|
-
s.framesIn++;
|
|
183
|
-
if (!s.runtime) try {
|
|
184
|
-
s.runtime = require_codec_runtime.DecodeRuntime.create({
|
|
185
|
-
codec: s.config.codec,
|
|
186
|
-
sourceSampleRate: s.config.sourceSampleRate,
|
|
187
|
-
sourceChannels: s.config.sourceChannels,
|
|
188
|
-
...s.config.extraData ? { extraData: s.config.extraData } : {},
|
|
189
|
-
targetSampleRate: s.config.targetSampleRate,
|
|
190
|
-
targetChannels: s.config.targetChannels,
|
|
191
|
-
targetFormat: s.config.targetFormat ?? "f32le"
|
|
192
|
-
});
|
|
193
|
-
} catch (err) {
|
|
194
|
-
this.logger?.error("audio-codec: decode runtime init failed", {
|
|
195
|
-
tags: { sessionId: input.sessionId },
|
|
196
|
-
meta: {
|
|
197
|
-
codec: s.config.codec,
|
|
198
|
-
error: require_dist.errMsg(err)
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
try {
|
|
204
|
-
const pcms = s.runtime.decode(input.data, input.pts);
|
|
205
|
-
for (const pcm of pcms) s.pcmQueue.push(pcm);
|
|
206
|
-
} catch (err) {
|
|
207
|
-
this.logger?.warn("audio-codec: decode frame failed", {
|
|
208
|
-
tags: { sessionId: input.sessionId },
|
|
209
|
-
meta: {
|
|
210
|
-
codec: s.config.codec,
|
|
211
|
-
error: require_dist.errMsg(err)
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
async pullPcm(input) {
|
|
217
|
-
const s = this.sessions.get(input.sessionId);
|
|
218
|
-
if (!s || s.kind !== "decode") throw new Error(`audio-codec: decode session '${input.sessionId}' not found`);
|
|
219
|
-
s.lastActivityMs = Date.now();
|
|
220
|
-
const out = s.pcmQueue.splice(0, input.maxCount);
|
|
221
|
-
s.framesOut += out.length;
|
|
222
|
-
return out;
|
|
223
|
-
}
|
|
224
|
-
async pushPcm(input) {
|
|
225
|
-
const s = this.sessions.get(input.sessionId);
|
|
226
|
-
if (!s || s.kind !== "encode") throw new Error(`audio-codec: encode session '${input.sessionId}' not found`);
|
|
227
|
-
s.lastActivityMs = Date.now();
|
|
228
|
-
s.framesIn++;
|
|
229
|
-
input.data;
|
|
230
|
-
input.pts;
|
|
231
|
-
}
|
|
232
|
-
async pullEncoded(input) {
|
|
233
|
-
const s = this.sessions.get(input.sessionId);
|
|
234
|
-
if (!s || s.kind !== "encode") throw new Error(`audio-codec: encode session '${input.sessionId}' not found`);
|
|
235
|
-
s.lastActivityMs = Date.now();
|
|
236
|
-
const out = s.encodedQueue.splice(0, input.maxCount);
|
|
237
|
-
s.framesOut += out.length;
|
|
238
|
-
return out;
|
|
239
|
-
}
|
|
240
|
-
async flushEncode(input) {
|
|
241
|
-
const s = this.sessions.get(input.sessionId);
|
|
242
|
-
if (!s || s.kind !== "encode") throw new Error(`audio-codec: encode session '${input.sessionId}' not found`);
|
|
243
|
-
s.lastActivityMs = Date.now();
|
|
244
|
-
const out = s.encodedQueue.splice(0);
|
|
245
|
-
s.framesOut += out.length;
|
|
246
|
-
return out;
|
|
247
|
-
}
|
|
248
|
-
async listActiveSessions() {
|
|
249
|
-
return [...this.sessions.values()].map((s) => ({
|
|
250
|
-
sessionId: s.sessionId,
|
|
251
|
-
kind: s.kind,
|
|
252
|
-
codec: s.config.codec,
|
|
253
|
-
sourceSampleRate: s.config.sourceSampleRate,
|
|
254
|
-
sourceChannels: s.config.sourceChannels,
|
|
255
|
-
targetSampleRate: s.config.targetSampleRate,
|
|
256
|
-
targetChannels: s.config.targetChannels,
|
|
257
|
-
format: this.resolveFormat(s),
|
|
258
|
-
...s.tag ? { tag: s.tag } : {},
|
|
259
|
-
createdAtMs: s.createdAtMs,
|
|
260
|
-
lastActivityMs: s.lastActivityMs,
|
|
261
|
-
framesIn: s.framesIn,
|
|
262
|
-
framesOut: s.framesOut
|
|
263
|
-
}));
|
|
264
|
-
}
|
|
265
|
-
resolveFormat(s) {
|
|
266
|
-
if (s.kind === "decode") return s.config.targetFormat ?? "f32le";
|
|
267
|
-
return s.config.sourceFormat ?? "f32le";
|
|
268
|
-
}
|
|
269
|
-
reapIdleSessions() {
|
|
270
|
-
const now = Date.now();
|
|
271
|
-
for (const [id, s] of this.sessions) {
|
|
272
|
-
const limit = s.config.idleMs ?? this.config.defaultIdleMs ?? DEFAULT_IDLE_MS;
|
|
273
|
-
if (now - s.lastActivityMs > limit) {
|
|
274
|
-
this.logger?.info("audio-codec: reaping idle session", {
|
|
275
|
-
tags: { sessionId: id },
|
|
276
|
-
meta: {
|
|
277
|
-
kind: s.kind,
|
|
278
|
-
idleMs: now - s.lastActivityMs,
|
|
279
|
-
limit
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
try {
|
|
283
|
-
this.disposeSession(s);
|
|
284
|
-
} catch (err) {
|
|
285
|
-
this.logger?.warn("audio-codec: dispose failed during reap", {
|
|
286
|
-
tags: { sessionId: id },
|
|
287
|
-
meta: { error: require_dist.errMsg(err) }
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
this.sessions.delete(id);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
disposeSession(s) {
|
|
295
|
-
if (s.kind === "decode" && s.runtime) {
|
|
296
|
-
try {
|
|
297
|
-
s.runtime.close();
|
|
298
|
-
} catch (err) {
|
|
299
|
-
this.logger?.warn("audio-codec: decode runtime close failed", {
|
|
300
|
-
tags: { sessionId: s.sessionId },
|
|
301
|
-
meta: { error: require_dist.errMsg(err) }
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
s.runtime = null;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
//#endregion
|
|
309
|
-
exports.AudioCodecNodeAvAddon = AudioCodecNodeAvAddon;
|
|
310
|
-
exports.default = AudioCodecNodeAvAddon;
|
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import { B as BaseAddon, _ as audioCodecCapability, z as errMsg } from "../dist-7Yx2dmuV.mjs";
|
|
2
|
-
import { t as DecodeRuntime } from "../codec-runtime-BsqlEjPi.mjs";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
//#region src/audio-codec-nodeav/addon/index.ts
|
|
5
|
-
var CODEC_CATALOG = [
|
|
6
|
-
{
|
|
7
|
-
codec: "pcm_mulaw",
|
|
8
|
-
canDecode: true,
|
|
9
|
-
canEncode: true,
|
|
10
|
-
label: "PCM µ-law (G.711)"
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
codec: "pcm_alaw",
|
|
14
|
-
canDecode: true,
|
|
15
|
-
canEncode: true,
|
|
16
|
-
label: "PCM A-law (G.711)"
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
codec: "aac",
|
|
20
|
-
canDecode: true,
|
|
21
|
-
canEncode: true,
|
|
22
|
-
label: "AAC"
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
codec: "aac_latm",
|
|
26
|
-
canDecode: true,
|
|
27
|
-
canEncode: false,
|
|
28
|
-
label: "AAC LATM"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
codec: "mpeg4-generic",
|
|
32
|
-
canDecode: true,
|
|
33
|
-
canEncode: false,
|
|
34
|
-
label: "AAC (MPEG4-GENERIC)"
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
codec: "opus",
|
|
38
|
-
canDecode: true,
|
|
39
|
-
canEncode: true,
|
|
40
|
-
label: "Opus"
|
|
41
|
-
}
|
|
42
|
-
];
|
|
43
|
-
function resolveCodecAlias(codec) {
|
|
44
|
-
const c = codec.toLowerCase();
|
|
45
|
-
if (c === "mpeg4-generic") return "aac";
|
|
46
|
-
if (c === "l16") return "pcm_s16be";
|
|
47
|
-
return c;
|
|
48
|
-
}
|
|
49
|
-
var DEFAULT_IDLE_MS = 3e4;
|
|
50
|
-
var REAPER_INTERVAL_MS = 5e3;
|
|
51
|
-
var DEFAULT_GLOBAL_CONFIG = { defaultIdleMs: DEFAULT_IDLE_MS };
|
|
52
|
-
/**
|
|
53
|
-
* Audio codec I/O box backed by node-av (libavcodec + libswresample).
|
|
54
|
-
*
|
|
55
|
-
* Each `createDecodeSession` / `createEncodeSession` allocates an
|
|
56
|
-
* independent libav codec context + resampler so consumers don't share
|
|
57
|
-
* resamplers — a 16kHz mono ASA subscriber and a 48kHz stereo WebRTC
|
|
58
|
-
* subscriber on the same source codec each get their own session.
|
|
59
|
-
*
|
|
60
|
-
* Phase 1 scaffolds the cap surface + session bookkeeping. The actual
|
|
61
|
-
* libav decode/encode wiring is filled in by follow-up commits — the
|
|
62
|
-
* stub keeps the cap registered so wiring on the broker side can land
|
|
63
|
-
* before the audio backend matures.
|
|
64
|
-
*/
|
|
65
|
-
var AudioCodecNodeAvAddon = class extends BaseAddon {
|
|
66
|
-
sessions = /* @__PURE__ */ new Map();
|
|
67
|
-
reaperTimer = null;
|
|
68
|
-
logger = null;
|
|
69
|
-
constructor() {
|
|
70
|
-
super(DEFAULT_GLOBAL_CONFIG);
|
|
71
|
-
}
|
|
72
|
-
async onInitialize() {
|
|
73
|
-
this.logger = this.ctx.logger;
|
|
74
|
-
this.reaperTimer = setInterval(() => this.reapIdleSessions(), REAPER_INTERVAL_MS);
|
|
75
|
-
if (typeof this.reaperTimer.unref === "function") this.reaperTimer.unref();
|
|
76
|
-
return [{
|
|
77
|
-
capability: audioCodecCapability,
|
|
78
|
-
provider: this
|
|
79
|
-
}];
|
|
80
|
-
}
|
|
81
|
-
async onShutdown() {
|
|
82
|
-
if (this.reaperTimer) {
|
|
83
|
-
clearInterval(this.reaperTimer);
|
|
84
|
-
this.reaperTimer = null;
|
|
85
|
-
}
|
|
86
|
-
for (const s of this.sessions.values()) this.disposeSession(s);
|
|
87
|
-
this.sessions.clear();
|
|
88
|
-
}
|
|
89
|
-
async listSupportedCodecs() {
|
|
90
|
-
return CODEC_CATALOG.map((e) => ({
|
|
91
|
-
codec: e.codec,
|
|
92
|
-
canDecode: e.canDecode,
|
|
93
|
-
canEncode: e.canEncode,
|
|
94
|
-
...e.label ? { label: e.label } : {}
|
|
95
|
-
}));
|
|
96
|
-
}
|
|
97
|
-
async canHandle(input) {
|
|
98
|
-
const resolved = resolveCodecAlias(input.codec);
|
|
99
|
-
const entry = CODEC_CATALOG.find((e) => e.codec === resolved);
|
|
100
|
-
if (!entry) return false;
|
|
101
|
-
return input.kind === "decode" ? entry.canDecode : entry.canEncode;
|
|
102
|
-
}
|
|
103
|
-
async createDecodeSession(input) {
|
|
104
|
-
const codec = resolveCodecAlias(input.codec);
|
|
105
|
-
const entry = CODEC_CATALOG.find((e) => e.codec === codec);
|
|
106
|
-
if (!entry || !entry.canDecode) throw new Error(`audio-codec: decode unsupported for codec '${input.codec}'`);
|
|
107
|
-
const sessionId = `dec-${randomUUID()}`;
|
|
108
|
-
const state = {
|
|
109
|
-
sessionId,
|
|
110
|
-
kind: "decode",
|
|
111
|
-
config: {
|
|
112
|
-
...input,
|
|
113
|
-
codec
|
|
114
|
-
},
|
|
115
|
-
...input.tag ? { tag: input.tag } : {},
|
|
116
|
-
createdAtMs: Date.now(),
|
|
117
|
-
lastActivityMs: Date.now(),
|
|
118
|
-
framesIn: 0,
|
|
119
|
-
framesOut: 0,
|
|
120
|
-
pcmQueue: [],
|
|
121
|
-
runtime: null
|
|
122
|
-
};
|
|
123
|
-
this.sessions.set(sessionId, state);
|
|
124
|
-
this.logger?.info("audio-codec: decode session created", {
|
|
125
|
-
tags: { sessionId },
|
|
126
|
-
meta: {
|
|
127
|
-
codec,
|
|
128
|
-
target: `${input.targetSampleRate}Hz×${input.targetChannels}`
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
return {
|
|
132
|
-
sessionId,
|
|
133
|
-
nodeId: this.ctx.kernel.localNodeId ?? "local"
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
async createEncodeSession(input) {
|
|
137
|
-
const codec = resolveCodecAlias(input.codec);
|
|
138
|
-
const entry = CODEC_CATALOG.find((e) => e.codec === codec);
|
|
139
|
-
if (!entry || !entry.canEncode) throw new Error(`audio-codec: encode unsupported for codec '${input.codec}'`);
|
|
140
|
-
const sessionId = `enc-${randomUUID()}`;
|
|
141
|
-
const state = {
|
|
142
|
-
sessionId,
|
|
143
|
-
kind: "encode",
|
|
144
|
-
config: {
|
|
145
|
-
...input,
|
|
146
|
-
codec
|
|
147
|
-
},
|
|
148
|
-
...input.tag ? { tag: input.tag } : {},
|
|
149
|
-
createdAtMs: Date.now(),
|
|
150
|
-
lastActivityMs: Date.now(),
|
|
151
|
-
framesIn: 0,
|
|
152
|
-
framesOut: 0,
|
|
153
|
-
encodedQueue: []
|
|
154
|
-
};
|
|
155
|
-
this.sessions.set(sessionId, state);
|
|
156
|
-
this.logger?.info("audio-codec: encode session created", {
|
|
157
|
-
tags: { sessionId },
|
|
158
|
-
meta: {
|
|
159
|
-
codec,
|
|
160
|
-
target: `${input.targetSampleRate}Hz×${input.targetChannels}`
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
return {
|
|
164
|
-
sessionId,
|
|
165
|
-
nodeId: this.ctx.kernel.localNodeId ?? "local"
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
async closeSession(input) {
|
|
169
|
-
const s = this.sessions.get(input.sessionId);
|
|
170
|
-
if (!s) return;
|
|
171
|
-
this.disposeSession(s);
|
|
172
|
-
this.sessions.delete(input.sessionId);
|
|
173
|
-
}
|
|
174
|
-
async pushEncodedFrame(input) {
|
|
175
|
-
const s = this.sessions.get(input.sessionId);
|
|
176
|
-
if (!s || s.kind !== "decode") throw new Error(`audio-codec: decode session '${input.sessionId}' not found`);
|
|
177
|
-
s.lastActivityMs = Date.now();
|
|
178
|
-
s.framesIn++;
|
|
179
|
-
if (!s.runtime) try {
|
|
180
|
-
s.runtime = DecodeRuntime.create({
|
|
181
|
-
codec: s.config.codec,
|
|
182
|
-
sourceSampleRate: s.config.sourceSampleRate,
|
|
183
|
-
sourceChannels: s.config.sourceChannels,
|
|
184
|
-
...s.config.extraData ? { extraData: s.config.extraData } : {},
|
|
185
|
-
targetSampleRate: s.config.targetSampleRate,
|
|
186
|
-
targetChannels: s.config.targetChannels,
|
|
187
|
-
targetFormat: s.config.targetFormat ?? "f32le"
|
|
188
|
-
});
|
|
189
|
-
} catch (err) {
|
|
190
|
-
this.logger?.error("audio-codec: decode runtime init failed", {
|
|
191
|
-
tags: { sessionId: input.sessionId },
|
|
192
|
-
meta: {
|
|
193
|
-
codec: s.config.codec,
|
|
194
|
-
error: errMsg(err)
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
try {
|
|
200
|
-
const pcms = s.runtime.decode(input.data, input.pts);
|
|
201
|
-
for (const pcm of pcms) s.pcmQueue.push(pcm);
|
|
202
|
-
} catch (err) {
|
|
203
|
-
this.logger?.warn("audio-codec: decode frame failed", {
|
|
204
|
-
tags: { sessionId: input.sessionId },
|
|
205
|
-
meta: {
|
|
206
|
-
codec: s.config.codec,
|
|
207
|
-
error: errMsg(err)
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
async pullPcm(input) {
|
|
213
|
-
const s = this.sessions.get(input.sessionId);
|
|
214
|
-
if (!s || s.kind !== "decode") throw new Error(`audio-codec: decode session '${input.sessionId}' not found`);
|
|
215
|
-
s.lastActivityMs = Date.now();
|
|
216
|
-
const out = s.pcmQueue.splice(0, input.maxCount);
|
|
217
|
-
s.framesOut += out.length;
|
|
218
|
-
return out;
|
|
219
|
-
}
|
|
220
|
-
async pushPcm(input) {
|
|
221
|
-
const s = this.sessions.get(input.sessionId);
|
|
222
|
-
if (!s || s.kind !== "encode") throw new Error(`audio-codec: encode session '${input.sessionId}' not found`);
|
|
223
|
-
s.lastActivityMs = Date.now();
|
|
224
|
-
s.framesIn++;
|
|
225
|
-
input.data;
|
|
226
|
-
input.pts;
|
|
227
|
-
}
|
|
228
|
-
async pullEncoded(input) {
|
|
229
|
-
const s = this.sessions.get(input.sessionId);
|
|
230
|
-
if (!s || s.kind !== "encode") throw new Error(`audio-codec: encode session '${input.sessionId}' not found`);
|
|
231
|
-
s.lastActivityMs = Date.now();
|
|
232
|
-
const out = s.encodedQueue.splice(0, input.maxCount);
|
|
233
|
-
s.framesOut += out.length;
|
|
234
|
-
return out;
|
|
235
|
-
}
|
|
236
|
-
async flushEncode(input) {
|
|
237
|
-
const s = this.sessions.get(input.sessionId);
|
|
238
|
-
if (!s || s.kind !== "encode") throw new Error(`audio-codec: encode session '${input.sessionId}' not found`);
|
|
239
|
-
s.lastActivityMs = Date.now();
|
|
240
|
-
const out = s.encodedQueue.splice(0);
|
|
241
|
-
s.framesOut += out.length;
|
|
242
|
-
return out;
|
|
243
|
-
}
|
|
244
|
-
async listActiveSessions() {
|
|
245
|
-
return [...this.sessions.values()].map((s) => ({
|
|
246
|
-
sessionId: s.sessionId,
|
|
247
|
-
kind: s.kind,
|
|
248
|
-
codec: s.config.codec,
|
|
249
|
-
sourceSampleRate: s.config.sourceSampleRate,
|
|
250
|
-
sourceChannels: s.config.sourceChannels,
|
|
251
|
-
targetSampleRate: s.config.targetSampleRate,
|
|
252
|
-
targetChannels: s.config.targetChannels,
|
|
253
|
-
format: this.resolveFormat(s),
|
|
254
|
-
...s.tag ? { tag: s.tag } : {},
|
|
255
|
-
createdAtMs: s.createdAtMs,
|
|
256
|
-
lastActivityMs: s.lastActivityMs,
|
|
257
|
-
framesIn: s.framesIn,
|
|
258
|
-
framesOut: s.framesOut
|
|
259
|
-
}));
|
|
260
|
-
}
|
|
261
|
-
resolveFormat(s) {
|
|
262
|
-
if (s.kind === "decode") return s.config.targetFormat ?? "f32le";
|
|
263
|
-
return s.config.sourceFormat ?? "f32le";
|
|
264
|
-
}
|
|
265
|
-
reapIdleSessions() {
|
|
266
|
-
const now = Date.now();
|
|
267
|
-
for (const [id, s] of this.sessions) {
|
|
268
|
-
const limit = s.config.idleMs ?? this.config.defaultIdleMs ?? DEFAULT_IDLE_MS;
|
|
269
|
-
if (now - s.lastActivityMs > limit) {
|
|
270
|
-
this.logger?.info("audio-codec: reaping idle session", {
|
|
271
|
-
tags: { sessionId: id },
|
|
272
|
-
meta: {
|
|
273
|
-
kind: s.kind,
|
|
274
|
-
idleMs: now - s.lastActivityMs,
|
|
275
|
-
limit
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
try {
|
|
279
|
-
this.disposeSession(s);
|
|
280
|
-
} catch (err) {
|
|
281
|
-
this.logger?.warn("audio-codec: dispose failed during reap", {
|
|
282
|
-
tags: { sessionId: id },
|
|
283
|
-
meta: { error: errMsg(err) }
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
this.sessions.delete(id);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
disposeSession(s) {
|
|
291
|
-
if (s.kind === "decode" && s.runtime) {
|
|
292
|
-
try {
|
|
293
|
-
s.runtime.close();
|
|
294
|
-
} catch (err) {
|
|
295
|
-
this.logger?.warn("audio-codec: decode runtime close failed", {
|
|
296
|
-
tags: { sessionId: s.sessionId },
|
|
297
|
-
meta: { error: errMsg(err) }
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
s.runtime = null;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
//#endregion
|
|
305
|
-
export { AudioCodecNodeAvAddon, AudioCodecNodeAvAddon as default };
|