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