@camstack/addon-pipeline 1.2.219 → 1.2.221
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 +661 -7
- package/dist/audio-analyzer/index.mjs +658 -5
- package/dist/chunk-emK7D4bc.js +35 -0
- package/dist/{default-detection-model-d7-Q_LMo.js → default-detection-model-B1NT1A38.js} +1 -1
- package/dist/{default-detection-model-Dsk1a5pY.mjs → default-detection-model-DkVFgubU.mjs} +1 -1
- package/dist/detection-pipeline/index.js +10 -9
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-wtNhtVVo.js → dist-ATDfcxgL.js} +282 -39
- package/dist/{dist-CKK0CBUW.mjs → dist-CEFC3PkP.mjs} +265 -5
- package/dist/{lazy-sharp-zoQ07I43.js → lazy-sharp-BuwKlQLR.js} +2 -2
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-BduNi21Z.js → node-C5jnTgpv.js} +4 -3
- package/dist/{node-CrNBWFbA.mjs → node-DR3fHqUq.mjs} +1 -1
- package/dist/pipeline-runner/index.js +222 -157
- package/dist/pipeline-runner/index.mjs +218 -154
- package/dist/{process-memory-Dwl0MFPH.js → process-memory-DQbCPnMA.js} +3 -2
- package/dist/{process-memory-BRQOtDN1.mjs → process-memory-DqYj5IAb.mjs} +1 -1
- package/dist/recorder/index.js +344 -127
- package/dist/recorder/index.mjs +342 -126
- package/dist/{segment-demux-js-D2o9ShNL.mjs → segment-demux-js-CddDwD69.mjs} +1 -1
- package/dist/{segment-demux-js-CFOU3biG.js → segment-demux-js-o9KBAz9p.js} +4 -3
- package/dist/session-decode/decode-worker-child.js +155 -74
- package/dist/session-decode/decode-worker-child.mjs +154 -73
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DN_IoShh.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D5XnLZWR.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CyMYDKfM.mjs +26 -0
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DICVQkNi.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CKUGpDns.mjs} +1 -1
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-n7c9pNlt.mjs → hostInit-C-VdcJS2.mjs} +3 -3
- package/dist/stream-broker/index.js +231 -112
- package/dist/stream-broker/index.mjs +224 -106
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/worker-protocol-B2MfQLlu.js +184 -0
- package/dist/worker-protocol-C_W-P_g-.mjs +161 -0
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-7hqrrd88.mjs +0 -26
- package/dist/worker-protocol-Cx3Nqreq.mjs +0 -264
- package/dist/worker-protocol-iWTFnnUM.js +0 -299
|
@@ -2,26 +2,27 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
+
const require_dist = require("../dist-ATDfcxgL.js");
|
|
7
|
+
const require_node = require("../node-C5jnTgpv.js");
|
|
7
8
|
const require_restream_intent = require("../restream-intent-B1Difcnq.js");
|
|
8
9
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
9
10
|
const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
|
|
10
|
-
const require_segment_demux_js = require("../segment-demux-js-
|
|
11
|
+
const require_segment_demux_js = require("../segment-demux-js-o9KBAz9p.js");
|
|
11
12
|
let node_os = require("node:os");
|
|
12
|
-
node_os =
|
|
13
|
+
node_os = require_chunk.__toESM(node_os);
|
|
13
14
|
let node_crypto = require("node:crypto");
|
|
14
|
-
node_crypto =
|
|
15
|
+
node_crypto = require_chunk.__toESM(node_crypto);
|
|
15
16
|
let node_fs = require("node:fs");
|
|
16
|
-
node_fs =
|
|
17
|
+
node_fs = require_chunk.__toESM(node_fs);
|
|
17
18
|
let node_path = require("node:path");
|
|
18
|
-
node_path =
|
|
19
|
+
node_path = require_chunk.__toESM(node_path);
|
|
19
20
|
let node_child_process = require("node:child_process");
|
|
20
21
|
let node_fs_promises = require("node:fs/promises");
|
|
21
22
|
let node_url = require("node:url");
|
|
22
23
|
let node_module = require("node:module");
|
|
23
24
|
let node_net = require("node:net");
|
|
24
|
-
node_net =
|
|
25
|
+
node_net = require_chunk.__toESM(node_net);
|
|
25
26
|
let node_dgram = require("node:dgram");
|
|
26
27
|
let events = require("events");
|
|
27
28
|
let net = require("net");
|
|
@@ -8380,112 +8381,74 @@ function depacketizeRfc3640(payload, opts = {
|
|
|
8380
8381
|
}
|
|
8381
8382
|
//#endregion
|
|
8382
8383
|
//#region src/stream-broker/rtsp/audio-rtp-decoder.ts
|
|
8383
|
-
/**
|
|
8384
|
-
var
|
|
8385
|
-
|
|
8384
|
+
/** G.711 is 8 kHz by definition (RFC 3551): the plane carries that rate. */
|
|
8385
|
+
var G711_SAMPLE_RATE = 8e3;
|
|
8386
|
+
/** One G.711 byte is one sample, so samples and bytes are the same count. */
|
|
8387
|
+
var CHUNK_BYTES = G711_SAMPLE_RATE * 500 / 1e3;
|
|
8388
|
+
/** The chunk `format` each RTP codec produces. */
|
|
8389
|
+
var CODEC_FORMAT = {
|
|
8390
|
+
PCMU: "pcmu",
|
|
8391
|
+
PCMA: "pcma"
|
|
8392
|
+
};
|
|
8386
8393
|
/**
|
|
8387
|
-
*
|
|
8394
|
+
* Chunks G.711 audio RTP payloads into ~0.5 s coded chunks at 8 kHz.
|
|
8388
8395
|
*
|
|
8389
|
-
* Buffers
|
|
8390
|
-
*
|
|
8396
|
+
* Buffers payload bytes and emits a `DecodedAudioChunk` carrying the source
|
|
8397
|
+
* codec once ~0.5 s has accumulated (4000 bytes).
|
|
8391
8398
|
*/
|
|
8392
8399
|
var AudioRtpDecoder = class {
|
|
8393
|
-
|
|
8400
|
+
format;
|
|
8394
8401
|
onChunk;
|
|
8395
8402
|
buffer;
|
|
8396
8403
|
bufferOffset = 0;
|
|
8397
8404
|
constructor(codec, onChunk) {
|
|
8398
|
-
this.
|
|
8405
|
+
this.format = CODEC_FORMAT[codec];
|
|
8399
8406
|
this.onChunk = onChunk;
|
|
8400
|
-
this.buffer =
|
|
8407
|
+
this.buffer = Buffer.allocUnsafe(CHUNK_BYTES);
|
|
8401
8408
|
}
|
|
8402
8409
|
/**
|
|
8403
|
-
* Process a raw RTP packet (header + payload).
|
|
8404
|
-
*
|
|
8410
|
+
* Process a raw RTP packet (header + payload). Strips the 12-byte RTP header
|
|
8411
|
+
* and appends the payload bytes verbatim, emitting whenever a chunk fills.
|
|
8412
|
+
* A payload larger than the remaining room spans chunks.
|
|
8405
8413
|
*/
|
|
8406
8414
|
processPacket(rtpData) {
|
|
8407
8415
|
if (rtpData.length <= 12) return;
|
|
8408
8416
|
const payload = rtpData.subarray(12);
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
const
|
|
8412
|
-
const
|
|
8413
|
-
this.
|
|
8414
|
-
this.
|
|
8417
|
+
let consumed = 0;
|
|
8418
|
+
while (consumed < payload.length) {
|
|
8419
|
+
const room = CHUNK_BYTES - this.bufferOffset;
|
|
8420
|
+
const take = Math.min(room, payload.length - consumed);
|
|
8421
|
+
payload.copy(this.buffer, this.bufferOffset, consumed, consumed + take);
|
|
8422
|
+
this.bufferOffset += take;
|
|
8423
|
+
consumed += take;
|
|
8424
|
+
if (this.bufferOffset >= CHUNK_BYTES) this.emit();
|
|
8415
8425
|
}
|
|
8416
8426
|
}
|
|
8417
|
-
/** Flush any remaining buffered
|
|
8427
|
+
/** Flush any remaining buffered bytes as a partial chunk. */
|
|
8418
8428
|
flush() {
|
|
8419
8429
|
if (this.bufferOffset === 0) return;
|
|
8420
|
-
|
|
8421
|
-
this.onChunk({
|
|
8422
|
-
data: Buffer.from(data),
|
|
8423
|
-
sampleRate: TARGET_SAMPLE_RATE,
|
|
8424
|
-
channels: 1,
|
|
8425
|
-
timestamp: Date.now()
|
|
8426
|
-
});
|
|
8427
|
-
this.bufferOffset = 0;
|
|
8430
|
+
this.emit();
|
|
8428
8431
|
}
|
|
8429
|
-
/** Reset the
|
|
8432
|
+
/** Reset the chunker state. */
|
|
8430
8433
|
reset() {
|
|
8431
8434
|
this.bufferOffset = 0;
|
|
8432
8435
|
}
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8436
|
+
/**
|
|
8437
|
+
* Emit what is buffered and start a fresh chunk. The bytes are COPIED out:
|
|
8438
|
+
* the accumulator is reused, and the plane holds every queued chunk by
|
|
8439
|
+
* reference until its consumer polls.
|
|
8440
|
+
*/
|
|
8441
|
+
emit() {
|
|
8442
|
+
this.onChunk({
|
|
8443
|
+
data: Buffer.from(this.buffer.subarray(0, this.bufferOffset)),
|
|
8444
|
+
sampleRate: G711_SAMPLE_RATE,
|
|
8445
|
+
channels: 1,
|
|
8446
|
+
timestamp: Date.now(),
|
|
8447
|
+
format: this.format
|
|
8448
|
+
});
|
|
8449
|
+
this.bufferOffset = 0;
|
|
8445
8450
|
}
|
|
8446
8451
|
};
|
|
8447
|
-
/** μ-law compressed byte → normalized float [-1.0, 1.0]. */
|
|
8448
|
-
function decodeUlaw(byte) {
|
|
8449
|
-
return ULAW_TABLE[byte];
|
|
8450
|
-
}
|
|
8451
|
-
/** A-law compressed byte → normalized float [-1.0, 1.0]. */
|
|
8452
|
-
function decodeAlaw(byte) {
|
|
8453
|
-
return ALAW_TABLE[byte];
|
|
8454
|
-
}
|
|
8455
|
-
/**
|
|
8456
|
-
* Build the μ-law decode table (ITU-T G.711).
|
|
8457
|
-
* Each of the 256 possible byte values maps to a 16-bit PCM sample,
|
|
8458
|
-
* which we normalize to [-1.0, 1.0] for f32le output.
|
|
8459
|
-
*/
|
|
8460
|
-
function buildUlawTable() {
|
|
8461
|
-
const table = new Float32Array(256);
|
|
8462
|
-
for (let i = 0; i < 256; i++) {
|
|
8463
|
-
const complemented = ~i & 255;
|
|
8464
|
-
const sign = (complemented & 128) !== 0 ? -1 : 1;
|
|
8465
|
-
const exponent = complemented >> 4 & 7;
|
|
8466
|
-
table[i] = sign * ((2 * (complemented & 15) + 33 << exponent) - 33) / 32768;
|
|
8467
|
-
}
|
|
8468
|
-
return table;
|
|
8469
|
-
}
|
|
8470
|
-
/**
|
|
8471
|
-
* Build the A-law decode table (ITU-T G.711).
|
|
8472
|
-
*/
|
|
8473
|
-
function buildAlawTable() {
|
|
8474
|
-
const table = new Float32Array(256);
|
|
8475
|
-
for (let i = 0; i < 256; i++) {
|
|
8476
|
-
const xored = i ^ 85;
|
|
8477
|
-
const sign = (xored & 128) !== 0 ? -1 : 1;
|
|
8478
|
-
const exponent = xored >> 4 & 7;
|
|
8479
|
-
const mantissa = xored & 15;
|
|
8480
|
-
let magnitude;
|
|
8481
|
-
if (exponent === 0) magnitude = 2 * mantissa + 1;
|
|
8482
|
-
else magnitude = 2 * mantissa + 33 << exponent - 1;
|
|
8483
|
-
table[i] = sign * magnitude / 32768;
|
|
8484
|
-
}
|
|
8485
|
-
return table;
|
|
8486
|
-
}
|
|
8487
|
-
var ULAW_TABLE = buildUlawTable();
|
|
8488
|
-
var ALAW_TABLE = buildAlawTable();
|
|
8489
8452
|
//#endregion
|
|
8490
8453
|
//#region src/stream-broker/rtsp/ffmpeg-source-reader.ts
|
|
8491
8454
|
/**
|
|
@@ -9483,6 +9446,47 @@ function extractQuoted(header, key) {
|
|
|
9483
9446
|
*/
|
|
9484
9447
|
var AUDIO_QUEUE_CAPACITY = 64;
|
|
9485
9448
|
/**
|
|
9449
|
+
* How long a subscription may go without a `pull` before the plane releases it
|
|
9450
|
+
* (D461).
|
|
9451
|
+
*
|
|
9452
|
+
* Until 2026-09-11 the one subscriber was `pipeline-orchestrator`, which the
|
|
9453
|
+
* hub restarts alongside the broker, and an abandoned subscription was a
|
|
9454
|
+
* theoretical leak. D461 moved the poller into `audio-analyzer`, which may sit
|
|
9455
|
+
* on another node and may die without the orchestrator's detach ever
|
|
9456
|
+
* arriving — and on THIS plane an orphan is not merely a queue nobody drains.
|
|
9457
|
+
* `subscriberCount` is what `hasDemand()` reads, so one orphan pins the
|
|
9458
|
+
* broker's source dial and its decode session open for the broker's whole
|
|
9459
|
+
* life: a camera nobody is listening to, kept streaming and decoding by a dead
|
|
9460
|
+
* process.
|
|
9461
|
+
*
|
|
9462
|
+
* The bound has to clear the longest LEGITIMATE gap between two polls. The
|
|
9463
|
+
* poller awaits its consumer per chunk, and the consumer is now the classifier
|
|
9464
|
+
* itself, whose `classify` carries a 30 s timeout — so a gap of tens of
|
|
9465
|
+
* seconds is healthy. Two minutes is past every one of those and well past the
|
|
9466
|
+
* ~30 s of audio the FIFO can hold, so a subscription this idle has already
|
|
9467
|
+
* lost audio and cannot be serving anyone.
|
|
9468
|
+
*/
|
|
9469
|
+
var AUDIO_SUBSCRIPTION_IDLE_MS = 12e4;
|
|
9470
|
+
/** `format` is optional on the wire and its absence means `f32le` (D455). */
|
|
9471
|
+
function formatOf(chunk) {
|
|
9472
|
+
return chunk.format ?? "f32le";
|
|
9473
|
+
}
|
|
9474
|
+
/**
|
|
9475
|
+
* Build the f32le twin of a coded chunk. Everything but `data` and `format`
|
|
9476
|
+
* is carried over — the rate, the channel count and the timestamp describe the
|
|
9477
|
+
* audio, not its encoding.
|
|
9478
|
+
*/
|
|
9479
|
+
function expandChunk(chunk, sourceFormat) {
|
|
9480
|
+
const bytes = require_dist.expandAudioChunkToF32le(chunk.data, sourceFormat);
|
|
9481
|
+
return {
|
|
9482
|
+
data: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength),
|
|
9483
|
+
sampleRate: chunk.sampleRate,
|
|
9484
|
+
channels: chunk.channels,
|
|
9485
|
+
timestamp: chunk.timestamp,
|
|
9486
|
+
format: "f32le"
|
|
9487
|
+
};
|
|
9488
|
+
}
|
|
9489
|
+
/**
|
|
9486
9490
|
* Owns the broker's decoded audio-chunk subscriptions. The broker feeds every
|
|
9487
9491
|
* decoded `DecodedAudioChunk` into `fanout`; each subscription's poll loop
|
|
9488
9492
|
* drains its own FIFO queue via `pull`.
|
|
@@ -9490,8 +9494,12 @@ var AUDIO_QUEUE_CAPACITY = 64;
|
|
|
9490
9494
|
var AudioChunkPlane = class {
|
|
9491
9495
|
logger;
|
|
9492
9496
|
subscriptions = /* @__PURE__ */ new Map();
|
|
9493
|
-
|
|
9497
|
+
/** Last `reapIdle` sweep — the lease is checked on a cadence, not per chunk. */
|
|
9498
|
+
lastReapAt = Date.now();
|
|
9499
|
+
onSubscriptionsReaped;
|
|
9500
|
+
constructor(logger, onSubscriptionsReaped) {
|
|
9494
9501
|
this.logger = logger;
|
|
9502
|
+
this.onSubscriptionsReaped = onSubscriptionsReaped;
|
|
9495
9503
|
}
|
|
9496
9504
|
/** Number of active audio-chunk subscriptions — surfaced as audio demand. */
|
|
9497
9505
|
get subscriberCount() {
|
|
@@ -9508,12 +9516,16 @@ var AudioChunkPlane = class {
|
|
|
9508
9516
|
tag: input?.tag ?? "unknown",
|
|
9509
9517
|
subscribedAt: Date.now(),
|
|
9510
9518
|
queue: new require_dist.RingBuffer(AUDIO_QUEUE_CAPACITY),
|
|
9511
|
-
|
|
9519
|
+
accept: new Set(input?.accept ?? []),
|
|
9520
|
+
servedKey: null,
|
|
9521
|
+
chunksDelivered: 0,
|
|
9522
|
+
lastPulledAt: Date.now()
|
|
9512
9523
|
};
|
|
9513
9524
|
this.subscriptions.set(subscriptionId, subscription);
|
|
9514
9525
|
this.logger?.info("audio-chunk subscription added", { meta: {
|
|
9515
9526
|
subscriptionId,
|
|
9516
|
-
tag: subscription.tag
|
|
9527
|
+
tag: subscription.tag,
|
|
9528
|
+
accept: input?.accept ?? []
|
|
9517
9529
|
} });
|
|
9518
9530
|
return { subscriptionId };
|
|
9519
9531
|
}
|
|
@@ -9525,10 +9537,44 @@ var AudioChunkPlane = class {
|
|
|
9525
9537
|
pull(subscriptionId, maxCount) {
|
|
9526
9538
|
const subscription = this.subscriptions.get(subscriptionId);
|
|
9527
9539
|
if (!subscription) return [];
|
|
9540
|
+
subscription.lastPulledAt = Date.now();
|
|
9528
9541
|
const chunks = subscription.queue.drain(maxCount);
|
|
9529
9542
|
subscription.chunksDelivered += chunks.length;
|
|
9530
9543
|
return chunks;
|
|
9531
9544
|
}
|
|
9545
|
+
/**
|
|
9546
|
+
* Release every subscription nobody has drained for
|
|
9547
|
+
* {@link AUDIO_SUBSCRIPTION_IDLE_MS}, and NAME each one (D461, D427).
|
|
9548
|
+
*
|
|
9549
|
+
* The lease is the net under the ownership split: the subscriber lives in a
|
|
9550
|
+
* process the orchestrator does not supervise, so a missed `detachDevice`
|
|
9551
|
+
* has to be survivable rather than trusted. It is deliberately driven from
|
|
9552
|
+
* `fanout` — the plane has no clock of its own, and the moment a dead
|
|
9553
|
+
* consumer costs anything is the moment there is a chunk to put in its
|
|
9554
|
+
* queue. A plane with nothing flowing has nothing to reclaim.
|
|
9555
|
+
*
|
|
9556
|
+
* Returns the ids released so the broker can re-evaluate its demand; an
|
|
9557
|
+
* orphan that is dropped but still counted would have fixed nothing.
|
|
9558
|
+
*/
|
|
9559
|
+
reapIdle(now = Date.now()) {
|
|
9560
|
+
const reaped = [];
|
|
9561
|
+
for (const subscription of [...this.subscriptions.values()]) {
|
|
9562
|
+
const idleMs = now - subscription.lastPulledAt;
|
|
9563
|
+
if (idleMs < 12e4) continue;
|
|
9564
|
+
this.subscriptions.delete(subscription.id);
|
|
9565
|
+
reaped.push(subscription.id);
|
|
9566
|
+
this.logger?.warn("audio-chunk subscription REAPED — nobody drained it", { meta: {
|
|
9567
|
+
subscriptionId: subscription.id,
|
|
9568
|
+
tag: subscription.tag,
|
|
9569
|
+
idleMs,
|
|
9570
|
+
leaseMs: AUDIO_SUBSCRIPTION_IDLE_MS,
|
|
9571
|
+
queued: subscription.queue.size,
|
|
9572
|
+
chunksDelivered: subscription.chunksDelivered,
|
|
9573
|
+
hint: "the consumer process died without detaching — its subscription was holding this broker in demand"
|
|
9574
|
+
} });
|
|
9575
|
+
}
|
|
9576
|
+
return reaped;
|
|
9577
|
+
}
|
|
9532
9578
|
/** Release a subscription. Returns `true` when a known subscription was released. */
|
|
9533
9579
|
unsubscribe(subscriptionId) {
|
|
9534
9580
|
const existed = this.subscriptions.delete(subscriptionId);
|
|
@@ -9539,9 +9585,67 @@ var AudioChunkPlane = class {
|
|
|
9539
9585
|
* Fan one decoded `DecodedAudioChunk` into every active subscription's FIFO
|
|
9540
9586
|
* queue. A full queue drops its oldest chunk (bounded safety valve for a
|
|
9541
9587
|
* stalled consumer). No-op when there are no subscriptions.
|
|
9588
|
+
*
|
|
9589
|
+
* ## The format each subscription gets (D455)
|
|
9590
|
+
*
|
|
9591
|
+
* The decoder now hands this plane the SOURCE bytes — one G.711 byte per
|
|
9592
|
+
* sample instead of four f32le ones — because the plane crosses hub-main
|
|
9593
|
+
* twice on its way to the analyzer and every leg was paying the expansion.
|
|
9594
|
+
* A subscription receives those bytes only if it DECLARED it can read them;
|
|
9595
|
+
* otherwise the plane expands to `f32le` here, which is byte-for-byte what
|
|
9596
|
+
* every consumer received before this existed.
|
|
9597
|
+
*
|
|
9598
|
+
* The expansion is done ONCE per fanout and shared by every subscription
|
|
9599
|
+
* that needs it: the chunk is immutable and the queue holds references.
|
|
9542
9600
|
*/
|
|
9543
9601
|
fanout(chunk) {
|
|
9544
|
-
|
|
9602
|
+
this.maybeReap();
|
|
9603
|
+
const sourceFormat = formatOf(chunk);
|
|
9604
|
+
let expanded = null;
|
|
9605
|
+
for (const subscription of this.subscriptions.values()) {
|
|
9606
|
+
if (sourceFormat === "f32le" || subscription.accept.has(sourceFormat)) {
|
|
9607
|
+
this.noteServed(subscription, sourceFormat, sourceFormat);
|
|
9608
|
+
subscription.queue.push(chunk);
|
|
9609
|
+
continue;
|
|
9610
|
+
}
|
|
9611
|
+
expanded ??= expandChunk(chunk, sourceFormat);
|
|
9612
|
+
this.noteServed(subscription, sourceFormat, "f32le");
|
|
9613
|
+
subscription.queue.push(expanded);
|
|
9614
|
+
}
|
|
9615
|
+
}
|
|
9616
|
+
/**
|
|
9617
|
+
* Run the idle-lease sweep on a cadence rather than on every chunk. A quarter
|
|
9618
|
+
* of the lease bounds how long an orphan outlives its bound by, and keeps
|
|
9619
|
+
* the per-chunk cost of `fanout` a single clock read.
|
|
9620
|
+
*/
|
|
9621
|
+
maybeReap() {
|
|
9622
|
+
const now = Date.now();
|
|
9623
|
+
if (now - this.lastReapAt < 12e4 / 4) return;
|
|
9624
|
+
this.lastReapAt = now;
|
|
9625
|
+
if (this.reapIdle(now).length > 0) this.onSubscriptionsReaped?.();
|
|
9626
|
+
}
|
|
9627
|
+
/**
|
|
9628
|
+
* Say what a subscription is being served — once, and again on every change.
|
|
9629
|
+
*
|
|
9630
|
+
* The audio decision of a relay is never silent (D427). Two things this line
|
|
9631
|
+
* answers that nothing else can: a consumer receiving a format it did not
|
|
9632
|
+
* expect (the failure that would otherwise surface as a level of +43 dBFS
|
|
9633
|
+
* with no cause), and a leg still paying 4x because its consumer declared no
|
|
9634
|
+
* `accept` — `expanded: true` is the byte cost, named where it is incurred.
|
|
9635
|
+
*/
|
|
9636
|
+
noteServed(subscription, sourceFormat, servedFormat) {
|
|
9637
|
+
const key = `${sourceFormat}>${servedFormat}`;
|
|
9638
|
+
if (subscription.servedKey === key) return;
|
|
9639
|
+
const previous = subscription.servedKey;
|
|
9640
|
+
subscription.servedKey = key;
|
|
9641
|
+
this.logger?.info("audio-chunk subscription serving", { meta: {
|
|
9642
|
+
subscriptionId: subscription.id,
|
|
9643
|
+
tag: subscription.tag,
|
|
9644
|
+
sourceFormat,
|
|
9645
|
+
servedFormat,
|
|
9646
|
+
expanded: servedFormat !== sourceFormat,
|
|
9647
|
+
previous
|
|
9648
|
+
} });
|
|
9545
9649
|
}
|
|
9546
9650
|
/** Diagnostic snapshot of every active audio-chunk subscription. */
|
|
9547
9651
|
listSubscribers() {
|
|
@@ -11839,7 +11943,7 @@ var StreamBroker = class StreamBroker {
|
|
|
11839
11943
|
});
|
|
11840
11944
|
this.preBuffer = new EncodedRingBuffer(StreamBroker.DEFAULT_PRE_BUFFER_SEC);
|
|
11841
11945
|
this.decoderSeedBuffer = new EncodedRingBuffer(StreamBroker.DECODER_SEED_BUFFER_SEC);
|
|
11842
|
-
this.audioChunkPlane = new AudioChunkPlane(logger?.child("audio-chunk-plane"));
|
|
11946
|
+
this.audioChunkPlane = new AudioChunkPlane(logger?.child("audio-chunk-plane"), () => this.checkDemand());
|
|
11843
11947
|
this.rtspRestreamer.onSessionCountChanged(() => this.checkDemand());
|
|
11844
11948
|
this.pipeServer.onClientCountChanged(() => this.checkDemand());
|
|
11845
11949
|
}
|
|
@@ -13194,7 +13298,10 @@ var StreamBroker = class StreamBroker {
|
|
|
13194
13298
|
subscribeAudioChunks(options) {
|
|
13195
13299
|
const tag = options?.tag;
|
|
13196
13300
|
if (!tag) this.logger?.warn(`subscribeAudioChunks called without tag — listClients will show 'unknown'`);
|
|
13197
|
-
const result = this.audioChunkPlane.subscribe({
|
|
13301
|
+
const result = this.audioChunkPlane.subscribe({
|
|
13302
|
+
tag,
|
|
13303
|
+
accept: options?.accept
|
|
13304
|
+
});
|
|
13198
13305
|
this.checkDemand();
|
|
13199
13306
|
return result;
|
|
13200
13307
|
}
|
|
@@ -13674,16 +13781,19 @@ var StreamBroker = class StreamBroker {
|
|
|
13674
13781
|
const codecUpper = audioTrack.codec.toUpperCase();
|
|
13675
13782
|
this.audioCodec = codecUpper;
|
|
13676
13783
|
const supportedNative = codecUpper === "PCMU" || codecUpper === "PCMA";
|
|
13677
|
-
this.logger?.info("audio: track detected", {
|
|
13678
|
-
|
|
13679
|
-
|
|
13680
|
-
|
|
13681
|
-
|
|
13682
|
-
|
|
13683
|
-
|
|
13684
|
-
|
|
13685
|
-
|
|
13686
|
-
|
|
13784
|
+
this.logger?.info("audio: track detected", {
|
|
13785
|
+
tags: { deviceId: this.numericDeviceId },
|
|
13786
|
+
meta: {
|
|
13787
|
+
codec: audioTrack.codec,
|
|
13788
|
+
codecUpper,
|
|
13789
|
+
clockRate: audioTrack.clockRate,
|
|
13790
|
+
channels: audioTrack.channels,
|
|
13791
|
+
fmtpKeys: Object.keys(audioTrack.fmtp),
|
|
13792
|
+
hasConfigHex: "config" in audioTrack.fmtp || "CONFIG" in audioTrack.fmtp,
|
|
13793
|
+
audioCodecApiAvailable: this.audioCodecApi !== null,
|
|
13794
|
+
supportedNative
|
|
13795
|
+
}
|
|
13796
|
+
});
|
|
13687
13797
|
let supported = supportedNative;
|
|
13688
13798
|
let effectiveSampleRate = audioTrack.clockRate;
|
|
13689
13799
|
let effectiveChannels = audioTrack.channels;
|
|
@@ -13691,11 +13801,17 @@ var StreamBroker = class StreamBroker {
|
|
|
13691
13801
|
this.audioRtpDecoder = new AudioRtpDecoder(codecUpper, (chunk) => {
|
|
13692
13802
|
this.audioChunkPlane.fanout(chunk);
|
|
13693
13803
|
});
|
|
13694
|
-
this.logger?.info("native: audio decoder initialized", {
|
|
13695
|
-
|
|
13696
|
-
|
|
13697
|
-
|
|
13698
|
-
|
|
13804
|
+
this.logger?.info("native: audio decoder initialized", {
|
|
13805
|
+
tags: { deviceId: this.numericDeviceId },
|
|
13806
|
+
meta: {
|
|
13807
|
+
codec: codecUpper,
|
|
13808
|
+
sampleRate: audioTrack.clockRate,
|
|
13809
|
+
channels: audioTrack.channels,
|
|
13810
|
+
planeSampleRate: G711_SAMPLE_RATE,
|
|
13811
|
+
planeFormat: codecUpper === "PCMA" ? "pcma" : "pcmu",
|
|
13812
|
+
preD459LevelDeficitDb: Math.round(require_dist.G711_SCALE_CORRECTION_DB[codecUpper === "PCMA" ? "PCMA" : "PCMU"] * 100) / 100
|
|
13813
|
+
}
|
|
13814
|
+
});
|
|
13699
13815
|
} else if (this.audioCodecApi) {
|
|
13700
13816
|
const capCfg = this.buildAudioCodecSessionConfig(audioTrack);
|
|
13701
13817
|
if (capCfg) {
|
|
@@ -17472,7 +17588,10 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17472
17588
|
await this.ensureBrokerForId(input.brokerId);
|
|
17473
17589
|
const broker = this.brokers.get(input.brokerId);
|
|
17474
17590
|
if (!broker) throw new Error(`stream-broker: no broker for "${input.brokerId}"`);
|
|
17475
|
-
const result = broker.subscribeAudioChunks({
|
|
17591
|
+
const result = broker.subscribeAudioChunks({
|
|
17592
|
+
tag: input.tag,
|
|
17593
|
+
accept: input.accept
|
|
17594
|
+
});
|
|
17476
17595
|
this.audioSubscriptionBroker.set(result.subscriptionId, input.brokerId);
|
|
17477
17596
|
return result;
|
|
17478
17597
|
}
|
|
@@ -27485,7 +27604,7 @@ async function demuxSegmentToPcmu(bytes, ffmpegPath = "ffmpeg") {
|
|
|
27485
27604
|
}
|
|
27486
27605
|
//#endregion
|
|
27487
27606
|
//#region src/stream-broker/recorded/segment-audio-js.ts
|
|
27488
|
-
var import_mp4box_all = /* @__PURE__ */
|
|
27607
|
+
var import_mp4box_all = /* @__PURE__ */ require_chunk.__toESM(require_segment_demux_js.require_mp4box_all());
|
|
27489
27608
|
/** True for an mp4box audio codec string that is Opus (`Opus`/`opus`). */
|
|
27490
27609
|
function isOpusCodec(codec) {
|
|
27491
27610
|
return /^opus$/i.test(codec);
|