@camstack/addon-provider-rtsp 1.1.19 → 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 +393 -231
- package/dist/addon.mjs +370 -231
- package/package.json +1 -1
package/dist/addon.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import net from "node:net";
|
|
1
2
|
//#region ../../node_modules/zod/v4/core/core.js
|
|
2
3
|
var _a$1;
|
|
3
4
|
function $constructor(name, initializer, params) {
|
|
@@ -4641,7 +4642,7 @@ function preprocess(fn, schema) {
|
|
|
4641
4642
|
});
|
|
4642
4643
|
}
|
|
4643
4644
|
//#endregion
|
|
4644
|
-
//#region ../types/dist/sleep-
|
|
4645
|
+
//#region ../types/dist/sleep-Cc14_yxc.mjs
|
|
4645
4646
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4646
4647
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4647
4648
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -4827,6 +4828,18 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
4827
4828
|
*/
|
|
4828
4829
|
EventCategory["PipelineCameraUpdated"] = "pipeline.camera-updated";
|
|
4829
4830
|
/**
|
|
4831
|
+
* The cluster camera-source OWNER changed (`clusterRoles.ingestNode`).
|
|
4832
|
+
* Emitted by addon-pipeline-orchestrator whenever it (re)derives node
|
|
4833
|
+
* capabilities — at boot, on agent online/offline, and on an ingest-node
|
|
4834
|
+
* flip. Carries the resolved `ownerNodeId`. The stream-broker consumes it to
|
|
4835
|
+
* keep its ingest-owner-gate decision current WITHOUT a per-`ensureBroker`
|
|
4836
|
+
* cross-process `getIngestOwner` query (push the authority's decision instead
|
|
4837
|
+
* of polling it on the hot path). Idempotent state — re-emitted on every
|
|
4838
|
+
* topology change, so a dropped event self-heals on the next one (plus the
|
|
4839
|
+
* broker's long backstop reconcile query).
|
|
4840
|
+
*/
|
|
4841
|
+
EventCategory["PipelineIngestOwnerChanged"] = "pipeline.ingest-owner-changed";
|
|
4842
|
+
/**
|
|
4830
4843
|
* Periodic snapshot of per-node pipeline-runner load
|
|
4831
4844
|
* (`RunnerLocalLoad`). Emitted ~1Hz by every runner so UI dashboards
|
|
4832
4845
|
* subscribe instead of polling `pipelineRunner.getLocalLoad`.
|
|
@@ -6910,6 +6923,36 @@ var ModelFormatsSchema = object({
|
|
|
6910
6923
|
tflite: ModelFormatEntrySchema.optional(),
|
|
6911
6924
|
pt: ModelFormatEntrySchema.optional()
|
|
6912
6925
|
});
|
|
6926
|
+
/**
|
|
6927
|
+
* Variant-selector grouping axes. Shared by the full `ModelCatalogEntry` and by
|
|
6928
|
+
* the reduced `PipelineModelOption` returned in `pipeline.getSchema()` so the
|
|
6929
|
+
* grouped Family→Tier→Variant picker renders identically in the config UI and
|
|
6930
|
+
* in the pipeline/device steppers. The flat `id` stays the source of truth for
|
|
6931
|
+
* resolution/download/persistence; this is a presentation overlay resolved back
|
|
6932
|
+
* to an `id`.
|
|
6933
|
+
*/
|
|
6934
|
+
var ModelVariantGroupSchema = object({
|
|
6935
|
+
/** Top-level family, e.g. `yolo26` (later `d-fine`, `rf-detr`). */
|
|
6936
|
+
family: string(),
|
|
6937
|
+
/** Size within the family, e.g. `n` | `s` | `m` | `l`. */
|
|
6938
|
+
tier: string(),
|
|
6939
|
+
/** Quantization axis. Omit ⇒ the fp32 base build. */
|
|
6940
|
+
precision: _enum(["fp32", "int8"]).optional(),
|
|
6941
|
+
/**
|
|
6942
|
+
* Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
|
|
6943
|
+
* latency-optimized export (e.g. ReLU-activation variant) — the slot the
|
|
6944
|
+
* future performance variants plug into.
|
|
6945
|
+
*/
|
|
6946
|
+
optimization: _enum(["standard", "fast"]).optional(),
|
|
6947
|
+
/**
|
|
6948
|
+
* Input-resolution axis (square input side, px). Omit ⇒ the family's native
|
|
6949
|
+
* resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
|
|
6950
|
+
* cheap latency lever — especially on Apple ANE and the Intel N100 — at a
|
|
6951
|
+
* small-object accuracy cost. Mirrors the model's `inputSize` but lifted onto
|
|
6952
|
+
* the group so the selector can offer it as a variant axis.
|
|
6953
|
+
*/
|
|
6954
|
+
resolution: number().int().positive().optional()
|
|
6955
|
+
});
|
|
6913
6956
|
var ModelCatalogEntrySchema = object({
|
|
6914
6957
|
id: string(),
|
|
6915
6958
|
name: string(),
|
|
@@ -6939,7 +6982,43 @@ var ModelCatalogEntrySchema = object({
|
|
|
6939
6982
|
* Auxiliary files required at runtime (labels JSON, charset dict, etc.).
|
|
6940
6983
|
* Downloaded into the same modelsDir alongside the model file.
|
|
6941
6984
|
*/
|
|
6942
|
-
extraFiles: array(ModelExtraFileSchema).readonly().optional()
|
|
6985
|
+
extraFiles: array(ModelExtraFileSchema).readonly().optional(),
|
|
6986
|
+
/**
|
|
6987
|
+
* LEGACY entry — retained in the catalog so a persisted operator selection
|
|
6988
|
+
* still RESOLVES (and can be re-activated), but hidden from the selectable
|
|
6989
|
+
* model list and excluded from the auto format-default pick. Set on the
|
|
6990
|
+
* superseded / consolidated models (older lineages, redundant fp16 IRs) so
|
|
6991
|
+
* the active lineup stays the coherent curated ladder without deleting a
|
|
6992
|
+
* model anyone may still be pinned to. `resolveModelForFormat` keeps honoring
|
|
6993
|
+
* an explicit legacy id that has a build for the node's format.
|
|
6994
|
+
*/
|
|
6995
|
+
legacy: boolean().optional(),
|
|
6996
|
+
/**
|
|
6997
|
+
* Measured quality/latency metadata — populated from the benchmark addon on
|
|
6998
|
+
* the real node classes. Absent = not yet measured (most entries today; the
|
|
6999
|
+
* catalog historically carried only `sizeMB`, a poor cross-architecture
|
|
7000
|
+
* speed proxy). `p95LatencyMs` is keyed by node class (e.g. `n100`, `mac`).
|
|
7001
|
+
*/
|
|
7002
|
+
metrics: object({
|
|
7003
|
+
map50: number().optional(),
|
|
7004
|
+
p95LatencyMs: record(string(), number()).optional()
|
|
7005
|
+
}).optional(),
|
|
7006
|
+
/**
|
|
7007
|
+
* SPDX-ish license id of the model weights (e.g. `AGPL-3.0` for Ultralytics
|
|
7008
|
+
* YOLO26, `GPL-3.0` for YOLOv9, `Apache-2.0` for D-FINE/RF-DETR). Matters for
|
|
7009
|
+
* the retraining addon and any future commercial distribution.
|
|
7010
|
+
*/
|
|
7011
|
+
license: string().optional(),
|
|
7012
|
+
/**
|
|
7013
|
+
* Variant-selector grouping. The UI groups models by `family` + `tier` and
|
|
7014
|
+
* offers `precision` / `optimization` as variant axes WITHIN a tier — so all
|
|
7015
|
+
* of a family's sizes and quantizations collapse into one grouped picker
|
|
7016
|
+
* instead of a flat list of `yolo26s`, `yolo26s-int8`, … Absent ⇒ ungrouped
|
|
7017
|
+
* (legacy / custom models) — never shown in the grouped selector. The flat
|
|
7018
|
+
* `id` stays the source of truth for resolution/download/persistence; grouping
|
|
7019
|
+
* is a presentation overlay resolved back to an `id`.
|
|
7020
|
+
*/
|
|
7021
|
+
group: ModelVariantGroupSchema.optional()
|
|
6943
7022
|
});
|
|
6944
7023
|
var ConvertTargetSchema = discriminatedUnion("format", [object({
|
|
6945
7024
|
format: literal("openvino"),
|
|
@@ -7000,8 +7079,8 @@ var RecordingModeSchema = _enum([
|
|
|
7000
7079
|
"onAudioThreshold"
|
|
7001
7080
|
]);
|
|
7002
7081
|
/**
|
|
7003
|
-
* First-class, authoritative per-camera storage mode — the
|
|
7004
|
-
* reads directly (never inferred from `rules`):
|
|
7082
|
+
* First-class, authoritative per-camera storage mode — the explicit choice the
|
|
7083
|
+
* UI reads directly (never inferred from `rules`):
|
|
7005
7084
|
* - `off` — not recording.
|
|
7006
7085
|
* - `events` — record only around triggers (motion / audio threshold),
|
|
7007
7086
|
* with pre/post-buffer.
|
|
@@ -8533,6 +8612,72 @@ function shallowEqual(a, b) {
|
|
|
8533
8612
|
for (const k of ak) if (a[k] !== b[k]) return false;
|
|
8534
8613
|
return true;
|
|
8535
8614
|
}
|
|
8615
|
+
/** Reject after `ms`; always clears its own timer. */
|
|
8616
|
+
async function withTimeout(promise, ms, label) {
|
|
8617
|
+
let timer;
|
|
8618
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
8619
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${label} timed out after ${String(ms)}ms`)), ms);
|
|
8620
|
+
});
|
|
8621
|
+
try {
|
|
8622
|
+
return await Promise.race([promise, timeout]);
|
|
8623
|
+
} finally {
|
|
8624
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
8625
|
+
}
|
|
8626
|
+
}
|
|
8627
|
+
/**
|
|
8628
|
+
* Start the reachability poll loop. Returns a handle whose `stop()` clears the
|
|
8629
|
+
* timer and prevents any further ticks. Start on device activation, stop on
|
|
8630
|
+
* device teardown (`removeDevice`) so no timer leaks.
|
|
8631
|
+
*/
|
|
8632
|
+
function startReachabilityPoll(options) {
|
|
8633
|
+
const intervalMs = options.intervalMs ?? 3e4;
|
|
8634
|
+
const failuresToOffline = options.failuresToOffline ?? 3;
|
|
8635
|
+
const probeTimeoutMs = options.probeTimeoutMs ?? 1e4;
|
|
8636
|
+
const runImmediately = options.runImmediately ?? true;
|
|
8637
|
+
let stopped = false;
|
|
8638
|
+
let running = false;
|
|
8639
|
+
let consecutiveFailures = 0;
|
|
8640
|
+
let timer;
|
|
8641
|
+
const tick = async () => {
|
|
8642
|
+
if (stopped) return;
|
|
8643
|
+
if (running) return;
|
|
8644
|
+
if (options.isEnabled && !options.isEnabled()) return;
|
|
8645
|
+
running = true;
|
|
8646
|
+
try {
|
|
8647
|
+
const reachable = await withTimeout(Promise.resolve().then(options.probe), probeTimeoutMs, "reachability probe");
|
|
8648
|
+
if (stopped) return;
|
|
8649
|
+
if (reachable) {
|
|
8650
|
+
consecutiveFailures = 0;
|
|
8651
|
+
options.setOnline(true);
|
|
8652
|
+
} else registerFailure("probe resolved unreachable");
|
|
8653
|
+
} catch (error) {
|
|
8654
|
+
if (stopped) return;
|
|
8655
|
+
registerFailure(error instanceof Error ? error.message : "probe threw");
|
|
8656
|
+
} finally {
|
|
8657
|
+
running = false;
|
|
8658
|
+
}
|
|
8659
|
+
};
|
|
8660
|
+
const registerFailure = (reason) => {
|
|
8661
|
+
consecutiveFailures += 1;
|
|
8662
|
+
options.logger?.debug("reachability probe failed", {
|
|
8663
|
+
reason,
|
|
8664
|
+
consecutiveFailures,
|
|
8665
|
+
failuresToOffline
|
|
8666
|
+
});
|
|
8667
|
+
if (consecutiveFailures >= failuresToOffline) options.setOnline(false);
|
|
8668
|
+
};
|
|
8669
|
+
timer = setInterval(() => {
|
|
8670
|
+
tick();
|
|
8671
|
+
}, intervalMs);
|
|
8672
|
+
if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
|
|
8673
|
+
if (runImmediately) tick();
|
|
8674
|
+
return { stop: () => {
|
|
8675
|
+
if (stopped) return;
|
|
8676
|
+
stopped = true;
|
|
8677
|
+
if (timer !== void 0) clearInterval(timer);
|
|
8678
|
+
timer = void 0;
|
|
8679
|
+
} };
|
|
8680
|
+
}
|
|
8536
8681
|
/**
|
|
8537
8682
|
* Generic device-level status snapshot. Auto-registered by `BaseDevice`
|
|
8538
8683
|
* for every device, regardless of provider — the kernel needs a uniform
|
|
@@ -9196,26 +9341,13 @@ onBrightnessChanged: { data: object({
|
|
|
9196
9341
|
*/
|
|
9197
9342
|
runtimeState: BrightnessStatusSchema
|
|
9198
9343
|
};
|
|
9344
|
+
/** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
|
|
9199
9345
|
var StreamFormatSchema = _enum([
|
|
9200
9346
|
"webrtc",
|
|
9201
9347
|
"hls",
|
|
9202
9348
|
"mjpeg",
|
|
9203
9349
|
"rtsp"
|
|
9204
9350
|
]);
|
|
9205
|
-
var StreamInfoSchema = object({
|
|
9206
|
-
streamId: string(),
|
|
9207
|
-
format: StreamFormatSchema,
|
|
9208
|
-
url: string().nullable(),
|
|
9209
|
-
active: boolean()
|
|
9210
|
-
});
|
|
9211
|
-
method(object({
|
|
9212
|
-
streamId: string(),
|
|
9213
|
-
sourceUrl: string(),
|
|
9214
|
-
codec: string().optional()
|
|
9215
|
-
}), _void(), { kind: "mutation" }), method(object({ streamId: string() }), _void(), { kind: "mutation" }), method(object({
|
|
9216
|
-
streamId: string(),
|
|
9217
|
-
format: StreamFormatSchema
|
|
9218
|
-
}), string().nullable()), method(_void(), array(StreamInfoSchema));
|
|
9219
9351
|
var RtspRestreamEntrySchema = object({
|
|
9220
9352
|
brokerId: string(),
|
|
9221
9353
|
url: string(),
|
|
@@ -10083,37 +10215,7 @@ var consumablesCapability = {
|
|
|
10083
10215
|
scope: "device",
|
|
10084
10216
|
deviceNative: true,
|
|
10085
10217
|
mode: "singleton",
|
|
10086
|
-
deviceTypes:
|
|
10087
|
-
DeviceType.Camera,
|
|
10088
|
-
DeviceType.Hub,
|
|
10089
|
-
DeviceType.Light,
|
|
10090
|
-
DeviceType.Siren,
|
|
10091
|
-
DeviceType.Switch,
|
|
10092
|
-
DeviceType.Sensor,
|
|
10093
|
-
DeviceType.Thermostat,
|
|
10094
|
-
DeviceType.Button,
|
|
10095
|
-
DeviceType.EventEmitter,
|
|
10096
|
-
DeviceType.Update,
|
|
10097
|
-
DeviceType.Generic,
|
|
10098
|
-
DeviceType.Notifier,
|
|
10099
|
-
DeviceType.Script,
|
|
10100
|
-
DeviceType.Automation,
|
|
10101
|
-
DeviceType.Lock,
|
|
10102
|
-
DeviceType.Cover,
|
|
10103
|
-
DeviceType.Valve,
|
|
10104
|
-
DeviceType.Humidifier,
|
|
10105
|
-
DeviceType.WaterHeater,
|
|
10106
|
-
DeviceType.Fan,
|
|
10107
|
-
DeviceType.MediaPlayer,
|
|
10108
|
-
DeviceType.AlarmPanel,
|
|
10109
|
-
DeviceType.Control,
|
|
10110
|
-
DeviceType.Presence,
|
|
10111
|
-
DeviceType.Weather,
|
|
10112
|
-
DeviceType.Vacuum,
|
|
10113
|
-
DeviceType.LawnMower,
|
|
10114
|
-
DeviceType.Container,
|
|
10115
|
-
DeviceType.Image
|
|
10116
|
-
],
|
|
10218
|
+
deviceTypes: Object.values(DeviceType),
|
|
10117
10219
|
deviceConfig: { ui: {
|
|
10118
10220
|
kind: "widget",
|
|
10119
10221
|
widgetId: "host/consumables-panel",
|
|
@@ -11706,7 +11808,6 @@ var PipelineDefaultStepSchema = lazy(() => object({
|
|
|
11706
11808
|
enabled: boolean(),
|
|
11707
11809
|
modelId: string(),
|
|
11708
11810
|
children: array(PipelineDefaultStepSchema).readonly(),
|
|
11709
|
-
engine: PipelineEngineChoiceSchema.optional(),
|
|
11710
11811
|
group: string().optional(),
|
|
11711
11812
|
settings: record(string(), unknown()).optional()
|
|
11712
11813
|
}));
|
|
@@ -11731,7 +11832,9 @@ var PipelineModelOptionSchema = object({
|
|
|
11731
11832
|
formats: record(string(), object({
|
|
11732
11833
|
downloaded: boolean(),
|
|
11733
11834
|
sizeMB: number()
|
|
11734
|
-
}))
|
|
11835
|
+
})),
|
|
11836
|
+
group: ModelVariantGroupSchema.optional(),
|
|
11837
|
+
legacy: boolean().optional()
|
|
11735
11838
|
});
|
|
11736
11839
|
var ConfigFieldBridge = custom();
|
|
11737
11840
|
var PipelineAddonSchemaSchema = object({
|
|
@@ -11782,15 +11885,42 @@ var EngineProvisioningSchema = object({
|
|
|
11782
11885
|
]),
|
|
11783
11886
|
progress: number().optional(),
|
|
11784
11887
|
error: string().optional(),
|
|
11785
|
-
nextRetryAt: number().optional()
|
|
11888
|
+
nextRetryAt: number().optional(),
|
|
11889
|
+
/**
|
|
11890
|
+
* Gate A (config-correctness gate at engine change): human-readable
|
|
11891
|
+
* config issues surfaced EAGERLY when the node's engine changes — model
|
|
11892
|
+
* substitutions ("chose X, running Y") and zero-build steps ("no model
|
|
11893
|
+
* has a <format> build"). Additive/optional: informational only, never
|
|
11894
|
+
* enforced here — `assertEngineReady` (readiness) still gates inference.
|
|
11895
|
+
* Absent/empty when the node-default tree resolves cleanly.
|
|
11896
|
+
*/
|
|
11897
|
+
configIssues: array(string()).optional()
|
|
11786
11898
|
});
|
|
11787
11899
|
var PipelineStepInputSchema = lazy(() => object({
|
|
11788
11900
|
addonId: string(),
|
|
11789
|
-
modelId: string(),
|
|
11901
|
+
modelId: string().optional(),
|
|
11790
11902
|
enabled: boolean().default(true),
|
|
11791
11903
|
children: array(PipelineStepInputSchema).optional(),
|
|
11792
11904
|
settings: record(string(), unknown()).optional()
|
|
11793
11905
|
}));
|
|
11906
|
+
var ModelSubstitutionSchema = object({
|
|
11907
|
+
addonId: string(),
|
|
11908
|
+
chosen: string(),
|
|
11909
|
+
running: string(),
|
|
11910
|
+
format: string()
|
|
11911
|
+
});
|
|
11912
|
+
var PipelineValidationIssueSchema = object({
|
|
11913
|
+
addonId: string(),
|
|
11914
|
+
kind: _enum(["unknown-addon", "no-format-build"]),
|
|
11915
|
+
detail: string()
|
|
11916
|
+
});
|
|
11917
|
+
var PipelineValidationResultSchema = object({
|
|
11918
|
+
ok: boolean(),
|
|
11919
|
+
issues: array(PipelineValidationIssueSchema).readonly(),
|
|
11920
|
+
substitutions: array(ModelSubstitutionSchema).readonly(),
|
|
11921
|
+
/** The node's `currentEngine.format` this validation ran against. */
|
|
11922
|
+
format: string()
|
|
11923
|
+
});
|
|
11794
11924
|
var ReferenceImageEntrySchema = object({
|
|
11795
11925
|
filename: string(),
|
|
11796
11926
|
stepIds: array(string()).readonly().optional()
|
|
@@ -11861,7 +11991,13 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
|
|
|
11861
11991
|
})) }), object({ success: literal(true) }), {
|
|
11862
11992
|
kind: "mutation",
|
|
11863
11993
|
auth: "admin"
|
|
11864
|
-
}), method(
|
|
11994
|
+
}), method(object({ nodeId: string() }), object({
|
|
11995
|
+
success: literal(true),
|
|
11996
|
+
regeneratedModelId: string().nullable()
|
|
11997
|
+
}), {
|
|
11998
|
+
kind: "mutation",
|
|
11999
|
+
auth: "admin"
|
|
12000
|
+
}), 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({
|
|
11865
12001
|
name: string(),
|
|
11866
12002
|
steps: array(PipelineTemplateStepSchema).readonly(),
|
|
11867
12003
|
engine: PipelineEngineChoiceSchema
|
|
@@ -12154,6 +12290,13 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
|
|
|
12154
12290
|
kind: literal("remote-restream"),
|
|
12155
12291
|
/** The camera's source-owner node (slice 1: always the hub). */
|
|
12156
12292
|
ownerNodeId: string(),
|
|
12293
|
+
/**
|
|
12294
|
+
* The owner's LAN-reachable host, resolved by the orchestrator from the
|
|
12295
|
+
* per-node `reachableHost` override (Cluster UI). When present the runner
|
|
12296
|
+
* dials THIS host for the owner's restream, in preference to the
|
|
12297
|
+
* `CAMSTACK_HUB_URL`-derived default. Absent → auto-detect fallback.
|
|
12298
|
+
*/
|
|
12299
|
+
ownerReachableHost: string().optional(),
|
|
12157
12300
|
/** Operator override for the owner host the runner dials. */
|
|
12158
12301
|
hubHostnameOverride: string().optional()
|
|
12159
12302
|
})]).describe("Per-camera frame-source mode for the runner (P2c)");
|
|
@@ -12162,13 +12305,11 @@ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal
|
|
|
12162
12305
|
* specific runner instance via `attachCamera`. Carries everything the
|
|
12163
12306
|
* runner needs to subscribe to the local broker and execute inference.
|
|
12164
12307
|
*
|
|
12165
|
-
* Stateless-pipeline model: the
|
|
12166
|
-
*
|
|
12167
|
-
*
|
|
12168
|
-
*
|
|
12169
|
-
*
|
|
12170
|
-
* `engine`/`steps`/`audio` are optional during the additive migration
|
|
12171
|
-
* window; once orchestrator + UI are migrated they become required.
|
|
12308
|
+
* Stateless-pipeline model: the pipeline content (`steps`, optional
|
|
12309
|
+
* `audio`) travels with the attach payload. The runner keeps it in RAM
|
|
12310
|
+
* for the lifetime of the attach — on rebalance, edit, or restart the
|
|
12311
|
+
* orchestrator re-sends the latest snapshot. Engine is NOT carried: it is
|
|
12312
|
+
* node-local, resolved by the executing runner at dispatch time.
|
|
12172
12313
|
*/
|
|
12173
12314
|
var RunnerCameraConfigSchema = object({
|
|
12174
12315
|
deviceId: number(),
|
|
@@ -12219,14 +12360,11 @@ var RunnerCameraConfigSchema = object({
|
|
|
12219
12360
|
*/
|
|
12220
12361
|
motionSources: MotionSourcesSchema.default(["analyzer"]),
|
|
12221
12362
|
pipelineEnabled: boolean().default(true),
|
|
12222
|
-
/** Engine choice for video steps (runtime+backend+format). */
|
|
12223
|
-
engine: PipelineEngineChoiceSchema.optional(),
|
|
12224
12363
|
/** Ordered tree of video steps. Absent → runner skips video detection. */
|
|
12225
12364
|
steps: array(PipelineStepInputSchema).readonly().optional(),
|
|
12226
12365
|
/** Audio classification branch. `enabled:false` disables, null skips. */
|
|
12227
12366
|
audio: object({
|
|
12228
|
-
|
|
12229
|
-
modelId: string(),
|
|
12367
|
+
modelId: string().optional(),
|
|
12230
12368
|
enabled: boolean()
|
|
12231
12369
|
}).nullable().optional(),
|
|
12232
12370
|
/**
|
|
@@ -18089,11 +18227,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
18089
18227
|
timestamp: number()
|
|
18090
18228
|
});
|
|
18091
18229
|
var CameraPipelineConfigSchema = object({
|
|
18092
|
-
engine: PipelineEngineChoiceSchema,
|
|
18230
|
+
engine: PipelineEngineChoiceSchema.optional(),
|
|
18093
18231
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
18094
18232
|
audio: object({
|
|
18095
|
-
engine: PipelineEngineChoiceSchema,
|
|
18096
|
-
modelId: string(),
|
|
18233
|
+
engine: PipelineEngineChoiceSchema.optional(),
|
|
18234
|
+
modelId: string().optional(),
|
|
18097
18235
|
enabled: boolean(),
|
|
18098
18236
|
settings: record(string(), unknown()).readonly().optional()
|
|
18099
18237
|
}).nullable().optional()
|
|
@@ -18108,7 +18246,7 @@ var PipelineTemplateSchema = object({
|
|
|
18108
18246
|
});
|
|
18109
18247
|
var AgentAddonConfigSchema = object({
|
|
18110
18248
|
enabled: boolean(),
|
|
18111
|
-
modelId: string(),
|
|
18249
|
+
modelId: string().optional(),
|
|
18112
18250
|
settings: record(string(), unknown()).readonly()
|
|
18113
18251
|
});
|
|
18114
18252
|
var AgentPipelineSettingsSchema = object({
|
|
@@ -18123,7 +18261,15 @@ var AgentPipelineSettingsSchema = object({
|
|
|
18123
18261
|
/** Node is eligible to run audio-analyzer sessions. */
|
|
18124
18262
|
audio: boolean().optional(),
|
|
18125
18263
|
/** Node is eligible to be the ingest / source-owner (serve the restream). */
|
|
18126
|
-
ingest: boolean().optional()
|
|
18264
|
+
ingest: boolean().optional(),
|
|
18265
|
+
/**
|
|
18266
|
+
* Operator override for the LAN host a cross-node decoder dials to reach
|
|
18267
|
+
* THIS node's restream (Cluster UI). Absent → auto-detect: a remote runner
|
|
18268
|
+
* falls back to its `CAMSTACK_HUB_URL`-derived host (the Moleculer address
|
|
18269
|
+
* it already uses to reach the hub). Set this only when the auto-detected
|
|
18270
|
+
* address is wrong (multi-homed host, NAT, custom interface).
|
|
18271
|
+
*/
|
|
18272
|
+
reachableHost: string().optional()
|
|
18127
18273
|
});
|
|
18128
18274
|
var CameraPipelineForAgentSchema = object({
|
|
18129
18275
|
steps: array(PipelineStepInputSchema).readonly(),
|
|
@@ -18229,6 +18375,15 @@ var GlobalMetricsSchema = object({
|
|
|
18229
18375
|
* capability providers.
|
|
18230
18376
|
*/
|
|
18231
18377
|
var CapabilityBindingsSchema = record(string(), string());
|
|
18378
|
+
/**
|
|
18379
|
+
* The cluster's single camera-source owner (`clusterRoles.ingestNode`) plus
|
|
18380
|
+
* its LAN-reachable host, if one is registered. See `getIngestOwner`.
|
|
18381
|
+
*/
|
|
18382
|
+
var IngestOwnerSchema = object({
|
|
18383
|
+
ownerNodeId: string(),
|
|
18384
|
+
reachableHost: string().optional(),
|
|
18385
|
+
configIssue: string().optional()
|
|
18386
|
+
});
|
|
18232
18387
|
/** Source block — always present; derives from the stream catalog. */
|
|
18233
18388
|
var CameraSourceStatusSchema = object({ streams: array(object({
|
|
18234
18389
|
camStreamId: string(),
|
|
@@ -18243,6 +18398,14 @@ var CameraAssignmentStatusSchema = object({
|
|
|
18243
18398
|
detectionNodeId: string().nullable(),
|
|
18244
18399
|
decoderNodeId: string().nullable(),
|
|
18245
18400
|
audioNodeId: string().nullable(),
|
|
18401
|
+
/**
|
|
18402
|
+
* The node that OWNS this camera's physical source pull (dials the RTSP and
|
|
18403
|
+
* hosts the broker/restream) — the cluster ingest owner today
|
|
18404
|
+
* (`clusterRoles.ingestNode`), per-camera once source assignment lands. Lets
|
|
18405
|
+
* the UI show WHERE a camera is sourced without SSH/logs, and is the node the
|
|
18406
|
+
* broker block below was read from (pinned). Nullable only pre-wiring.
|
|
18407
|
+
*/
|
|
18408
|
+
sourceNodeId: string().nullable(),
|
|
18246
18409
|
pinned: object({
|
|
18247
18410
|
detection: boolean(),
|
|
18248
18411
|
decoder: boolean(),
|
|
@@ -18375,7 +18538,7 @@ method(object({
|
|
|
18375
18538
|
}), object({ success: literal(true) }), {
|
|
18376
18539
|
kind: "mutation",
|
|
18377
18540
|
auth: "admin"
|
|
18378
|
-
}), method(object({
|
|
18541
|
+
}), method(_void(), IngestOwnerSchema), method(object({
|
|
18379
18542
|
deviceId: number(),
|
|
18380
18543
|
nodeId: string()
|
|
18381
18544
|
}), _void(), {
|
|
@@ -18444,6 +18607,12 @@ method(object({
|
|
|
18444
18607
|
}), object({ success: literal(true) }), {
|
|
18445
18608
|
kind: "mutation",
|
|
18446
18609
|
auth: "admin"
|
|
18610
|
+
}), method(object({
|
|
18611
|
+
agentNodeId: string(),
|
|
18612
|
+
reachableHost: string().nullable()
|
|
18613
|
+
}), object({ success: literal(true) }), {
|
|
18614
|
+
kind: "mutation",
|
|
18615
|
+
auth: "admin"
|
|
18447
18616
|
}), method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()), method(object({
|
|
18448
18617
|
deviceId: number(),
|
|
18449
18618
|
addonId: string(),
|
|
@@ -18488,22 +18657,6 @@ method(object({
|
|
|
18488
18657
|
kind: "mutation",
|
|
18489
18658
|
auth: "admin"
|
|
18490
18659
|
});
|
|
18491
|
-
var RegisteredStreamSchema = object({
|
|
18492
|
-
streamId: string(),
|
|
18493
|
-
label: string().optional(),
|
|
18494
|
-
codec: string(),
|
|
18495
|
-
type: _enum(["video", "audio"]),
|
|
18496
|
-
sourceUrl: string()
|
|
18497
|
-
});
|
|
18498
|
-
var ExposedResourceSchema = object({
|
|
18499
|
-
streamId: string(),
|
|
18500
|
-
format: string(),
|
|
18501
|
-
value: string()
|
|
18502
|
-
});
|
|
18503
|
-
method(object({
|
|
18504
|
-
deviceId: number(),
|
|
18505
|
-
streams: array(RegisteredStreamSchema).readonly()
|
|
18506
|
-
}), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), array(ExposedResourceSchema).readonly());
|
|
18507
18660
|
/**
|
|
18508
18661
|
* Query filter for settings-store collections.
|
|
18509
18662
|
*/
|
|
@@ -18656,9 +18809,9 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
|
|
|
18656
18809
|
/**
|
|
18657
18810
|
* A single device snapshot returned as base64 JPEG/PNG.
|
|
18658
18811
|
*
|
|
18659
|
-
*
|
|
18660
|
-
*
|
|
18661
|
-
*
|
|
18812
|
+
* The `SnapshotAddon` wrapper returns this shape whether the frame came from
|
|
18813
|
+
* the device-native provider (onboard capture) or from the stream-broker
|
|
18814
|
+
* prebuffer fallback.
|
|
18662
18815
|
*/
|
|
18663
18816
|
var SnapshotImageSchema = object({
|
|
18664
18817
|
base64: string(),
|
|
@@ -18733,10 +18886,6 @@ var snapshotCapability = {
|
|
|
18733
18886
|
kind: "poll"
|
|
18734
18887
|
}
|
|
18735
18888
|
};
|
|
18736
|
-
method(object({ deviceId: number() }), boolean()), method(object({
|
|
18737
|
-
deviceId: number(),
|
|
18738
|
-
streamId: string().optional()
|
|
18739
|
-
}), SnapshotImageSchema.nullable());
|
|
18740
18889
|
/**
|
|
18741
18890
|
* `sso-bridge` — internal hub-only cap that lets SSO-style auth
|
|
18742
18891
|
* providers (OIDC, SAML, magic-link, …) mint an HMAC-signed token
|
|
@@ -19097,9 +19246,10 @@ method(object({
|
|
|
19097
19246
|
auth: "admin"
|
|
19098
19247
|
});
|
|
19099
19248
|
/**
|
|
19100
|
-
* Optional client-side hints sent at session creation to help the
|
|
19101
|
-
*
|
|
19102
|
-
*
|
|
19249
|
+
* Optional client-side hints sent at session creation to help the provider
|
|
19250
|
+
* pick the best native source. All fields optional — a viewer that knows
|
|
19251
|
+
* nothing still gets a sane default. (Relocated from the retired `webrtc`
|
|
19252
|
+
* collection cap; this `webrtc-session` cap is the live signaling surface.)
|
|
19103
19253
|
*/
|
|
19104
19254
|
var webrtcClientHintsSchema = object({
|
|
19105
19255
|
viewportWidth: number().int().positive().optional(),
|
|
@@ -19110,22 +19260,6 @@ var webrtcClientHintsSchema = object({
|
|
|
19110
19260
|
/** Hard tier override; takes precedence over scoring when registered. */
|
|
19111
19261
|
prefersTier: string().optional()
|
|
19112
19262
|
}).partial();
|
|
19113
|
-
method(object({
|
|
19114
|
-
streamId: string(),
|
|
19115
|
-
sdpOffer: string()
|
|
19116
|
-
}), string(), { kind: "mutation" }), method(object({ streamId: string() }), boolean()), method(object({
|
|
19117
|
-
streamId: string(),
|
|
19118
|
-
codec: string()
|
|
19119
|
-
}), _void(), { kind: "mutation" }), method(object({ streamId: string() }), _void(), { kind: "mutation" }), method(object({
|
|
19120
|
-
streamId: string(),
|
|
19121
|
-
hints: webrtcClientHintsSchema.optional()
|
|
19122
|
-
}), object({
|
|
19123
|
-
sessionId: string(),
|
|
19124
|
-
sdpOffer: string()
|
|
19125
|
-
}), { kind: "mutation" }), method(object({
|
|
19126
|
-
sessionId: string(),
|
|
19127
|
-
sdpAnswer: string()
|
|
19128
|
-
}), _void(), { kind: "mutation" }), method(object({ sessionId: string() }), _void(), { kind: "mutation" }), method(object({ streamId: string() }), boolean());
|
|
19129
19263
|
/**
|
|
19130
19264
|
* Discriminated target for a WebRTC session. The client sends this
|
|
19131
19265
|
* structured object instead of building / parsing brokerId strings;
|
|
@@ -24181,6 +24315,12 @@ Object.freeze({
|
|
|
24181
24315
|
addonId: null,
|
|
24182
24316
|
access: "create"
|
|
24183
24317
|
},
|
|
24318
|
+
"pipelineExecutor.resetToDefault": {
|
|
24319
|
+
capName: "pipeline-executor",
|
|
24320
|
+
capScope: "system",
|
|
24321
|
+
addonId: null,
|
|
24322
|
+
access: "delete"
|
|
24323
|
+
},
|
|
24184
24324
|
"pipelineExecutor.runAudioTest": {
|
|
24185
24325
|
capName: "pipeline-executor",
|
|
24186
24326
|
capScope: "system",
|
|
@@ -24229,6 +24369,12 @@ Object.freeze({
|
|
|
24229
24369
|
addonId: null,
|
|
24230
24370
|
access: "create"
|
|
24231
24371
|
},
|
|
24372
|
+
"pipelineExecutor.validatePipeline": {
|
|
24373
|
+
capName: "pipeline-executor",
|
|
24374
|
+
capScope: "system",
|
|
24375
|
+
addonId: null,
|
|
24376
|
+
access: "view"
|
|
24377
|
+
},
|
|
24232
24378
|
"pipelineOrchestrator.assignAudio": {
|
|
24233
24379
|
capName: "pipeline-orchestrator",
|
|
24234
24380
|
capScope: "system",
|
|
@@ -24337,6 +24483,12 @@ Object.freeze({
|
|
|
24337
24483
|
addonId: null,
|
|
24338
24484
|
access: "view"
|
|
24339
24485
|
},
|
|
24486
|
+
"pipelineOrchestrator.getIngestOwner": {
|
|
24487
|
+
capName: "pipeline-orchestrator",
|
|
24488
|
+
capScope: "system",
|
|
24489
|
+
addonId: null,
|
|
24490
|
+
access: "view"
|
|
24491
|
+
},
|
|
24340
24492
|
"pipelineOrchestrator.getPipelineAssignment": {
|
|
24341
24493
|
capName: "pipeline-orchestrator",
|
|
24342
24494
|
capScope: "system",
|
|
@@ -24409,6 +24561,12 @@ Object.freeze({
|
|
|
24409
24561
|
addonId: null,
|
|
24410
24562
|
access: "create"
|
|
24411
24563
|
},
|
|
24564
|
+
"pipelineOrchestrator.setAgentReachableHost": {
|
|
24565
|
+
capName: "pipeline-orchestrator",
|
|
24566
|
+
capScope: "system",
|
|
24567
|
+
addonId: null,
|
|
24568
|
+
access: "create"
|
|
24569
|
+
},
|
|
24412
24570
|
"pipelineOrchestrator.setCameraPipelineForAgent": {
|
|
24413
24571
|
capName: "pipeline-orchestrator",
|
|
24414
24572
|
capScope: "system",
|
|
@@ -24739,24 +24897,6 @@ Object.freeze({
|
|
|
24739
24897
|
addonId: null,
|
|
24740
24898
|
access: "create"
|
|
24741
24899
|
},
|
|
24742
|
-
"restreamer.getExposedResources": {
|
|
24743
|
-
capName: "restreamer",
|
|
24744
|
-
capScope: "system",
|
|
24745
|
-
addonId: null,
|
|
24746
|
-
access: "view"
|
|
24747
|
-
},
|
|
24748
|
-
"restreamer.registerDevice": {
|
|
24749
|
-
capName: "restreamer",
|
|
24750
|
-
capScope: "system",
|
|
24751
|
-
addonId: null,
|
|
24752
|
-
access: "create"
|
|
24753
|
-
},
|
|
24754
|
-
"restreamer.unregisterDevice": {
|
|
24755
|
-
capName: "restreamer",
|
|
24756
|
-
capScope: "system",
|
|
24757
|
-
addonId: null,
|
|
24758
|
-
access: "delete"
|
|
24759
|
-
},
|
|
24760
24900
|
"scriptRunner.run": {
|
|
24761
24901
|
capName: "script-runner",
|
|
24762
24902
|
capScope: "device",
|
|
@@ -24859,18 +24999,6 @@ Object.freeze({
|
|
|
24859
24999
|
addonId: null,
|
|
24860
25000
|
access: "create"
|
|
24861
25001
|
},
|
|
24862
|
-
"snapshotProvider.getSnapshot": {
|
|
24863
|
-
capName: "snapshot-provider",
|
|
24864
|
-
capScope: "system",
|
|
24865
|
-
addonId: null,
|
|
24866
|
-
access: "view"
|
|
24867
|
-
},
|
|
24868
|
-
"snapshotProvider.supportsDevice": {
|
|
24869
|
-
capName: "snapshot-provider",
|
|
24870
|
-
capScope: "system",
|
|
24871
|
-
addonId: null,
|
|
24872
|
-
access: "view"
|
|
24873
|
-
},
|
|
24874
25002
|
"ssoBridge.signBridgeToken": {
|
|
24875
25003
|
capName: "sso-bridge",
|
|
24876
25004
|
capScope: "system",
|
|
@@ -25297,30 +25425,6 @@ Object.freeze({
|
|
|
25297
25425
|
addonId: null,
|
|
25298
25426
|
access: "view"
|
|
25299
25427
|
},
|
|
25300
|
-
"streamingEngine.getStreamUrl": {
|
|
25301
|
-
capName: "streaming-engine",
|
|
25302
|
-
capScope: "system",
|
|
25303
|
-
addonId: null,
|
|
25304
|
-
access: "view"
|
|
25305
|
-
},
|
|
25306
|
-
"streamingEngine.listStreams": {
|
|
25307
|
-
capName: "streaming-engine",
|
|
25308
|
-
capScope: "system",
|
|
25309
|
-
addonId: null,
|
|
25310
|
-
access: "view"
|
|
25311
|
-
},
|
|
25312
|
-
"streamingEngine.registerStream": {
|
|
25313
|
-
capName: "streaming-engine",
|
|
25314
|
-
capScope: "system",
|
|
25315
|
-
addonId: null,
|
|
25316
|
-
access: "create"
|
|
25317
|
-
},
|
|
25318
|
-
"streamingEngine.unregisterStream": {
|
|
25319
|
-
capName: "streaming-engine",
|
|
25320
|
-
capScope: "system",
|
|
25321
|
-
addonId: null,
|
|
25322
|
-
access: "delete"
|
|
25323
|
-
},
|
|
25324
25428
|
"streamParams.getConfigSchema": {
|
|
25325
25429
|
capName: "stream-params",
|
|
25326
25430
|
capScope: "device",
|
|
@@ -25687,54 +25791,6 @@ Object.freeze({
|
|
|
25687
25791
|
addonId: null,
|
|
25688
25792
|
access: "create"
|
|
25689
25793
|
},
|
|
25690
|
-
"webrtc.closeSession": {
|
|
25691
|
-
capName: "webrtc",
|
|
25692
|
-
capScope: "system",
|
|
25693
|
-
addonId: null,
|
|
25694
|
-
access: "create"
|
|
25695
|
-
},
|
|
25696
|
-
"webrtc.createSession": {
|
|
25697
|
-
capName: "webrtc",
|
|
25698
|
-
capScope: "system",
|
|
25699
|
-
addonId: null,
|
|
25700
|
-
access: "create"
|
|
25701
|
-
},
|
|
25702
|
-
"webrtc.handleAnswer": {
|
|
25703
|
-
capName: "webrtc",
|
|
25704
|
-
capScope: "system",
|
|
25705
|
-
addonId: null,
|
|
25706
|
-
access: "create"
|
|
25707
|
-
},
|
|
25708
|
-
"webrtc.handleOffer": {
|
|
25709
|
-
capName: "webrtc",
|
|
25710
|
-
capScope: "system",
|
|
25711
|
-
addonId: null,
|
|
25712
|
-
access: "create"
|
|
25713
|
-
},
|
|
25714
|
-
"webrtc.hasAdaptiveBitrate": {
|
|
25715
|
-
capName: "webrtc",
|
|
25716
|
-
capScope: "system",
|
|
25717
|
-
addonId: null,
|
|
25718
|
-
access: "view"
|
|
25719
|
-
},
|
|
25720
|
-
"webrtc.registerStream": {
|
|
25721
|
-
capName: "webrtc",
|
|
25722
|
-
capScope: "system",
|
|
25723
|
-
addonId: null,
|
|
25724
|
-
access: "create"
|
|
25725
|
-
},
|
|
25726
|
-
"webrtc.supportsStream": {
|
|
25727
|
-
capName: "webrtc",
|
|
25728
|
-
capScope: "system",
|
|
25729
|
-
addonId: null,
|
|
25730
|
-
access: "view"
|
|
25731
|
-
},
|
|
25732
|
-
"webrtc.unregisterStream": {
|
|
25733
|
-
capName: "webrtc",
|
|
25734
|
-
capScope: "system",
|
|
25735
|
-
addonId: null,
|
|
25736
|
-
access: "delete"
|
|
25737
|
-
},
|
|
25738
25794
|
"webrtcSession.addIceCandidate": {
|
|
25739
25795
|
capName: "webrtc-session",
|
|
25740
25796
|
capScope: "device",
|
|
@@ -25948,6 +26004,72 @@ function maskUrlCredentials(rawUrl) {
|
|
|
25948
26004
|
return rawUrl;
|
|
25949
26005
|
}
|
|
25950
26006
|
}
|
|
26007
|
+
//#endregion
|
|
26008
|
+
//#region src/rtsp-reachability.ts
|
|
26009
|
+
/**
|
|
26010
|
+
* Control-plane reachability probe for a generic RTSP camera.
|
|
26011
|
+
*
|
|
26012
|
+
* Unlike Hikvision/Amcrest/ONVIF, an RTSP source exposes NO management API —
|
|
26013
|
+
* we only have an `rtsp://` URL. Reachability is therefore probed at the RTSP
|
|
26014
|
+
* control channel itself:
|
|
26015
|
+
*
|
|
26016
|
+
* 1. open a TCP connection to the RTSP `host:port` (default 554);
|
|
26017
|
+
* 2. best-effort send an RTSP `OPTIONS` request;
|
|
26018
|
+
* 3. treat an `RTSP/…` response OR a successful TCP handshake (even without
|
|
26019
|
+
* a response before the deadline) as REACHABLE.
|
|
26020
|
+
*
|
|
26021
|
+
* A successful TCP handshake already proves the camera's RTSP port is live —
|
|
26022
|
+
* the `OPTIONS` round-trip is a stronger confirmation when the server answers
|
|
26023
|
+
* promptly, but we deliberately fall back to the bare connect so a camera that
|
|
26024
|
+
* gates `OPTIONS` behind auth (or is simply slow to answer) is not wrongly
|
|
26025
|
+
* marked offline.
|
|
26026
|
+
*
|
|
26027
|
+
* The socket is always destroyed; the probe never throws and resolves within
|
|
26028
|
+
* `timeoutMs`.
|
|
26029
|
+
*/
|
|
26030
|
+
var DEFAULT_RTSP_PORT = 554;
|
|
26031
|
+
/** Parse `host` + `port` out of an `rtsp://` URL. Credentials/path ignored. */
|
|
26032
|
+
function parseRtspTarget(url) {
|
|
26033
|
+
try {
|
|
26034
|
+
const parsed = new URL(url);
|
|
26035
|
+
if (!parsed.hostname) return null;
|
|
26036
|
+
const port = parsed.port ? Number.parseInt(parsed.port, 10) : DEFAULT_RTSP_PORT;
|
|
26037
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) return null;
|
|
26038
|
+
return {
|
|
26039
|
+
host: parsed.hostname,
|
|
26040
|
+
port
|
|
26041
|
+
};
|
|
26042
|
+
} catch {
|
|
26043
|
+
return null;
|
|
26044
|
+
}
|
|
26045
|
+
}
|
|
26046
|
+
async function probeRtspReachable(url, timeoutMs) {
|
|
26047
|
+
const target = parseRtspTarget(url);
|
|
26048
|
+
if (!target) return false;
|
|
26049
|
+
return new Promise((resolve) => {
|
|
26050
|
+
let settled = false;
|
|
26051
|
+
let connected = false;
|
|
26052
|
+
const socket = new net.Socket();
|
|
26053
|
+
const finish = (reachable) => {
|
|
26054
|
+
if (settled) return;
|
|
26055
|
+
settled = true;
|
|
26056
|
+
socket.destroy();
|
|
26057
|
+
resolve(reachable);
|
|
26058
|
+
};
|
|
26059
|
+
socket.setTimeout(timeoutMs);
|
|
26060
|
+
socket.once("timeout", () => finish(connected));
|
|
26061
|
+
socket.once("error", () => finish(false));
|
|
26062
|
+
socket.once("connect", () => {
|
|
26063
|
+
connected = true;
|
|
26064
|
+
const request = `OPTIONS ${url} RTSP/1.0\r\nCSeq: 1\r\nUser-Agent: camstack-reachability\r\n\r\n`;
|
|
26065
|
+
socket.write(request, () => {});
|
|
26066
|
+
});
|
|
26067
|
+
socket.once("data", (buf) => {
|
|
26068
|
+
finish(buf.toString("latin1").startsWith("RTSP/") || connected);
|
|
26069
|
+
});
|
|
26070
|
+
socket.connect(target.port, target.host);
|
|
26071
|
+
});
|
|
26072
|
+
}
|
|
25951
26073
|
var rtspCameraSchema = object({
|
|
25952
26074
|
streams: preprocess((raw) => {
|
|
25953
26075
|
if (!Array.isArray(raw)) return raw;
|
|
@@ -25985,15 +26107,44 @@ var legacyStreamSchema = object({
|
|
|
25985
26107
|
label: CamProfileSchema,
|
|
25986
26108
|
url: string()
|
|
25987
26109
|
});
|
|
25988
|
-
var RtspCamera = class extends BaseDevice {
|
|
26110
|
+
var RtspCamera = class RtspCamera extends BaseDevice {
|
|
25989
26111
|
type = DeviceType.Camera;
|
|
25990
26112
|
features = [];
|
|
26113
|
+
/** Control-plane reachability poll — an RTSP `OPTIONS`/TCP-connect probe to
|
|
26114
|
+
* the camera's RTSP port every 30s drives `device.online`, decoupled from
|
|
26115
|
+
* stream-broker video health. Started in `onActivate`, stopped in
|
|
26116
|
+
* `removeDevice`. */
|
|
26117
|
+
reachabilityPoll = null;
|
|
25991
26118
|
constructor(ctx) {
|
|
25992
26119
|
super(ctx, rtspCameraSchema, { type: DeviceType.Camera });
|
|
25993
26120
|
this.migrateLegacyStreamsIfNeeded(ctx.persistedConfig ?? {});
|
|
25994
26121
|
this.registerSnapshotProvider();
|
|
25995
26122
|
this.registerStreamCatalogProvider();
|
|
25996
26123
|
}
|
|
26124
|
+
/** Phase 5 — device is live: begin the control-plane reachability poll. */
|
|
26125
|
+
async onActivate() {
|
|
26126
|
+
this.startReachabilityPolling();
|
|
26127
|
+
}
|
|
26128
|
+
/** Start the RTSP reachability poll: a lightweight `OPTIONS`/TCP-connect
|
|
26129
|
+
* probe to the first configured stream's `host:port` every 30s drives
|
|
26130
|
+
* `device.online`, with hysteresis. Idempotent. */
|
|
26131
|
+
startReachabilityPolling() {
|
|
26132
|
+
if (this.reachabilityPoll) return;
|
|
26133
|
+
this.reachabilityPoll = startReachabilityPoll({
|
|
26134
|
+
probe: async () => {
|
|
26135
|
+
const url = this.config.get("streams")[0]?.url;
|
|
26136
|
+
if (!url) return false;
|
|
26137
|
+
return probeRtspReachable(url, RtspCamera.RTSP_PROBE_TIMEOUT_MS);
|
|
26138
|
+
},
|
|
26139
|
+
setOnline: (online) => {
|
|
26140
|
+
this.markOnline(online);
|
|
26141
|
+
},
|
|
26142
|
+
isEnabled: () => !this.disabled,
|
|
26143
|
+
logger: this.ctx.logger
|
|
26144
|
+
});
|
|
26145
|
+
}
|
|
26146
|
+
/** Per-probe TCP/RTSP round-trip budget — a dead host resolves as a miss. */
|
|
26147
|
+
static RTSP_PROBE_TIMEOUT_MS = 5e3;
|
|
25997
26148
|
/**
|
|
25998
26149
|
* Build this camera's pull-able stream catalog — the descriptors the
|
|
25999
26150
|
* stream-broker needs to dial. Single source of truth feeding the
|
|
@@ -26084,6 +26235,8 @@ var RtspCamera = class extends BaseDevice {
|
|
|
26084
26235
|
}
|
|
26085
26236
|
async removeDevice() {
|
|
26086
26237
|
this.ctx.logger.info("Removing RTSP camera", { meta: { stableId: this.stableId } });
|
|
26238
|
+
this.reachabilityPoll?.stop();
|
|
26239
|
+
this.reachabilityPoll = null;
|
|
26087
26240
|
const streams = this.config.get("streams");
|
|
26088
26241
|
await Promise.all(streams.map((s) => this.ctx.api.streamBroker.retractCameraStream.mutate({
|
|
26089
26242
|
deviceId: this.id,
|
|
@@ -26361,21 +26514,7 @@ var RtspProviderAddon = class extends BaseDeviceProvider {
|
|
|
26361
26514
|
super({});
|
|
26362
26515
|
}
|
|
26363
26516
|
async onInitialize() {
|
|
26364
|
-
|
|
26365
|
-
this.subscribe({ category: EventCategory.DeviceStateChanged }, (event) => {
|
|
26366
|
-
const data = event.data;
|
|
26367
|
-
if (data.capName !== "camera-streams") return;
|
|
26368
|
-
const deviceId = data.deviceId;
|
|
26369
|
-
if (typeof deviceId !== "number") return;
|
|
26370
|
-
const registry = this.ctx.kernel.deviceRegistry;
|
|
26371
|
-
if (!registry) return;
|
|
26372
|
-
if (registry.getAddonId(deviceId) !== this.addonId) return;
|
|
26373
|
-
const device = registry.getById(deviceId);
|
|
26374
|
-
if (!device) return;
|
|
26375
|
-
const online = data.slice?.online === true;
|
|
26376
|
-
if (device.online !== online) device.online = online;
|
|
26377
|
-
});
|
|
26378
|
-
return regs;
|
|
26517
|
+
return await super.onInitialize();
|
|
26379
26518
|
}
|
|
26380
26519
|
async onGetCreationSchema(type) {
|
|
26381
26520
|
if (type !== DeviceType.Camera) return null;
|