@camstack/addon-pipeline 1.1.39 → 1.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +168 -61
- package/dist/audio-analyzer/index.mjs +168 -61
- package/dist/detection-pipeline/index.js +2 -2
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-DbGdZ8Nr.js → dist-BJTPkJFw.js} +247 -3
- package/dist/{dist-DvvYzO58.mjs → dist-v6cKLmU3.mjs} +247 -3
- package/dist/{frame-handle-plane-B7jMIZc2.js → frame-handle-plane-BP8YV4sF.js} +54 -5
- package/dist/{frame-handle-plane-BFzoIfkd.mjs → frame-handle-plane-DKAXTtfn.mjs} +54 -5
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +893 -12
- package/dist/pipeline-runner/index.mjs +892 -13
- package/dist/recorder/index.js +2 -2
- package/dist/recorder/index.mjs +2 -2
- package/dist/session-decode/decode-worker-child.js +544 -0
- package/dist/session-decode/decode-worker-child.mjs +543 -0
- package/dist/stream-broker/_stub.js +44 -44
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DezFy4Fu.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C2l0kmD4.mjs} +3 -3
- 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
- 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
- package/dist/stream-broker/{hostInit-BOf0hg9q.mjs → hostInit-DHaCRMjM.mjs} +3 -3
- package/dist/stream-broker/index.js +268 -38
- package/dist/stream-broker/index.mjs +268 -38
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/worker-protocol-BCfO8gUF.js +67 -0
- package/dist/worker-protocol-pk7qdYXt.mjs +56 -0
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-Br2fqwe7.js → MaskShapeCanvas-DI4BY7W2-B6fza7ic.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DYXRf30g.js → MotionZonesSettings-NcxxQN8r-Cy-4iTog.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DLpqLgPo.js → PrivacyMaskSettings-APgPLF7p-CYX33lJb.js} +1 -1
- package/embed-dist/assets/index-BPkayx9u.js +81 -0
- package/embed-dist/assets/index-CSFtK41z.css +2 -0
- package/embed-dist/index.html +2 -2
- package/package.json +6 -3
- package/python/inference_pool.py +3 -1
- 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
- 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
- package/embed-dist/assets/index-C5UpuPr8.css +0 -2
- package/embed-dist/assets/index-CR367rYR.js +0 -81
|
@@ -3,20 +3,20 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_frame_handle_plane = require("../frame-handle-plane-
|
|
6
|
+
const require_dist = require("../dist-BJTPkJFw.js");
|
|
7
|
+
const require_frame_handle_plane = require("../frame-handle-plane-BP8YV4sF.js");
|
|
8
8
|
let node_crypto = require("node:crypto");
|
|
9
9
|
node_crypto = require_model_download_service_C_IHWnXx.__toESM(node_crypto);
|
|
10
|
+
let node_child_process = require("node:child_process");
|
|
11
|
+
let node_url = require("node:url");
|
|
10
12
|
let node_fs = require("node:fs");
|
|
11
13
|
node_fs = require_model_download_service_C_IHWnXx.__toESM(node_fs);
|
|
12
14
|
let node_path = require("node:path");
|
|
13
15
|
node_path = require_model_download_service_C_IHWnXx.__toESM(node_path);
|
|
14
16
|
let node_os = require("node:os");
|
|
15
17
|
node_os = require_model_download_service_C_IHWnXx.__toESM(node_os);
|
|
16
|
-
let node_child_process = require("node:child_process");
|
|
17
18
|
let node_dns_promises = require("node:dns/promises");
|
|
18
19
|
let node_module = require("node:module");
|
|
19
|
-
let node_url = require("node:url");
|
|
20
20
|
let node_net = require("node:net");
|
|
21
21
|
node_net = require_model_download_service_C_IHWnXx.__toESM(node_net);
|
|
22
22
|
let net = require("net");
|
|
@@ -4752,6 +4752,14 @@ var RtspSession = class {
|
|
|
4752
4752
|
userAgent = null;
|
|
4753
4753
|
/** When true, this session receives video-only (no audio RTP). */
|
|
4754
4754
|
muted;
|
|
4755
|
+
/**
|
|
4756
|
+
* Latch for a `muted` (local-decoder loopback) session that has started
|
|
4757
|
+
* dropping video packets on socket backlog. While set, every non-keyframe
|
|
4758
|
+
* packet is dropped — even after the backlog clears — until the next keyframe
|
|
4759
|
+
* access unit, so the decoder always resumes on a clean GOP boundary instead
|
|
4760
|
+
* of mid-GOP with reference-broken (missing P-frame) packets.
|
|
4761
|
+
*/
|
|
4762
|
+
droppingUntilKeyframe = false;
|
|
4755
4763
|
constructor(socket, sdp, onClose, muted) {
|
|
4756
4764
|
this.socket = socket;
|
|
4757
4765
|
this.sdp = sdp;
|
|
@@ -4801,13 +4809,35 @@ var RtspSession = class {
|
|
|
4801
4809
|
bytesSent: this.bytesSent
|
|
4802
4810
|
};
|
|
4803
4811
|
}
|
|
4804
|
-
/**
|
|
4805
|
-
|
|
4812
|
+
/**
|
|
4813
|
+
* Send a video RTP packet via TCP interleaved framing.
|
|
4814
|
+
*
|
|
4815
|
+
* `prime` marks a packet from the current-GOP catch-up burst replayed on
|
|
4816
|
+
* join — written unconditionally (bounded one-shot the local decoder drains
|
|
4817
|
+
* fast; dropping mid-burst would break frame references). A prime burst
|
|
4818
|
+
* starts at a keyframe by construction, so it also clears the
|
|
4819
|
+
* `droppingUntilKeyframe` latch.
|
|
4820
|
+
*
|
|
4821
|
+
* `isKeyframe` marks a packet that begins a keyframe access unit — the clean
|
|
4822
|
+
* GOP boundary at which a latched `muted` session resumes sending.
|
|
4823
|
+
*/
|
|
4824
|
+
sendRtp(packet, prime = false, isKeyframe = false) {
|
|
4806
4825
|
if (!this.playing || this.closed) return;
|
|
4807
4826
|
if (this.tracks.length === 0) return;
|
|
4808
|
-
if (this.
|
|
4809
|
-
|
|
4810
|
-
|
|
4827
|
+
if (prime) this.droppingUntilKeyframe = false;
|
|
4828
|
+
else {
|
|
4829
|
+
if (this.droppingUntilKeyframe) {
|
|
4830
|
+
if (!isKeyframe) return;
|
|
4831
|
+
this.droppingUntilKeyframe = false;
|
|
4832
|
+
}
|
|
4833
|
+
if (this.socket.writableLength > 2097152) {
|
|
4834
|
+
if (this.muted) {
|
|
4835
|
+
this.droppingUntilKeyframe = true;
|
|
4836
|
+
return;
|
|
4837
|
+
}
|
|
4838
|
+
this.close();
|
|
4839
|
+
return;
|
|
4840
|
+
}
|
|
4811
4841
|
}
|
|
4812
4842
|
const channel = this.tracks[0].rtpChannel;
|
|
4813
4843
|
const frame = Buffer.allocUnsafe(4 + packet.data.length);
|
|
@@ -5532,6 +5562,16 @@ function appendAudioMediaBlock(videoSdp, audioBlock) {
|
|
|
5532
5562
|
//#endregion
|
|
5533
5563
|
//#region src/stream-broker/rtsp/rtsp-restreamer.ts
|
|
5534
5564
|
/**
|
|
5565
|
+
* Max age (ms) of a prime burst a MUTED (local-decoder loopback) session will
|
|
5566
|
+
* accept. A muted decoder must anchor at the LIVE edge — a full-GOP catch-up
|
|
5567
|
+
* burst (2-3s old) makes libav base its pts 2-3s in the past and, with ~zero
|
|
5568
|
+
* decode headroom, it never drains that backlog. So a muted session only joins
|
|
5569
|
+
* off a burst already near live (span ≤ this); otherwise it is left pending to
|
|
5570
|
+
* begin at the next live keyframe. Non-muted sessions (viewer / recorder) still
|
|
5571
|
+
* get the full-GOP burst for an instant picture.
|
|
5572
|
+
*/
|
|
5573
|
+
var LIVE_EDGE_MAX_SPAN_MS = 500;
|
|
5574
|
+
/**
|
|
5535
5575
|
* Per-broker RTSP restreamer.
|
|
5536
5576
|
*
|
|
5537
5577
|
* Operates in two modes:
|
|
@@ -5577,9 +5617,22 @@ var RtspRestreamer = class {
|
|
|
5577
5617
|
collectingKeyframeRtp = false;
|
|
5578
5618
|
logger = null;
|
|
5579
5619
|
_onSessionCountChanged = null;
|
|
5620
|
+
/**
|
|
5621
|
+
* Supplies the broker's current-GOP source-RTP snapshot (keyframe..now) so a
|
|
5622
|
+
* joining decoder is primed with the WHOLE recent GOP as a burst and
|
|
5623
|
+
* fast-forwards to the live edge — instead of a lone keyframe up to a GOP old
|
|
5624
|
+
* followed by reference-broken live packets. Mirrors Scrypted's
|
|
5625
|
+
* prebuffer/findSyncFrame. Returns [] until a keyframe is seen (falls back to
|
|
5626
|
+
* the single cached keyframe AU).
|
|
5627
|
+
*/
|
|
5628
|
+
rtpPreBufferSupplier = null;
|
|
5580
5629
|
constructor(streamName) {
|
|
5581
5630
|
this.streamName = streamName;
|
|
5582
5631
|
}
|
|
5632
|
+
/** Wire the broker's current-GOP RTP snapshot supplier (see field doc). */
|
|
5633
|
+
setRtpPreBufferSupplier(supplier) {
|
|
5634
|
+
this.rtpPreBufferSupplier = supplier;
|
|
5635
|
+
}
|
|
5583
5636
|
/** True when using RTP passthrough (native client) instead of Annex-B re-packetization. */
|
|
5584
5637
|
get rtpPassthroughMode() {
|
|
5585
5638
|
return this._rtpPassthroughMode;
|
|
@@ -5665,9 +5718,8 @@ var RtspRestreamer = class {
|
|
|
5665
5718
|
total: this.sessions.size
|
|
5666
5719
|
} });
|
|
5667
5720
|
this._onSessionCountChanged?.();
|
|
5668
|
-
if (this._rtpPassthroughMode)
|
|
5669
|
-
|
|
5670
|
-
} else if (this.lastKeyframeNals.length > 0 && this.packetizer) setTimeout(() => this.servePendingSessions(), 0);
|
|
5721
|
+
if (this._rtpPassthroughMode) setTimeout(() => this.servePendingSessionsRtp(), 0);
|
|
5722
|
+
else if (this.lastKeyframeNals.length > 0 && this.packetizer) setTimeout(() => this.servePendingSessions(), 0);
|
|
5671
5723
|
}
|
|
5672
5724
|
/**
|
|
5673
5725
|
* Set the camera's SDP directly (from DESCRIBE response).
|
|
@@ -5699,6 +5751,7 @@ var RtspRestreamer = class {
|
|
|
5699
5751
|
data: rtpData,
|
|
5700
5752
|
marker: (rtpData[1] & 128) !== 0
|
|
5701
5753
|
};
|
|
5754
|
+
const keyframeAuStart = (isKeyframe || isParameterSet) && !this.collectingKeyframeRtp;
|
|
5702
5755
|
if (isKeyframe || isParameterSet) {
|
|
5703
5756
|
if (!this.collectingKeyframeRtp) {
|
|
5704
5757
|
this.collectingKeyframeRtp = true;
|
|
@@ -5714,23 +5767,49 @@ var RtspRestreamer = class {
|
|
|
5714
5767
|
}
|
|
5715
5768
|
if (this.sessions.size === 0) return;
|
|
5716
5769
|
this.servePendingSessionsRtp();
|
|
5770
|
+
if (keyframeAuStart) for (const session of this.sessions.values()) {
|
|
5771
|
+
if (!session.isMuted() || !session.isPlaying()) continue;
|
|
5772
|
+
this.pendingKeyframe.delete(session.getSessionId());
|
|
5773
|
+
}
|
|
5717
5774
|
for (const session of this.sessions.values()) {
|
|
5718
5775
|
if (this.pendingKeyframe.has(session.getSessionId())) continue;
|
|
5719
|
-
if (session.isPlaying()) session.sendRtp(rtpPacket);
|
|
5776
|
+
if (session.isPlaying()) session.sendRtp(rtpPacket, false, isKeyframe || isParameterSet);
|
|
5720
5777
|
}
|
|
5721
5778
|
}
|
|
5722
5779
|
/** Send cached keyframe RTP packets to sessions that transitioned to PLAY. */
|
|
5723
5780
|
servePendingSessionsRtp() {
|
|
5724
5781
|
if (this.pendingKeyframe.size === 0) return;
|
|
5725
|
-
|
|
5782
|
+
const ringPackets = this.rtpPreBufferSupplier?.() ?? [];
|
|
5783
|
+
const fromRing = ringPackets.length > 0;
|
|
5784
|
+
const primePackets = fromRing ? ringPackets.map((data) => ({
|
|
5785
|
+
data,
|
|
5786
|
+
marker: (data[1] & 128) !== 0
|
|
5787
|
+
})) : this.lastKeyframeRtpPackets;
|
|
5788
|
+
if (primePackets.length === 0) return;
|
|
5789
|
+
const firstTs = primePackets[0].data.readUInt32BE(4);
|
|
5790
|
+
const spanMs = (primePackets[primePackets.length - 1].data.readUInt32BE(4) - firstTs | 0) / 90;
|
|
5726
5791
|
const ready = [];
|
|
5727
5792
|
for (const sessionId of this.pendingKeyframe) if (this.sessions.get(sessionId)?.isPlaying()) ready.push(sessionId);
|
|
5728
5793
|
if (ready.length === 0) return;
|
|
5729
|
-
|
|
5794
|
+
const served = [];
|
|
5795
|
+
for (const sessionId of ready) {
|
|
5796
|
+
const session = this.sessions.get(sessionId);
|
|
5797
|
+
if (!session) continue;
|
|
5798
|
+
if (session.isMuted() && (!fromRing || spanMs > LIVE_EDGE_MAX_SPAN_MS)) continue;
|
|
5799
|
+
served.push(sessionId);
|
|
5800
|
+
}
|
|
5801
|
+
if (served.length === 0) return;
|
|
5802
|
+
this.logger?.info("restream prime (lat-probe)", { meta: {
|
|
5803
|
+
source: fromRing ? "gop-burst" : "keyframe-only",
|
|
5804
|
+
packets: primePackets.length,
|
|
5805
|
+
spanMs: Math.round(spanMs),
|
|
5806
|
+
clients: served.length
|
|
5807
|
+
} });
|
|
5808
|
+
for (const rtpPacket of primePackets) for (const sessionId of served) {
|
|
5730
5809
|
const session = this.sessions.get(sessionId);
|
|
5731
|
-
if (session?.isPlaying()) session.sendRtp(rtpPacket);
|
|
5810
|
+
if (session?.isPlaying()) session.sendRtp(rtpPacket, true);
|
|
5732
5811
|
}
|
|
5733
|
-
for (const id of
|
|
5812
|
+
for (const id of served) this.pendingKeyframe.delete(id);
|
|
5734
5813
|
}
|
|
5735
5814
|
/**
|
|
5736
5815
|
* Push a raw audio RTP packet from the native RTSP client.
|
|
@@ -8640,6 +8719,38 @@ var AudioCodecSession = class {
|
|
|
8640
8719
|
sessionId: this.sessionId
|
|
8641
8720
|
};
|
|
8642
8721
|
}
|
|
8722
|
+
/**
|
|
8723
|
+
* Tear down the upstream decode session + 40ms poll loop WITHOUT
|
|
8724
|
+
* permanently closing this handle. The broker calls this from its
|
|
8725
|
+
* audio-consumer demand-gate: when audio consumers drop to zero the
|
|
8726
|
+
* upstream decoder session and its poll loop are pure waste, so we
|
|
8727
|
+
* release them. Unlike {@link close} this leaves the handle reusable —
|
|
8728
|
+
* the next pushed packet re-creates the session lazily via
|
|
8729
|
+
* `ensureSession()`. Safe to call repeatedly.
|
|
8730
|
+
*/
|
|
8731
|
+
async idle() {
|
|
8732
|
+
if (this.closed) return;
|
|
8733
|
+
if (this.creating) await this.creating;
|
|
8734
|
+
if (this.closed) return;
|
|
8735
|
+
if (this.pollTimer) {
|
|
8736
|
+
clearInterval(this.pollTimer);
|
|
8737
|
+
this.pollTimer = null;
|
|
8738
|
+
}
|
|
8739
|
+
if (!this.sessionId) return;
|
|
8740
|
+
const sid = this.sessionId;
|
|
8741
|
+
const nodeId = this.sessionNodeId ?? void 0;
|
|
8742
|
+
this.sessionId = null;
|
|
8743
|
+
this.sessionNodeId = null;
|
|
8744
|
+
try {
|
|
8745
|
+
await this.api.closeSession({
|
|
8746
|
+
sessionId: sid,
|
|
8747
|
+
...nodeId ? { nodeId } : {}
|
|
8748
|
+
});
|
|
8749
|
+
} catch (err) {
|
|
8750
|
+
this.logger?.warn("audio-codec: idle closeSession failed", { meta: { error: require_dist.errMsg(err) } });
|
|
8751
|
+
}
|
|
8752
|
+
this.logger?.info("audio-codec: decode session idled (no audio consumer)", { meta: { sessionId: sid } });
|
|
8753
|
+
}
|
|
8643
8754
|
async close() {
|
|
8644
8755
|
if (this.closed) return;
|
|
8645
8756
|
this.closed = true;
|
|
@@ -8867,12 +8978,25 @@ var EncodedRingBuffer = class {
|
|
|
8867
8978
|
clear() {
|
|
8868
8979
|
this.packets.length = 0;
|
|
8869
8980
|
}
|
|
8870
|
-
/**
|
|
8981
|
+
/**
|
|
8982
|
+
* Remove packets older than maxDurationMs based on PTS.
|
|
8983
|
+
*
|
|
8984
|
+
* PTS here is the source RTP timestamp (90kHz → ms), which is NOT
|
|
8985
|
+
* monotonic across the buffer's lifetime: an RTSP/rfc4571 redial re-bases
|
|
8986
|
+
* it to a fresh random value (RFC 3550) and the 32-bit field wraps every
|
|
8987
|
+
* ~13.25h. A purely `pts < cutoff` head-trim stalls forever whenever the
|
|
8988
|
+
* newest packet's pts drops below the stale head's — the buffer then
|
|
8989
|
+
* retains the entire pre-discontinuity history (an unbounded native-memory
|
|
8990
|
+
* leak of full-bitrate AnnexB copies). So we also evict any head packet
|
|
8991
|
+
* whose pts is GREATER than the newest packet's: it can only belong to a
|
|
8992
|
+
* prior, higher-based epoch that the source has since jumped away from.
|
|
8993
|
+
*/
|
|
8871
8994
|
evict() {
|
|
8872
8995
|
if (this.packets.length === 0) return;
|
|
8873
|
-
const
|
|
8996
|
+
const newest = this.packets[this.packets.length - 1].pts;
|
|
8997
|
+
const cutoff = newest - this.maxDurationMs;
|
|
8874
8998
|
let removeCount = 0;
|
|
8875
|
-
while (removeCount < this.packets.length && this.packets[removeCount].pts < cutoff) removeCount++;
|
|
8999
|
+
while (removeCount < this.packets.length && (this.packets[removeCount].pts < cutoff || this.packets[removeCount].pts > newest)) removeCount++;
|
|
8876
9000
|
if (removeCount > 0) this.packets.splice(0, removeCount);
|
|
8877
9001
|
}
|
|
8878
9002
|
};
|
|
@@ -9152,6 +9276,14 @@ var INITIAL_RECONNECT_DELAY_MS = 1e3;
|
|
|
9152
9276
|
var MAX_RECONNECT_DELAY_MS = 3e4;
|
|
9153
9277
|
/** Grace period before suspending after last consumer leaves. */
|
|
9154
9278
|
var SUSPEND_GRACE_MS = 5e3;
|
|
9279
|
+
/**
|
|
9280
|
+
* Grace window before an idle audio-codec decode session is torn down
|
|
9281
|
+
* after the last audio consumer detaches. Mirrors the frame-handle /
|
|
9282
|
+
* suspend occupancy-linger pattern: a brief gap between consumers (e.g.
|
|
9283
|
+
* a WebRTC renegotiation or an analyzer re-subscribe) must not thrash
|
|
9284
|
+
* the upstream decode session create/destroy.
|
|
9285
|
+
*/
|
|
9286
|
+
var AUDIO_CONSUMER_LINGER_MS = 5e3;
|
|
9155
9287
|
/** Timeout for push-mode sources: if no packet arrives within this window, emit error placeholder. */
|
|
9156
9288
|
var PUSH_STALL_TIMEOUT_MS = 15e3;
|
|
9157
9289
|
var StreamBroker = class StreamBroker {
|
|
@@ -9227,6 +9359,12 @@ var StreamBroker = class StreamBroker {
|
|
|
9227
9359
|
pushRtpFlagDepacketizer = null;
|
|
9228
9360
|
audioRtpDecoder = null;
|
|
9229
9361
|
audioCodecSession = null;
|
|
9362
|
+
/**
|
|
9363
|
+
* Pending occupancy-linger teardown of {@link audioCodecSession} after
|
|
9364
|
+
* the last audio consumer detached. Cancelled when a consumer returns
|
|
9365
|
+
* within {@link AUDIO_CONSUMER_LINGER_MS}. See {@link reconcileAudioConsumer}.
|
|
9366
|
+
*/
|
|
9367
|
+
audioConsumerLingerTimer;
|
|
9230
9368
|
audioRtpSeen = 0;
|
|
9231
9369
|
/** Detected audio codec from RTSP DESCRIBE (e.g. 'PCMU', 'PCMA'). */
|
|
9232
9370
|
audioCodec = null;
|
|
@@ -9440,6 +9578,7 @@ var StreamBroker = class StreamBroker {
|
|
|
9440
9578
|
this.pipeServer = new StreamPipeServer(logger?.child("pipe-server"));
|
|
9441
9579
|
this.rtspRestreamer = new RtspRestreamer(deviceId);
|
|
9442
9580
|
if (logger) this.rtspRestreamer.setLogger(logger.child("rtsp"));
|
|
9581
|
+
this.rtspRestreamer.setRtpPreBufferSupplier(() => this.getRtpPreBuffer());
|
|
9443
9582
|
this.preBuffer = new EncodedRingBuffer(StreamBroker.DEFAULT_PRE_BUFFER_SEC);
|
|
9444
9583
|
this.decoderSeedBuffer = new EncodedRingBuffer(StreamBroker.DECODER_SEED_BUFFER_SEC);
|
|
9445
9584
|
this.audioChunkPlane = new AudioChunkPlane(logger?.child("audio-chunk-plane"));
|
|
@@ -9688,11 +9827,54 @@ var StreamBroker = class StreamBroker {
|
|
|
9688
9827
|
return false;
|
|
9689
9828
|
}
|
|
9690
9829
|
/**
|
|
9830
|
+
* True when at least one consumer needs DECODED audio. The audio-specific
|
|
9831
|
+
* subset of {@link hasDemand} — deliberately excludes the video
|
|
9832
|
+
* frame-handle plane so a video-only viewer never spins up the
|
|
9833
|
+
* AAC/Opus decode chain. `encodedCallbacks` covers WebRTC (its G.711
|
|
9834
|
+
* egress needs decoded PCM); `audioChunkPlane` covers the analyzer.
|
|
9835
|
+
*/
|
|
9836
|
+
hasAudioConsumer() {
|
|
9837
|
+
return this.encodedCallbacks.size > 0 || this.audioChunkPlane.subscriberCount > 0;
|
|
9838
|
+
}
|
|
9839
|
+
/**
|
|
9840
|
+
* Demand-gate the audio-codec decode session lifecycle. Called from
|
|
9841
|
+
* every consumer add/remove edge (via {@link checkDemand}). While an
|
|
9842
|
+
* audio consumer exists, cancel any pending linger-close. When
|
|
9843
|
+
* consumers drop to zero, arm a short linger timer that tears the
|
|
9844
|
+
* upstream decode session + 40ms poll loop down (via
|
|
9845
|
+
* `AudioCodecSession.idle`) so a momentary gap between consumers
|
|
9846
|
+
* doesn't thrash create/destroy. The next gated packet re-arms the
|
|
9847
|
+
* session lazily through `ensureSession()`. No-op when no session is
|
|
9848
|
+
* armed (native PCMU/PCMA cameras never build one).
|
|
9849
|
+
*/
|
|
9850
|
+
reconcileAudioConsumer() {
|
|
9851
|
+
if (!this.audioCodecSession) return;
|
|
9852
|
+
if (this.hasAudioConsumer()) {
|
|
9853
|
+
if (this.audioConsumerLingerTimer) {
|
|
9854
|
+
clearTimeout(this.audioConsumerLingerTimer);
|
|
9855
|
+
this.audioConsumerLingerTimer = void 0;
|
|
9856
|
+
}
|
|
9857
|
+
return;
|
|
9858
|
+
}
|
|
9859
|
+
if (this.audioConsumerLingerTimer) return;
|
|
9860
|
+
this.audioConsumerLingerTimer = setTimeout(() => {
|
|
9861
|
+
this.audioConsumerLingerTimer = void 0;
|
|
9862
|
+
if (this.hasAudioConsumer()) return;
|
|
9863
|
+
const session = this.audioCodecSession;
|
|
9864
|
+
if (!session) return;
|
|
9865
|
+
session.idle().catch((err) => {
|
|
9866
|
+
this.logger?.warn("audio-codec: idle teardown failed", { meta: { error: require_dist.errMsg(err) } });
|
|
9867
|
+
});
|
|
9868
|
+
}, AUDIO_CONSUMER_LINGER_MS);
|
|
9869
|
+
if (typeof this.audioConsumerLingerTimer.unref === "function") this.audioConsumerLingerTimer.unref();
|
|
9870
|
+
}
|
|
9871
|
+
/**
|
|
9691
9872
|
* Check demand and schedule suspend/resume. Called after every subscriber
|
|
9692
9873
|
* add/remove and after pre-buffer enable/disable.
|
|
9693
9874
|
*/
|
|
9694
9875
|
checkDemand() {
|
|
9695
9876
|
if (this.manualStop || this.stopping) return;
|
|
9877
|
+
this.reconcileAudioConsumer();
|
|
9696
9878
|
if (this.hasDemand()) {
|
|
9697
9879
|
if (!this.demandActive) {
|
|
9698
9880
|
this.demandActive = true;
|
|
@@ -9838,7 +10020,7 @@ var StreamBroker = class StreamBroker {
|
|
|
9838
10020
|
if (packet.type === "audio" && this.source?.type === "push" && this.audioCodecSession) {
|
|
9839
10021
|
const codec = packet.codec.toLowerCase();
|
|
9840
10022
|
if (codec !== "pcmu" && codec !== "pcma") {
|
|
9841
|
-
this.audioCodecSession.pushRawFrame(packet.data);
|
|
10023
|
+
if (this.hasAudioConsumer()) this.audioCodecSession.pushRawFrame(packet.data);
|
|
9842
10024
|
return;
|
|
9843
10025
|
}
|
|
9844
10026
|
}
|
|
@@ -10279,7 +10461,8 @@ var StreamBroker = class StreamBroker {
|
|
|
10279
10461
|
logger: this.logger?.child("frame-handle-plane"),
|
|
10280
10462
|
resolveStreamInfo: () => this.resolveFrameHandleStreamInfo(),
|
|
10281
10463
|
localNodeId: this.localNodeId,
|
|
10282
|
-
getSeedPackets: () => this.decoderSeedBuffer.getPackets()
|
|
10464
|
+
getSeedPackets: () => this.decoderSeedBuffer.getPackets(),
|
|
10465
|
+
isDebugEnabled: () => this.streamingDebug
|
|
10283
10466
|
});
|
|
10284
10467
|
return this.frameHandlePlane;
|
|
10285
10468
|
}
|
|
@@ -10361,6 +10544,17 @@ var StreamBroker = class StreamBroker {
|
|
|
10361
10544
|
subscribedAt: entry.subscribedAt,
|
|
10362
10545
|
packetsDelivered: entry.packetsDelivered
|
|
10363
10546
|
});
|
|
10547
|
+
if (this._preBufferEnabled && encoded.length === 0) encoded.push({
|
|
10548
|
+
id: "prebuffer-hold",
|
|
10549
|
+
channel: "annexb",
|
|
10550
|
+
attribution: {
|
|
10551
|
+
kind: "warmup",
|
|
10552
|
+
userAgent: "prebuffer",
|
|
10553
|
+
media: "video"
|
|
10554
|
+
},
|
|
10555
|
+
subscribedAt: this.startedAt,
|
|
10556
|
+
packetsDelivered: 0
|
|
10557
|
+
});
|
|
10364
10558
|
return {
|
|
10365
10559
|
rtsp: this.rtspRestreamer.listSessionInfos(),
|
|
10366
10560
|
decoded: (this.frameHandlePlane?.listSubscribers() ?? []).map((s) => ({
|
|
@@ -10726,7 +10920,7 @@ var StreamBroker = class StreamBroker {
|
|
|
10726
10920
|
hasCodecSession: this.audioCodecSession !== null
|
|
10727
10921
|
} });
|
|
10728
10922
|
this.audioRtpDecoder?.processPacket(rtpData);
|
|
10729
|
-
this.audioCodecSession?.processPacket(rtpData);
|
|
10923
|
+
if (this.hasAudioConsumer()) this.audioCodecSession?.processPacket(rtpData);
|
|
10730
10924
|
if ((this.audioCodec === "PCMU" || this.audioCodec === "PCMA") && this.encodedCallbacks.size > 0 && rtpData.length > 12) {
|
|
10731
10925
|
const payload = rtpData.subarray(12);
|
|
10732
10926
|
if (payload.length > 0) {
|
|
@@ -11207,6 +11401,10 @@ var StreamBroker = class StreamBroker {
|
|
|
11207
11401
|
this.audioRtpDecoder.flush();
|
|
11208
11402
|
this.audioRtpDecoder = null;
|
|
11209
11403
|
}
|
|
11404
|
+
if (this.audioConsumerLingerTimer) {
|
|
11405
|
+
clearTimeout(this.audioConsumerLingerTimer);
|
|
11406
|
+
this.audioConsumerLingerTimer = void 0;
|
|
11407
|
+
}
|
|
11210
11408
|
if (this.audioCodecSession) {
|
|
11211
11409
|
const session = this.audioCodecSession;
|
|
11212
11410
|
this.audioCodecSession = null;
|
|
@@ -14257,6 +14455,11 @@ var StreamBrokerManager = class {
|
|
|
14257
14455
|
break;
|
|
14258
14456
|
}
|
|
14259
14457
|
this.assignments.set(deviceId, nextAssignment);
|
|
14458
|
+
if (current.map.high !== newMap.high) for (const camStreamId of [current.map.high, newMap.high]) {
|
|
14459
|
+
if (!camStreamId) continue;
|
|
14460
|
+
const broker = this.brokers.get(brokerIdFor(deviceId, camStreamId));
|
|
14461
|
+
if (broker) this.applyPreBufferConfig(broker, deviceId, camStreamId);
|
|
14462
|
+
}
|
|
14260
14463
|
for (const [camId, prevCount] of oldPushConsumers) if ((newPushConsumers.get(camId) ?? 0) === 0 && prevCount > 0) this.emitCamStreamIdle(deviceId, camId);
|
|
14261
14464
|
for (const [camId, nextCount] of newPushConsumers) if ((oldPushConsumers.get(camId) ?? 0) === 0 && nextCount > 0) {
|
|
14262
14465
|
let triggerProfile = "mid";
|
|
@@ -14462,7 +14665,7 @@ var StreamBrokerManager = class {
|
|
|
14462
14665
|
broker.setRestreamers(this.restreamers);
|
|
14463
14666
|
broker.setRestreamUrlResolver(() => this.rtspProvider.getEntry(brokerId)?.mutedUrl ?? null);
|
|
14464
14667
|
broker.setFfmpegBinaryPath(this.ffmpegConfig.binaryPath);
|
|
14465
|
-
this.applyPreBufferConfig(broker, deviceId, camStreamId
|
|
14668
|
+
this.applyPreBufferConfig(broker, deviceId, camStreamId);
|
|
14466
14669
|
if (this.deviceOverrides.get(deviceId)?.streamingDebug) broker.setStreamingDebug(true);
|
|
14467
14670
|
broker.setSourceProvider(() => {
|
|
14468
14671
|
const latest = this.cameraStreams.get(deviceId)?.get(camStreamId);
|
|
@@ -14598,34 +14801,48 @@ var StreamBrokerManager = class {
|
|
|
14598
14801
|
};
|
|
14599
14802
|
}
|
|
14600
14803
|
/**
|
|
14804
|
+
* Whether a stream is warmed BY DEFAULT (no explicit override): the device's
|
|
14805
|
+
* `high` profile, on a non-battery camera. Single source of truth used by both
|
|
14806
|
+
* the broker-config default (`applyPreBufferConfig`) and the settings-patch
|
|
14807
|
+
* override synthesis (`defaultStreamPreBuffer`) so a seconds-only patch never
|
|
14808
|
+
* silently disables the high-profile warm default.
|
|
14809
|
+
*/
|
|
14810
|
+
isDefaultWarm(deviceId, camStreamId) {
|
|
14811
|
+
return findProfileForStream(this.assignments.get(deviceId)?.map ?? {}, camStreamId) === "high" && !this.deviceHasFeature(deviceId, require_dist.DeviceFeature.BatteryOperated);
|
|
14812
|
+
}
|
|
14813
|
+
/**
|
|
14601
14814
|
* Default `StreamPreBuffer` for a brand-new override entry — mirrors
|
|
14602
|
-
* the
|
|
14603
|
-
*
|
|
14604
|
-
*
|
|
14605
|
-
*
|
|
14606
|
-
*
|
|
14815
|
+
* the effective broker default: `enabled` reflects the high-profile warm
|
|
14816
|
+
* default (`isDefaultWarm`), so a seconds-only patch on a warm high stream
|
|
14817
|
+
* inherits `enabled: true` instead of silently forcing it OFF. Operators
|
|
14818
|
+
* still override explicitly per-stream via the device-settings sub-tabs;
|
|
14819
|
+
* detection / recording paths pass `forceForDetection` to
|
|
14820
|
+
* `getEffectivePreBufferSec` when they need a buffered window irrespective
|
|
14821
|
+
* of the operator toggle.
|
|
14607
14822
|
*/
|
|
14608
|
-
defaultStreamPreBuffer(
|
|
14823
|
+
defaultStreamPreBuffer(deviceId, camStreamId) {
|
|
14609
14824
|
return {
|
|
14610
|
-
enabled:
|
|
14825
|
+
enabled: this.isDefaultWarm(deviceId, camStreamId),
|
|
14611
14826
|
seconds: this.defaultPreBufferSec
|
|
14612
14827
|
};
|
|
14613
14828
|
}
|
|
14614
14829
|
/**
|
|
14615
14830
|
* Apply the effective pre-buffer config to a fresh broker instance.
|
|
14616
|
-
* Default behaviour
|
|
14617
|
-
*
|
|
14618
|
-
* recording subscriber attaches.
|
|
14831
|
+
* Default behaviour: the HIGH profile stream is warm (see below);
|
|
14832
|
+
* every other stream stays OFF — idle until the operator opts in or
|
|
14833
|
+
* a detection / recording subscriber attaches. An explicit per-stream
|
|
14834
|
+
* operator override always wins over both defaults.
|
|
14619
14835
|
*/
|
|
14620
|
-
applyPreBufferConfig(broker, deviceId, camStreamId
|
|
14836
|
+
applyPreBufferConfig(broker, deviceId, camStreamId) {
|
|
14621
14837
|
const perStream = this.deviceOverrides.get(deviceId)?.preBuffer?.[camStreamId];
|
|
14622
14838
|
if (perStream) {
|
|
14623
14839
|
broker.setPreBufferEnabled(perStream.enabled);
|
|
14624
14840
|
broker.setPreBufferDuration(perStream.enabled ? perStream.seconds : 0);
|
|
14625
14841
|
return;
|
|
14626
14842
|
}
|
|
14627
|
-
|
|
14628
|
-
broker.
|
|
14843
|
+
const defaultWarm = this.isDefaultWarm(deviceId, camStreamId);
|
|
14844
|
+
broker.setPreBufferEnabled(defaultWarm);
|
|
14845
|
+
broker.setPreBufferDuration(defaultWarm ? this.defaultPreBufferSec : 0);
|
|
14629
14846
|
}
|
|
14630
14847
|
async fanOutRegistration(deviceId, brokerId, cam, broker) {
|
|
14631
14848
|
const log = this.logger.withTags({
|
|
@@ -17410,6 +17627,15 @@ var BROKER_METRICS_HEARTBEAT_MS = 3e4;
|
|
|
17410
17627
|
/** Throttle for persisting the per-stream probe map — bounds writes regardless
|
|
17411
17628
|
* of streaming churn while keeping the durable copy recent enough for restart. */
|
|
17412
17629
|
var PROBE_SNAPSHOTS_PERSIST_INTERVAL_MS = 6e4;
|
|
17630
|
+
/**
|
|
17631
|
+
* Defensive timeout for the per-session `networkQuality.getDeviceStats` poll.
|
|
17632
|
+
* The stat is an OPTIONAL input to the adaptive downgrade ladder — never worth
|
|
17633
|
+
* blocking the loop for. When the hub-resident provider is transiently
|
|
17634
|
+
* undiscoverable the child's unowned-cap routing falls to the Moleculer
|
|
17635
|
+
* broker-fallback, whose discovery wait is ~30s; without this bound a single
|
|
17636
|
+
* poll would stall the adaptive loop for that long. On timeout we simply treat
|
|
17637
|
+
* the sample as absent (the RTCP Receiver-Report path drives the ladder anyway). */
|
|
17638
|
+
var NETWORK_QUALITY_STATS_TIMEOUT_MS = 2e3;
|
|
17413
17639
|
var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
17414
17640
|
brokerManager = null;
|
|
17415
17641
|
/** ffmpeg binary path from the `ffmpeg` config section, mirrored here so the
|
|
@@ -17738,10 +17964,14 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
17738
17964
|
}
|
|
17739
17965
|
},
|
|
17740
17966
|
clientLossPct: async (deviceId) => {
|
|
17967
|
+
const ctrl = new AbortController();
|
|
17968
|
+
const timer = setTimeout(() => ctrl.abort(), NETWORK_QUALITY_STATS_TIMEOUT_MS);
|
|
17741
17969
|
try {
|
|
17742
|
-
return (await this.ctx.api.networkQuality.getDeviceStats.query({ deviceId }))?.client?.packetLossPercent ?? null;
|
|
17970
|
+
return (await this.ctx.api.networkQuality.getDeviceStats.query({ deviceId }, { signal: ctrl.signal }))?.client?.packetLossPercent ?? null;
|
|
17743
17971
|
} catch {
|
|
17744
17972
|
return null;
|
|
17973
|
+
} finally {
|
|
17974
|
+
clearTimeout(timer);
|
|
17745
17975
|
}
|
|
17746
17976
|
},
|
|
17747
17977
|
resolveTranscodeDecodeHwAccel: () => this.resolveTranscodeDecodeHwAccel(),
|