@camstack/addon-pipeline 1.2.137 → 1.2.138
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 +5 -5
- package/dist/audio-analyzer/index.mjs +3 -3
- package/dist/detection-pipeline/index.js +12 -12
- package/dist/detection-pipeline/index.mjs +5 -5
- package/dist/{dist-4EMKvxCp.js → dist-BuIP0499.js} +40 -0
- package/dist/{dist-g0Or5_pR.mjs → dist-CmEYRK7G.mjs} +35 -1
- package/dist/{event-loop-stall-monitor-DDm07Ei6.js → event-loop-stall-monitor-Bef2jXMX.js} +1 -1
- package/dist/{event-loop-stall-monitor-CskL9Lgu.mjs → event-loop-stall-monitor-JGTFb0Uv.mjs} +1 -1
- package/dist/{lazy-sharp-BlA9PxWk.js → lazy-sharp-DH2eVehm.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-BOHn8gpk.js → node-ChdX9lv7.js} +1 -1
- package/dist/{node-Bo1g8hLO.mjs → node-J07GBWT3.mjs} +1 -1
- package/dist/pipeline-runner/index.js +55 -12
- package/dist/pipeline-runner/index.mjs +54 -11
- package/dist/{process-memory-C3084DOt.js → process-memory-CJoZemF4.js} +1 -1
- package/dist/{process-memory-DmrHWybb.mjs → process-memory-DAKxUb9d.mjs} +1 -1
- package/dist/recorder/index.js +6 -6
- package/dist/recorder/index.mjs +3 -3
- package/dist/{segment-demux-js-BrQ_A0oi.js → segment-demux-js-BgXbXZnp.js} +1 -1
- package/dist/{segment-demux-js-C-a2pghM.mjs → segment-demux-js-Ck9ly4bX.mjs} +1 -1
- package/dist/session-decode/decode-worker-child.js +115 -5
- package/dist/session-decode/decode-worker-child.mjs +114 -4
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-AH6OJ3hJ.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cad8960O.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-xdmO8wYp.mjs +26 -0
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-XNyn-FTM.mjs → hostInit-Gd3OcDIa.mjs} +3 -3
- package/dist/stream-broker/index.js +160 -23
- package/dist/stream-broker/index.mjs +160 -23
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-BLfaD5uC.js → worker-protocol-Bpz62eyB.js} +2 -1
- package/dist/{worker-protocol-vc8WWhjx.mjs → worker-protocol-wMS29PG9.mjs} +2 -1
- package/package.json +5 -3
- package/dist/addon-utils-C6b5Tnia.js +0 -625
- package/dist/addon-utils-C8KTuPdE.mjs +0 -583
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CQyiEd0l.mjs +0 -26
|
@@ -2,12 +2,12 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_node = require("../node-
|
|
5
|
+
const require_dist = require("../dist-BuIP0499.js");
|
|
6
|
+
const require_node = require("../node-ChdX9lv7.js");
|
|
7
7
|
const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
|
|
8
8
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
9
9
|
const require_retire_root_keys = require("../retire-root-keys-KE6D6Xh_.js");
|
|
10
|
-
const require_segment_demux_js = require("../segment-demux-js-
|
|
10
|
+
const require_segment_demux_js = require("../segment-demux-js-BgXbXZnp.js");
|
|
11
11
|
let node_crypto = require("node:crypto");
|
|
12
12
|
node_crypto = require_dist.__toESM(node_crypto);
|
|
13
13
|
let node_fs = require("node:fs");
|
|
@@ -2245,6 +2245,20 @@ function appendAudioMediaBlock(videoSdp, audioBlock) {
|
|
|
2245
2245
|
return `${videoSdp.replace(/[\r\n]+$/, "")}\r\n${audioBlock}\r\n`;
|
|
2246
2246
|
}
|
|
2247
2247
|
//#endregion
|
|
2248
|
+
//#region src/stream-broker/rtsp/join-burst-admission.ts
|
|
2249
|
+
/**
|
|
2250
|
+
* Decide whether this session may take this burst.
|
|
2251
|
+
*
|
|
2252
|
+
* Pure and total: no clock, no logger, no restreamer state. The restreamer maps
|
|
2253
|
+
* the result onto its counters and its `continue`.
|
|
2254
|
+
*/
|
|
2255
|
+
function admitJoinBurst(input) {
|
|
2256
|
+
if (input.muted && !input.detectionIntent) return !input.fromRing || input.spanMs > input.liveEdgeMaxSpanMs ? "live-edge" : "none";
|
|
2257
|
+
if (!input.detectionIntent && input.spanMs > input.bound.maxSpanMs) return "span";
|
|
2258
|
+
if (input.packets > input.bound.maxPackets) return "packets";
|
|
2259
|
+
return "none";
|
|
2260
|
+
}
|
|
2261
|
+
//#endregion
|
|
2248
2262
|
//#region src/stream-broker/rtsp/rtsp-types.ts
|
|
2249
2263
|
/** RTSP method constants. */
|
|
2250
2264
|
var RTSP_METHODS = [
|
|
@@ -2871,8 +2885,6 @@ var RtspRestreamer = class {
|
|
|
2871
2885
|
if (ready.length === 0) return;
|
|
2872
2886
|
const preRollPlans = /* @__PURE__ */ new Map();
|
|
2873
2887
|
const bound = this.currentPrimeBound();
|
|
2874
|
-
const overSpan = spanMs > bound.maxSpanMs;
|
|
2875
|
-
const overPackets = primePackets.length > bound.maxPackets;
|
|
2876
2888
|
/**
|
|
2877
2889
|
* Resolve (and memoize) the plan for one intent.
|
|
2878
2890
|
*
|
|
@@ -2938,14 +2950,18 @@ var RtspRestreamer = class {
|
|
|
2938
2950
|
continue;
|
|
2939
2951
|
}
|
|
2940
2952
|
}
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2953
|
+
const withhold = admitJoinBurst({
|
|
2954
|
+
muted: session.isMuted(),
|
|
2955
|
+
detectionIntent,
|
|
2956
|
+
fromRing,
|
|
2957
|
+
spanMs,
|
|
2958
|
+
packets: primePackets.length,
|
|
2959
|
+
bound,
|
|
2960
|
+
liveEdgeMaxSpanMs: LIVE_EDGE_MAX_SPAN_MS
|
|
2961
|
+
});
|
|
2962
|
+
if (withhold !== "none") {
|
|
2963
|
+
if (withhold === "live-edge") this.dropsWithheldMutedLiveEdge++;
|
|
2964
|
+
else if (withhold === "span") this.dropsWithheldSpan++;
|
|
2949
2965
|
else this.dropsWithheldPackets++;
|
|
2950
2966
|
withheld++;
|
|
2951
2967
|
continue;
|
|
@@ -4131,6 +4147,90 @@ function buildAudioCodecCapApi(api) {
|
|
|
4131
4147
|
flushEncode: (input) => router.flushEncode.mutate(input)
|
|
4132
4148
|
};
|
|
4133
4149
|
}
|
|
4150
|
+
//#endregion
|
|
4151
|
+
//#region src/stream-broker/stream-broker/catalog-pull-backoff.ts
|
|
4152
|
+
/**
|
|
4153
|
+
* CATALOG-PULL BACKOFF — how often the broker asks a camera whose
|
|
4154
|
+
* `stream-catalog` pull keeps failing.
|
|
4155
|
+
*
|
|
4156
|
+
* ## What this exists to stop
|
|
4157
|
+
*
|
|
4158
|
+
* 2026-08-25, device 3629 ("Videocamera porta retro", Reolink). Its Baichuan
|
|
4159
|
+
* session expired under a socket that stayed open, so every
|
|
4160
|
+
* `stream-catalog.getCatalog` answered `buildVideoStreamOptions failed …
|
|
4161
|
+
* responseCode 400`. The reconcile poll asked again 30 s later. And again. For
|
|
4162
|
+
* 35 minutes, ~70 identical failures, each one a login attempt, a 10 s timeout
|
|
4163
|
+
* budget and a `debug` line nobody was reading.
|
|
4164
|
+
*
|
|
4165
|
+
* The fleet was never at risk from that camera — `reconcileAllCatalogs` fans
|
|
4166
|
+
* out with `Promise.allSettled` and `reconcileDeviceCatalogNow` catches its own
|
|
4167
|
+
* pull, so exactly one device was omitted (that isolation is frozen by
|
|
4168
|
+
* `scripts/check-catalog-per-device-isolation.ts`). What was wrong is that the
|
|
4169
|
+
* omission was invisible and the retry was a flat 30 s hammer that could not
|
|
4170
|
+
* have recovered anyway.
|
|
4171
|
+
*
|
|
4172
|
+
* ## The shape
|
|
4173
|
+
*
|
|
4174
|
+
* The ladder is {@link BOOT_RECOVERY_BACKOFF_MS} — the repo's ONE retry
|
|
4175
|
+
* schedule for work that can fail for reasons that pass: two cheap attempts
|
|
4176
|
+
* (15 s, 60 s) for a blip, then the long tail (5 m → 90 m) for a camera that
|
|
4177
|
+
* is genuinely gone.
|
|
4178
|
+
*
|
|
4179
|
+
* ## The one place this DIVERGES from boot recovery, and why
|
|
4180
|
+
*
|
|
4181
|
+
* The boot ladder ENDS: it runs out and manual re-arm is the only path. This
|
|
4182
|
+
* one CLAMPS at the last rung and keeps asking every 90 minutes forever.
|
|
4183
|
+
*
|
|
4184
|
+
* Boot recovery can end because a human is present — a node that just booted
|
|
4185
|
+
* wrong is a node someone is looking at. A camera whose session expired at
|
|
4186
|
+
* 03:00 has nobody. The reconcile poll is the ONLY thing that will ever ask it
|
|
4187
|
+
* again: no event fires when a camera silently starts working, and a device
|
|
4188
|
+
* that never re-enters the catalog is a device with no broker, no recording
|
|
4189
|
+
* and no detection, permanently. Ending the chain would convert a transient
|
|
4190
|
+
* session expiry into a permanent outage — precisely the failure it is meant
|
|
4191
|
+
* to bound. A 90-minute floor is not a spin: it is ~16 pulls a day for one
|
|
4192
|
+
* broken camera, against 2 880 before this.
|
|
4193
|
+
*/
|
|
4194
|
+
/**
|
|
4195
|
+
* The rung the ladder settles on. Read off the shared schedule rather than
|
|
4196
|
+
* written down twice; the literal only keeps the expression total for a
|
|
4197
|
+
* hypothetically empty ladder, and is not the clamp.
|
|
4198
|
+
*/
|
|
4199
|
+
var LAST_RUNG_MS = require_dist.BOOT_RECOVERY_BACKOFF_MS[require_dist.BOOT_RECOVERY_BACKOFF_MS.length - 1] ?? 90 * 6e4;
|
|
4200
|
+
/**
|
|
4201
|
+
* Delay before the next poll-driven attempt, given how many pulls in a row
|
|
4202
|
+
* have now failed (1 = the first failure).
|
|
4203
|
+
*
|
|
4204
|
+
* Clamped at the last rung rather than exhausted — see the module doc.
|
|
4205
|
+
*/
|
|
4206
|
+
function catalogPullRetryDelayMs(consecutiveFailures) {
|
|
4207
|
+
return require_dist.BOOT_RECOVERY_BACKOFF_MS[Math.max(consecutiveFailures, 1) - 1] ?? LAST_RUNG_MS;
|
|
4208
|
+
}
|
|
4209
|
+
/**
|
|
4210
|
+
* Advance the failure memory after one more failed pull.
|
|
4211
|
+
*
|
|
4212
|
+
* @param previous state before this failure, or `undefined` for the first one
|
|
4213
|
+
* @param nowMs wall clock at the moment of the failure
|
|
4214
|
+
*/
|
|
4215
|
+
function nextCatalogPullFailureState(previous, nowMs) {
|
|
4216
|
+
const consecutiveFailures = (previous?.consecutiveFailures ?? 0) + 1;
|
|
4217
|
+
return {
|
|
4218
|
+
consecutiveFailures,
|
|
4219
|
+
nextAttemptAtMs: nowMs + catalogPullRetryDelayMs(consecutiveFailures)
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
/**
|
|
4223
|
+
* May a POLL-driven pull run for this device right now?
|
|
4224
|
+
*
|
|
4225
|
+
* Only the poll is gated. An event-driven reconcile (device registered,
|
|
4226
|
+
* stream params changed) is EVIDENCE that something moved and always
|
|
4227
|
+
* attempts — a backoff that ignores new information is just a slower way of
|
|
4228
|
+
* being wrong.
|
|
4229
|
+
*/
|
|
4230
|
+
function mayAttemptPolledCatalogPull(state, nowMs) {
|
|
4231
|
+
if (state === void 0) return true;
|
|
4232
|
+
return nowMs >= state.nextAttemptAtMs;
|
|
4233
|
+
}
|
|
4134
4234
|
/**
|
|
4135
4235
|
* Minimum gap between two re-dials of the same device. A re-dial costs a
|
|
4136
4236
|
* ~2 s video interruption on every source of the camera; at the 30 s
|
|
@@ -14645,6 +14745,13 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
14645
14745
|
deviceOverridePersister = null;
|
|
14646
14746
|
profileMapPersister = null;
|
|
14647
14747
|
deviceCatalogOperations = /* @__PURE__ */ new Map();
|
|
14748
|
+
/**
|
|
14749
|
+
* Devices whose `stream-catalog` pull is currently failing, and when the
|
|
14750
|
+
* POLL may ask again. Absent = healthy; an entry is deleted the moment a
|
|
14751
|
+
* pull succeeds. See `catalog-pull-backoff.ts` for the ladder and for the
|
|
14752
|
+
* outage that put it here.
|
|
14753
|
+
*/
|
|
14754
|
+
catalogPullFailures = /* @__PURE__ */ new Map();
|
|
14648
14755
|
eventBus = null;
|
|
14649
14756
|
/**
|
|
14650
14757
|
* Per-broker stream health flag set by the watchdog. `undefined`
|
|
@@ -16196,26 +16303,54 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
16196
16303
|
/**
|
|
16197
16304
|
* Pull one camera's `stream-catalog` and reconcile its cam-streams: upsert
|
|
16198
16305
|
* every descriptor, retract any cam-stream no longer in the catalog. A pull
|
|
16199
|
-
* that throws (provider mid-restart, no `stream-catalog` cap
|
|
16200
|
-
*
|
|
16201
|
-
*
|
|
16202
|
-
*
|
|
16306
|
+
* that throws (provider mid-restart, no `stream-catalog` cap, a camera whose
|
|
16307
|
+
* session died) omits THIS DEVICE ONLY — it is logged at `warn` with the
|
|
16308
|
+
* device's id and re-asked on the `catalog-pull-backoff.ts` ladder. An EMPTY
|
|
16309
|
+
* catalog is also skipped rather than tearing down (a provider probe can
|
|
16310
|
+
* transiently return none); genuine removal flows through the
|
|
16311
|
+
* device-unregistered path.
|
|
16312
|
+
*
|
|
16313
|
+
* `trigger` decides whether the backoff applies — see
|
|
16314
|
+
* {@link CatalogReconcileTrigger}. It defaults to `'event'` (always attempt)
|
|
16315
|
+
* so only the caller that actually repeats forever pays the throttle.
|
|
16203
16316
|
*/
|
|
16204
|
-
async reconcileDeviceCatalog(deviceId) {
|
|
16205
|
-
await this.runDeviceCatalogOperation(deviceId, () => this.reconcileDeviceCatalogNow(deviceId));
|
|
16317
|
+
async reconcileDeviceCatalog(deviceId, trigger = "event") {
|
|
16318
|
+
await this.runDeviceCatalogOperation(deviceId, () => this.reconcileDeviceCatalogNow(deviceId, trigger));
|
|
16206
16319
|
}
|
|
16207
|
-
async reconcileDeviceCatalogNow(deviceId) {
|
|
16320
|
+
async reconcileDeviceCatalogNow(deviceId, trigger) {
|
|
16208
16321
|
if (!this.api) return;
|
|
16322
|
+
const failure = this.catalogPullFailures.get(deviceId);
|
|
16323
|
+
if (trigger === "poll" && !mayAttemptPolledCatalogPull(failure, Date.now())) {
|
|
16324
|
+
this.logger.debug("reconcileDeviceCatalog: skipped — device is on catalog-pull backoff", {
|
|
16325
|
+
tags: { deviceId },
|
|
16326
|
+
meta: {
|
|
16327
|
+
consecutiveFailures: failure?.consecutiveFailures ?? 0,
|
|
16328
|
+
nextAttemptInMs: Math.max(0, (failure?.nextAttemptAtMs ?? 0) - Date.now())
|
|
16329
|
+
}
|
|
16330
|
+
});
|
|
16331
|
+
return;
|
|
16332
|
+
}
|
|
16209
16333
|
let descriptors;
|
|
16210
16334
|
try {
|
|
16211
16335
|
descriptors = await this.withCatalogTimeout(this.api.streamCatalog.getCatalog.query({ deviceId }), deviceId) ?? [];
|
|
16212
16336
|
} catch (err) {
|
|
16213
|
-
this.
|
|
16337
|
+
const next = nextCatalogPullFailureState(this.catalogPullFailures.get(deviceId), Date.now());
|
|
16338
|
+
this.catalogPullFailures.set(deviceId, next);
|
|
16339
|
+
this.logger.warn("stream-catalog pull failed — device omitted from this catalog reconcile", {
|
|
16214
16340
|
tags: { deviceId },
|
|
16215
|
-
meta: {
|
|
16341
|
+
meta: {
|
|
16342
|
+
trigger,
|
|
16343
|
+
consecutiveFailures: next.consecutiveFailures,
|
|
16344
|
+
retryInMs: catalogPullRetryDelayMs(next.consecutiveFailures),
|
|
16345
|
+
error: require_dist.errMsg(err)
|
|
16346
|
+
}
|
|
16216
16347
|
});
|
|
16217
16348
|
return;
|
|
16218
16349
|
}
|
|
16350
|
+
if (this.catalogPullFailures.delete(deviceId)) this.logger.info("stream-catalog pull recovered — device is back in the catalog", {
|
|
16351
|
+
tags: { deviceId },
|
|
16352
|
+
meta: { descriptors: descriptors.length }
|
|
16353
|
+
});
|
|
16219
16354
|
if (descriptors.length === 0) return;
|
|
16220
16355
|
const keep = new Set(descriptors.map((d) => d.camStreamId));
|
|
16221
16356
|
await this.publishCatalog(deviceId, descriptors);
|
|
@@ -16250,10 +16385,11 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
16250
16385
|
await this.releaseDisabledDeviceBrokers(d.id, firstSeen);
|
|
16251
16386
|
} else this.disabledDevices.delete(d.id);
|
|
16252
16387
|
const live = cameras.filter((d) => !d.disabled);
|
|
16253
|
-
await Promise.allSettled(live.map((d) => this.reconcileDeviceCatalog(d.id)));
|
|
16388
|
+
await Promise.allSettled(live.map((d) => this.reconcileDeviceCatalog(d.id, "poll")));
|
|
16254
16389
|
await Promise.allSettled(live.map((d) => this.reconcileDeviceAudioPlane(d.id)));
|
|
16255
16390
|
const liveIds = new Set(live.map((d) => d.id));
|
|
16256
16391
|
for (const deviceId of [...this.audioPlaneState.keys()]) if (!liveIds.has(deviceId)) this.audioPlaneState.delete(deviceId);
|
|
16392
|
+
for (const deviceId of [...this.catalogPullFailures.keys()]) if (!liveIds.has(deviceId)) this.catalogPullFailures.delete(deviceId);
|
|
16257
16393
|
}
|
|
16258
16394
|
/**
|
|
16259
16395
|
* ONE device's audio-plane reconcile pass. See `audio-plane-reconcile.ts`
|
|
@@ -16395,6 +16531,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
16395
16531
|
await this.runDeviceCatalogOperation(deviceId, () => this.retractDeviceNow(deviceId, opts));
|
|
16396
16532
|
}
|
|
16397
16533
|
async retractDeviceNow(deviceId, opts) {
|
|
16534
|
+
this.catalogPullFailures.delete(deviceId);
|
|
16398
16535
|
const existing = this.cameraStreams.get(deviceId);
|
|
16399
16536
|
if (existing) for (const camStreamId of Array.from(existing.keys())) await this.retractCameraStream({
|
|
16400
16537
|
deviceId,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { $ as loadContributionCapability, $t as
|
|
3
|
-
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-
|
|
2
|
+
import { $ as loadContributionCapability, $t as string, At as BaseAddon, C as RATE_CONTROL_RELAXED, Ct as errMsg, D as RingBuffer, Dt as invocationFromEncodeProfile, Et as buildFfmpegArgs$1, F as createHwAccelCache, G as egressTranscodeSharingKey, Gt as array, I as createLogChannelsProvider, It as isEvent, K as egressTransportFromRequest, Kt as boolean, Lt as makeProfileBrokerId, Mt as DeviceFeature, N as batteryCapability, Nt as DeviceType, Ot as isSoftwareDecode, P as cameraStreamsCapability, Pt as createEvent, Qt as record, R as declareLogChannel, Rt as makeSourceBrokerId, St as webrtcSessionCapability, V as deriveBatteryPresence, Vt as parseProfileBrokerId, Wt as _enum, Xt as number, Yt as literal, Zt as object, bt as streamBrokerCapability, en as union, et as logChannelsCapability, g as EncodeProfileSchema, ht as resolveEgressDecodeHwAccel, i as BatteryStatusSchema, jt as CAM_PROFILE_ORDER, k as addonWidgetsSourceCapability, kt as BOOT_RECOVERY_BACKOFF_MS, nt as maskUrlCredentials, qt as discriminatedUnion, tn as EventCategory, w as RATE_CONTROL_TIGHT, wt as AUDIO_PRESETS, zt as nodePin } from "../dist-CmEYRK7G.mjs";
|
|
3
|
+
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-J07GBWT3.mjs";
|
|
4
4
|
import { t as parseRestreamPath } from "../restream-intent-B4BXZra7.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-DMolfhsP.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-Ck9ly4bX.mjs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import * as crypto$1 from "node:crypto";
|
|
10
10
|
import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
@@ -2242,6 +2242,20 @@ function appendAudioMediaBlock(videoSdp, audioBlock) {
|
|
|
2242
2242
|
return `${videoSdp.replace(/[\r\n]+$/, "")}\r\n${audioBlock}\r\n`;
|
|
2243
2243
|
}
|
|
2244
2244
|
//#endregion
|
|
2245
|
+
//#region src/stream-broker/rtsp/join-burst-admission.ts
|
|
2246
|
+
/**
|
|
2247
|
+
* Decide whether this session may take this burst.
|
|
2248
|
+
*
|
|
2249
|
+
* Pure and total: no clock, no logger, no restreamer state. The restreamer maps
|
|
2250
|
+
* the result onto its counters and its `continue`.
|
|
2251
|
+
*/
|
|
2252
|
+
function admitJoinBurst(input) {
|
|
2253
|
+
if (input.muted && !input.detectionIntent) return !input.fromRing || input.spanMs > input.liveEdgeMaxSpanMs ? "live-edge" : "none";
|
|
2254
|
+
if (!input.detectionIntent && input.spanMs > input.bound.maxSpanMs) return "span";
|
|
2255
|
+
if (input.packets > input.bound.maxPackets) return "packets";
|
|
2256
|
+
return "none";
|
|
2257
|
+
}
|
|
2258
|
+
//#endregion
|
|
2245
2259
|
//#region src/stream-broker/rtsp/rtsp-types.ts
|
|
2246
2260
|
/** RTSP method constants. */
|
|
2247
2261
|
var RTSP_METHODS = [
|
|
@@ -2868,8 +2882,6 @@ var RtspRestreamer = class {
|
|
|
2868
2882
|
if (ready.length === 0) return;
|
|
2869
2883
|
const preRollPlans = /* @__PURE__ */ new Map();
|
|
2870
2884
|
const bound = this.currentPrimeBound();
|
|
2871
|
-
const overSpan = spanMs > bound.maxSpanMs;
|
|
2872
|
-
const overPackets = primePackets.length > bound.maxPackets;
|
|
2873
2885
|
/**
|
|
2874
2886
|
* Resolve (and memoize) the plan for one intent.
|
|
2875
2887
|
*
|
|
@@ -2935,14 +2947,18 @@ var RtspRestreamer = class {
|
|
|
2935
2947
|
continue;
|
|
2936
2948
|
}
|
|
2937
2949
|
}
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2950
|
+
const withhold = admitJoinBurst({
|
|
2951
|
+
muted: session.isMuted(),
|
|
2952
|
+
detectionIntent,
|
|
2953
|
+
fromRing,
|
|
2954
|
+
spanMs,
|
|
2955
|
+
packets: primePackets.length,
|
|
2956
|
+
bound,
|
|
2957
|
+
liveEdgeMaxSpanMs: LIVE_EDGE_MAX_SPAN_MS
|
|
2958
|
+
});
|
|
2959
|
+
if (withhold !== "none") {
|
|
2960
|
+
if (withhold === "live-edge") this.dropsWithheldMutedLiveEdge++;
|
|
2961
|
+
else if (withhold === "span") this.dropsWithheldSpan++;
|
|
2946
2962
|
else this.dropsWithheldPackets++;
|
|
2947
2963
|
withheld++;
|
|
2948
2964
|
continue;
|
|
@@ -4128,6 +4144,90 @@ function buildAudioCodecCapApi(api) {
|
|
|
4128
4144
|
flushEncode: (input) => router.flushEncode.mutate(input)
|
|
4129
4145
|
};
|
|
4130
4146
|
}
|
|
4147
|
+
//#endregion
|
|
4148
|
+
//#region src/stream-broker/stream-broker/catalog-pull-backoff.ts
|
|
4149
|
+
/**
|
|
4150
|
+
* CATALOG-PULL BACKOFF — how often the broker asks a camera whose
|
|
4151
|
+
* `stream-catalog` pull keeps failing.
|
|
4152
|
+
*
|
|
4153
|
+
* ## What this exists to stop
|
|
4154
|
+
*
|
|
4155
|
+
* 2026-08-25, device 3629 ("Videocamera porta retro", Reolink). Its Baichuan
|
|
4156
|
+
* session expired under a socket that stayed open, so every
|
|
4157
|
+
* `stream-catalog.getCatalog` answered `buildVideoStreamOptions failed …
|
|
4158
|
+
* responseCode 400`. The reconcile poll asked again 30 s later. And again. For
|
|
4159
|
+
* 35 minutes, ~70 identical failures, each one a login attempt, a 10 s timeout
|
|
4160
|
+
* budget and a `debug` line nobody was reading.
|
|
4161
|
+
*
|
|
4162
|
+
* The fleet was never at risk from that camera — `reconcileAllCatalogs` fans
|
|
4163
|
+
* out with `Promise.allSettled` and `reconcileDeviceCatalogNow` catches its own
|
|
4164
|
+
* pull, so exactly one device was omitted (that isolation is frozen by
|
|
4165
|
+
* `scripts/check-catalog-per-device-isolation.ts`). What was wrong is that the
|
|
4166
|
+
* omission was invisible and the retry was a flat 30 s hammer that could not
|
|
4167
|
+
* have recovered anyway.
|
|
4168
|
+
*
|
|
4169
|
+
* ## The shape
|
|
4170
|
+
*
|
|
4171
|
+
* The ladder is {@link BOOT_RECOVERY_BACKOFF_MS} — the repo's ONE retry
|
|
4172
|
+
* schedule for work that can fail for reasons that pass: two cheap attempts
|
|
4173
|
+
* (15 s, 60 s) for a blip, then the long tail (5 m → 90 m) for a camera that
|
|
4174
|
+
* is genuinely gone.
|
|
4175
|
+
*
|
|
4176
|
+
* ## The one place this DIVERGES from boot recovery, and why
|
|
4177
|
+
*
|
|
4178
|
+
* The boot ladder ENDS: it runs out and manual re-arm is the only path. This
|
|
4179
|
+
* one CLAMPS at the last rung and keeps asking every 90 minutes forever.
|
|
4180
|
+
*
|
|
4181
|
+
* Boot recovery can end because a human is present — a node that just booted
|
|
4182
|
+
* wrong is a node someone is looking at. A camera whose session expired at
|
|
4183
|
+
* 03:00 has nobody. The reconcile poll is the ONLY thing that will ever ask it
|
|
4184
|
+
* again: no event fires when a camera silently starts working, and a device
|
|
4185
|
+
* that never re-enters the catalog is a device with no broker, no recording
|
|
4186
|
+
* and no detection, permanently. Ending the chain would convert a transient
|
|
4187
|
+
* session expiry into a permanent outage — precisely the failure it is meant
|
|
4188
|
+
* to bound. A 90-minute floor is not a spin: it is ~16 pulls a day for one
|
|
4189
|
+
* broken camera, against 2 880 before this.
|
|
4190
|
+
*/
|
|
4191
|
+
/**
|
|
4192
|
+
* The rung the ladder settles on. Read off the shared schedule rather than
|
|
4193
|
+
* written down twice; the literal only keeps the expression total for a
|
|
4194
|
+
* hypothetically empty ladder, and is not the clamp.
|
|
4195
|
+
*/
|
|
4196
|
+
var LAST_RUNG_MS = BOOT_RECOVERY_BACKOFF_MS[BOOT_RECOVERY_BACKOFF_MS.length - 1] ?? 90 * 6e4;
|
|
4197
|
+
/**
|
|
4198
|
+
* Delay before the next poll-driven attempt, given how many pulls in a row
|
|
4199
|
+
* have now failed (1 = the first failure).
|
|
4200
|
+
*
|
|
4201
|
+
* Clamped at the last rung rather than exhausted — see the module doc.
|
|
4202
|
+
*/
|
|
4203
|
+
function catalogPullRetryDelayMs(consecutiveFailures) {
|
|
4204
|
+
return BOOT_RECOVERY_BACKOFF_MS[Math.max(consecutiveFailures, 1) - 1] ?? LAST_RUNG_MS;
|
|
4205
|
+
}
|
|
4206
|
+
/**
|
|
4207
|
+
* Advance the failure memory after one more failed pull.
|
|
4208
|
+
*
|
|
4209
|
+
* @param previous state before this failure, or `undefined` for the first one
|
|
4210
|
+
* @param nowMs wall clock at the moment of the failure
|
|
4211
|
+
*/
|
|
4212
|
+
function nextCatalogPullFailureState(previous, nowMs) {
|
|
4213
|
+
const consecutiveFailures = (previous?.consecutiveFailures ?? 0) + 1;
|
|
4214
|
+
return {
|
|
4215
|
+
consecutiveFailures,
|
|
4216
|
+
nextAttemptAtMs: nowMs + catalogPullRetryDelayMs(consecutiveFailures)
|
|
4217
|
+
};
|
|
4218
|
+
}
|
|
4219
|
+
/**
|
|
4220
|
+
* May a POLL-driven pull run for this device right now?
|
|
4221
|
+
*
|
|
4222
|
+
* Only the poll is gated. An event-driven reconcile (device registered,
|
|
4223
|
+
* stream params changed) is EVIDENCE that something moved and always
|
|
4224
|
+
* attempts — a backoff that ignores new information is just a slower way of
|
|
4225
|
+
* being wrong.
|
|
4226
|
+
*/
|
|
4227
|
+
function mayAttemptPolledCatalogPull(state, nowMs) {
|
|
4228
|
+
if (state === void 0) return true;
|
|
4229
|
+
return nowMs >= state.nextAttemptAtMs;
|
|
4230
|
+
}
|
|
4131
4231
|
/**
|
|
4132
4232
|
* Minimum gap between two re-dials of the same device. A re-dial costs a
|
|
4133
4233
|
* ~2 s video interruption on every source of the camera; at the 30 s
|
|
@@ -14642,6 +14742,13 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
14642
14742
|
deviceOverridePersister = null;
|
|
14643
14743
|
profileMapPersister = null;
|
|
14644
14744
|
deviceCatalogOperations = /* @__PURE__ */ new Map();
|
|
14745
|
+
/**
|
|
14746
|
+
* Devices whose `stream-catalog` pull is currently failing, and when the
|
|
14747
|
+
* POLL may ask again. Absent = healthy; an entry is deleted the moment a
|
|
14748
|
+
* pull succeeds. See `catalog-pull-backoff.ts` for the ladder and for the
|
|
14749
|
+
* outage that put it here.
|
|
14750
|
+
*/
|
|
14751
|
+
catalogPullFailures = /* @__PURE__ */ new Map();
|
|
14645
14752
|
eventBus = null;
|
|
14646
14753
|
/**
|
|
14647
14754
|
* Per-broker stream health flag set by the watchdog. `undefined`
|
|
@@ -16193,26 +16300,54 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
16193
16300
|
/**
|
|
16194
16301
|
* Pull one camera's `stream-catalog` and reconcile its cam-streams: upsert
|
|
16195
16302
|
* every descriptor, retract any cam-stream no longer in the catalog. A pull
|
|
16196
|
-
* that throws (provider mid-restart, no `stream-catalog` cap
|
|
16197
|
-
*
|
|
16198
|
-
*
|
|
16199
|
-
*
|
|
16303
|
+
* that throws (provider mid-restart, no `stream-catalog` cap, a camera whose
|
|
16304
|
+
* session died) omits THIS DEVICE ONLY — it is logged at `warn` with the
|
|
16305
|
+
* device's id and re-asked on the `catalog-pull-backoff.ts` ladder. An EMPTY
|
|
16306
|
+
* catalog is also skipped rather than tearing down (a provider probe can
|
|
16307
|
+
* transiently return none); genuine removal flows through the
|
|
16308
|
+
* device-unregistered path.
|
|
16309
|
+
*
|
|
16310
|
+
* `trigger` decides whether the backoff applies — see
|
|
16311
|
+
* {@link CatalogReconcileTrigger}. It defaults to `'event'` (always attempt)
|
|
16312
|
+
* so only the caller that actually repeats forever pays the throttle.
|
|
16200
16313
|
*/
|
|
16201
|
-
async reconcileDeviceCatalog(deviceId) {
|
|
16202
|
-
await this.runDeviceCatalogOperation(deviceId, () => this.reconcileDeviceCatalogNow(deviceId));
|
|
16314
|
+
async reconcileDeviceCatalog(deviceId, trigger = "event") {
|
|
16315
|
+
await this.runDeviceCatalogOperation(deviceId, () => this.reconcileDeviceCatalogNow(deviceId, trigger));
|
|
16203
16316
|
}
|
|
16204
|
-
async reconcileDeviceCatalogNow(deviceId) {
|
|
16317
|
+
async reconcileDeviceCatalogNow(deviceId, trigger) {
|
|
16205
16318
|
if (!this.api) return;
|
|
16319
|
+
const failure = this.catalogPullFailures.get(deviceId);
|
|
16320
|
+
if (trigger === "poll" && !mayAttemptPolledCatalogPull(failure, Date.now())) {
|
|
16321
|
+
this.logger.debug("reconcileDeviceCatalog: skipped — device is on catalog-pull backoff", {
|
|
16322
|
+
tags: { deviceId },
|
|
16323
|
+
meta: {
|
|
16324
|
+
consecutiveFailures: failure?.consecutiveFailures ?? 0,
|
|
16325
|
+
nextAttemptInMs: Math.max(0, (failure?.nextAttemptAtMs ?? 0) - Date.now())
|
|
16326
|
+
}
|
|
16327
|
+
});
|
|
16328
|
+
return;
|
|
16329
|
+
}
|
|
16206
16330
|
let descriptors;
|
|
16207
16331
|
try {
|
|
16208
16332
|
descriptors = await this.withCatalogTimeout(this.api.streamCatalog.getCatalog.query({ deviceId }), deviceId) ?? [];
|
|
16209
16333
|
} catch (err) {
|
|
16210
|
-
this.
|
|
16334
|
+
const next = nextCatalogPullFailureState(this.catalogPullFailures.get(deviceId), Date.now());
|
|
16335
|
+
this.catalogPullFailures.set(deviceId, next);
|
|
16336
|
+
this.logger.warn("stream-catalog pull failed — device omitted from this catalog reconcile", {
|
|
16211
16337
|
tags: { deviceId },
|
|
16212
|
-
meta: {
|
|
16338
|
+
meta: {
|
|
16339
|
+
trigger,
|
|
16340
|
+
consecutiveFailures: next.consecutiveFailures,
|
|
16341
|
+
retryInMs: catalogPullRetryDelayMs(next.consecutiveFailures),
|
|
16342
|
+
error: errMsg(err)
|
|
16343
|
+
}
|
|
16213
16344
|
});
|
|
16214
16345
|
return;
|
|
16215
16346
|
}
|
|
16347
|
+
if (this.catalogPullFailures.delete(deviceId)) this.logger.info("stream-catalog pull recovered — device is back in the catalog", {
|
|
16348
|
+
tags: { deviceId },
|
|
16349
|
+
meta: { descriptors: descriptors.length }
|
|
16350
|
+
});
|
|
16216
16351
|
if (descriptors.length === 0) return;
|
|
16217
16352
|
const keep = new Set(descriptors.map((d) => d.camStreamId));
|
|
16218
16353
|
await this.publishCatalog(deviceId, descriptors);
|
|
@@ -16247,10 +16382,11 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
16247
16382
|
await this.releaseDisabledDeviceBrokers(d.id, firstSeen);
|
|
16248
16383
|
} else this.disabledDevices.delete(d.id);
|
|
16249
16384
|
const live = cameras.filter((d) => !d.disabled);
|
|
16250
|
-
await Promise.allSettled(live.map((d) => this.reconcileDeviceCatalog(d.id)));
|
|
16385
|
+
await Promise.allSettled(live.map((d) => this.reconcileDeviceCatalog(d.id, "poll")));
|
|
16251
16386
|
await Promise.allSettled(live.map((d) => this.reconcileDeviceAudioPlane(d.id)));
|
|
16252
16387
|
const liveIds = new Set(live.map((d) => d.id));
|
|
16253
16388
|
for (const deviceId of [...this.audioPlaneState.keys()]) if (!liveIds.has(deviceId)) this.audioPlaneState.delete(deviceId);
|
|
16389
|
+
for (const deviceId of [...this.catalogPullFailures.keys()]) if (!liveIds.has(deviceId)) this.catalogPullFailures.delete(deviceId);
|
|
16254
16390
|
}
|
|
16255
16391
|
/**
|
|
16256
16392
|
* ONE device's audio-plane reconcile pass. See `audio-plane-reconcile.ts`
|
|
@@ -16392,6 +16528,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
16392
16528
|
await this.runDeviceCatalogOperation(deviceId, () => this.retractDeviceNow(deviceId, opts));
|
|
16393
16529
|
}
|
|
16394
16530
|
async retractDeviceNow(deviceId, opts) {
|
|
16531
|
+
this.catalogPullFailures.delete(deviceId);
|
|
16395
16532
|
const existing = this.cameraStreams.get(deviceId);
|
|
16396
16533
|
if (existing) for (const camStreamId of Array.from(existing.keys())) await this.retractCameraStream({
|
|
16397
16534
|
deviceId,
|
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cad8960O.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_dist = require("./dist-
|
|
1
|
+
const require_dist = require("./dist-BuIP0499.js");
|
|
2
2
|
//#region src/session-decode/native-lease-config.ts
|
|
3
3
|
/**
|
|
4
4
|
* Resolution of the native-frame hold / tile knobs for one decode worker.
|
|
@@ -253,6 +253,7 @@ function isWorkerReply(value) {
|
|
|
253
253
|
case "ended": return true;
|
|
254
254
|
case "error": return typeof value.message === "string" && (value.frameId === void 0 || isFiniteNumber(value.frameId));
|
|
255
255
|
case "log": return isWorkerLogLevel(value.level) && typeof value.message === "string";
|
|
256
|
+
case "dialTiming": return isFiniteNumber(value.hwContextMs) && isFiniteNumber(value.demuxerOpenMs) && isFiniteNumber(value.decoderCreateMs) && isFiniteNumber(value.firstDecodeMs) && typeof value.streamInfoProbed === "boolean";
|
|
256
257
|
default: return false;
|
|
257
258
|
}
|
|
258
259
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { b as NativeLeaseSettingsSchema, p as DEFAULT_NATIVE_LEASE_SETTINGS, y as NativeLeaseAdmissionSchema } from "./dist-
|
|
1
|
+
import { b as NativeLeaseSettingsSchema, p as DEFAULT_NATIVE_LEASE_SETTINGS, y as NativeLeaseAdmissionSchema } from "./dist-CmEYRK7G.mjs";
|
|
2
2
|
//#region src/session-decode/native-lease-config.ts
|
|
3
3
|
/**
|
|
4
4
|
* Resolution of the native-frame hold / tile knobs for one decode worker.
|
|
@@ -253,6 +253,7 @@ function isWorkerReply(value) {
|
|
|
253
253
|
case "ended": return true;
|
|
254
254
|
case "error": return typeof value.message === "string" && (value.frameId === void 0 || isFiniteNumber(value.frameId));
|
|
255
255
|
case "log": return isWorkerLogLevel(value.level) && typeof value.message === "string";
|
|
256
|
+
case "dialTiming": return isFiniteNumber(value.hwContextMs) && isFiniteNumber(value.demuxerOpenMs) && isFiniteNumber(value.decoderCreateMs) && isFiniteNumber(value.firstDecodeMs) && typeof value.streamInfoProbed === "boolean";
|
|
256
257
|
default: return false;
|
|
257
258
|
}
|
|
258
259
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.138",
|
|
4
4
|
"description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -109,7 +109,8 @@
|
|
|
109
109
|
"execution": {
|
|
110
110
|
"placement": "any-node",
|
|
111
111
|
"heapProfile": "heavy",
|
|
112
|
-
"group": "detection"
|
|
112
|
+
"group": "detection",
|
|
113
|
+
"rssBudgetMb": 1536
|
|
113
114
|
},
|
|
114
115
|
"protected": true,
|
|
115
116
|
"icon": "assets/icon.svg",
|
|
@@ -152,7 +153,8 @@
|
|
|
152
153
|
"entry": "./dist/audio-analyzer/index.js",
|
|
153
154
|
"execution": {
|
|
154
155
|
"placement": "any-node",
|
|
155
|
-
"heapProfile": "heavy"
|
|
156
|
+
"heapProfile": "heavy",
|
|
157
|
+
"rssBudgetMb": 512
|
|
156
158
|
},
|
|
157
159
|
"capabilities": [
|
|
158
160
|
{
|