@camstack/addon-pipeline 1.2.310 → 1.2.312
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/pipeline-runner/index.js +28 -1
- package/dist/pipeline-runner/index.mjs +28 -1
- package/dist/stream-broker/index.js +137 -13
- package/dist/stream-broker/index.mjs +137 -13
- package/package.json +1 -1
|
@@ -3885,6 +3885,24 @@ function isParkedDetailFresh(parked, nowMs) {
|
|
|
3885
3885
|
function buildDetailNotAttachedError(deviceId) {
|
|
3886
3886
|
return /* @__PURE__ */ new Error(`${DETAIL_NOT_ATTACHED_MARKER} — camera ${deviceId} is not attached and no detail state is parked within the grace window`);
|
|
3887
3887
|
}
|
|
3888
|
+
/**
|
|
3889
|
+
* The wire marker for the typed handle-expired refusal (D688): the frame handle
|
|
3890
|
+
* a detail dispatch named is past the native lease age bound, so every later
|
|
3891
|
+
* ask for it is refused too. Twin constant in
|
|
3892
|
+
* `addon-post-analysis/src/pipeline-analytics/detail-refusal.ts` — keep in sync.
|
|
3893
|
+
* An error rather than a result field for the same reason as
|
|
3894
|
+
* {@link DETAIL_NOT_ATTACHED_MARKER}: the cap output schema strips unknown keys.
|
|
3895
|
+
*/
|
|
3896
|
+
var DETAIL_HANDLE_EXPIRED_MARKER = "runDetailSubtree-refused:handle-expired";
|
|
3897
|
+
/**
|
|
3898
|
+
* The error the cap method throws when the dispatch's only pixel source was a
|
|
3899
|
+
* frame handle the lease registry refused as expired. The dispatcher recognises
|
|
3900
|
+
* the marker, retires the handle for that track and falls back to its parked
|
|
3901
|
+
* frame — it never names the same handle again.
|
|
3902
|
+
*/
|
|
3903
|
+
function buildDetailHandleExpiredError(deviceId, handleAgeMs) {
|
|
3904
|
+
return /* @__PURE__ */ new Error(`${DETAIL_HANDLE_EXPIRED_MARKER} — camera ${deviceId}: the frame handle is past the native lease age bound (age ${handleAgeMs ?? "unknown"} ms)`);
|
|
3905
|
+
}
|
|
3888
3906
|
//#endregion
|
|
3889
3907
|
//#region src/pipeline-runner/face-parent-geometry.ts
|
|
3890
3908
|
/** Is the centre of `face` inside `parent` (edges inclusive)? */
|
|
@@ -3970,7 +3988,11 @@ var FaceOutsideParentLog = class {
|
|
|
3970
3988
|
//#region src/pipeline-runner/detail-subtree.ts
|
|
3971
3989
|
/** Did the subtree run refuse because the camera is not attached (park lapsed)? */
|
|
3972
3990
|
function isDetailNotAttachedRefusal(outcome) {
|
|
3973
|
-
return "refusal" in outcome;
|
|
3991
|
+
return "refusal" in outcome && outcome.refusal === "not-attached";
|
|
3992
|
+
}
|
|
3993
|
+
/** Did the subtree run refuse because the named frame handle is past its lease age bound? */
|
|
3994
|
+
function isDetailHandleExpiredRefusal(outcome) {
|
|
3995
|
+
return "refusal" in outcome && outcome.refusal === "handle-expired";
|
|
3974
3996
|
}
|
|
3975
3997
|
/** Did the native-lease fetch report a miss (rather than return pixels)? */
|
|
3976
3998
|
function isNativeCropMiss(fetch) {
|
|
@@ -4272,6 +4294,10 @@ async function runDetailSubtree(deps, input) {
|
|
|
4272
4294
|
nativeWorkerReason: nativeMiss?.workerReason ?? null
|
|
4273
4295
|
}
|
|
4274
4296
|
});
|
|
4297
|
+
if (nativeMiss?.miss === "handle-expired") return {
|
|
4298
|
+
refusal: "handle-expired",
|
|
4299
|
+
handleAgeMs: nativeMiss.handleAgeMs
|
|
4300
|
+
};
|
|
4275
4301
|
return null;
|
|
4276
4302
|
}
|
|
4277
4303
|
const nativeCropRef = input.frameHandle !== void 0 && crop.source === "native" ? {
|
|
@@ -10071,6 +10097,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
10071
10097
|
recordFaceOutsideParent: (drop) => this.faceOutsideParentLog.record(drop)
|
|
10072
10098
|
}, input);
|
|
10073
10099
|
if (outcome !== null && isDetailNotAttachedRefusal(outcome)) throw buildDetailNotAttachedError(input.deviceId);
|
|
10100
|
+
if (outcome !== null && isDetailHandleExpiredRefusal(outcome)) throw buildDetailHandleExpiredError(input.deviceId, outcome.handleAgeMs);
|
|
10074
10101
|
return outcome;
|
|
10075
10102
|
}
|
|
10076
10103
|
/**
|
|
@@ -3877,6 +3877,24 @@ function isParkedDetailFresh(parked, nowMs) {
|
|
|
3877
3877
|
function buildDetailNotAttachedError(deviceId) {
|
|
3878
3878
|
return /* @__PURE__ */ new Error(`${DETAIL_NOT_ATTACHED_MARKER} — camera ${deviceId} is not attached and no detail state is parked within the grace window`);
|
|
3879
3879
|
}
|
|
3880
|
+
/**
|
|
3881
|
+
* The wire marker for the typed handle-expired refusal (D688): the frame handle
|
|
3882
|
+
* a detail dispatch named is past the native lease age bound, so every later
|
|
3883
|
+
* ask for it is refused too. Twin constant in
|
|
3884
|
+
* `addon-post-analysis/src/pipeline-analytics/detail-refusal.ts` — keep in sync.
|
|
3885
|
+
* An error rather than a result field for the same reason as
|
|
3886
|
+
* {@link DETAIL_NOT_ATTACHED_MARKER}: the cap output schema strips unknown keys.
|
|
3887
|
+
*/
|
|
3888
|
+
var DETAIL_HANDLE_EXPIRED_MARKER = "runDetailSubtree-refused:handle-expired";
|
|
3889
|
+
/**
|
|
3890
|
+
* The error the cap method throws when the dispatch's only pixel source was a
|
|
3891
|
+
* frame handle the lease registry refused as expired. The dispatcher recognises
|
|
3892
|
+
* the marker, retires the handle for that track and falls back to its parked
|
|
3893
|
+
* frame — it never names the same handle again.
|
|
3894
|
+
*/
|
|
3895
|
+
function buildDetailHandleExpiredError(deviceId, handleAgeMs) {
|
|
3896
|
+
return /* @__PURE__ */ new Error(`${DETAIL_HANDLE_EXPIRED_MARKER} — camera ${deviceId}: the frame handle is past the native lease age bound (age ${handleAgeMs ?? "unknown"} ms)`);
|
|
3897
|
+
}
|
|
3880
3898
|
//#endregion
|
|
3881
3899
|
//#region src/pipeline-runner/face-parent-geometry.ts
|
|
3882
3900
|
/** Is the centre of `face` inside `parent` (edges inclusive)? */
|
|
@@ -3962,7 +3980,11 @@ var FaceOutsideParentLog = class {
|
|
|
3962
3980
|
//#region src/pipeline-runner/detail-subtree.ts
|
|
3963
3981
|
/** Did the subtree run refuse because the camera is not attached (park lapsed)? */
|
|
3964
3982
|
function isDetailNotAttachedRefusal(outcome) {
|
|
3965
|
-
return "refusal" in outcome;
|
|
3983
|
+
return "refusal" in outcome && outcome.refusal === "not-attached";
|
|
3984
|
+
}
|
|
3985
|
+
/** Did the subtree run refuse because the named frame handle is past its lease age bound? */
|
|
3986
|
+
function isDetailHandleExpiredRefusal(outcome) {
|
|
3987
|
+
return "refusal" in outcome && outcome.refusal === "handle-expired";
|
|
3966
3988
|
}
|
|
3967
3989
|
/** Did the native-lease fetch report a miss (rather than return pixels)? */
|
|
3968
3990
|
function isNativeCropMiss(fetch) {
|
|
@@ -4264,6 +4286,10 @@ async function runDetailSubtree(deps, input) {
|
|
|
4264
4286
|
nativeWorkerReason: nativeMiss?.workerReason ?? null
|
|
4265
4287
|
}
|
|
4266
4288
|
});
|
|
4289
|
+
if (nativeMiss?.miss === "handle-expired") return {
|
|
4290
|
+
refusal: "handle-expired",
|
|
4291
|
+
handleAgeMs: nativeMiss.handleAgeMs
|
|
4292
|
+
};
|
|
4267
4293
|
return null;
|
|
4268
4294
|
}
|
|
4269
4295
|
const nativeCropRef = input.frameHandle !== void 0 && crop.source === "native" ? {
|
|
@@ -10063,6 +10089,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
10063
10089
|
recordFaceOutsideParent: (drop) => this.faceOutsideParentLog.record(drop)
|
|
10064
10090
|
}, input);
|
|
10065
10091
|
if (outcome !== null && isDetailNotAttachedRefusal(outcome)) throw buildDetailNotAttachedError(input.deviceId);
|
|
10092
|
+
if (outcome !== null && isDetailHandleExpiredRefusal(outcome)) throw buildDetailHandleExpiredError(input.deviceId, outcome.handleAgeMs);
|
|
10066
10093
|
return outcome;
|
|
10067
10094
|
}
|
|
10068
10095
|
/**
|
|
@@ -8498,6 +8498,41 @@ function resolveSourceParser(setting, sourceType) {
|
|
|
8498
8498
|
}
|
|
8499
8499
|
}
|
|
8500
8500
|
//#endregion
|
|
8501
|
+
//#region src/stream-broker/catalog-absence.ts
|
|
8502
|
+
function judgeCatalogAbsence(input) {
|
|
8503
|
+
const { marks, listed, candidates, nowMs, confirmMinMs } = input;
|
|
8504
|
+
const nextMarks = /* @__PURE__ */ new Map();
|
|
8505
|
+
const marked = [];
|
|
8506
|
+
const cleared = [];
|
|
8507
|
+
const retract = [];
|
|
8508
|
+
for (const camStreamId of candidates) {
|
|
8509
|
+
const markedAt = marks.get(camStreamId);
|
|
8510
|
+
if (listed.has(camStreamId)) {
|
|
8511
|
+
if (markedAt !== void 0) cleared.push(camStreamId);
|
|
8512
|
+
continue;
|
|
8513
|
+
}
|
|
8514
|
+
if (markedAt === void 0) {
|
|
8515
|
+
nextMarks.set(camStreamId, nowMs);
|
|
8516
|
+
marked.push(camStreamId);
|
|
8517
|
+
continue;
|
|
8518
|
+
}
|
|
8519
|
+
if (nowMs - markedAt < confirmMinMs) {
|
|
8520
|
+
nextMarks.set(camStreamId, markedAt);
|
|
8521
|
+
continue;
|
|
8522
|
+
}
|
|
8523
|
+
retract.push({
|
|
8524
|
+
camStreamId,
|
|
8525
|
+
absentForMs: nowMs - markedAt
|
|
8526
|
+
});
|
|
8527
|
+
}
|
|
8528
|
+
return {
|
|
8529
|
+
nextMarks,
|
|
8530
|
+
marked,
|
|
8531
|
+
cleared,
|
|
8532
|
+
retract
|
|
8533
|
+
};
|
|
8534
|
+
}
|
|
8535
|
+
//#endregion
|
|
8501
8536
|
//#region src/stream-broker/rtmp/flv-parser.ts
|
|
8502
8537
|
var AudioSoundFormat = /* @__PURE__ */ function(AudioSoundFormat) {
|
|
8503
8538
|
AudioSoundFormat[AudioSoundFormat["PCM_BE"] = 0] = "PCM_BE";
|
|
@@ -17098,6 +17133,13 @@ var RtspEnabledSchema = require_dist.record(require_dist.string(), require_dist.
|
|
|
17098
17133
|
* one observation.
|
|
17099
17134
|
*/
|
|
17100
17135
|
var DEPARTURE_CONFIRM_MIN_MS = 3e4;
|
|
17136
|
+
/**
|
|
17137
|
+
* The catalog reconcile's second omission of a stream must be at least this
|
|
17138
|
+
* long after the first (D689) — the same one-poll-interval floor as the
|
|
17139
|
+
* departed-device sweep, for the same reason: an event-driven pull next to a
|
|
17140
|
+
* poll, or the startup pass's quick re-runs, are one observation.
|
|
17141
|
+
*/
|
|
17142
|
+
var CATALOG_ABSENCE_CONFIRM_MIN_MS = DEPARTURE_CONFIRM_MIN_MS;
|
|
17101
17143
|
var PUSH_KINDS = new Set(["push-annexb"]);
|
|
17102
17144
|
/** Walk a profile map and return the slot that points at `camStreamId`, or null. */
|
|
17103
17145
|
function findProfileForStream(map, camStreamId) {
|
|
@@ -17304,6 +17346,14 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17304
17346
|
* agrees, and a reappearance clears it (D49).
|
|
17305
17347
|
*/
|
|
17306
17348
|
departureMarks = /* @__PURE__ */ new Map();
|
|
17349
|
+
/**
|
|
17350
|
+
* Per device: provider streams the broker holds that a successful catalog
|
|
17351
|
+
* pull OMITTED, and when (D689). A mark, not a verdict — see
|
|
17352
|
+
* `catalog-absence.ts`. Dropped when the stream is retracted by any path and
|
|
17353
|
+
* when the device is disabled (no pulls run then, so a mark would outlive
|
|
17354
|
+
* the observation it records).
|
|
17355
|
+
*/
|
|
17356
|
+
catalogAbsenceMarks = /* @__PURE__ */ new Map();
|
|
17307
17357
|
eventBus = null;
|
|
17308
17358
|
/**
|
|
17309
17359
|
* Per-broker stream health flag set by the watchdog. `undefined`
|
|
@@ -19034,6 +19084,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19034
19084
|
await this.releaseBroker(deviceId, camStreamId);
|
|
19035
19085
|
streamMap.delete(camStreamId);
|
|
19036
19086
|
if (streamMap.size === 0) this.cameraStreams.delete(deviceId);
|
|
19087
|
+
this.dropCatalogAbsenceMark(deviceId, camStreamId);
|
|
19037
19088
|
this.emitCamStreamsChanged(deviceId);
|
|
19038
19089
|
return { success: true };
|
|
19039
19090
|
}
|
|
@@ -19095,13 +19146,21 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19095
19146
|
}
|
|
19096
19147
|
/**
|
|
19097
19148
|
* Pull one camera's `stream-catalog` and reconcile its cam-streams: upsert
|
|
19098
|
-
* every descriptor, retract
|
|
19149
|
+
* every descriptor, retract a cam-stream the catalog no longer lists. A pull
|
|
19099
19150
|
* that throws (provider mid-restart, no `stream-catalog` cap, a camera whose
|
|
19100
19151
|
* session died) omits THIS DEVICE ONLY — it is logged at `warn` with the
|
|
19101
|
-
* device's id and re-asked on the `catalog-pull-backoff.ts` ladder.
|
|
19102
|
-
*
|
|
19103
|
-
*
|
|
19104
|
-
*
|
|
19152
|
+
* device's id and re-asked on the `catalog-pull-backoff.ts` ladder.
|
|
19153
|
+
*
|
|
19154
|
+
* The retract DESTROYS work (broker, restreamer, every writer and viewer on
|
|
19155
|
+
* it), so it is never decided by one read (D689, D49): a pull that omits a
|
|
19156
|
+
* stream only marks it, and the retract needs a later successful pull, at
|
|
19157
|
+
* least {@link CATALOG_ABSENCE_CONFIRM_MIN_MS} after the mark, to agree. A
|
|
19158
|
+
* failed pull, or one that answered nothing at all (`null`), decides
|
|
19159
|
+
* nothing. Neither does an answered EMPTY catalog: providers answer `[]`
|
|
19160
|
+
* both for "not mine" and for "don't know yet" (a sleeping Reolink battery
|
|
19161
|
+
* camera, a rebooting Hikvision), so it is not a read that may destroy work
|
|
19162
|
+
* — it neither marks nor clears. Genuine removal of a whole camera is the
|
|
19163
|
+
* `DeviceUnregistered` path and the departed-device sweep (D685).
|
|
19105
19164
|
*
|
|
19106
19165
|
* `trigger` decides whether the backoff applies — see
|
|
19107
19166
|
* {@link CatalogReconcileTrigger}. It defaults to `'event'` (always attempt)
|
|
@@ -19123,9 +19182,9 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19123
19182
|
});
|
|
19124
19183
|
return;
|
|
19125
19184
|
}
|
|
19126
|
-
let
|
|
19185
|
+
let answer;
|
|
19127
19186
|
try {
|
|
19128
|
-
|
|
19187
|
+
answer = await this.withCatalogTimeout(this.api.streamCatalog.getCatalog.query({ deviceId }), deviceId);
|
|
19129
19188
|
} catch (err) {
|
|
19130
19189
|
const next = nextCatalogPullFailureState(this.catalogPullFailures.get(deviceId), Date.now());
|
|
19131
19190
|
this.catalogPullFailures.set(deviceId, next);
|
|
@@ -19142,21 +19201,85 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19142
19201
|
}
|
|
19143
19202
|
if (this.catalogPullFailures.delete(deviceId)) this.logger.info("stream-catalog pull recovered — device is back in the catalog", {
|
|
19144
19203
|
tags: { deviceId },
|
|
19145
|
-
meta: { descriptors:
|
|
19204
|
+
meta: { descriptors: answer?.length ?? null }
|
|
19146
19205
|
});
|
|
19147
|
-
if (
|
|
19148
|
-
|
|
19206
|
+
if (answer === null || answer === void 0) {
|
|
19207
|
+
this.logger.debug("stream-catalog pull answered nothing — the registry is left as it is", {
|
|
19208
|
+
tags: { deviceId },
|
|
19209
|
+
meta: { trigger }
|
|
19210
|
+
});
|
|
19211
|
+
return;
|
|
19212
|
+
}
|
|
19213
|
+
const descriptors = answer;
|
|
19214
|
+
if (descriptors.length === 0) {
|
|
19215
|
+
this.logger.debug("stream-catalog pull answered empty — ignored: neither marks nor clears a stream (D689)", {
|
|
19216
|
+
tags: { deviceId },
|
|
19217
|
+
meta: {
|
|
19218
|
+
trigger,
|
|
19219
|
+
pendingMarks: this.catalogAbsenceMarks.get(deviceId)?.size ?? 0
|
|
19220
|
+
}
|
|
19221
|
+
});
|
|
19222
|
+
return;
|
|
19223
|
+
}
|
|
19149
19224
|
await this.publishCatalog(deviceId, descriptors);
|
|
19225
|
+
await this.retractStreamsTwoPullsOmitted(deviceId, descriptors, trigger);
|
|
19226
|
+
}
|
|
19227
|
+
/**
|
|
19228
|
+
* The retract half of a SUCCESSFUL catalog pull (D689): mark what this pull
|
|
19229
|
+
* omitted, unmark what it lists again, retract what two pulls a reconcile
|
|
19230
|
+
* interval apart both omitted. See `catalog-absence.ts`.
|
|
19231
|
+
*/
|
|
19232
|
+
async retractStreamsTwoPullsOmitted(deviceId, descriptors, trigger) {
|
|
19150
19233
|
const existing = this.cameraStreams.get(deviceId);
|
|
19151
|
-
|
|
19152
|
-
|
|
19153
|
-
|
|
19234
|
+
const candidates = existing ? Array.from(existing.values()).filter((cam) => !cam.brokerOwned).map((cam) => cam.camStreamId) : [];
|
|
19235
|
+
const verdict = judgeCatalogAbsence({
|
|
19236
|
+
marks: this.catalogAbsenceMarks.get(deviceId) ?? /* @__PURE__ */ new Map(),
|
|
19237
|
+
listed: new Set(descriptors.map((d) => d.camStreamId)),
|
|
19238
|
+
candidates,
|
|
19239
|
+
nowMs: Date.now(),
|
|
19240
|
+
confirmMinMs: CATALOG_ABSENCE_CONFIRM_MIN_MS
|
|
19241
|
+
});
|
|
19242
|
+
if (verdict.nextMarks.size > 0) this.catalogAbsenceMarks.set(deviceId, verdict.nextMarks);
|
|
19243
|
+
else this.catalogAbsenceMarks.delete(deviceId);
|
|
19244
|
+
for (const camStreamId of verdict.marked) this.logger.info("stream absent from the catalog — retracted only if a pull a reconcile interval later agrees", {
|
|
19245
|
+
tags: { deviceId },
|
|
19246
|
+
meta: {
|
|
19247
|
+
camStreamId,
|
|
19248
|
+
trigger,
|
|
19249
|
+
confirmAfterMs: CATALOG_ABSENCE_CONFIRM_MIN_MS
|
|
19250
|
+
}
|
|
19251
|
+
});
|
|
19252
|
+
for (const camStreamId of verdict.cleared) this.logger.info("stream is back in the catalog — pending retract cancelled", {
|
|
19253
|
+
tags: { deviceId },
|
|
19254
|
+
meta: {
|
|
19255
|
+
camStreamId,
|
|
19256
|
+
trigger
|
|
19257
|
+
}
|
|
19258
|
+
});
|
|
19259
|
+
for (const { camStreamId, absentForMs } of verdict.retract) {
|
|
19260
|
+
this.logger.warn("stream absent from two catalog pulls — retracted", {
|
|
19261
|
+
tags: { deviceId },
|
|
19262
|
+
meta: {
|
|
19263
|
+
camStreamId,
|
|
19264
|
+
absentForMs,
|
|
19265
|
+
trigger
|
|
19266
|
+
}
|
|
19267
|
+
});
|
|
19268
|
+
await this.retractCameraStream({
|
|
19154
19269
|
deviceId,
|
|
19155
19270
|
camStreamId,
|
|
19156
19271
|
preserveAssignment: true
|
|
19157
19272
|
});
|
|
19158
19273
|
}
|
|
19159
19274
|
}
|
|
19275
|
+
/** Forget one stream's pending catalog-absence mark — it was retracted by some path. */
|
|
19276
|
+
dropCatalogAbsenceMark(deviceId, camStreamId) {
|
|
19277
|
+
const marks = this.catalogAbsenceMarks.get(deviceId);
|
|
19278
|
+
if (!marks?.has(camStreamId)) return;
|
|
19279
|
+
const next = new Map([...marks].filter(([id]) => id !== camStreamId));
|
|
19280
|
+
if (next.size > 0) this.catalogAbsenceMarks.set(deviceId, next);
|
|
19281
|
+
else this.catalogAbsenceMarks.delete(deviceId);
|
|
19282
|
+
}
|
|
19160
19283
|
/**
|
|
19161
19284
|
* Enumerate every camera and reconcile its catalog. The reconcile backstop
|
|
19162
19285
|
* (poll cadence) and the start-up fetch. Per-device failures are isolated so
|
|
@@ -19176,6 +19299,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19176
19299
|
for (const d of cameras) if (d.disabled) {
|
|
19177
19300
|
const firstSeen = !this.disabledDevices.has(d.id);
|
|
19178
19301
|
this.disabledDevices.add(d.id);
|
|
19302
|
+
this.catalogAbsenceMarks.delete(d.id);
|
|
19179
19303
|
await this.releaseDisabledDeviceBrokers(d.id, firstSeen);
|
|
19180
19304
|
} else this.disabledDevices.delete(d.id);
|
|
19181
19305
|
const live = cameras.filter((d) => !d.disabled);
|
|
@@ -8494,6 +8494,41 @@ function resolveSourceParser(setting, sourceType) {
|
|
|
8494
8494
|
}
|
|
8495
8495
|
}
|
|
8496
8496
|
//#endregion
|
|
8497
|
+
//#region src/stream-broker/catalog-absence.ts
|
|
8498
|
+
function judgeCatalogAbsence(input) {
|
|
8499
|
+
const { marks, listed, candidates, nowMs, confirmMinMs } = input;
|
|
8500
|
+
const nextMarks = /* @__PURE__ */ new Map();
|
|
8501
|
+
const marked = [];
|
|
8502
|
+
const cleared = [];
|
|
8503
|
+
const retract = [];
|
|
8504
|
+
for (const camStreamId of candidates) {
|
|
8505
|
+
const markedAt = marks.get(camStreamId);
|
|
8506
|
+
if (listed.has(camStreamId)) {
|
|
8507
|
+
if (markedAt !== void 0) cleared.push(camStreamId);
|
|
8508
|
+
continue;
|
|
8509
|
+
}
|
|
8510
|
+
if (markedAt === void 0) {
|
|
8511
|
+
nextMarks.set(camStreamId, nowMs);
|
|
8512
|
+
marked.push(camStreamId);
|
|
8513
|
+
continue;
|
|
8514
|
+
}
|
|
8515
|
+
if (nowMs - markedAt < confirmMinMs) {
|
|
8516
|
+
nextMarks.set(camStreamId, markedAt);
|
|
8517
|
+
continue;
|
|
8518
|
+
}
|
|
8519
|
+
retract.push({
|
|
8520
|
+
camStreamId,
|
|
8521
|
+
absentForMs: nowMs - markedAt
|
|
8522
|
+
});
|
|
8523
|
+
}
|
|
8524
|
+
return {
|
|
8525
|
+
nextMarks,
|
|
8526
|
+
marked,
|
|
8527
|
+
cleared,
|
|
8528
|
+
retract
|
|
8529
|
+
};
|
|
8530
|
+
}
|
|
8531
|
+
//#endregion
|
|
8497
8532
|
//#region src/stream-broker/rtmp/flv-parser.ts
|
|
8498
8533
|
var AudioSoundFormat = /* @__PURE__ */ function(AudioSoundFormat) {
|
|
8499
8534
|
AudioSoundFormat[AudioSoundFormat["PCM_BE"] = 0] = "PCM_BE";
|
|
@@ -17094,6 +17129,13 @@ var RtspEnabledSchema = record(string(), boolean());
|
|
|
17094
17129
|
* one observation.
|
|
17095
17130
|
*/
|
|
17096
17131
|
var DEPARTURE_CONFIRM_MIN_MS = 3e4;
|
|
17132
|
+
/**
|
|
17133
|
+
* The catalog reconcile's second omission of a stream must be at least this
|
|
17134
|
+
* long after the first (D689) — the same one-poll-interval floor as the
|
|
17135
|
+
* departed-device sweep, for the same reason: an event-driven pull next to a
|
|
17136
|
+
* poll, or the startup pass's quick re-runs, are one observation.
|
|
17137
|
+
*/
|
|
17138
|
+
var CATALOG_ABSENCE_CONFIRM_MIN_MS = DEPARTURE_CONFIRM_MIN_MS;
|
|
17097
17139
|
var PUSH_KINDS = new Set(["push-annexb"]);
|
|
17098
17140
|
/** Walk a profile map and return the slot that points at `camStreamId`, or null. */
|
|
17099
17141
|
function findProfileForStream(map, camStreamId) {
|
|
@@ -17300,6 +17342,14 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
17300
17342
|
* agrees, and a reappearance clears it (D49).
|
|
17301
17343
|
*/
|
|
17302
17344
|
departureMarks = /* @__PURE__ */ new Map();
|
|
17345
|
+
/**
|
|
17346
|
+
* Per device: provider streams the broker holds that a successful catalog
|
|
17347
|
+
* pull OMITTED, and when (D689). A mark, not a verdict — see
|
|
17348
|
+
* `catalog-absence.ts`. Dropped when the stream is retracted by any path and
|
|
17349
|
+
* when the device is disabled (no pulls run then, so a mark would outlive
|
|
17350
|
+
* the observation it records).
|
|
17351
|
+
*/
|
|
17352
|
+
catalogAbsenceMarks = /* @__PURE__ */ new Map();
|
|
17303
17353
|
eventBus = null;
|
|
17304
17354
|
/**
|
|
17305
17355
|
* Per-broker stream health flag set by the watchdog. `undefined`
|
|
@@ -19030,6 +19080,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19030
19080
|
await this.releaseBroker(deviceId, camStreamId);
|
|
19031
19081
|
streamMap.delete(camStreamId);
|
|
19032
19082
|
if (streamMap.size === 0) this.cameraStreams.delete(deviceId);
|
|
19083
|
+
this.dropCatalogAbsenceMark(deviceId, camStreamId);
|
|
19033
19084
|
this.emitCamStreamsChanged(deviceId);
|
|
19034
19085
|
return { success: true };
|
|
19035
19086
|
}
|
|
@@ -19091,13 +19142,21 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19091
19142
|
}
|
|
19092
19143
|
/**
|
|
19093
19144
|
* Pull one camera's `stream-catalog` and reconcile its cam-streams: upsert
|
|
19094
|
-
* every descriptor, retract
|
|
19145
|
+
* every descriptor, retract a cam-stream the catalog no longer lists. A pull
|
|
19095
19146
|
* that throws (provider mid-restart, no `stream-catalog` cap, a camera whose
|
|
19096
19147
|
* session died) omits THIS DEVICE ONLY — it is logged at `warn` with the
|
|
19097
|
-
* device's id and re-asked on the `catalog-pull-backoff.ts` ladder.
|
|
19098
|
-
*
|
|
19099
|
-
*
|
|
19100
|
-
*
|
|
19148
|
+
* device's id and re-asked on the `catalog-pull-backoff.ts` ladder.
|
|
19149
|
+
*
|
|
19150
|
+
* The retract DESTROYS work (broker, restreamer, every writer and viewer on
|
|
19151
|
+
* it), so it is never decided by one read (D689, D49): a pull that omits a
|
|
19152
|
+
* stream only marks it, and the retract needs a later successful pull, at
|
|
19153
|
+
* least {@link CATALOG_ABSENCE_CONFIRM_MIN_MS} after the mark, to agree. A
|
|
19154
|
+
* failed pull, or one that answered nothing at all (`null`), decides
|
|
19155
|
+
* nothing. Neither does an answered EMPTY catalog: providers answer `[]`
|
|
19156
|
+
* both for "not mine" and for "don't know yet" (a sleeping Reolink battery
|
|
19157
|
+
* camera, a rebooting Hikvision), so it is not a read that may destroy work
|
|
19158
|
+
* — it neither marks nor clears. Genuine removal of a whole camera is the
|
|
19159
|
+
* `DeviceUnregistered` path and the departed-device sweep (D685).
|
|
19101
19160
|
*
|
|
19102
19161
|
* `trigger` decides whether the backoff applies — see
|
|
19103
19162
|
* {@link CatalogReconcileTrigger}. It defaults to `'event'` (always attempt)
|
|
@@ -19119,9 +19178,9 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19119
19178
|
});
|
|
19120
19179
|
return;
|
|
19121
19180
|
}
|
|
19122
|
-
let
|
|
19181
|
+
let answer;
|
|
19123
19182
|
try {
|
|
19124
|
-
|
|
19183
|
+
answer = await this.withCatalogTimeout(this.api.streamCatalog.getCatalog.query({ deviceId }), deviceId);
|
|
19125
19184
|
} catch (err) {
|
|
19126
19185
|
const next = nextCatalogPullFailureState(this.catalogPullFailures.get(deviceId), Date.now());
|
|
19127
19186
|
this.catalogPullFailures.set(deviceId, next);
|
|
@@ -19138,21 +19197,85 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19138
19197
|
}
|
|
19139
19198
|
if (this.catalogPullFailures.delete(deviceId)) this.logger.info("stream-catalog pull recovered — device is back in the catalog", {
|
|
19140
19199
|
tags: { deviceId },
|
|
19141
|
-
meta: { descriptors:
|
|
19200
|
+
meta: { descriptors: answer?.length ?? null }
|
|
19142
19201
|
});
|
|
19143
|
-
if (
|
|
19144
|
-
|
|
19202
|
+
if (answer === null || answer === void 0) {
|
|
19203
|
+
this.logger.debug("stream-catalog pull answered nothing — the registry is left as it is", {
|
|
19204
|
+
tags: { deviceId },
|
|
19205
|
+
meta: { trigger }
|
|
19206
|
+
});
|
|
19207
|
+
return;
|
|
19208
|
+
}
|
|
19209
|
+
const descriptors = answer;
|
|
19210
|
+
if (descriptors.length === 0) {
|
|
19211
|
+
this.logger.debug("stream-catalog pull answered empty — ignored: neither marks nor clears a stream (D689)", {
|
|
19212
|
+
tags: { deviceId },
|
|
19213
|
+
meta: {
|
|
19214
|
+
trigger,
|
|
19215
|
+
pendingMarks: this.catalogAbsenceMarks.get(deviceId)?.size ?? 0
|
|
19216
|
+
}
|
|
19217
|
+
});
|
|
19218
|
+
return;
|
|
19219
|
+
}
|
|
19145
19220
|
await this.publishCatalog(deviceId, descriptors);
|
|
19221
|
+
await this.retractStreamsTwoPullsOmitted(deviceId, descriptors, trigger);
|
|
19222
|
+
}
|
|
19223
|
+
/**
|
|
19224
|
+
* The retract half of a SUCCESSFUL catalog pull (D689): mark what this pull
|
|
19225
|
+
* omitted, unmark what it lists again, retract what two pulls a reconcile
|
|
19226
|
+
* interval apart both omitted. See `catalog-absence.ts`.
|
|
19227
|
+
*/
|
|
19228
|
+
async retractStreamsTwoPullsOmitted(deviceId, descriptors, trigger) {
|
|
19146
19229
|
const existing = this.cameraStreams.get(deviceId);
|
|
19147
|
-
|
|
19148
|
-
|
|
19149
|
-
|
|
19230
|
+
const candidates = existing ? Array.from(existing.values()).filter((cam) => !cam.brokerOwned).map((cam) => cam.camStreamId) : [];
|
|
19231
|
+
const verdict = judgeCatalogAbsence({
|
|
19232
|
+
marks: this.catalogAbsenceMarks.get(deviceId) ?? /* @__PURE__ */ new Map(),
|
|
19233
|
+
listed: new Set(descriptors.map((d) => d.camStreamId)),
|
|
19234
|
+
candidates,
|
|
19235
|
+
nowMs: Date.now(),
|
|
19236
|
+
confirmMinMs: CATALOG_ABSENCE_CONFIRM_MIN_MS
|
|
19237
|
+
});
|
|
19238
|
+
if (verdict.nextMarks.size > 0) this.catalogAbsenceMarks.set(deviceId, verdict.nextMarks);
|
|
19239
|
+
else this.catalogAbsenceMarks.delete(deviceId);
|
|
19240
|
+
for (const camStreamId of verdict.marked) this.logger.info("stream absent from the catalog — retracted only if a pull a reconcile interval later agrees", {
|
|
19241
|
+
tags: { deviceId },
|
|
19242
|
+
meta: {
|
|
19243
|
+
camStreamId,
|
|
19244
|
+
trigger,
|
|
19245
|
+
confirmAfterMs: CATALOG_ABSENCE_CONFIRM_MIN_MS
|
|
19246
|
+
}
|
|
19247
|
+
});
|
|
19248
|
+
for (const camStreamId of verdict.cleared) this.logger.info("stream is back in the catalog — pending retract cancelled", {
|
|
19249
|
+
tags: { deviceId },
|
|
19250
|
+
meta: {
|
|
19251
|
+
camStreamId,
|
|
19252
|
+
trigger
|
|
19253
|
+
}
|
|
19254
|
+
});
|
|
19255
|
+
for (const { camStreamId, absentForMs } of verdict.retract) {
|
|
19256
|
+
this.logger.warn("stream absent from two catalog pulls — retracted", {
|
|
19257
|
+
tags: { deviceId },
|
|
19258
|
+
meta: {
|
|
19259
|
+
camStreamId,
|
|
19260
|
+
absentForMs,
|
|
19261
|
+
trigger
|
|
19262
|
+
}
|
|
19263
|
+
});
|
|
19264
|
+
await this.retractCameraStream({
|
|
19150
19265
|
deviceId,
|
|
19151
19266
|
camStreamId,
|
|
19152
19267
|
preserveAssignment: true
|
|
19153
19268
|
});
|
|
19154
19269
|
}
|
|
19155
19270
|
}
|
|
19271
|
+
/** Forget one stream's pending catalog-absence mark — it was retracted by some path. */
|
|
19272
|
+
dropCatalogAbsenceMark(deviceId, camStreamId) {
|
|
19273
|
+
const marks = this.catalogAbsenceMarks.get(deviceId);
|
|
19274
|
+
if (!marks?.has(camStreamId)) return;
|
|
19275
|
+
const next = new Map([...marks].filter(([id]) => id !== camStreamId));
|
|
19276
|
+
if (next.size > 0) this.catalogAbsenceMarks.set(deviceId, next);
|
|
19277
|
+
else this.catalogAbsenceMarks.delete(deviceId);
|
|
19278
|
+
}
|
|
19156
19279
|
/**
|
|
19157
19280
|
* Enumerate every camera and reconcile its catalog. The reconcile backstop
|
|
19158
19281
|
* (poll cadence) and the start-up fetch. Per-device failures are isolated so
|
|
@@ -19172,6 +19295,7 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
19172
19295
|
for (const d of cameras) if (d.disabled) {
|
|
19173
19296
|
const firstSeen = !this.disabledDevices.has(d.id);
|
|
19174
19297
|
this.disabledDevices.add(d.id);
|
|
19298
|
+
this.catalogAbsenceMarks.delete(d.id);
|
|
19175
19299
|
await this.releaseDisabledDeviceBrokers(d.id, firstSeen);
|
|
19176
19300
|
} else this.disabledDevices.delete(d.id);
|
|
19177
19301
|
const live = cameras.filter((d) => !d.disabled);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.312",
|
|
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",
|