@camstack/addon-provider-rtsp 1.1.18 → 1.1.20
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/addon.js +402 -232
- package/dist/addon.mjs +379 -232
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let node_net = require("node:net");
|
|
25
|
+
node_net = __toESM(node_net);
|
|
2
26
|
//#region ../../node_modules/zod/v4/core/core.js
|
|
3
27
|
var _a$1;
|
|
4
28
|
function $constructor(name, initializer, params) {
|
|
@@ -4642,7 +4666,7 @@ function preprocess(fn, schema) {
|
|
|
4642
4666
|
});
|
|
4643
4667
|
}
|
|
4644
4668
|
//#endregion
|
|
4645
|
-
//#region ../types/dist/sleep-
|
|
4669
|
+
//#region ../types/dist/sleep-Cc14_yxc.mjs
|
|
4646
4670
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4647
4671
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4648
4672
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -4828,6 +4852,18 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
4828
4852
|
*/
|
|
4829
4853
|
EventCategory["PipelineCameraUpdated"] = "pipeline.camera-updated";
|
|
4830
4854
|
/**
|
|
4855
|
+
* The cluster camera-source OWNER changed (`clusterRoles.ingestNode`).
|
|
4856
|
+
* Emitted by addon-pipeline-orchestrator whenever it (re)derives node
|
|
4857
|
+
* capabilities — at boot, on agent online/offline, and on an ingest-node
|
|
4858
|
+
* flip. Carries the resolved `ownerNodeId`. The stream-broker consumes it to
|
|
4859
|
+
* keep its ingest-owner-gate decision current WITHOUT a per-`ensureBroker`
|
|
4860
|
+
* cross-process `getIngestOwner` query (push the authority's decision instead
|
|
4861
|
+
* of polling it on the hot path). Idempotent state — re-emitted on every
|
|
4862
|
+
* topology change, so a dropped event self-heals on the next one (plus the
|
|
4863
|
+
* broker's long backstop reconcile query).
|
|
4864
|
+
*/
|
|
4865
|
+
EventCategory["PipelineIngestOwnerChanged"] = "pipeline.ingest-owner-changed";
|
|
4866
|
+
/**
|
|
4831
4867
|
* Periodic snapshot of per-node pipeline-runner load
|
|
4832
4868
|
* (`RunnerLocalLoad`). Emitted ~1Hz by every runner so UI dashboards
|
|
4833
4869
|
* subscribe instead of polling `pipelineRunner.getLocalLoad`.
|
|
@@ -6911,6 +6947,36 @@ var ModelFormatsSchema = object({
|
|
|
6911
6947
|
tflite: ModelFormatEntrySchema.optional(),
|
|
6912
6948
|
pt: ModelFormatEntrySchema.optional()
|
|
6913
6949
|
});
|
|
6950
|
+
/**
|
|
6951
|
+
* Variant-selector grouping axes. Shared by the full `ModelCatalogEntry` and by
|
|
6952
|
+
* the reduced `PipelineModelOption` returned in `pipeline.getSchema()` so the
|
|
6953
|
+
* grouped Family→Tier→Variant picker renders identically in the config UI and
|
|
6954
|
+
* in the pipeline/device steppers. The flat `id` stays the source of truth for
|
|
6955
|
+
* resolution/download/persistence; this is a presentation overlay resolved back
|
|
6956
|
+
* to an `id`.
|
|
6957
|
+
*/
|
|
6958
|
+
var ModelVariantGroupSchema = object({
|
|
6959
|
+
/** Top-level family, e.g. `yolo26` (later `d-fine`, `rf-detr`). */
|
|
6960
|
+
family: string(),
|
|
6961
|
+
/** Size within the family, e.g. `n` | `s` | `m` | `l`. */
|
|
6962
|
+
tier: string(),
|
|
6963
|
+
/** Quantization axis. Omit ⇒ the fp32 base build. */
|
|
6964
|
+
precision: _enum(["fp32", "int8"]).optional(),
|
|
6965
|
+
/**
|
|
6966
|
+
* Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
|
|
6967
|
+
* latency-optimized export (e.g. ReLU-activation variant) — the slot the
|
|
6968
|
+
* future performance variants plug into.
|
|
6969
|
+
*/
|
|
6970
|
+
optimization: _enum(["standard", "fast"]).optional(),
|
|
6971
|
+
/**
|
|
6972
|
+
* Input-resolution axis (square input side, px). Omit ⇒ the family's native
|
|
6973
|
+
* resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
|
|
6974
|
+
* cheap latency lever — especially on Apple ANE and the Intel N100 — at a
|
|
6975
|
+
* small-object accuracy cost. Mirrors the model's `inputSize` but lifted onto
|
|
6976
|
+
* the group so the selector can offer it as a variant axis.
|
|
6977
|
+
*/
|
|
6978
|
+
resolution: number().int().positive().optional()
|
|
6979
|
+
});
|
|
6914
6980
|
var ModelCatalogEntrySchema = object({
|
|
6915
6981
|
id: string(),
|
|
6916
6982
|
name: string(),
|
|
@@ -6940,7 +7006,43 @@ var ModelCatalogEntrySchema = object({
|
|
|
6940
7006
|
* Auxiliary files required at runtime (labels JSON, charset dict, etc.).
|
|
6941
7007
|
* Downloaded into the same modelsDir alongside the model file.
|
|
6942
7008
|
*/
|
|
6943
|
-
extraFiles: array(ModelExtraFileSchema).readonly().optional()
|
|
7009
|
+
extraFiles: array(ModelExtraFileSchema).readonly().optional(),
|
|
7010
|
+
/**
|
|
7011
|
+
* LEGACY entry — retained in the catalog so a persisted operator selection
|
|
7012
|
+
* still RESOLVES (and can be re-activated), but hidden from the selectable
|
|
7013
|
+
* model list and excluded from the auto format-default pick. Set on the
|
|
7014
|
+
* superseded / consolidated models (older lineages, redundant fp16 IRs) so
|
|
7015
|
+
* the active lineup stays the coherent curated ladder without deleting a
|
|
7016
|
+
* model anyone may still be pinned to. `resolveModelForFormat` keeps honoring
|
|
7017
|
+
* an explicit legacy id that has a build for the node's format.
|
|
7018
|
+
*/
|
|
7019
|
+
legacy: boolean().optional(),
|
|
7020
|
+
/**
|
|
7021
|
+
* Measured quality/latency metadata — populated from the benchmark addon on
|
|
7022
|
+
* the real node classes. Absent = not yet measured (most entries today; the
|
|
7023
|
+
* catalog historically carried only `sizeMB`, a poor cross-architecture
|
|
7024
|
+
* speed proxy). `p95LatencyMs` is keyed by node class (e.g. `n100`, `mac`).
|
|
7025
|
+
*/
|
|
7026
|
+
metrics: object({
|
|
7027
|
+
map50: number().optional(),
|
|
7028
|
+
p95LatencyMs: record(string(), number()).optional()
|
|
7029
|
+
}).optional(),
|
|
7030
|
+
/**
|
|
7031
|
+
* SPDX-ish license id of the model weights (e.g. `AGPL-3.0` for Ultralytics
|
|
7032
|
+
* YOLO26, `GPL-3.0` for YOLOv9, `Apache-2.0` for D-FINE/RF-DETR). Matters for
|
|
7033
|
+
* the retraining addon and any future commercial distribution.
|
|
7034
|
+
*/
|
|
7035
|
+
license: string().optional(),
|
|
7036
|
+
/**
|
|
7037
|
+
* Variant-selector grouping. The UI groups models by `family` + `tier` and
|
|
7038
|
+
* offers `precision` / `optimization` as variant axes WITHIN a tier — so all
|
|
7039
|
+
* of a family's sizes and quantizations collapse into one grouped picker
|
|
7040
|
+
* instead of a flat list of `yolo26s`, `yolo26s-int8`, … Absent ⇒ ungrouped
|
|
7041
|
+
* (legacy / custom models) — never shown in the grouped selector. The flat
|
|
7042
|
+
* `id` stays the source of truth for resolution/download/persistence; grouping
|
|
7043
|
+
* is a presentation overlay resolved back to an `id`.
|
|
7044
|
+
*/
|
|
7045
|
+
group: ModelVariantGroupSchema.optional()
|
|
6944
7046
|
});
|
|
6945
7047
|
var ConvertTargetSchema = discriminatedUnion("format", [object({
|
|
6946
7048
|
format: literal("openvino"),
|
|
@@ -7001,8 +7103,8 @@ var RecordingModeSchema = _enum([
|
|
|
7001
7103
|
"onAudioThreshold"
|
|
7002
7104
|
]);
|
|
7003
7105
|
/**
|
|
7004
|
-
* First-class, authoritative per-camera storage mode — the
|
|
7005
|
-
* reads directly (never inferred from `rules`):
|
|
7106
|
+
* First-class, authoritative per-camera storage mode — the explicit choice the
|
|
7107
|
+
* UI reads directly (never inferred from `rules`):
|
|
7006
7108
|
* - `off` — not recording.
|
|
7007
7109
|
* - `events` — record only around triggers (motion / audio threshold),
|
|
7008
7110
|
* with pre/post-buffer.
|
|
@@ -8534,6 +8636,72 @@ function shallowEqual(a, b) {
|
|
|
8534
8636
|
for (const k of ak) if (a[k] !== b[k]) return false;
|
|
8535
8637
|
return true;
|
|
8536
8638
|
}
|
|
8639
|
+
/** Reject after `ms`; always clears its own timer. */
|
|
8640
|
+
async function withTimeout(promise, ms, label) {
|
|
8641
|
+
let timer;
|
|
8642
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
8643
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${label} timed out after ${String(ms)}ms`)), ms);
|
|
8644
|
+
});
|
|
8645
|
+
try {
|
|
8646
|
+
return await Promise.race([promise, timeout]);
|
|
8647
|
+
} finally {
|
|
8648
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
8649
|
+
}
|
|
8650
|
+
}
|
|
8651
|
+
/**
|
|
8652
|
+
* Start the reachability poll loop. Returns a handle whose `stop()` clears the
|
|
8653
|
+
* timer and prevents any further ticks. Start on device activation, stop on
|
|
8654
|
+
* device teardown (`removeDevice`) so no timer leaks.
|
|
8655
|
+
*/
|
|
8656
|
+
function startReachabilityPoll(options) {
|
|
8657
|
+
const intervalMs = options.intervalMs ?? 3e4;
|
|
8658
|
+
const failuresToOffline = options.failuresToOffline ?? 3;
|
|
8659
|
+
const probeTimeoutMs = options.probeTimeoutMs ?? 1e4;
|
|
8660
|
+
const runImmediately = options.runImmediately ?? true;
|
|
8661
|
+
let stopped = false;
|
|
8662
|
+
let running = false;
|
|
8663
|
+
let consecutiveFailures = 0;
|
|
8664
|
+
let timer;
|
|
8665
|
+
const tick = async () => {
|
|
8666
|
+
if (stopped) return;
|
|
8667
|
+
if (running) return;
|
|
8668
|
+
if (options.isEnabled && !options.isEnabled()) return;
|
|
8669
|
+
running = true;
|
|
8670
|
+
try {
|
|
8671
|
+
const reachable = await withTimeout(Promise.resolve().then(options.probe), probeTimeoutMs, "reachability probe");
|
|
8672
|
+
if (stopped) return;
|
|
8673
|
+
if (reachable) {
|
|
8674
|
+
consecutiveFailures = 0;
|
|
8675
|
+
options.setOnline(true);
|
|
8676
|
+
} else registerFailure("probe resolved unreachable");
|
|
8677
|
+
} catch (error) {
|
|
8678
|
+
if (stopped) return;
|
|
8679
|
+
registerFailure(error instanceof Error ? error.message : "probe threw");
|
|
8680
|
+
} finally {
|
|
8681
|
+
running = false;
|
|
8682
|
+
}
|
|
8683
|
+
};
|
|
8684
|
+
const registerFailure = (reason) => {
|
|
8685
|
+
consecutiveFailures += 1;
|
|
8686
|
+
options.logger?.debug("reachability probe failed", {
|
|
8687
|
+
reason,
|
|
8688
|
+
consecutiveFailures,
|
|
8689
|
+
failuresToOffline
|
|
8690
|
+
});
|
|
8691
|
+
if (consecutiveFailures >= failuresToOffline) options.setOnline(false);
|
|
8692
|
+
};
|
|
8693
|
+
timer = setInterval(() => {
|
|
8694
|
+
tick();
|
|
8695
|
+
}, intervalMs);
|
|
8696
|
+
if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
|
|
8697
|
+
if (runImmediately) tick();
|
|
8698
|
+
return { stop: () => {
|
|
8699
|
+
if (stopped) return;
|
|
8700
|
+
stopped = true;
|
|
8701
|
+
if (timer !== void 0) clearInterval(timer);
|
|
8702
|
+
timer = void 0;
|
|
8703
|
+
} };
|
|
8704
|
+
}
|
|
8537
8705
|
/**
|
|
8538
8706
|
* Generic device-level status snapshot. Auto-registered by `BaseDevice`
|
|
8539
8707
|
* for every device, regardless of provider — the kernel needs a uniform
|
|
@@ -9197,26 +9365,13 @@ onBrightnessChanged: { data: object({
|
|
|
9197
9365
|
*/
|
|
9198
9366
|
runtimeState: BrightnessStatusSchema
|
|
9199
9367
|
};
|
|
9368
|
+
/** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
|
|
9200
9369
|
var StreamFormatSchema = _enum([
|
|
9201
9370
|
"webrtc",
|
|
9202
9371
|
"hls",
|
|
9203
9372
|
"mjpeg",
|
|
9204
9373
|
"rtsp"
|
|
9205
9374
|
]);
|
|
9206
|
-
var StreamInfoSchema = object({
|
|
9207
|
-
streamId: string(),
|
|
9208
|
-
format: StreamFormatSchema,
|
|
9209
|
-
url: string().nullable(),
|
|
9210
|
-
active: boolean()
|
|
9211
|
-
});
|
|
9212
|
-
method(object({
|
|
9213
|
-
streamId: string(),
|
|
9214
|
-
sourceUrl: string(),
|
|
9215
|
-
codec: string().optional()
|
|
9216
|
-
}), _void(), { kind: "mutation" }), method(object({ streamId: string() }), _void(), { kind: "mutation" }), method(object({
|
|
9217
|
-
streamId: string(),
|
|
9218
|
-
format: StreamFormatSchema
|
|
9219
|
-
}), string().nullable()), method(_void(), array(StreamInfoSchema));
|
|
9220
9375
|
var RtspRestreamEntrySchema = object({
|
|
9221
9376
|
brokerId: string(),
|
|
9222
9377
|
url: string(),
|
|
@@ -10084,37 +10239,7 @@ var consumablesCapability = {
|
|
|
10084
10239
|
scope: "device",
|
|
10085
10240
|
deviceNative: true,
|
|
10086
10241
|
mode: "singleton",
|
|
10087
|
-
deviceTypes:
|
|
10088
|
-
DeviceType.Camera,
|
|
10089
|
-
DeviceType.Hub,
|
|
10090
|
-
DeviceType.Light,
|
|
10091
|
-
DeviceType.Siren,
|
|
10092
|
-
DeviceType.Switch,
|
|
10093
|
-
DeviceType.Sensor,
|
|
10094
|
-
DeviceType.Thermostat,
|
|
10095
|
-
DeviceType.Button,
|
|
10096
|
-
DeviceType.EventEmitter,
|
|
10097
|
-
DeviceType.Update,
|
|
10098
|
-
DeviceType.Generic,
|
|
10099
|
-
DeviceType.Notifier,
|
|
10100
|
-
DeviceType.Script,
|
|
10101
|
-
DeviceType.Automation,
|
|
10102
|
-
DeviceType.Lock,
|
|
10103
|
-
DeviceType.Cover,
|
|
10104
|
-
DeviceType.Valve,
|
|
10105
|
-
DeviceType.Humidifier,
|
|
10106
|
-
DeviceType.WaterHeater,
|
|
10107
|
-
DeviceType.Fan,
|
|
10108
|
-
DeviceType.MediaPlayer,
|
|
10109
|
-
DeviceType.AlarmPanel,
|
|
10110
|
-
DeviceType.Control,
|
|
10111
|
-
DeviceType.Presence,
|
|
10112
|
-
DeviceType.Weather,
|
|
10113
|
-
DeviceType.Vacuum,
|
|
10114
|
-
DeviceType.LawnMower,
|
|
10115
|
-
DeviceType.Container,
|
|
10116
|
-
DeviceType.Image
|
|
10117
|
-
],
|
|
10242
|
+
deviceTypes: Object.values(DeviceType),
|
|
10118
10243
|
deviceConfig: { ui: {
|
|
10119
10244
|
kind: "widget",
|
|
10120
10245
|
widgetId: "host/consumables-panel",
|
|
@@ -11707,7 +11832,6 @@ var PipelineDefaultStepSchema = lazy(() => object({
|
|
|
11707
11832
|
enabled: boolean(),
|
|
11708
11833
|
modelId: string(),
|
|
11709
11834
|
children: array(PipelineDefaultStepSchema).readonly(),
|
|
11710
|
-
engine: PipelineEngineChoiceSchema.optional(),
|
|
11711
11835
|
group: string().optional(),
|
|
11712
11836
|
settings: record(string(), unknown()).optional()
|
|
11713
11837
|
}));
|
|
@@ -11732,7 +11856,9 @@ var PipelineModelOptionSchema = object({
|
|
|
11732
11856
|
formats: record(string(), object({
|
|
11733
11857
|
downloaded: boolean(),
|
|
11734
11858
|
sizeMB: number()
|
|
11735
|
-
}))
|
|
11859
|
+
})),
|
|
11860
|
+
group: ModelVariantGroupSchema.optional(),
|
|
11861
|
+
legacy: boolean().optional()
|
|
11736
11862
|
});
|
|
11737
11863
|
var ConfigFieldBridge = custom();
|
|
11738
11864
|
var PipelineAddonSchemaSchema = object({
|
|
@@ -11783,15 +11909,42 @@ var EngineProvisioningSchema = object({
|
|
|
11783
11909
|
]),
|
|
11784
11910
|
progress: number().optional(),
|
|
11785
11911
|
error: string().optional(),
|
|
11786
|
-
nextRetryAt: number().optional()
|
|
11912
|
+
nextRetryAt: number().optional(),
|
|
11913
|
+
/**
|
|
11914
|
+
* Gate A (config-correctness gate at engine change): human-readable
|
|
11915
|
+
* config issues surfaced EAGERLY when the node's engine changes — model
|
|
11916
|
+
* substitutions ("chose X, running Y") and zero-build steps ("no model
|
|
11917
|
+
* has a <format> build"). Additive/optional: informational only, never
|
|
11918
|
+
* enforced here — `assertEngineReady` (readiness) still gates inference.
|
|
11919
|
+
* Absent/empty when the node-default tree resolves cleanly.
|
|
11920
|
+
*/
|
|
11921
|
+
configIssues: array(string()).optional()
|
|
11787
11922
|
});
|
|
11788
11923
|
var PipelineStepInputSchema = lazy(() => object({
|
|
11789
11924
|
addonId: string(),
|
|
11790
|
-
modelId: string(),
|
|
11925
|
+
modelId: string().optional(),
|
|
11791
11926
|
enabled: boolean().default(true),
|
|
11792
11927
|
children: array(PipelineStepInputSchema).optional(),
|
|
11793
11928
|
settings: record(string(), unknown()).optional()
|
|
11794
11929
|
}));
|
|
11930
|
+
var ModelSubstitutionSchema = object({
|
|
11931
|
+
addonId: string(),
|
|
11932
|
+
chosen: string(),
|
|
11933
|
+
running: string(),
|
|
11934
|
+
format: string()
|
|
11935
|
+
});
|
|
11936
|
+
var PipelineValidationIssueSchema = object({
|
|
11937
|
+
addonId: string(),
|
|
11938
|
+
kind: _enum(["unknown-addon", "no-format-build"]),
|
|
11939
|
+
detail: string()
|
|
11940
|
+
});
|
|
11941
|
+
var PipelineValidationResultSchema = object({
|
|
11942
|
+
ok: boolean(),
|
|
11943
|
+
issues: array(PipelineValidationIssueSchema).readonly(),
|
|
11944
|
+
substitutions: array(ModelSubstitutionSchema).readonly(),
|
|
11945
|
+
/** The node's `currentEngine.format` this validation ran against. */
|
|
11946
|
+
format: string()
|
|
11947
|
+
});
|
|
11795
11948
|
var ReferenceImageEntrySchema = object({
|
|
11796
11949
|
filename: string(),
|
|
11797
11950
|
stepIds: array(string()).readonly().optional()
|
|
@@ -11862,7 +12015,13 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
|
|
|
11862
12015
|
})) }), object({ success: literal(true) }), {
|
|
11863
12016
|
kind: "mutation",
|
|
11864
12017
|
auth: "admin"
|
|
11865
|
-
}), method(
|
|
12018
|
+
}), method(object({ nodeId: string() }), object({
|
|
12019
|
+
success: literal(true),
|
|
12020
|
+
regeneratedModelId: string().nullable()
|
|
12021
|
+
}), {
|
|
12022
|
+
kind: "mutation",
|
|
12023
|
+
auth: "admin"
|
|
12024
|
+
}), method(_void(), PipelineSchemaSchema), method(_void(), array(PipelineDefaultStepSchema).readonly().nullable()), method(_void(), PipelineConfigBridge), method(_void(), ConfigUISchemaBridge), method(object({ steps: array(PipelineStepInputSchema) }), PipelineValidationResultSchema), method(_void(), array(PipelineTemplateSchema$1).readonly()), method(object({
|
|
11866
12025
|
name: string(),
|
|
11867
12026
|
steps: array(PipelineTemplateStepSchema).readonly(),
|
|
11868
12027
|
engine: PipelineEngineChoiceSchema
|
|
@@ -12155,6 +12314,13 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
|
|
|
12155
12314
|
kind: literal("remote-restream"),
|
|
12156
12315
|
/** The camera's source-owner node (slice 1: always the hub). */
|
|
12157
12316
|
ownerNodeId: string(),
|
|
12317
|
+
/**
|
|
12318
|
+
* The owner's LAN-reachable host, resolved by the orchestrator from the
|
|
12319
|
+
* per-node `reachableHost` override (Cluster UI). When present the runner
|
|
12320
|
+
* dials THIS host for the owner's restream, in preference to the
|
|
12321
|
+
* `CAMSTACK_HUB_URL`-derived default. Absent → auto-detect fallback.
|
|
12322
|
+
*/
|
|
12323
|
+
ownerReachableHost: string().optional(),
|
|
12158
12324
|
/** Operator override for the owner host the runner dials. */
|
|
12159
12325
|
hubHostnameOverride: string().optional()
|
|
12160
12326
|
})]).describe("Per-camera frame-source mode for the runner (P2c)");
|
|
@@ -12163,13 +12329,11 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
|
|
|
12163
12329
|
* specific runner instance via `attachCamera`. Carries everything the
|
|
12164
12330
|
* runner needs to subscribe to the local broker and execute inference.
|
|
12165
12331
|
*
|
|
12166
|
-
* Stateless-pipeline model: the
|
|
12167
|
-
*
|
|
12168
|
-
*
|
|
12169
|
-
*
|
|
12170
|
-
*
|
|
12171
|
-
* `engine`/`steps`/`audio` are optional during the additive migration
|
|
12172
|
-
* window; once orchestrator + UI are migrated they become required.
|
|
12332
|
+
* Stateless-pipeline model: the pipeline content (`steps`, optional
|
|
12333
|
+
* `audio`) travels with the attach payload. The runner keeps it in RAM
|
|
12334
|
+
* for the lifetime of the attach — on rebalance, edit, or restart the
|
|
12335
|
+
* orchestrator re-sends the latest snapshot. Engine is NOT carried: it is
|
|
12336
|
+
* node-local, resolved by the executing runner at dispatch time.
|
|
12173
12337
|
*/
|
|
12174
12338
|
var RunnerCameraConfigSchema = object({
|
|
12175
12339
|
deviceId: number(),
|
|
@@ -12220,14 +12384,11 @@ var RunnerCameraConfigSchema = object({
|
|
|
12220
12384
|
*/
|
|
12221
12385
|
motionSources: MotionSourcesSchema.default(["analyzer"]),
|
|
12222
12386
|
pipelineEnabled: boolean().default(true),
|
|
12223
|
-
/** Engine choice for video steps (runtime+backend+format). */
|
|
12224
|
-
engine: PipelineEngineChoiceSchema.optional(),
|
|
12225
12387
|
/** Ordered tree of video steps. Absent → runner skips video detection. */
|
|
12226
12388
|
steps: array(PipelineStepInputSchema).readonly().optional(),
|
|
12227
12389
|
/** Audio classification branch. `enabled:false` disables, null skips. */
|
|
12228
12390
|
audio: object({
|
|
12229
|
-
|
|
12230
|
-
modelId: string(),
|
|
12391
|
+
modelId: string().optional(),
|
|
12231
12392
|
enabled: boolean()
|
|
12232
12393
|
}).nullable().optional(),
|
|
12233
12394
|
/**
|
|
@@ -18090,11 +18251,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
18090
18251
|
timestamp: number()
|
|
18091
18252
|
});
|
|
18092
18253
|
var CameraPipelineConfigSchema = object({
|
|
18093
|
-
engine: PipelineEngineChoiceSchema,
|
|
18254
|
+
engine: PipelineEngineChoiceSchema.optional(),
|
|
18094
18255
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
18095
18256
|
audio: object({
|
|
18096
|
-
engine: PipelineEngineChoiceSchema,
|
|
18097
|
-
modelId: string(),
|
|
18257
|
+
engine: PipelineEngineChoiceSchema.optional(),
|
|
18258
|
+
modelId: string().optional(),
|
|
18098
18259
|
enabled: boolean(),
|
|
18099
18260
|
settings: record(string(), unknown()).readonly().optional()
|
|
18100
18261
|
}).nullable().optional()
|
|
@@ -18109,7 +18270,7 @@ var PipelineTemplateSchema = object({
|
|
|
18109
18270
|
});
|
|
18110
18271
|
var AgentAddonConfigSchema = object({
|
|
18111
18272
|
enabled: boolean(),
|
|
18112
|
-
modelId: string(),
|
|
18273
|
+
modelId: string().optional(),
|
|
18113
18274
|
settings: record(string(), unknown()).readonly()
|
|
18114
18275
|
});
|
|
18115
18276
|
var AgentPipelineSettingsSchema = object({
|
|
@@ -18124,7 +18285,15 @@ var AgentPipelineSettingsSchema = object({
|
|
|
18124
18285
|
/** Node is eligible to run audio-analyzer sessions. */
|
|
18125
18286
|
audio: boolean().optional(),
|
|
18126
18287
|
/** Node is eligible to be the ingest / source-owner (serve the restream). */
|
|
18127
|
-
ingest: boolean().optional()
|
|
18288
|
+
ingest: boolean().optional(),
|
|
18289
|
+
/**
|
|
18290
|
+
* Operator override for the LAN host a cross-node decoder dials to reach
|
|
18291
|
+
* THIS node's restream (Cluster UI). Absent → auto-detect: a remote runner
|
|
18292
|
+
* falls back to its `CAMSTACK_HUB_URL`-derived host (the Moleculer address
|
|
18293
|
+
* it already uses to reach the hub). Set this only when the auto-detected
|
|
18294
|
+
* address is wrong (multi-homed host, NAT, custom interface).
|
|
18295
|
+
*/
|
|
18296
|
+
reachableHost: string().optional()
|
|
18128
18297
|
});
|
|
18129
18298
|
var CameraPipelineForAgentSchema = object({
|
|
18130
18299
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
@@ -18230,6 +18399,15 @@ var GlobalMetricsSchema = object({
|
|
|
18230
18399
|
* capability providers.
|
|
18231
18400
|
*/
|
|
18232
18401
|
var CapabilityBindingsSchema = record(string(), string());
|
|
18402
|
+
/**
|
|
18403
|
+
* The cluster's single camera-source owner (`clusterRoles.ingestNode`) plus
|
|
18404
|
+
* its LAN-reachable host, if one is registered. See `getIngestOwner`.
|
|
18405
|
+
*/
|
|
18406
|
+
var IngestOwnerSchema = object({
|
|
18407
|
+
ownerNodeId: string(),
|
|
18408
|
+
reachableHost: string().optional(),
|
|
18409
|
+
configIssue: string().optional()
|
|
18410
|
+
});
|
|
18233
18411
|
/** Source block — always present; derives from the stream catalog. */
|
|
18234
18412
|
var CameraSourceStatusSchema = object({ streams: array(object({
|
|
18235
18413
|
camStreamId: string(),
|
|
@@ -18244,6 +18422,14 @@ var CameraAssignmentStatusSchema = object({
|
|
|
18244
18422
|
detectionNodeId: string().nullable(),
|
|
18245
18423
|
decoderNodeId: string().nullable(),
|
|
18246
18424
|
audioNodeId: string().nullable(),
|
|
18425
|
+
/**
|
|
18426
|
+
* The node that OWNS this camera's physical source pull (dials the RTSP and
|
|
18427
|
+
* hosts the broker/restream) — the cluster ingest owner today
|
|
18428
|
+
* (`clusterRoles.ingestNode`), per-camera once source assignment lands. Lets
|
|
18429
|
+
* the UI show WHERE a camera is sourced without SSH/logs, and is the node the
|
|
18430
|
+
* broker block below was read from (pinned). Nullable only pre-wiring.
|
|
18431
|
+
*/
|
|
18432
|
+
sourceNodeId: string().nullable(),
|
|
18247
18433
|
pinned: object({
|
|
18248
18434
|
detection: boolean(),
|
|
18249
18435
|
decoder: boolean(),
|
|
@@ -18376,7 +18562,7 @@ method(object({
|
|
|
18376
18562
|
}), object({ success: literal(true) }), {
|
|
18377
18563
|
kind: "mutation",
|
|
18378
18564
|
auth: "admin"
|
|
18379
|
-
}), method(object({
|
|
18565
|
+
}), method(_void(), IngestOwnerSchema), method(object({
|
|
18380
18566
|
deviceId: number(),
|
|
18381
18567
|
nodeId: string()
|
|
18382
18568
|
}), _void(), {
|
|
@@ -18445,6 +18631,12 @@ method(object({
|
|
|
18445
18631
|
}), object({ success: literal(true) }), {
|
|
18446
18632
|
kind: "mutation",
|
|
18447
18633
|
auth: "admin"
|
|
18634
|
+
}), method(object({
|
|
18635
|
+
agentNodeId: string(),
|
|
18636
|
+
reachableHost: string().nullable()
|
|
18637
|
+
}), object({ success: literal(true) }), {
|
|
18638
|
+
kind: "mutation",
|
|
18639
|
+
auth: "admin"
|
|
18448
18640
|
}), method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()), method(object({
|
|
18449
18641
|
deviceId: number(),
|
|
18450
18642
|
addonId: string(),
|
|
@@ -18489,22 +18681,6 @@ method(object({
|
|
|
18489
18681
|
kind: "mutation",
|
|
18490
18682
|
auth: "admin"
|
|
18491
18683
|
});
|
|
18492
|
-
var RegisteredStreamSchema = object({
|
|
18493
|
-
streamId: string(),
|
|
18494
|
-
label: string().optional(),
|
|
18495
|
-
codec: string(),
|
|
18496
|
-
type: _enum(["video", "audio"]),
|
|
18497
|
-
sourceUrl: string()
|
|
18498
|
-
});
|
|
18499
|
-
var ExposedResourceSchema = object({
|
|
18500
|
-
streamId: string(),
|
|
18501
|
-
format: string(),
|
|
18502
|
-
value: string()
|
|
18503
|
-
});
|
|
18504
|
-
method(object({
|
|
18505
|
-
deviceId: number(),
|
|
18506
|
-
streams: array(RegisteredStreamSchema).readonly()
|
|
18507
|
-
}), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), array(ExposedResourceSchema).readonly());
|
|
18508
18684
|
/**
|
|
18509
18685
|
* Query filter for settings-store collections.
|
|
18510
18686
|
*/
|
|
@@ -18657,9 +18833,9 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
|
|
|
18657
18833
|
/**
|
|
18658
18834
|
* A single device snapshot returned as base64 JPEG/PNG.
|
|
18659
18835
|
*
|
|
18660
|
-
*
|
|
18661
|
-
*
|
|
18662
|
-
*
|
|
18836
|
+
* The `SnapshotAddon` wrapper returns this shape whether the frame came from
|
|
18837
|
+
* the device-native provider (onboard capture) or from the stream-broker
|
|
18838
|
+
* prebuffer fallback.
|
|
18663
18839
|
*/
|
|
18664
18840
|
var SnapshotImageSchema = object({
|
|
18665
18841
|
base64: string(),
|
|
@@ -18734,10 +18910,6 @@ var snapshotCapability = {
|
|
|
18734
18910
|
kind: "poll"
|
|
18735
18911
|
}
|
|
18736
18912
|
};
|
|
18737
|
-
method(object({ deviceId: number() }), boolean()), method(object({
|
|
18738
|
-
deviceId: number(),
|
|
18739
|
-
streamId: string().optional()
|
|
18740
|
-
}), SnapshotImageSchema.nullable());
|
|
18741
18913
|
/**
|
|
18742
18914
|
* `sso-bridge` — internal hub-only cap that lets SSO-style auth
|
|
18743
18915
|
* providers (OIDC, SAML, magic-link, …) mint an HMAC-signed token
|
|
@@ -19098,9 +19270,10 @@ method(object({
|
|
|
19098
19270
|
auth: "admin"
|
|
19099
19271
|
});
|
|
19100
19272
|
/**
|
|
19101
|
-
* Optional client-side hints sent at session creation to help the
|
|
19102
|
-
*
|
|
19103
|
-
*
|
|
19273
|
+
* Optional client-side hints sent at session creation to help the provider
|
|
19274
|
+
* pick the best native source. All fields optional — a viewer that knows
|
|
19275
|
+
* nothing still gets a sane default. (Relocated from the retired `webrtc`
|
|
19276
|
+
* collection cap; this `webrtc-session` cap is the live signaling surface.)
|
|
19104
19277
|
*/
|
|
19105
19278
|
var webrtcClientHintsSchema = object({
|
|
19106
19279
|
viewportWidth: number().int().positive().optional(),
|
|
@@ -19111,22 +19284,6 @@ var webrtcClientHintsSchema = object({
|
|
|
19111
19284
|
/** Hard tier override; takes precedence over scoring when registered. */
|
|
19112
19285
|
prefersTier: string().optional()
|
|
19113
19286
|
}).partial();
|
|
19114
|
-
method(object({
|
|
19115
|
-
streamId: string(),
|
|
19116
|
-
sdpOffer: string()
|
|
19117
|
-
}), string(), { kind: "mutation" }), method(object({ streamId: string() }), boolean()), method(object({
|
|
19118
|
-
streamId: string(),
|
|
19119
|
-
codec: string()
|
|
19120
|
-
}), _void(), { kind: "mutation" }), method(object({ streamId: string() }), _void(), { kind: "mutation" }), method(object({
|
|
19121
|
-
streamId: string(),
|
|
19122
|
-
hints: webrtcClientHintsSchema.optional()
|
|
19123
|
-
}), object({
|
|
19124
|
-
sessionId: string(),
|
|
19125
|
-
sdpOffer: string()
|
|
19126
|
-
}), { kind: "mutation" }), method(object({
|
|
19127
|
-
sessionId: string(),
|
|
19128
|
-
sdpAnswer: string()
|
|
19129
|
-
}), _void(), { kind: "mutation" }), method(object({ sessionId: string() }), _void(), { kind: "mutation" }), method(object({ streamId: string() }), boolean());
|
|
19130
19287
|
/**
|
|
19131
19288
|
* Discriminated target for a WebRTC session. The client sends this
|
|
19132
19289
|
* structured object instead of building / parsing brokerId strings;
|
|
@@ -19613,7 +19770,15 @@ var FrameworkPackageStatusSchema = object({
|
|
|
19613
19770
|
latestVersion: string().nullable(),
|
|
19614
19771
|
hasUpdate: boolean(),
|
|
19615
19772
|
/** Optional manifest description for the row tooltip. */
|
|
19616
|
-
description: string().optional()
|
|
19773
|
+
description: string().optional(),
|
|
19774
|
+
/**
|
|
19775
|
+
* Content build-id (md5 of the resolved `dist/` tree) of the code the hub
|
|
19776
|
+
* ACTUALLY loaded. Framework packages ship code changes without always
|
|
19777
|
+
* bumping `currentVersion`, so semver alone hides "same version, new code".
|
|
19778
|
+
* `null` when the dist can't be hashed (not installed / empty). The admin-UI
|
|
19779
|
+
* surfaces this so a stale-code hub is visible even at an unchanged version.
|
|
19780
|
+
*/
|
|
19781
|
+
buildId: string().nullable()
|
|
19617
19782
|
});
|
|
19618
19783
|
var LogStreamEntrySchema = object({
|
|
19619
19784
|
timestamp: string(),
|
|
@@ -24174,6 +24339,12 @@ Object.freeze({
|
|
|
24174
24339
|
addonId: null,
|
|
24175
24340
|
access: "create"
|
|
24176
24341
|
},
|
|
24342
|
+
"pipelineExecutor.resetToDefault": {
|
|
24343
|
+
capName: "pipeline-executor",
|
|
24344
|
+
capScope: "system",
|
|
24345
|
+
addonId: null,
|
|
24346
|
+
access: "delete"
|
|
24347
|
+
},
|
|
24177
24348
|
"pipelineExecutor.runAudioTest": {
|
|
24178
24349
|
capName: "pipeline-executor",
|
|
24179
24350
|
capScope: "system",
|
|
@@ -24222,6 +24393,12 @@ Object.freeze({
|
|
|
24222
24393
|
addonId: null,
|
|
24223
24394
|
access: "create"
|
|
24224
24395
|
},
|
|
24396
|
+
"pipelineExecutor.validatePipeline": {
|
|
24397
|
+
capName: "pipeline-executor",
|
|
24398
|
+
capScope: "system",
|
|
24399
|
+
addonId: null,
|
|
24400
|
+
access: "view"
|
|
24401
|
+
},
|
|
24225
24402
|
"pipelineOrchestrator.assignAudio": {
|
|
24226
24403
|
capName: "pipeline-orchestrator",
|
|
24227
24404
|
capScope: "system",
|
|
@@ -24330,6 +24507,12 @@ Object.freeze({
|
|
|
24330
24507
|
addonId: null,
|
|
24331
24508
|
access: "view"
|
|
24332
24509
|
},
|
|
24510
|
+
"pipelineOrchestrator.getIngestOwner": {
|
|
24511
|
+
capName: "pipeline-orchestrator",
|
|
24512
|
+
capScope: "system",
|
|
24513
|
+
addonId: null,
|
|
24514
|
+
access: "view"
|
|
24515
|
+
},
|
|
24333
24516
|
"pipelineOrchestrator.getPipelineAssignment": {
|
|
24334
24517
|
capName: "pipeline-orchestrator",
|
|
24335
24518
|
capScope: "system",
|
|
@@ -24402,6 +24585,12 @@ Object.freeze({
|
|
|
24402
24585
|
addonId: null,
|
|
24403
24586
|
access: "create"
|
|
24404
24587
|
},
|
|
24588
|
+
"pipelineOrchestrator.setAgentReachableHost": {
|
|
24589
|
+
capName: "pipeline-orchestrator",
|
|
24590
|
+
capScope: "system",
|
|
24591
|
+
addonId: null,
|
|
24592
|
+
access: "create"
|
|
24593
|
+
},
|
|
24405
24594
|
"pipelineOrchestrator.setCameraPipelineForAgent": {
|
|
24406
24595
|
capName: "pipeline-orchestrator",
|
|
24407
24596
|
capScope: "system",
|
|
@@ -24732,24 +24921,6 @@ Object.freeze({
|
|
|
24732
24921
|
addonId: null,
|
|
24733
24922
|
access: "create"
|
|
24734
24923
|
},
|
|
24735
|
-
"restreamer.getExposedResources": {
|
|
24736
|
-
capName: "restreamer",
|
|
24737
|
-
capScope: "system",
|
|
24738
|
-
addonId: null,
|
|
24739
|
-
access: "view"
|
|
24740
|
-
},
|
|
24741
|
-
"restreamer.registerDevice": {
|
|
24742
|
-
capName: "restreamer",
|
|
24743
|
-
capScope: "system",
|
|
24744
|
-
addonId: null,
|
|
24745
|
-
access: "create"
|
|
24746
|
-
},
|
|
24747
|
-
"restreamer.unregisterDevice": {
|
|
24748
|
-
capName: "restreamer",
|
|
24749
|
-
capScope: "system",
|
|
24750
|
-
addonId: null,
|
|
24751
|
-
access: "delete"
|
|
24752
|
-
},
|
|
24753
24924
|
"scriptRunner.run": {
|
|
24754
24925
|
capName: "script-runner",
|
|
24755
24926
|
capScope: "device",
|
|
@@ -24852,18 +25023,6 @@ Object.freeze({
|
|
|
24852
25023
|
addonId: null,
|
|
24853
25024
|
access: "create"
|
|
24854
25025
|
},
|
|
24855
|
-
"snapshotProvider.getSnapshot": {
|
|
24856
|
-
capName: "snapshot-provider",
|
|
24857
|
-
capScope: "system",
|
|
24858
|
-
addonId: null,
|
|
24859
|
-
access: "view"
|
|
24860
|
-
},
|
|
24861
|
-
"snapshotProvider.supportsDevice": {
|
|
24862
|
-
capName: "snapshot-provider",
|
|
24863
|
-
capScope: "system",
|
|
24864
|
-
addonId: null,
|
|
24865
|
-
access: "view"
|
|
24866
|
-
},
|
|
24867
25026
|
"ssoBridge.signBridgeToken": {
|
|
24868
25027
|
capName: "sso-bridge",
|
|
24869
25028
|
capScope: "system",
|
|
@@ -25290,30 +25449,6 @@ Object.freeze({
|
|
|
25290
25449
|
addonId: null,
|
|
25291
25450
|
access: "view"
|
|
25292
25451
|
},
|
|
25293
|
-
"streamingEngine.getStreamUrl": {
|
|
25294
|
-
capName: "streaming-engine",
|
|
25295
|
-
capScope: "system",
|
|
25296
|
-
addonId: null,
|
|
25297
|
-
access: "view"
|
|
25298
|
-
},
|
|
25299
|
-
"streamingEngine.listStreams": {
|
|
25300
|
-
capName: "streaming-engine",
|
|
25301
|
-
capScope: "system",
|
|
25302
|
-
addonId: null,
|
|
25303
|
-
access: "view"
|
|
25304
|
-
},
|
|
25305
|
-
"streamingEngine.registerStream": {
|
|
25306
|
-
capName: "streaming-engine",
|
|
25307
|
-
capScope: "system",
|
|
25308
|
-
addonId: null,
|
|
25309
|
-
access: "create"
|
|
25310
|
-
},
|
|
25311
|
-
"streamingEngine.unregisterStream": {
|
|
25312
|
-
capName: "streaming-engine",
|
|
25313
|
-
capScope: "system",
|
|
25314
|
-
addonId: null,
|
|
25315
|
-
access: "delete"
|
|
25316
|
-
},
|
|
25317
25452
|
"streamParams.getConfigSchema": {
|
|
25318
25453
|
capName: "stream-params",
|
|
25319
25454
|
capScope: "device",
|
|
@@ -25680,54 +25815,6 @@ Object.freeze({
|
|
|
25680
25815
|
addonId: null,
|
|
25681
25816
|
access: "create"
|
|
25682
25817
|
},
|
|
25683
|
-
"webrtc.closeSession": {
|
|
25684
|
-
capName: "webrtc",
|
|
25685
|
-
capScope: "system",
|
|
25686
|
-
addonId: null,
|
|
25687
|
-
access: "create"
|
|
25688
|
-
},
|
|
25689
|
-
"webrtc.createSession": {
|
|
25690
|
-
capName: "webrtc",
|
|
25691
|
-
capScope: "system",
|
|
25692
|
-
addonId: null,
|
|
25693
|
-
access: "create"
|
|
25694
|
-
},
|
|
25695
|
-
"webrtc.handleAnswer": {
|
|
25696
|
-
capName: "webrtc",
|
|
25697
|
-
capScope: "system",
|
|
25698
|
-
addonId: null,
|
|
25699
|
-
access: "create"
|
|
25700
|
-
},
|
|
25701
|
-
"webrtc.handleOffer": {
|
|
25702
|
-
capName: "webrtc",
|
|
25703
|
-
capScope: "system",
|
|
25704
|
-
addonId: null,
|
|
25705
|
-
access: "create"
|
|
25706
|
-
},
|
|
25707
|
-
"webrtc.hasAdaptiveBitrate": {
|
|
25708
|
-
capName: "webrtc",
|
|
25709
|
-
capScope: "system",
|
|
25710
|
-
addonId: null,
|
|
25711
|
-
access: "view"
|
|
25712
|
-
},
|
|
25713
|
-
"webrtc.registerStream": {
|
|
25714
|
-
capName: "webrtc",
|
|
25715
|
-
capScope: "system",
|
|
25716
|
-
addonId: null,
|
|
25717
|
-
access: "create"
|
|
25718
|
-
},
|
|
25719
|
-
"webrtc.supportsStream": {
|
|
25720
|
-
capName: "webrtc",
|
|
25721
|
-
capScope: "system",
|
|
25722
|
-
addonId: null,
|
|
25723
|
-
access: "view"
|
|
25724
|
-
},
|
|
25725
|
-
"webrtc.unregisterStream": {
|
|
25726
|
-
capName: "webrtc",
|
|
25727
|
-
capScope: "system",
|
|
25728
|
-
addonId: null,
|
|
25729
|
-
access: "delete"
|
|
25730
|
-
},
|
|
25731
25818
|
"webrtcSession.addIceCandidate": {
|
|
25732
25819
|
capName: "webrtc-session",
|
|
25733
25820
|
capScope: "device",
|
|
@@ -25941,6 +26028,72 @@ function maskUrlCredentials(rawUrl) {
|
|
|
25941
26028
|
return rawUrl;
|
|
25942
26029
|
}
|
|
25943
26030
|
}
|
|
26031
|
+
//#endregion
|
|
26032
|
+
//#region src/rtsp-reachability.ts
|
|
26033
|
+
/**
|
|
26034
|
+
* Control-plane reachability probe for a generic RTSP camera.
|
|
26035
|
+
*
|
|
26036
|
+
* Unlike Hikvision/Amcrest/ONVIF, an RTSP source exposes NO management API —
|
|
26037
|
+
* we only have an `rtsp://` URL. Reachability is therefore probed at the RTSP
|
|
26038
|
+
* control channel itself:
|
|
26039
|
+
*
|
|
26040
|
+
* 1. open a TCP connection to the RTSP `host:port` (default 554);
|
|
26041
|
+
* 2. best-effort send an RTSP `OPTIONS` request;
|
|
26042
|
+
* 3. treat an `RTSP/…` response OR a successful TCP handshake (even without
|
|
26043
|
+
* a response before the deadline) as REACHABLE.
|
|
26044
|
+
*
|
|
26045
|
+
* A successful TCP handshake already proves the camera's RTSP port is live —
|
|
26046
|
+
* the `OPTIONS` round-trip is a stronger confirmation when the server answers
|
|
26047
|
+
* promptly, but we deliberately fall back to the bare connect so a camera that
|
|
26048
|
+
* gates `OPTIONS` behind auth (or is simply slow to answer) is not wrongly
|
|
26049
|
+
* marked offline.
|
|
26050
|
+
*
|
|
26051
|
+
* The socket is always destroyed; the probe never throws and resolves within
|
|
26052
|
+
* `timeoutMs`.
|
|
26053
|
+
*/
|
|
26054
|
+
var DEFAULT_RTSP_PORT = 554;
|
|
26055
|
+
/** Parse `host` + `port` out of an `rtsp://` URL. Credentials/path ignored. */
|
|
26056
|
+
function parseRtspTarget(url) {
|
|
26057
|
+
try {
|
|
26058
|
+
const parsed = new URL(url);
|
|
26059
|
+
if (!parsed.hostname) return null;
|
|
26060
|
+
const port = parsed.port ? Number.parseInt(parsed.port, 10) : DEFAULT_RTSP_PORT;
|
|
26061
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) return null;
|
|
26062
|
+
return {
|
|
26063
|
+
host: parsed.hostname,
|
|
26064
|
+
port
|
|
26065
|
+
};
|
|
26066
|
+
} catch {
|
|
26067
|
+
return null;
|
|
26068
|
+
}
|
|
26069
|
+
}
|
|
26070
|
+
async function probeRtspReachable(url, timeoutMs) {
|
|
26071
|
+
const target = parseRtspTarget(url);
|
|
26072
|
+
if (!target) return false;
|
|
26073
|
+
return new Promise((resolve) => {
|
|
26074
|
+
let settled = false;
|
|
26075
|
+
let connected = false;
|
|
26076
|
+
const socket = new node_net.default.Socket();
|
|
26077
|
+
const finish = (reachable) => {
|
|
26078
|
+
if (settled) return;
|
|
26079
|
+
settled = true;
|
|
26080
|
+
socket.destroy();
|
|
26081
|
+
resolve(reachable);
|
|
26082
|
+
};
|
|
26083
|
+
socket.setTimeout(timeoutMs);
|
|
26084
|
+
socket.once("timeout", () => finish(connected));
|
|
26085
|
+
socket.once("error", () => finish(false));
|
|
26086
|
+
socket.once("connect", () => {
|
|
26087
|
+
connected = true;
|
|
26088
|
+
const request = `OPTIONS ${url} RTSP/1.0\r\nCSeq: 1\r\nUser-Agent: camstack-reachability\r\n\r\n`;
|
|
26089
|
+
socket.write(request, () => {});
|
|
26090
|
+
});
|
|
26091
|
+
socket.once("data", (buf) => {
|
|
26092
|
+
finish(buf.toString("latin1").startsWith("RTSP/") || connected);
|
|
26093
|
+
});
|
|
26094
|
+
socket.connect(target.port, target.host);
|
|
26095
|
+
});
|
|
26096
|
+
}
|
|
25944
26097
|
var rtspCameraSchema = object({
|
|
25945
26098
|
streams: preprocess((raw) => {
|
|
25946
26099
|
if (!Array.isArray(raw)) return raw;
|
|
@@ -25978,15 +26131,44 @@ var legacyStreamSchema = object({
|
|
|
25978
26131
|
label: CamProfileSchema,
|
|
25979
26132
|
url: string()
|
|
25980
26133
|
});
|
|
25981
|
-
var RtspCamera = class extends BaseDevice {
|
|
26134
|
+
var RtspCamera = class RtspCamera extends BaseDevice {
|
|
25982
26135
|
type = DeviceType.Camera;
|
|
25983
26136
|
features = [];
|
|
26137
|
+
/** Control-plane reachability poll — an RTSP `OPTIONS`/TCP-connect probe to
|
|
26138
|
+
* the camera's RTSP port every 30s drives `device.online`, decoupled from
|
|
26139
|
+
* stream-broker video health. Started in `onActivate`, stopped in
|
|
26140
|
+
* `removeDevice`. */
|
|
26141
|
+
reachabilityPoll = null;
|
|
25984
26142
|
constructor(ctx) {
|
|
25985
26143
|
super(ctx, rtspCameraSchema, { type: DeviceType.Camera });
|
|
25986
26144
|
this.migrateLegacyStreamsIfNeeded(ctx.persistedConfig ?? {});
|
|
25987
26145
|
this.registerSnapshotProvider();
|
|
25988
26146
|
this.registerStreamCatalogProvider();
|
|
25989
26147
|
}
|
|
26148
|
+
/** Phase 5 — device is live: begin the control-plane reachability poll. */
|
|
26149
|
+
async onActivate() {
|
|
26150
|
+
this.startReachabilityPolling();
|
|
26151
|
+
}
|
|
26152
|
+
/** Start the RTSP reachability poll: a lightweight `OPTIONS`/TCP-connect
|
|
26153
|
+
* probe to the first configured stream's `host:port` every 30s drives
|
|
26154
|
+
* `device.online`, with hysteresis. Idempotent. */
|
|
26155
|
+
startReachabilityPolling() {
|
|
26156
|
+
if (this.reachabilityPoll) return;
|
|
26157
|
+
this.reachabilityPoll = startReachabilityPoll({
|
|
26158
|
+
probe: async () => {
|
|
26159
|
+
const url = this.config.get("streams")[0]?.url;
|
|
26160
|
+
if (!url) return false;
|
|
26161
|
+
return probeRtspReachable(url, RtspCamera.RTSP_PROBE_TIMEOUT_MS);
|
|
26162
|
+
},
|
|
26163
|
+
setOnline: (online) => {
|
|
26164
|
+
this.markOnline(online);
|
|
26165
|
+
},
|
|
26166
|
+
isEnabled: () => !this.disabled,
|
|
26167
|
+
logger: this.ctx.logger
|
|
26168
|
+
});
|
|
26169
|
+
}
|
|
26170
|
+
/** Per-probe TCP/RTSP round-trip budget — a dead host resolves as a miss. */
|
|
26171
|
+
static RTSP_PROBE_TIMEOUT_MS = 5e3;
|
|
25990
26172
|
/**
|
|
25991
26173
|
* Build this camera's pull-able stream catalog — the descriptors the
|
|
25992
26174
|
* stream-broker needs to dial. Single source of truth feeding the
|
|
@@ -26077,6 +26259,8 @@ var RtspCamera = class extends BaseDevice {
|
|
|
26077
26259
|
}
|
|
26078
26260
|
async removeDevice() {
|
|
26079
26261
|
this.ctx.logger.info("Removing RTSP camera", { meta: { stableId: this.stableId } });
|
|
26262
|
+
this.reachabilityPoll?.stop();
|
|
26263
|
+
this.reachabilityPoll = null;
|
|
26080
26264
|
const streams = this.config.get("streams");
|
|
26081
26265
|
await Promise.all(streams.map((s) => this.ctx.api.streamBroker.retractCameraStream.mutate({
|
|
26082
26266
|
deviceId: this.id,
|
|
@@ -26354,21 +26538,7 @@ var RtspProviderAddon = class extends BaseDeviceProvider {
|
|
|
26354
26538
|
super({});
|
|
26355
26539
|
}
|
|
26356
26540
|
async onInitialize() {
|
|
26357
|
-
|
|
26358
|
-
this.subscribe({ category: EventCategory.DeviceStateChanged }, (event) => {
|
|
26359
|
-
const data = event.data;
|
|
26360
|
-
if (data.capName !== "camera-streams") return;
|
|
26361
|
-
const deviceId = data.deviceId;
|
|
26362
|
-
if (typeof deviceId !== "number") return;
|
|
26363
|
-
const registry = this.ctx.kernel.deviceRegistry;
|
|
26364
|
-
if (!registry) return;
|
|
26365
|
-
if (registry.getAddonId(deviceId) !== this.addonId) return;
|
|
26366
|
-
const device = registry.getById(deviceId);
|
|
26367
|
-
if (!device) return;
|
|
26368
|
-
const online = data.slice?.online === true;
|
|
26369
|
-
if (device.online !== online) device.online = online;
|
|
26370
|
-
});
|
|
26371
|
-
return regs;
|
|
26541
|
+
return await super.onInitialize();
|
|
26372
26542
|
}
|
|
26373
26543
|
async onGetCreationSchema(type) {
|
|
26374
26544
|
if (type !== DeviceType.Camera) return null;
|