@camstack/addon-pipeline 1.2.253 → 1.2.255
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-C3QhNmlf.js → default-detection-model-CCFY8YLF.js} +1 -1
- package/dist/{default-detection-model-CaOyxW54.mjs → default-detection-model-DRTO2E6G.mjs} +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-UDv0mi6-.mjs → dist-BS-l_k7M.mjs} +243 -26
- package/dist/{dist-G5Uib9Bn.js → dist-CqqhxcYH.js} +243 -26
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-5P6235Qa.mjs → node-DjvGLcbe.mjs} +1 -1
- package/dist/{node-ByNljnzV.js → node-bdHZWpo1.js} +1 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +4 -4
- package/dist/{process-memory-DBzJCO-_.mjs → process-memory-CrDzHWbQ.mjs} +1 -1
- package/dist/{process-memory-3HWRtZgr.js → process-memory-hQeELS5T.js} +1 -1
- package/dist/recorder/index.js +3 -3
- package/dist/recorder/index.mjs +3 -3
- package/dist/{restream-intent-MeJhGCK9.mjs → restream-intent-D2VT97-U.mjs} +3 -3
- package/dist/{restream-intent-C4adO0P8.js → restream-intent-VVJiUAkh.js} +3 -3
- package/dist/{segment-demux-js-CUFnkl3n.mjs → segment-demux-js-BnYubI9B.mjs} +1 -1
- package/dist/{segment-demux-js-CtQrIvn7.js → segment-demux-js-CaDmuf08.js} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Bg9efO4H.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-qftSJVIp.mjs} +3 -3
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-k114uFcJ.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CPFjd94C.mjs} +1 -1
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BHtcf-Mo.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-D8WW_JG-.mjs} +1 -1
- 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-DYYAMbPi.mjs → hostInit-BbuA-Tci.mjs} +3 -3
- package/dist/stream-broker/index.js +162 -158
- package/dist/stream-broker/index.mjs +162 -158
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -3,12 +3,12 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_node = require("../node-
|
|
8
|
-
const require_restream_intent = require("../restream-intent-
|
|
6
|
+
const require_dist = require("../dist-CqqhxcYH.js");
|
|
7
|
+
const require_node = require("../node-bdHZWpo1.js");
|
|
8
|
+
const require_restream_intent = require("../restream-intent-VVJiUAkh.js");
|
|
9
9
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
10
10
|
const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
|
|
11
|
-
const require_segment_demux_js = require("../segment-demux-js-
|
|
11
|
+
const require_segment_demux_js = require("../segment-demux-js-CaDmuf08.js");
|
|
12
12
|
let node_os = require("node:os");
|
|
13
13
|
node_os = require_chunk.__toESM(node_os);
|
|
14
14
|
let node_crypto = require("node:crypto");
|
|
@@ -654,8 +654,8 @@ function healthEventIsOperatorFacing(camStreamId) {
|
|
|
654
654
|
return !PRIVATE_STREAM_PREFIXES.some((prefix) => camStreamId.startsWith(prefix));
|
|
655
655
|
}
|
|
656
656
|
//#endregion
|
|
657
|
-
//#region src/stream-broker/stream-broker/
|
|
658
|
-
function
|
|
657
|
+
//#region src/stream-broker/stream-broker/sentry-eligibility.ts
|
|
658
|
+
function sentryEligible(input) {
|
|
659
659
|
if (input.batteryOperated === null || input.declaresSignals === null) return {
|
|
660
660
|
eligible: false,
|
|
661
661
|
reason: "unknown"
|
|
@@ -671,7 +671,7 @@ function prestreamEligible(input) {
|
|
|
671
671
|
return { eligible: true };
|
|
672
672
|
}
|
|
673
673
|
//#endregion
|
|
674
|
-
//#region src/stream-broker/stream-broker/
|
|
674
|
+
//#region src/stream-broker/stream-broker/sentry-signal-selection.ts
|
|
675
675
|
function nextEnabledSignals(input) {
|
|
676
676
|
const base = input.stored ?? input.declared;
|
|
677
677
|
const set = new Set(base);
|
|
@@ -689,7 +689,7 @@ function nextEnabledSignals(input) {
|
|
|
689
689
|
* A stream a consumer merely watches is not a reason to wake a battery camera —
|
|
690
690
|
* a Frigate-style puller holds a session open for ever, and treating that as
|
|
691
691
|
* demand keeps the camera awake for ever, which is the whole point of the
|
|
692
|
-
* battery (D173). So a
|
|
692
|
+
* battery (D173). So a sentry consumer is FED without being INTENT, and the
|
|
693
693
|
* only thing that dials is a signal the camera reported about itself.
|
|
694
694
|
*
|
|
695
695
|
* This module is the timing half of that, and it is pure so the arithmetic can
|
|
@@ -3130,11 +3130,11 @@ var RtspRestreamer = class {
|
|
|
3130
3130
|
return this.sessions.size;
|
|
3131
3131
|
}
|
|
3132
3132
|
/**
|
|
3133
|
-
* Sessions that count as LIVE INTENT — everything except a
|
|
3133
|
+
* Sessions that count as LIVE INTENT — everything except a sentry dial.
|
|
3134
3134
|
*
|
|
3135
3135
|
* The split exists so a Frigate-style puller can hold a session open for ever
|
|
3136
3136
|
* against a battery camera without keeping it awake (D173). For a broker with
|
|
3137
|
-
* no
|
|
3137
|
+
* no sentry session this equals {@link getSessionCount} exactly, which is
|
|
3138
3138
|
* why nothing else changed behaviour.
|
|
3139
3139
|
*/
|
|
3140
3140
|
getLiveIntentSessionCount() {
|
|
@@ -3142,7 +3142,7 @@ var RtspRestreamer = class {
|
|
|
3142
3142
|
for (const session of this.sessions.values()) if (!session.isPrestreamIntent()) n += 1;
|
|
3143
3143
|
return n;
|
|
3144
3144
|
}
|
|
3145
|
-
/** Sessions asking only for the picture. Drives the
|
|
3145
|
+
/** Sessions asking only for the picture. Drives the sentry pump, nothing
|
|
3146
3146
|
* else — it must never reach a dial decision. */
|
|
3147
3147
|
getPrestreamSessionCount() {
|
|
3148
3148
|
let n = 0;
|
|
@@ -3903,12 +3903,12 @@ var RtspSession = class {
|
|
|
3903
3903
|
/**
|
|
3904
3904
|
* Whether this dial asked for the STREAM without asking for the CAMERA.
|
|
3905
3905
|
*
|
|
3906
|
-
* The one question the demand split turns on: a
|
|
3906
|
+
* The one question the demand split turns on: a sentry session is FED but
|
|
3907
3907
|
* is not INTENT, so it never dials a sleeping camera. See
|
|
3908
|
-
* `
|
|
3908
|
+
* `SENTRY_PATH_SUFFIX`.
|
|
3909
3909
|
*/
|
|
3910
3910
|
isPrestreamIntent() {
|
|
3911
|
-
return this.declaration.intent === "
|
|
3911
|
+
return this.declaration.intent === "sentry";
|
|
3912
3912
|
}
|
|
3913
3913
|
/**
|
|
3914
3914
|
* Ms of retained history this session ASKED for on its dial. `0` = none.
|
|
@@ -4207,7 +4207,7 @@ var RtspListenServer = class RtspListenServer {
|
|
|
4207
4207
|
brokerEnabled = /* @__PURE__ */ new Map();
|
|
4208
4208
|
/** brokerId → warm-up closure registered alongside the restreamer */
|
|
4209
4209
|
brokerWarmUps = /* @__PURE__ */ new Map();
|
|
4210
|
-
/** brokerId →
|
|
4210
|
+
/** brokerId → sentry picture-window opener */
|
|
4211
4211
|
brokerPrestreamOpeners = /* @__PURE__ */ new Map();
|
|
4212
4212
|
/** Generate a random token for a stream path */
|
|
4213
4213
|
static generateToken() {
|
|
@@ -4219,7 +4219,7 @@ var RtspListenServer = class RtspListenServer {
|
|
|
4219
4219
|
* If not, generate a new one.
|
|
4220
4220
|
* Returns the token used.
|
|
4221
4221
|
*/
|
|
4222
|
-
registerRestreamer(brokerId, restreamer, warmUp, existingToken,
|
|
4222
|
+
registerRestreamer(brokerId, restreamer, warmUp, existingToken, sentryOpener) {
|
|
4223
4223
|
const oldToken = this.brokerTokens.get(brokerId);
|
|
4224
4224
|
if (oldToken) {
|
|
4225
4225
|
this.restreamers.delete(oldToken);
|
|
@@ -4230,7 +4230,7 @@ var RtspListenServer = class RtspListenServer {
|
|
|
4230
4230
|
this.brokerTokens.set(brokerId, token);
|
|
4231
4231
|
this.tokenBrokers.set(token, brokerId);
|
|
4232
4232
|
if (warmUp) this.brokerWarmUps.set(brokerId, warmUp);
|
|
4233
|
-
if (
|
|
4233
|
+
if (sentryOpener) this.brokerPrestreamOpeners.set(brokerId, sentryOpener);
|
|
4234
4234
|
return token;
|
|
4235
4235
|
}
|
|
4236
4236
|
unregisterRestreamer(brokerId) {
|
|
@@ -4342,9 +4342,9 @@ var RtspListenServer = class RtspListenServer {
|
|
|
4342
4342
|
socket.destroy();
|
|
4343
4343
|
return;
|
|
4344
4344
|
}
|
|
4345
|
-
const
|
|
4346
|
-
const warmUp = brokerId && !
|
|
4347
|
-
const opener = brokerId &&
|
|
4345
|
+
const sentryDial = declaration.intent === "sentry";
|
|
4346
|
+
const warmUp = brokerId && !sentryDial ? this.brokerWarmUps.get(brokerId) : void 0;
|
|
4347
|
+
const opener = brokerId && sentryDial ? this.brokerPrestreamOpeners.get(brokerId) : void 0;
|
|
4348
4348
|
const releaseWarmUp = warmUp ? warmUp() : opener?.() ?? null;
|
|
4349
4349
|
let released = false;
|
|
4350
4350
|
const releaseOnce = () => {
|
|
@@ -7648,16 +7648,16 @@ function runGif(input, args) {
|
|
|
7648
7648
|
});
|
|
7649
7649
|
}
|
|
7650
7650
|
//#endregion
|
|
7651
|
-
//#region src/stream-broker/stream-broker/
|
|
7651
|
+
//#region src/stream-broker/stream-broker/sentry-encode.ts
|
|
7652
7652
|
/**
|
|
7653
|
-
* ONE short-lived ffmpeg turns the
|
|
7653
|
+
* ONE short-lived ffmpeg turns the sentry's picture into ONE Annex-B IDR.
|
|
7654
7654
|
*
|
|
7655
7655
|
* ── Why one encode and not a continuous child ─────────────────────────────
|
|
7656
7656
|
* The obvious shape is `-loop 1 -framerate 1` and let ffmpeg emit the picture
|
|
7657
7657
|
* for as long as the camera is waking. That is a resident child per sleeping
|
|
7658
7658
|
* camera, for the whole wake, to redraw a picture that changes at most hourly.
|
|
7659
7659
|
* So the child encodes exactly ONE frame (`-frames:v 1`) and exits, the caller
|
|
7660
|
-
* caches the bytes, and {@link
|
|
7660
|
+
* caches the bytes, and {@link SentryPump} re-emits them. A new encode
|
|
7661
7661
|
* happens only when the picture changes.
|
|
7662
7662
|
*
|
|
7663
7663
|
* ── Why every frame carries its own parameter sets ────────────────────────
|
|
@@ -7682,10 +7682,10 @@ function runGif(input, args) {
|
|
|
7682
7682
|
*/
|
|
7683
7683
|
/**
|
|
7684
7684
|
* Bound on the encode. A single 4 MP still on `ultrafast` is milliseconds; a
|
|
7685
|
-
* child that has not answered in this long is hung, and the
|
|
7685
|
+
* child that has not answered in this long is hung, and the sentry would
|
|
7686
7686
|
* otherwise hold a promise open for the rest of the process's life.
|
|
7687
7687
|
*/
|
|
7688
|
-
var
|
|
7688
|
+
var SENTRY_ENCODE_TIMEOUT_MS = 1e4;
|
|
7689
7689
|
/** Grace before escalating SIGTERM → SIGKILL on a child that will not go. */
|
|
7690
7690
|
var KILL_GRACE_MS = 500;
|
|
7691
7691
|
/** x264/x265 params shared by both codecs: one-frame GOP, headers on it. */
|
|
@@ -7740,7 +7740,7 @@ function buildPrestreamEncodeArgs(plan) {
|
|
|
7740
7740
|
}
|
|
7741
7741
|
/**
|
|
7742
7742
|
* Run the encode. Resolves with the Annex-B bytes, rejects with a named reason
|
|
7743
|
-
* — the caller turns a rejection into a refusal log and no
|
|
7743
|
+
* — the caller turns a rejection into a refusal log and no sentry, which is
|
|
7744
7744
|
* the same outcome as a black screen and never worse than one.
|
|
7745
7745
|
*/
|
|
7746
7746
|
function encodePrestreamIdr(options) {
|
|
@@ -7752,7 +7752,7 @@ function encodePrestreamIdr(options) {
|
|
|
7752
7752
|
try {
|
|
7753
7753
|
child = spawnFn(options.ffmpegPath, args);
|
|
7754
7754
|
} catch (err) {
|
|
7755
|
-
reject(/* @__PURE__ */ new Error(`
|
|
7755
|
+
reject(/* @__PURE__ */ new Error(`sentry encode could not spawn ffmpeg: ${require_dist.errMsg(err)}`));
|
|
7756
7756
|
return;
|
|
7757
7757
|
}
|
|
7758
7758
|
const chunks = [];
|
|
@@ -7767,8 +7767,8 @@ function encodePrestreamIdr(options) {
|
|
|
7767
7767
|
};
|
|
7768
7768
|
const timer = setTimeout(() => {
|
|
7769
7769
|
killChild(child);
|
|
7770
|
-
finish({ error: /* @__PURE__ */ new Error(`
|
|
7771
|
-
},
|
|
7770
|
+
finish({ error: /* @__PURE__ */ new Error(`sentry encode timed out after ${String(SENTRY_ENCODE_TIMEOUT_MS)} ms`) });
|
|
7771
|
+
}, SENTRY_ENCODE_TIMEOUT_MS);
|
|
7772
7772
|
timer.unref?.();
|
|
7773
7773
|
child.stdout?.on("data", (chunk) => chunks.push(chunk));
|
|
7774
7774
|
child.stderr?.on("data", (chunk) => {
|
|
@@ -7776,19 +7776,19 @@ function encodePrestreamIdr(options) {
|
|
|
7776
7776
|
});
|
|
7777
7777
|
child.stdin?.on("error", () => {});
|
|
7778
7778
|
child.on("error", (err) => {
|
|
7779
|
-
finish({ error: /* @__PURE__ */ new Error(`
|
|
7779
|
+
finish({ error: /* @__PURE__ */ new Error(`sentry encode failed: ${err.message}`) });
|
|
7780
7780
|
});
|
|
7781
7781
|
child.on("exit", (code, signal) => {
|
|
7782
7782
|
if (code !== 0) {
|
|
7783
|
-
finish({ error: /* @__PURE__ */ new Error(`
|
|
7783
|
+
finish({ error: /* @__PURE__ */ new Error(`sentry encode exited ${String(code ?? signal)}: ${stderrTail.trim() || "no stderr"}`) });
|
|
7784
7784
|
return;
|
|
7785
7785
|
}
|
|
7786
7786
|
const bytes = Buffer.concat(chunks);
|
|
7787
7787
|
if (bytes.length === 0) {
|
|
7788
|
-
finish({ error: /* @__PURE__ */ new Error("
|
|
7788
|
+
finish({ error: /* @__PURE__ */ new Error("sentry encode produced no bytes") });
|
|
7789
7789
|
return;
|
|
7790
7790
|
}
|
|
7791
|
-
options.logger?.debug("
|
|
7791
|
+
options.logger?.debug("sentry: picture encoded", {
|
|
7792
7792
|
...tags,
|
|
7793
7793
|
meta: {
|
|
7794
7794
|
codec: options.plan.codec,
|
|
@@ -7810,9 +7810,9 @@ function killChild(child) {
|
|
|
7810
7810
|
}, KILL_GRACE_MS).unref?.();
|
|
7811
7811
|
}
|
|
7812
7812
|
//#endregion
|
|
7813
|
-
//#region src/stream-broker/stream-broker/
|
|
7813
|
+
//#region src/stream-broker/stream-broker/sentry-picture.ts
|
|
7814
7814
|
/**
|
|
7815
|
-
* The PICTURE the
|
|
7815
|
+
* The PICTURE the sentry shows: the camera's own last frame under a "Waking"
|
|
7816
7816
|
* treatment, or — when there is none — a generated card carrying its name.
|
|
7817
7817
|
*
|
|
7818
7818
|
* ── Why a fallback exists at all ──────────────────────────────────────────
|
|
@@ -7843,16 +7843,18 @@ function killChild(child) {
|
|
|
7843
7843
|
*/
|
|
7844
7844
|
async function composePrestreamPicture(input) {
|
|
7845
7845
|
const tags = input.deviceId === void 0 ? {} : { tags: { deviceId: input.deviceId } };
|
|
7846
|
+
const reason = "sleeping";
|
|
7846
7847
|
if (input.cachedJpeg !== null && input.cachedJpeg.length > 0) try {
|
|
7847
7848
|
return {
|
|
7848
7849
|
jpeg: await (0, _camstack_system_addon_utils.renderCourtesyOverlayJpeg)(input.cachedJpeg, {
|
|
7849
7850
|
deviceName: input.deviceName,
|
|
7850
|
-
reason
|
|
7851
|
+
reason
|
|
7851
7852
|
}),
|
|
7852
|
-
source: "cached-snapshot"
|
|
7853
|
+
source: "cached-snapshot",
|
|
7854
|
+
reason
|
|
7853
7855
|
};
|
|
7854
7856
|
} catch (err) {
|
|
7855
|
-
input.logger?.warn("
|
|
7857
|
+
input.logger?.warn("sentry: cached frame could not be treated — using the card", {
|
|
7856
7858
|
...tags,
|
|
7857
7859
|
meta: { error: require_dist.errMsg(err) }
|
|
7858
7860
|
});
|
|
@@ -7860,11 +7862,12 @@ async function composePrestreamPicture(input) {
|
|
|
7860
7862
|
return {
|
|
7861
7863
|
jpeg: await (0, _camstack_system_addon_utils.renderCourtesyJpeg)({
|
|
7862
7864
|
deviceName: input.deviceName,
|
|
7863
|
-
reason
|
|
7865
|
+
reason,
|
|
7864
7866
|
width: input.width,
|
|
7865
7867
|
height: input.height
|
|
7866
7868
|
}),
|
|
7867
|
-
source: "generated-card"
|
|
7869
|
+
source: "generated-card",
|
|
7870
|
+
reason
|
|
7868
7871
|
};
|
|
7869
7872
|
}
|
|
7870
7873
|
function planPrestream(input) {
|
|
@@ -7879,7 +7882,7 @@ function planPrestream(input) {
|
|
|
7879
7882
|
} };
|
|
7880
7883
|
}
|
|
7881
7884
|
/**
|
|
7882
|
-
* `null` for a codec nobody has read. NOT a default of h264: a
|
|
7885
|
+
* `null` for a codec nobody has read. NOT a default of h264: a sentry in the
|
|
7883
7886
|
* wrong codec is a stream the consumer cannot decode at all, which is worse
|
|
7884
7887
|
* than the black screen it replaces.
|
|
7885
7888
|
*/
|
|
@@ -11981,11 +11984,11 @@ var OPUS_SILENCE_FRAME_SAMPLES = 960;
|
|
|
11981
11984
|
var OPUS_SILENCE_PRIME_FRAMES = 6;
|
|
11982
11985
|
/**
|
|
11983
11986
|
* Produce ONE canonical 20 ms Opus silence packet by encoding PCM zeros through
|
|
11984
|
-
* the audio-codec cap, for the WebRTC session's
|
|
11987
|
+
* the audio-codec cap, for the WebRTC session's sentry A/V-sync padding
|
|
11985
11988
|
* (Opus has no fixed "silence byte" like PCMU's 0xFF — silence must be encoded).
|
|
11986
11989
|
* A short-lived encode session is created, primed with a few zero frames, and
|
|
11987
11990
|
* flushed; the LAST steady-state frame is returned. Returns `null` when the cap
|
|
11988
|
-
* is unavailable or yields nothing so the caller can skip
|
|
11991
|
+
* is unavailable or yields nothing so the caller can skip sentry padding.
|
|
11989
11992
|
*/
|
|
11990
11993
|
async function encodeOpusSilenceFrame(api, cfg) {
|
|
11991
11994
|
let sessionId = null;
|
|
@@ -12068,9 +12071,9 @@ function getPlaceholderFrame(kind, codec) {
|
|
|
12068
12071
|
return buf;
|
|
12069
12072
|
}
|
|
12070
12073
|
//#endregion
|
|
12071
|
-
//#region src/stream-broker/stream-broker/
|
|
12074
|
+
//#region src/stream-broker/stream-broker/sentry-pump.ts
|
|
12072
12075
|
/**
|
|
12073
|
-
* The
|
|
12076
|
+
* The sentry pump: ONE encode, then a 1 fps re-emit until real media lands.
|
|
12074
12077
|
*
|
|
12075
12078
|
* ── What it is for ────────────────────────────────────────────────────────
|
|
12076
12079
|
* A lazy source is BUILT on demand, and on this hub that build costs 2.2 to
|
|
@@ -12092,12 +12095,12 @@ function getPlaceholderFrame(kind, codec) {
|
|
|
12092
12095
|
* 3. **It never dials.** The pump has no source, no socket and no demand. It
|
|
12093
12096
|
* is started alongside the publisher request, never instead of it.
|
|
12094
12097
|
*
|
|
12095
|
-
* Emission is the caller's: {@link
|
|
12098
|
+
* Emission is the caller's: {@link SentryPumpOptions.emit} is where the
|
|
12096
12099
|
* `isPlaceholder: true` packet is built, because that flag belongs to the
|
|
12097
12100
|
* broker's packet contract and the containment that reads it lives there.
|
|
12098
12101
|
*/
|
|
12099
|
-
var
|
|
12100
|
-
var
|
|
12102
|
+
var DEFAULT_SENTRY_INTERVAL_MS = 1e3;
|
|
12103
|
+
var SentryPump = class {
|
|
12101
12104
|
options;
|
|
12102
12105
|
intervalMs;
|
|
12103
12106
|
tags;
|
|
@@ -12109,7 +12112,7 @@ var PrestreamPump = class {
|
|
|
12109
12112
|
emitted = 0;
|
|
12110
12113
|
constructor(options) {
|
|
12111
12114
|
this.options = options;
|
|
12112
|
-
this.intervalMs = options.intervalMs ??
|
|
12115
|
+
this.intervalMs = options.intervalMs ?? DEFAULT_SENTRY_INTERVAL_MS;
|
|
12113
12116
|
this.tags = options.deviceId === void 0 ? {} : { tags: { deviceId: options.deviceId } };
|
|
12114
12117
|
}
|
|
12115
12118
|
/** Whether the window is currently being filled (or about to be). */
|
|
@@ -12128,16 +12131,16 @@ var PrestreamPump = class {
|
|
|
12128
12131
|
this.emitted = 0;
|
|
12129
12132
|
this.options.prepare({ sourceUrl }).then((frame) => {
|
|
12130
12133
|
if (!this.active) {
|
|
12131
|
-
this.options.logger?.debug("
|
|
12134
|
+
this.options.logger?.debug("sentry: preparation landed after the window closed", { ...this.tags });
|
|
12132
12135
|
return;
|
|
12133
12136
|
}
|
|
12134
12137
|
if (frame === null) {
|
|
12135
|
-
this.options.logger?.info("
|
|
12138
|
+
this.options.logger?.info("sentry: refused — the window goes unfilled", { ...this.tags });
|
|
12136
12139
|
this.active = false;
|
|
12137
12140
|
return;
|
|
12138
12141
|
}
|
|
12139
12142
|
this.frame = frame;
|
|
12140
|
-
this.options.logger?.info("
|
|
12143
|
+
this.options.logger?.info("sentry: filling the wake window", {
|
|
12141
12144
|
...this.tags,
|
|
12142
12145
|
meta: {
|
|
12143
12146
|
codec: frame.codec,
|
|
@@ -12149,7 +12152,7 @@ var PrestreamPump = class {
|
|
|
12149
12152
|
this.tick();
|
|
12150
12153
|
this.timer = setInterval(() => this.tick(), this.intervalMs);
|
|
12151
12154
|
}).catch((err) => {
|
|
12152
|
-
this.options.logger?.warn("
|
|
12155
|
+
this.options.logger?.warn("sentry: preparation failed — the window goes unfilled", {
|
|
12153
12156
|
...this.tags,
|
|
12154
12157
|
meta: { error: require_dist.errMsg(err) }
|
|
12155
12158
|
});
|
|
@@ -12158,7 +12161,7 @@ var PrestreamPump = class {
|
|
|
12158
12161
|
}
|
|
12159
12162
|
/**
|
|
12160
12163
|
* Close the window. Called on the first NON-placeholder packet — the one edge
|
|
12161
|
-
* the
|
|
12164
|
+
* the sentry is defined by — and on the broker's own teardown paths so a
|
|
12162
12165
|
* stopped broker leaves no timer behind.
|
|
12163
12166
|
*/
|
|
12164
12167
|
stop(reason) {
|
|
@@ -12168,7 +12171,7 @@ var PrestreamPump = class {
|
|
|
12168
12171
|
clearInterval(this.timer);
|
|
12169
12172
|
this.timer = void 0;
|
|
12170
12173
|
}
|
|
12171
|
-
this.options.logger?.info("
|
|
12174
|
+
this.options.logger?.info("sentry: window closed", {
|
|
12172
12175
|
...this.tags,
|
|
12173
12176
|
meta: {
|
|
12174
12177
|
reason,
|
|
@@ -12479,12 +12482,12 @@ var StreamBroker = class StreamBroker {
|
|
|
12479
12482
|
reconnectTimer;
|
|
12480
12483
|
placeholderTimer;
|
|
12481
12484
|
/**
|
|
12482
|
-
* The
|
|
12483
|
-
* `undefined` on a broker whose manager supplied none — the pre-
|
|
12485
|
+
* The sentry pump, armed by the manager via `setPrestreamProvider`.
|
|
12486
|
+
* `undefined` on a broker whose manager supplied none — the pre-sentry
|
|
12484
12487
|
* behaviour, unchanged.
|
|
12485
12488
|
*/
|
|
12486
|
-
|
|
12487
|
-
/** The manager's hook for "real media started" — see `
|
|
12489
|
+
sentryPump;
|
|
12490
|
+
/** The manager's hook for "real media started" — see `SentryProvider`. */
|
|
12488
12491
|
onRealMediaStarted;
|
|
12489
12492
|
/**
|
|
12490
12493
|
* Latched once real media has been seen on this session.
|
|
@@ -12494,9 +12497,9 @@ var StreamBroker = class StreamBroker {
|
|
|
12494
12497
|
* window that real bytes have already closed, and the consumer would get a
|
|
12495
12498
|
* still picture painted over live video every second. Reset exactly where
|
|
12496
12499
|
* `firstKeyframeReceived` is — broker stop and suspend — so a battery camera
|
|
12497
|
-
* that goes back to sleep gets its
|
|
12500
|
+
* that goes back to sleep gets its sentry again on the next wake.
|
|
12498
12501
|
*/
|
|
12499
|
-
|
|
12502
|
+
sentryWindowClosed = false;
|
|
12500
12503
|
reconnectDelayMs = INITIAL_RECONNECT_DELAY_MS;
|
|
12501
12504
|
/**
|
|
12502
12505
|
* Dial rate-limiter (2026-07-23 storm fix): the earliest wall-clock at
|
|
@@ -12772,7 +12775,7 @@ var StreamBroker = class StreamBroker {
|
|
|
12772
12775
|
* that socket down. In that window `resolveCurrentSource()` truthfully
|
|
12773
12776
|
* answers with a socket nobody can dial.
|
|
12774
12777
|
*
|
|
12775
|
-
* That cost device 640 its
|
|
12778
|
+
* That cost device 640 its sentry on 2026-09-16: the window re-opened
|
|
12776
12779
|
* (which `suspend()` does deliberately, for a consumer that never left) with
|
|
12777
12780
|
* the real url, `planPrestream` refused it `not-lazy`, and the refusal is
|
|
12778
12781
|
* terminal — the operator's client sat dark for ten seconds and gave up six
|
|
@@ -12781,8 +12784,8 @@ var StreamBroker = class StreamBroker {
|
|
|
12781
12784
|
* NOT cleared when `start()` is later called with a materialized url: that
|
|
12782
12785
|
* happens on every wake, and clearing it there is the bug again. A broker
|
|
12783
12786
|
* genuinely reconfigured onto a permanent mains source keeps a stale value,
|
|
12784
|
-
* which costs nothing — `
|
|
12785
|
-
* the authority on whether a camera gets a
|
|
12787
|
+
* which costs nothing — `sentryEligible` (battery + declared signals) is
|
|
12788
|
+
* the authority on whether a camera gets a sentry at all, and it is asked
|
|
12786
12789
|
* first, in the manager.
|
|
12787
12790
|
*/
|
|
12788
12791
|
lazySourceUrl;
|
|
@@ -13045,7 +13048,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13045
13048
|
this.requestSourceRefresh = notify;
|
|
13046
13049
|
}
|
|
13047
13050
|
/**
|
|
13048
|
-
* Arm the
|
|
13051
|
+
* Arm the SENTRY — the fill for the window between "someone asked for this
|
|
13049
13052
|
* camera" and "the camera is awake".
|
|
13050
13053
|
*
|
|
13051
13054
|
* Manager-owned, because everything the fill needs is the manager's: the
|
|
@@ -13053,13 +13056,13 @@ var StreamBroker = class StreamBroker {
|
|
|
13053
13056
|
* snapshot, its name, and the ffmpeg binary. The broker contributes the one
|
|
13054
13057
|
* thing it owns — WHEN the window is open — and nothing else.
|
|
13055
13058
|
*
|
|
13056
|
-
* A broker with no provider behaves exactly as it did before the
|
|
13059
|
+
* A broker with no provider behaves exactly as it did before the sentry
|
|
13057
13060
|
* existed: the lazy branch asks the publisher to materialize and the consumer
|
|
13058
13061
|
* sees black, which is the outcome every refusal falls back to.
|
|
13059
13062
|
*/
|
|
13060
13063
|
setPrestreamProvider(provider) {
|
|
13061
13064
|
this.onRealMediaStarted = provider.onRealMediaStarted;
|
|
13062
|
-
this.
|
|
13065
|
+
this.sentryPump = new SentryPump({
|
|
13063
13066
|
prepare: provider.prepare,
|
|
13064
13067
|
emit: (frame) => {
|
|
13065
13068
|
const now = Date.now();
|
|
@@ -13179,7 +13182,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13179
13182
|
*
|
|
13180
13183
|
* Here and not only in `start()` because a broker that has never dialled is a
|
|
13181
13184
|
* real and common state: a battery camera nobody has asked for yet, with a
|
|
13182
|
-
*
|
|
13185
|
+
* sentry consumer attached, has a recipe and no socket. Learning it from
|
|
13183
13186
|
* the dial alone would leave exactly that broker unable to describe itself.
|
|
13184
13187
|
*/
|
|
13185
13188
|
noteLazyUrl(url) {
|
|
@@ -13256,8 +13259,8 @@ var StreamBroker = class StreamBroker {
|
|
|
13256
13259
|
clearInterval(this.placeholderTimer);
|
|
13257
13260
|
this.placeholderTimer = void 0;
|
|
13258
13261
|
}
|
|
13259
|
-
this.
|
|
13260
|
-
this.
|
|
13262
|
+
this.sentryPump?.stop("broker stopped");
|
|
13263
|
+
this.sentryWindowClosed = false;
|
|
13261
13264
|
if (this.reconnectTimer) {
|
|
13262
13265
|
clearTimeout(this.reconnectTimer);
|
|
13263
13266
|
this.reconnectTimer = void 0;
|
|
@@ -13427,7 +13430,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13427
13430
|
*
|
|
13428
13431
|
* - the caller has already checked the signal is ENABLED for this camera
|
|
13429
13432
|
* (the broker manager owns that setting), and
|
|
13430
|
-
* - somebody is actually waiting on the
|
|
13433
|
+
* - somebody is actually waiting on the sentry. Waking a camera nobody is
|
|
13431
13434
|
* watching is the one thing a battery camera must never do, and a signal
|
|
13432
13435
|
* arriving with no consumer is exactly that.
|
|
13433
13436
|
*
|
|
@@ -13470,7 +13473,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13470
13473
|
if (delay <= 0) return;
|
|
13471
13474
|
this.wakeHoldTimer = setTimeout(() => {
|
|
13472
13475
|
this.wakeHoldTimer = void 0;
|
|
13473
|
-
this.logger?.info("wake hold lapsed — back to the
|
|
13476
|
+
this.logger?.info("wake hold lapsed — back to the sentry picture", {
|
|
13474
13477
|
tags: { deviceId: this.numericDeviceId },
|
|
13475
13478
|
meta: {
|
|
13476
13479
|
brokerId: this.deviceId,
|
|
@@ -13483,20 +13486,20 @@ var StreamBroker = class StreamBroker {
|
|
|
13483
13486
|
this.wakeHoldTimer.unref?.();
|
|
13484
13487
|
}
|
|
13485
13488
|
/**
|
|
13486
|
-
* Anything that must be FED,
|
|
13489
|
+
* Anything that must be FED, sentry sessions included.
|
|
13487
13490
|
*
|
|
13488
|
-
* The only consumer of this is the
|
|
13491
|
+
* The only consumer of this is the sentry pump: it says "somebody is
|
|
13489
13492
|
* watching, keep the picture going", and says nothing about whether the
|
|
13490
13493
|
* camera may be dialled. {@link hasLiveIntent} answers that, and it is the
|
|
13491
13494
|
* one every dial path asks.
|
|
13492
13495
|
*/
|
|
13493
13496
|
hasAnyConsumer() {
|
|
13494
13497
|
if (this.hasLiveIntent()) return true;
|
|
13495
|
-
if (this.
|
|
13498
|
+
if (this.sentryHolds > 0) return true;
|
|
13496
13499
|
return this.rtspRestreamer.getPrestreamSessionCount() > 0;
|
|
13497
13500
|
}
|
|
13498
13501
|
/**
|
|
13499
|
-
* Somebody is DESCRIBING a
|
|
13502
|
+
* Somebody is DESCRIBING a sentry and has no session yet.
|
|
13500
13503
|
*
|
|
13501
13504
|
* The RTSP dispatch reads the SDP before `addSession` runs, so a window that
|
|
13502
13505
|
* waits for the session is too late: the consumer is handed an empty SDP and
|
|
@@ -13504,28 +13507,28 @@ var StreamBroker = class StreamBroker {
|
|
|
13504
13507
|
* on the attach and closed 42 ms later having emitted zero frames, because
|
|
13505
13508
|
* ffmpeg had already given up.
|
|
13506
13509
|
*
|
|
13507
|
-
* This is the
|
|
13510
|
+
* This is the sentry counterpart of `BrokerWarmUp` and its exact opposite
|
|
13508
13511
|
* in effect: the warm-up subscribes demand, which DIALS; this opens the
|
|
13509
13512
|
* picture, which never does. A battery camera is not touched.
|
|
13510
13513
|
*/
|
|
13511
13514
|
openPrestreamWindow() {
|
|
13512
|
-
this.
|
|
13515
|
+
this.sentryHolds += 1;
|
|
13513
13516
|
this.checkDemand();
|
|
13514
13517
|
let released = false;
|
|
13515
13518
|
return () => {
|
|
13516
13519
|
if (released) return;
|
|
13517
13520
|
released = true;
|
|
13518
|
-
this.
|
|
13521
|
+
this.sentryHolds = Math.max(0, this.sentryHolds - 1);
|
|
13519
13522
|
this.checkDemand();
|
|
13520
13523
|
};
|
|
13521
13524
|
}
|
|
13522
|
-
/** DESCRIBE-window
|
|
13523
|
-
|
|
13525
|
+
/** DESCRIBE-window sentry holds: sessions that do not exist yet. */
|
|
13526
|
+
sentryHolds = 0;
|
|
13524
13527
|
/**
|
|
13525
13528
|
* Consumers that may DIAL. Every term of the former `hasDemand`, with the
|
|
13526
|
-
* RTSP session count filtered to exclude
|
|
13529
|
+
* RTSP session count filtered to exclude sentry dials, plus the wake hold.
|
|
13527
13530
|
*
|
|
13528
|
-
* For a broker with no
|
|
13531
|
+
* For a broker with no sentry session attached this is term-for-term what
|
|
13529
13532
|
* `hasDemand` was, which is why every existing camera, consumer and test
|
|
13530
13533
|
* behaves identically.
|
|
13531
13534
|
*/
|
|
@@ -13570,7 +13573,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13570
13573
|
});
|
|
13571
13574
|
}
|
|
13572
13575
|
/**
|
|
13573
|
-
* Encode ONE canonical 20 ms Opus silence packet for a session's
|
|
13576
|
+
* Encode ONE canonical 20 ms Opus silence packet for a session's sentry
|
|
13574
13577
|
* A/V-sync padding (Opus has no fixed silence byte). Returns `null` when the
|
|
13575
13578
|
* audio-codec cap is unavailable. See {@link encodeOpusSilenceFrame}.
|
|
13576
13579
|
*/
|
|
@@ -13680,16 +13683,16 @@ var StreamBroker = class StreamBroker {
|
|
|
13680
13683
|
if (!this.hasLiveIntent() && !this.manualStop && !this.stopping) this.suspend();
|
|
13681
13684
|
}, SUSPEND_GRACE_MS);
|
|
13682
13685
|
}
|
|
13683
|
-
this.
|
|
13686
|
+
this.sentryWanted = this.hasAnyConsumer() && !this.hasLiveIntent();
|
|
13684
13687
|
this.reconcilePrestreamWindow();
|
|
13685
13688
|
}
|
|
13686
13689
|
/**
|
|
13687
|
-
* Open or close the
|
|
13690
|
+
* Open or close the sentry window to match {@link sentryWanted}.
|
|
13688
13691
|
*
|
|
13689
|
-
* This is the half that was missing. `
|
|
13692
|
+
* This is the half that was missing. `sentryWanted` has been computed
|
|
13690
13693
|
* correctly since the feature shipped, but the only thing that ever STARTED
|
|
13691
13694
|
* the pump was the lazy branch of `start()` — a DIAL, which needs live
|
|
13692
|
-
* intent, which a
|
|
13695
|
+
* intent, which a sentry session does not have by construction. So the
|
|
13693
13696
|
* window opened only when something else had already woken the camera, and
|
|
13694
13697
|
* `suspend()` closed it for good.
|
|
13695
13698
|
*
|
|
@@ -13699,22 +13702,22 @@ var StreamBroker = class StreamBroker {
|
|
|
13699
13702
|
* demand check costs one encode per window and no more.
|
|
13700
13703
|
*/
|
|
13701
13704
|
reconcilePrestreamWindow() {
|
|
13702
|
-
if (!this.
|
|
13703
|
-
if (this.
|
|
13705
|
+
if (!this.sentryPump) return;
|
|
13706
|
+
if (this.sentryWanted && !this.sentryWindowClosed) {
|
|
13704
13707
|
const url = this.lazySourceUrl ?? this.resolveCurrentSource()?.url ?? this.source?.url;
|
|
13705
|
-
if (url !== void 0) this.
|
|
13708
|
+
if (url !== void 0) this.sentryPump.start(url);
|
|
13706
13709
|
return;
|
|
13707
13710
|
}
|
|
13708
|
-
if (!this.
|
|
13711
|
+
if (!this.sentryWanted) this.sentryPump.stop("no sentry consumer");
|
|
13709
13712
|
}
|
|
13710
13713
|
/**
|
|
13711
|
-
* Whether a
|
|
13714
|
+
* Whether a sentry picture should be flowing right now: somebody is
|
|
13712
13715
|
* watching and the camera is not up. Read by the pump.
|
|
13713
13716
|
*/
|
|
13714
|
-
|
|
13717
|
+
sentryWanted = false;
|
|
13715
13718
|
/** Whether the picture should be flowing — the pump's one question. */
|
|
13716
13719
|
wantsPrestream() {
|
|
13717
|
-
return this.
|
|
13720
|
+
return this.sentryWanted;
|
|
13718
13721
|
}
|
|
13719
13722
|
/** Suspend the stream: kill ffmpeg/native client, clear buffers, go idle. Source is preserved for resume. */
|
|
13720
13723
|
suspend() {
|
|
@@ -13739,8 +13742,8 @@ var StreamBroker = class StreamBroker {
|
|
|
13739
13742
|
clearInterval(this.placeholderTimer);
|
|
13740
13743
|
this.placeholderTimer = void 0;
|
|
13741
13744
|
}
|
|
13742
|
-
this.
|
|
13743
|
-
this.
|
|
13745
|
+
this.sentryPump?.stop("broker suspended");
|
|
13746
|
+
this.sentryWindowClosed = false;
|
|
13744
13747
|
this.reconcilePrestreamWindow();
|
|
13745
13748
|
this.preBuffer.clear();
|
|
13746
13749
|
this.decoderSeedBuffer.clear();
|
|
@@ -13825,13 +13828,13 @@ var StreamBroker = class StreamBroker {
|
|
|
13825
13828
|
}
|
|
13826
13829
|
pushEncodedPacket(packet) {
|
|
13827
13830
|
if (this.stopping) return;
|
|
13828
|
-
if (packet.isPlaceholder !== true && !this.
|
|
13829
|
-
this.
|
|
13830
|
-
this.
|
|
13831
|
+
if (packet.isPlaceholder !== true && !this.sentryWindowClosed) {
|
|
13832
|
+
this.sentryWindowClosed = true;
|
|
13833
|
+
this.sentryPump?.stop("first real packet");
|
|
13831
13834
|
try {
|
|
13832
13835
|
this.onRealMediaStarted?.();
|
|
13833
13836
|
} catch (err) {
|
|
13834
|
-
this.logger?.debug("
|
|
13837
|
+
this.logger?.debug("sentry: real-media hook threw", {
|
|
13835
13838
|
tags: { deviceId: this.numericDeviceId ?? 0 },
|
|
13836
13839
|
meta: { error: String(err) }
|
|
13837
13840
|
});
|
|
@@ -15580,7 +15583,7 @@ var StreamBroker = class StreamBroker {
|
|
|
15580
15583
|
});
|
|
15581
15584
|
this._status = "error";
|
|
15582
15585
|
this.notifySourceRefresh();
|
|
15583
|
-
if (!this.
|
|
15586
|
+
if (!this.sentryWindowClosed) this.sentryPump?.start(source.url);
|
|
15584
15587
|
this.scheduleReconnect();
|
|
15585
15588
|
return;
|
|
15586
15589
|
}
|
|
@@ -16312,16 +16315,16 @@ var DeviceOverrideSchema = require_dist.object({
|
|
|
16312
16315
|
sourceParser: require_dist.record(require_dist.string(), SourceParserSettingSchema).optional(),
|
|
16313
16316
|
sourceParserArgs: require_dist.record(require_dist.string(), SourceParserArgsEntrySchema).optional(),
|
|
16314
16317
|
/**
|
|
16315
|
-
* The on-demand
|
|
16318
|
+
* The on-demand SENTRY, per camera.
|
|
16316
16319
|
*
|
|
16317
16320
|
* Absent means the operator has never touched it, which is NOT the same as
|
|
16318
16321
|
* off: `enabledSignals` absent means "every signal this camera recommends",
|
|
16319
|
-
* so a Reolink works the moment a
|
|
16322
|
+
* so a Reolink works the moment a sentry URL is dialled without an
|
|
16320
16323
|
* operator hunting for a checkbox. An empty ARRAY is a deliberate "none" and
|
|
16321
16324
|
* is honoured — the two are different answers and the shape keeps them
|
|
16322
16325
|
* apart.
|
|
16323
16326
|
*/
|
|
16324
|
-
|
|
16327
|
+
sentry: require_dist.object({
|
|
16325
16328
|
/** Signal codes allowed to wake this camera. Absent ⇒ the recommended set. */
|
|
16326
16329
|
enabledSignals: require_dist.array(require_dist.string()).optional(),
|
|
16327
16330
|
/** How long the camera stays up after the LAST signal. Absent ⇒ default. */
|
|
@@ -16515,13 +16518,13 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
16515
16518
|
/** Per-device published cam-stream registry. */
|
|
16516
16519
|
cameraStreams = /* @__PURE__ */ new Map();
|
|
16517
16520
|
/**
|
|
16518
|
-
* The encoded
|
|
16521
|
+
* The encoded sentry frame per broker, with the digest of the picture it
|
|
16519
16522
|
* was made from. One entry is one still image — kilobytes — and it is
|
|
16520
16523
|
* dropped with the broker in `releaseBroker`.
|
|
16521
16524
|
*/
|
|
16522
|
-
/** Brokers with a background
|
|
16523
|
-
|
|
16524
|
-
|
|
16525
|
+
/** Brokers with a background sentry re-encode in flight (single-flight). */
|
|
16526
|
+
sentryRefreshing = /* @__PURE__ */ new Set();
|
|
16527
|
+
sentryFrames = /* @__PURE__ */ new Map();
|
|
16525
16528
|
/** Monotonic insertion counter per device — feeds `CameraStreamInternal.order`. */
|
|
16526
16529
|
streamOrderSeq = /* @__PURE__ */ new Map();
|
|
16527
16530
|
/** Persisted operator intent: which cam stream serves which profile. */
|
|
@@ -17413,7 +17416,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17413
17416
|
* client-side and we get a typed payload.
|
|
17414
17417
|
*/
|
|
17415
17418
|
/**
|
|
17416
|
-
* Build the per-broker
|
|
17419
|
+
* Build the per-broker SENTRY provider — the fill for the window between
|
|
17417
17420
|
* "someone asked for this camera" and "the camera is awake".
|
|
17418
17421
|
*
|
|
17419
17422
|
* On this hub that window is 2.2-12.4 s for a lazy source, and it is not
|
|
@@ -17426,19 +17429,19 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17426
17429
|
* cached snapshot, its name, and the ffmpeg binary path.
|
|
17427
17430
|
*
|
|
17428
17431
|
* `null` is a REFUSAL, named in the log. Refusing is cheap — the consumer
|
|
17429
|
-
* gets the black screen it got before the
|
|
17432
|
+
* gets the black screen it got before the sentry existed — and the
|
|
17430
17433
|
* alternative for an unknown geometry is the decoder freeze the whole
|
|
17431
17434
|
* synthetic-frame path is shaped around.
|
|
17432
17435
|
*/
|
|
17433
17436
|
buildPrestreamProvider(deviceId, camStreamId, brokerId, logger) {
|
|
17434
17437
|
return {
|
|
17435
17438
|
prepare: async ({ sourceUrl }) => {
|
|
17436
|
-
const gate =
|
|
17439
|
+
const gate = sentryEligible({
|
|
17437
17440
|
batteryOperated: await this.isBatteryOperated(deviceId),
|
|
17438
17441
|
declaresSignals: (await this.declaredSignalCodes(deviceId)).length > 0
|
|
17439
17442
|
});
|
|
17440
17443
|
if (!gate.eligible) {
|
|
17441
|
-
logger.info("
|
|
17444
|
+
logger.info("sentry: not offered for this camera", {
|
|
17442
17445
|
tags: { deviceId },
|
|
17443
17446
|
meta: {
|
|
17444
17447
|
brokerId,
|
|
@@ -17456,7 +17459,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17456
17459
|
codec
|
|
17457
17460
|
});
|
|
17458
17461
|
if ("refusal" in planned) {
|
|
17459
|
-
logger.info("
|
|
17462
|
+
logger.info("sentry: no fill for this camera", {
|
|
17460
17463
|
tags: { deviceId },
|
|
17461
17464
|
meta: {
|
|
17462
17465
|
refusal: planned.refusal,
|
|
@@ -17476,7 +17479,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17476
17479
|
* The window shows the camera's last snapshot, and nothing was
|
|
17477
17480
|
* refreshing it: a battery camera refuses a capture while it believes
|
|
17478
17481
|
* itself asleep, and that belief lags the wake by seconds — so the only
|
|
17479
|
-
* attempts ever made were the
|
|
17482
|
+
* attempts ever made were the sentry's own, while composing, which is
|
|
17480
17483
|
* BEFORE any media exists. Measured on device 640 (2026-09-16): four
|
|
17481
17484
|
* wakes in forty minutes, the encoded picture `bytes=23912` every time,
|
|
17482
17485
|
* and between first packet and suspend not one capture was even
|
|
@@ -17499,12 +17502,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17499
17502
|
const api = this.api;
|
|
17500
17503
|
if (!api) return;
|
|
17501
17504
|
api.snapshot.getSnapshot.query({ deviceId }).then(() => {
|
|
17502
|
-
logger.debug("
|
|
17505
|
+
logger.debug("sentry: picture refreshed from the live window", {
|
|
17503
17506
|
tags: { deviceId },
|
|
17504
17507
|
meta: { brokerId }
|
|
17505
17508
|
});
|
|
17506
17509
|
}).catch((err) => {
|
|
17507
|
-
logger.debug("
|
|
17510
|
+
logger.debug("sentry: live-window refresh failed — keeping the older picture", {
|
|
17508
17511
|
tags: { deviceId },
|
|
17509
17512
|
meta: {
|
|
17510
17513
|
brokerId,
|
|
@@ -17525,7 +17528,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17525
17528
|
*/
|
|
17526
17529
|
async preparePrestreamFrame(deviceId, brokerId, plan, logger) {
|
|
17527
17530
|
if (!this.api) {
|
|
17528
|
-
logger.info("
|
|
17531
|
+
logger.info("sentry: no api access — the window goes unfilled", { tags: { deviceId } });
|
|
17529
17532
|
return null;
|
|
17530
17533
|
}
|
|
17531
17534
|
const api = this.api;
|
|
@@ -17534,7 +17537,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17534
17537
|
const image = await api.snapshot.getSnapshot.query({ deviceId });
|
|
17535
17538
|
if (image) cachedJpeg = Buffer.from(image.base64, "base64");
|
|
17536
17539
|
} catch (err) {
|
|
17537
|
-
logger.debug("
|
|
17540
|
+
logger.debug("sentry: cached snapshot read failed — falling back to the card", {
|
|
17538
17541
|
tags: { deviceId },
|
|
17539
17542
|
meta: { error: require_dist.errMsg(err) }
|
|
17540
17543
|
});
|
|
@@ -17544,15 +17547,15 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17544
17547
|
const device = await api.deviceManager.getDevice.query({ deviceId });
|
|
17545
17548
|
if (device?.name) deviceName = device.name;
|
|
17546
17549
|
} catch (err) {
|
|
17547
|
-
logger.debug("
|
|
17550
|
+
logger.debug("sentry: device name read failed — using the numeric id", {
|
|
17548
17551
|
tags: { deviceId },
|
|
17549
17552
|
meta: { error: require_dist.errMsg(err) }
|
|
17550
17553
|
});
|
|
17551
17554
|
}
|
|
17552
17555
|
const key = `${(0, node_crypto.createHash)("sha1").update(cachedJpeg ?? Buffer.alloc(0)).update(deviceName).digest("hex")}:${String(plan.width)}x${String(plan.height)}:${plan.codec}`;
|
|
17553
|
-
const cached = this.
|
|
17556
|
+
const cached = this.sentryFrames.get(brokerId);
|
|
17554
17557
|
if (cached && cached.key === key) {
|
|
17555
|
-
logger.debug("
|
|
17558
|
+
logger.debug("sentry: re-using the encoded frame", {
|
|
17556
17559
|
tags: { deviceId },
|
|
17557
17560
|
meta: { bytes: cached.frame.annexB.length }
|
|
17558
17561
|
});
|
|
@@ -17561,7 +17564,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17561
17564
|
const cachedGeometry = cached ? cached.key.slice(cached.key.indexOf(":")) : null;
|
|
17562
17565
|
const wantedGeometry = key.slice(key.indexOf(":"));
|
|
17563
17566
|
if (cached && cachedGeometry === wantedGeometry) {
|
|
17564
|
-
logger.info("
|
|
17567
|
+
logger.info("sentry: serving the previous picture while the new one encodes", {
|
|
17565
17568
|
tags: { deviceId },
|
|
17566
17569
|
meta: { bytes: cached.frame.annexB.length }
|
|
17567
17570
|
});
|
|
@@ -17586,10 +17589,11 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17586
17589
|
logger,
|
|
17587
17590
|
deviceId
|
|
17588
17591
|
});
|
|
17589
|
-
logger.info("
|
|
17592
|
+
logger.info("sentry: picture composed", {
|
|
17590
17593
|
tags: { deviceId },
|
|
17591
17594
|
meta: {
|
|
17592
17595
|
source: picture.source,
|
|
17596
|
+
reason: picture.reason,
|
|
17593
17597
|
width: plan.width,
|
|
17594
17598
|
height: plan.height,
|
|
17595
17599
|
codec: plan.codec
|
|
@@ -17605,7 +17609,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17605
17609
|
deviceId
|
|
17606
17610
|
});
|
|
17607
17611
|
} catch (err) {
|
|
17608
|
-
logger.warn("
|
|
17612
|
+
logger.warn("sentry: encode failed — the window goes unfilled", {
|
|
17609
17613
|
tags: { deviceId },
|
|
17610
17614
|
meta: { error: require_dist.errMsg(err) }
|
|
17611
17615
|
});
|
|
@@ -17615,7 +17619,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17615
17619
|
annexB,
|
|
17616
17620
|
codec: plan.codec
|
|
17617
17621
|
};
|
|
17618
|
-
this.
|
|
17622
|
+
this.sentryFrames.set(brokerId, {
|
|
17619
17623
|
key,
|
|
17620
17624
|
frame
|
|
17621
17625
|
});
|
|
@@ -17629,10 +17633,10 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17629
17633
|
* operator is actually waiting for.
|
|
17630
17634
|
*/
|
|
17631
17635
|
encodePrestreamInBackground(deviceId, brokerId, plan, key, cachedJpeg, deviceName, logger) {
|
|
17632
|
-
if (this.
|
|
17633
|
-
this.
|
|
17636
|
+
if (this.sentryRefreshing.has(brokerId)) return;
|
|
17637
|
+
this.sentryRefreshing.add(brokerId);
|
|
17634
17638
|
this.composeAndEncodePrestream(deviceId, brokerId, plan, key, cachedJpeg, deviceName, logger).catch(() => {}).finally(() => {
|
|
17635
|
-
this.
|
|
17639
|
+
this.sentryRefreshing.delete(brokerId);
|
|
17636
17640
|
});
|
|
17637
17641
|
}
|
|
17638
17642
|
subscribePlaceholderStateSources(bus) {
|
|
@@ -19639,7 +19643,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19639
19643
|
reason: "unknown-code"
|
|
19640
19644
|
};
|
|
19641
19645
|
}
|
|
19642
|
-
const settings = this.deviceOverrides.get(input.deviceId)?.
|
|
19646
|
+
const settings = this.deviceOverrides.get(input.deviceId)?.sentry;
|
|
19643
19647
|
if (!(settings?.enabledSignals ?? declared).includes(input.code)) return {
|
|
19644
19648
|
accepted: false,
|
|
19645
19649
|
reason: "not-enabled"
|
|
@@ -19821,11 +19825,11 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19821
19825
|
label: hwAccelOptionLabel(setting)
|
|
19822
19826
|
}));
|
|
19823
19827
|
const declaredSignals = await this.declaredSignalList(deviceId);
|
|
19824
|
-
const
|
|
19828
|
+
const sentryGate = sentryEligible({
|
|
19825
19829
|
batteryOperated: await this.isBatteryOperated(deviceId),
|
|
19826
19830
|
declaresSignals: declaredSignals.length > 0
|
|
19827
19831
|
});
|
|
19828
|
-
const
|
|
19832
|
+
const sentryOffered = sentryGate.eligible;
|
|
19829
19833
|
const streamTabs = streams.filter((s) => !s.camStreamId.startsWith("derived:")).map((stream) => {
|
|
19830
19834
|
const camStreamId = stream.camStreamId;
|
|
19831
19835
|
const brokerId = brokerIdFor(deviceId, camStreamId);
|
|
@@ -19925,18 +19929,18 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19925
19929
|
tooltip: "Copy muted URL to clipboard"
|
|
19926
19930
|
}]
|
|
19927
19931
|
},
|
|
19928
|
-
...
|
|
19932
|
+
...sentryOffered ? [{
|
|
19929
19933
|
type: "text",
|
|
19930
19934
|
key: `rtspPrestreamUrl:${camStreamId}`,
|
|
19931
|
-
label: "RTSP URL (
|
|
19935
|
+
label: "RTSP URL (sentry)",
|
|
19932
19936
|
description: "Video-only. Shows this camera’s last picture while it sleeps and cuts to live video when one of its wake signals fires — without a consumer on this URL ever keeping the camera awake.",
|
|
19933
19937
|
readonlyField: true,
|
|
19934
19938
|
span: 2,
|
|
19935
|
-
value: rtspEntry?.url ? `${rtspEntry.url}/
|
|
19939
|
+
value: rtspEntry?.url ? `${rtspEntry.url}/sentry` : "inactive",
|
|
19936
19940
|
actions: [{
|
|
19937
19941
|
action: "copy-value",
|
|
19938
19942
|
icon: "copy",
|
|
19939
|
-
tooltip: "Copy
|
|
19943
|
+
tooltip: "Copy sentry URL to clipboard"
|
|
19940
19944
|
}]
|
|
19941
19945
|
}] : [],
|
|
19942
19946
|
{
|
|
@@ -20124,25 +20128,25 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
20124
20128
|
]
|
|
20125
20129
|
}]
|
|
20126
20130
|
};
|
|
20127
|
-
const
|
|
20128
|
-
const
|
|
20129
|
-
id: "stream-broker-
|
|
20131
|
+
const sentryCfg = override?.sentry;
|
|
20132
|
+
const sentrySections = sentryGate.eligible ? [{
|
|
20133
|
+
id: "stream-broker-sentry",
|
|
20130
20134
|
title: "Wake signals",
|
|
20131
20135
|
tab: "stream-broker",
|
|
20132
20136
|
location: "top-tab",
|
|
20133
20137
|
order: 50,
|
|
20134
|
-
description: "A consumer can dial this camera’s
|
|
20138
|
+
description: "A consumer can dial this camera’s sentry URL (the profile URL with /sentry) and be shown the last picture while the camera sleeps. It is fed without keeping the camera awake: only the signals below wake it, and it goes back to sleep once they stop.",
|
|
20135
20139
|
fields: [...declaredSignals.map((signal) => ({
|
|
20136
20140
|
type: "boolean",
|
|
20137
20141
|
style: "switch",
|
|
20138
|
-
key: `
|
|
20142
|
+
key: `sentrySignal:${signal.code}`,
|
|
20139
20143
|
label: signal.label,
|
|
20140
20144
|
description: signal.recommended ? "Recommended by this camera’s driver." : "Offered by this camera’s driver.",
|
|
20141
20145
|
default: signal.recommended,
|
|
20142
|
-
value:
|
|
20146
|
+
value: sentryCfg?.enabledSignals === void 0 ? true : sentryCfg.enabledSignals.includes(signal.code)
|
|
20143
20147
|
})), {
|
|
20144
20148
|
type: "number",
|
|
20145
|
-
key: "
|
|
20149
|
+
key: "sentryHoldMs",
|
|
20146
20150
|
label: "Stay awake after the last signal",
|
|
20147
20151
|
description: "How long the camera keeps streaming once its signals stop. Extended by each new signal, never stacked — continuous motion keeps it up and it sleeps this long after the last one. One run is capped at five minutes whatever this says, and a camera whose sensor never stops is left alone for ten minutes after that.",
|
|
20148
20152
|
min: 1e3,
|
|
@@ -20150,7 +20154,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
20150
20154
|
step: 1e3,
|
|
20151
20155
|
unit: "ms",
|
|
20152
20156
|
default: DEFAULT_WAKE_HOLD_MS,
|
|
20153
|
-
value:
|
|
20157
|
+
value: sentryCfg?.holdMs ?? 15e3
|
|
20154
20158
|
}]
|
|
20155
20159
|
}] : [];
|
|
20156
20160
|
const diagnosticsSection = {
|
|
@@ -20179,7 +20183,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
20179
20183
|
};
|
|
20180
20184
|
return { sections: [
|
|
20181
20185
|
streamBrokerSection,
|
|
20182
|
-
...
|
|
20186
|
+
...sentrySections,
|
|
20183
20187
|
diagnosticsSection
|
|
20184
20188
|
] };
|
|
20185
20189
|
}
|
|
@@ -20293,27 +20297,27 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
20293
20297
|
nextOverride.sourceParserArgs = spa;
|
|
20294
20298
|
continue;
|
|
20295
20299
|
}
|
|
20296
|
-
if (fieldKey.startsWith("
|
|
20300
|
+
if (fieldKey.startsWith("sentrySignal:")) {
|
|
20297
20301
|
overrideDirty = true;
|
|
20298
20302
|
const next = nextEnabledSignals({
|
|
20299
20303
|
declared: await this.declaredSignalCodes(deviceId),
|
|
20300
|
-
stored: nextOverride.
|
|
20301
|
-
code: fieldKey.slice(
|
|
20304
|
+
stored: nextOverride.sentry?.enabledSignals,
|
|
20305
|
+
code: fieldKey.slice(13),
|
|
20302
20306
|
enabled: value === null || value === void 0 ? true : Boolean(value)
|
|
20303
20307
|
});
|
|
20304
|
-
nextOverride.
|
|
20305
|
-
...nextOverride.
|
|
20308
|
+
nextOverride.sentry = {
|
|
20309
|
+
...nextOverride.sentry,
|
|
20306
20310
|
enabledSignals: [...next]
|
|
20307
20311
|
};
|
|
20308
20312
|
continue;
|
|
20309
20313
|
}
|
|
20310
|
-
if (fieldKey === "
|
|
20314
|
+
if (fieldKey === "sentryHoldMs") {
|
|
20311
20315
|
overrideDirty = true;
|
|
20312
20316
|
const raw = typeof value === "number" ? value : Number(value);
|
|
20313
20317
|
if (!Number.isFinite(raw)) continue;
|
|
20314
20318
|
const clamped = Math.min(3e5, Math.max(1e3, Math.round(raw)));
|
|
20315
|
-
nextOverride.
|
|
20316
|
-
...nextOverride.
|
|
20319
|
+
nextOverride.sentry = {
|
|
20320
|
+
...nextOverride.sentry,
|
|
20317
20321
|
holdMs: clamped
|
|
20318
20322
|
};
|
|
20319
20323
|
continue;
|
|
@@ -21156,7 +21160,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
21156
21160
|
this.streamOfflineReasonByBroker.delete(brokerId);
|
|
21157
21161
|
await broker.stop();
|
|
21158
21162
|
this.brokers.delete(brokerId);
|
|
21159
|
-
this.
|
|
21163
|
+
this.sentryFrames.delete(brokerId);
|
|
21160
21164
|
this.sweepFrameSubscriptions(brokerId);
|
|
21161
21165
|
this.rtspProvider.persistTokens();
|
|
21162
21166
|
this.syncProfileRestreamers(deviceId);
|
|
@@ -24724,7 +24728,7 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
24724
24728
|
* silence packets (0xFF bytes, 160 samples each = 20 ms) so the audio
|
|
24725
24729
|
* sender's first wall-clock instant aligns with the video sender's first
|
|
24726
24730
|
* wall-clock instant, then the real packet continues seamlessly. See
|
|
24727
|
-
* `
|
|
24731
|
+
* `sentryAudioSilenceIfNeeded` + `computeAudioSilencePadPackets`.
|
|
24728
24732
|
*/
|
|
24729
24733
|
firstAudioFramePadded = false;
|
|
24730
24734
|
/** PCMU frame size in samples per packet — 160 = 20 ms at 8 kHz. */
|
|
@@ -25120,7 +25124,7 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
25120
25124
|
return this.audioClock().codec === "opus";
|
|
25121
25125
|
}
|
|
25122
25126
|
/**
|
|
25123
|
-
* Inject the canonical 20 ms Opus silence packet used for
|
|
25127
|
+
* Inject the canonical 20 ms Opus silence packet used for sentry A/V-sync
|
|
25124
25128
|
* padding. Broker-provided (encoded via the audio-codec cap) because Opus has
|
|
25125
25129
|
* no fixed silence byte the way PCMU does (0xFF).
|
|
25126
25130
|
*/
|
|
@@ -25134,7 +25138,7 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
25134
25138
|
const senderCodec = this.audioSender.codec;
|
|
25135
25139
|
const fallbackPt = codec === "Pcmu" || codec === "Pcma" ? 0 : 111;
|
|
25136
25140
|
const pt = senderCodec?.payloadType ?? fallbackPt;
|
|
25137
|
-
this.
|
|
25141
|
+
this.sentryAudioSilenceIfNeeded(rtpTs >>> 0, pt);
|
|
25138
25142
|
try {
|
|
25139
25143
|
const header = new werift.RtpHeader();
|
|
25140
25144
|
header.payloadType = pt;
|
|
@@ -25173,7 +25177,7 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
25173
25177
|
* diag reads ~0). Any failure on a single `sendRtp` is logged and the loop
|
|
25174
25178
|
* continues — partial alignment is better than aborting.
|
|
25175
25179
|
*/
|
|
25176
|
-
|
|
25180
|
+
sentryAudioSilenceIfNeeded(realRtpTs, payloadType) {
|
|
25177
25181
|
if (this.firstAudioFramePadded) return;
|
|
25178
25182
|
this.firstAudioFramePadded = true;
|
|
25179
25183
|
if (!this.audioSender || !_werift) return;
|