@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.
Files changed (40) hide show
  1. package/dist/audio-analyzer/index.js +661 -7
  2. package/dist/audio-analyzer/index.mjs +658 -5
  3. package/dist/chunk-emK7D4bc.js +35 -0
  4. package/dist/{default-detection-model-d7-Q_LMo.js → default-detection-model-B1NT1A38.js} +1 -1
  5. package/dist/{default-detection-model-Dsk1a5pY.mjs → default-detection-model-DkVFgubU.mjs} +1 -1
  6. package/dist/detection-pipeline/index.js +10 -9
  7. package/dist/detection-pipeline/index.mjs +4 -4
  8. package/dist/{dist-wtNhtVVo.js → dist-ATDfcxgL.js} +282 -39
  9. package/dist/{dist-CKK0CBUW.mjs → dist-CEFC3PkP.mjs} +265 -5
  10. package/dist/{lazy-sharp-zoQ07I43.js → lazy-sharp-BuwKlQLR.js} +2 -2
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-BduNi21Z.js → node-C5jnTgpv.js} +4 -3
  14. package/dist/{node-CrNBWFbA.mjs → node-DR3fHqUq.mjs} +1 -1
  15. package/dist/pipeline-runner/index.js +222 -157
  16. package/dist/pipeline-runner/index.mjs +218 -154
  17. package/dist/{process-memory-Dwl0MFPH.js → process-memory-DQbCPnMA.js} +3 -2
  18. package/dist/{process-memory-BRQOtDN1.mjs → process-memory-DqYj5IAb.mjs} +1 -1
  19. package/dist/recorder/index.js +344 -127
  20. package/dist/recorder/index.mjs +342 -126
  21. package/dist/{segment-demux-js-D2o9ShNL.mjs → segment-demux-js-CddDwD69.mjs} +1 -1
  22. package/dist/{segment-demux-js-CFOU3biG.js → segment-demux-js-o9KBAz9p.js} +4 -3
  23. package/dist/session-decode/decode-worker-child.js +155 -74
  24. package/dist/session-decode/decode-worker-child.mjs +154 -73
  25. package/dist/stream-broker/_stub.js +2 -2
  26. 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
  27. 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
  28. 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
  29. package/dist/stream-broker/demux-worker-child.js +1 -1
  30. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  31. package/dist/stream-broker/{hostInit-n7c9pNlt.mjs → hostInit-C-VdcJS2.mjs} +3 -3
  32. package/dist/stream-broker/index.js +231 -112
  33. package/dist/stream-broker/index.mjs +224 -106
  34. package/dist/stream-broker/remoteEntry.js +1 -1
  35. package/dist/worker-protocol-B2MfQLlu.js +184 -0
  36. package/dist/worker-protocol-C_W-P_g-.mjs +161 -0
  37. package/package.json +1 -1
  38. 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
  39. package/dist/worker-protocol-Cx3Nqreq.mjs +0 -264
  40. package/dist/worker-protocol-iWTFnnUM.js +0 -299
@@ -1,10 +1,10 @@
1
1
  import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
2
- import { A as addonWidgetsSourceCapability, Bt as invocationFromEncodeProfile, C as RATE_CONTROL_RELAXED, F as cameraStreamsCapability, Ft as webrtcSessionCapability, Gt as DeviceFeature, H as deriveBatteryPresence, Ht as BOOT_RECOVERY_BACKOFF_MS, I as createHwAccelCache, It as errMsg, K as egressTranscodeSharingKey, Kt as DeviceType, L as createLogChannelsProvider, Lt as AUDIO_PRESETS, Mt as streamBrokerCapability, O as RingBuffer, P as batteryCapability, Qt as nodePin, Tt as resolveEgressDecodeHwAccel, Ut as BaseAddon, Vt as isSoftwareDecode, Wt as CAM_PROFILE_ORDER, Xt as makeProfileBrokerId, Yt as isEvent, Zt as makeSourceBrokerId, an as boolean, at as logChannelsCapability, cn as literal, en as parseProfileBrokerId, fn as record, h as EncodeProfileSchema, hn as EventCategory, i as BatteryStatusSchema, in as array, it as loadContributionCapability, ln as number, mn as union, on as discriminatedUnion, pn as string, q as egressTransportFromRequest, qt as createEvent, rn as _enum, st as maskUrlCredentials, un as object, w as RATE_CONTROL_TIGHT, z as declareLogChannel, zt as buildFfmpegArgs$1 } from "../dist-CKK0CBUW.mjs";
3
- import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-CrNBWFbA.mjs";
2
+ import { $t as makeProfileBrokerId, B as declareLogChannel, Bt as buildFfmpegArgs$1, Et as resolveEgressDecodeHwAccel, F as batteryCapability, Gt as CAM_PROFILE_ORDER, Ht as isSoftwareDecode, I as cameraStreamsCapability, It as webrtcSessionCapability, J as egressTransportFromRequest, Kt as DeviceFeature, L as createHwAccelCache, Lt as errMsg, Nt as streamBrokerCapability, Qt as isEvent, R as createLogChannelsProvider, Rt as AUDIO_PRESETS, T as RATE_CONTROL_TIGHT, U as deriveBatteryPresence, Ut as BOOT_RECOVERY_BACKOFF_MS, Vt as invocationFromEncodeProfile, Wt as BaseAddon, Xt as expandAudioChunkToF32le, Yt as createEvent, _n as union, at as loadContributionCapability, cn as boolean, ct as maskUrlCredentials, dn as literal, en as makeSourceBrokerId, fn as number, gn as string, h as EncodeProfileSchema, hn as record, i as BatteryStatusSchema, j as addonWidgetsSourceCapability, k as RingBuffer, ln as discriminatedUnion, on as _enum, ot as logChannelsCapability, pn as object, q as egressTranscodeSharingKey, qt as DeviceType, rn as parseProfileBrokerId, sn as array, tn as nodePin, v as G711_SCALE_CORRECTION_DB, vn as EventCategory, w as RATE_CONTROL_RELAXED } from "../dist-CEFC3PkP.mjs";
3
+ import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-DR3fHqUq.mjs";
4
4
  import { c as PREBUFFER_MAX_MS, d as resolvePrebuffer, i as parseRestreamPath, l as capPreRollRequestMs, n as NO_DECLARATION, u as prebufferRetentionMs } from "../restream-intent-DWe2kInY.mjs";
