@camstack/addon-pipeline 1.2.252 → 1.2.254
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-CaOyxW54.mjs → default-detection-model-BegEf29d.mjs} +1 -1
- package/dist/{default-detection-model-C3QhNmlf.js → default-detection-model-DQj3yiG8.js} +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-UDv0mi6-.mjs → dist-CkGvx3Fy.mjs} +1 -1
- package/dist/{dist-G5Uib9Bn.js → dist-DQsOgAJp.js} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-ByNljnzV.js → node-Bg7LGOQ3.js} +1 -1
- package/dist/{node-5P6235Qa.mjs → node-Dwlty0jj.mjs} +1 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +4 -4
- package/dist/{process-memory-3HWRtZgr.js → process-memory-COrQw_pf.js} +1 -1
- package/dist/{process-memory-DBzJCO-_.mjs → process-memory-CzjYHqAz.mjs} +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-BAoEDsxT.mjs} +1 -1
- package/dist/{segment-demux-js-CtQrIvn7.js → segment-demux-js-BIrnzZiP.js} +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DpxY0D_G.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-rumvXmC-.mjs} +3 -3
- 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-B6AhXDBP.mjs → hostInit-CFFft0gC.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
|
@@ -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 { c as PREBUFFER_MAX_MS, d as resolvePrebuffer, i as parseRestreamPath, l as capPreRollRequestMs, n as NO_DECLARATION, u as prebufferRetentionMs } from "../restream-intent-
|
|
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-CkGvx3Fy.mjs";
|
|
3
|
+
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-Dwlty0jj.mjs";
|
|
4
|
+
import { c as PREBUFFER_MAX_MS, d as resolvePrebuffer, i as parseRestreamPath, l as capPreRollRequestMs, n as NO_DECLARATION, u as prebufferRetentionMs } from "../restream-intent-D2VT97-U.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-BAoEDsxT.mjs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import { networkInterfaces, tmpdir } from "node:os";
|
|
@@ -650,8 +650,8 @@ function healthEventIsOperatorFacing(camStreamId) {
|
|
|
650
650
|
return !PRIVATE_STREAM_PREFIXES.some((prefix) => camStreamId.startsWith(prefix));
|
|
651
651
|
}
|
|
652
652
|
//#endregion
|
|
653
|
-
//#region src/stream-broker/stream-broker/
|
|
654
|
-
function
|
|
653
|
+
//#region src/stream-broker/stream-broker/sentry-eligibility.ts
|
|
654
|
+
function sentryEligible(input) {
|
|
655
655
|
if (input.batteryOperated === null || input.declaresSignals === null) return {
|
|
656
656
|
eligible: false,
|
|
657
657
|
reason: "unknown"
|
|
@@ -667,7 +667,7 @@ function prestreamEligible(input) {
|
|
|
667
667
|
return { eligible: true };
|
|
668
668
|
}
|
|
669
669
|
//#endregion
|
|
670
|
-
//#region src/stream-broker/stream-broker/
|
|
670
|
+
//#region src/stream-broker/stream-broker/sentry-signal-selection.ts
|
|
671
671
|
function nextEnabledSignals(input) {
|
|
672
672
|
const base = input.stored ?? input.declared;
|
|
673
673
|
const set = new Set(base);
|
|
@@ -685,7 +685,7 @@ function nextEnabledSignals(input) {
|
|
|
685
685
|
* A stream a consumer merely watches is not a reason to wake a battery camera —
|
|
686
686
|
* a Frigate-style puller holds a session open for ever, and treating that as
|
|
687
687
|
* demand keeps the camera awake for ever, which is the whole point of the
|
|
688
|
-
* battery (D173). So a
|
|
688
|
+
* battery (D173). So a sentry consumer is FED without being INTENT, and the
|
|
689
689
|
* only thing that dials is a signal the camera reported about itself.
|
|
690
690
|
*
|
|
691
691
|
* This module is the timing half of that, and it is pure so the arithmetic can
|
|
@@ -3126,11 +3126,11 @@ var RtspRestreamer = class {
|
|
|
3126
3126
|
return this.sessions.size;
|
|
3127
3127
|
}
|
|
3128
3128
|
/**
|
|
3129
|
-
* Sessions that count as LIVE INTENT — everything except a
|
|
3129
|
+
* Sessions that count as LIVE INTENT — everything except a sentry dial.
|
|
3130
3130
|
*
|
|
3131
3131
|
* The split exists so a Frigate-style puller can hold a session open for ever
|
|
3132
3132
|
* against a battery camera without keeping it awake (D173). For a broker with
|
|
3133
|
-
* no
|
|
3133
|
+
* no sentry session this equals {@link getSessionCount} exactly, which is
|
|
3134
3134
|
* why nothing else changed behaviour.
|
|
3135
3135
|
*/
|
|
3136
3136
|
getLiveIntentSessionCount() {
|
|
@@ -3138,7 +3138,7 @@ var RtspRestreamer = class {
|
|
|
3138
3138
|
for (const session of this.sessions.values()) if (!session.isPrestreamIntent()) n += 1;
|
|
3139
3139
|
return n;
|
|
3140
3140
|
}
|
|
3141
|
-
/** Sessions asking only for the picture. Drives the
|
|
3141
|
+
/** Sessions asking only for the picture. Drives the sentry pump, nothing
|
|
3142
3142
|
* else — it must never reach a dial decision. */
|
|
3143
3143
|
getPrestreamSessionCount() {
|
|
3144
3144
|
let n = 0;
|
|
@@ -3899,12 +3899,12 @@ var RtspSession = class {
|
|
|
3899
3899
|
/**
|
|
3900
3900
|
* Whether this dial asked for the STREAM without asking for the CAMERA.
|
|
3901
3901
|
*
|
|
3902
|
-
* The one question the demand split turns on: a
|
|
3902
|
+
* The one question the demand split turns on: a sentry session is FED but
|
|
3903
3903
|
* is not INTENT, so it never dials a sleeping camera. See
|
|
3904
|
-
* `
|
|
3904
|
+
* `SENTRY_PATH_SUFFIX`.
|
|
3905
3905
|
*/
|
|
3906
3906
|
isPrestreamIntent() {
|
|
3907
|
-
return this.declaration.intent === "
|
|
3907
|
+
return this.declaration.intent === "sentry";
|
|
3908
3908
|
}
|
|
3909
3909
|
/**
|
|
3910
3910
|
* Ms of retained history this session ASKED for on its dial. `0` = none.
|
|
@@ -4203,7 +4203,7 @@ var RtspListenServer = class RtspListenServer {
|
|
|
4203
4203
|
brokerEnabled = /* @__PURE__ */ new Map();
|
|
4204
4204
|
/** brokerId → warm-up closure registered alongside the restreamer */
|
|
4205
4205
|
brokerWarmUps = /* @__PURE__ */ new Map();
|
|
4206
|
-
/** brokerId →
|
|
4206
|
+
/** brokerId → sentry picture-window opener */
|
|
4207
4207
|
brokerPrestreamOpeners = /* @__PURE__ */ new Map();
|
|
4208
4208
|
/** Generate a random token for a stream path */
|
|
4209
4209
|
static generateToken() {
|
|
@@ -4215,7 +4215,7 @@ var RtspListenServer = class RtspListenServer {
|
|
|
4215
4215
|
* If not, generate a new one.
|
|
4216
4216
|
* Returns the token used.
|
|
4217
4217
|
*/
|
|
4218
|
-
registerRestreamer(brokerId, restreamer, warmUp, existingToken,
|
|
4218
|
+
registerRestreamer(brokerId, restreamer, warmUp, existingToken, sentryOpener) {
|
|
4219
4219
|
const oldToken = this.brokerTokens.get(brokerId);
|
|
4220
4220
|
if (oldToken) {
|
|
4221
4221
|
this.restreamers.delete(oldToken);
|
|
@@ -4226,7 +4226,7 @@ var RtspListenServer = class RtspListenServer {
|
|
|
4226
4226
|
this.brokerTokens.set(brokerId, token);
|
|
4227
4227
|
this.tokenBrokers.set(token, brokerId);
|
|
4228
4228
|
if (warmUp) this.brokerWarmUps.set(brokerId, warmUp);
|
|
4229
|
-
if (
|
|
4229
|
+
if (sentryOpener) this.brokerPrestreamOpeners.set(brokerId, sentryOpener);
|
|
4230
4230
|
return token;
|
|
4231
4231
|
}
|
|
4232
4232
|
unregisterRestreamer(brokerId) {
|
|
@@ -4338,9 +4338,9 @@ var RtspListenServer = class RtspListenServer {
|
|
|
4338
4338
|
socket.destroy();
|
|
4339
4339
|
return;
|
|
4340
4340
|
}
|
|
4341
|
-
const
|
|
4342
|
-
const warmUp = brokerId && !
|
|
4343
|
-
const opener = brokerId &&
|
|
4341
|
+
const sentryDial = declaration.intent === "sentry";
|
|
4342
|
+
const warmUp = brokerId && !sentryDial ? this.brokerWarmUps.get(brokerId) : void 0;
|
|
4343
|
+
const opener = brokerId && sentryDial ? this.brokerPrestreamOpeners.get(brokerId) : void 0;
|
|
4344
4344
|
const releaseWarmUp = warmUp ? warmUp() : opener?.() ?? null;
|
|
4345
4345
|
let released = false;
|
|
4346
4346
|
const releaseOnce = () => {
|
|
@@ -7644,16 +7644,16 @@ function runGif(input, args) {
|
|
|
7644
7644
|
});
|
|
7645
7645
|
}
|
|
7646
7646
|
//#endregion
|
|
7647
|
-
//#region src/stream-broker/stream-broker/
|
|
7647
|
+
//#region src/stream-broker/stream-broker/sentry-encode.ts
|
|
7648
7648
|
/**
|
|
7649
|
-
* ONE short-lived ffmpeg turns the
|
|
7649
|
+
* ONE short-lived ffmpeg turns the sentry's picture into ONE Annex-B IDR.
|
|
7650
7650
|
*
|
|
7651
7651
|
* ── Why one encode and not a continuous child ─────────────────────────────
|
|
7652
7652
|
* The obvious shape is `-loop 1 -framerate 1` and let ffmpeg emit the picture
|
|
7653
7653
|
* for as long as the camera is waking. That is a resident child per sleeping
|
|
7654
7654
|
* camera, for the whole wake, to redraw a picture that changes at most hourly.
|
|
7655
7655
|
* So the child encodes exactly ONE frame (`-frames:v 1`) and exits, the caller
|
|
7656
|
-
* caches the bytes, and {@link
|
|
7656
|
+
* caches the bytes, and {@link SentryPump} re-emits them. A new encode
|
|
7657
7657
|
* happens only when the picture changes.
|
|
7658
7658
|
*
|
|
7659
7659
|
* ── Why every frame carries its own parameter sets ────────────────────────
|
|
@@ -7678,10 +7678,10 @@ function runGif(input, args) {
|
|
|
7678
7678
|
*/
|
|
7679
7679
|
/**
|
|
7680
7680
|
* Bound on the encode. A single 4 MP still on `ultrafast` is milliseconds; a
|
|
7681
|
-
* child that has not answered in this long is hung, and the
|
|
7681
|
+
* child that has not answered in this long is hung, and the sentry would
|
|
7682
7682
|
* otherwise hold a promise open for the rest of the process's life.
|
|
7683
7683
|
*/
|
|
7684
|
-
var
|
|
7684
|
+
var SENTRY_ENCODE_TIMEOUT_MS = 1e4;
|
|
7685
7685
|
/** Grace before escalating SIGTERM → SIGKILL on a child that will not go. */
|
|
7686
7686
|
var KILL_GRACE_MS = 500;
|
|
7687
7687
|
/** x264/x265 params shared by both codecs: one-frame GOP, headers on it. */
|
|
@@ -7736,7 +7736,7 @@ function buildPrestreamEncodeArgs(plan) {
|
|
|
7736
7736
|
}
|
|
7737
7737
|
/**
|
|
7738
7738
|
* Run the encode. Resolves with the Annex-B bytes, rejects with a named reason
|
|
7739
|
-
* — the caller turns a rejection into a refusal log and no
|
|
7739
|
+
* — the caller turns a rejection into a refusal log and no sentry, which is
|
|
7740
7740
|
* the same outcome as a black screen and never worse than one.
|
|
7741
7741
|
*/
|
|
7742
7742
|
function encodePrestreamIdr(options) {
|
|
@@ -7748,7 +7748,7 @@ function encodePrestreamIdr(options) {
|
|
|
7748
7748
|
try {
|
|
7749
7749
|
child = spawnFn(options.ffmpegPath, args);
|
|
7750
7750
|
} catch (err) {
|
|
7751
|
-
reject(/* @__PURE__ */ new Error(`
|
|
7751
|
+
reject(/* @__PURE__ */ new Error(`sentry encode could not spawn ffmpeg: ${errMsg(err)}`));
|
|
7752
7752
|
return;
|
|
7753
7753
|
}
|
|
7754
7754
|
const chunks = [];
|
|
@@ -7763,8 +7763,8 @@ function encodePrestreamIdr(options) {
|
|
|
7763
7763
|
};
|
|
7764
7764
|
const timer = setTimeout(() => {
|
|
7765
7765
|
killChild(child);
|
|
7766
|
-
finish({ error: /* @__PURE__ */ new Error(`
|
|
7767
|
-
},
|
|
7766
|
+
finish({ error: /* @__PURE__ */ new Error(`sentry encode timed out after ${String(SENTRY_ENCODE_TIMEOUT_MS)} ms`) });
|
|
7767
|
+
}, SENTRY_ENCODE_TIMEOUT_MS);
|
|
7768
7768
|
timer.unref?.();
|
|
7769
7769
|
child.stdout?.on("data", (chunk) => chunks.push(chunk));
|
|
7770
7770
|
child.stderr?.on("data", (chunk) => {
|
|
@@ -7772,19 +7772,19 @@ function encodePrestreamIdr(options) {
|
|
|
7772
7772
|
});
|
|
7773
7773
|
child.stdin?.on("error", () => {});
|
|
7774
7774
|
child.on("error", (err) => {
|
|
7775
|
-
finish({ error: /* @__PURE__ */ new Error(`
|
|
7775
|
+
finish({ error: /* @__PURE__ */ new Error(`sentry encode failed: ${err.message}`) });
|
|
7776
7776
|
});
|
|
7777
7777
|
child.on("exit", (code, signal) => {
|
|
7778
7778
|
if (code !== 0) {
|
|
7779
|
-
finish({ error: /* @__PURE__ */ new Error(`
|
|
7779
|
+
finish({ error: /* @__PURE__ */ new Error(`sentry encode exited ${String(code ?? signal)}: ${stderrTail.trim() || "no stderr"}`) });
|
|
7780
7780
|
return;
|
|
7781
7781
|
}
|
|
7782
7782
|
const bytes = Buffer.concat(chunks);
|
|
7783
7783
|
if (bytes.length === 0) {
|
|
7784
|
-
finish({ error: /* @__PURE__ */ new Error("
|
|
7784
|
+
finish({ error: /* @__PURE__ */ new Error("sentry encode produced no bytes") });
|
|
7785
7785
|
return;
|
|
7786
7786
|
}
|
|
7787
|
-
options.logger?.debug("
|
|
7787
|
+
options.logger?.debug("sentry: picture encoded", {
|
|
7788
7788
|
...tags,
|
|
7789
7789
|
meta: {
|
|
7790
7790
|
codec: options.plan.codec,
|
|
@@ -7806,9 +7806,9 @@ function killChild(child) {
|
|
|
7806
7806
|
}, KILL_GRACE_MS).unref?.();
|
|
7807
7807
|
}
|
|
7808
7808
|
//#endregion
|
|
7809
|
-
//#region src/stream-broker/stream-broker/
|
|
7809
|
+
//#region src/stream-broker/stream-broker/sentry-picture.ts
|
|
7810
7810
|
/**
|
|
7811
|
-
* The PICTURE the
|
|
7811
|
+
* The PICTURE the sentry shows: the camera's own last frame under a "Waking"
|
|
7812
7812
|
* treatment, or — when there is none — a generated card carrying its name.
|
|
7813
7813
|
*
|
|
7814
7814
|
* ── Why a fallback exists at all ──────────────────────────────────────────
|
|
@@ -7839,16 +7839,18 @@ function killChild(child) {
|
|
|
7839
7839
|
*/
|
|
7840
7840
|
async function composePrestreamPicture(input) {
|
|
7841
7841
|
const tags = input.deviceId === void 0 ? {} : { tags: { deviceId: input.deviceId } };
|
|
7842
|
+
const reason = "sleeping";
|
|
7842
7843
|
if (input.cachedJpeg !== null && input.cachedJpeg.length > 0) try {
|
|
7843
7844
|
return {
|
|
7844
7845
|
jpeg: await renderCourtesyOverlayJpeg(input.cachedJpeg, {
|
|
7845
7846
|
deviceName: input.deviceName,
|
|
7846
|
-
reason
|
|
7847
|
+
reason
|
|
7847
7848
|
}),
|
|
7848
|
-
source: "cached-snapshot"
|
|
7849
|
+
source: "cached-snapshot",
|
|
7850
|
+
reason
|
|
7849
7851
|
};
|
|
7850
7852
|
} catch (err) {
|
|
7851
|
-
input.logger?.warn("
|
|
7853
|
+
input.logger?.warn("sentry: cached frame could not be treated — using the card", {
|
|
7852
7854
|
...tags,
|
|
7853
7855
|
meta: { error: errMsg(err) }
|
|
7854
7856
|
});
|
|
@@ -7856,11 +7858,12 @@ async function composePrestreamPicture(input) {
|
|
|
7856
7858
|
return {
|
|
7857
7859
|
jpeg: await renderCourtesyJpeg({
|
|
7858
7860
|
deviceName: input.deviceName,
|
|
7859
|
-
reason
|
|
7861
|
+
reason,
|
|
7860
7862
|
width: input.width,
|
|
7861
7863
|
height: input.height
|
|
7862
7864
|
}),
|
|
7863
|
-
source: "generated-card"
|
|
7865
|
+
source: "generated-card",
|
|
7866
|
+
reason
|
|
7864
7867
|
};
|
|
7865
7868
|
}
|
|
7866
7869
|
function planPrestream(input) {
|
|
@@ -7875,7 +7878,7 @@ function planPrestream(input) {
|
|
|
7875
7878
|
} };
|
|
7876
7879
|
}
|
|
7877
7880
|
/**
|
|
7878
|
-
* `null` for a codec nobody has read. NOT a default of h264: a
|
|
7881
|
+
* `null` for a codec nobody has read. NOT a default of h264: a sentry in the
|
|
7879
7882
|
* wrong codec is a stream the consumer cannot decode at all, which is worse
|
|
7880
7883
|
* than the black screen it replaces.
|
|
7881
7884
|
*/
|
|
@@ -11977,11 +11980,11 @@ var OPUS_SILENCE_FRAME_SAMPLES = 960;
|
|
|
11977
11980
|
var OPUS_SILENCE_PRIME_FRAMES = 6;
|
|
11978
11981
|
/**
|
|
11979
11982
|
* Produce ONE canonical 20 ms Opus silence packet by encoding PCM zeros through
|
|
11980
|
-
* the audio-codec cap, for the WebRTC session's
|
|
11983
|
+
* the audio-codec cap, for the WebRTC session's sentry A/V-sync padding
|
|
11981
11984
|
* (Opus has no fixed "silence byte" like PCMU's 0xFF — silence must be encoded).
|
|
11982
11985
|
* A short-lived encode session is created, primed with a few zero frames, and
|
|
11983
11986
|
* flushed; the LAST steady-state frame is returned. Returns `null` when the cap
|
|
11984
|
-
* is unavailable or yields nothing so the caller can skip
|
|
11987
|
+
* is unavailable or yields nothing so the caller can skip sentry padding.
|
|
11985
11988
|
*/
|
|
11986
11989
|
async function encodeOpusSilenceFrame(api, cfg) {
|
|
11987
11990
|
let sessionId = null;
|
|
@@ -12064,9 +12067,9 @@ function getPlaceholderFrame(kind, codec) {
|
|
|
12064
12067
|
return buf;
|
|
12065
12068
|
}
|
|
12066
12069
|
//#endregion
|
|
12067
|
-
//#region src/stream-broker/stream-broker/
|
|
12070
|
+
//#region src/stream-broker/stream-broker/sentry-pump.ts
|
|
12068
12071
|
/**
|
|
12069
|
-
* The
|
|
12072
|
+
* The sentry pump: ONE encode, then a 1 fps re-emit until real media lands.
|
|
12070
12073
|
*
|
|
12071
12074
|
* ── What it is for ────────────────────────────────────────────────────────
|
|
12072
12075
|
* A lazy source is BUILT on demand, and on this hub that build costs 2.2 to
|
|
@@ -12088,12 +12091,12 @@ function getPlaceholderFrame(kind, codec) {
|
|
|
12088
12091
|
* 3. **It never dials.** The pump has no source, no socket and no demand. It
|
|
12089
12092
|
* is started alongside the publisher request, never instead of it.
|
|
12090
12093
|
*
|
|
12091
|
-
* Emission is the caller's: {@link
|
|
12094
|
+
* Emission is the caller's: {@link SentryPumpOptions.emit} is where the
|
|
12092
12095
|
* `isPlaceholder: true` packet is built, because that flag belongs to the
|
|
12093
12096
|
* broker's packet contract and the containment that reads it lives there.
|
|
12094
12097
|
*/
|
|
12095
|
-
var
|
|
12096
|
-
var
|
|
12098
|
+
var DEFAULT_SENTRY_INTERVAL_MS = 1e3;
|
|
12099
|
+
var SentryPump = class {
|
|
12097
12100
|
options;
|
|
12098
12101
|
intervalMs;
|
|
12099
12102
|
tags;
|
|
@@ -12105,7 +12108,7 @@ var PrestreamPump = class {
|
|
|
12105
12108
|
emitted = 0;
|
|
12106
12109
|
constructor(options) {
|
|
12107
12110
|
this.options = options;
|
|
12108
|
-
this.intervalMs = options.intervalMs ??
|
|
12111
|
+
this.intervalMs = options.intervalMs ?? DEFAULT_SENTRY_INTERVAL_MS;
|
|
12109
12112
|
this.tags = options.deviceId === void 0 ? {} : { tags: { deviceId: options.deviceId } };
|
|
12110
12113
|
}
|
|
12111
12114
|
/** Whether the window is currently being filled (or about to be). */
|
|
@@ -12124,16 +12127,16 @@ var PrestreamPump = class {
|
|
|
12124
12127
|
this.emitted = 0;
|
|
12125
12128
|
this.options.prepare({ sourceUrl }).then((frame) => {
|
|
12126
12129
|
if (!this.active) {
|
|
12127
|
-
this.options.logger?.debug("
|
|
12130
|
+
this.options.logger?.debug("sentry: preparation landed after the window closed", { ...this.tags });
|
|
12128
12131
|
return;
|
|
12129
12132
|
}
|
|
12130
12133
|
if (frame === null) {
|
|
12131
|
-
this.options.logger?.info("
|
|
12134
|
+
this.options.logger?.info("sentry: refused — the window goes unfilled", { ...this.tags });
|
|
12132
12135
|
this.active = false;
|
|
12133
12136
|
return;
|
|
12134
12137
|
}
|
|
12135
12138
|
this.frame = frame;
|
|
12136
|
-
this.options.logger?.info("
|
|
12139
|
+
this.options.logger?.info("sentry: filling the wake window", {
|
|
12137
12140
|
...this.tags,
|
|
12138
12141
|
meta: {
|
|
12139
12142
|
codec: frame.codec,
|
|
@@ -12145,7 +12148,7 @@ var PrestreamPump = class {
|
|
|
12145
12148
|
this.tick();
|
|
12146
12149
|
this.timer = setInterval(() => this.tick(), this.intervalMs);
|
|
12147
12150
|
}).catch((err) => {
|
|
12148
|
-
this.options.logger?.warn("
|
|
12151
|
+
this.options.logger?.warn("sentry: preparation failed — the window goes unfilled", {
|
|
12149
12152
|
...this.tags,
|
|
12150
12153
|
meta: { error: errMsg(err) }
|
|
12151
12154
|
});
|
|
@@ -12154,7 +12157,7 @@ var PrestreamPump = class {
|
|
|
12154
12157
|
}
|
|
12155
12158
|
/**
|
|
12156
12159
|
* Close the window. Called on the first NON-placeholder packet — the one edge
|
|
12157
|
-
* the
|
|
12160
|
+
* the sentry is defined by — and on the broker's own teardown paths so a
|
|
12158
12161
|
* stopped broker leaves no timer behind.
|
|
12159
12162
|
*/
|
|
12160
12163
|
stop(reason) {
|
|
@@ -12164,7 +12167,7 @@ var PrestreamPump = class {
|
|
|
12164
12167
|
clearInterval(this.timer);
|
|
12165
12168
|
this.timer = void 0;
|
|
12166
12169
|
}
|
|
12167
|
-
this.options.logger?.info("
|
|
12170
|
+
this.options.logger?.info("sentry: window closed", {
|
|
12168
12171
|
...this.tags,
|
|
12169
12172
|
meta: {
|
|
12170
12173
|
reason,
|
|
@@ -12475,12 +12478,12 @@ var StreamBroker = class StreamBroker {
|
|
|
12475
12478
|
reconnectTimer;
|
|
12476
12479
|
placeholderTimer;
|
|
12477
12480
|
/**
|
|
12478
|
-
* The
|
|
12479
|
-
* `undefined` on a broker whose manager supplied none — the pre-
|
|
12481
|
+
* The sentry pump, armed by the manager via `setPrestreamProvider`.
|
|
12482
|
+
* `undefined` on a broker whose manager supplied none — the pre-sentry
|
|
12480
12483
|
* behaviour, unchanged.
|
|
12481
12484
|
*/
|
|
12482
|
-
|
|
12483
|
-
/** The manager's hook for "real media started" — see `
|
|
12485
|
+
sentryPump;
|
|
12486
|
+
/** The manager's hook for "real media started" — see `SentryProvider`. */
|
|
12484
12487
|
onRealMediaStarted;
|
|
12485
12488
|
/**
|
|
12486
12489
|
* Latched once real media has been seen on this session.
|
|
@@ -12490,9 +12493,9 @@ var StreamBroker = class StreamBroker {
|
|
|
12490
12493
|
* window that real bytes have already closed, and the consumer would get a
|
|
12491
12494
|
* still picture painted over live video every second. Reset exactly where
|
|
12492
12495
|
* `firstKeyframeReceived` is — broker stop and suspend — so a battery camera
|
|
12493
|
-
* that goes back to sleep gets its
|
|
12496
|
+
* that goes back to sleep gets its sentry again on the next wake.
|
|
12494
12497
|
*/
|
|
12495
|
-
|
|
12498
|
+
sentryWindowClosed = false;
|
|
12496
12499
|
reconnectDelayMs = INITIAL_RECONNECT_DELAY_MS;
|
|
12497
12500
|
/**
|
|
12498
12501
|
* Dial rate-limiter (2026-07-23 storm fix): the earliest wall-clock at
|
|
@@ -12768,7 +12771,7 @@ var StreamBroker = class StreamBroker {
|
|
|
12768
12771
|
* that socket down. In that window `resolveCurrentSource()` truthfully
|
|
12769
12772
|
* answers with a socket nobody can dial.
|
|
12770
12773
|
*
|
|
12771
|
-
* That cost device 640 its
|
|
12774
|
+
* That cost device 640 its sentry on 2026-09-16: the window re-opened
|
|
12772
12775
|
* (which `suspend()` does deliberately, for a consumer that never left) with
|
|
12773
12776
|
* the real url, `planPrestream` refused it `not-lazy`, and the refusal is
|
|
12774
12777
|
* terminal — the operator's client sat dark for ten seconds and gave up six
|
|
@@ -12777,8 +12780,8 @@ var StreamBroker = class StreamBroker {
|
|
|
12777
12780
|
* NOT cleared when `start()` is later called with a materialized url: that
|
|
12778
12781
|
* happens on every wake, and clearing it there is the bug again. A broker
|
|
12779
12782
|
* genuinely reconfigured onto a permanent mains source keeps a stale value,
|
|
12780
|
-
* which costs nothing — `
|
|
12781
|
-
* the authority on whether a camera gets a
|
|
12783
|
+
* which costs nothing — `sentryEligible` (battery + declared signals) is
|
|
12784
|
+
* the authority on whether a camera gets a sentry at all, and it is asked
|
|
12782
12785
|
* first, in the manager.
|
|
12783
12786
|
*/
|
|
12784
12787
|
lazySourceUrl;
|
|
@@ -13041,7 +13044,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13041
13044
|
this.requestSourceRefresh = notify;
|
|
13042
13045
|
}
|
|
13043
13046
|
/**
|
|
13044
|
-
* Arm the
|
|
13047
|
+
* Arm the SENTRY — the fill for the window between "someone asked for this
|
|
13045
13048
|
* camera" and "the camera is awake".
|
|
13046
13049
|
*
|
|
13047
13050
|
* Manager-owned, because everything the fill needs is the manager's: the
|
|
@@ -13049,13 +13052,13 @@ var StreamBroker = class StreamBroker {
|
|
|
13049
13052
|
* snapshot, its name, and the ffmpeg binary. The broker contributes the one
|
|
13050
13053
|
* thing it owns — WHEN the window is open — and nothing else.
|
|
13051
13054
|
*
|
|
13052
|
-
* A broker with no provider behaves exactly as it did before the
|
|
13055
|
+
* A broker with no provider behaves exactly as it did before the sentry
|
|
13053
13056
|
* existed: the lazy branch asks the publisher to materialize and the consumer
|
|
13054
13057
|
* sees black, which is the outcome every refusal falls back to.
|
|
13055
13058
|
*/
|
|
13056
13059
|
setPrestreamProvider(provider) {
|
|
13057
13060
|
this.onRealMediaStarted = provider.onRealMediaStarted;
|
|
13058
|
-
this.
|
|
13061
|
+
this.sentryPump = new SentryPump({
|
|
13059
13062
|
prepare: provider.prepare,
|
|
13060
13063
|
emit: (frame) => {
|
|
13061
13064
|
const now = Date.now();
|
|
@@ -13175,7 +13178,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13175
13178
|
*
|
|
13176
13179
|
* Here and not only in `start()` because a broker that has never dialled is a
|
|
13177
13180
|
* real and common state: a battery camera nobody has asked for yet, with a
|
|
13178
|
-
*
|
|
13181
|
+
* sentry consumer attached, has a recipe and no socket. Learning it from
|
|
13179
13182
|
* the dial alone would leave exactly that broker unable to describe itself.
|
|
13180
13183
|
*/
|
|
13181
13184
|
noteLazyUrl(url) {
|
|
@@ -13252,8 +13255,8 @@ var StreamBroker = class StreamBroker {
|
|
|
13252
13255
|
clearInterval(this.placeholderTimer);
|
|
13253
13256
|
this.placeholderTimer = void 0;
|
|
13254
13257
|
}
|
|
13255
|
-
this.
|
|
13256
|
-
this.
|
|
13258
|
+
this.sentryPump?.stop("broker stopped");
|
|
13259
|
+
this.sentryWindowClosed = false;
|
|
13257
13260
|
if (this.reconnectTimer) {
|
|
13258
13261
|
clearTimeout(this.reconnectTimer);
|
|
13259
13262
|
this.reconnectTimer = void 0;
|
|
@@ -13423,7 +13426,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13423
13426
|
*
|
|
13424
13427
|
* - the caller has already checked the signal is ENABLED for this camera
|
|
13425
13428
|
* (the broker manager owns that setting), and
|
|
13426
|
-
* - somebody is actually waiting on the
|
|
13429
|
+
* - somebody is actually waiting on the sentry. Waking a camera nobody is
|
|
13427
13430
|
* watching is the one thing a battery camera must never do, and a signal
|
|
13428
13431
|
* arriving with no consumer is exactly that.
|
|
13429
13432
|
*
|
|
@@ -13466,7 +13469,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13466
13469
|
if (delay <= 0) return;
|
|
13467
13470
|
this.wakeHoldTimer = setTimeout(() => {
|
|
13468
13471
|
this.wakeHoldTimer = void 0;
|
|
13469
|
-
this.logger?.info("wake hold lapsed — back to the
|
|
13472
|
+
this.logger?.info("wake hold lapsed — back to the sentry picture", {
|
|
13470
13473
|
tags: { deviceId: this.numericDeviceId },
|
|
13471
13474
|
meta: {
|
|
13472
13475
|
brokerId: this.deviceId,
|
|
@@ -13479,20 +13482,20 @@ var StreamBroker = class StreamBroker {
|
|
|
13479
13482
|
this.wakeHoldTimer.unref?.();
|
|
13480
13483
|
}
|
|
13481
13484
|
/**
|
|
13482
|
-
* Anything that must be FED,
|
|
13485
|
+
* Anything that must be FED, sentry sessions included.
|
|
13483
13486
|
*
|
|
13484
|
-
* The only consumer of this is the
|
|
13487
|
+
* The only consumer of this is the sentry pump: it says "somebody is
|
|
13485
13488
|
* watching, keep the picture going", and says nothing about whether the
|
|
13486
13489
|
* camera may be dialled. {@link hasLiveIntent} answers that, and it is the
|
|
13487
13490
|
* one every dial path asks.
|
|
13488
13491
|
*/
|
|
13489
13492
|
hasAnyConsumer() {
|
|
13490
13493
|
if (this.hasLiveIntent()) return true;
|
|
13491
|
-
if (this.
|
|
13494
|
+
if (this.sentryHolds > 0) return true;
|
|
13492
13495
|
return this.rtspRestreamer.getPrestreamSessionCount() > 0;
|
|
13493
13496
|
}
|
|
13494
13497
|
/**
|
|
13495
|
-
* Somebody is DESCRIBING a
|
|
13498
|
+
* Somebody is DESCRIBING a sentry and has no session yet.
|
|
13496
13499
|
*
|
|
13497
13500
|
* The RTSP dispatch reads the SDP before `addSession` runs, so a window that
|
|
13498
13501
|
* waits for the session is too late: the consumer is handed an empty SDP and
|
|
@@ -13500,28 +13503,28 @@ var StreamBroker = class StreamBroker {
|
|
|
13500
13503
|
* on the attach and closed 42 ms later having emitted zero frames, because
|
|
13501
13504
|
* ffmpeg had already given up.
|
|
13502
13505
|
*
|
|
13503
|
-
* This is the
|
|
13506
|
+
* This is the sentry counterpart of `BrokerWarmUp` and its exact opposite
|
|
13504
13507
|
* in effect: the warm-up subscribes demand, which DIALS; this opens the
|
|
13505
13508
|
* picture, which never does. A battery camera is not touched.
|
|
13506
13509
|
*/
|
|
13507
13510
|
openPrestreamWindow() {
|
|
13508
|
-
this.
|
|
13511
|
+
this.sentryHolds += 1;
|
|
13509
13512
|
this.checkDemand();
|
|
13510
13513
|
let released = false;
|
|
13511
13514
|
return () => {
|
|
13512
13515
|
if (released) return;
|
|
13513
13516
|
released = true;
|
|
13514
|
-
this.
|
|
13517
|
+
this.sentryHolds = Math.max(0, this.sentryHolds - 1);
|
|
13515
13518
|
this.checkDemand();
|
|
13516
13519
|
};
|
|
13517
13520
|
}
|
|
13518
|
-
/** DESCRIBE-window
|
|
13519
|
-
|
|
13521
|
+
/** DESCRIBE-window sentry holds: sessions that do not exist yet. */
|
|
13522
|
+
sentryHolds = 0;
|
|
13520
13523
|
/**
|
|
13521
13524
|
* Consumers that may DIAL. Every term of the former `hasDemand`, with the
|
|
13522
|
-
* RTSP session count filtered to exclude
|
|
13525
|
+
* RTSP session count filtered to exclude sentry dials, plus the wake hold.
|
|
13523
13526
|
*
|
|
13524
|
-
* For a broker with no
|
|
13527
|
+
* For a broker with no sentry session attached this is term-for-term what
|
|
13525
13528
|
* `hasDemand` was, which is why every existing camera, consumer and test
|
|
13526
13529
|
* behaves identically.
|
|
13527
13530
|
*/
|
|
@@ -13566,7 +13569,7 @@ var StreamBroker = class StreamBroker {
|
|
|
13566
13569
|
});
|
|
13567
13570
|
}
|
|
13568
13571
|
/**
|
|
13569
|
-
* Encode ONE canonical 20 ms Opus silence packet for a session's
|
|
13572
|
+
* Encode ONE canonical 20 ms Opus silence packet for a session's sentry
|
|
13570
13573
|
* A/V-sync padding (Opus has no fixed silence byte). Returns `null` when the
|
|
13571
13574
|
* audio-codec cap is unavailable. See {@link encodeOpusSilenceFrame}.
|
|
13572
13575
|
*/
|
|
@@ -13676,16 +13679,16 @@ var StreamBroker = class StreamBroker {
|
|
|
13676
13679
|
if (!this.hasLiveIntent() && !this.manualStop && !this.stopping) this.suspend();
|
|
13677
13680
|
}, SUSPEND_GRACE_MS);
|
|
13678
13681
|
}
|
|
13679
|
-
this.
|
|
13682
|
+
this.sentryWanted = this.hasAnyConsumer() && !this.hasLiveIntent();
|
|
13680
13683
|
this.reconcilePrestreamWindow();
|
|
13681
13684
|
}
|
|
13682
13685
|
/**
|
|
13683
|
-
* Open or close the
|
|
13686
|
+
* Open or close the sentry window to match {@link sentryWanted}.
|
|
13684
13687
|
*
|
|
13685
|
-
* This is the half that was missing. `
|
|
13688
|
+
* This is the half that was missing. `sentryWanted` has been computed
|
|
13686
13689
|
* correctly since the feature shipped, but the only thing that ever STARTED
|
|
13687
13690
|
* the pump was the lazy branch of `start()` — a DIAL, which needs live
|
|
13688
|
-
* intent, which a
|
|
13691
|
+
* intent, which a sentry session does not have by construction. So the
|
|
13689
13692
|
* window opened only when something else had already woken the camera, and
|
|
13690
13693
|
* `suspend()` closed it for good.
|
|
13691
13694
|
*
|
|
@@ -13695,22 +13698,22 @@ var StreamBroker = class StreamBroker {
|
|
|
13695
13698
|
* demand check costs one encode per window and no more.
|
|
13696
13699
|
*/
|
|
13697
13700
|
reconcilePrestreamWindow() {
|
|
13698
|
-
if (!this.
|
|
13699
|
-
if (this.
|
|
13701
|
+
if (!this.sentryPump) return;
|
|
13702
|
+
if (this.sentryWanted && !this.sentryWindowClosed) {
|
|
13700
13703
|
const url = this.lazySourceUrl ?? this.resolveCurrentSource()?.url ?? this.source?.url;
|
|
13701
|
-
if (url !== void 0) this.
|
|
13704
|
+
if (url !== void 0) this.sentryPump.start(url);
|
|
13702
13705
|
return;
|
|
13703
13706
|
}
|
|
13704
|
-
if (!this.
|
|
13707
|
+
if (!this.sentryWanted) this.sentryPump.stop("no sentry consumer");
|
|
13705
13708
|
}
|
|
13706
13709
|
/**
|
|
13707
|
-
* Whether a
|
|
13710
|
+
* Whether a sentry picture should be flowing right now: somebody is
|
|
13708
13711
|
* watching and the camera is not up. Read by the pump.
|
|
13709
13712
|
*/
|
|
13710
|
-
|
|
13713
|
+
sentryWanted = false;
|
|
13711
13714
|
/** Whether the picture should be flowing — the pump's one question. */
|
|
13712
13715
|
wantsPrestream() {
|
|
13713
|
-
return this.
|
|
13716
|
+
return this.sentryWanted;
|
|
13714
13717
|
}
|
|
13715
13718
|
/** Suspend the stream: kill ffmpeg/native client, clear buffers, go idle. Source is preserved for resume. */
|
|
13716
13719
|
suspend() {
|
|
@@ -13735,8 +13738,8 @@ var StreamBroker = class StreamBroker {
|
|
|
13735
13738
|
clearInterval(this.placeholderTimer);
|
|
13736
13739
|
this.placeholderTimer = void 0;
|
|
13737
13740
|
}
|
|
13738
|
-
this.
|
|
13739
|
-
this.
|
|
13741
|
+
this.sentryPump?.stop("broker suspended");
|
|
13742
|
+
this.sentryWindowClosed = false;
|
|
13740
13743
|
this.reconcilePrestreamWindow();
|
|
13741
13744
|
this.preBuffer.clear();
|
|
13742
13745
|
this.decoderSeedBuffer.clear();
|
|
@@ -13821,13 +13824,13 @@ var StreamBroker = class StreamBroker {
|
|
|
13821
13824
|
}
|
|
13822
13825
|
pushEncodedPacket(packet) {
|
|
13823
13826
|
if (this.stopping) return;
|
|
13824
|
-
if (packet.isPlaceholder !== true && !this.
|
|
13825
|
-
this.
|
|
13826
|
-
this.
|
|
13827
|
+
if (packet.isPlaceholder !== true && !this.sentryWindowClosed) {
|
|
13828
|
+
this.sentryWindowClosed = true;
|
|
13829
|
+
this.sentryPump?.stop("first real packet");
|
|
13827
13830
|
try {
|
|
13828
13831
|
this.onRealMediaStarted?.();
|
|
13829
13832
|
} catch (err) {
|
|
13830
|
-
this.logger?.debug("
|
|
13833
|
+
this.logger?.debug("sentry: real-media hook threw", {
|
|
13831
13834
|
tags: { deviceId: this.numericDeviceId ?? 0 },
|
|
13832
13835
|
meta: { error: String(err) }
|
|
13833
13836
|
});
|
|
@@ -15576,7 +15579,7 @@ var StreamBroker = class StreamBroker {
|
|
|
15576
15579
|
});
|
|
15577
15580
|
this._status = "error";
|
|
15578
15581
|
this.notifySourceRefresh();
|
|
15579
|
-
if (!this.
|
|
15582
|
+
if (!this.sentryWindowClosed) this.sentryPump?.start(source.url);
|
|
15580
15583
|
this.scheduleReconnect();
|
|
15581
15584
|
return;
|
|
15582
15585
|
}
|
|
@@ -16308,16 +16311,16 @@ var DeviceOverrideSchema = object({
|
|
|
16308
16311
|
sourceParser: record(string(), SourceParserSettingSchema).optional(),
|
|
16309
16312
|
sourceParserArgs: record(string(), SourceParserArgsEntrySchema).optional(),
|
|
16310
16313
|
/**
|
|
16311
|
-
* The on-demand
|
|
16314
|
+
* The on-demand SENTRY, per camera.
|
|
16312
16315
|
*
|
|
16313
16316
|
* Absent means the operator has never touched it, which is NOT the same as
|
|
16314
16317
|
* off: `enabledSignals` absent means "every signal this camera recommends",
|
|
16315
|
-
* so a Reolink works the moment a
|
|
16318
|
+
* so a Reolink works the moment a sentry URL is dialled without an
|
|
16316
16319
|
* operator hunting for a checkbox. An empty ARRAY is a deliberate "none" and
|
|
16317
16320
|
* is honoured — the two are different answers and the shape keeps them
|
|
16318
16321
|
* apart.
|
|
16319
16322
|
*/
|
|
16320
|
-
|
|
16323
|
+
sentry: object({
|
|
16321
16324
|
/** Signal codes allowed to wake this camera. Absent ⇒ the recommended set. */
|
|
16322
16325
|
enabledSignals: array(string()).optional(),
|
|
16323
16326
|
/** How long the camera stays up after the LAST signal. Absent ⇒ default. */
|
|
@@ -16511,13 +16514,13 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
16511
16514
|
/** Per-device published cam-stream registry. */
|
|
16512
16515
|
cameraStreams = /* @__PURE__ */ new Map();
|
|
16513
16516
|
/**
|
|
16514
|
-
* The encoded
|
|
16517
|
+
* The encoded sentry frame per broker, with the digest of the picture it
|
|
16515
16518
|
* was made from. One entry is one still image — kilobytes — and it is
|
|
16516
16519
|
* dropped with the broker in `releaseBroker`.
|
|
16517
16520
|
*/
|
|
16518
|
-
/** Brokers with a background
|
|
16519
|
-
|
|
16520
|
-
|
|
16521
|
+
/** Brokers with a background sentry re-encode in flight (single-flight). */
|
|
16522
|
+
sentryRefreshing = /* @__PURE__ */ new Set();
|
|
16523
|
+
sentryFrames = /* @__PURE__ */ new Map();
|
|
16521
16524
|
/** Monotonic insertion counter per device — feeds `CameraStreamInternal.order`. */
|
|
16522
16525
|
streamOrderSeq = /* @__PURE__ */ new Map();
|
|
16523
16526
|
/** Persisted operator intent: which cam stream serves which profile. */
|
|
@@ -17409,7 +17412,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17409
17412
|
* client-side and we get a typed payload.
|
|
17410
17413
|
*/
|
|
17411
17414
|
/**
|
|
17412
|
-
* Build the per-broker
|
|
17415
|
+
* Build the per-broker SENTRY provider — the fill for the window between
|
|
17413
17416
|
* "someone asked for this camera" and "the camera is awake".
|
|
17414
17417
|
*
|
|
17415
17418
|
* On this hub that window is 2.2-12.4 s for a lazy source, and it is not
|
|
@@ -17422,19 +17425,19 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17422
17425
|
* cached snapshot, its name, and the ffmpeg binary path.
|
|
17423
17426
|
*
|
|
17424
17427
|
* `null` is a REFUSAL, named in the log. Refusing is cheap — the consumer
|
|
17425
|
-
* gets the black screen it got before the
|
|
17428
|
+
* gets the black screen it got before the sentry existed — and the
|
|
17426
17429
|
* alternative for an unknown geometry is the decoder freeze the whole
|
|
17427
17430
|
* synthetic-frame path is shaped around.
|
|
17428
17431
|
*/
|
|
17429
17432
|
buildPrestreamProvider(deviceId, camStreamId, brokerId, logger) {
|
|
17430
17433
|
return {
|
|
17431
17434
|
prepare: async ({ sourceUrl }) => {
|
|
17432
|
-
const gate =
|
|
17435
|
+
const gate = sentryEligible({
|
|
17433
17436
|
batteryOperated: await this.isBatteryOperated(deviceId),
|
|
17434
17437
|
declaresSignals: (await this.declaredSignalCodes(deviceId)).length > 0
|
|
17435
17438
|
});
|
|
17436
17439
|
if (!gate.eligible) {
|
|
17437
|
-
logger.info("
|
|
17440
|
+
logger.info("sentry: not offered for this camera", {
|
|
17438
17441
|
tags: { deviceId },
|
|
17439
17442
|
meta: {
|
|
17440
17443
|
brokerId,
|
|
@@ -17452,7 +17455,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17452
17455
|
codec
|
|
17453
17456
|
});
|
|
17454
17457
|
if ("refusal" in planned) {
|
|
17455
|
-
logger.info("
|
|
17458
|
+
logger.info("sentry: no fill for this camera", {
|
|
17456
17459
|
tags: { deviceId },
|
|
17457
17460
|
meta: {
|
|
17458
17461
|
refusal: planned.refusal,
|
|
@@ -17472,7 +17475,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17472
17475
|
* The window shows the camera's last snapshot, and nothing was
|
|
17473
17476
|
* refreshing it: a battery camera refuses a capture while it believes
|
|
17474
17477
|
* itself asleep, and that belief lags the wake by seconds — so the only
|
|
17475
|
-
* attempts ever made were the
|
|
17478
|
+
* attempts ever made were the sentry's own, while composing, which is
|
|
17476
17479
|
* BEFORE any media exists. Measured on device 640 (2026-09-16): four
|
|
17477
17480
|
* wakes in forty minutes, the encoded picture `bytes=23912` every time,
|
|
17478
17481
|
* and between first packet and suspend not one capture was even
|
|
@@ -17495,12 +17498,12 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17495
17498
|
const api = this.api;
|
|
17496
17499
|
if (!api) return;
|
|
17497
17500
|
api.snapshot.getSnapshot.query({ deviceId }).then(() => {
|
|
17498
|
-
logger.debug("
|
|
17501
|
+
logger.debug("sentry: picture refreshed from the live window", {
|
|
17499
17502
|
tags: { deviceId },
|
|
17500
17503
|
meta: { brokerId }
|
|
17501
17504
|
});
|
|
17502
17505
|
}).catch((err) => {
|
|
17503
|
-
logger.debug("
|
|
17506
|
+
logger.debug("sentry: live-window refresh failed — keeping the older picture", {
|
|
17504
17507
|
tags: { deviceId },
|
|
17505
17508
|
meta: {
|
|
17506
17509
|
brokerId,
|
|
@@ -17521,7 +17524,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17521
17524
|
*/
|
|
17522
17525
|
async preparePrestreamFrame(deviceId, brokerId, plan, logger) {
|
|
17523
17526
|
if (!this.api) {
|
|
17524
|
-
logger.info("
|
|
17527
|
+
logger.info("sentry: no api access — the window goes unfilled", { tags: { deviceId } });
|
|
17525
17528
|
return null;
|
|
17526
17529
|
}
|
|
17527
17530
|
const api = this.api;
|
|
@@ -17530,7 +17533,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17530
17533
|
const image = await api.snapshot.getSnapshot.query({ deviceId });
|
|
17531
17534
|
if (image) cachedJpeg = Buffer.from(image.base64, "base64");
|
|
17532
17535
|
} catch (err) {
|
|
17533
|
-
logger.debug("
|
|
17536
|
+
logger.debug("sentry: cached snapshot read failed — falling back to the card", {
|
|
17534
17537
|
tags: { deviceId },
|
|
17535
17538
|
meta: { error: errMsg(err) }
|
|
17536
17539
|
});
|
|
@@ -17540,15 +17543,15 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17540
17543
|
const device = await api.deviceManager.getDevice.query({ deviceId });
|
|
17541
17544
|
if (device?.name) deviceName = device.name;
|
|
17542
17545
|
} catch (err) {
|
|
17543
|
-
logger.debug("
|
|
17546
|
+
logger.debug("sentry: device name read failed — using the numeric id", {
|
|
17544
17547
|
tags: { deviceId },
|
|
17545
17548
|
meta: { error: errMsg(err) }
|
|
17546
17549
|
});
|
|
17547
17550
|
}
|
|
17548
17551
|
const key = `${createHash("sha1").update(cachedJpeg ?? Buffer.alloc(0)).update(deviceName).digest("hex")}:${String(plan.width)}x${String(plan.height)}:${plan.codec}`;
|
|
17549
|
-
const cached = this.
|
|
17552
|
+
const cached = this.sentryFrames.get(brokerId);
|
|
17550
17553
|
if (cached && cached.key === key) {
|
|
17551
|
-
logger.debug("
|
|
17554
|
+
logger.debug("sentry: re-using the encoded frame", {
|
|
17552
17555
|
tags: { deviceId },
|
|
17553
17556
|
meta: { bytes: cached.frame.annexB.length }
|
|
17554
17557
|
});
|
|
@@ -17557,7 +17560,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17557
17560
|
const cachedGeometry = cached ? cached.key.slice(cached.key.indexOf(":")) : null;
|
|
17558
17561
|
const wantedGeometry = key.slice(key.indexOf(":"));
|
|
17559
17562
|
if (cached && cachedGeometry === wantedGeometry) {
|
|
17560
|
-
logger.info("
|
|
17563
|
+
logger.info("sentry: serving the previous picture while the new one encodes", {
|
|
17561
17564
|
tags: { deviceId },
|
|
17562
17565
|
meta: { bytes: cached.frame.annexB.length }
|
|
17563
17566
|
});
|
|
@@ -17582,10 +17585,11 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17582
17585
|
logger,
|
|
17583
17586
|
deviceId
|
|
17584
17587
|
});
|
|
17585
|
-
logger.info("
|
|
17588
|
+
logger.info("sentry: picture composed", {
|
|
17586
17589
|
tags: { deviceId },
|
|
17587
17590
|
meta: {
|
|
17588
17591
|
source: picture.source,
|
|
17592
|
+
reason: picture.reason,
|
|
17589
17593
|
width: plan.width,
|
|
17590
17594
|
height: plan.height,
|
|
17591
17595
|
codec: plan.codec
|
|
@@ -17601,7 +17605,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17601
17605
|
deviceId
|
|
17602
17606
|
});
|
|
17603
17607
|
} catch (err) {
|
|
17604
|
-
logger.warn("
|
|
17608
|
+
logger.warn("sentry: encode failed — the window goes unfilled", {
|
|
17605
17609
|
tags: { deviceId },
|
|
17606
17610
|
meta: { error: errMsg(err) }
|
|
17607
17611
|
});
|
|
@@ -17611,7 +17615,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17611
17615
|
annexB,
|
|
17612
17616
|
codec: plan.codec
|
|
17613
17617
|
};
|
|
17614
|
-
this.
|
|
17618
|
+
this.sentryFrames.set(brokerId, {
|
|
17615
17619
|
key,
|
|
17616
17620
|
frame
|
|
17617
17621
|
});
|
|
@@ -17625,10 +17629,10 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17625
17629
|
* operator is actually waiting for.
|
|
17626
17630
|
*/
|
|
17627
17631
|
encodePrestreamInBackground(deviceId, brokerId, plan, key, cachedJpeg, deviceName, logger) {
|
|
17628
|
-
if (this.
|
|
17629
|
-
this.
|
|
17632
|
+
if (this.sentryRefreshing.has(brokerId)) return;
|
|
17633
|
+
this.sentryRefreshing.add(brokerId);
|
|
17630
17634
|
this.composeAndEncodePrestream(deviceId, brokerId, plan, key, cachedJpeg, deviceName, logger).catch(() => {}).finally(() => {
|
|
17631
|
-
this.
|
|
17635
|
+
this.sentryRefreshing.delete(brokerId);
|
|
17632
17636
|
});
|
|
17633
17637
|
}
|
|
17634
17638
|
subscribePlaceholderStateSources(bus) {
|
|
@@ -19635,7 +19639,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19635
19639
|
reason: "unknown-code"
|
|
19636
19640
|
};
|
|
19637
19641
|
}
|
|
19638
|
-
const settings = this.deviceOverrides.get(input.deviceId)?.
|
|
19642
|
+
const settings = this.deviceOverrides.get(input.deviceId)?.sentry;
|
|
19639
19643
|
if (!(settings?.enabledSignals ?? declared).includes(input.code)) return {
|
|
19640
19644
|
accepted: false,
|
|
19641
19645
|
reason: "not-enabled"
|
|
@@ -19817,11 +19821,11 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19817
19821
|
label: hwAccelOptionLabel(setting)
|
|
19818
19822
|
}));
|
|
19819
19823
|
const declaredSignals = await this.declaredSignalList(deviceId);
|
|
19820
|
-
const
|
|
19824
|
+
const sentryGate = sentryEligible({
|
|
19821
19825
|
batteryOperated: await this.isBatteryOperated(deviceId),
|
|
19822
19826
|
declaresSignals: declaredSignals.length > 0
|
|
19823
19827
|
});
|
|
19824
|
-
const
|
|
19828
|
+
const sentryOffered = sentryGate.eligible;
|
|
19825
19829
|
const streamTabs = streams.filter((s) => !s.camStreamId.startsWith("derived:")).map((stream) => {
|
|
19826
19830
|
const camStreamId = stream.camStreamId;
|
|
19827
19831
|
const brokerId = brokerIdFor(deviceId, camStreamId);
|
|
@@ -19921,18 +19925,18 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19921
19925
|
tooltip: "Copy muted URL to clipboard"
|
|
19922
19926
|
}]
|
|
19923
19927
|
},
|
|
19924
|
-
...
|
|
19928
|
+
...sentryOffered ? [{
|
|
19925
19929
|
type: "text",
|
|
19926
19930
|
key: `rtspPrestreamUrl:${camStreamId}`,
|
|
19927
|
-
label: "RTSP URL (
|
|
19931
|
+
label: "RTSP URL (sentry)",
|
|
19928
19932
|
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.",
|
|
19929
19933
|
readonlyField: true,
|
|
19930
19934
|
span: 2,
|
|
19931
|
-
value: rtspEntry?.url ? `${rtspEntry.url}/
|
|
19935
|
+
value: rtspEntry?.url ? `${rtspEntry.url}/sentry` : "inactive",
|
|
19932
19936
|
actions: [{
|
|
19933
19937
|
action: "copy-value",
|
|
19934
19938
|
icon: "copy",
|
|
19935
|
-
tooltip: "Copy
|
|
19939
|
+
tooltip: "Copy sentry URL to clipboard"
|
|
19936
19940
|
}]
|
|
19937
19941
|
}] : [],
|
|
19938
19942
|
{
|
|
@@ -20120,25 +20124,25 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
20120
20124
|
]
|
|
20121
20125
|
}]
|
|
20122
20126
|
};
|
|
20123
|
-
const
|
|
20124
|
-
const
|
|
20125
|
-
id: "stream-broker-
|
|
20127
|
+
const sentryCfg = override?.sentry;
|
|
20128
|
+
const sentrySections = sentryGate.eligible ? [{
|
|
20129
|
+
id: "stream-broker-sentry",
|
|
20126
20130
|
title: "Wake signals",
|
|
20127
20131
|
tab: "stream-broker",
|
|
20128
20132
|
location: "top-tab",
|
|
20129
20133
|
order: 50,
|
|
20130
|
-
description: "A consumer can dial this camera’s
|
|
20134
|
+
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.",
|
|
20131
20135
|
fields: [...declaredSignals.map((signal) => ({
|
|
20132
20136
|
type: "boolean",
|
|
20133
20137
|
style: "switch",
|
|
20134
|
-
key: `
|
|
20138
|
+
key: `sentrySignal:${signal.code}`,
|
|
20135
20139
|
label: signal.label,
|
|
20136
20140
|
description: signal.recommended ? "Recommended by this camera’s driver." : "Offered by this camera’s driver.",
|
|
20137
20141
|
default: signal.recommended,
|
|
20138
|
-
value:
|
|
20142
|
+
value: sentryCfg?.enabledSignals === void 0 ? true : sentryCfg.enabledSignals.includes(signal.code)
|
|
20139
20143
|
})), {
|
|
20140
20144
|
type: "number",
|
|
20141
|
-
key: "
|
|
20145
|
+
key: "sentryHoldMs",
|
|
20142
20146
|
label: "Stay awake after the last signal",
|
|
20143
20147
|
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.",
|
|
20144
20148
|
min: 1e3,
|
|
@@ -20146,7 +20150,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
20146
20150
|
step: 1e3,
|
|
20147
20151
|
unit: "ms",
|
|
20148
20152
|
default: DEFAULT_WAKE_HOLD_MS,
|
|
20149
|
-
value:
|
|
20153
|
+
value: sentryCfg?.holdMs ?? 15e3
|
|
20150
20154
|
}]
|
|
20151
20155
|
}] : [];
|
|
20152
20156
|
const diagnosticsSection = {
|
|
@@ -20175,7 +20179,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
20175
20179
|
};
|
|
20176
20180
|
return { sections: [
|
|
20177
20181
|
streamBrokerSection,
|
|
20178
|
-
...
|
|
20182
|
+
...sentrySections,
|
|
20179
20183
|
diagnosticsSection
|
|
20180
20184
|
] };
|
|
20181
20185
|
}
|
|
@@ -20289,27 +20293,27 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
20289
20293
|
nextOverride.sourceParserArgs = spa;
|
|
20290
20294
|
continue;
|
|
20291
20295
|
}
|
|
20292
|
-
if (fieldKey.startsWith("
|
|
20296
|
+
if (fieldKey.startsWith("sentrySignal:")) {
|
|
20293
20297
|
overrideDirty = true;
|
|
20294
20298
|
const next = nextEnabledSignals({
|
|
20295
20299
|
declared: await this.declaredSignalCodes(deviceId),
|
|
20296
|
-
stored: nextOverride.
|
|
20297
|
-
code: fieldKey.slice(
|
|
20300
|
+
stored: nextOverride.sentry?.enabledSignals,
|
|
20301
|
+
code: fieldKey.slice(13),
|
|
20298
20302
|
enabled: value === null || value === void 0 ? true : Boolean(value)
|
|
20299
20303
|
});
|
|
20300
|
-
nextOverride.
|
|
20301
|
-
...nextOverride.
|
|
20304
|
+
nextOverride.sentry = {
|
|
20305
|
+
...nextOverride.sentry,
|
|
20302
20306
|
enabledSignals: [...next]
|
|
20303
20307
|
};
|
|
20304
20308
|
continue;
|
|
20305
20309
|
}
|
|
20306
|
-
if (fieldKey === "
|
|
20310
|
+
if (fieldKey === "sentryHoldMs") {
|
|
20307
20311
|
overrideDirty = true;
|
|
20308
20312
|
const raw = typeof value === "number" ? value : Number(value);
|
|
20309
20313
|
if (!Number.isFinite(raw)) continue;
|
|
20310
20314
|
const clamped = Math.min(3e5, Math.max(1e3, Math.round(raw)));
|
|
20311
|
-
nextOverride.
|
|
20312
|
-
...nextOverride.
|
|
20315
|
+
nextOverride.sentry = {
|
|
20316
|
+
...nextOverride.sentry,
|
|
20313
20317
|
holdMs: clamped
|
|
20314
20318
|
};
|
|
20315
20319
|
continue;
|
|
@@ -21152,7 +21156,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
21152
21156
|
this.streamOfflineReasonByBroker.delete(brokerId);
|
|
21153
21157
|
await broker.stop();
|
|
21154
21158
|
this.brokers.delete(brokerId);
|
|
21155
|
-
this.
|
|
21159
|
+
this.sentryFrames.delete(brokerId);
|
|
21156
21160
|
this.sweepFrameSubscriptions(brokerId);
|
|
21157
21161
|
this.rtspProvider.persistTokens();
|
|
21158
21162
|
this.syncProfileRestreamers(deviceId);
|
|
@@ -24720,7 +24724,7 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
24720
24724
|
* silence packets (0xFF bytes, 160 samples each = 20 ms) so the audio
|
|
24721
24725
|
* sender's first wall-clock instant aligns with the video sender's first
|
|
24722
24726
|
* wall-clock instant, then the real packet continues seamlessly. See
|
|
24723
|
-
* `
|
|
24727
|
+
* `sentryAudioSilenceIfNeeded` + `computeAudioSilencePadPackets`.
|
|
24724
24728
|
*/
|
|
24725
24729
|
firstAudioFramePadded = false;
|
|
24726
24730
|
/** PCMU frame size in samples per packet — 160 = 20 ms at 8 kHz. */
|
|
@@ -25116,7 +25120,7 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
25116
25120
|
return this.audioClock().codec === "opus";
|
|
25117
25121
|
}
|
|
25118
25122
|
/**
|
|
25119
|
-
* Inject the canonical 20 ms Opus silence packet used for
|
|
25123
|
+
* Inject the canonical 20 ms Opus silence packet used for sentry A/V-sync
|
|
25120
25124
|
* padding. Broker-provided (encoded via the audio-codec cap) because Opus has
|
|
25121
25125
|
* no fixed silence byte the way PCMU does (0xFF).
|
|
25122
25126
|
*/
|
|
@@ -25130,7 +25134,7 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
25130
25134
|
const senderCodec = this.audioSender.codec;
|
|
25131
25135
|
const fallbackPt = codec === "Pcmu" || codec === "Pcma" ? 0 : 111;
|
|
25132
25136
|
const pt = senderCodec?.payloadType ?? fallbackPt;
|
|
25133
|
-
this.
|
|
25137
|
+
this.sentryAudioSilenceIfNeeded(rtpTs >>> 0, pt);
|
|
25134
25138
|
try {
|
|
25135
25139
|
const header = new werift.RtpHeader();
|
|
25136
25140
|
header.payloadType = pt;
|
|
@@ -25169,7 +25173,7 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
25169
25173
|
* diag reads ~0). Any failure on a single `sendRtp` is logged and the loop
|
|
25170
25174
|
* continues — partial alignment is better than aborting.
|
|
25171
25175
|
*/
|
|
25172
|
-
|
|
25176
|
+
sentryAudioSilenceIfNeeded(realRtpTs, payloadType) {
|
|
25173
25177
|
if (this.firstAudioFramePadded) return;
|
|
25174
25178
|
this.firstAudioFramePadded = true;
|
|
25175
25179
|
if (!this.audioSender || !_werift) return;
|