@camstack/addon-pipeline-orchestrator 0.1.25 → 0.1.27
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/@mf-types/compiled-types/widgets/zone-editor/ZoneForm.d.ts +1 -1
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneForm.d.ts.map +1 -1
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneList.d.ts +1 -1
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneList.d.ts.map +1 -1
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneOverlay.d.ts.map +1 -1
- package/dist/@mf-types/compiled-types/widgets/zone-editor/types.d.ts +33 -0
- package/dist/@mf-types/compiled-types/widgets/zone-editor/types.d.ts.map +1 -0
- package/dist/@mf-types.zip +0 -0
- package/dist/{ReactKonva--rywLr1Y.mjs → ReactKonva-BpqYt5jc.mjs} +2 -2
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-CP1zJ0aB.mjs +20 -0
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-B1VWqPID.mjs +35 -0
- package/dist/{__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react__loadShare__.mjs-0qpbQxoV.mjs → __mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react__loadShare__.mjs-ZXZUECVq.mjs} +5 -5
- package/dist/{__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-DekuE8px.mjs → __mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-WhBt7NtJ.mjs} +1 -1
- package/dist/{__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-Ba_7PYkj.mjs → __mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-DF7SvkCe.mjs} +1 -1
- package/dist/{__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react_mf_2_konva__loadShare__.mjs-DSZIXeAx.mjs → __mfe_internal__addon_pipeline_orchestrator_widgets__loadShare__react_mf_2_konva__loadShare__.mjs-BjxkVuVo.mjs} +5 -4
- package/dist/_stub.js +445 -777
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-CF16SlpF.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-zq9nTFza.mjs} +6 -6
- package/dist/{client-CzjQ3uuI.mjs → client-BOhSywdX.mjs} +2 -2
- package/dist/{hostInit-CVui0qjL.mjs → hostInit-Cn3hiNRr.mjs} +13 -13
- package/dist/{index-DaulYonp.mjs → index-3tmcVweY.mjs} +1 -1
- package/dist/{index-C1DnrJuR.mjs → index-Bx39JFVr.mjs} +1 -1
- package/dist/{index-BmY66bNn.mjs → index-C_khSbT0.mjs} +2 -2
- package/dist/{index-DOuehnyb.mjs → index-D4m79gq7.mjs} +1 -1
- package/dist/{index-BuYTzV_S.mjs → index-D_QOQy3W.mjs} +7138 -5661
- package/dist/{index-BCEx31Mh.mjs → index-Dy2V7VOm.mjs} +3808 -3277
- package/dist/{index-Cbqs9uJn.mjs → index-kp_mtnZv.mjs} +1 -1
- package/dist/index.js +775 -72
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +775 -72
- package/dist/index.mjs.map +1 -1
- package/dist/{jsx-runtime-DACJhJOv.mjs → jsx-runtime-ChcQDQxt.mjs} +1 -1
- package/dist/remoteEntry.js +1 -1
- package/dist/{schemas-ChN4Ih0h.mjs → schemas-ClCuS4qa.mjs} +151 -141
- package/package.json +4 -1
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneCanvas.d.ts +0 -60
- package/dist/@mf-types/compiled-types/widgets/zone-editor/ZoneCanvas.d.ts.map +0 -1
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-C4HmLg0z.mjs +0 -20
- package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-DEuqbomC.mjs +0 -34
package/dist/index.mjs
CHANGED
|
@@ -4778,6 +4778,16 @@ function record(keyType, valueType, params) {
|
|
|
4778
4778
|
...normalizeParams(params)
|
|
4779
4779
|
});
|
|
4780
4780
|
}
|
|
4781
|
+
function partialRecord(keyType, valueType, params) {
|
|
4782
|
+
const k = clone(keyType);
|
|
4783
|
+
k._zod.values = void 0;
|
|
4784
|
+
return new ZodRecord({
|
|
4785
|
+
type: "record",
|
|
4786
|
+
keyType: k,
|
|
4787
|
+
valueType,
|
|
4788
|
+
...normalizeParams(params)
|
|
4789
|
+
});
|
|
4790
|
+
}
|
|
4781
4791
|
const ZodEnum = /* @__PURE__ */ $constructor("ZodEnum", (inst, def) => {
|
|
4782
4792
|
$ZodEnum.init(inst, def);
|
|
4783
4793
|
ZodType.init(inst, def);
|
|
@@ -5054,6 +5064,37 @@ function _instanceof(cls, params = {}) {
|
|
|
5054
5064
|
};
|
|
5055
5065
|
return inst;
|
|
5056
5066
|
}
|
|
5067
|
+
const wiringProbeKindSchema = _enum(["singleton", "device", "widget"]);
|
|
5068
|
+
const wiringProbeResultSchema = object({
|
|
5069
|
+
capName: string(),
|
|
5070
|
+
kind: wiringProbeKindSchema,
|
|
5071
|
+
deviceId: number().optional(),
|
|
5072
|
+
reachable: boolean(),
|
|
5073
|
+
latencyMs: number(),
|
|
5074
|
+
error: string().optional()
|
|
5075
|
+
});
|
|
5076
|
+
const wiringAddonHealthSchema = object({
|
|
5077
|
+
addonId: string(),
|
|
5078
|
+
caps: array(wiringProbeResultSchema).readonly(),
|
|
5079
|
+
widgets: array(wiringProbeResultSchema).readonly()
|
|
5080
|
+
});
|
|
5081
|
+
const wiringNodeHealthSchema = object({
|
|
5082
|
+
nodeId: string(),
|
|
5083
|
+
addons: array(wiringAddonHealthSchema).readonly()
|
|
5084
|
+
});
|
|
5085
|
+
object({
|
|
5086
|
+
/** True only when every probed target is reachable. */
|
|
5087
|
+
ok: boolean(),
|
|
5088
|
+
/** True when at least one target is unreachable. */
|
|
5089
|
+
degraded: boolean(),
|
|
5090
|
+
checkedAt: string(),
|
|
5091
|
+
nodes: array(wiringNodeHealthSchema).readonly(),
|
|
5092
|
+
summary: object({
|
|
5093
|
+
total: number(),
|
|
5094
|
+
reachable: number(),
|
|
5095
|
+
unreachable: number()
|
|
5096
|
+
})
|
|
5097
|
+
});
|
|
5057
5098
|
const MODEL_FORMATS = ["onnx", "coreml", "openvino", "tflite", "pt"];
|
|
5058
5099
|
const WELL_KNOWN_TABS = [
|
|
5059
5100
|
{ id: "overview", label: "Overview", icon: "layout-dashboard", order: -10 },
|
|
@@ -6601,7 +6642,7 @@ const SpatialDetectionSchema = object({
|
|
|
6601
6642
|
bbox: BoundingBoxSchema
|
|
6602
6643
|
});
|
|
6603
6644
|
const AudioChunkInputSchema = object({
|
|
6604
|
-
data: _instanceof(
|
|
6645
|
+
data: _instanceof(Uint8Array),
|
|
6605
6646
|
sampleRate: number(),
|
|
6606
6647
|
channels: number(),
|
|
6607
6648
|
timestamp: number(),
|
|
@@ -7276,7 +7317,23 @@ const RunnerCameraConfigSchema = object({
|
|
|
7276
7317
|
* whenever its `zones` device-state slice changes, so the runner's
|
|
7277
7318
|
* copy stays in sync. Empty array → no zone filtering.
|
|
7278
7319
|
*/
|
|
7279
|
-
zones: array(ZoneSchema).readonly().default([])
|
|
7320
|
+
zones: array(ZoneSchema).readonly().default([]),
|
|
7321
|
+
/**
|
|
7322
|
+
* When true (default) and the camera's `motionSources` contains only
|
|
7323
|
+
* `'onboard'`, the runner dynamically opens the same WASM frame-diff
|
|
7324
|
+
* motion-frames subscription on each `MotionOnMotionChanged
|
|
7325
|
+
* source:'onboard'` event and tears it down after `motionCooldownMs`.
|
|
7326
|
+
* This causes `runMotionAnalysis` to emit `MotionZonesRaw` /
|
|
7327
|
+
* `MotionAnalysis` during active-motion windows without the
|
|
7328
|
+
* substream being held open continuously.
|
|
7329
|
+
*
|
|
7330
|
+
* Set to `false` to disable the dynamic analyzer for this camera
|
|
7331
|
+
* (e.g. very low-bandwidth links where the extra substream is
|
|
7332
|
+
* undesirable). Has no effect when `motionSources` already includes
|
|
7333
|
+
* `'analyzer'` — the analyzer runs continuously in that case and
|
|
7334
|
+
* this gate is bypassed.
|
|
7335
|
+
*/
|
|
7336
|
+
onboardMotionDrivesAnalyzer: boolean().default(true)
|
|
7280
7337
|
});
|
|
7281
7338
|
const RunnerCameraDeviceUIFields = [
|
|
7282
7339
|
{
|
|
@@ -7336,6 +7393,16 @@ const RunnerCameraDeviceUIFields = [
|
|
|
7336
7393
|
showValue: true,
|
|
7337
7394
|
unit: "s",
|
|
7338
7395
|
displayScale: 1e3
|
|
7396
|
+
},
|
|
7397
|
+
// Only meaningful for onboard-only cameras — when `motionSources`
|
|
7398
|
+
// contains `'analyzer'` the analyzer runs continuously already.
|
|
7399
|
+
{
|
|
7400
|
+
key: "onboardMotionDrivesAnalyzer",
|
|
7401
|
+
type: "boolean",
|
|
7402
|
+
label: "Run motion analyzer on onboard motion",
|
|
7403
|
+
description: "When onboard motion is detected, temporarily start the frame-diff analyzer to emit zone results. Stops after the motion cooldown window.",
|
|
7404
|
+
default: true,
|
|
7405
|
+
showWhen: { field: "motionSources", includes: "onboard" }
|
|
7339
7406
|
}
|
|
7340
7407
|
];
|
|
7341
7408
|
const RunnerLocalLoadSchema = object({
|
|
@@ -7473,22 +7540,68 @@ MotionTriggerStatusSchema.extend({
|
|
|
7473
7540
|
}) }
|
|
7474
7541
|
}
|
|
7475
7542
|
});
|
|
7543
|
+
const MaskPointSchema = object({
|
|
7544
|
+
x: number(),
|
|
7545
|
+
y: number()
|
|
7546
|
+
});
|
|
7547
|
+
const MaskRectShapeSchema = object({
|
|
7548
|
+
kind: literal("rect"),
|
|
7549
|
+
x: number(),
|
|
7550
|
+
y: number(),
|
|
7551
|
+
width: number(),
|
|
7552
|
+
height: number()
|
|
7553
|
+
});
|
|
7554
|
+
const MaskPolygonShapeSchema = object({
|
|
7555
|
+
kind: literal("polygon"),
|
|
7556
|
+
points: array(MaskPointSchema)
|
|
7557
|
+
});
|
|
7558
|
+
const MaskGridShapeSchema = object({
|
|
7559
|
+
kind: literal("grid"),
|
|
7560
|
+
gridWidth: number(),
|
|
7561
|
+
gridHeight: number(),
|
|
7562
|
+
cells: array(boolean())
|
|
7563
|
+
});
|
|
7564
|
+
const MaskLineShapeSchema = object({
|
|
7565
|
+
kind: literal("line"),
|
|
7566
|
+
points: array(MaskPointSchema)
|
|
7567
|
+
});
|
|
7568
|
+
discriminatedUnion("kind", [
|
|
7569
|
+
MaskRectShapeSchema,
|
|
7570
|
+
MaskPolygonShapeSchema,
|
|
7571
|
+
MaskGridShapeSchema,
|
|
7572
|
+
MaskLineShapeSchema
|
|
7573
|
+
]);
|
|
7574
|
+
const MaskShapeKindSchema = _enum(["rect", "polygon", "grid", "line"]);
|
|
7575
|
+
const MaskPolygonVerticesSchema = object({
|
|
7576
|
+
min: number(),
|
|
7577
|
+
max: number()
|
|
7578
|
+
});
|
|
7579
|
+
const MaskGridDimsSchema = object({
|
|
7580
|
+
width: number(),
|
|
7581
|
+
height: number()
|
|
7582
|
+
});
|
|
7583
|
+
const MotionZoneRegionSchema = object({
|
|
7584
|
+
id: number(),
|
|
7585
|
+
enabled: boolean(),
|
|
7586
|
+
shape: MaskGridShapeSchema
|
|
7587
|
+
});
|
|
7476
7588
|
object({
|
|
7477
7589
|
enabled: boolean(),
|
|
7478
7590
|
sensitivity: number(),
|
|
7479
|
-
/**
|
|
7480
|
-
|
|
7591
|
+
/** Grid region(s). Today exactly one `grid` shape. */
|
|
7592
|
+
regions: array(MotionZoneRegionSchema),
|
|
7481
7593
|
lastFetchedAt: number()
|
|
7482
7594
|
});
|
|
7483
7595
|
const MotionZoneOptionsSchema = object({
|
|
7484
|
-
|
|
7485
|
-
|
|
7596
|
+
maxRegions: number(),
|
|
7597
|
+
supportedShapes: array(MaskShapeKindSchema),
|
|
7598
|
+
grid: MaskGridDimsSchema,
|
|
7486
7599
|
sensitivity: object({ min: number(), max: number(), step: number() })
|
|
7487
7600
|
});
|
|
7488
7601
|
const MotionZonePatchSchema = object({
|
|
7489
7602
|
enabled: boolean().optional(),
|
|
7490
7603
|
sensitivity: number().optional(),
|
|
7491
|
-
|
|
7604
|
+
regions: array(MotionZoneRegionSchema).optional()
|
|
7492
7605
|
});
|
|
7493
7606
|
({
|
|
7494
7607
|
deviceTypes: [DeviceType.Camera],
|
|
@@ -7501,6 +7614,102 @@ const MotionZonePatchSchema = object({
|
|
|
7501
7614
|
)
|
|
7502
7615
|
}
|
|
7503
7616
|
});
|
|
7617
|
+
const NativeObjectClassEnum = _enum([
|
|
7618
|
+
"person",
|
|
7619
|
+
"vehicle",
|
|
7620
|
+
"animal",
|
|
7621
|
+
"face",
|
|
7622
|
+
"package",
|
|
7623
|
+
"other"
|
|
7624
|
+
]);
|
|
7625
|
+
const NativeDetectionSchema = object({
|
|
7626
|
+
class: NativeObjectClassEnum,
|
|
7627
|
+
timestamp: number(),
|
|
7628
|
+
/** Firmware-provided confidence [0..1]. Reolink pushes don't carry it → undefined. */
|
|
7629
|
+
confidence: number().min(0).max(1).optional()
|
|
7630
|
+
});
|
|
7631
|
+
const NativeObjectDetectionStatusSchema = object({
|
|
7632
|
+
/**
|
|
7633
|
+
* Last observed instance per class. Missing entries mean the class
|
|
7634
|
+
* is supported but nothing has been seen since the provider started.
|
|
7635
|
+
*
|
|
7636
|
+
* MUST be a partial record: providers seed an empty `{}` on cold-start
|
|
7637
|
+
* and write one class at a time as detections arrive. In Zod 4
|
|
7638
|
+
* `z.record(enum, …)` is EXHAUSTIVE (requires every enum key), so a
|
|
7639
|
+
* partial write throws "expected object, received undefined" for every
|
|
7640
|
+
* unseen class. `z.partialRecord` keeps the enum-key narrowing while
|
|
7641
|
+
* allowing the sparse shape the providers actually write.
|
|
7642
|
+
*/
|
|
7643
|
+
lastByClass: partialRecord(NativeObjectClassEnum, NativeDetectionSchema.nullable()),
|
|
7644
|
+
/** Classes the firmware is capable of detecting — enumerated at device register. */
|
|
7645
|
+
supportedClasses: array(NativeObjectClassEnum).readonly(),
|
|
7646
|
+
/**
|
|
7647
|
+
* Whether forwarding of onboard AI detections is enabled for this device.
|
|
7648
|
+
* Default true (on cold-start) — detections flow unconditionally before
|
|
7649
|
+
* the toggle is saved, so defaulting true preserves existing behaviour.
|
|
7650
|
+
*/
|
|
7651
|
+
enabled: boolean()
|
|
7652
|
+
});
|
|
7653
|
+
NativeObjectDetectionStatusSchema.extend({
|
|
7654
|
+
/** Required by createRuntimeStateBridge — epoch ms of last refresh. */
|
|
7655
|
+
lastFetchedAt: number()
|
|
7656
|
+
});
|
|
7657
|
+
({
|
|
7658
|
+
deviceTypes: [DeviceType.Camera],
|
|
7659
|
+
methods: {
|
|
7660
|
+
setEnabled: method(
|
|
7661
|
+
object({ deviceId: number(), enabled: boolean() }),
|
|
7662
|
+
_void(),
|
|
7663
|
+
{ kind: "mutation", auth: "admin" }
|
|
7664
|
+
)
|
|
7665
|
+
},
|
|
7666
|
+
events: {
|
|
7667
|
+
onDetected: { data: object({
|
|
7668
|
+
deviceId: number(),
|
|
7669
|
+
detection: NativeDetectionSchema
|
|
7670
|
+
}) }
|
|
7671
|
+
}
|
|
7672
|
+
});
|
|
7673
|
+
const PrivacyMaskShapeSchema = discriminatedUnion("kind", [
|
|
7674
|
+
MaskRectShapeSchema,
|
|
7675
|
+
MaskPolygonShapeSchema
|
|
7676
|
+
]);
|
|
7677
|
+
const PrivacyMaskRegionSchema = object({
|
|
7678
|
+
/** Slot id, 0-based. Stable across read/write. */
|
|
7679
|
+
id: number(),
|
|
7680
|
+
/** Whether this zone is active (blanked out by the camera). */
|
|
7681
|
+
enabled: boolean(),
|
|
7682
|
+
shape: PrivacyMaskShapeSchema
|
|
7683
|
+
});
|
|
7684
|
+
object({
|
|
7685
|
+
enabled: boolean(),
|
|
7686
|
+
/** Active zones (normalized 0..1). Length ≤ maxRegions. */
|
|
7687
|
+
regions: array(PrivacyMaskRegionSchema),
|
|
7688
|
+
lastFetchedAt: number()
|
|
7689
|
+
});
|
|
7690
|
+
const PrivacyMaskOptionsSchema = object({
|
|
7691
|
+
/** Maximum number of supported zones. */
|
|
7692
|
+
maxRegions: number(),
|
|
7693
|
+
/** Shape kinds this camera accepts — Reolink: ['rect']; Hikvision: ['rect','polygon']. */
|
|
7694
|
+
supportedShapes: array(MaskShapeKindSchema),
|
|
7695
|
+
/** Polygon vertex bounds when 'polygon' is supported (Hikvision: {min:4,max:4}). */
|
|
7696
|
+
polygonVertices: MaskPolygonVerticesSchema.optional()
|
|
7697
|
+
});
|
|
7698
|
+
const PrivacyMaskPatchSchema = object({
|
|
7699
|
+
enabled: boolean().optional(),
|
|
7700
|
+
regions: array(PrivacyMaskRegionSchema).optional()
|
|
7701
|
+
});
|
|
7702
|
+
({
|
|
7703
|
+
deviceTypes: [DeviceType.Camera],
|
|
7704
|
+
methods: {
|
|
7705
|
+
getOptions: method(object({ deviceId: number() }), PrivacyMaskOptionsSchema),
|
|
7706
|
+
setMask: method(
|
|
7707
|
+
object({ deviceId: number(), patch: PrivacyMaskPatchSchema }),
|
|
7708
|
+
_void(),
|
|
7709
|
+
{ kind: "mutation", auth: "admin" }
|
|
7710
|
+
)
|
|
7711
|
+
}
|
|
7712
|
+
});
|
|
7504
7713
|
const AutotrackTargetTypeSchema = string().describe("Vendor target string (people/vehicle/pet); empty = camera default");
|
|
7505
7714
|
const PtzAutotrackSettingsSchema = object({
|
|
7506
7715
|
targetType: AutotrackTargetTypeSchema,
|
|
@@ -8022,7 +8231,8 @@ const SettingsUpdateResultSchema = object({
|
|
|
8022
8231
|
object({
|
|
8023
8232
|
addonId: string(),
|
|
8024
8233
|
nodeId: string().optional(),
|
|
8025
|
-
overlay: record(string(), unknown()).optional()
|
|
8234
|
+
overlay: record(string(), unknown()).optional(),
|
|
8235
|
+
cap: string().optional()
|
|
8026
8236
|
}),
|
|
8027
8237
|
SettingsSchemaWithValuesSchema.nullable()
|
|
8028
8238
|
),
|
|
@@ -8753,7 +8963,14 @@ const OauthIntegrationDescriptorSchema = object({
|
|
|
8753
8963
|
/** Allowed redirect_uri prefixes. /api/oauth2/authorize rejects any
|
|
8754
8964
|
* redirect_uri that does not start with one of these. Required —
|
|
8755
8965
|
* an empty list means the integration can never complete linking. */
|
|
8756
|
-
allowedRedirectPrefixes: array(string()).min(1)
|
|
8966
|
+
allowedRedirectPrefixes: array(string()).min(1),
|
|
8967
|
+
/** Optional public origin (no trailing slash) that this integration's
|
|
8968
|
+
* issued codes/tokens should carry as the `hubUrl` claim — typically the
|
|
8969
|
+
* operator-selected external-access endpoint resolved by the addon. When
|
|
8970
|
+
* present, /api/oauth2/authorize bakes THIS into the code instead of the
|
|
8971
|
+
* hub-global `publicHubUrl()`, so a forked exporter addon (which can't set
|
|
8972
|
+
* the hub's env) drives the claim that its cloud Lambda routes back on. */
|
|
8973
|
+
hubUrl: string().optional()
|
|
8757
8974
|
});
|
|
8758
8975
|
({
|
|
8759
8976
|
methods: {
|
|
@@ -9371,7 +9588,20 @@ const WebrtcStreamChoiceSchema = object({
|
|
|
9371
9588
|
object({
|
|
9372
9589
|
deviceId: number().int().nonnegative(),
|
|
9373
9590
|
target: WebrtcStreamTargetSchema,
|
|
9374
|
-
hints: webrtcClientHintsSchema.optional()
|
|
9591
|
+
hints: webrtcClientHintsSchema.optional(),
|
|
9592
|
+
/**
|
|
9593
|
+
* SERVER-INJECTED — NOT a client hint. The hub layer that holds
|
|
9594
|
+
* the tRPC request context (and therefore the client IP) sets
|
|
9595
|
+
* this to `true` when the viewer's source IP is non-LAN
|
|
9596
|
+
* (4G/CGNAT/internet). The broker then forces TURN-relay-only
|
|
9597
|
+
* ICE for the session so a CGNAT client (which can only offer a
|
|
9598
|
+
* relay candidate) gets a clean relay↔relay media path instead
|
|
9599
|
+
* of werift nominating a dead host/hairpin-srflx pair. LAN
|
|
9600
|
+
* clients leave this absent/false and keep the low-latency
|
|
9601
|
+
* direct (host/srflx) path. Clients MUST NOT send this — the
|
|
9602
|
+
* server overwrites it from the request context.
|
|
9603
|
+
*/
|
|
9604
|
+
relayOnly: boolean().optional()
|
|
9375
9605
|
}),
|
|
9376
9606
|
object({ sessionId: string(), sdpOffer: string() }),
|
|
9377
9607
|
{ kind: "mutation" }
|
|
@@ -9394,7 +9624,22 @@ const WebrtcStreamChoiceSchema = object({
|
|
|
9394
9624
|
deviceId: number().int().nonnegative(),
|
|
9395
9625
|
target: WebrtcStreamTargetSchema.optional(),
|
|
9396
9626
|
sdpOffer: string(),
|
|
9397
|
-
sessionId: string().optional()
|
|
9627
|
+
sessionId: string().optional(),
|
|
9628
|
+
/**
|
|
9629
|
+
* Force TURN-relay-only ICE for this session. Two kinds of caller
|
|
9630
|
+
* set it:
|
|
9631
|
+
* - A cloud peer like Alexa's RTCSessionController (reachable
|
|
9632
|
+
* only via TURN, never our host/srflx behind NAT) passes
|
|
9633
|
+
* `true` from its own trusted addon context.
|
|
9634
|
+
* - The hub injects it for browser client-offer viewers from the
|
|
9635
|
+
* request's source IP (non-LAN ⇒ true), exactly as it does for
|
|
9636
|
+
* `createSession`.
|
|
9637
|
+
* A LAN/Tailscale browser doing client-offer passthrough leaves it
|
|
9638
|
+
* absent/false so a direct host pair carries full native quality.
|
|
9639
|
+
* Untrusted browser clients MUST NOT send it — the hub overwrites
|
|
9640
|
+
* it from the request context.
|
|
9641
|
+
*/
|
|
9642
|
+
relayOnly: boolean().optional()
|
|
9398
9643
|
}),
|
|
9399
9644
|
object({ sessionId: string(), sdpAnswer: string() }),
|
|
9400
9645
|
{ kind: "mutation" }
|
|
@@ -9408,6 +9653,46 @@ const WebrtcStreamChoiceSchema = object({
|
|
|
9408
9653
|
_void(),
|
|
9409
9654
|
{ kind: "mutation" }
|
|
9410
9655
|
),
|
|
9656
|
+
/**
|
|
9657
|
+
* Trickle ICE — add a remote (client) ICE candidate to a live session.
|
|
9658
|
+
* Lets the client send its SDP offer/answer IMMEDIATELY (before ICE
|
|
9659
|
+
* gathering finishes) and deliver candidates as they arrive, so the
|
|
9660
|
+
* connection establishes in ~0s instead of waiting for full gathering.
|
|
9661
|
+
* The dual of `getIceCandidates`. Mirrors Scrypted's signaling.
|
|
9662
|
+
*/
|
|
9663
|
+
addIceCandidate: method(
|
|
9664
|
+
object({
|
|
9665
|
+
deviceId: number().int().nonnegative(),
|
|
9666
|
+
sessionId: string(),
|
|
9667
|
+
candidate: string(),
|
|
9668
|
+
sdpMid: string().nullable().optional(),
|
|
9669
|
+
sdpMLineIndex: number().int().nullable().optional()
|
|
9670
|
+
}),
|
|
9671
|
+
_void(),
|
|
9672
|
+
{ kind: "mutation" }
|
|
9673
|
+
),
|
|
9674
|
+
/**
|
|
9675
|
+
* Trickle ICE — poll the server's gathered ICE candidates for a session.
|
|
9676
|
+
* The server answers immediately (no gathering wait) and the client polls
|
|
9677
|
+
* this to receive host/srflx/relay candidates as werift gathers them,
|
|
9678
|
+
* adding each to its PeerConnection. Returns all candidates gathered so
|
|
9679
|
+
* far; the client dedupes. `done` flips true once gathering completes.
|
|
9680
|
+
*/
|
|
9681
|
+
getIceCandidates: method(
|
|
9682
|
+
object({
|
|
9683
|
+
deviceId: number().int().nonnegative(),
|
|
9684
|
+
sessionId: string()
|
|
9685
|
+
}),
|
|
9686
|
+
object({
|
|
9687
|
+
candidates: array(object({
|
|
9688
|
+
candidate: string(),
|
|
9689
|
+
sdpMid: string().nullable(),
|
|
9690
|
+
sdpMLineIndex: number().int().nullable()
|
|
9691
|
+
})),
|
|
9692
|
+
done: boolean()
|
|
9693
|
+
}),
|
|
9694
|
+
{ kind: "query" }
|
|
9695
|
+
),
|
|
9411
9696
|
closeSession: method(
|
|
9412
9697
|
object({
|
|
9413
9698
|
deviceId: number().int().nonnegative(),
|
|
@@ -11083,6 +11368,16 @@ const UpdateConfigInput = object({
|
|
|
11083
11368
|
({
|
|
11084
11369
|
deviceTypes: [DeviceType.Camera]
|
|
11085
11370
|
});
|
|
11371
|
+
const cameraPipelineConfigCapability = {
|
|
11372
|
+
name: "camera-pipeline-config",
|
|
11373
|
+
scope: "device",
|
|
11374
|
+
mode: "singleton",
|
|
11375
|
+
kind: "wrapper",
|
|
11376
|
+
defaultActive: true,
|
|
11377
|
+
deviceTypes: [DeviceType.Camera],
|
|
11378
|
+
exposesDeviceSettings: true,
|
|
11379
|
+
methods: {}
|
|
11380
|
+
};
|
|
11086
11381
|
const TrackStateSchema = _enum(["new", "entered", "left", "moving", "idle"]);
|
|
11087
11382
|
const EventKindSchema = _enum(["motion", "object", "audio"]);
|
|
11088
11383
|
const TrackPositionSchema = object({
|
|
@@ -11910,36 +12205,28 @@ const IntercomStatusSchema = object({
|
|
|
11910
12205
|
}) }
|
|
11911
12206
|
}
|
|
11912
12207
|
});
|
|
11913
|
-
const
|
|
11914
|
-
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
object({
|
|
11928
|
-
/**
|
|
11929
|
-
* Last observed instance per class. Undefined entries mean the class
|
|
11930
|
-
* is supported but nothing has been seen since the provider started.
|
|
11931
|
-
*/
|
|
11932
|
-
lastByClass: record(NativeObjectClassEnum, NativeDetectionSchema.nullable()),
|
|
11933
|
-
/** Classes the firmware is capable of detecting — enumerated at device register. */
|
|
11934
|
-
supportedClasses: array(NativeObjectClassEnum).readonly()
|
|
12208
|
+
const CamStreamDescriptorSchema = object({
|
|
12209
|
+
camStreamId: string().min(1),
|
|
12210
|
+
kind: CamStreamKindSchema,
|
|
12211
|
+
url: string().optional(),
|
|
12212
|
+
codec: string().optional(),
|
|
12213
|
+
resolution: CamStreamResolutionSchema.optional(),
|
|
12214
|
+
fps: number().positive().optional(),
|
|
12215
|
+
label: string().optional(),
|
|
12216
|
+
/** Device-level features (e.g. `battery-operated`) — drives broker policy. */
|
|
12217
|
+
deviceFeatures: array(string()).optional(),
|
|
12218
|
+
/** Eligible for automatic profile assignment. Absent = `true`. */
|
|
12219
|
+
autoEligible: boolean().optional(),
|
|
12220
|
+
/** Transport-specific opaque metadata (e.g. rfc4571 SDP). */
|
|
12221
|
+
metadata: record(string(), unknown()).optional()
|
|
11935
12222
|
});
|
|
11936
12223
|
({
|
|
11937
12224
|
deviceTypes: [DeviceType.Camera],
|
|
11938
|
-
|
|
11939
|
-
|
|
11940
|
-
deviceId: number(),
|
|
11941
|
-
|
|
11942
|
-
|
|
12225
|
+
methods: {
|
|
12226
|
+
getCatalog: method(
|
|
12227
|
+
object({ deviceId: number().int().nonnegative() }),
|
|
12228
|
+
array(CamStreamDescriptorSchema).readonly()
|
|
12229
|
+
)
|
|
11943
12230
|
}
|
|
11944
12231
|
});
|
|
11945
12232
|
const ModelFormatSchema = _enum(MODEL_FORMATS);
|
|
@@ -12807,6 +13094,18 @@ const TopologyProcessSchema = object({
|
|
|
12807
13094
|
services: array(TopologyServiceSchema).readonly(),
|
|
12808
13095
|
groupId: string().optional()
|
|
12809
13096
|
});
|
|
13097
|
+
const TopologyCategoryAddonSchema = object({
|
|
13098
|
+
id: string(),
|
|
13099
|
+
status: string(),
|
|
13100
|
+
cpuPercent: number(),
|
|
13101
|
+
memoryRss: number()
|
|
13102
|
+
});
|
|
13103
|
+
const TopologyCategorySchema = object({
|
|
13104
|
+
category: string(),
|
|
13105
|
+
total: number(),
|
|
13106
|
+
healthy: number(),
|
|
13107
|
+
addons: array(TopologyCategoryAddonSchema).readonly()
|
|
13108
|
+
});
|
|
12810
13109
|
const TopologyNodeSchema = object({
|
|
12811
13110
|
id: string(),
|
|
12812
13111
|
name: string(),
|
|
@@ -12831,7 +13130,15 @@ const TopologyNodeSchema = object({
|
|
|
12831
13130
|
status: string()
|
|
12832
13131
|
})
|
|
12833
13132
|
).readonly(),
|
|
12834
|
-
processes: array(TopologyProcessSchema).readonly()
|
|
13133
|
+
processes: array(TopologyProcessSchema).readonly(),
|
|
13134
|
+
categories: array(TopologyCategorySchema).readonly()
|
|
13135
|
+
});
|
|
13136
|
+
const CapUsageEdgeSchema = object({
|
|
13137
|
+
callerAddonId: string(),
|
|
13138
|
+
providerAddonId: string(),
|
|
13139
|
+
capName: string(),
|
|
13140
|
+
callsPerMin: number(),
|
|
13141
|
+
lastCallAtMs: number()
|
|
12835
13142
|
});
|
|
12836
13143
|
const ClusterAddonNodeDeploymentSchema = object({
|
|
12837
13144
|
nodeId: string(),
|
|
@@ -12915,13 +13222,7 @@ const RenameNodeResultSchema = object({
|
|
|
12915
13222
|
object({
|
|
12916
13223
|
windowSeconds: number().int().positive().max(300).default(60)
|
|
12917
13224
|
}),
|
|
12918
|
-
array(
|
|
12919
|
-
callerAddonId: string(),
|
|
12920
|
-
providerAddonId: string(),
|
|
12921
|
-
capName: string(),
|
|
12922
|
-
callsPerMin: number(),
|
|
12923
|
-
lastCallAtMs: number()
|
|
12924
|
-
})).readonly(),
|
|
13225
|
+
array(CapUsageEdgeSchema).readonly(),
|
|
12925
13226
|
{ auth: "admin" }
|
|
12926
13227
|
),
|
|
12927
13228
|
/**
|
|
@@ -13133,7 +13434,8 @@ const PackageUpdateSchema = object({
|
|
|
13133
13434
|
currentVersion: string(),
|
|
13134
13435
|
latestVersion: string(),
|
|
13135
13436
|
category: _enum(["addon", "core"]),
|
|
13136
|
-
requiresRestart: boolean()
|
|
13437
|
+
requiresRestart: boolean(),
|
|
13438
|
+
isSystem: boolean()
|
|
13137
13439
|
});
|
|
13138
13440
|
const PackageVersionInfoSchema = object({
|
|
13139
13441
|
version: string(),
|
|
@@ -13166,6 +13468,42 @@ const UpdateFrameworkPackageResultSchema = object({
|
|
|
13166
13468
|
/** Ms-epoch the server scheduled its self-restart. */
|
|
13167
13469
|
restartingAt: number()
|
|
13168
13470
|
});
|
|
13471
|
+
const BulkUpdateItemStatusSchema = _enum([
|
|
13472
|
+
"queued",
|
|
13473
|
+
"updating",
|
|
13474
|
+
"done",
|
|
13475
|
+
"done-pending-restart",
|
|
13476
|
+
"failed"
|
|
13477
|
+
]);
|
|
13478
|
+
const BulkUpdateItemSchema = object({
|
|
13479
|
+
name: string(),
|
|
13480
|
+
isSystem: boolean(),
|
|
13481
|
+
fromVersion: string(),
|
|
13482
|
+
toVersion: string(),
|
|
13483
|
+
status: BulkUpdateItemStatusSchema,
|
|
13484
|
+
error: string().optional(),
|
|
13485
|
+
startedAtMs: number().optional(),
|
|
13486
|
+
completedAtMs: number().optional()
|
|
13487
|
+
});
|
|
13488
|
+
const BulkUpdatePhaseSchema = _enum([
|
|
13489
|
+
"regular",
|
|
13490
|
+
"system",
|
|
13491
|
+
"restarting",
|
|
13492
|
+
"finalizing"
|
|
13493
|
+
]);
|
|
13494
|
+
const BulkUpdateStateSchema = object({
|
|
13495
|
+
id: string(),
|
|
13496
|
+
nodeId: string(),
|
|
13497
|
+
startedAtMs: number(),
|
|
13498
|
+
completedAtMs: number().optional(),
|
|
13499
|
+
total: number(),
|
|
13500
|
+
completed: number(),
|
|
13501
|
+
failed: number(),
|
|
13502
|
+
current: string().nullable(),
|
|
13503
|
+
phase: BulkUpdatePhaseSchema,
|
|
13504
|
+
cancelled: boolean(),
|
|
13505
|
+
items: array(BulkUpdateItemSchema).readonly()
|
|
13506
|
+
});
|
|
13169
13507
|
const FrameworkPackageStatusSchema = object({
|
|
13170
13508
|
packageName: string(),
|
|
13171
13509
|
currentVersion: string(),
|
|
@@ -13303,7 +13641,7 @@ const CustomActionInputSchema = object({
|
|
|
13303
13641
|
getLastRestart: method(
|
|
13304
13642
|
_void(),
|
|
13305
13643
|
object({
|
|
13306
|
-
kind: _enum(["framework-update", "manual", "system"]),
|
|
13644
|
+
kind: _enum(["framework-update", "manual", "system", "framework-bulk-update"]),
|
|
13307
13645
|
packageName: string().optional(),
|
|
13308
13646
|
fromVersion: string().optional(),
|
|
13309
13647
|
toVersion: string().optional(),
|
|
@@ -13393,11 +13731,70 @@ const CustomActionInputSchema = object({
|
|
|
13393
13731
|
updateFrameworkPackage: method(
|
|
13394
13732
|
object({
|
|
13395
13733
|
packageName: string().min(1),
|
|
13396
|
-
version: string().optional()
|
|
13734
|
+
version: string().optional(),
|
|
13735
|
+
deferRestart: boolean().optional()
|
|
13397
13736
|
}),
|
|
13398
13737
|
UpdateFrameworkPackageResultSchema,
|
|
13399
13738
|
{ kind: "mutation", auth: "admin" }
|
|
13400
13739
|
),
|
|
13740
|
+
/**
|
|
13741
|
+
* Kicks off a server-side bulk update operation and returns the bulk
|
|
13742
|
+
* id immediately. The operation runs asynchronously; observe progress
|
|
13743
|
+
* via the `AddonsBulkUpdateProgress` event or `getBulkUpdateState`.
|
|
13744
|
+
* Items with `isSystem: true` use `deferRestart` — the hub restarts
|
|
13745
|
+
* ONCE at the end of the system phase, after all system packages are
|
|
13746
|
+
* installed.
|
|
13747
|
+
*
|
|
13748
|
+
* `items[].version` is REQUIRED — callers must pass the resolved
|
|
13749
|
+
* version from `listUpdates`. There is no `'latest'` default here
|
|
13750
|
+
* (unlike `updatePackage`) to guarantee deterministic bulk rolls.
|
|
13751
|
+
*/
|
|
13752
|
+
startBulkUpdate: method(
|
|
13753
|
+
object({
|
|
13754
|
+
nodeId: string(),
|
|
13755
|
+
items: array(object({
|
|
13756
|
+
name: string(),
|
|
13757
|
+
version: string(),
|
|
13758
|
+
isSystem: boolean()
|
|
13759
|
+
})).readonly()
|
|
13760
|
+
}),
|
|
13761
|
+
object({ id: string() }),
|
|
13762
|
+
{ kind: "mutation", auth: "admin" }
|
|
13763
|
+
),
|
|
13764
|
+
/**
|
|
13765
|
+
* Returns the current state of a bulk update by id.
|
|
13766
|
+
* Returns `null` if the id is unknown or has been auto-cleaned
|
|
13767
|
+
* (5 minutes after `completedAt` the record is evicted from memory).
|
|
13768
|
+
*/
|
|
13769
|
+
getBulkUpdateState: method(
|
|
13770
|
+
object({ id: string() }),
|
|
13771
|
+
BulkUpdateStateSchema.nullable(),
|
|
13772
|
+
{ auth: "admin" }
|
|
13773
|
+
),
|
|
13774
|
+
/**
|
|
13775
|
+
* Cancels an in-flight bulk update. The update loop exits after the
|
|
13776
|
+
* currently-processing item completes — cancellation is not
|
|
13777
|
+
* instantaneous. Has no effect once the `restarting` phase has been
|
|
13778
|
+
* entered (the hub is already shutting down at that point).
|
|
13779
|
+
* Returns `{ cancelled: false }` if the id is unknown, the operation
|
|
13780
|
+
* has already completed, or the `restarting` phase is active.
|
|
13781
|
+
*/
|
|
13782
|
+
cancelBulkUpdate: method(
|
|
13783
|
+
object({ id: string() }),
|
|
13784
|
+
object({ cancelled: boolean() }),
|
|
13785
|
+
{ kind: "mutation", auth: "admin" }
|
|
13786
|
+
),
|
|
13787
|
+
/**
|
|
13788
|
+
* Lists all currently active (non-completed) bulk updates.
|
|
13789
|
+
* If `nodeId` is provided, filters to only bulk updates targeting
|
|
13790
|
+
* that node. Useful for restoring an in-progress banner on a fresh
|
|
13791
|
+
* page load when the UI reconnects mid-operation.
|
|
13792
|
+
*/
|
|
13793
|
+
listActiveBulkUpdates: method(
|
|
13794
|
+
object({ nodeId: string().optional() }),
|
|
13795
|
+
array(BulkUpdateStateSchema).readonly(),
|
|
13796
|
+
{ auth: "admin" }
|
|
13797
|
+
),
|
|
13401
13798
|
getVersions: method(
|
|
13402
13799
|
object({ name: string() }),
|
|
13403
13800
|
array(PackageVersionInfoSchema).readonly()
|
|
@@ -13525,6 +13922,7 @@ var EventCategory = /* @__PURE__ */ ((EventCategory2) => {
|
|
|
13525
13922
|
EventCategory2["StreamBrokerOnCamStreamDemand"] = "stream-broker.onCamStreamDemand";
|
|
13526
13923
|
EventCategory2["StreamBrokerOnCamStreamIdle"] = "stream-broker.onCamStreamIdle";
|
|
13527
13924
|
EventCategory2["StreamBrokerOnRequestStreamSourceRefresh"] = "stream-broker.onRequestStreamSourceRefresh";
|
|
13925
|
+
EventCategory2["StreamParamsChanged"] = "stream-params.changed";
|
|
13528
13926
|
EventCategory2["DeviceStateChanged"] = "device.state-changed";
|
|
13529
13927
|
EventCategory2["BatteryOnStatusChanged"] = "battery.onStatusChanged";
|
|
13530
13928
|
EventCategory2["DoorbellOnPressed"] = "doorbell.onPressed";
|
|
@@ -13572,6 +13970,7 @@ var EventCategory = /* @__PURE__ */ ((EventCategory2) => {
|
|
|
13572
13970
|
EventCategory2["NetworkTunnelStarted"] = "network.tunnel.started";
|
|
13573
13971
|
EventCategory2["NetworkTunnelStopped"] = "network.tunnel.stopped";
|
|
13574
13972
|
EventCategory2["LocalNetworkChanged"] = "network.local.changed";
|
|
13973
|
+
EventCategory2["MeshNetworkChanged"] = "network.mesh.changed";
|
|
13575
13974
|
EventCategory2["BackupCompleted"] = "backup.completed";
|
|
13576
13975
|
EventCategory2["BackupRestored"] = "backup.restored";
|
|
13577
13976
|
EventCategory2["NotificationDispatched"] = "notification.dispatched";
|
|
@@ -13582,6 +13981,7 @@ var EventCategory = /* @__PURE__ */ ((EventCategory2) => {
|
|
|
13582
13981
|
EventCategory2["DeviceAwake"] = "device.awake";
|
|
13583
13982
|
EventCategory2["DeviceSleeping"] = "device.sleeping";
|
|
13584
13983
|
EventCategory2["RetentionCleanup"] = "retention.cleanup";
|
|
13984
|
+
EventCategory2["AddonsBulkUpdateProgress"] = "addons.bulk-update-progress";
|
|
13585
13985
|
return EventCategory2;
|
|
13586
13986
|
})(EventCategory || {});
|
|
13587
13987
|
function isEvent(event2, category) {
|
|
@@ -13734,7 +14134,7 @@ class BaseAddon {
|
|
|
13734
14134
|
}
|
|
13735
14135
|
// ── Settings schemas (override to provide UI) ─────────────────────────
|
|
13736
14136
|
/** Override to provide global-level settings UI schema. */
|
|
13737
|
-
globalSettingsSchema() {
|
|
14137
|
+
globalSettingsSchema(_cap) {
|
|
13738
14138
|
return null;
|
|
13739
14139
|
}
|
|
13740
14140
|
/** Override to provide device-level settings UI schema. */
|
|
@@ -13748,8 +14148,8 @@ class BaseAddon {
|
|
|
13748
14148
|
// blob and every addon used exactly one of them; the distinction was
|
|
13749
14149
|
// never semantically load-bearing. `global` won because it was the
|
|
13750
14150
|
// widely-used one and the name reads naturally (per-node addon config).
|
|
13751
|
-
async getGlobalSettings(overlay) {
|
|
13752
|
-
const schema = this.globalSettingsSchema();
|
|
14151
|
+
async getGlobalSettings(overlay, cap) {
|
|
14152
|
+
const schema = this.globalSettingsSchema(cap);
|
|
13753
14153
|
if (!schema) return { sections: [] };
|
|
13754
14154
|
const raw = await this._ctx?.settings?.readAddonStore() ?? {};
|
|
13755
14155
|
return hydrateSchema(schema, overlay ? { ...raw, ...overlay } : raw);
|
|
@@ -14168,6 +14568,14 @@ class ReadinessRegistry {
|
|
|
14168
14568
|
cleanup();
|
|
14169
14569
|
reject(new ReadinessTimeoutError(capName, scope, this.now() - start));
|
|
14170
14570
|
}, timeoutMs);
|
|
14571
|
+
const PENDING_LOG_INTERVAL_MS = 3e4;
|
|
14572
|
+
const pendingLogTimer = setInterval(() => {
|
|
14573
|
+
if (settled) return;
|
|
14574
|
+
this.logger?.warn(
|
|
14575
|
+
`readiness: still awaiting ${capName} (${scopeKey(scope)}) after ${this.now() - start}ms`
|
|
14576
|
+
);
|
|
14577
|
+
}, PENDING_LOG_INTERVAL_MS);
|
|
14578
|
+
if (typeof pendingLogTimer.unref === "function") pendingLogTimer.unref();
|
|
14171
14579
|
const onAbort = () => {
|
|
14172
14580
|
if (settled) return;
|
|
14173
14581
|
settled = true;
|
|
@@ -14178,6 +14586,7 @@ class ReadinessRegistry {
|
|
|
14178
14586
|
function cleanup() {
|
|
14179
14587
|
unsubscribe();
|
|
14180
14588
|
if (timer !== null) clearTimeout(timer);
|
|
14589
|
+
clearInterval(pendingLogTimer);
|
|
14181
14590
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
14182
14591
|
}
|
|
14183
14592
|
});
|
|
@@ -14731,6 +15140,8 @@ function balanceAudio(input) {
|
|
|
14731
15140
|
}
|
|
14732
15141
|
const POLL_INTERVAL_MS = 200;
|
|
14733
15142
|
const PULL_MAX_COUNT = 8;
|
|
15143
|
+
const RESUBSCRIBE_AFTER_FAILURES = 2;
|
|
15144
|
+
const RESUBSCRIBE_THROTTLE_TICKS = 5;
|
|
14734
15145
|
async function startAudioChunkPoller(options) {
|
|
14735
15146
|
const { api, brokerId, tag, onChunk, logger } = options;
|
|
14736
15147
|
let subscriptionId;
|
|
@@ -14748,6 +15159,19 @@ async function startAudioChunkPoller(options) {
|
|
|
14748
15159
|
}
|
|
14749
15160
|
let stopped = false;
|
|
14750
15161
|
let timer;
|
|
15162
|
+
let consecutiveFailures = 0;
|
|
15163
|
+
const resubscribe = async () => {
|
|
15164
|
+
try {
|
|
15165
|
+
const result = await api.streamBroker.subscribeAudioChunks.mutate({ brokerId, tag });
|
|
15166
|
+
subscriptionId = result.subscriptionId;
|
|
15167
|
+
logger.info("audio-chunk poller: re-subscribed after broker outage", {
|
|
15168
|
+
meta: { brokerId, tag, subscriptionId, afterFailures: consecutiveFailures }
|
|
15169
|
+
});
|
|
15170
|
+
return true;
|
|
15171
|
+
} catch {
|
|
15172
|
+
return false;
|
|
15173
|
+
}
|
|
15174
|
+
};
|
|
14751
15175
|
const tick = async () => {
|
|
14752
15176
|
if (stopped) return;
|
|
14753
15177
|
try {
|
|
@@ -14755,14 +15179,24 @@ async function startAudioChunkPoller(options) {
|
|
|
14755
15179
|
subscriptionId,
|
|
14756
15180
|
maxCount: PULL_MAX_COUNT
|
|
14757
15181
|
});
|
|
15182
|
+
if (consecutiveFailures > 0) {
|
|
15183
|
+
logger.info("audio-chunk poller: stream recovered", { meta: { brokerId, subscriptionId } });
|
|
15184
|
+
}
|
|
15185
|
+
consecutiveFailures = 0;
|
|
14758
15186
|
for (const chunk of chunks) {
|
|
14759
15187
|
if (stopped) break;
|
|
14760
15188
|
await onChunk(chunk);
|
|
14761
15189
|
}
|
|
14762
15190
|
} catch (err) {
|
|
14763
|
-
|
|
14764
|
-
|
|
14765
|
-
|
|
15191
|
+
consecutiveFailures += 1;
|
|
15192
|
+
if (consecutiveFailures === 1) {
|
|
15193
|
+
logger.warn("audio-chunk poller: pullAudioChunks failed — attempting recovery", {
|
|
15194
|
+
meta: { brokerId, subscriptionId, error: errMsg(err) }
|
|
15195
|
+
});
|
|
15196
|
+
}
|
|
15197
|
+
if (!stopped && consecutiveFailures >= RESUBSCRIBE_AFTER_FAILURES && (consecutiveFailures - RESUBSCRIBE_AFTER_FAILURES) % RESUBSCRIBE_THROTTLE_TICKS === 0) {
|
|
15198
|
+
await resubscribe();
|
|
15199
|
+
}
|
|
14766
15200
|
}
|
|
14767
15201
|
if (!stopped) {
|
|
14768
15202
|
timer = setTimeout(() => void tick(), POLL_INTERVAL_MS);
|
|
@@ -14783,6 +15217,18 @@ async function startAudioChunkPoller(options) {
|
|
|
14783
15217
|
});
|
|
14784
15218
|
};
|
|
14785
15219
|
}
|
|
15220
|
+
function desiredDeviceIdsFromSlots(slots) {
|
|
15221
|
+
const result = /* @__PURE__ */ new Set();
|
|
15222
|
+
for (const slot of slots) {
|
|
15223
|
+
if (slot.status !== "unassigned" && slot.sourceCamStreamId !== null) {
|
|
15224
|
+
result.add(slot.deviceId);
|
|
15225
|
+
}
|
|
15226
|
+
}
|
|
15227
|
+
return result;
|
|
15228
|
+
}
|
|
15229
|
+
function computeDispatchGap(desired, known) {
|
|
15230
|
+
return [...desired].filter((id) => !known.has(id));
|
|
15231
|
+
}
|
|
14786
15232
|
const PHASE_MODE_VALUES = /* @__PURE__ */ new Set([
|
|
14787
15233
|
"disabled",
|
|
14788
15234
|
"always-on",
|
|
@@ -14794,6 +15240,7 @@ function isPipelinePhaseMode(v) {
|
|
|
14794
15240
|
const PREFERRED_AGENT_SETTING = "preferredAgent";
|
|
14795
15241
|
const AUDIO_NODE_SETTING = "audioNodeId";
|
|
14796
15242
|
const DEFAULT_BROKER_CALL_TIMEOUT_MS = 5e3;
|
|
15243
|
+
const RECONCILE_DEBOUNCE_MS = 200;
|
|
14797
15244
|
const AGENT_SETTINGS_KEY = "agentSettings";
|
|
14798
15245
|
const CAMERA_SETTINGS_KEY = "cameraSettings";
|
|
14799
15246
|
const TEMPLATES_KEY = "templates";
|
|
@@ -14945,6 +15392,12 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
14945
15392
|
loadShedState = /* @__PURE__ */ new Map();
|
|
14946
15393
|
/** Timer for the auto-resume sweep. */
|
|
14947
15394
|
loadShedResumeTimer = null;
|
|
15395
|
+
/** Pending `scheduleReconcile` debounce timer. */
|
|
15396
|
+
reconcileTimer = null;
|
|
15397
|
+
/** True while `reconcileDispatch` is awaiting the RPC round-trip. */
|
|
15398
|
+
reconcileInFlight = false;
|
|
15399
|
+
/** Set to true when a reconcile is requested while one is already in-flight; triggers a follow-up pass. */
|
|
15400
|
+
reconcileRerunRequested = false;
|
|
14948
15401
|
initTimestamp = 0;
|
|
14949
15402
|
constructor() {
|
|
14950
15403
|
super({});
|
|
@@ -15003,6 +15456,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15003
15456
|
meta: { error: msg }
|
|
15004
15457
|
});
|
|
15005
15458
|
});
|
|
15459
|
+
this.scheduleReconcile();
|
|
15006
15460
|
}
|
|
15007
15461
|
);
|
|
15008
15462
|
this.watchCapability(["pipeline-executor", "analysis-pipeline"], {
|
|
@@ -15100,6 +15554,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15100
15554
|
});
|
|
15101
15555
|
}, PROFILE_SLOTS_DEBOUNCE_MS)
|
|
15102
15556
|
);
|
|
15557
|
+
this.scheduleReconcile();
|
|
15103
15558
|
}
|
|
15104
15559
|
);
|
|
15105
15560
|
this.unsubDeviceUnregistered = this.ctx.eventBus.subscribe(
|
|
@@ -15279,9 +15734,15 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15279
15734
|
}
|
|
15280
15735
|
]
|
|
15281
15736
|
};
|
|
15737
|
+
this.scheduleReconcile();
|
|
15282
15738
|
return {
|
|
15283
15739
|
providers: [
|
|
15284
15740
|
{ capability: pipelineOrchestratorCapability, provider: this },
|
|
15741
|
+
// D12 re-home: the per-device settings contribution (motion/pipeline
|
|
15742
|
+
// tabs + zones top-tab) rides this device-scoped defaultActive wrapper
|
|
15743
|
+
// so it auto-binds to every camera and the binding-driven aggregate
|
|
15744
|
+
// invokes it. `this` already implements the three contribution methods.
|
|
15745
|
+
{ capability: cameraPipelineConfigCapability, provider: this },
|
|
15285
15746
|
{ capability: zonesCapability, provider: this.zonesProvider },
|
|
15286
15747
|
{ capability: zoneRulesCapability, provider: this.zoneRulesProvider },
|
|
15287
15748
|
{ capability: addonWidgetsSourceCapability, provider: widgetsProvider }
|
|
@@ -15406,6 +15867,10 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15406
15867
|
this.profileSlotTimers.clear();
|
|
15407
15868
|
this.profileSlotTimers = null;
|
|
15408
15869
|
}
|
|
15870
|
+
if (this.reconcileTimer !== null) {
|
|
15871
|
+
clearTimeout(this.reconcileTimer);
|
|
15872
|
+
this.reconcileTimer = null;
|
|
15873
|
+
}
|
|
15409
15874
|
this.unsubDeviceRegistered?.();
|
|
15410
15875
|
this.unsubDeviceRegistered = null;
|
|
15411
15876
|
this.unsubDeviceUnregistered?.();
|
|
@@ -15481,7 +15946,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15481
15946
|
);
|
|
15482
15947
|
this.cameraConfigs.set(runnerConfig.deviceId, runnerConfig);
|
|
15483
15948
|
const cfg = this.globalSettings ?? {};
|
|
15484
|
-
const minThreshold = typeof cfg.fpsMinThreshold === "number" ? cfg.fpsMinThreshold :
|
|
15949
|
+
const minThreshold = typeof cfg.fpsMinThreshold === "number" ? cfg.fpsMinThreshold : PipelineOrchestratorAddon.DEFAULT_FPS_MIN_THRESHOLD;
|
|
15485
15950
|
const windowMs = typeof cfg.fpsLowWindowMs === "number" && cfg.fpsLowWindowMs > 0 ? cfg.fpsLowWindowMs : PipelineOrchestratorAddon.DEFAULT_FPS_LOW_WINDOW_MS;
|
|
15486
15951
|
if (minThreshold > 0 && this.initTimestamp && Date.now() - this.initTimestamp > 3e4) {
|
|
15487
15952
|
const now = Date.now();
|
|
@@ -15924,7 +16389,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15924
16389
|
const api = ctx?.api;
|
|
15925
16390
|
if (!api) continue;
|
|
15926
16391
|
try {
|
|
15927
|
-
const load = await
|
|
16392
|
+
const load = await this.queryLocalLoadBounded(nodeId);
|
|
15928
16393
|
loads.push(load);
|
|
15929
16394
|
} catch (err) {
|
|
15930
16395
|
const msg = errMsg(err);
|
|
@@ -15949,6 +16414,53 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15949
16414
|
}
|
|
15950
16415
|
this.cachedAgentLoad = next;
|
|
15951
16416
|
}
|
|
16417
|
+
/**
|
|
16418
|
+
* Per-node `getLocalLoad` budget (ms). A WEDGED runner — transport up
|
|
16419
|
+
* enough to stay in the service registry, but whose `broker.call` neither
|
|
16420
|
+
* resolves nor rejects (observed live as
|
|
16421
|
+
* "[brokerTransportLink] still awaiting … after 18min") — would otherwise
|
|
16422
|
+
* stall `collectAgentLoad`, and through it `dispatchCamera` →
|
|
16423
|
+
* `startDetection` for EVERY camera, indefinitely: the transport link does
|
|
16424
|
+
* not abort an in-flight `broker.call` (see trpc-links.ts), so neither the
|
|
16425
|
+
* try/catch nor an AbortSignal can unstick it. Tunable via the
|
|
16426
|
+
* `agentLoadTimeoutMs` global setting; the default is generous enough that
|
|
16427
|
+
* a healthy-but-slow node is never falsely skipped (the pinned-discovery
|
|
16428
|
+
* window alone is 2s).
|
|
16429
|
+
*/
|
|
16430
|
+
static DEFAULT_AGENT_LOAD_TIMEOUT_MS = 4e3;
|
|
16431
|
+
agentLoadBudgetMs() {
|
|
16432
|
+
const cfg = this.globalSettings ?? {};
|
|
16433
|
+
const value = cfg.agentLoadTimeoutMs;
|
|
16434
|
+
return typeof value === "number" && value > 0 ? value : PipelineOrchestratorAddon.DEFAULT_AGENT_LOAD_TIMEOUT_MS;
|
|
16435
|
+
}
|
|
16436
|
+
/**
|
|
16437
|
+
* `getLocalLoad` for one runner node, bounded by `agentLoadBudgetMs`. On
|
|
16438
|
+
* timeout it rejects so `collectAgentLoad`'s catch treats the node exactly
|
|
16439
|
+
* like an offline one (logged at debug, skipped). The underlying query is
|
|
16440
|
+
* left to settle on its own — Moleculer rejects it when the node
|
|
16441
|
+
* disconnects — rather than leaking an unbounded await into the dispatch
|
|
16442
|
+
* path.
|
|
16443
|
+
*/
|
|
16444
|
+
async queryLocalLoadBounded(nodeId) {
|
|
16445
|
+
const api = this.ctx?.api;
|
|
16446
|
+
if (!api) throw new Error("queryLocalLoadBounded: addon not initialized");
|
|
16447
|
+
const budgetMs = this.agentLoadBudgetMs();
|
|
16448
|
+
let timer;
|
|
16449
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
16450
|
+
timer = setTimeout(
|
|
16451
|
+
() => reject(new Error(`getLocalLoad exceeded ${budgetMs}ms budget for ${nodeId}`)),
|
|
16452
|
+
budgetMs
|
|
16453
|
+
);
|
|
16454
|
+
});
|
|
16455
|
+
try {
|
|
16456
|
+
return await Promise.race([
|
|
16457
|
+
api.pipelineRunner.getLocalLoad.query({ nodeId }),
|
|
16458
|
+
timeout
|
|
16459
|
+
]);
|
|
16460
|
+
} finally {
|
|
16461
|
+
if (timer) clearTimeout(timer);
|
|
16462
|
+
}
|
|
16463
|
+
}
|
|
15952
16464
|
async readPreferredAgent(deviceId) {
|
|
15953
16465
|
const ctx = this.ctx;
|
|
15954
16466
|
if (!ctx?.settings) return null;
|
|
@@ -16246,6 +16758,44 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
16246
16758
|
"audio-analyzer ready — node now available for audio dispatch",
|
|
16247
16759
|
{ tags: { nodeId }, meta: { epoch: t.epoch } }
|
|
16248
16760
|
);
|
|
16761
|
+
await this.reattachAudioForNode(nodeId);
|
|
16762
|
+
this.scheduleReconcile();
|
|
16763
|
+
}
|
|
16764
|
+
/**
|
|
16765
|
+
* Re-establish audio subscriptions for every camera whose audio is routed
|
|
16766
|
+
* to `nodeId`, after that node's audio-analyzer (re)started. Mirrors the
|
|
16767
|
+
* audio block in `startDetection` (drop prior sub, re-subscribe, keep only
|
|
16768
|
+
* if detection is active). Idempotent.
|
|
16769
|
+
*/
|
|
16770
|
+
async reattachAudioForNode(nodeId) {
|
|
16771
|
+
for (const [deviceId, audioNode] of this.audioNodeByDevice) {
|
|
16772
|
+
if (audioNode !== nodeId) continue;
|
|
16773
|
+
const config2 = this.cameraConfigs.get(deviceId);
|
|
16774
|
+
if (!config2) continue;
|
|
16775
|
+
const prior = this.audioSubscriptions.get(deviceId);
|
|
16776
|
+
if (prior) {
|
|
16777
|
+
try {
|
|
16778
|
+
prior();
|
|
16779
|
+
} catch {
|
|
16780
|
+
}
|
|
16781
|
+
this.audioSubscriptions.delete(deviceId);
|
|
16782
|
+
}
|
|
16783
|
+
try {
|
|
16784
|
+
const unsub = await this.subscribeAudioStream(deviceId, config2);
|
|
16785
|
+
if (unsub) {
|
|
16786
|
+
if (this.activeDetections.has(deviceId)) {
|
|
16787
|
+
this.audioSubscriptions.set(deviceId, unsub);
|
|
16788
|
+
} else {
|
|
16789
|
+
unsub();
|
|
16790
|
+
}
|
|
16791
|
+
}
|
|
16792
|
+
} catch (err) {
|
|
16793
|
+
this.ctx.logger.error("audio re-attach on analyzer readiness failed", {
|
|
16794
|
+
tags: { deviceId, nodeId },
|
|
16795
|
+
meta: { error: errMsg(err) }
|
|
16796
|
+
});
|
|
16797
|
+
}
|
|
16798
|
+
}
|
|
16249
16799
|
}
|
|
16250
16800
|
/**
|
|
16251
16801
|
* Act on a `detection-pipeline` transition: on `ready` with a new
|
|
@@ -16317,6 +16867,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
16317
16867
|
});
|
|
16318
16868
|
});
|
|
16319
16869
|
}, 3e3);
|
|
16870
|
+
this.scheduleReconcile();
|
|
16320
16871
|
} catch (err) {
|
|
16321
16872
|
this.ctx.logger.debug("readiness seed+redispatch failed", {
|
|
16322
16873
|
tags: { nodeId },
|
|
@@ -16361,6 +16912,75 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
16361
16912
|
}
|
|
16362
16913
|
}
|
|
16363
16914
|
}
|
|
16915
|
+
// ── Dispatch reconcile ───────────────────────────────────────────────
|
|
16916
|
+
/**
|
|
16917
|
+
* Coalesce bursts of topology/slot-change signals into a single
|
|
16918
|
+
* `reconcileDispatch` pass. Mirrors `scheduleRehydrate` in
|
|
16919
|
+
* `packages/kernel/src/moleculer/readiness-context.ts`.
|
|
16920
|
+
*/
|
|
16921
|
+
scheduleReconcile() {
|
|
16922
|
+
if (this.reconcileTimer !== null) clearTimeout(this.reconcileTimer);
|
|
16923
|
+
this.reconcileTimer = setTimeout(() => {
|
|
16924
|
+
this.reconcileTimer = null;
|
|
16925
|
+
void this.reconcileDispatch();
|
|
16926
|
+
}, RECONCILE_DEBOUNCE_MS);
|
|
16927
|
+
}
|
|
16928
|
+
/**
|
|
16929
|
+
* RPC snapshot-reconcile: pull the current broker fleet over
|
|
16930
|
+
* `listAllProfileSlots`, diff against `cameraConfigs`, and dispatch
|
|
16931
|
+
* the gap via `handleDeviceRegistered`.
|
|
16932
|
+
*
|
|
16933
|
+
* Additive only — never retracts or re-dispatches a known device.
|
|
16934
|
+
* Guards against re-entrant calls: a trailing request schedules one
|
|
16935
|
+
* extra pass after the in-flight one completes.
|
|
16936
|
+
*/
|
|
16937
|
+
async reconcileDispatch() {
|
|
16938
|
+
if (this.reconcileInFlight) {
|
|
16939
|
+
this.reconcileRerunRequested = true;
|
|
16940
|
+
return;
|
|
16941
|
+
}
|
|
16942
|
+
this.reconcileInFlight = true;
|
|
16943
|
+
try {
|
|
16944
|
+
const api = this.api;
|
|
16945
|
+
if (!api) {
|
|
16946
|
+
this.ctx.logger.debug(
|
|
16947
|
+
"dispatch reconcile skipped — ctx.api not yet available"
|
|
16948
|
+
);
|
|
16949
|
+
return;
|
|
16950
|
+
}
|
|
16951
|
+
let slots;
|
|
16952
|
+
try {
|
|
16953
|
+
slots = await api.streamBroker.listAllProfileSlots.query();
|
|
16954
|
+
} catch (err) {
|
|
16955
|
+
this.ctx.logger.debug("dispatch reconcile skipped — stream-broker not ready", {
|
|
16956
|
+
meta: { error: errMsg(err) }
|
|
16957
|
+
});
|
|
16958
|
+
return;
|
|
16959
|
+
}
|
|
16960
|
+
const desired = desiredDeviceIdsFromSlots(slots);
|
|
16961
|
+
const known = new Set(this.cameraConfigs.keys());
|
|
16962
|
+
const gap = computeDispatchGap(desired, known);
|
|
16963
|
+
this.ctx.logger.info("dispatch reconcile", {
|
|
16964
|
+
meta: { desired: desired.size, known: known.size, gap }
|
|
16965
|
+
});
|
|
16966
|
+
for (const deviceId of gap) {
|
|
16967
|
+
try {
|
|
16968
|
+
await this.handleDeviceRegistered(deviceId);
|
|
16969
|
+
} catch (err) {
|
|
16970
|
+
this.ctx.logger.warn("dispatch reconcile: handleDeviceRegistered failed", {
|
|
16971
|
+
tags: { deviceId },
|
|
16972
|
+
meta: { error: errMsg(err) }
|
|
16973
|
+
});
|
|
16974
|
+
}
|
|
16975
|
+
}
|
|
16976
|
+
} finally {
|
|
16977
|
+
this.reconcileInFlight = false;
|
|
16978
|
+
if (this.reconcileRerunRequested) {
|
|
16979
|
+
this.reconcileRerunRequested = false;
|
|
16980
|
+
this.scheduleReconcile();
|
|
16981
|
+
}
|
|
16982
|
+
}
|
|
16983
|
+
}
|
|
16364
16984
|
// ── Capability bindings (cap methods) ────────────────────────────────
|
|
16365
16985
|
async getCapabilityBindings(input) {
|
|
16366
16986
|
const ctx = this.ctx;
|
|
@@ -17142,11 +17762,11 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17142
17762
|
key: "fpsMinThreshold",
|
|
17143
17763
|
type: "slider",
|
|
17144
17764
|
label: "Min FPS Threshold",
|
|
17145
|
-
description: "If a camera sustains FPS below this value for the duration set by 'Low FPS Window' below, it is paused (load-shed) and new dispatches are blocked. Auto-resumes with exponential backoff. 0 = disabled.",
|
|
17765
|
+
description: "Per-camera low-FPS load shedding. If a camera sustains FPS below this value for the duration set by 'Low FPS Window' below, it is paused (load-shed) and new dispatches are blocked. Auto-resumes with exponential backoff. 0 = disabled (default) — a single slow camera is never detached; only capacity-based placement applies.",
|
|
17146
17766
|
min: 0,
|
|
17147
17767
|
max: 10,
|
|
17148
17768
|
step: 1,
|
|
17149
|
-
default:
|
|
17769
|
+
default: PipelineOrchestratorAddon.DEFAULT_FPS_MIN_THRESHOLD,
|
|
17150
17770
|
showValue: true,
|
|
17151
17771
|
unit: "fps"
|
|
17152
17772
|
},
|
|
@@ -17443,8 +18063,41 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17443
18063
|
}
|
|
17444
18064
|
]
|
|
17445
18065
|
};
|
|
18066
|
+
const hasNativeObjectDetection = await this.deviceHasNativeObjectDetectionCap(input.deviceId);
|
|
18067
|
+
const nativeObjectDetectionSections = [];
|
|
18068
|
+
if (hasNativeObjectDetection) {
|
|
18069
|
+
let nativeObjectDetectionEnabled = true;
|
|
18070
|
+
try {
|
|
18071
|
+
const status = await this.ctx.api?.nativeObjectDetection.getStatus.query({
|
|
18072
|
+
deviceId: input.deviceId
|
|
18073
|
+
});
|
|
18074
|
+
if (status !== void 0 && status !== null && "enabled" in status) {
|
|
18075
|
+
nativeObjectDetectionEnabled = Boolean(
|
|
18076
|
+
status.enabled
|
|
18077
|
+
);
|
|
18078
|
+
}
|
|
18079
|
+
} catch {
|
|
18080
|
+
}
|
|
18081
|
+
nativeObjectDetectionSections.push({
|
|
18082
|
+
id: "onboard-object-detection",
|
|
18083
|
+
title: "Onboard Object Detection",
|
|
18084
|
+
tab: "motion",
|
|
18085
|
+
order: 10,
|
|
18086
|
+
fields: [
|
|
18087
|
+
{
|
|
18088
|
+
type: "boolean",
|
|
18089
|
+
key: "nativeObjectDetectionEnabled",
|
|
18090
|
+
label: "Use onboard object detection",
|
|
18091
|
+
description: "When enabled, AI detections from the camera firmware are forwarded to the system notification and recording pipeline. Onboard motion events are never suppressed.",
|
|
18092
|
+
default: true,
|
|
18093
|
+
immediate: true,
|
|
18094
|
+
value: nativeObjectDetectionEnabled
|
|
18095
|
+
}
|
|
18096
|
+
]
|
|
18097
|
+
});
|
|
18098
|
+
}
|
|
17446
18099
|
return {
|
|
17447
|
-
sections: [...baseSections, zonesSection]
|
|
18100
|
+
sections: [...baseSections, zonesSection, ...nativeObjectDetectionSections]
|
|
17448
18101
|
};
|
|
17449
18102
|
}
|
|
17450
18103
|
// `getCameraPipelineWithFallback` was a thin wrapper over
|
|
@@ -17505,11 +18158,25 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17505
18158
|
}
|
|
17506
18159
|
async applyDeviceSettingsPatch(input) {
|
|
17507
18160
|
const { pipelinePatch, rest } = this.splitPipelinePatch(input.patch);
|
|
18161
|
+
const { nativeObjectDetectionEnabled, ...orchestratorRest } = rest;
|
|
18162
|
+
if (nativeObjectDetectionEnabled !== void 0) {
|
|
18163
|
+
try {
|
|
18164
|
+
await this.ctx.api?.nativeObjectDetection.setEnabled.mutate({
|
|
18165
|
+
deviceId: input.deviceId,
|
|
18166
|
+
enabled: Boolean(nativeObjectDetectionEnabled)
|
|
18167
|
+
});
|
|
18168
|
+
} catch (err) {
|
|
18169
|
+
this.ctx.logger.warn("Failed to route nativeObjectDetectionEnabled to cap", {
|
|
18170
|
+
tags: { deviceId: input.deviceId },
|
|
18171
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
18172
|
+
});
|
|
18173
|
+
}
|
|
18174
|
+
}
|
|
17508
18175
|
if (Object.keys(pipelinePatch).length > 0) {
|
|
17509
18176
|
await this.applyPipelinePatch(input.deviceId, pipelinePatch);
|
|
17510
18177
|
}
|
|
17511
|
-
if (Object.keys(
|
|
17512
|
-
await this.writeDeviceOrchestratorSettings(input.deviceId,
|
|
18178
|
+
if (Object.keys(orchestratorRest).length > 0) {
|
|
18179
|
+
await this.writeDeviceOrchestratorSettings(input.deviceId, orchestratorRest);
|
|
17513
18180
|
}
|
|
17514
18181
|
return { success: true };
|
|
17515
18182
|
}
|
|
@@ -17684,6 +18351,23 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17684
18351
|
return false;
|
|
17685
18352
|
}
|
|
17686
18353
|
}
|
|
18354
|
+
/**
|
|
18355
|
+
* Returns true when the camera has a native `native-object-detection`
|
|
18356
|
+
* binding (i.e. the driver registered the cap). Used to gate the
|
|
18357
|
+
* "Use onboard object detection" toggle in `getDeviceSettingsContribution`.
|
|
18358
|
+
*/
|
|
18359
|
+
async deviceHasNativeObjectDetectionCap(deviceId) {
|
|
18360
|
+
const api = this.api;
|
|
18361
|
+
if (!api) return false;
|
|
18362
|
+
try {
|
|
18363
|
+
const bindings = await api.deviceManager.getBindings.query({ deviceId });
|
|
18364
|
+
return bindings.entries.some(
|
|
18365
|
+
(e) => e.kind === "native" && e.capName === "native-object-detection"
|
|
18366
|
+
);
|
|
18367
|
+
} catch {
|
|
18368
|
+
return false;
|
|
18369
|
+
}
|
|
18370
|
+
}
|
|
17687
18371
|
async resolveDeviceDetectionSettings(deviceId) {
|
|
17688
18372
|
const ctx = this.ctx;
|
|
17689
18373
|
if (!ctx?.settings) {
|
|
@@ -17748,6 +18432,8 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17748
18432
|
const detectionMode = typeof userDetectionMode === "string" && isPipelinePhaseMode(userDetectionMode) ? userDetectionMode : profile?.defaults.detectionMode ?? "on-motion";
|
|
17749
18433
|
const userAudioMode = raw["audioMode"];
|
|
17750
18434
|
const audioMode = typeof userAudioMode === "string" && isPipelinePhaseMode(userAudioMode) ? userAudioMode : profile?.defaults.audioMode ?? "always-on";
|
|
18435
|
+
const userOnboardMotionDrivesAnalyzer = raw["onboardMotionDrivesAnalyzer"];
|
|
18436
|
+
const onboardMotionDrivesAnalyzer = typeof userOnboardMotionDrivesAnalyzer === "boolean" ? userOnboardMotionDrivesAnalyzer : true;
|
|
17751
18437
|
return {
|
|
17752
18438
|
motionDetectionEnabled,
|
|
17753
18439
|
pipelineEnabled,
|
|
@@ -17758,7 +18444,8 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17758
18444
|
detectionFps: mustNumber("detectionFps"),
|
|
17759
18445
|
motionCooldownMs: mustNumber("motionCooldownMs"),
|
|
17760
18446
|
detectionMode,
|
|
17761
|
-
audioMode
|
|
18447
|
+
audioMode,
|
|
18448
|
+
onboardMotionDrivesAnalyzer
|
|
17762
18449
|
};
|
|
17763
18450
|
} catch (err) {
|
|
17764
18451
|
const msg = errMsg(err);
|
|
@@ -17866,7 +18553,8 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17866
18553
|
motionCooldownMs: resolved.motionCooldownMs,
|
|
17867
18554
|
audioStreamId: void 0,
|
|
17868
18555
|
detectionMode: resolved.detectionMode,
|
|
17869
|
-
audioMode: resolved.audioMode
|
|
18556
|
+
audioMode: resolved.audioMode,
|
|
18557
|
+
onboardMotionDrivesAnalyzer: resolved.onboardMotionDrivesAnalyzer
|
|
17870
18558
|
};
|
|
17871
18559
|
}
|
|
17872
18560
|
/**
|
|
@@ -17901,7 +18589,8 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17901
18589
|
audio: pipelineConfig.audio ?? null,
|
|
17902
18590
|
zones,
|
|
17903
18591
|
detectionMode: config2.detectionMode,
|
|
17904
|
-
audioMode: config2.audioMode
|
|
18592
|
+
audioMode: config2.audioMode,
|
|
18593
|
+
onboardMotionDrivesAnalyzer: config2.onboardMotionDrivesAnalyzer
|
|
17905
18594
|
};
|
|
17906
18595
|
let dispatchedNodeId = null;
|
|
17907
18596
|
try {
|
|
@@ -18086,6 +18775,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18086
18775
|
if (a.detectionFps !== b.detectionFps) return false;
|
|
18087
18776
|
if (a.motionCooldownMs !== b.motionCooldownMs) return false;
|
|
18088
18777
|
if (a.audioStreamId !== b.audioStreamId) return false;
|
|
18778
|
+
if (a.onboardMotionDrivesAnalyzer !== b.onboardMotionDrivesAnalyzer) return false;
|
|
18089
18779
|
if (a.motionSources.length !== b.motionSources.length) return false;
|
|
18090
18780
|
for (const s of a.motionSources) if (!b.motionSources.includes(s)) return false;
|
|
18091
18781
|
return true;
|
|
@@ -18175,9 +18865,20 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18175
18865
|
* across recoveries.
|
|
18176
18866
|
*/
|
|
18177
18867
|
static DEFAULT_FPS_LOW_WINDOW_MS = 6e4;
|
|
18868
|
+
/**
|
|
18869
|
+
* Default min-FPS threshold for the per-camera low-fps load shedder.
|
|
18870
|
+
*
|
|
18871
|
+
* `0` = DISABLED by default: a single slow camera (e.g. an on-motion
|
|
18872
|
+
* camera idling at <1 fps between motion events) must NOT be detached or
|
|
18873
|
+
* block new dispatches. Capacity-based assignment (`collectAgentLoad` +
|
|
18874
|
+
* `balance()`, the "high-water" placement logic) is independent of this
|
|
18875
|
+
* and stays active. Operators can re-enable per-camera shedding by setting
|
|
18876
|
+
* a positive `fpsMinThreshold` in global settings.
|
|
18877
|
+
*/
|
|
18878
|
+
static DEFAULT_FPS_MIN_THRESHOLD = 0;
|
|
18178
18879
|
enforceLoadManagement(deviceId, metrics) {
|
|
18179
18880
|
const cfg = this.globalSettings ?? {};
|
|
18180
|
-
const threshold = typeof cfg.fpsMinThreshold === "number" ? cfg.fpsMinThreshold :
|
|
18881
|
+
const threshold = typeof cfg.fpsMinThreshold === "number" ? cfg.fpsMinThreshold : PipelineOrchestratorAddon.DEFAULT_FPS_MIN_THRESHOLD;
|
|
18181
18882
|
const windowMs = typeof cfg.fpsLowWindowMs === "number" && cfg.fpsLowWindowMs > 0 ? cfg.fpsLowWindowMs : PipelineOrchestratorAddon.DEFAULT_FPS_LOW_WINDOW_MS;
|
|
18182
18883
|
if (threshold <= 0) return;
|
|
18183
18884
|
if (metrics.phase !== "active") return;
|
|
@@ -18297,7 +18998,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18297
18998
|
async handleInferenceResult(payload) {
|
|
18298
18999
|
const ctx = this.ctx;
|
|
18299
19000
|
if (!ctx) return;
|
|
18300
|
-
const { deviceId, frame, frameHandle } = payload;
|
|
19001
|
+
const { deviceId, frame, frameHandle, capturedAt } = payload;
|
|
18301
19002
|
if (frame.detections.length === 0) return;
|
|
18302
19003
|
this.ctx.eventBus.emit({
|
|
18303
19004
|
id: `detection-${deviceId}-${Date.now()}`,
|
|
@@ -18305,8 +19006,10 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18305
19006
|
source: { type: "device", id: deviceId, nodeId: "hub", addonId: "pipeline-orchestrator", deviceId },
|
|
18306
19007
|
timestamp: /* @__PURE__ */ new Date(),
|
|
18307
19008
|
// Forward the upstream shm-ring `frameHandle` so post-analysis
|
|
18308
|
-
// consumers (Task 8) can resolve the original frame zero-copy
|
|
18309
|
-
|
|
19009
|
+
// consumers (Task 8) can resolve the original frame zero-copy, and
|
|
19010
|
+
// `capturedAt` (shm-commit wall-clock) so a UI/probe consumer can
|
|
19011
|
+
// measure true frame-capture → delivery latency.
|
|
19012
|
+
data: { frame, analysisResults: [], frameHandle, ...typeof capturedAt === "number" ? { capturedAt } : {} }
|
|
18310
19013
|
});
|
|
18311
19014
|
}
|
|
18312
19015
|
/**
|
|
@@ -18432,8 +19135,8 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18432
19135
|
}
|
|
18433
19136
|
try {
|
|
18434
19137
|
const byteLength = chunk.data.byteLength;
|
|
18435
|
-
const
|
|
18436
|
-
|
|
19138
|
+
const data = new Uint8Array(byteLength);
|
|
19139
|
+
data.set(
|
|
18437
19140
|
new Uint8Array(
|
|
18438
19141
|
chunk.data.buffer,
|
|
18439
19142
|
chunk.data.byteOffset,
|
|
@@ -18441,7 +19144,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18441
19144
|
)
|
|
18442
19145
|
);
|
|
18443
19146
|
const audioChunkInput = {
|
|
18444
|
-
data
|
|
19147
|
+
data,
|
|
18445
19148
|
sampleRate: chunk.sampleRate,
|
|
18446
19149
|
channels: chunk.channels,
|
|
18447
19150
|
timestamp: chunk.timestamp,
|