5
5
  import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
6
6
  import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
7
- import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-D2o9ShNL.mjs";
7
+ import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-CddDwD69.mjs";
8
8
  import { createRequire } from "node:module";
9
9
  import * as os from "node:os";
10
10
  import { networkInterfaces, tmpdir } from "node:os";
@@ -8377,112 +8377,74 @@ function depacketizeRfc3640(payload, opts = {
8377
8377
  }
8378
8378
  //#endregion
8379
8379
  //#region src/stream-broker/rtsp/audio-rtp-decoder.ts
8380
- /** Target sample rate for decoded audio (matches ffmpeg path). */
8381
- var TARGET_SAMPLE_RATE = 16e3;
8382
- var CHUNK_SAMPLES = TARGET_SAMPLE_RATE * 500 / 1e3;
8380
+ /** G.711 is 8 kHz by definition (RFC 3551): the plane carries that rate. */
8381
+ var G711_SAMPLE_RATE = 8e3;
8382
+ /** One G.711 byte is one sample, so samples and bytes are the same count. */
8383
+ var CHUNK_BYTES = G711_SAMPLE_RATE * 500 / 1e3;
8384
+ /** The chunk `format` each RTP codec produces. */
8385
+ var CODEC_FORMAT = {
8386
+ PCMU: "pcmu",
8387
+ PCMA: "pcma"
8388
+ };
8383
8389
  /**
8384
- * Decodes G.711 audio RTP packets into PCM f32le chunks.
8390
+ * Chunks G.711 audio RTP payloads into ~0.5 s coded chunks at 8 kHz.
8385
8391
  *
8386
- * Buffers decoded samples and emits DecodedAudioChunk when enough
8387
- * samples accumulate (~0.5s), matching the ffmpeg audio output cadence.
8392
+ * Buffers payload bytes and emits a `DecodedAudioChunk` carrying the source
8393
+ * codec once ~0.5 s has accumulated (4000 bytes).
8388
8394
  */
8389
8395
  var AudioRtpDecoder = class {
8390
- codec;
8396
+ format;
8391
8397
  onChunk;
8392
8398
  buffer;
8393
8399
  bufferOffset = 0;
8394
8400
  constructor(codec, onChunk) {
8395
- this.codec = codec;
8401
+ this.format = CODEC_FORMAT[codec];
8396
8402
  this.onChunk = onChunk;
8397
- this.buffer = new Float32Array(CHUNK_SAMPLES);
8403
+ this.buffer = Buffer.allocUnsafe(CHUNK_BYTES);
8398
8404
  }
8399
8405
  /**
8400
- * Process a raw RTP packet (header + payload).
8401
- * Strips the 12-byte RTP header, decodes G.711 payload, upsamples 8→16kHz.
8406
+ * Process a raw RTP packet (header + payload). Strips the 12-byte RTP header
8407
+ * and appends the payload bytes verbatim, emitting whenever a chunk fills.
8408
+ * A payload larger than the remaining room spans chunks.
8402
8409
  */
8403
8410
  processPacket(rtpData) {
8404
8411
  if (rtpData.length <= 12) return;
8405
8412
  const payload = rtpData.subarray(12);
8406
- const decode = this.codec === "PCMU" ? decodeUlaw : decodeAlaw;
8407
- for (let i = 0; i < payload.length; i++) {
8408
- const current = decode(payload[i]);
8409
- const next = i + 1 < payload.length ? decode(payload[i + 1]) : current;
8410
- this.pushSample(current);
8411
- this.pushSample((current + next) * .5);
8413
+ let consumed = 0;
8414
+ while (consumed < payload.length) {
8415
+ const room = CHUNK_BYTES - this.bufferOffset;
8416
+ const take = Math.min(room, payload.length - consumed);
8417
+ payload.copy(this.buffer, this.bufferOffset, consumed, consumed + take);
8418
+ this.bufferOffset += take;
8419
+ consumed += take;
8420
+ if (this.bufferOffset >= CHUNK_BYTES) this.emit();
8412
8421
  }
8413
8422
  }
8414
- /** Flush any remaining buffered samples as a partial chunk. */
8423
+ /** Flush any remaining buffered bytes as a partial chunk. */
8415
8424
  flush() {
8416
8425
  if (this.bufferOffset === 0) return;
8417
- const data = Buffer.from(this.buffer.buffer, this.buffer.byteOffset, this.bufferOffset * 4);
8418
- this.onChunk({
8419
- data: Buffer.from(data),
8420
- sampleRate: TARGET_SAMPLE_RATE,
8421
- channels: 1,
8422
- timestamp: Date.now()
8423
- });
8424
- this.bufferOffset = 0;
8426
+ this.emit();
8425
8427
  }
8426
- /** Reset the decoder state. */
8428
+ /** Reset the chunker state. */
8427
8429
  reset() {
8428
8430
  this.bufferOffset = 0;
8429
8431
  }
8430
- pushSample(sample) {
8431
- this.buffer[this.bufferOffset++] = sample;
8432
- if (this.bufferOffset >= CHUNK_SAMPLES) {
8433
- const data = Buffer.from(this.buffer.buffer, this.buffer.byteOffset, CHUNK_SAMPLES * 4);
8434
- this.onChunk({
8435
- data: Buffer.from(data),
8436
- sampleRate: TARGET_SAMPLE_RATE,
8437
- channels: 1,
8438
- timestamp: Date.now()
8439
- });
8440
- this.bufferOffset = 0;
8441
- }
8432
+ /**
8433
+ * Emit what is buffered and start a fresh chunk. The bytes are COPIED out:
8434
+ * the accumulator is reused, and the plane holds every queued chunk by
8435
+ * reference until its consumer polls.
8436
+ */
8437
+ emit() {
8438
+ this.onChunk({
8439
+ data: Buffer.from(this.buffer.subarray(0, this.bufferOffset)),
8440
+ sampleRate: G711_SAMPLE_RATE,
8441
+ channels: 1,
8442
+ timestamp: Date.now(),
8443
+ format: this.format
8444
+ });
8445
+ this.bufferOffset = 0;
8442
8446
  }
8443
8447
  };
8444
- /** μ-law compressed byte → normalized float [-1.0, 1.0]. */
8445
- function decodeUlaw(byte) {
8446
- return ULAW_TABLE[byte];
8447
- }
8448
- /** A-law compressed byte → normalized float [-1.0, 1.0]. */
8449
- function decodeAlaw(byte) {
8450
- return ALAW_TABLE[byte];
8451
- }
8452
- /**
8453
- * Build the μ-law decode table (ITU-T G.711).
8454
- * Each of the 256 possible byte values maps to a 16-bit PCM sample,
8455
- * which we normalize to [-1.0, 1.0] for f32le output.
8456
- */
8457
- function buildUlawTable() {
8458
- const table = new Float32Array(256);
8459
- for (let i = 0; i < 256; i++) {
8460
- const complemented = ~i & 255;
8461
- const sign = (complemented & 128) !== 0 ? -1 : 1;
8462
- const exponent = complemented >> 4 & 7;
8463
- table[i] = sign * ((2 * (complemented & 15) + 33 << exponent) - 33) / 32768;
8464
- }
8465
- return table;
8466
- }
8467
- /**
8468
- * Build the A-law decode table (ITU-T G.711).
8469
- */
8470
- function buildAlawTable() {
8471
- const table = new Float32Array(256);
8472
- for (let i = 0; i < 256; i++) {
8473
- const xored = i ^ 85;
8474
- const sign = (xored & 128) !== 0 ? -1 : 1;
8475
- const exponent = xored >> 4 & 7;
8476
- const mantissa = xored & 15;
8477
- let magnitude;
8478
- if (exponent === 0) magnitude = 2 * mantissa + 1;
8479
- else magnitude = 2 * mantissa + 33 << exponent - 1;
8480
- table[i] = sign * magnitude / 32768;
8481
- }
8482
- return table;
8483
- }
8484
- var ULAW_TABLE = buildUlawTable();
8485
- var ALAW_TABLE = buildAlawTable();
8486
8448
  //#endregion
8487
8449
  //#region src/stream-broker/rtsp/ffmpeg-source-reader.ts
8488
8450
  /**
@@ -9480,6 +9442,47 @@ function extractQuoted(header, key) {
9480
9442
  */
9481
9443
  var AUDIO_QUEUE_CAPACITY = 64;
9482
9444
  /**
9445
+ * How long a subscription may go without a `pull` before the plane releases it
9446
+ * (D461).
9447
+ *
9448
+ * Until 2026-09-11 the one subscriber was `pipeline-orchestrator`, which the
9449
+ * hub restarts alongside the broker, and an abandoned subscription was a
9450
+ * theoretical leak. D461 moved the poller into `audio-analyzer`, which may sit
9451
+ * on another node and may die without the orchestrator's detach ever
9452
+ * arriving — and on THIS plane an orphan is not merely a queue nobody drains.
9453
+ * `subscriberCount` is what `hasDemand()` reads, so one orphan pins the
9454
+ * broker's source dial and its decode session open for the broker's whole
9455
+ * life: a camera nobody is listening to, kept streaming and decoding by a dead
9456
+ * process.
9457
+ *
9458
+ * The bound has to clear the longest LEGITIMATE gap between two polls. The
9459
+ * poller awaits its consumer per chunk, and the consumer is now the classifier
9460
+ * itself, whose `classify` carries a 30 s timeout — so a gap of tens of
9461
+ * seconds is healthy. Two minutes is past every one of those and well past the
9462
+ * ~30 s of audio the FIFO can hold, so a subscription this idle has already
9463
+ * lost audio and cannot be serving anyone.
9464
+ */
9465
+ var AUDIO_SUBSCRIPTION_IDLE_MS = 12e4;
9466
+ /** `format` is optional on the wire and its absence means `f32le` (D455). */
9467
+ function formatOf(chunk) {
9468
+ return chunk.format ?? "f32le";
9469
+ }
9470
+ /**
9471
+ * Build the f32le twin of a coded chunk. Everything but `data` and `format`
9472
+ * is carried over — the rate, the channel count and the timestamp describe the
9473
+ * audio, not its encoding.
9474
+ */
9475
+ function expandChunk(chunk, sourceFormat) {
9476
+ const bytes = expandAudioChunkToF32le(chunk.data, sourceFormat);
9477
+ return {
9478
+ data: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength),
9479
+ sampleRate: chunk.sampleRate,
9480
+ channels: chunk.channels,
9481
+ timestamp: chunk.timestamp,
9482
+ format: "f32le"
9483
+ };
9484
+ }
9485
+ /**
9483
9486
  * Owns the broker's decoded audio-chunk subscriptions. The broker feeds every
9484
9487
  * decoded `DecodedAudioChunk` into `fanout`; each subscription's poll loop
9485
9488
  * drains its own FIFO queue via `pull`.
@@ -9487,8 +9490,12 @@ var AUDIO_QUEUE_CAPACITY = 64;
9487
9490
  var AudioChunkPlane = class {
9488
9491
  logger;
9489
9492
  subscriptions = /* @__PURE__ */ new Map();
9490
- constructor(logger) {
9493
+ /** Last `reapIdle` sweep — the lease is checked on a cadence, not per chunk. */
9494
+ lastReapAt = Date.now();
9495
+ onSubscriptionsReaped;
9496
+ constructor(logger, onSubscriptionsReaped) {
9491
9497
  this.logger = logger;
9498
+ this.onSubscriptionsReaped = onSubscriptionsReaped;
9492
9499
  }
9493
9500
  /** Number of active audio-chunk subscriptions — surfaced as audio demand. */
9494
9501
  get subscriberCount() {
@@ -9505,12 +9512,16 @@ var AudioChunkPlane = class {
9505
9512
  tag: input?.tag ?? "unknown",
9506
9513
  subscribedAt: Date.now(),
9507
9514
  queue: new RingBuffer(AUDIO_QUEUE_CAPACITY),
9508
- chunksDelivered: 0
9515
+ accept: new Set(input?.accept ?? []),
9516
+ servedKey: null,
9517
+ chunksDelivered: 0,
9518
+ lastPulledAt: Date.now()
9509
9519
  };
9510
9520
  this.subscriptions.set(subscriptionId, subscription);
9511
9521
  this.logger?.info("audio-chunk subscription added", { meta: {
9512
9522
  subscriptionId,
9513
- tag: subscription.tag
9523
+ tag: subscription.tag,
9524
+ accept: input?.accept ?? []
9514
9525
  } });
9515
9526
  return { subscriptionId };
9516
9527
  }
@@ -9522,10 +9533,44 @@ var AudioChunkPlane = class {
9522
9533
  pull(subscriptionId, maxCount) {
9523
9534
  const subscription = this.subscriptions.get(subscriptionId);
9524
9535
  if (!subscription) return [];
9536
+ subscription.lastPulledAt = Date.now();
9525
9537
  const chunks = subscription.queue.drain(maxCount);
9526
9538
  subscription.chunksDelivered += chunks.length;
9527
9539
  return chunks;
9528
9540
  }
9541
+ /**
9542
+ * Release every subscription nobody has drained for
9543
+ * {@link AUDIO_SUBSCRIPTION_IDLE_MS}, and NAME each one (D461, D427).
9544
+ *
9545
+ * The lease is the net under the ownership split: the subscriber lives in a
9546
+ * process the orchestrator does not supervise, so a missed `detachDevice`
9547
+ * has to be survivable rather than trusted. It is deliberately driven from
9548
+ * `fanout` — the plane has no clock of its own, and the moment a dead
9549
+ * consumer costs anything is the moment there is a chunk to put in its
9550
+ * queue. A plane with nothing flowing has nothing to reclaim.
9551
+ *
9552
+ * Returns the ids released so the broker can re-evaluate its demand; an
9553
+ * orphan that is dropped but still counted would have fixed nothing.
9554
+ */
9555
+ reapIdle(now = Date.now()) {
9556
+ const reaped = [];
9557
+ for (const subscription of [...this.subscriptions.values()]) {
9558
+ const idleMs = now - subscription.lastPulledAt;
9559
+ if (idleMs < 12e4) continue;
9560
+ this.subscriptions.delete(subscription.id);
9561
+ reaped.push(subscription.id);
9562
+ this.logger?.warn("audio-chunk subscription REAPED — nobody drained it", { meta: {
9563
+ subscriptionId: subscription.id,
9564
+ tag: subscription.tag,
9565
+ idleMs,
9566
+ leaseMs: AUDIO_SUBSCRIPTION_IDLE_MS,
9567
+ queued: subscription.queue.size,
9568
+ chunksDelivered: subscription.chunksDelivered,
9569
+ hint: "the consumer process died without detaching — its subscription was holding this broker in demand"
9570
+ } });
9571
+ }
9572
+ return reaped;
9573
+ }
9529
9574
  /** Release a subscription. Returns `true` when a known subscription was released. */
9530
9575
  unsubscribe(subscriptionId) {
9531
9576
  const existed = this.subscriptions.delete(subscriptionId);
@@ -9536,9 +9581,67 @@ var AudioChunkPlane = class {
9536
9581
  * Fan one decoded `DecodedAudioChunk` into every active subscription's FIFO
9537
9582
  * queue. A full queue drops its oldest chunk (bounded safety valve for a
9538
9583
  * stalled consumer). No-op when there are no subscriptions.
9584
+ *
9585
+ * ## The format each subscription gets (D455)
9586
+ *
9587
+ * The decoder now hands this plane the SOURCE bytes — one G.711 byte per
9588
+ * sample instead of four f32le ones — because the plane crosses hub-main
9589
+ * twice on its way to the analyzer and every leg was paying the expansion.
9590
+ * A subscription receives those bytes only if it DECLARED it can read them;
9591
+ * otherwise the plane expands to `f32le` here, which is byte-for-byte what
9592
+ * every consumer received before this existed.
9593
+ *
9594
+ * The expansion is done ONCE per fanout and shared by every subscription
9595
+ * that needs it: the chunk is immutable and the queue holds references.
9539
9596
  */
9540
9597
  fanout(chunk) {
9541
- for (const subscription of this.subscriptions.values()) subscription.queue.push(chunk);
9598
+ this.maybeReap();
9599
+ const sourceFormat = formatOf(chunk);
9600
+ let expanded = null;
9601
+ for (const subscription of this.subscriptions.values()) {
9602
+ if (sourceFormat === "f32le" || subscription.accept.has(sourceFormat)) {
9603
+ this.noteServed(subscription, sourceFormat, sourceFormat);
9604
+ subscription.queue.push(chunk);
9605
+ continue;
9606
+ }
9607
+ expanded ??= expandChunk(chunk, sourceFormat);
9608
+ this.noteServed(subscription, sourceFormat, "f32le");
9609
+ subscription.queue.push(expanded);
9610
+ }
9611
+ }
9612
+ /**
9613
+ * Run the idle-lease sweep on a cadence rather than on every chunk. A quarter
9614
+ * of the lease bounds how long an orphan outlives its bound by, and keeps
9615
+ * the per-chunk cost of `fanout` a single clock read.
9616
+ */
9617
+ maybeReap() {
9618
+ const now = Date.now();
9619
+ if (now - this.lastReapAt < 12e4 / 4) return;
9620
+ this.lastReapAt = now;
9621
+ if (this.reapIdle(now).length > 0) this.onSubscriptionsReaped?.();
9622
+ }
9623
+ /**
9624
+ * Say what a subscription is being served — once, and again on every change.
9625
+ *
9626
+ * The audio decision of a relay is never silent (D427). Two things this line
9627
+ * answers that nothing else can: a consumer receiving a format it did not
9628
+ * expect (the failure that would otherwise surface as a level of +43 dBFS
9629
+ * with no cause), and a leg still paying 4x because its consumer declared no
9630
+ * `accept` — `expanded: true` is the byte cost, named where it is incurred.
9631
+ */
9632
+ noteServed(subscription, sourceFormat, servedFormat) {
9633
+ const key = `${sourceFormat}>${servedFormat}`;
9634
+ if (subscription.servedKey === key) return;
9635
+ const previous = subscription.servedKey;
9636
+ subscription.servedKey = key;
9637
+ this.logger?.info("audio-chunk subscription serving", { meta: {
9638
+ subscriptionId: subscription.id,
9639
+ tag: subscription.tag,
9640
+ sourceFormat,
9641
+ servedFormat,
9642
+ expanded: servedFormat !== sourceFormat,
9643
+ previous
9644
+ } });
9542
9645
  }
9543
9646
  /** Diagnostic snapshot of every active audio-chunk subscription. */
9544
9647
  listSubscribers() {
@@ -11836,7 +11939,7 @@ var StreamBroker = class StreamBroker {
11836
11939
  });
11837
11940
  this.preBuffer = new EncodedRingBuffer(StreamBroker.DEFAULT_PRE_BUFFER_SEC);
11838
11941
  this.decoderSeedBuffer = new EncodedRingBuffer(StreamBroker.DECODER_SEED_BUFFER_SEC);
11839
- this.audioChunkPlane = new AudioChunkPlane(logger?.child("audio-chunk-plane"));
11942
+ this.audioChunkPlane = new AudioChunkPlane(logger?.child("audio-chunk-plane"), () => this.checkDemand());
11840
11943
  this.rtspRestreamer.onSessionCountChanged(() => this.checkDemand());
11841
11944
  this.pipeServer.onClientCountChanged(() => this.checkDemand());
11842
11945
  }
@@ -13191,7 +13294,10 @@ var StreamBroker = class StreamBroker {
13191
13294
  subscribeAudioChunks(options) {
13192
13295
  const tag = options?.tag;
13193
13296
  if (!tag) this.logger?.warn(`subscribeAudioChunks called without tag — listClients will show 'unknown'`);
13194
- const result = this.audioChunkPlane.subscribe({ tag });
13297
+ const result = this.audioChunkPlane.subscribe({
13298
+ tag,
13299
+ accept: options?.accept
13300
+ });
13195
13301
  this.checkDemand();
13196
13302
  return result;
13197
13303
  }
@@ -13671,16 +13777,19 @@ var StreamBroker = class StreamBroker {
13671
13777
  const codecUpper = audioTrack.codec.toUpperCase();
13672
13778
  this.audioCodec = codecUpper;
13673
13779
  const supportedNative = codecUpper === "PCMU" || codecUpper === "PCMA";
13674
- this.logger?.info("audio: track detected", { meta: {
13675
- codec: audioTrack.codec,
13676
- codecUpper,
13677
- clockRate: audioTrack.clockRate,
13678
- channels: audioTrack.channels,
13679
- fmtpKeys: Object.keys(audioTrack.fmtp),
13680
- hasConfigHex: "config" in audioTrack.fmtp || "CONFIG" in audioTrack.fmtp,
13681
- audioCodecApiAvailable: this.audioCodecApi !== null,
13682
- supportedNative
13683
- } });
13780
+ this.logger?.info("audio: track detected", {
13781
+ tags: { deviceId: this.numericDeviceId },
13782
+ meta: {
13783
+ codec: audioTrack.codec,
13784
+ codecUpper,
13785
+ clockRate: audioTrack.clockRate,
13786
+ channels: audioTrack.channels,
13787
+ fmtpKeys: Object.keys(audioTrack.fmtp),
13788
+ hasConfigHex: "config" in audioTrack.fmtp || "CONFIG" in audioTrack.fmtp,
13789
+ audioCodecApiAvailable: this.audioCodecApi !== null,
13790
+ supportedNative
13791
+ }
13792
+ });
13684
13793
  let supported = supportedNative;
13685
13794
  let effectiveSampleRate = audioTrack.clockRate;
13686
13795
  let effectiveChannels = audioTrack.channels;
@@ -13688,11 +13797,17 @@ var StreamBroker = class StreamBroker {
13688
13797
  this.audioRtpDecoder = new AudioRtpDecoder(codecUpper, (chunk) => {
13689
13798
  this.audioChunkPlane.fanout(chunk);
13690
13799
  });
13691
- this.logger?.info("native: audio decoder initialized", { meta: {
13692
- codec: codecUpper,
13693
- sampleRate: audioTrack.clockRate,
13694
- channels: audioTrack.channels
13695
- } });
13800
+ this.logger?.info("native: audio decoder initialized", {
13801
+ tags: { deviceId: this.numericDeviceId },
13802
+ meta: {
13803
+ codec: codecUpper,
13804
+ sampleRate: audioTrack.clockRate,
13805
+ channels: audioTrack.channels,
13806
+ planeSampleRate: G711_SAMPLE_RATE,
13807
+ planeFormat: codecUpper === "PCMA" ? "pcma" : "pcmu",
13808
+ preD459LevelDeficitDb: Math.round(G711_SCALE_CORRECTION_DB[codecUpper === "PCMA" ? "PCMA" : "PCMU"] * 100) / 100
13809
+ }
13810
+ });
13696
13811
  } else if (this.audioCodecApi) {
13697
13812
  const capCfg = this.buildAudioCodecSessionConfig(audioTrack);
13698
13813
  if (capCfg) {
@@ -17469,7 +17584,10 @@ var StreamBrokerManager = class StreamBrokerManager {
17469
17584
  await this.ensureBrokerForId(input.brokerId);
17470
17585
  const broker = this.brokers.get(input.brokerId);
17471
17586
  if (!broker) throw new Error(`stream-broker: no broker for "${input.brokerId}"`);
17472
- const result = broker.subscribeAudioChunks({ tag: input.tag });
17587
+ const result = broker.subscribeAudioChunks({
17588
+ tag: input.tag,
17589
+ accept: input.accept
17590
+ });
17473
17591
  this.audioSubscriptionBroker.set(result.subscriptionId, input.brokerId);
17474
17592
  return result;
17475
17593
  }
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DN_IoShh.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D5XnLZWR.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
@@ -0,0 +1,184 @@
1
+ //#region src/session-decode/native-lease-knobs.ts
2
+ /** Every knob, in the order the spawn line names them. */
3
+ var NATIVE_LEASE_KNOB_NAMES = [
4
+ "holdFrames",
5
+ "budgetMb",
6
+ "activityMs",
7
+ "admission",
8
+ "tileBudgetMb",
9
+ "sceneBudgetMb"
10
+ ];
11
+ /** The numeric knobs — every knob but `admission`. */
12
+ var NATIVE_LEASE_NUMBER_KNOBS = [
13
+ "holdFrames",
14
+ "budgetMb",
15
+ "activityMs",
16
+ "tileBudgetMb",
17
+ "sceneBudgetMb"
18
+ ];
19
+ var KNOB_SOURCES = [
20
+ "setting",
21
+ "env",
22
+ "default"
23
+ ];
24
+ function isRecord$1(value) {
25
+ return typeof value === "object" && value !== null;
26
+ }
27
+ function isNonNegativeInteger(value) {
28
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
29
+ }
30
+ function isAdmission(value) {
31
+ return value === "all" || value === "inferred";
32
+ }
33
+ function isKnobSource(value) {
34
+ return typeof value === "string" && KNOB_SOURCES.some((s) => s === value);
35
+ }
36
+ /**
37
+ * Shape guard for the wire. The parent already bounded every value through the
38
+ * knob's own Zod schema; what is checked here is what a store constructor would
39
+ * otherwise turn into a silent misconfiguration — a non-integer, a negative
40
+ * budget, a hold of zero (a worker that holds nothing can crop nothing), an
41
+ * admission word that is neither mode, a source that names no level.
42
+ */
43
+ function isWorkerLeaseKnobs(value) {
44
+ if (!isRecord$1(value) || !isRecord$1(value.values) || !isRecord$1(value.sources)) return false;
45
+ const { values, sources } = value;
46
+ for (const knob of NATIVE_LEASE_NUMBER_KNOBS) if (!isNonNegativeInteger(values[knob])) return false;
47
+ if (!isNonNegativeInteger(values.holdFrames) || values.holdFrames < 1) return false;
48
+ if (!isAdmission(values.admission)) return false;
49
+ return NATIVE_LEASE_KNOB_NAMES.every((knob) => isKnobSource(sources[knob]));
50
+ }
51
+ /**
52
+ * The one-line spawn diagnostic. Names the effective value AND its source for
53
+ * every knob: without the source, a log saying `holdFrames=8` cannot distinguish
54
+ * "the operator's setting arrived" from "someone left a shell variable set", and
55
+ * a setting that silently fails to arrive is exactly the failure the source
56
+ * field exists to make visible.
57
+ */
58
+ function formatNativeLeaseKnobs(resolved) {
59
+ const part = (knob) => `${knob}=${resolved.values[knob]} source=${resolved.sources[knob]}`;
60
+ return `native lease knobs — ${NATIVE_LEASE_KNOB_NAMES.map(part).join("; ")}`;
61
+ }
62
+ //#endregion
63
+ //#region src/session-decode/worker-protocol.ts
64
+ /**
65
+ * Typed IPC protocol between the per-session decode coordinator (Task 3 —
66
+ * `child_process.fork`s the worker) and the decode-worker child
67
+ * (`decode-worker-child.ts`, Task 2). Pure module: no node-av, no
68
+ * `process.on`/`process.send` here — those live in `decode-worker-child.ts`,
69
+ * which documents why raw IPC is sanctioned for that file.
70
+ *
71
+ * The protocol is a request/response pull: the parent asks for the next
72
+ * frame (`pull`), gets back metadata + an opaque `frameId`, then asks for
73
+ * pixel bytes for that id with an optional ROI (`toBuffer`). No shared
74
+ * memory, no `/dev/shm` — frames travel as IPC messages only.
75
+ */
76
+ /**
77
+ * Classify a worker diagnostic line into a {@link WorkerLogLevel}. Failure /
78
+ * fault lines surface at `warn` (visible at the default log level so a
79
+ * native-crop failure is finally seen in `addons.getLogs`); routine
80
+ * provenance / transition / lifecycle lines stay `debug`. Pure + exported so
81
+ * the mapping is unit-testable without spawning a worker.
82
+ */
83
+ function logLevelForLine(line) {
84
+ return /\b(fail(?:ed|ure)?|error|exception|unhandled rejection|corrupt|EIO|EINVAL)\b/i.test(line) ? "warn" : "debug";
85
+ }
86
+ function isRecord(value) {
87
+ return typeof value === "object" && value !== null;
88
+ }
89
+ function isFiniteNumber(value) {
90
+ return typeof value === "number" && Number.isFinite(value);
91
+ }
92
+ function isWorkerFrameFormat(value) {
93
+ return value === "rgb" || value === "gray";
94
+ }
95
+ function isWorkerBufferFormat(value) {
96
+ return isWorkerFrameFormat(value) || value === "jpeg";
97
+ }
98
+ function isWorkerLogLevel(value) {
99
+ return value === "debug" || value === "info" || value === "warn" || value === "error";
100
+ }
101
+ function isWorkerCropRegion(value) {
102
+ return isRecord(value) && isFiniteNumber(value.left) && isFiniteNumber(value.top) && isFiniteNumber(value.width) && isFiniteNumber(value.height);
103
+ }
104
+ function isWorkerResizeTarget(value) {
105
+ return isRecord(value) && isFiniteNumber(value.width) && isFiniteNumber(value.height);
106
+ }
107
+ function isFrameImageOptions(value) {
108
+ if (!isRecord(value)) return false;
109
+ if (value.crop !== void 0 && !isWorkerCropRegion(value.crop)) return false;
110
+ if (value.resize !== void 0 && !isWorkerResizeTarget(value.resize)) return false;
111
+ if (value.format !== void 0 && !isWorkerBufferFormat(value.format)) return false;
112
+ return true;
113
+ }
114
+ function isNormalizedBbox(value) {
115
+ return isRecord(value) && isFiniteNumber(value.x) && isFiniteNumber(value.y) && isFiniteNumber(value.w) && isFiniteNumber(value.h);
116
+ }
117
+ function isVideoFrameSource(value) {
118
+ return isRecord(value) && typeof value.restreamUrl === "string" && isFiniteNumber(value.deviceId) && typeof value.codec === "string";
119
+ }
120
+ function isVideoFrameGenOptions(value) {
121
+ if (!isRecord(value)) return false;
122
+ if (value.fps !== void 0 && !isFiniteNumber(value.fps)) return false;
123
+ if (value.format !== void 0 && !isWorkerFrameFormat(value.format)) return false;
124
+ if (value.preRollMs !== void 0 && !isFiniteNumber(value.preRollMs)) return false;
125
+ if (value.preRollMaxFrames !== void 0 && !isFiniteNumber(value.preRollMaxFrames)) return false;
126
+ return true;
127
+ }
128
+ /** Discriminated-union guard for {@link WorkerRequest} — no `as` casts. */
129
+ function isWorkerRequest(value) {
130
+ if (!isRecord(value)) return false;
131
+ switch (value.kind) {
132
+ case "start": return isVideoFrameSource(value.source) && isVideoFrameGenOptions(value.opts) && isWorkerLeaseKnobs(value.lease);
133
+ case "pull": return true;
134
+ case "toBuffer": return isFiniteNumber(value.frameId) && (value.opts === void 0 || isFrameImageOptions(value.opts));
135
+ case "nativeCrop": return isFiniteNumber(value.requestId) && isFiniteNumber(value.frameId) && isNormalizedBbox(value.bbox) && (value.maxWidth === void 0 || isFiniteNumber(value.maxWidth));
136
+ case "cutTiles": return isFiniteNumber(value.frameId) && Array.isArray(value.bboxes) && value.bboxes.every(isNormalizedBbox);
137
+ case "releaseNativeLease":
138
+ case "markInferred": return isFiniteNumber(value.frameId);
139
+ case "stop": return true;
140
+ default: return false;
141
+ }
142
+ }
143
+ /** Discriminated-union guard for {@link WorkerReply} — no `as` casts. */
144
+ function isWorkerReply(value) {
145
+ if (!isRecord(value)) return false;
146
+ switch (value.kind) {
147
+ case "frame": return isFiniteNumber(value.frameId) && isFiniteNumber(value.timestamp) && isFiniteNumber(value.width) && isFiniteNumber(value.height) && isWorkerFrameFormat(value.format) && (value.capturedAt === void 0 || isFiniteNumber(value.capturedAt)) && (value.preRoll === void 0 || typeof value.preRoll === "boolean");
148
+ case "buffer": return isFiniteNumber(value.frameId) && value.bytes instanceof Uint8Array;
149
+ case "nativeCropResult": return isFiniteNumber(value.requestId) && value.bytes instanceof Uint8Array && isFiniteNumber(value.width) && isFiniteNumber(value.height) && (value.source === void 0 || value.source === "lease" || value.source === "tile");
150
+ case "nativeCropMiss": return isFiniteNumber(value.requestId) && (value.reason === void 0 || typeof value.reason === "string");
151
+ case "ended": return true;
152
+ case "ready": return typeof value.warm === "boolean";
153
+ case "error": return typeof value.message === "string" && (value.frameId === void 0 || isFiniteNumber(value.frameId));
154
+ case "log": return isWorkerLogLevel(value.level) && typeof value.message === "string";
155
+ case "preRollReport": return isFiniteNumber(value.offered) && isFiniteNumber(value.inferred) && isFiniteNumber(value.dropped) && isRecord(value.droppedByReason) && isFiniteNumber(value.strideMs) && typeof value.subsampled === "boolean" && isFiniteNumber(value.spanMs);
156
+ case "dialTiming": return isFiniteNumber(value.hwContextMs) && isFiniteNumber(value.demuxerOpenMs) && isFiniteNumber(value.decoderCreateMs) && isFiniteNumber(value.firstDecodeMs) && typeof value.streamInfoProbed === "boolean";
157
+ default: return false;
158
+ }
159
+ }
160
+ //#endregion
161
+ Object.defineProperty(exports, "formatNativeLeaseKnobs", {
162
+ enumerable: true,
163
+ get: function() {
164
+ return formatNativeLeaseKnobs;
165
+ }
166
+ });
167
+ Object.defineProperty(exports, "isWorkerReply", {
168
+ enumerable: true,
169
+ get: function() {
170
+ return isWorkerReply;
171
+ }
172
+ });
173
+ Object.defineProperty(exports, "isWorkerRequest", {
174
+ enumerable: true,
175
+ get: function() {
176
+ return isWorkerRequest;
177
+ }
178
+ });
179
+ Object.defineProperty(exports, "logLevelForLine", {
180
+ enumerable: true,
181
+ get: function() {
182
+ return logLevelForLine;
183
+ }
184
+ });