@camstack/addon-pipeline 1.2.257 → 1.2.259
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 +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/{default-detection-model-BTzxxm-z.mjs → default-detection-model-BZ54sUY2.mjs} +1 -1
- package/dist/{default-detection-model-C8CHpxQl.js → default-detection-model-vVWcdv0g.js} +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-BMARLLlX.js → dist-CsJJPDQW.js} +154 -1
- package/dist/{dist-xsaGDCOu.mjs → dist-D3w2fwD4.mjs} +154 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-Ca-vE0xX.js → node-1vbEWzPZ.js} +1 -1
- package/dist/{node-Fv4ByT36.mjs → node-IHGNrWH9.mjs} +1 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +4 -4
- package/dist/{process-memory-C3Wjmvm0.js → process-memory-DynU4177.js} +1 -1
- package/dist/{process-memory-BgtQADAY.mjs → process-memory-jhOBJEil.mjs} +1 -1
- package/dist/recorder/index.js +293 -19
- package/dist/recorder/index.mjs +293 -19
- package/dist/{restream-intent-D2VT97-U.mjs → restream-intent-CW79dvFV.mjs} +118 -2
- package/dist/{restream-intent-VVJiUAkh.js → restream-intent-iKGCB9IR.js} +135 -1
- package/dist/{segment-demux-js-C7C6c-g2.mjs → segment-demux-js-BZAiu_2h.mjs} +1 -1
- package/dist/{segment-demux-js-DHV7EOQf.js → segment-demux-js-OQT6wqXH.js} +1 -1
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CJWLoj3Z.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CFcO9OoY.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CTHexfai.mjs +26 -0
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-BzBcvawL.mjs → hostInit-rU23F6nl.mjs} +3 -3
- package/dist/stream-broker/index.js +476 -17
- package/dist/stream-broker/index.mjs +476 -17
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-ReCGmeOG.mjs +0 -26
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { A as RingBuffer, B as declareLogChannel, Bt as buildFfmpegArgs$1, E as RATE_CONTROL_TIGHT, Et as resolveEgressDecodeHwAccel, Gt as CAM_PROFILE_ORDER, H as deriveBatteryPresence, Ht as isSoftwareDecode, I as batteryCapability, It as webrtcSessionCapability, K as egressTranscodeSharingKey, Kt as DeviceFeature, L as cameraStreamsCapability, Lt as errMsg, M as addonWidgetsSourceCapability, Nt as streamBrokerCapability, Qt as expandAudioChunkToF32le, R as createHwAccelCache, Rt as AUDIO_PRESETS, T as RATE_CONTROL_RELAXED, Ut as BOOT_RECOVERY_BACKOFF_MS, Vt as invocationFromEncodeProfile, Wt as BaseAddon, Yt as createEvent, _n as record, an as parseProfileBrokerId, at as logChannelsCapability, bn as EventCategory, cn as _enum, dn as discriminatedUnion, en as isEvent, h as EncodeProfileSchema, hn as object, i as BatteryStatusSchema, it as loadContributionCapability, ln as array, mn as number, nn as makeSourceBrokerId, pn as literal, q as egressTransportFromRequest, qt as DeviceType, rn as nodePin, st as maskUrlCredentials, tn as makeProfileBrokerId, un as boolean, v as G711_SCALE_CORRECTION_DB, vn as string, xt as rankStreams, yn as union, z as createLogChannelsProvider } from "../dist-
|
|
3
|
-
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-
|
|
4
|
-
import {
|
|
2
|
+
import { A as RingBuffer, B as declareLogChannel, Bt as buildFfmpegArgs$1, E as RATE_CONTROL_TIGHT, Et as resolveEgressDecodeHwAccel, Gt as CAM_PROFILE_ORDER, H as deriveBatteryPresence, Ht as isSoftwareDecode, I as batteryCapability, It as webrtcSessionCapability, K as egressTranscodeSharingKey, Kt as DeviceFeature, L as cameraStreamsCapability, Lt as errMsg, M as addonWidgetsSourceCapability, Nt as streamBrokerCapability, Qt as expandAudioChunkToF32le, R as createHwAccelCache, Rt as AUDIO_PRESETS, T as RATE_CONTROL_RELAXED, Ut as BOOT_RECOVERY_BACKOFF_MS, Vt as invocationFromEncodeProfile, Wt as BaseAddon, Yt as createEvent, _n as record, an as parseProfileBrokerId, at as logChannelsCapability, bn as EventCategory, cn as _enum, dn as discriminatedUnion, en as isEvent, h as EncodeProfileSchema, hn as object, i as BatteryStatusSchema, it as loadContributionCapability, ln as array, mn as number, nn as makeSourceBrokerId, pn as literal, q as egressTransportFromRequest, qt as DeviceType, rn as nodePin, st as maskUrlCredentials, tn as makeProfileBrokerId, un as boolean, v as G711_SCALE_CORRECTION_DB, vn as string, xt as rankStreams, yn as union, z as createLogChannelsProvider } from "../dist-D3w2fwD4.mjs";
|
|
3
|
+
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-IHGNrWH9.mjs";
|
|
4
|
+
import { a as parseRestreamPath, d as PREBUFFER_MAX_MS, f as capPreRollRequestMs, i as isLiveEdgeOnlyDial, m as resolvePrebuffer, n as NO_DECLARATION, p as prebufferRetentionMs, s as sdpVariantFor, u as withSentryIntent } from "../restream-intent-CW79dvFV.mjs";
|
|
5
5
|
import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
|
|
6
6
|
import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
|
|
7
|
-
import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-
|
|
7
|
+
import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-BZAiu_2h.mjs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import { networkInterfaces, tmpdir } from "node:os";
|
|
@@ -2546,6 +2546,218 @@ function deviceIdFromBrokerId$1(brokerId) {
|
|
|
2546
2546
|
const parsed = Number(brokerId.split("/")[0]);
|
|
2547
2547
|
return Number.isFinite(parsed) ? parsed : void 0;
|
|
2548
2548
|
}
|
|
2549
|
+
//#endregion
|
|
2550
|
+
//#region src/stream-broker/rtsp/audio/g711-silence.ts
|
|
2551
|
+
var SILENCE_BYTE = {
|
|
2552
|
+
/** µ-law digital zero. */
|
|
2553
|
+
PCMU: 255,
|
|
2554
|
+
/** A-law digital zero. */
|
|
2555
|
+
PCMA: 213
|
|
2556
|
+
};
|
|
2557
|
+
var STATIC_PAYLOAD_TYPE = {
|
|
2558
|
+
PCMU: 0,
|
|
2559
|
+
PCMA: 8
|
|
2560
|
+
};
|
|
2561
|
+
/** G.711 is 8 kHz mono by definition (RFC 3551). */
|
|
2562
|
+
var G711_CLOCK_RATE_HZ = 8e3;
|
|
2563
|
+
var G711_CHANNELS = 1;
|
|
2564
|
+
/**
|
|
2565
|
+
* The track for a codec name as the camera's SDP spelled it, or `null`.
|
|
2566
|
+
*
|
|
2567
|
+
* `null` is a REFUSAL with a reason the caller must log, never a fallback to
|
|
2568
|
+
* PCMU: a sentry filled with the wrong codec's bytes is audible garbage on
|
|
2569
|
+
* every consumer, which is strictly worse than the silence of no track at all.
|
|
2570
|
+
*/
|
|
2571
|
+
function g711TrackFor(codec) {
|
|
2572
|
+
if (codec === void 0) return null;
|
|
2573
|
+
const upper = codec.toUpperCase();
|
|
2574
|
+
if (upper !== "PCMU" && upper !== "PCMA") return null;
|
|
2575
|
+
return {
|
|
2576
|
+
codec: upper,
|
|
2577
|
+
payloadType: STATIC_PAYLOAD_TYPE[upper],
|
|
2578
|
+
clockRateHz: G711_CLOCK_RATE_HZ,
|
|
2579
|
+
channels: G711_CHANNELS
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
/**
|
|
2583
|
+
* The `m=audio` block grafted onto the sentry's video-only SDP.
|
|
2584
|
+
*
|
|
2585
|
+
* Built rather than extracted from the camera's SDP on purpose. A static
|
|
2586
|
+
* payload type makes the description fully determined by the codec, so the
|
|
2587
|
+
* block is correct even when no camera SDP exists — which is the state this
|
|
2588
|
+
* whole path is for. `extractAudioMediaBlock` cannot serve here: it remaps a
|
|
2589
|
+
* dynamic payload type to 97 for the ffmpeg egress graft, and a remap is
|
|
2590
|
+
* exactly what must not happen to a track the camera will later fill itself.
|
|
2591
|
+
*/
|
|
2592
|
+
function g711AudioMediaBlock(track, trackId) {
|
|
2593
|
+
return [
|
|
2594
|
+
`m=audio 0 RTP/AVP ${track.payloadType}`,
|
|
2595
|
+
"c=IN IP4 0.0.0.0",
|
|
2596
|
+
`a=rtpmap:${track.payloadType} ${track.codec}/${track.clockRateHz}`,
|
|
2597
|
+
`a=control:trackID=${trackId}`
|
|
2598
|
+
].join("\r\n");
|
|
2599
|
+
}
|
|
2600
|
+
/**
|
|
2601
|
+
* One complete RTP packet of silence: 12-byte header plus one frame of the
|
|
2602
|
+
* codec's zero byte. Pure — the caller owns the sequence, the clock and the
|
|
2603
|
+
* decision to send.
|
|
2604
|
+
*/
|
|
2605
|
+
function buildG711SilenceRtp(input) {
|
|
2606
|
+
const packet = Buffer.alloc(172, SILENCE_BYTE[input.track.codec]);
|
|
2607
|
+
packet[0] = 128;
|
|
2608
|
+
packet[1] = (input.marker ? 128 : 0) | input.track.payloadType & 127;
|
|
2609
|
+
packet.writeUInt16BE(input.sequenceNumber & 65535, 2);
|
|
2610
|
+
packet.writeUInt32BE(input.rtpTimestamp >>> 0, 4);
|
|
2611
|
+
packet.writeUInt32BE(input.ssrc >>> 0, 8);
|
|
2612
|
+
return packet;
|
|
2613
|
+
}
|
|
2614
|
+
//#endregion
|
|
2615
|
+
//#region src/stream-broker/rtsp/audio/sentry-audio-silence.ts
|
|
2616
|
+
/**
|
|
2617
|
+
* How long the camera must be quiet before the filler takes the track back.
|
|
2618
|
+
*
|
|
2619
|
+
* Longer than any plausible inter-packet gap on a live G.711 stream (20 ms
|
|
2620
|
+
* nominal, 50 pps) by two orders of magnitude, so a momentary stutter in real
|
|
2621
|
+
* audio can never be papered over with generated frames; short enough that a
|
|
2622
|
+
* consumer's decoder does not see a hole when the camera goes back to sleep.
|
|
2623
|
+
*/
|
|
2624
|
+
var DEFAULT_RESUME_GAP_MS = 1e3;
|
|
2625
|
+
var SentryAudioSilence = class {
|
|
2626
|
+
options;
|
|
2627
|
+
frameMs;
|
|
2628
|
+
resumeGapMs;
|
|
2629
|
+
tags;
|
|
2630
|
+
ssrc;
|
|
2631
|
+
timer;
|
|
2632
|
+
resumeTimer;
|
|
2633
|
+
sequenceNumber;
|
|
2634
|
+
rtpTimestamp = 0;
|
|
2635
|
+
markNext = true;
|
|
2636
|
+
emitted = 0;
|
|
2637
|
+
/** Frames that reached at least one session. See {@link stopRun}. */
|
|
2638
|
+
delivered = 0;
|
|
2639
|
+
lastRealAudioAt = 0;
|
|
2640
|
+
wanted = false;
|
|
2641
|
+
constructor(options) {
|
|
2642
|
+
this.options = options;
|
|
2643
|
+
this.frameMs = options.frameMs ?? 20;
|
|
2644
|
+
this.resumeGapMs = options.resumeGapMs ?? DEFAULT_RESUME_GAP_MS;
|
|
2645
|
+
this.tags = options.deviceId === void 0 ? {} : { tags: { deviceId: options.deviceId } };
|
|
2646
|
+
this.ssrc = Math.random() * 4294967295 >>> 0;
|
|
2647
|
+
this.sequenceNumber = Math.random() * 65535 & 65535;
|
|
2648
|
+
}
|
|
2649
|
+
/** The audio shape this filler was built for. */
|
|
2650
|
+
get track() {
|
|
2651
|
+
return this.options.track;
|
|
2652
|
+
}
|
|
2653
|
+
/** True while packets are actually being produced. */
|
|
2654
|
+
get running() {
|
|
2655
|
+
return this.timer !== void 0;
|
|
2656
|
+
}
|
|
2657
|
+
/**
|
|
2658
|
+
* Somebody wants the track filled. Idempotent — reconciliation calls this on
|
|
2659
|
+
* every session edge, and a second call while a run is in flight must not
|
|
2660
|
+
* restart the clock or re-log.
|
|
2661
|
+
*/
|
|
2662
|
+
want() {
|
|
2663
|
+
this.wanted = true;
|
|
2664
|
+
this.startRun();
|
|
2665
|
+
}
|
|
2666
|
+
/**
|
|
2667
|
+
* Nobody wants the track filled any more (the last sentry left, the operator
|
|
2668
|
+
* muted the device, the broker is going away). Idempotent.
|
|
2669
|
+
*/
|
|
2670
|
+
release(reason) {
|
|
2671
|
+
this.wanted = false;
|
|
2672
|
+
this.clearResume();
|
|
2673
|
+
this.stopRun(reason);
|
|
2674
|
+
}
|
|
2675
|
+
/**
|
|
2676
|
+
* The camera's own audio just arrived.
|
|
2677
|
+
*
|
|
2678
|
+
* Synchronous, and the first thing `pushAudioRtpPassthrough` does: the timer
|
|
2679
|
+
* is gone before this packet is written to any session, so no generated frame
|
|
2680
|
+
* can follow it. Called on EVERY real packet, not only the first — the rolling
|
|
2681
|
+
* gap is what lets the filler come back when the camera sleeps again, without
|
|
2682
|
+
* a second signal anybody has to remember to send.
|
|
2683
|
+
*/
|
|
2684
|
+
noteRealAudio(nowMs) {
|
|
2685
|
+
this.lastRealAudioAt = nowMs;
|
|
2686
|
+
this.stopRun("real audio");
|
|
2687
|
+
this.armResume();
|
|
2688
|
+
}
|
|
2689
|
+
startRun() {
|
|
2690
|
+
if (this.timer !== void 0) return;
|
|
2691
|
+
if (!this.wanted) return;
|
|
2692
|
+
if (Date.now() - this.lastRealAudioAt < this.resumeGapMs) {
|
|
2693
|
+
this.armResume();
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
this.rtpTimestamp = Math.floor(Date.now() * this.options.track.clockRateHz / 1e3) >>> 0;
|
|
2697
|
+
this.markNext = true;
|
|
2698
|
+
this.emitted = 0;
|
|
2699
|
+
this.delivered = 0;
|
|
2700
|
+
this.timer = setInterval(() => this.tick(), this.frameMs);
|
|
2701
|
+
this.options.logger?.info("sentry audio: filling the track with silence", {
|
|
2702
|
+
...this.tags,
|
|
2703
|
+
meta: {
|
|
2704
|
+
codec: this.options.track.codec,
|
|
2705
|
+
payloadType: this.options.track.payloadType,
|
|
2706
|
+
frameMs: this.frameMs,
|
|
2707
|
+
clockRateHz: this.options.track.clockRateHz
|
|
2708
|
+
}
|
|
2709
|
+
});
|
|
2710
|
+
}
|
|
2711
|
+
stopRun(reason) {
|
|
2712
|
+
if (this.timer === void 0) return;
|
|
2713
|
+
clearInterval(this.timer);
|
|
2714
|
+
this.timer = void 0;
|
|
2715
|
+
this.options.logger?.info("sentry audio: stopped filling the track", {
|
|
2716
|
+
...this.tags,
|
|
2717
|
+
meta: {
|
|
2718
|
+
reason,
|
|
2719
|
+
framesEmitted: this.emitted,
|
|
2720
|
+
framesDelivered: this.delivered
|
|
2721
|
+
}
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
/**
|
|
2725
|
+
* Re-check once the gap has elapsed.
|
|
2726
|
+
*
|
|
2727
|
+
* A single pending timeout rather than a free-running ticker that decides per
|
|
2728
|
+
* frame: while the camera is awake this costs one timer per second instead of
|
|
2729
|
+
* fifty, and while it is asleep it costs nothing at all — the run is the
|
|
2730
|
+
* ticker.
|
|
2731
|
+
*/
|
|
2732
|
+
armResume() {
|
|
2733
|
+
if (!this.wanted) return;
|
|
2734
|
+
if (this.resumeTimer !== void 0) return;
|
|
2735
|
+
this.resumeTimer = setTimeout(() => {
|
|
2736
|
+
this.resumeTimer = void 0;
|
|
2737
|
+
this.startRun();
|
|
2738
|
+
}, this.resumeGapMs);
|
|
2739
|
+
this.resumeTimer.unref?.();
|
|
2740
|
+
}
|
|
2741
|
+
clearResume() {
|
|
2742
|
+
if (this.resumeTimer === void 0) return;
|
|
2743
|
+
clearTimeout(this.resumeTimer);
|
|
2744
|
+
this.resumeTimer = void 0;
|
|
2745
|
+
}
|
|
2746
|
+
tick() {
|
|
2747
|
+
const rtp = buildG711SilenceRtp({
|
|
2748
|
+
track: this.options.track,
|
|
2749
|
+
ssrc: this.ssrc,
|
|
2750
|
+
sequenceNumber: this.sequenceNumber,
|
|
2751
|
+
rtpTimestamp: this.rtpTimestamp,
|
|
2752
|
+
marker: this.markNext
|
|
2753
|
+
});
|
|
2754
|
+
this.sequenceNumber = this.sequenceNumber + 1 & 65535;
|
|
2755
|
+
this.rtpTimestamp = this.rtpTimestamp + 160 >>> 0;
|
|
2756
|
+
this.markNext = false;
|
|
2757
|
+
this.emitted += 1;
|
|
2758
|
+
if (this.options.emit(rtp) > 0) this.delivered += 1;
|
|
2759
|
+
}
|
|
2760
|
+
};
|
|
2549
2761
|
/**
|
|
2550
2762
|
* Extract the `m=audio` media block from an ffmpeg-generated SDP, normalised
|
|
2551
2763
|
* for grafting (port 0, non-colliding payload type, own `a=control`, explicit
|
|
@@ -2593,7 +2805,7 @@ function appendAudioMediaBlock(videoSdp, audioBlock) {
|
|
|
2593
2805
|
* the result onto its counters and its `continue`.
|
|
2594
2806
|
*/
|
|
2595
2807
|
function admitJoinBurst(input) {
|
|
2596
|
-
if (input.
|
|
2808
|
+
if (input.liveEdgeOnly) return !input.fromRing || input.spanMs > input.liveEdgeMaxSpanMs ? "live-edge" : "none";
|
|
2597
2809
|
if (!input.detectionIntent && input.spanMs > input.bound.maxSpanMs) return "span";
|
|
2598
2810
|
if (input.packets > input.bound.maxPackets) return "packets";
|
|
2599
2811
|
return "none";
|
|
@@ -2795,6 +3007,14 @@ var LIVE_EDGE_MAX_SPAN_MS = 500;
|
|
|
2795
3007
|
*/
|
|
2796
3008
|
var PRIME_MAX_SPAN_MS = 2e3;
|
|
2797
3009
|
/**
|
|
3010
|
+
* The `a=control:trackID=` the sentry's grafted audio block is given.
|
|
3011
|
+
*
|
|
3012
|
+
* The same id the transcode egress grafts its own audio onto
|
|
3013
|
+
* (`extractAudioMediaBlock`'s default) and the same one `sendAudioRtp` writes
|
|
3014
|
+
* to (`tracks[1]`), because the video track is always trackID=0.
|
|
3015
|
+
*/
|
|
3016
|
+
var SENTRY_AUDIO_TRACK_ID = 1;
|
|
3017
|
+
/**
|
|
2798
3018
|
* Defensive packet ceiling on a join burst, independent of {@link
|
|
2799
3019
|
* PRIME_MAX_SPAN_MS}. The ring is trimmed to one GOP by RTP timestamp, so a
|
|
2800
3020
|
* burst this large means the timestamps stopped describing the stream (wrap,
|
|
@@ -2887,6 +3107,27 @@ var RtspRestreamer = class {
|
|
|
2887
3107
|
* a broker going idle overnight would wake up distributing audio again.
|
|
2888
3108
|
*/
|
|
2889
3109
|
deviceAudioMuted = false;
|
|
3110
|
+
/**
|
|
3111
|
+
* The camera's audio shape, remembered so a SENTRY can be handed a track to
|
|
3112
|
+
* fill while the camera is asleep.
|
|
3113
|
+
*
|
|
3114
|
+
* NOT cleared by {@link resetStreamState}, for exactly the reason
|
|
3115
|
+
* {@link deviceAudioMuted} is not: that method resets what belongs to one
|
|
3116
|
+
* CONNECTION, and this describes the CAMERA. A sleeping camera has no SDP to
|
|
3117
|
+
* read — it is the whole premise of the sentry — so if this did not survive
|
|
3118
|
+
* the suspend there would never be an audio track to announce at the moment
|
|
3119
|
+
* one is needed.
|
|
3120
|
+
*
|
|
3121
|
+
* `null` means nothing has ever described this camera's audio to us (a cold
|
|
3122
|
+
* hub whose battery camera has not been awake since boot), or its codec is
|
|
3123
|
+
* one whose silence cannot be generated exactly. Both are logged; neither is
|
|
3124
|
+
* guessed at.
|
|
3125
|
+
*/
|
|
3126
|
+
sentryAudioTrack = null;
|
|
3127
|
+
/** Fills a sentry's audio track while the camera sleeps. Built on demand. */
|
|
3128
|
+
sentryAudioSilence = null;
|
|
3129
|
+
/** One-shot guard for "a sentry attached and there is no track to fill". */
|
|
3130
|
+
sentryAudioGapLogged = false;
|
|
2890
3131
|
/** Cached last keyframe NALs (SPS/PPS + IDR) for late-joining clients. */
|
|
2891
3132
|
lastKeyframeNals = [];
|
|
2892
3133
|
lastKeyframeNalBytes = 0;
|
|
@@ -3095,6 +3336,156 @@ var RtspRestreamer = class {
|
|
|
3095
3336
|
return this.mutedSdp ?? this.sdp;
|
|
3096
3337
|
}
|
|
3097
3338
|
/**
|
|
3339
|
+
* The SDP a SENTRY dial is answered with: the picture it is being served,
|
|
3340
|
+
* plus the audio track that picture will be accompanied by.
|
|
3341
|
+
*
|
|
3342
|
+
* Three cases, in this order and no other:
|
|
3343
|
+
*
|
|
3344
|
+
* 1. The operator muted this camera broker-side (D83). That mute IS a device
|
|
3345
|
+
* policy and outranks everything here — the sentry is answered video-only,
|
|
3346
|
+
* exactly as every other consumer is.
|
|
3347
|
+
* 2. The current connection already describes audio (the camera is up and we
|
|
3348
|
+
* are serving its own SDP). It is served verbatim: grafting on top would
|
|
3349
|
+
* announce the track twice, and the camera's own description is by
|
|
3350
|
+
* definition the one its RTP will match.
|
|
3351
|
+
* 3. Otherwise — the sleeping case, where the SDP is built from the
|
|
3352
|
+
* placeholder frame and has no audio at all — the retained shape is
|
|
3353
|
+
* grafted on as trackID=1.
|
|
3354
|
+
*
|
|
3355
|
+
* Without a retained shape this degrades to the video-only SDP, which is what
|
|
3356
|
+
* the whole path used to serve unconditionally. That is a REFUSAL, reported
|
|
3357
|
+
* by {@link reconcileSentryAudio}: a sentry with no audio is a sentry that
|
|
3358
|
+
* will never have any, and the operator has to be able to see it rather than
|
|
3359
|
+
* infer it from a silent stream.
|
|
3360
|
+
*/
|
|
3361
|
+
getSentrySdp() {
|
|
3362
|
+
if (this.deviceAudioMuted) return this.getMutedSdp();
|
|
3363
|
+
const full = this.sdp;
|
|
3364
|
+
if (full !== null && /^m=audio/m.test(full)) return full;
|
|
3365
|
+
const videoOnly = this.getMutedSdp();
|
|
3366
|
+
const track = this.sentryAudioTrack;
|
|
3367
|
+
if (track === null || videoOnly === null) return videoOnly;
|
|
3368
|
+
return appendAudioMediaBlock(videoOnly, g711AudioMediaBlock(track, SENTRY_AUDIO_TRACK_ID));
|
|
3369
|
+
}
|
|
3370
|
+
/**
|
|
3371
|
+
* The SDP for a parsed dial's variant.
|
|
3372
|
+
*
|
|
3373
|
+
* One switch and not three call sites: the listen server reads the SDP twice
|
|
3374
|
+
* (once to wait for it, once to answer with it), and those two reads
|
|
3375
|
+
* disagreeing about which variant a dial gets is a class of bug that costs a
|
|
3376
|
+
* consumer an empty DESCRIBE.
|
|
3377
|
+
*/
|
|
3378
|
+
getSdpFor(variant) {
|
|
3379
|
+
if (variant === "sentry") return this.getSentrySdp();
|
|
3380
|
+
if (variant === "muted") return this.getMutedSdp();
|
|
3381
|
+
return this.getSdp();
|
|
3382
|
+
}
|
|
3383
|
+
/**
|
|
3384
|
+
* Remember (or forget) the camera's audio shape for the sentry.
|
|
3385
|
+
*
|
|
3386
|
+
* Called from the broker's `onAudioTrack`, which is the only place the
|
|
3387
|
+
* camera's own SDP is parsed. `null` forgets it — a camera whose audio was
|
|
3388
|
+
* turned off, or whose codec this system cannot fill exactly.
|
|
3389
|
+
*/
|
|
3390
|
+
retainSentryAudioTrack(track) {
|
|
3391
|
+
const prior = this.sentryAudioTrack;
|
|
3392
|
+
if (prior?.codec === track?.codec && prior?.payloadType === track?.payloadType) return;
|
|
3393
|
+
this.sentryAudioTrack = track;
|
|
3394
|
+
this.sentryAudioSilence?.release("audio shape changed");
|
|
3395
|
+
this.sentryAudioSilence = null;
|
|
3396
|
+
this.sentryAudioGapLogged = false;
|
|
3397
|
+
this.logger?.info("sentry audio: track shape retained", {
|
|
3398
|
+
tags: { deviceId: this.deviceId },
|
|
3399
|
+
meta: {
|
|
3400
|
+
streamName: this.streamName,
|
|
3401
|
+
codec: track?.codec ?? null
|
|
3402
|
+
}
|
|
3403
|
+
});
|
|
3404
|
+
this.reconcileSentryAudio();
|
|
3405
|
+
}
|
|
3406
|
+
/** The retained audio shape, or `null`. Survives a suspend by design. */
|
|
3407
|
+
getSentryAudioTrack() {
|
|
3408
|
+
return this.sentryAudioTrack;
|
|
3409
|
+
}
|
|
3410
|
+
/**
|
|
3411
|
+
* Whether generated silence is being produced for this stream right now.
|
|
3412
|
+
*
|
|
3413
|
+
* Exposed because "is the filler still running" is not answerable from the
|
|
3414
|
+
* wire: a run whose last sentry has left writes to nobody, so a socket that
|
|
3415
|
+
* sees nothing looks exactly like a timer that was released and exactly like
|
|
3416
|
+
* one that was leaked. That ambiguity survived a deliberate sabotage of
|
|
3417
|
+
* {@link removeSession}'s reconcile with the whole suite green.
|
|
3418
|
+
*/
|
|
3419
|
+
isSentryAudioFilling() {
|
|
3420
|
+
return this.sentryAudioSilence?.running === true;
|
|
3421
|
+
}
|
|
3422
|
+
/**
|
|
3423
|
+
* Start or stop filling the sentry audio track to match who is attached.
|
|
3424
|
+
*
|
|
3425
|
+
* Called on every edge that can change the answer — a session arriving or
|
|
3426
|
+
* leaving, the operator's mute, a new audio shape — and never on a per-packet
|
|
3427
|
+
* path. Idempotent in both directions.
|
|
3428
|
+
*/
|
|
3429
|
+
reconcileSentryAudio() {
|
|
3430
|
+
if (!(this.getPrestreamSessionCount() > 0 && !this.deviceAudioMuted)) {
|
|
3431
|
+
this.sentryAudioSilence?.release("no sentry session");
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
3434
|
+
const track = this.sentryAudioTrack;
|
|
3435
|
+
if (track === null) {
|
|
3436
|
+
if (!this.sentryAudioGapLogged) {
|
|
3437
|
+
this.sentryAudioGapLogged = true;
|
|
3438
|
+
this.logger?.info("sentry audio: no track to fill — this dial gets no audio", {
|
|
3439
|
+
tags: { deviceId: this.deviceId },
|
|
3440
|
+
meta: {
|
|
3441
|
+
streamName: this.streamName,
|
|
3442
|
+
reason: "no retained g711 audio shape"
|
|
3443
|
+
}
|
|
3444
|
+
});
|
|
3445
|
+
}
|
|
3446
|
+
return;
|
|
3447
|
+
}
|
|
3448
|
+
if (this.sentryAudioSilence === null) this.sentryAudioSilence = new SentryAudioSilence({
|
|
3449
|
+
track,
|
|
3450
|
+
emit: (rtp) => this.sendSilenceToSentrySessions(rtp),
|
|
3451
|
+
...this.logger ? { logger: this.logger } : {},
|
|
3452
|
+
...this.deviceId === void 0 ? {} : { deviceId: this.deviceId }
|
|
3453
|
+
});
|
|
3454
|
+
this.sentryAudioSilence.want();
|
|
3455
|
+
}
|
|
3456
|
+
/**
|
|
3457
|
+
* Hand one generated packet to every sentry session that can take it.
|
|
3458
|
+
*
|
|
3459
|
+
* Gated on PLAY, on the sentry intent and on the session's own mute — and
|
|
3460
|
+
* deliberately NOT on `pendingKeyframe`, which is the one gate the real audio
|
|
3461
|
+
* path applies and this one must not.
|
|
3462
|
+
*
|
|
3463
|
+
* That gate exists to keep live audio behind a video join REPLAY, so a
|
|
3464
|
+
* session's two tracks are not handed media from two different moments.
|
|
3465
|
+
* Generated silence belongs to no replay and to no GOP. Worse, waiting for
|
|
3466
|
+
* the video would reproduce the defect: the placeholder picture arrives at
|
|
3467
|
+
* 1 fps at best and not at all when the sentry refuses (unknown geometry, no
|
|
3468
|
+
* cached snapshot), so the track a consumer's ffmpeg is waiting on would stay
|
|
3469
|
+
* empty for exactly as long as the picture did — which is the stall the
|
|
3470
|
+
* announced-but-unfilled `m=audio` was feared for in the first place.
|
|
3471
|
+
*
|
|
3472
|
+
* Returns how many took it, so a run that delivers to nobody is visible.
|
|
3473
|
+
*/
|
|
3474
|
+
sendSilenceToSentrySessions(rtp) {
|
|
3475
|
+
const packet = {
|
|
3476
|
+
data: rtp,
|
|
3477
|
+
marker: (rtp[1] & 128) !== 0
|
|
3478
|
+
};
|
|
3479
|
+
let served = 0;
|
|
3480
|
+
for (const session of this.sessions.values()) {
|
|
3481
|
+
if (!session.isPrestreamIntent()) continue;
|
|
3482
|
+
if (session.isMuted()) continue;
|
|
3483
|
+
if (!session.isPlaying()) continue;
|
|
3484
|
+
if (session.sendAudioRtp(packet) === "written") served += 1;
|
|
3485
|
+
}
|
|
3486
|
+
return served;
|
|
3487
|
+
}
|
|
3488
|
+
/**
|
|
3098
3489
|
* Apply the broker-side audio mute. Returns how many audio-bearing pull
|
|
3099
3490
|
* sessions were closed so they re-`DESCRIBE` against the new SDP.
|
|
3100
3491
|
*
|
|
@@ -3106,6 +3497,7 @@ var RtspRestreamer = class {
|
|
|
3106
3497
|
setAudioMuted(muted) {
|
|
3107
3498
|
if (this.deviceAudioMuted === muted) return 0;
|
|
3108
3499
|
this.deviceAudioMuted = muted;
|
|
3500
|
+
this.reconcileSentryAudio();
|
|
3109
3501
|
const audioBearing = [...this.sessions.values()].filter((s) => !s.isMuted());
|
|
3110
3502
|
for (const session of audioBearing) this.killSession(session.getSessionId());
|
|
3111
3503
|
if (audioBearing.length > 0) this.logger?.info("audio mute changed — closing audio-bearing RTSP pulls so they re-DESCRIBE", {
|
|
@@ -3157,6 +3549,7 @@ var RtspRestreamer = class {
|
|
|
3157
3549
|
total: this.sessions.size
|
|
3158
3550
|
} });
|
|
3159
3551
|
this._onSessionCountChanged?.();
|
|
3552
|
+
this.reconcileSentryAudio();
|
|
3160
3553
|
if (this._rtpPassthroughMode) setTimeout(() => this.servePendingSessionsRtp(), 0);
|
|
3161
3554
|
else if (this.lastKeyframeNals.length > 0 && this.packetizer) setTimeout(() => this.servePendingSessions(), 0);
|
|
3162
3555
|
}
|
|
@@ -3332,7 +3725,7 @@ var RtspRestreamer = class {
|
|
|
3332
3725
|
}
|
|
3333
3726
|
}
|
|
3334
3727
|
const withhold = admitJoinBurst({
|
|
3335
|
-
|
|
3728
|
+
liveEdgeOnly: session.isLiveEdgeOnly(),
|
|
3336
3729
|
detectionIntent,
|
|
3337
3730
|
fromRing,
|
|
3338
3731
|
spanMs,
|
|
@@ -3595,6 +3988,7 @@ var RtspRestreamer = class {
|
|
|
3595
3988
|
*/
|
|
3596
3989
|
pushAudioRtpPassthrough(rtpData) {
|
|
3597
3990
|
if (this.deviceAudioMuted) return;
|
|
3991
|
+
this.sentryAudioSilence?.noteRealAudio(Date.now());
|
|
3598
3992
|
if (this.sessions.size === 0) return;
|
|
3599
3993
|
const rtpPacket = {
|
|
3600
3994
|
data: rtpData,
|
|
@@ -3615,6 +4009,7 @@ var RtspRestreamer = class {
|
|
|
3615
4009
|
total: this.sessions.size
|
|
3616
4010
|
} });
|
|
3617
4011
|
this._onSessionCountChanged?.();
|
|
4012
|
+
this.reconcileSentryAudio();
|
|
3618
4013
|
}
|
|
3619
4014
|
/**
|
|
3620
4015
|
* Force-close + remove a session by id. Used by the admin `killClient`
|
|
@@ -3643,6 +4038,7 @@ var RtspRestreamer = class {
|
|
|
3643
4038
|
}
|
|
3644
4039
|
destroy() {
|
|
3645
4040
|
this._onSessionCountChanged = null;
|
|
4041
|
+
this.sentryAudioSilence?.release("restreamer destroyed");
|
|
3646
4042
|
this.abortAllReplays();
|
|
3647
4043
|
this.deframer?.flush();
|
|
3648
4044
|
for (const session of this.sessions.values()) session.close();
|
|
@@ -3812,9 +4208,11 @@ var RtspSession = class {
|
|
|
3812
4208
|
* {@link NO_DECLARATION} when it declared nothing — which is every consumer
|
|
3813
4209
|
* except the per-session detection decode and the recorder's writer.
|
|
3814
4210
|
*
|
|
3815
|
-
* Kept separate from {@link muted} deliberately:
|
|
3816
|
-
*
|
|
3817
|
-
*
|
|
4211
|
+
* Kept separate from {@link muted} deliberately: the two say different things
|
|
4212
|
+
* and, since D525, they no longer coincide — a `/sentry` dial declares an
|
|
4213
|
+
* intent and is NOT muted, while the join rule that used to read `isMuted()`
|
|
4214
|
+
* now reads {@link isLiveEdgeOnly} and keeps every older population's
|
|
4215
|
+
* behaviour byte-for-byte.
|
|
3818
4216
|
*
|
|
3819
4217
|
* One field and not two, because the purpose and the amount are one
|
|
3820
4218
|
* statement (D382): a session that names a purpose without naming an amount
|
|
@@ -3876,6 +4274,20 @@ var RtspSession = class {
|
|
|
3876
4274
|
return this.muted;
|
|
3877
4275
|
}
|
|
3878
4276
|
/**
|
|
4277
|
+
* Whether this session is a RE-CLOCKING leg that may only join at the live
|
|
4278
|
+
* edge — the second question `isMuted()` used to answer.
|
|
4279
|
+
*
|
|
4280
|
+
* Derived from the one authority (`isLiveEdgeOnlyDial`) rather than stored,
|
|
4281
|
+
* so the rule cannot drift between the parser and the session. Unchanged for
|
|
4282
|
+
* every population that existed before the sentry gained audio.
|
|
4283
|
+
*/
|
|
4284
|
+
isLiveEdgeOnly() {
|
|
4285
|
+
return isLiveEdgeOnlyDial({
|
|
4286
|
+
muted: this.muted,
|
|
4287
|
+
declaration: this.declaration
|
|
4288
|
+
});
|
|
4289
|
+
}
|
|
4290
|
+
/**
|
|
3879
4291
|
* The `User-Agent` this client put on its first request, or `null`.
|
|
3880
4292
|
*
|
|
3881
4293
|
* The ONLY identity that crosses from a consumer process into this session.
|
|
@@ -4325,7 +4737,9 @@ var RtspListenServer = class RtspListenServer {
|
|
|
4325
4737
|
async dispatch(socket, requestBuffer) {
|
|
4326
4738
|
const uriMatch = (requestBuffer.split("\r\n")[0] ?? "").match(/\s(rtsp:\/\/[^\s]+)\s/);
|
|
4327
4739
|
const pathMatch = (uriMatch ? uriMatch[1] : "").match(/rtsp:\/\/[^/]+(\/.*?)(?:\s|$)/);
|
|
4328
|
-
const
|
|
4740
|
+
const parsed = parseRestreamPath(pathMatch ? pathMatch[1].replace(/^\//, "").replace(/\/trackID=\d+$/, "") : "");
|
|
4741
|
+
const { lookupPath, muted: isMuted, declaration } = parsed;
|
|
4742
|
+
const sdpVariant = sdpVariantFor(parsed);
|
|
4329
4743
|
const restreamer = this.restreamers.get(lookupPath);
|
|
4330
4744
|
if (!restreamer) {
|
|
4331
4745
|
socket.write(`RTSP/1.0 404 Not Found\r\nCSeq: 1\r\n\r\n`);
|
|
@@ -4352,8 +4766,8 @@ var RtspListenServer = class RtspListenServer {
|
|
|
4352
4766
|
};
|
|
4353
4767
|
socket.once("close", releaseOnce);
|
|
4354
4768
|
socket.once("error", releaseOnce);
|
|
4355
|
-
await this.waitForSdp(restreamer,
|
|
4356
|
-
const session = new RtspSession(socket,
|
|
4769
|
+
await this.waitForSdp(restreamer, sdpVariant, 3e3);
|
|
4770
|
+
const session = new RtspSession(socket, restreamer.getSdpFor(sdpVariant) ?? restreamer.getSdp() ?? "", () => {
|
|
4357
4771
|
restreamer.removeSession(session.getSessionId());
|
|
4358
4772
|
releaseOnce();
|
|
4359
4773
|
}, isMuted, declaration);
|
|
@@ -4368,10 +4782,10 @@ var RtspListenServer = class RtspListenServer {
|
|
|
4368
4782
|
* a cold broker that's typically ~200-1000 ms. Resolves `true` if the
|
|
4369
4783
|
* SDP becomes non-empty within the budget, `false` on timeout.
|
|
4370
4784
|
*/
|
|
4371
|
-
async waitForSdp(restreamer,
|
|
4785
|
+
async waitForSdp(restreamer, variant, timeoutMs) {
|
|
4372
4786
|
const start = Date.now();
|
|
4373
4787
|
const have = () => {
|
|
4374
|
-
const s =
|
|
4788
|
+
const s = restreamer.getSdpFor(variant) ?? restreamer.getSdp();
|
|
4375
4789
|
return !!s && s.length > 0;
|
|
4376
4790
|
};
|
|
4377
4791
|
if (have()) return true;
|
|
@@ -15018,6 +15432,12 @@ var StreamBroker = class StreamBroker {
|
|
|
15018
15432
|
supportedNative
|
|
15019
15433
|
}
|
|
15020
15434
|
});
|
|
15435
|
+
const sentryTrack = g711TrackFor(codecUpper);
|
|
15436
|
+
this.rtspRestreamer.retainSentryAudioTrack(sentryTrack);
|
|
15437
|
+
if (sentryTrack === null) this.logger?.info("sentry audio: codec cannot be filled — a sentry gets no audio", {
|
|
15438
|
+
tags: { deviceId: this.numericDeviceId },
|
|
15439
|
+
meta: { codec: codecUpper }
|
|
15440
|
+
});
|
|
15021
15441
|
let supported = supportedNative;
|
|
15022
15442
|
let effectiveSampleRate = audioTrack.clockRate;
|
|
15023
15443
|
let effectiveChannels = audioTrack.channels;
|
|
@@ -15866,6 +16286,7 @@ var TranscodePipelineManager = class {
|
|
|
15866
16286
|
resolveStreamHwAccel;
|
|
15867
16287
|
spawnFn;
|
|
15868
16288
|
acquireChannel;
|
|
16289
|
+
sentryOffered;
|
|
15869
16290
|
constructor(opts) {
|
|
15870
16291
|
this.logger = opts.logger;
|
|
15871
16292
|
this.hwaccel = opts.hwaccel ?? null;
|
|
@@ -15877,6 +16298,7 @@ var TranscodePipelineManager = class {
|
|
|
15877
16298
|
this.resolveStreamHwAccel = opts.resolveStreamHwAccel ?? null;
|
|
15878
16299
|
this.spawnFn = opts.spawnFn ?? spawn;
|
|
15879
16300
|
this.acquireChannel = opts.acquireChannel ?? null;
|
|
16301
|
+
this.sentryOffered = opts.sentryOffered ?? null;
|
|
15880
16302
|
}
|
|
15881
16303
|
async acquire(input) {
|
|
15882
16304
|
const wantedAudio = input.audio ?? "aac";
|
|
@@ -15902,8 +16324,9 @@ var TranscodePipelineManager = class {
|
|
|
15902
16324
|
const passVideoCodec = sourceCodec ?? "H264";
|
|
15903
16325
|
const url = this.resolveSourceUrl(input.deviceId, sourceStream.camStreamId, wantedAudio === "none");
|
|
15904
16326
|
const key = pipelineKeyFor(input.deviceId, sourceStream.camStreamId, "copy", wantedAudio, outResolution, outputArgs, false);
|
|
15905
|
-
return this.
|
|
16327
|
+
return await this.shareOrCreatePassthroughAsync(key, async () => ({
|
|
15906
16328
|
url,
|
|
16329
|
+
sentryUrl: await this.resolveSentryUrl(input.deviceId, url),
|
|
15907
16330
|
videoCodec: passVideoCodec,
|
|
15908
16331
|
audioCodec: "passthrough",
|
|
15909
16332
|
resolution: outResolution,
|
|
@@ -15973,6 +16396,7 @@ var TranscodePipelineManager = class {
|
|
|
15973
16396
|
const url = await egress.start();
|
|
15974
16397
|
const source = {
|
|
15975
16398
|
url,
|
|
16399
|
+
sentryUrl: null,
|
|
15976
16400
|
videoCodec: opts.targetCodec,
|
|
15977
16401
|
audioCodec: opts.audio,
|
|
15978
16402
|
resolution: opts.outResolution,
|
|
@@ -16148,13 +16572,44 @@ var TranscodePipelineManager = class {
|
|
|
16148
16572
|
}
|
|
16149
16573
|
return entry;
|
|
16150
16574
|
}
|
|
16151
|
-
|
|
16575
|
+
/**
|
|
16576
|
+
* The SENTRY counterpart of a passthrough url, or `null`.
|
|
16577
|
+
*
|
|
16578
|
+
* `sentryEligible` is asked through the injected lookup — the broker manager
|
|
16579
|
+
* owns those facts — and an unwired lookup is a NO. A refusal is silent on
|
|
16580
|
+
* purpose: "this mains camera has no sentry" is the answer for most of the
|
|
16581
|
+
* fleet and logging it per acquire would be noise. A FAILED ask is not: it is
|
|
16582
|
+
* a question that could not be answered, and it is reported as such before
|
|
16583
|
+
* being resolved to `null`.
|
|
16584
|
+
*/
|
|
16585
|
+
async resolveSentryUrl(deviceId, url) {
|
|
16586
|
+
if (this.sentryOffered === null) return null;
|
|
16587
|
+
try {
|
|
16588
|
+
if (!await this.sentryOffered(deviceId)) return null;
|
|
16589
|
+
} catch (error) {
|
|
16590
|
+
this.logger.warn("getStreamWithCodec: could not decide whether a sentry dial is offered", {
|
|
16591
|
+
tags: { deviceId },
|
|
16592
|
+
meta: { error: errMsg(error) }
|
|
16593
|
+
});
|
|
16594
|
+
return null;
|
|
16595
|
+
}
|
|
16596
|
+
return withSentryIntent(url);
|
|
16597
|
+
}
|
|
16598
|
+
async shareOrCreatePassthroughAsync(key, factory) {
|
|
16599
|
+
const existing = this.entries.get(key);
|
|
16600
|
+
if (existing) {
|
|
16601
|
+
this.shareEntry(existing);
|
|
16602
|
+
return existing.source;
|
|
16603
|
+
}
|
|
16604
|
+
return this.shareOrCreatePassthrough(key, await factory());
|
|
16605
|
+
}
|
|
16606
|
+
shareOrCreatePassthrough(key, made) {
|
|
16152
16607
|
const existing = this.entries.get(key);
|
|
16153
16608
|
if (existing) {
|
|
16154
16609
|
this.shareEntry(existing);
|
|
16155
16610
|
return existing.source;
|
|
16156
16611
|
}
|
|
16157
|
-
const source =
|
|
16612
|
+
const source = made;
|
|
16158
16613
|
const entry = {
|
|
16159
16614
|
key,
|
|
16160
16615
|
source,
|
|
@@ -16821,6 +17276,10 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
16821
17276
|
resolveProfileSource: (deviceId, profile) => this.assignments.get(deviceId)?.map[profile] ?? null,
|
|
16822
17277
|
ffmpegConfig: () => this.ffmpegConfig,
|
|
16823
17278
|
resolveStreamHwAccel: (deviceId, camStreamId) => this.getStreamHwAccelOverride(deviceId, camStreamId),
|
|
17279
|
+
sentryOffered: async (deviceId) => sentryEligible({
|
|
17280
|
+
batteryOperated: await this.isBatteryOperated(deviceId),
|
|
17281
|
+
declaresSignals: (await this.declaredSignalCodes(deviceId)).length > 0
|
|
17282
|
+
}).eligible,
|
|
16824
17283
|
acquireChannel: (brokerId) => {
|
|
16825
17284
|
const restreamer = new RtspRestreamer(brokerId);
|
|
16826
17285
|
const token = this.rtspServer.registerRestreamer(brokerId, restreamer);
|
|
@@ -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-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CFcO9OoY.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.259",
|
|
4
4
|
"description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|