@camstack/addon-pipeline 1.1.39 → 1.1.40

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 (38) hide show
  1. package/dist/audio-analyzer/index.js +168 -61
  2. package/dist/audio-analyzer/index.mjs +168 -61
  3. package/dist/detection-pipeline/index.js +2 -2
  4. package/dist/detection-pipeline/index.mjs +2 -2
  5. package/dist/{dist-DbGdZ8Nr.js → dist-BJTPkJFw.js} +247 -3
  6. package/dist/{dist-DvvYzO58.mjs → dist-v6cKLmU3.mjs} +247 -3
  7. package/dist/{frame-handle-plane-B7jMIZc2.js → frame-handle-plane-BP8YV4sF.js} +54 -5
  8. package/dist/{frame-handle-plane-BFzoIfkd.mjs → frame-handle-plane-DKAXTtfn.mjs} +54 -5
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/pipeline-runner/index.js +893 -12
  12. package/dist/pipeline-runner/index.mjs +892 -13
  13. package/dist/recorder/index.js +2 -2
  14. package/dist/recorder/index.mjs +2 -2
  15. package/dist/session-decode/decode-worker-child.js +511 -0
  16. package/dist/session-decode/decode-worker-child.mjs +510 -0
  17. package/dist/stream-broker/_stub.js +44 -44
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DezFy4Fu.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-6I7-nufj.mjs} +3 -3
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DqJvWBKS.mjs +26 -0
  20. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-dGka8yZY.mjs +26 -0
  21. package/dist/stream-broker/{hostInit-BOf0hg9q.mjs → hostInit-CUkFGqiI.mjs} +3 -3
  22. package/dist/stream-broker/index.js +268 -38
  23. package/dist/stream-broker/index.mjs +268 -38
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/dist/worker-protocol-BCfO8gUF.js +67 -0
  26. package/dist/worker-protocol-pk7qdYXt.mjs +56 -0
  27. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-Br2fqwe7.js → MaskShapeCanvas-DI4BY7W2-Cr_fCzY5.js} +1 -1
  28. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DYXRf30g.js → MotionZonesSettings-NcxxQN8r-COcvoHDH.js} +1 -1
  29. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DLpqLgPo.js → PrivacyMaskSettings-APgPLF7p-Bv-bbyJl.js} +1 -1
  30. package/embed-dist/assets/index-Ctbq9AXh.css +2 -0
  31. package/embed-dist/assets/index-jMj5PyJk.js +81 -0
  32. package/embed-dist/index.html +2 -2
  33. package/package.json +6 -3
  34. package/python/inference_pool.py +3 -1
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DhY3MZ2C.mjs +0 -26
  36. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BsrrdOBU.mjs +0 -26
  37. package/embed-dist/assets/index-C5UpuPr8.css +0 -2
  38. package/embed-dist/assets/index-CR367rYR.js +0 -81
@@ -1,18 +1,18 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { $ as discriminatedUnion, B as DeviceType, D as nodePin, F as webrtcSessionCapability, G as makeSourceBrokerId, H as createEvent, I as errMsg, L as BaseAddon, N as streamBrokerCapability, Q as boolean, R as CAM_PROFILE_ORDER, V as EventCategory, W as makeProfileBrokerId, X as _enum, Z as array, at as string, c as EncodeProfileSchema, d as RingBuffer, g as cameraStreamsCapability, it as record, nt as number, ot as union, p as addonWidgetsSourceCapability, q as parseProfileBrokerId, rt as object, tt as literal, w as maskUrlCredentials, z as DeviceFeature } from "../dist-DvvYzO58.mjs";
3
- import { n as DecoderSessionProxy, t as FrameHandlePlane } from "../frame-handle-plane-BFzoIfkd.mjs";
2
+ import { $ as discriminatedUnion, B as DeviceType, D as nodePin, F as webrtcSessionCapability, G as makeSourceBrokerId, H as createEvent, I as errMsg, L as BaseAddon, N as streamBrokerCapability, Q as boolean, R as CAM_PROFILE_ORDER, V as EventCategory, W as makeProfileBrokerId, X as _enum, Z as array, at as string, c as EncodeProfileSchema, d as RingBuffer, g as cameraStreamsCapability, it as record, nt as number, ot as union, p as addonWidgetsSourceCapability, q as parseProfileBrokerId, rt as object, tt as literal, w as maskUrlCredentials, z as DeviceFeature } from "../dist-v6cKLmU3.mjs";
3
+ import { n as DecoderSessionProxy, t as FrameHandlePlane } from "../frame-handle-plane-DKAXTtfn.mjs";
4
4
  import { t as createFileDataPlaneHandler } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import * as crypto$1 from "node:crypto";
7
7
  import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
8
+ import { spawn } from "node:child_process";
9
+ import { pathToFileURL } from "node:url";
8
10
  import * as fs from "node:fs";
9
11
  import * as path$1 from "node:path";
10
12
  import path, { join } from "node:path";
11
13
  import * as os from "node:os";
12
14
  import { networkInterfaces, tmpdir } from "node:os";
13
- import { spawn } from "node:child_process";
14
15
  import { lookup } from "node:dns/promises";
15
- import { pathToFileURL } from "node:url";
16
16
  import * as net$1 from "node:net";
17
17
  import net from "node:net";
18
18
  import { Socket } from "net";
