@camstack/addon-mqtt-broker 1.2.95 → 1.2.97

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.
@@ -5390,6 +5390,86 @@ var ZodIssueCode = {
5390
5390
  /** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */
5391
5391
  var ZodFirstPartyTypeKind;
5392
5392
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5393
+ //#endregion
5394
+ //#region ../types/dist/sleep-BnujYGPe.mjs
5395
+ /**
5396
+ * The audio chunk plane's byte format, and the ONE expansion from a coded
5397
+ * window to float samples (D455).
5398
+ *
5399
+ * ## Why a format at all
5400
+ *
5401
+ * D450 took the plane off its 8 → 16 kHz upsample: it carries the SOURCE
5402
+ * RATE, and the one consumer that needs 16 kHz resamples next to the model.
5403
+ * It left the FORMAT alone — the broker still turned each G.711 byte into a
5404
+ * 4-byte f32le sample before the bytes entered the transport, so every leg of
5405
+ * the plane carried four times the source. The plane crosses hub-main twice on
5406
+ * the way to the analyzer, and the fleet's G.711 cameras are ~79 % of it.
5407
+ *
5408
+ * So the plane carries the source BYTES too, and whoever needs floats expands
5409
+ * them where it needs them. That is the same argument D450 made for the rate,
5410
+ * one step further along the same wire.
5411
+ *
5412
+ * ## Why the expansion lives here
5413
+ *
5414
+ * Two packages need it and they must never disagree: `addon-pipeline`'s broker
5415
+ * (which still has to serve a subscriber that did NOT ask for coded bytes —
5416
+ * `AudioChunkPlane` expands per subscription) and
5417
+ * `addon-pipeline-orchestrator`'s `AudioWindowAccumulator` (which flushes an
5418
+ * f32le window to the analyzer cap, whose `AudioChunkInput` contract is
5419
+ * unchanged and stays f32le). Both bundle the bare `@camstack/types` entry
5420
+ * into their own dist (`self-contained` externals), so this travels with a
5421
+ * `camstack deploy` and needs no published server.
5422
+ *
5423
+ * A second μ-law table anywhere else is the defect this module exists to
5424
+ * prevent. (`stream-broker.ts`'s `mulawToPcm` / `alawToPcm` are the ENCODE
5425
+ * direction for the WebRTC egress — a different transform, not a copy.)
5426
+ *
5427
+ * ## Absent means f32le
5428
+ *
5429
+ * `format` is optional on the wire and its absence means `f32le` — today's
5430
+ * bytes, byte for byte. A peer that never heard of the field is served what it
5431
+ * has always been served, because the broker only emits a coded window to a
5432
+ * subscription that DECLARED it accepts one (`AudioSubscribeOptions.accept`).
5433
+ * That is the D448 `rawForward` negotiation, and it is what makes this
5434
+ * deployable one addon at a time across three nodes.
5435
+ */
5436
+ /** Every byte format the audio chunk plane can carry. `f32le` is the default. */
5437
+ var AUDIO_CHUNK_FORMATS = [
5438
+ "f32le",
5439
+ "pcmu",
5440
+ "pcma"
5441
+ ];
5442
+ /**
5443
+ * Build the μ-law decode table (ITU-T G.711). Each of the 256 byte values maps
5444
+ * to a 16-bit PCM sample, normalised to [-1.0, 1.0] for f32le output.
5445
+ *
5446
+ * Moved here verbatim from `audio-rtp-decoder.ts`, which no longer decodes:
5447
+ * it buffers the coded bytes and the plane's consumers expand.
5448
+ */
5449
+ function buildUlawTable() {
5450
+ const table = new Float32Array(256);
5451
+ for (let i = 0; i < 256; i++) {
5452
+ const complemented = ~i & 255;
5453
+ const sign = (complemented & 128) !== 0 ? -1 : 1;
5454
+ const exponent = complemented >> 4 & 7;
5455
+ table[i] = sign * ((8 * (complemented & 15) + 132 << exponent) - 132) / 32768;
5456
+ }
5457
+ return table;
5458
+ }
5459
+ /** Build the A-law decode table (ITU-T G.711). */
5460
+ function buildAlawTable() {
5461
+ const table = new Float32Array(256);
5462
+ for (let i = 0; i < 256; i++) {
5463
+ const xored = i ^ 85;
5464
+ const sign = (xored & 128) !== 0 ? 1 : -1;
5465
+ const exponent = xored >> 4 & 7;
5466
+ const mantissa = xored & 15;
5467
+ table[i] = sign * (exponent === 0 ? 16 * mantissa + 8 : 16 * mantissa + 264 << exponent - 1) / 32768;
5468
+ }
5469
+ return table;
5470
+ }
5471
+ buildUlawTable();
5472
+ buildAlawTable();
5393
5473
  Object.fromEntries([
5394
5474
  {
5395
5475
  id: "overview",
@@ -6682,11 +6762,20 @@ var SubscribeFramesResultSchema = object({
6682
6762
  * (the wire-serialisable supertype of `Buffer`) to match `DecodedFrameSchema`
6683
6763
  * / `EncodedPacketSchema`'s precedent; a `Buffer` is assignable to it.
6684
6764
  */
6765
+ var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
6685
6766
  var DecodedAudioChunkSchema = object({
6686
6767
  data: _instanceof(Uint8Array),
6687
6768
  sampleRate: number().int().positive(),
6688
6769
  channels: number().int().positive(),
6689
- timestamp: number()
6770
+ timestamp: number(),
6771
+ /**
6772
+ * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
6773
+ * byte, for any peer that never heard of this field. A coded window
6774
+ * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
6775
+ * subscription that DECLARED it accepts one, so absence can never mean
6776
+ * "coded bytes a consumer will read as floats" (D455).
6777
+ */
6778
+ format: AudioChunkFormatSchema.optional()
6690
6779
  });
6691
6780
  /**
6692
6781
  * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). The
@@ -6698,7 +6787,18 @@ var DecodedAudioChunkSchema = object({
6698
6787
  var SubscribeAudioChunksInputSchema = object({
6699
6788
  brokerId: string(),
6700
6789
  /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
6701
- tag: string().optional()
6790
+ tag: string().optional(),
6791
+ /**
6792
+ * Byte formats this subscriber can READ, best first. The broker serves the
6793
+ * chunk's own format when it is in this list and expands to `f32le`
6794
+ * otherwise, so a subscriber is never handed bytes it cannot interpret.
6795
+ *
6796
+ * Absent (or without the source format) means `f32le` — the behaviour every
6797
+ * subscriber had before D455, unchanged. This is the negotiation half of
6798
+ * the source-bytes lever: it is what lets the broker and its consumers
6799
+ * deploy one at a time across three nodes.
6800
+ */
6801
+ accept: array(AudioChunkFormatSchema).readonly().optional()
6702
6802
  });
6703
6803
  /** Result of `stream-broker.subscribeAudioChunks`. */
6704
6804
  var SubscribeAudioChunksResultSchema = object({
@@ -10670,6 +10770,51 @@ var AudioAnalysisSettingsSchema = object({
10670
10770
  minConfidence: number().min(0).max(1).default(.3),
10671
10771
  allowedClasses: array(string()).default([])
10672
10772
  });
10773
+ /**
10774
+ * `attachDevice` — the analyzer PULLS a camera's audio from the broker (D461).
10775
+ *
10776
+ * Until D461 the orchestrator drained the broker's chunk plane, accumulated
10777
+ * ~1 s windows and pushed them back out as `analyseChunk`. It neither produced
10778
+ * nor consumed the audio: the PCM crossed hub-main twice for a process that
10779
+ * only buffered it. `attachDevice` inverts the direction — the analyzer opens
10780
+ * its own `subscribeAudioChunks` against the broker and the subscriber IS the
10781
+ * decoder, so the coded G.711 bytes D455 put on the plane stay coded all the
10782
+ * way to the one expansion that feeds the model.
10783
+ *
10784
+ * The orchestrator still owns the POLICY (the `audioMode` gate, the on-motion
10785
+ * window, the per-device node assignment, the settings read) and therefore
10786
+ * still owns the attach/detach pair. It no longer owns the bytes.
10787
+ */
10788
+ var AudioAttachDeviceInputSchema = object({
10789
+ deviceId: number(),
10790
+ /** Broker id (`<deviceId>/<camStreamId>`) carrying this camera's audio. */
10791
+ brokerId: string(),
10792
+ /**
10793
+ * `clusterRoles.ingestNode` — the node whose broker owns the source dial.
10794
+ * Every `streamBroker` call the attachment makes is pinned to it, exactly as
10795
+ * the orchestrator's poller pinned them before the move.
10796
+ */
10797
+ ingestNodeId: string(),
10798
+ /**
10799
+ * Resolved once by the orchestrator at attach time, exactly as it was read
10800
+ * once per subscription before D461. The analyzer does NOT re-resolve per
10801
+ * window: a settings change re-attaches, which is what always happened.
10802
+ */
10803
+ settings: AudioAnalysisSettingsSchema
10804
+ });
10805
+ var AudioAttachDeviceResultSchema = object({
10806
+ /** False only when the analyzer is shutting down and refused to attach. */
10807
+ attached: boolean(),
10808
+ /**
10809
+ * True when the attachment replaced a live one for the same device. An
10810
+ * attach is idempotent by REPLACEMENT — two pollers on one camera would
10811
+ * double the broker's fanout and neither would know about the other.
10812
+ */
10813
+ replaced: boolean()
10814
+ });
10815
+ var AudioDetachDeviceResultSchema = object({
10816
+ /** False when no attachment existed — detach is idempotent. */
10817
+ detached: boolean() });
10673
10818
  var AudioClassificationResultSchema = object({
10674
10819
  labels: array(AudioClassificationLabelSchema).readonly(),
10675
10820
  rawLabels: array(AudioClassificationLabelSchema).readonly().optional(),
@@ -10678,7 +10823,7 @@ var AudioClassificationResultSchema = object({
10678
10823
  method(object({
10679
10824
  chunk: AudioChunkInputSchema,
10680
10825
  settings: AudioAnalysisSettingsSchema
10681
- }), AudioAnalysisResultSchema.nullable(), { kind: "mutation" }), method(AudioChunkInputSchema, AudioClassificationResultSchema, { timeoutMs: 3e4 }), method(_void(), boolean()), method(_void(), _void(), { kind: "mutation" }), method(_void(), object({ backend: string() }), {
10826
+ }), AudioAnalysisResultSchema.nullable(), { kind: "mutation" }), method(AudioChunkInputSchema, AudioClassificationResultSchema, { timeoutMs: 3e4 }), method(AudioAttachDeviceInputSchema, AudioAttachDeviceResultSchema, { kind: "mutation" }), method(object({ deviceId: number() }), AudioDetachDeviceResultSchema, { kind: "mutation" }), method(_void(), boolean()), method(_void(), _void(), { kind: "mutation" }), method(_void(), object({ backend: string() }), {
10682
10827
  kind: "mutation",
10683
10828
  auth: "admin"
10684
10829
  });
@@ -20282,6 +20427,14 @@ var NativeCropResultSchema = object({
20282
20427
  * set `encodeJpeg: true`; `bytes` is then absent.
20283
20428
  */
20284
20429
  jpeg: string().optional(),
20430
+ /**
20431
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
20432
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
20433
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
20434
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
20435
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
20436
+ */
20437
+ jpegBytes: _instanceof(Uint8Array).optional(),
20285
20438
  width: number().int().positive(),
20286
20439
  height: number().int().positive(),
20287
20440
  /**
@@ -20348,7 +20501,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
20348
20501
  })]);
20349
20502
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
20350
20503
  var ParkedTrackFrameSchema = object({
20351
- jpeg: string(),
20504
+ /**
20505
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
20506
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
20507
+ * Exactly one of the two is present.
20508
+ */
20509
+ jpeg: string().optional(),
20510
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
20511
+ jpegBytes: _instanceof(Uint8Array).optional(),
20352
20512
  width: number().int().positive(),
20353
20513
  height: number().int().positive(),
20354
20514
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -20935,6 +21095,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
20935
21095
  bbox: NativeCropBboxSchema,
20936
21096
  maxWidth: number().int().positive().optional(),
20937
21097
  /**
21098
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21099
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21100
+ * wire — never assume consent: a pre-D462 caller parses the field as
21101
+ * base64 and bytes would decode to garbage rather than fail.
21102
+ */
21103
+ acceptJpegBytes: boolean().optional(),
21104
+ /**
20938
21105
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
20939
21106
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
20940
21107
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -21002,7 +21169,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21002
21169
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
21003
21170
  deviceId: number(),
21004
21171
  trackId: string(),
21005
- kind: ParkedFrameKindSchema
21172
+ kind: ParkedFrameKindSchema,
21173
+ /**
21174
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21175
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21176
+ * wire — never assume consent: a pre-D462 caller parses the field as
21177
+ * base64 and bytes would decode to garbage rather than fail.
21178
+ */
21179
+ acceptJpegBytes: boolean().optional()
21006
21180
  }), ParkedTrackFrameSchema.nullable()), method(object({
21007
21181
  deviceId: number(),
21008
21182
  trackId: string()
@@ -30641,12 +30815,24 @@ Object.freeze({
30641
30815
  addonId: null,
30642
30816
  access: "create"
30643
30817
  },
30818
+ "audioAnalyzer.attachDevice": {
30819
+ capName: "audio-analyzer",
30820
+ capScope: "system",
30821
+ addonId: null,
30822
+ access: "create"
30823
+ },
30644
30824
  "audioAnalyzer.classify": {
30645
30825
  capName: "audio-analyzer",
30646
30826
  capScope: "system",
30647
30827
  addonId: null,
30648
30828
  access: "view"
30649
30829
  },
30830
+ "audioAnalyzer.detachDevice": {
30831
+ capName: "audio-analyzer",
30832
+ capScope: "system",
30833
+ addonId: null,
30834
+ access: "create"
30835
+ },
30650
30836
  "audioAnalyzer.dispose": {
30651
30837
  capName: "audio-analyzer",
30652
30838
  capScope: "system",
@@ -36490,11 +36676,21 @@ Object.freeze({
36490
36676
  form: "single",
36491
36677
  optional: false
36492
36678
  }],
36679
+ "audioAnalyzer.attachDevice": [{
36680
+ name: "deviceId",
36681
+ form: "single",
36682
+ optional: false
36683
+ }],
36493
36684
  "audioAnalyzer.classify": [{
36494
36685
  name: "deviceId",
36495
36686
  form: "single",
36496
36687
  optional: true
36497
36688
  }],
36689
+ "audioAnalyzer.detachDevice": [{
36690
+ name: "deviceId",
36691
+ form: "single",
36692
+ optional: false
36693
+ }],
36498
36694
  "audioMetrics.getCurrentSnapshot": [{
36499
36695
  name: "deviceId",
36500
36696
  form: "single",
@@ -38346,6 +38542,52 @@ Object.freeze({
38346
38542
  "network-access": "ingress",
38347
38543
  "smtp-provider": "email"
38348
38544
  });
38545
+ var G711_SCALE_CORRECTION_DB = {
38546
+ PCMU: 20 * Math.log10(4),
38547
+ PCMA: 20 * Math.log10(8)
38548
+ };
38549
+ /**
38550
+ * Restate a dBFS number that was MEASURED through the pre-epoch decoder as the
38551
+ * same intent on the ITU-T scale (D460).
38552
+ *
38553
+ * ## When this applies, and when it is the wrong thing to reach for
38554
+ *
38555
+ * An absolute-dBFS number in this repo is one of two things, and only one of
38556
+ * them converts:
38557
+ *
38558
+ * - **A statement about the scale** — "-55 dBFS is near silence", "-25 dBFS
38559
+ * is loud". It was true on the ITU-T scale before the epoch and it is true
38560
+ * after. The defect was never in the number; it was that 19 of this hub's
38561
+ * 25 cameras did not obey it. Converting such a number takes something
38562
+ * correct and makes it wrong, in order to preserve a bug.
38563
+ * - **A measurement taken through the old decoder** — a value someone read
38564
+ * off a meter that under-reported by exactly 4× (PCMU) or 8× (PCMA). It
38565
+ * describes a sound that was really {@link G711_SCALE_CORRECTION_DB} dB
38566
+ * louder. That is what this function is for.
38567
+ *
38568
+ * Telling the two apart is a question about PROVENANCE, not about arithmetic,
38569
+ * and it cannot be answered from the number. It is answered by the comment the
38570
+ * author left — which is why `scripts/check-dbfs-era.mts` makes leaving one
38571
+ * mandatory.
38572
+ *
38573
+ * ## Why a function and not a typed-in number
38574
+ *
38575
+ * `-55 + 12.04` written into a source file is, six months later, completely
38576
+ * indistinguishable from a threshold somebody simply preferred. Calling this
38577
+ * keeps the derivation, the law, and the original measurement all visible at
38578
+ * the call site, so a future reader can disagree with the *premise* instead of
38579
+ * having to reverse-engineer the sum.
38580
+ *
38581
+ * **This is not a runtime gain.** It converts an authored CONSTANT once, where
38582
+ * it is declared. It must never be applied to a live sample or a stored
38583
+ * `AudioEvent.dbfs`: the decoder is correct now, and a second authority
38584
+ * adjusting numbers the decoder already got right is the original defect with
38585
+ * an extra place to argue with (D459).
38586
+ */
38587
+ function ituDbfsFromPreEpoch(law, authoredDbfs) {
38588
+ return authoredDbfs + G711_SCALE_CORRECTION_DB[law];
38589
+ }
38590
+ Math.round(ituDbfsFromPreEpoch("PCMU", -55));
38349
38591
  /** Schema defaults — an untouched sub-field must author exactly these. */
38350
38592
  var NC_AUDIO_DEFAULTS = {
38351
38593
  hitPercent: 60,
@@ -5385,6 +5385,86 @@ var ZodIssueCode = {
5385
5385
  /** @deprecated Do not use. Stub definition, only included for zod-to-json-schema compatibility. */
5386
5386
  var ZodFirstPartyTypeKind;
5387
5387
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5388
+ //#endregion
5389
+ //#region ../types/dist/sleep-BnujYGPe.mjs
5390
+ /**
5391
+ * The audio chunk plane's byte format, and the ONE expansion from a coded
5392
+ * window to float samples (D455).
5393
+ *
5394
+ * ## Why a format at all
5395
+ *
5396
+ * D450 took the plane off its 8 → 16 kHz upsample: it carries the SOURCE
5397
+ * RATE, and the one consumer that needs 16 kHz resamples next to the model.
5398
+ * It left the FORMAT alone — the broker still turned each G.711 byte into a
5399
+ * 4-byte f32le sample before the bytes entered the transport, so every leg of
5400
+ * the plane carried four times the source. The plane crosses hub-main twice on
5401
+ * the way to the analyzer, and the fleet's G.711 cameras are ~79 % of it.
5402
+ *
5403
+ * So the plane carries the source BYTES too, and whoever needs floats expands
5404
+ * them where it needs them. That is the same argument D450 made for the rate,
5405
+ * one step further along the same wire.
5406
+ *
5407
+ * ## Why the expansion lives here
5408
+ *
5409
+ * Two packages need it and they must never disagree: `addon-pipeline`'s broker
5410
+ * (which still has to serve a subscriber that did NOT ask for coded bytes —
5411
+ * `AudioChunkPlane` expands per subscription) and
5412
+ * `addon-pipeline-orchestrator`'s `AudioWindowAccumulator` (which flushes an
5413
+ * f32le window to the analyzer cap, whose `AudioChunkInput` contract is
5414
+ * unchanged and stays f32le). Both bundle the bare `@camstack/types` entry
5415
+ * into their own dist (`self-contained` externals), so this travels with a
5416
+ * `camstack deploy` and needs no published server.
5417
+ *
5418
+ * A second μ-law table anywhere else is the defect this module exists to
5419
+ * prevent. (`stream-broker.ts`'s `mulawToPcm` / `alawToPcm` are the ENCODE
5420
+ * direction for the WebRTC egress — a different transform, not a copy.)
5421
+ *
5422
+ * ## Absent means f32le
5423
+ *
5424
+ * `format` is optional on the wire and its absence means `f32le` — today's
5425
+ * bytes, byte for byte. A peer that never heard of the field is served what it
5426
+ * has always been served, because the broker only emits a coded window to a
5427
+ * subscription that DECLARED it accepts one (`AudioSubscribeOptions.accept`).
5428
+ * That is the D448 `rawForward` negotiation, and it is what makes this
5429
+ * deployable one addon at a time across three nodes.
5430
+ */
5431
+ /** Every byte format the audio chunk plane can carry. `f32le` is the default. */
5432
+ var AUDIO_CHUNK_FORMATS = [
5433
+ "f32le",
5434
+ "pcmu",
5435
+ "pcma"
5436
+ ];
5437
+ /**
5438
+ * Build the μ-law decode table (ITU-T G.711). Each of the 256 byte values maps
5439
+ * to a 16-bit PCM sample, normalised to [-1.0, 1.0] for f32le output.
5440
+ *
5441
+ * Moved here verbatim from `audio-rtp-decoder.ts`, which no longer decodes:
5442
+ * it buffers the coded bytes and the plane's consumers expand.
5443
+ */
5444
+ function buildUlawTable() {
5445
+ const table = new Float32Array(256);
5446
+ for (let i = 0; i < 256; i++) {
5447
+ const complemented = ~i & 255;
5448
+ const sign = (complemented & 128) !== 0 ? -1 : 1;
5449
+ const exponent = complemented >> 4 & 7;
5450
+ table[i] = sign * ((8 * (complemented & 15) + 132 << exponent) - 132) / 32768;
5451
+ }
5452
+ return table;
5453
+ }
5454
+ /** Build the A-law decode table (ITU-T G.711). */
5455
+ function buildAlawTable() {
5456
+ const table = new Float32Array(256);
5457
+ for (let i = 0; i < 256; i++) {
5458
+ const xored = i ^ 85;
5459
+ const sign = (xored & 128) !== 0 ? 1 : -1;
5460
+ const exponent = xored >> 4 & 7;
5461
+ const mantissa = xored & 15;
5462
+ table[i] = sign * (exponent === 0 ? 16 * mantissa + 8 : 16 * mantissa + 264 << exponent - 1) / 32768;
5463
+ }
5464
+ return table;
5465
+ }
5466
+ buildUlawTable();
5467
+ buildAlawTable();
5388
5468
  Object.fromEntries([
5389
5469
  {
5390
5470
  id: "overview",
@@ -6677,11 +6757,20 @@ var SubscribeFramesResultSchema = object({
6677
6757
  * (the wire-serialisable supertype of `Buffer`) to match `DecodedFrameSchema`
6678
6758
  * / `EncodedPacketSchema`'s precedent; a `Buffer` is assignable to it.
6679
6759
  */
6760
+ var AudioChunkFormatSchema = _enum(AUDIO_CHUNK_FORMATS);
6680
6761
  var DecodedAudioChunkSchema = object({
6681
6762
  data: _instanceof(Uint8Array),
6682
6763
  sampleRate: number().int().positive(),
6683
6764
  channels: number().int().positive(),
6684
- timestamp: number()
6765
+ timestamp: number(),
6766
+ /**
6767
+ * Byte format of `data`. ABSENT MEANS `f32le` — today's bytes, byte for
6768
+ * byte, for any peer that never heard of this field. A coded window
6769
+ * (`pcmu` / `pcma`, one byte per sample) is only ever emitted to a
6770
+ * subscription that DECLARED it accepts one, so absence can never mean
6771
+ * "coded bytes a consumer will read as floats" (D455).
6772
+ */
6773
+ format: AudioChunkFormatSchema.optional()
6685
6774
  });
6686
6775
  /**
6687
6776
  * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). The
@@ -6693,7 +6782,18 @@ var DecodedAudioChunkSchema = object({
6693
6782
  var SubscribeAudioChunksInputSchema = object({
6694
6783
  brokerId: string(),
6695
6784
  /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
6696
- tag: string().optional()
6785
+ tag: string().optional(),
6786
+ /**
6787
+ * Byte formats this subscriber can READ, best first. The broker serves the
6788
+ * chunk's own format when it is in this list and expands to `f32le`
6789
+ * otherwise, so a subscriber is never handed bytes it cannot interpret.
6790
+ *
6791
+ * Absent (or without the source format) means `f32le` — the behaviour every
6792
+ * subscriber had before D455, unchanged. This is the negotiation half of
6793
+ * the source-bytes lever: it is what lets the broker and its consumers
6794
+ * deploy one at a time across three nodes.
6795
+ */
6796
+ accept: array(AudioChunkFormatSchema).readonly().optional()
6697
6797
  });
6698
6798
  /** Result of `stream-broker.subscribeAudioChunks`. */
6699
6799
  var SubscribeAudioChunksResultSchema = object({
@@ -10665,6 +10765,51 @@ var AudioAnalysisSettingsSchema = object({
10665
10765
  minConfidence: number().min(0).max(1).default(.3),
10666
10766
  allowedClasses: array(string()).default([])
10667
10767
  });
10768
+ /**
10769
+ * `attachDevice` — the analyzer PULLS a camera's audio from the broker (D461).
10770
+ *
10771
+ * Until D461 the orchestrator drained the broker's chunk plane, accumulated
10772
+ * ~1 s windows and pushed them back out as `analyseChunk`. It neither produced
10773
+ * nor consumed the audio: the PCM crossed hub-main twice for a process that
10774
+ * only buffered it. `attachDevice` inverts the direction — the analyzer opens
10775
+ * its own `subscribeAudioChunks` against the broker and the subscriber IS the
10776
+ * decoder, so the coded G.711 bytes D455 put on the plane stay coded all the
10777
+ * way to the one expansion that feeds the model.
10778
+ *
10779
+ * The orchestrator still owns the POLICY (the `audioMode` gate, the on-motion
10780
+ * window, the per-device node assignment, the settings read) and therefore
10781
+ * still owns the attach/detach pair. It no longer owns the bytes.
10782
+ */
10783
+ var AudioAttachDeviceInputSchema = object({
10784
+ deviceId: number(),
10785
+ /** Broker id (`<deviceId>/<camStreamId>`) carrying this camera's audio. */
10786
+ brokerId: string(),
10787
+ /**
10788
+ * `clusterRoles.ingestNode` — the node whose broker owns the source dial.
10789
+ * Every `streamBroker` call the attachment makes is pinned to it, exactly as
10790
+ * the orchestrator's poller pinned them before the move.
10791
+ */
10792
+ ingestNodeId: string(),
10793
+ /**
10794
+ * Resolved once by the orchestrator at attach time, exactly as it was read
10795
+ * once per subscription before D461. The analyzer does NOT re-resolve per
10796
+ * window: a settings change re-attaches, which is what always happened.
10797
+ */
10798
+ settings: AudioAnalysisSettingsSchema
10799
+ });
10800
+ var AudioAttachDeviceResultSchema = object({
10801
+ /** False only when the analyzer is shutting down and refused to attach. */
10802
+ attached: boolean(),
10803
+ /**
10804
+ * True when the attachment replaced a live one for the same device. An
10805
+ * attach is idempotent by REPLACEMENT — two pollers on one camera would
10806
+ * double the broker's fanout and neither would know about the other.
10807
+ */
10808
+ replaced: boolean()
10809
+ });
10810
+ var AudioDetachDeviceResultSchema = object({
10811
+ /** False when no attachment existed — detach is idempotent. */
10812
+ detached: boolean() });
10668
10813
  var AudioClassificationResultSchema = object({
10669
10814
  labels: array(AudioClassificationLabelSchema).readonly(),
10670
10815
  rawLabels: array(AudioClassificationLabelSchema).readonly().optional(),
@@ -10673,7 +10818,7 @@ var AudioClassificationResultSchema = object({
10673
10818
  method(object({
10674
10819
  chunk: AudioChunkInputSchema,
10675
10820
  settings: AudioAnalysisSettingsSchema
10676
- }), AudioAnalysisResultSchema.nullable(), { kind: "mutation" }), method(AudioChunkInputSchema, AudioClassificationResultSchema, { timeoutMs: 3e4 }), method(_void(), boolean()), method(_void(), _void(), { kind: "mutation" }), method(_void(), object({ backend: string() }), {
10821
+ }), AudioAnalysisResultSchema.nullable(), { kind: "mutation" }), method(AudioChunkInputSchema, AudioClassificationResultSchema, { timeoutMs: 3e4 }), method(AudioAttachDeviceInputSchema, AudioAttachDeviceResultSchema, { kind: "mutation" }), method(object({ deviceId: number() }), AudioDetachDeviceResultSchema, { kind: "mutation" }), method(_void(), boolean()), method(_void(), _void(), { kind: "mutation" }), method(_void(), object({ backend: string() }), {
10677
10822
  kind: "mutation",
10678
10823
  auth: "admin"
10679
10824
  });
@@ -20277,6 +20422,14 @@ var NativeCropResultSchema = object({
20277
20422
  * set `encodeJpeg: true`; `bytes` is then absent.
20278
20423
  */
20279
20424
  jpeg: string().optional(),
20425
+ /**
20426
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
20427
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
20428
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
20429
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
20430
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
20431
+ */
20432
+ jpegBytes: _instanceof(Uint8Array).optional(),
20280
20433
  width: number().int().positive(),
20281
20434
  height: number().int().positive(),
20282
20435
  /**
@@ -20343,7 +20496,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
20343
20496
  })]);
20344
20497
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
20345
20498
  var ParkedTrackFrameSchema = object({
20346
- jpeg: string(),
20499
+ /**
20500
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
20501
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
20502
+ * Exactly one of the two is present.
20503
+ */
20504
+ jpeg: string().optional(),
20505
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
20506
+ jpegBytes: _instanceof(Uint8Array).optional(),
20347
20507
  width: number().int().positive(),
20348
20508
  height: number().int().positive(),
20349
20509
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -20930,6 +21090,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
20930
21090
  bbox: NativeCropBboxSchema,
20931
21091
  maxWidth: number().int().positive().optional(),
20932
21092
  /**
21093
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21094
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21095
+ * wire — never assume consent: a pre-D462 caller parses the field as
21096
+ * base64 and bytes would decode to garbage rather than fail.
21097
+ */
21098
+ acceptJpegBytes: boolean().optional(),
21099
+ /**
20933
21100
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
20934
21101
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
20935
21102
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -20997,7 +21164,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
20997
21164
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
20998
21165
  deviceId: number(),
20999
21166
  trackId: string(),
21000
- kind: ParkedFrameKindSchema
21167
+ kind: ParkedFrameKindSchema,
21168
+ /**
21169
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21170
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21171
+ * wire — never assume consent: a pre-D462 caller parses the field as
21172
+ * base64 and bytes would decode to garbage rather than fail.
21173
+ */
21174
+ acceptJpegBytes: boolean().optional()
21001
21175
  }), ParkedTrackFrameSchema.nullable()), method(object({
21002
21176
  deviceId: number(),
21003
21177
  trackId: string()
@@ -30636,12 +30810,24 @@ Object.freeze({
30636
30810
  addonId: null,
30637
30811
  access: "create"
30638
30812
  },
30813
+ "audioAnalyzer.attachDevice": {
30814
+ capName: "audio-analyzer",
30815
+ capScope: "system",
30816
+ addonId: null,
30817
+ access: "create"
30818
+ },
30639
30819
  "audioAnalyzer.classify": {
30640
30820
  capName: "audio-analyzer",
30641
30821
  capScope: "system",
30642
30822
  addonId: null,
30643
30823
  access: "view"
30644
30824
  },
30825
+ "audioAnalyzer.detachDevice": {
30826
+ capName: "audio-analyzer",
30827
+ capScope: "system",
30828
+ addonId: null,
30829
+ access: "create"
30830
+ },
30645
30831
  "audioAnalyzer.dispose": {
30646
30832
  capName: "audio-analyzer",
30647
30833
  capScope: "system",
@@ -36485,11 +36671,21 @@ Object.freeze({
36485
36671
  form: "single",
36486
36672
  optional: false
36487
36673
  }],
36674
+ "audioAnalyzer.attachDevice": [{
36675
+ name: "deviceId",
36676
+ form: "single",
36677
+ optional: false
36678
+ }],
36488
36679
  "audioAnalyzer.classify": [{
36489
36680
  name: "deviceId",
36490
36681
  form: "single",
36491
36682
  optional: true
36492
36683
  }],
36684
+ "audioAnalyzer.detachDevice": [{
36685
+ name: "deviceId",
36686
+ form: "single",
36687
+ optional: false
36688
+ }],
36493
36689
  "audioMetrics.getCurrentSnapshot": [{
36494
36690
  name: "deviceId",
36495
36691
  form: "single",
@@ -38341,6 +38537,52 @@ Object.freeze({
38341
38537
  "network-access": "ingress",
38342
38538
  "smtp-provider": "email"
38343
38539
  });
38540
+ var G711_SCALE_CORRECTION_DB = {
38541
+ PCMU: 20 * Math.log10(4),
38542
+ PCMA: 20 * Math.log10(8)
38543
+ };
38544
+ /**
38545
+ * Restate a dBFS number that was MEASURED through the pre-epoch decoder as the
38546
+ * same intent on the ITU-T scale (D460).
38547
+ *
38548
+ * ## When this applies, and when it is the wrong thing to reach for
38549
+ *
38550
+ * An absolute-dBFS number in this repo is one of two things, and only one of
38551
+ * them converts:
38552
+ *
38553
+ * - **A statement about the scale** — "-55 dBFS is near silence", "-25 dBFS
38554
+ * is loud". It was true on the ITU-T scale before the epoch and it is true
38555
+ * after. The defect was never in the number; it was that 19 of this hub's
38556
+ * 25 cameras did not obey it. Converting such a number takes something
38557
+ * correct and makes it wrong, in order to preserve a bug.
38558
+ * - **A measurement taken through the old decoder** — a value someone read
38559
+ * off a meter that under-reported by exactly 4× (PCMU) or 8× (PCMA). It
38560
+ * describes a sound that was really {@link G711_SCALE_CORRECTION_DB} dB
38561
+ * louder. That is what this function is for.
38562
+ *
38563
+ * Telling the two apart is a question about PROVENANCE, not about arithmetic,
38564
+ * and it cannot be answered from the number. It is answered by the comment the
38565
+ * author left — which is why `scripts/check-dbfs-era.mts` makes leaving one
38566
+ * mandatory.
38567
+ *
38568
+ * ## Why a function and not a typed-in number
38569
+ *
38570
+ * `-55 + 12.04` written into a source file is, six months later, completely
38571
+ * indistinguishable from a threshold somebody simply preferred. Calling this
38572
+ * keeps the derivation, the law, and the original measurement all visible at
38573
+ * the call site, so a future reader can disagree with the *premise* instead of
38574
+ * having to reverse-engineer the sum.
38575
+ *
38576
+ * **This is not a runtime gain.** It converts an authored CONSTANT once, where
38577
+ * it is declared. It must never be applied to a live sample or a stored
38578
+ * `AudioEvent.dbfs`: the decoder is correct now, and a second authority
38579
+ * adjusting numbers the decoder already got right is the original defect with
38580
+ * an extra place to argue with (D459).
38581
+ */
38582
+ function ituDbfsFromPreEpoch(law, authoredDbfs) {
38583
+ return authoredDbfs + G711_SCALE_CORRECTION_DB[law];
38584
+ }
38585
+ Math.round(ituDbfsFromPreEpoch("PCMU", -55));
38344
38586
  /** Schema defaults — an untouched sub-field must author exactly these. */
38345
38587
  var NC_AUDIO_DEFAULTS = {
38346
38588
  hitPercent: 60,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-mqtt-broker",
3
- "version": "1.2.95",
3
+ "version": "1.2.97",
4
4
  "description": "MQTT broker registry addon for CamStack — manages external broker entries + an optional embedded aedes broker. Consumers spin up their own `mqtt.js` clients via the `mqtt-broker` cap.",
5
5
  "keywords": [
6
6
  "camstack",