@@ -4748,6 +4748,14 @@ var RtspSession = class {
4748
4748
  userAgent = null;
4749
4749
  /** When true, this session receives video-only (no audio RTP). */
4750
4750
  muted;
4751
+ /**
4752
+ * Latch for a `muted` (local-decoder loopback) session that has started
4753
+ * dropping video packets on socket backlog. While set, every non-keyframe
4754
+ * packet is dropped — even after the backlog clears — until the next keyframe
4755
+ * access unit, so the decoder always resumes on a clean GOP boundary instead
4756
+ * of mid-GOP with reference-broken (missing P-frame) packets.
4757
+ */
4758
+ droppingUntilKeyframe = false;
4751
4759
  constructor(socket, sdp, onClose, muted) {
4752
4760
  this.socket = socket;
4753
4761
  this.sdp = sdp;
@@ -4797,13 +4805,35 @@ var RtspSession = class {
4797
4805
  bytesSent: this.bytesSent
4798
4806
  };
4799
4807
  }
4800
- /** Send a video RTP packet via TCP interleaved framing. */
4801
- sendRtp(packet) {
4808
+ /**
4809
+ * Send a video RTP packet via TCP interleaved framing.
4810
+ *
4811
+ * `prime` marks a packet from the current-GOP catch-up burst replayed on
4812
+ * join — written unconditionally (bounded one-shot the local decoder drains
4813
+ * fast; dropping mid-burst would break frame references). A prime burst
4814
+ * starts at a keyframe by construction, so it also clears the
4815
+ * `droppingUntilKeyframe` latch.
4816
+ *
4817
+ * `isKeyframe` marks a packet that begins a keyframe access unit — the clean
4818
+ * GOP boundary at which a latched `muted` session resumes sending.
4819
+ */
4820
+ sendRtp(packet, prime = false, isKeyframe = false) {
4802
4821
  if (!this.playing || this.closed) return;
4803
4822
  if (this.tracks.length === 0) return;
4804
- if (this.socket.writableLength > 2097152) {
4805
- this.close();
4806
- return;
4823
+ if (prime) this.droppingUntilKeyframe = false;
4824
+ else {
4825
+ if (this.droppingUntilKeyframe) {
4826
+ if (!isKeyframe) return;
4827
+ this.droppingUntilKeyframe = false;
4828
+ }
4829
+ if (this.socket.writableLength > 2097152) {
4830
+ if (this.muted) {
4831
+ this.droppingUntilKeyframe = true;
4832
+ return;
4833
+ }
4834
+ this.close();
4835
+ return;
4836
+ }
4807
4837
  }
4808
4838
  const channel = this.tracks[0].rtpChannel;
4809
4839
  const frame = Buffer.allocUnsafe(4 + packet.data.length);
@@ -5528,6 +5558,16 @@ function appendAudioMediaBlock(videoSdp, audioBlock) {
5528
5558
  //#endregion
5529
5559
  //#region src/stream-broker/rtsp/rtsp-restreamer.ts
5530
5560
  /**
5561
+ * Max age (ms) of a prime burst a MUTED (local-decoder loopback) session will
5562
+ * accept. A muted decoder must anchor at the LIVE edge — a full-GOP catch-up
5563
+ * burst (2-3s old) makes libav base its pts 2-3s in the past and, with ~zero
5564
+ * decode headroom, it never drains that backlog. So a muted session only joins
5565
+ * off a burst already near live (span ≤ this); otherwise it is left pending to
5566
+ * begin at the next live keyframe. Non-muted sessions (viewer / recorder) still
5567
+ * get the full-GOP burst for an instant picture.
5568
+ */
5569
+ var LIVE_EDGE_MAX_SPAN_MS = 500;
5570
+ /**
5531
5571
  * Per-broker RTSP restreamer.
5532
5572
  *
5533
5573
  * Operates in two modes:
@@ -5573,9 +5613,22 @@ var RtspRestreamer = class {
5573
5613
  collectingKeyframeRtp = false;
5574
5614
  logger = null;
5575
5615
  _onSessionCountChanged = null;
5616
+ /**
5617
+ * Supplies the broker's current-GOP source-RTP snapshot (keyframe..now) so a
5618
+ * joining decoder is primed with the WHOLE recent GOP as a burst and
5619
+ * fast-forwards to the live edge — instead of a lone keyframe up to a GOP old
5620
+ * followed by reference-broken live packets. Mirrors Scrypted's
5621
+ * prebuffer/findSyncFrame. Returns [] until a keyframe is seen (falls back to
5622
+ * the single cached keyframe AU).
5623
+ */
5624
+ rtpPreBufferSupplier = null;
5576
5625
  constructor(streamName) {
5577
5626
  this.streamName = streamName;
5578
5627
  }
5628
+ /** Wire the broker's current-GOP RTP snapshot supplier (see field doc). */
5629
+ setRtpPreBufferSupplier(supplier) {
5630
+ this.rtpPreBufferSupplier = supplier;
5631
+ }
5579
5632
  /** True when using RTP passthrough (native client) instead of Annex-B re-packetization. */
5580
5633
  get rtpPassthroughMode() {
5581
5634
  return this._rtpPassthroughMode;
@@ -5661,9 +5714,8 @@ var RtspRestreamer = class {
5661
5714
  total: this.sessions.size
5662
5715
  } });
5663
5716
  this._onSessionCountChanged?.();
5664
- if (this._rtpPassthroughMode) {
5665
- if (this.lastKeyframeRtpPackets.length > 0) setTimeout(() => this.servePendingSessionsRtp(), 0);
5666
- } else if (this.lastKeyframeNals.length > 0 && this.packetizer) setTimeout(() => this.servePendingSessions(), 0);
5717
+ if (this._rtpPassthroughMode) setTimeout(() => this.servePendingSessionsRtp(), 0);
5718
+ else if (this.lastKeyframeNals.length > 0 && this.packetizer) setTimeout(() => this.servePendingSessions(), 0);
5667
5719
  }
5668
5720
  /**
5669
5721
  * Set the camera's SDP directly (from DESCRIBE response).
@@ -5695,6 +5747,7 @@ var RtspRestreamer = class {
5695
5747
  data: rtpData,
5696
5748
  marker: (rtpData[1] & 128) !== 0
5697
5749
  };
5750
+ const keyframeAuStart = (isKeyframe || isParameterSet) && !this.collectingKeyframeRtp;
5698
5751
  if (isKeyframe || isParameterSet) {
5699
5752
  if (!this.collectingKeyframeRtp) {
5700
5753
  this.collectingKeyframeRtp = true;
@@ -5710,23 +5763,49 @@ var RtspRestreamer = class {
5710
5763
  }
5711
5764
  if (this.sessions.size === 0) return;
5712
5765
  this.servePendingSessionsRtp();
5766
+ if (keyframeAuStart) for (const session of this.sessions.values()) {
5767
+ if (!session.isMuted() || !session.isPlaying()) continue;
5768
+ this.pendingKeyframe.delete(session.getSessionId());
5769
+ }
5713
5770
  for (const session of this.sessions.values()) {
5714
5771
  if (this.pendingKeyframe.has(session.getSessionId())) continue;
5715
- if (session.isPlaying()) session.sendRtp(rtpPacket);
5772
+ if (session.isPlaying()) session.sendRtp(rtpPacket, false, isKeyframe || isParameterSet);
5716
5773
  }
5717
5774
  }
5718
5775
  /** Send cached keyframe RTP packets to sessions that transitioned to PLAY. */
5719
5776
  servePendingSessionsRtp() {
5720
5777
  if (this.pendingKeyframe.size === 0) return;
5721
- if (this.lastKeyframeRtpPackets.length === 0) return;
5778
+ const ringPackets = this.rtpPreBufferSupplier?.() ?? [];
5779
+ const fromRing = ringPackets.length > 0;
5780
+ const primePackets = fromRing ? ringPackets.map((data) => ({
5781
+ data,
5782
+ marker: (data[1] & 128) !== 0
5783
+ })) : this.lastKeyframeRtpPackets;
5784
+ if (primePackets.length === 0) return;
5785
+ const firstTs = primePackets[0].data.readUInt32BE(4);
5786
+ const spanMs = (primePackets[primePackets.length - 1].data.readUInt32BE(4) - firstTs | 0) / 90;
5722
5787
  const ready = [];
5723
5788
  for (const sessionId of this.pendingKeyframe) if (this.sessions.get(sessionId)?.isPlaying()) ready.push(sessionId);
5724
5789
  if (ready.length === 0) return;
5725
- for (const rtpPacket of this.lastKeyframeRtpPackets) for (const sessionId of ready) {
5790
+ const served = [];
5791
+ for (const sessionId of ready) {
5792
+ const session = this.sessions.get(sessionId);
5793
+ if (!session) continue;
5794
+ if (session.isMuted() && (!fromRing || spanMs > LIVE_EDGE_MAX_SPAN_MS)) continue;
5795
+ served.push(sessionId);
5796
+ }
5797
+ if (served.length === 0) return;
5798
+ this.logger?.info("restream prime (lat-probe)", { meta: {
5799
+ source: fromRing ? "gop-burst" : "keyframe-only",
5800
+ packets: primePackets.length,
5801
+ spanMs: Math.round(spanMs),
5802
+ clients: served.length
5803
+ } });
5804
+ for (const rtpPacket of primePackets) for (const sessionId of served) {
5726
5805
  const session = this.sessions.get(sessionId);
5727
- if (session?.isPlaying()) session.sendRtp(rtpPacket);
5806
+ if (session?.isPlaying()) session.sendRtp(rtpPacket, true);
5728
5807
  }
5729
- for (const id of ready) this.pendingKeyframe.delete(id);
5808
+ for (const id of served) this.pendingKeyframe.delete(id);
5730
5809
  }
5731
5810
  /**
5732
5811
  * Push a raw audio RTP packet from the native RTSP client.
@@ -8636,6 +8715,38 @@ var AudioCodecSession = class {
8636
8715
  sessionId: this.sessionId
8637
8716
  };
8638
8717
  }
8718
+ /**
8719
+ * Tear down the upstream decode session + 40ms poll loop WITHOUT
8720
+ * permanently closing this handle. The broker calls this from its
8721
+ * audio-consumer demand-gate: when audio consumers drop to zero the
8722
+ * upstream decoder session and its poll loop are pure waste, so we
8723
+ * release them. Unlike {@link close} this leaves the handle reusable —
8724
+ * the next pushed packet re-creates the session lazily via
8725
+ * `ensureSession()`. Safe to call repeatedly.
8726
+ */
8727
+ async idle() {
8728
+ if (this.closed) return;
8729
+ if (this.creating) await this.creating;
8730
+ if (this.closed) return;
8731
+ if (this.pollTimer) {
8732
+ clearInterval(this.pollTimer);
8733
+ this.pollTimer = null;
8734
+ }
8735
+ if (!this.sessionId) return;
8736
+ const sid = this.sessionId;
8737
+ const nodeId = this.sessionNodeId ?? void 0;
8738
+ this.sessionId = null;
8739
+ this.sessionNodeId = null;
8740
+ try {
8741
+ await this.api.closeSession({
8742
+ sessionId: sid,
8743
+ ...nodeId ? { nodeId } : {}
8744
+ });
8745
+ } catch (err) {
8746
+ this.logger?.warn("audio-codec: idle closeSession failed", { meta: { error: errMsg(err) } });
8747
+ }
8748
+ this.logger?.info("audio-codec: decode session idled (no audio consumer)", { meta: { sessionId: sid } });
8749
+ }
8639
8750
  async close() {
8640
8751
  if (this.closed) return;
8641
8752
  this.closed = true;
@@ -8863,12 +8974,25 @@ var EncodedRingBuffer = class {
8863
8974
  clear() {
8864
8975
  this.packets.length = 0;
8865
8976
  }
8866
- /** Remove packets older than maxDurationMs based on PTS. */
8977
+ /**
8978
+ * Remove packets older than maxDurationMs based on PTS.
8979
+ *
8980
+ * PTS here is the source RTP timestamp (90kHz → ms), which is NOT
8981
+ * monotonic across the buffer's lifetime: an RTSP/rfc4571 redial re-bases
8982
+ * it to a fresh random value (RFC 3550) and the 32-bit field wraps every
8983
+ * ~13.25h. A purely `pts < cutoff` head-trim stalls forever whenever the
8984
+ * newest packet's pts drops below the stale head's — the buffer then
8985
+ * retains the entire pre-discontinuity history (an unbounded native-memory
8986
+ * leak of full-bitrate AnnexB copies). So we also evict any head packet
8987
+ * whose pts is GREATER than the newest packet's: it can only belong to a
8988
+ * prior, higher-based epoch that the source has since jumped away from.
8989
+ */
8867
8990
  evict() {
8868
8991
  if (this.packets.length === 0) return;
8869
- const cutoff = this.packets[this.packets.length - 1].pts - this.maxDurationMs;
8992
+ const newest = this.packets[this.packets.length - 1].pts;
8993
+ const cutoff = newest - this.maxDurationMs;
8870
8994
  let removeCount = 0;
8871
- while (removeCount < this.packets.length && this.packets[removeCount].pts < cutoff) removeCount++;
8995
+ while (removeCount < this.packets.length && (this.packets[removeCount].pts < cutoff || this.packets[removeCount].pts > newest)) removeCount++;
8872
8996
  if (removeCount > 0) this.packets.splice(0, removeCount);
8873
8997
  }
8874
8998
  };
@@ -9148,6 +9272,14 @@ var INITIAL_RECONNECT_DELAY_MS = 1e3;
9148
9272
  var MAX_RECONNECT_DELAY_MS = 3e4;
9149
9273
  /** Grace period before suspending after last consumer leaves. */
9150
9274
  var SUSPEND_GRACE_MS = 5e3;
9275
+ /**
9276
+ * Grace window before an idle audio-codec decode session is torn down
9277
+ * after the last audio consumer detaches. Mirrors the frame-handle /
9278
+ * suspend occupancy-linger pattern: a brief gap between consumers (e.g.
9279
+ * a WebRTC renegotiation or an analyzer re-subscribe) must not thrash
9280
+ * the upstream decode session create/destroy.
9281
+ */
9282
+ var AUDIO_CONSUMER_LINGER_MS = 5e3;
9151
9283
  /** Timeout for push-mode sources: if no packet arrives within this window, emit error placeholder. */
9152
9284
  var PUSH_STALL_TIMEOUT_MS = 15e3;
9153
9285
  var StreamBroker = class StreamBroker {
@@ -9223,6 +9355,12 @@ var StreamBroker = class StreamBroker {
9223
9355
  pushRtpFlagDepacketizer = null;
9224
9356
  audioRtpDecoder = null;
9225
9357
  audioCodecSession = null;
9358
+ /**
9359
+ * Pending occupancy-linger teardown of {@link audioCodecSession} after
9360
+ * the last audio consumer detached. Cancelled when a consumer returns
9361
+ * within {@link AUDIO_CONSUMER_LINGER_MS}. See {@link reconcileAudioConsumer}.
9362
+ */
9363
+ audioConsumerLingerTimer;
9226
9364
  audioRtpSeen = 0;
9227
9365
  /** Detected audio codec from RTSP DESCRIBE (e.g. 'PCMU', 'PCMA'). */
9228
9366
  audioCodec = null;
@@ -9436,6 +9574,7 @@ var StreamBroker = class StreamBroker {
9436
9574
  this.pipeServer = new StreamPipeServer(logger?.child("pipe-server"));
9437
9575
  this.rtspRestreamer = new RtspRestreamer(deviceId);
9438
9576
  if (logger) this.rtspRestreamer.setLogger(logger.child("rtsp"));
9577
+ this.rtspRestreamer.setRtpPreBufferSupplier(() => this.getRtpPreBuffer());
9439
9578
  this.preBuffer = new EncodedRingBuffer(StreamBroker.DEFAULT_PRE_BUFFER_SEC);
9440
9579
  this.decoderSeedBuffer = new EncodedRingBuffer(StreamBroker.DECODER_SEED_BUFFER_SEC);
9441
9580
  this.audioChunkPlane = new AudioChunkPlane(logger?.child("audio-chunk-plane"));
@@ -9684,11 +9823,54 @@ var StreamBroker = class StreamBroker {
9684
9823
  return false;
9685
9824
  }
9686
9825
  /**
9826
+ * True when at least one consumer needs DECODED audio. The audio-specific
9827
+ * subset of {@link hasDemand} — deliberately excludes the video
9828
+ * frame-handle plane so a video-only viewer never spins up the
9829
+ * AAC/Opus decode chain. `encodedCallbacks` covers WebRTC (its G.711
9830
+ * egress needs decoded PCM); `audioChunkPlane` covers the analyzer.
9831
+ */
9832
+ hasAudioConsumer() {
9833
+ return this.encodedCallbacks.size > 0 || this.audioChunkPlane.subscriberCount > 0;
9834
+ }
9835
+ /**
9836
+ * Demand-gate the audio-codec decode session lifecycle. Called from
9837
+ * every consumer add/remove edge (via {@link checkDemand}). While an
9838
+ * audio consumer exists, cancel any pending linger-close. When
9839
+ * consumers drop to zero, arm a short linger timer that tears the
9840
+ * upstream decode session + 40ms poll loop down (via
9841
+ * `AudioCodecSession.idle`) so a momentary gap between consumers
9842
+ * doesn't thrash create/destroy. The next gated packet re-arms the
9843
+ * session lazily through `ensureSession()`. No-op when no session is
9844
+ * armed (native PCMU/PCMA cameras never build one).
9845
+ */
9846
+ reconcileAudioConsumer() {
9847
+ if (!this.audioCodecSession) return;
9848
+ if (this.hasAudioConsumer()) {
9849
+ if (this.audioConsumerLingerTimer) {
9850
+ clearTimeout(this.audioConsumerLingerTimer);
9851
+ this.audioConsumerLingerTimer = void 0;
9852
+ }
9853
+ return;
9854
+ }
9855
+ if (this.audioConsumerLingerTimer) return;
9856
+ this.audioConsumerLingerTimer = setTimeout(() => {
9857
+ this.audioConsumerLingerTimer = void 0;
9858
+ if (this.hasAudioConsumer()) return;
9859
+ const session = this.audioCodecSession;
9860
+ if (!session) return;
9861
+ session.idle().catch((err) => {
9862
+ this.logger?.warn("audio-codec: idle teardown failed", { meta: { error: errMsg(err) } });
9863
+ });
9864
+ }, AUDIO_CONSUMER_LINGER_MS);
9865
+ if (typeof this.audioConsumerLingerTimer.unref === "function") this.audioConsumerLingerTimer.unref();
9866
+ }
9867
+ /**
9687
9868
  * Check demand and schedule suspend/resume. Called after every subscriber
9688
9869
  * add/remove and after pre-buffer enable/disable.
9689
9870
  */
9690
9871
  checkDemand() {
9691
9872
  if (this.manualStop || this.stopping) return;
9873
+ this.reconcileAudioConsumer();
9692
9874
  if (this.hasDemand()) {
9693
9875
  if (!this.demandActive) {
9694
9876
  this.demandActive = true;
@@ -9834,7 +10016,7 @@ var StreamBroker = class StreamBroker {
9834
10016
  if (packet.type === "audio" && this.source?.type === "push" && this.audioCodecSession) {
9835
10017
  const codec = packet.codec.toLowerCase();
9836
10018
  if (codec !== "pcmu" && codec !== "pcma") {
9837
- this.audioCodecSession.pushRawFrame(packet.data);
10019
+ if (this.hasAudioConsumer()) this.audioCodecSession.pushRawFrame(packet.data);
9838
10020
  return;
9839
10021
  }
9840
10022
  }
@@ -10275,7 +10457,8 @@ var StreamBroker = class StreamBroker {
10275
10457
  logger: this.logger?.child("frame-handle-plane"),
10276
10458
  resolveStreamInfo: () => this.resolveFrameHandleStreamInfo(),
10277
10459
  localNodeId: this.localNodeId,
10278
- getSeedPackets: () => this.decoderSeedBuffer.getPackets()
10460
+ getSeedPackets: () => this.decoderSeedBuffer.getPackets(),
10461
+ isDebugEnabled: () => this.streamingDebug
10279
10462
  });
10280
10463
  return this.frameHandlePlane;
10281
10464
  }
@@ -10357,6 +10540,17 @@ var StreamBroker = class StreamBroker {
10357
10540
  subscribedAt: entry.subscribedAt,
10358
10541
  packetsDelivered: entry.packetsDelivered
10359
10542
  });
10543
+ if (this._preBufferEnabled && encoded.length === 0) encoded.push({
10544
+ id: "prebuffer-hold",
10545
+ channel: "annexb",
10546
+ attribution: {
10547
+ kind: "warmup",
10548
+ userAgent: "prebuffer",
10549
+ media: "video"
10550
+ },
10551
+ subscribedAt: this.startedAt,
10552
+ packetsDelivered: 0
10553
+ });
10360
10554
  return {
10361
10555
  rtsp: this.rtspRestreamer.listSessionInfos(),
10362
10556
  decoded: (this.frameHandlePlane?.listSubscribers() ?? []).map((s) => ({
@@ -10722,7 +10916,7 @@ var StreamBroker = class StreamBroker {
10722
10916
  hasCodecSession: this.audioCodecSession !== null
10723
10917
  } });
10724
10918
  this.audioRtpDecoder?.processPacket(rtpData);
10725
- this.audioCodecSession?.processPacket(rtpData);
10919
+ if (this.hasAudioConsumer()) this.audioCodecSession?.processPacket(rtpData);
10726
10920
  if ((this.audioCodec === "PCMU" || this.audioCodec === "PCMA") && this.encodedCallbacks.size > 0 && rtpData.length > 12) {
10727
10921
  const payload = rtpData.subarray(12);
10728
10922
  if (payload.length > 0) {
@@ -11203,6 +11397,10 @@ var StreamBroker = class StreamBroker {
11203
11397
  this.audioRtpDecoder.flush();
11204
11398
  this.audioRtpDecoder = null;
11205
11399
  }
11400
+ if (this.audioConsumerLingerTimer) {
11401
+ clearTimeout(this.audioConsumerLingerTimer);
11402
+ this.audioConsumerLingerTimer = void 0;
11403
+ }
11206
11404
  if (this.audioCodecSession) {
11207
11405
  const session = this.audioCodecSession;
11208
11406
  this.audioCodecSession = null;
@@ -14253,6 +14451,11 @@ var StreamBrokerManager = class {
14253
14451
  break;
14254
14452
  }
14255
14453
  this.assignments.set(deviceId, nextAssignment);
14454
+ if (current.map.high !== newMap.high) for (const camStreamId of [current.map.high, newMap.high]) {
14455
+ if (!camStreamId) continue;
14456
+ const broker = this.brokers.get(brokerIdFor(deviceId, camStreamId));
14457
+ if (broker) this.applyPreBufferConfig(broker, deviceId, camStreamId);
14458
+ }
14256
14459
  for (const [camId, prevCount] of oldPushConsumers) if ((newPushConsumers.get(camId) ?? 0) === 0 && prevCount > 0) this.emitCamStreamIdle(deviceId, camId);
14257
14460
  for (const [camId, nextCount] of newPushConsumers) if ((oldPushConsumers.get(camId) ?? 0) === 0 && nextCount > 0) {
14258
14461
  let triggerProfile = "mid";
@@ -14458,7 +14661,7 @@ var StreamBrokerManager = class {
14458
14661
  broker.setRestreamers(this.restreamers);
14459
14662
  broker.setRestreamUrlResolver(() => this.rtspProvider.getEntry(brokerId)?.mutedUrl ?? null);
14460
14663
  broker.setFfmpegBinaryPath(this.ffmpegConfig.binaryPath);
14461
- this.applyPreBufferConfig(broker, deviceId, camStreamId, cam);
14664
+ this.applyPreBufferConfig(broker, deviceId, camStreamId);
14462
14665
  if (this.deviceOverrides.get(deviceId)?.streamingDebug) broker.setStreamingDebug(true);
14463
14666
  broker.setSourceProvider(() => {
14464
14667
  const latest = this.cameraStreams.get(deviceId)?.get(camStreamId);
@@ -14594,34 +14797,48 @@ var StreamBrokerManager = class {
14594
14797
  };
14595
14798
  }
14596
14799
  /**
14800
+ * Whether a stream is warmed BY DEFAULT (no explicit override): the device's
14801
+ * `high` profile, on a non-battery camera. Single source of truth used by both
14802
+ * the broker-config default (`applyPreBufferConfig`) and the settings-patch
14803
+ * override synthesis (`defaultStreamPreBuffer`) so a seconds-only patch never
14804
+ * silently disables the high-profile warm default.
14805
+ */
14806
+ isDefaultWarm(deviceId, camStreamId) {
14807
+ return findProfileForStream(this.assignments.get(deviceId)?.map ?? {}, camStreamId) === "high" && !this.deviceHasFeature(deviceId, DeviceFeature.BatteryOperated);
14808
+ }
14809
+ /**
14597
14810
  * Default `StreamPreBuffer` for a brand-new override entry — mirrors
14598
- * the UI-side default in `getDeviceSettingsContribution`. Pre-buffer
14599
- * is OFF by default for every stream. Operators opt in per-stream
14600
- * via the device-settings sub-tabs; detection / recording paths
14601
- * pass `forceForDetection` to `getEffectivePreBufferSec` when they
14602
- * need a buffered window irrespective of the operator toggle.
14811
+ * the effective broker default: `enabled` reflects the high-profile warm
14812
+ * default (`isDefaultWarm`), so a seconds-only patch on a warm high stream
14813
+ * inherits `enabled: true` instead of silently forcing it OFF. Operators
14814
+ * still override explicitly per-stream via the device-settings sub-tabs;
14815
+ * detection / recording paths pass `forceForDetection` to
14816
+ * `getEffectivePreBufferSec` when they need a buffered window irrespective
14817
+ * of the operator toggle.
14603
14818
  */
14604
- defaultStreamPreBuffer(_deviceId, _camStreamId) {
14819
+ defaultStreamPreBuffer(deviceId, camStreamId) {
14605
14820
  return {
14606
- enabled: false,
14821
+ enabled: this.isDefaultWarm(deviceId, camStreamId),
14607
14822
  seconds: this.defaultPreBufferSec
14608
14823
  };
14609
14824
  }
14610
14825
  /**
14611
14826
  * Apply the effective pre-buffer config to a fresh broker instance.
14612
- * Default behaviour is OFF for every cam stream keeps RAM and the
14613
- * upstream reader idle until the operator opts in or a detection /
14614
- * recording subscriber attaches.
14827
+ * Default behaviour: the HIGH profile stream is warm (see below);
14828
+ * every other stream stays OFF — idle until the operator opts in or
14829
+ * a detection / recording subscriber attaches. An explicit per-stream
14830
+ * operator override always wins over both defaults.
14615
14831
  */
14616
- applyPreBufferConfig(broker, deviceId, camStreamId, _cam) {
14832
+ applyPreBufferConfig(broker, deviceId, camStreamId) {
14617
14833
  const perStream = this.deviceOverrides.get(deviceId)?.preBuffer?.[camStreamId];
14618
14834
  if (perStream) {
14619
14835
  broker.setPreBufferEnabled(perStream.enabled);
14620
14836
  broker.setPreBufferDuration(perStream.enabled ? perStream.seconds : 0);
14621
14837
  return;
14622
14838
  }
14623
- broker.setPreBufferEnabled(false);
14624
- broker.setPreBufferDuration(0);
14839
+ const defaultWarm = this.isDefaultWarm(deviceId, camStreamId);
14840
+ broker.setPreBufferEnabled(defaultWarm);
14841
+ broker.setPreBufferDuration(defaultWarm ? this.defaultPreBufferSec : 0);
14625
14842
  }
14626
14843
  async fanOutRegistration(deviceId, brokerId, cam, broker) {
14627
14844
  const log = this.logger.withTags({
@@ -17406,6 +17623,15 @@ var BROKER_METRICS_HEARTBEAT_MS = 3e4;
17406
17623
  /** Throttle for persisting the per-stream probe map — bounds writes regardless
17407
17624
  * of streaming churn while keeping the durable copy recent enough for restart. */
17408
17625
  var PROBE_SNAPSHOTS_PERSIST_INTERVAL_MS = 6e4;
17626
+ /**
17627
+ * Defensive timeout for the per-session `networkQuality.getDeviceStats` poll.
17628
+ * The stat is an OPTIONAL input to the adaptive downgrade ladder — never worth
17629
+ * blocking the loop for. When the hub-resident provider is transiently
17630
+ * undiscoverable the child's unowned-cap routing falls to the Moleculer
17631
+ * broker-fallback, whose discovery wait is ~30s; without this bound a single
17632
+ * poll would stall the adaptive loop for that long. On timeout we simply treat
17633
+ * the sample as absent (the RTCP Receiver-Report path drives the ladder anyway). */
17634
+ var NETWORK_QUALITY_STATS_TIMEOUT_MS = 2e3;
17409
17635
  var StreamBrokerAddon = class extends BaseAddon {
17410
17636
  brokerManager = null;
17411
17637
  /** ffmpeg binary path from the `ffmpeg` config section, mirrored here so the
@@ -17734,10 +17960,14 @@ var StreamBrokerAddon = class extends BaseAddon {
17734
17960
  }
17735
17961
  },
17736
17962
  clientLossPct: async (deviceId) => {
17963
+ const ctrl = new AbortController();
17964
+ const timer = setTimeout(() => ctrl.abort(), NETWORK_QUALITY_STATS_TIMEOUT_MS);
17737
17965
  try {
17738
- return (await this.ctx.api.networkQuality.getDeviceStats.query({ deviceId }))?.client?.packetLossPercent ?? null;
17966
+ return (await this.ctx.api.networkQuality.getDeviceStats.query({ deviceId }, { signal: ctrl.signal }))?.client?.packetLossPercent ?? null;
17739
17967
  } catch {
17740
17968
  return null;
17969
+ } finally {
17970
+ clearTimeout(timer);
17741
17971
  }
17742
17972
  },
17743
17973
  resolveTranscodeDecodeHwAccel: () => this.resolveTranscodeDecodeHwAccel(),
@@ -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-DezFy4Fu.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-6I7-nufj.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
@@ -0,0 +1,67 @@
1
+ //#region src/session-decode/worker-protocol.ts
2
+ function isRecord(value) {
3
+ return typeof value === "object" && value !== null;
4
+ }
5
+ function isFiniteNumber(value) {
6
+ return typeof value === "number" && Number.isFinite(value);
7
+ }
8
+ function isWorkerFrameFormat(value) {
9
+ return value === "rgb" || value === "gray";
10
+ }
11
+ function isWorkerCropRegion(value) {
12
+ return isRecord(value) && isFiniteNumber(value.left) && isFiniteNumber(value.top) && isFiniteNumber(value.width) && isFiniteNumber(value.height);
13
+ }
14
+ function isWorkerResizeTarget(value) {
15
+ return isRecord(value) && isFiniteNumber(value.width) && isFiniteNumber(value.height);
16
+ }
17
+ function isFrameImageOptions(value) {
18
+ if (!isRecord(value)) return false;
19
+ if (value.crop !== void 0 && !isWorkerCropRegion(value.crop)) return false;
20
+ if (value.resize !== void 0 && !isWorkerResizeTarget(value.resize)) return false;
21
+ if (value.format !== void 0 && !isWorkerFrameFormat(value.format)) return false;
22
+ return true;
23
+ }
24
+ function isVideoFrameSource(value) {
25
+ return isRecord(value) && typeof value.restreamUrl === "string" && isFiniteNumber(value.deviceId) && typeof value.codec === "string";
26
+ }
27
+ function isVideoFrameGenOptions(value) {
28
+ if (!isRecord(value)) return false;
29
+ if (value.fps !== void 0 && !isFiniteNumber(value.fps)) return false;
30
+ if (value.format !== void 0 && !isWorkerFrameFormat(value.format)) return false;
31
+ return true;
32
+ }
33
+ /** Discriminated-union guard for {@link WorkerRequest} — no `as` casts. */
34
+ function isWorkerRequest(value) {
35
+ if (!isRecord(value)) return false;
36
+ switch (value.kind) {
37
+ case "start": return isVideoFrameSource(value.source) && isVideoFrameGenOptions(value.opts);
38
+ case "pull": return true;
39
+ case "toBuffer": return isFiniteNumber(value.frameId) && (value.opts === void 0 || isFrameImageOptions(value.opts));
40
+ case "stop": return true;
41
+ default: return false;
42
+ }
43
+ }
44
+ /** Discriminated-union guard for {@link WorkerReply} — no `as` casts. */
45
+ function isWorkerReply(value) {
46
+ if (!isRecord(value)) return false;
47
+ switch (value.kind) {
48
+ case "frame": return isFiniteNumber(value.frameId) && isFiniteNumber(value.timestamp) && isFiniteNumber(value.width) && isFiniteNumber(value.height) && isWorkerFrameFormat(value.format);
49
+ case "buffer": return isFiniteNumber(value.frameId) && value.bytes instanceof Uint8Array;
50
+ case "ended": return true;
51
+ case "error": return typeof value.message === "string" && (value.frameId === void 0 || isFiniteNumber(value.frameId));
52
+ default: return false;
53
+ }
54
+ }
55
+ //#endregion
56
+ Object.defineProperty(exports, "isWorkerReply", {
57
+ enumerable: true,
58
+ get: function() {
59
+ return isWorkerReply;
60
+ }
61
+ });
62
+ Object.defineProperty(exports, "isWorkerRequest", {
63
+ enumerable: true,
64
+ get: function() {
65
+ return isWorkerRequest;
66
+ }
67
+ });
@@ -0,0 +1,56 @@
1
+ //#region src/session-decode/worker-protocol.ts
2
+ function isRecord(value) {
3
+ return typeof value === "object" && value !== null;
4
+ }
5
+ function isFiniteNumber(value) {
6
+ return typeof value === "number" && Number.isFinite(value);
7
+ }
8
+ function isWorkerFrameFormat(value) {
9
+ return value === "rgb" || value === "gray";
10
+ }
11
+ function isWorkerCropRegion(value) {
12
+ return isRecord(value) && isFiniteNumber(value.left) && isFiniteNumber(value.top) && isFiniteNumber(value.width) && isFiniteNumber(value.height);
13
+ }
14
+ function isWorkerResizeTarget(value) {
15
+ return isRecord(value) && isFiniteNumber(value.width) && isFiniteNumber(value.height);
16
+ }
17
+ function isFrameImageOptions(value) {
18
+ if (!isRecord(value)) return false;
19
+ if (value.crop !== void 0 && !isWorkerCropRegion(value.crop)) return false;
20
+ if (value.resize !== void 0 && !isWorkerResizeTarget(value.resize)) return false;
21
+ if (value.format !== void 0 && !isWorkerFrameFormat(value.format)) return false;
22
+ return true;
23
+ }
24
+ function isVideoFrameSource(value) {
25
+ return isRecord(value) && typeof value.restreamUrl === "string" && isFiniteNumber(value.deviceId) && typeof value.codec === "string";
26
+ }
27
+ function isVideoFrameGenOptions(value) {
28
+ if (!isRecord(value)) return false;
29
+ if (value.fps !== void 0 && !isFiniteNumber(value.fps)) return false;
30
+ if (value.format !== void 0 && !isWorkerFrameFormat(value.format)) return false;
31
+ return true;
32
+ }
33
+ /** Discriminated-union guard for {@link WorkerRequest} — no `as` casts. */
34
+ function isWorkerRequest(value) {
35
+ if (!isRecord(value)) return false;
36
+ switch (value.kind) {
37
+ case "start": return isVideoFrameSource(value.source) && isVideoFrameGenOptions(value.opts);
38
+ case "pull": return true;
39
+ case "toBuffer": return isFiniteNumber(value.frameId) && (value.opts === void 0 || isFrameImageOptions(value.opts));
40
+ case "stop": return true;
41
+ default: return false;
42
+ }
43
+ }
44
+ /** Discriminated-union guard for {@link WorkerReply} — no `as` casts. */
45
+ function isWorkerReply(value) {
46
+ if (!isRecord(value)) return false;
47
+ switch (value.kind) {
48
+ case "frame": return isFiniteNumber(value.frameId) && isFiniteNumber(value.timestamp) && isFiniteNumber(value.width) && isFiniteNumber(value.height) && isWorkerFrameFormat(value.format);
49
+ case "buffer": return isFiniteNumber(value.frameId) && value.bytes instanceof Uint8Array;
50
+ case "ended": return true;
51
+ case "error": return typeof value.message === "string" && (value.frameId === void 0 || isFiniteNumber(value.frameId));
52
+ default: return false;
53
+ }
54
+ }
55
+ //#endregion
56
+ export { isWorkerRequest as n, isWorkerReply as t };
@@ -1,4 +1,4 @@
1
- import{d as e,f as t,g as n,h as r,m as i,p as a}from"./index-CR367rYR.js";var o=n(t()),s=n(e(),1),c=Math.PI/180;function l(){return typeof window<`u`&&({}.toString.call(window)===`[object Window]`||{}.toString.call(window)===`[object global]`)}var u=typeof global<`u`?global:typeof window<`u`?window:typeof WorkerGlobalScope<`u`?self:{},d={_global:u,version:`10.3.0`,isBrowser:l(),isUnminified:/param/.test(function(e){}.toString()),dblClickWindow:400,getAngle(e){return d.angleDeg?e*c:e},enableTrace:!1,pointerEventsEnabled:!0,autoDrawEnabled:!0,hitOnDragEnabled:!1,capturePointerEventsEnabled:!1,_mouseListenClick:!1,_touchListenClick:!1,_pointerListenClick:!1,_mouseInDblClickWindow:!1,_touchInDblClickWindow:!1,_pointerInDblClickWindow:!1,_mouseDblClickPointerId:null,_touchDblClickPointerId:null,_pointerDblClickPointerId:null,_renderBackend:`web`,legacyTextRendering:!1,pixelRatio:typeof window<`u`&&window.devicePixelRatio||1,dragDistance:3,angleDeg:!0,showWarnings:!0,dragButtons:[0,1],isDragging(){return d.DD.isDragging},isTransforming(){return d.Transformer?.isTransforming()??!1},isDragReady(){return!!d.DD.node},releaseCanvasOnDestroy:!0,document:u.document,_injectGlobal(e){u.Konva!==void 0&&console.error(`Several Konva instances detected. It is not recommended to use multiple Konva instances in the same environment.`),u.Konva=e}},f=e=>{d[e.prototype.getClassName()]=e};d._injectGlobal(d);var p=`Konva.js unsupported environment.
1
+ import{d as e,f as t,g as n,h as r,m as i,p as a}from"./index-jMj5PyJk.js";var o=n(t()),s=n(e(),1),c=Math.PI/180;function l(){return typeof window<`u`&&({}.toString.call(window)===`[object Window]`||{}.toString.call(window)===`[object global]`)}var u=typeof global<`u`?global:typeof window<`u`?window:typeof WorkerGlobalScope<`u`?self:{},d={_global:u,version:`10.3.0`,isBrowser:l(),isUnminified:/param/.test(function(e){}.toString()),dblClickWindow:400,getAngle(e){return d.angleDeg?e*c:e},enableTrace:!1,pointerEventsEnabled:!0,autoDrawEnabled:!0,hitOnDragEnabled:!1,capturePointerEventsEnabled:!1,_mouseListenClick:!1,_touchListenClick:!1,_pointerListenClick:!1,_mouseInDblClickWindow:!1,_touchInDblClickWindow:!1,_pointerInDblClickWindow:!1,_mouseDblClickPointerId:null,_touchDblClickPointerId:null,_pointerDblClickPointerId:null,_renderBackend:`web`,legacyTextRendering:!1,pixelRatio:typeof window<`u`&&window.devicePixelRatio||1,dragDistance:3,angleDeg:!0,showWarnings:!0,dragButtons:[0,1],isDragging(){return d.DD.isDragging},isTransforming(){return d.Transformer?.isTransforming()??!1},isDragReady(){return!!d.DD.node},releaseCanvasOnDestroy:!0,document:u.document,_injectGlobal(e){u.Konva!==void 0&&console.error(`Several Konva instances detected. It is not recommended to use multiple Konva instances in the same environment.`),u.Konva=e}},f=e=>{d[e.prototype.getClassName()]=e};d._injectGlobal(d);var p=`Konva.js unsupported environment.
2
2
 
3
3
  Looks like you are trying to use Konva.js in Node.js environment. because "document" object is undefined.
4
4