@camstack/addon-pipeline-orchestrator 0.1.26 → 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-iOQz8pwN.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-HZ0iFEJA.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-CUXiTSWS.mjs → index-Dy2V7VOm.mjs} +3775 -3279
- package/dist/{index-Cbqs9uJn.mjs → index-kp_mtnZv.mjs} +1 -1
- package/dist/index.js +753 -64
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +753 -64
- 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);
|
|
@@ -13147,7 +13434,8 @@ const PackageUpdateSchema = object({
|
|
|
13147
13434
|
currentVersion: string(),
|
|
13148
13435
|
latestVersion: string(),
|
|
13149
13436
|
category: _enum(["addon", "core"]),
|
|
13150
|
-
requiresRestart: boolean()
|
|
13437
|
+
requiresRestart: boolean(),
|
|
13438
|
+
isSystem: boolean()
|
|
13151
13439
|
});
|
|
13152
13440
|
const PackageVersionInfoSchema = object({
|
|
13153
13441
|
version: string(),
|
|
@@ -13180,6 +13468,42 @@ const UpdateFrameworkPackageResultSchema = object({
|
|
|
13180
13468
|
/** Ms-epoch the server scheduled its self-restart. */
|
|
13181
13469
|
restartingAt: number()
|
|
13182
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
|
+
});
|
|
13183
13507
|
const FrameworkPackageStatusSchema = object({
|
|
13184
13508
|
packageName: string(),
|
|
13185
13509
|
currentVersion: string(),
|
|
@@ -13317,7 +13641,7 @@ const CustomActionInputSchema = object({
|
|
|
13317
13641
|
getLastRestart: method(
|
|
13318
13642
|
_void(),
|
|
13319
13643
|
object({
|
|
13320
|
-
kind: _enum(["framework-update", "manual", "system"]),
|
|
13644
|
+
kind: _enum(["framework-update", "manual", "system", "framework-bulk-update"]),
|
|
13321
13645
|
packageName: string().optional(),
|
|
13322
13646
|
fromVersion: string().optional(),
|
|
13323
13647
|
toVersion: string().optional(),
|
|
@@ -13407,11 +13731,70 @@ const CustomActionInputSchema = object({
|
|
|
13407
13731
|
updateFrameworkPackage: method(
|
|
13408
13732
|
object({
|
|
13409
13733
|
packageName: string().min(1),
|
|
13410
|
-
version: string().optional()
|
|
13734
|
+
version: string().optional(),
|
|
13735
|
+
deferRestart: boolean().optional()
|
|
13411
13736
|
}),
|
|
13412
13737
|
UpdateFrameworkPackageResultSchema,
|
|
13413
13738
|
{ kind: "mutation", auth: "admin" }
|
|
13414
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
|
+
),
|
|
13415
13798
|
getVersions: method(
|
|
13416
13799
|
object({ name: string() }),
|
|
13417
13800
|
array(PackageVersionInfoSchema).readonly()
|
|
@@ -13539,6 +13922,7 @@ var EventCategory = /* @__PURE__ */ ((EventCategory2) => {
|
|
|
13539
13922
|
EventCategory2["StreamBrokerOnCamStreamDemand"] = "stream-broker.onCamStreamDemand";
|
|
13540
13923
|
EventCategory2["StreamBrokerOnCamStreamIdle"] = "stream-broker.onCamStreamIdle";
|
|
13541
13924
|
EventCategory2["StreamBrokerOnRequestStreamSourceRefresh"] = "stream-broker.onRequestStreamSourceRefresh";
|
|
13925
|
+
EventCategory2["StreamParamsChanged"] = "stream-params.changed";
|
|
13542
13926
|
EventCategory2["DeviceStateChanged"] = "device.state-changed";
|
|
13543
13927
|
EventCategory2["BatteryOnStatusChanged"] = "battery.onStatusChanged";
|
|
13544
13928
|
EventCategory2["DoorbellOnPressed"] = "doorbell.onPressed";
|
|
@@ -13586,6 +13970,7 @@ var EventCategory = /* @__PURE__ */ ((EventCategory2) => {
|
|
|
13586
13970
|
EventCategory2["NetworkTunnelStarted"] = "network.tunnel.started";
|
|
13587
13971
|
EventCategory2["NetworkTunnelStopped"] = "network.tunnel.stopped";
|
|
13588
13972
|
EventCategory2["LocalNetworkChanged"] = "network.local.changed";
|
|
13973
|
+
EventCategory2["MeshNetworkChanged"] = "network.mesh.changed";
|
|
13589
13974
|
EventCategory2["BackupCompleted"] = "backup.completed";
|
|
13590
13975
|
EventCategory2["BackupRestored"] = "backup.restored";
|
|
13591
13976
|
EventCategory2["NotificationDispatched"] = "notification.dispatched";
|
|
@@ -13596,6 +13981,7 @@ var EventCategory = /* @__PURE__ */ ((EventCategory2) => {
|
|
|
13596
13981
|
EventCategory2["DeviceAwake"] = "device.awake";
|
|
13597
13982
|
EventCategory2["DeviceSleeping"] = "device.sleeping";
|
|
13598
13983
|
EventCategory2["RetentionCleanup"] = "retention.cleanup";
|
|
13984
|
+
EventCategory2["AddonsBulkUpdateProgress"] = "addons.bulk-update-progress";
|
|
13599
13985
|
return EventCategory2;
|
|
13600
13986
|
})(EventCategory || {});
|
|
13601
13987
|
function isEvent(event2, category) {
|
|
@@ -13748,7 +14134,7 @@ class BaseAddon {
|
|
|
13748
14134
|
}
|
|
13749
14135
|
// ── Settings schemas (override to provide UI) ─────────────────────────
|
|
13750
14136
|
/** Override to provide global-level settings UI schema. */
|
|
13751
|
-
globalSettingsSchema() {
|
|
14137
|
+
globalSettingsSchema(_cap) {
|
|
13752
14138
|
return null;
|
|
13753
14139
|
}
|
|
13754
14140
|
/** Override to provide device-level settings UI schema. */
|
|
@@ -13762,8 +14148,8 @@ class BaseAddon {
|
|
|
13762
14148
|
// blob and every addon used exactly one of them; the distinction was
|
|
13763
14149
|
// never semantically load-bearing. `global` won because it was the
|
|
13764
14150
|
// widely-used one and the name reads naturally (per-node addon config).
|
|
13765
|
-
async getGlobalSettings(overlay) {
|
|
13766
|
-
const schema = this.globalSettingsSchema();
|
|
14151
|
+
async getGlobalSettings(overlay, cap) {
|
|
14152
|
+
const schema = this.globalSettingsSchema(cap);
|
|
13767
14153
|
if (!schema) return { sections: [] };
|
|
13768
14154
|
const raw = await this._ctx?.settings?.readAddonStore() ?? {};
|
|
13769
14155
|
return hydrateSchema(schema, overlay ? { ...raw, ...overlay } : raw);
|
|
@@ -14182,6 +14568,14 @@ class ReadinessRegistry {
|
|
|
14182
14568
|
cleanup();
|
|
14183
14569
|
reject(new ReadinessTimeoutError(capName, scope, this.now() - start));
|
|
14184
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();
|
|
14185
14579
|
const onAbort = () => {
|
|
14186
14580
|
if (settled) return;
|
|
14187
14581
|
settled = true;
|
|
@@ -14192,6 +14586,7 @@ class ReadinessRegistry {
|
|
|
14192
14586
|
function cleanup() {
|
|
14193
14587
|
unsubscribe();
|
|
14194
14588
|
if (timer !== null) clearTimeout(timer);
|
|
14589
|
+
clearInterval(pendingLogTimer);
|
|
14195
14590
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
14196
14591
|
}
|
|
14197
14592
|
});
|
|
@@ -14745,6 +15140,8 @@ function balanceAudio(input) {
|
|
|
14745
15140
|
}
|
|
14746
15141
|
const POLL_INTERVAL_MS = 200;
|
|
14747
15142
|
const PULL_MAX_COUNT = 8;
|
|
15143
|
+
const RESUBSCRIBE_AFTER_FAILURES = 2;
|
|
15144
|
+
const RESUBSCRIBE_THROTTLE_TICKS = 5;
|
|
14748
15145
|
async function startAudioChunkPoller(options) {
|
|
14749
15146
|
const { api, brokerId, tag, onChunk, logger } = options;
|
|
14750
15147
|
let subscriptionId;
|
|
@@ -14762,6 +15159,19 @@ async function startAudioChunkPoller(options) {
|
|
|
14762
15159
|
}
|
|
14763
15160
|
let stopped = false;
|
|
14764
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
|
+
};
|
|
14765
15175
|
const tick = async () => {
|
|
14766
15176
|
if (stopped) return;
|
|
14767
15177
|
try {
|
|
@@ -14769,14 +15179,24 @@ async function startAudioChunkPoller(options) {
|
|
|
14769
15179
|
subscriptionId,
|
|
14770
15180
|
maxCount: PULL_MAX_COUNT
|
|
14771
15181
|
});
|
|
15182
|
+
if (consecutiveFailures > 0) {
|
|
15183
|
+
logger.info("audio-chunk poller: stream recovered", { meta: { brokerId, subscriptionId } });
|
|
15184
|
+
}
|
|
15185
|
+
consecutiveFailures = 0;
|
|
14772
15186
|
for (const chunk of chunks) {
|
|
14773
15187
|
if (stopped) break;
|
|
14774
15188
|
await onChunk(chunk);
|
|
14775
15189
|
}
|
|
14776
15190
|
} catch (err) {
|
|
14777
|
-
|
|
14778
|
-
|
|
14779
|
-
|
|
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
|
+
}
|
|
14780
15200
|
}
|
|
14781
15201
|
if (!stopped) {
|
|
14782
15202
|
timer = setTimeout(() => void tick(), POLL_INTERVAL_MS);
|
|
@@ -14797,6 +15217,18 @@ async function startAudioChunkPoller(options) {
|
|
|
14797
15217
|
});
|
|
14798
15218
|
};
|
|
14799
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
|
+
}
|
|
14800
15232
|
const PHASE_MODE_VALUES = /* @__PURE__ */ new Set([
|
|
14801
15233
|
"disabled",
|
|
14802
15234
|
"always-on",
|
|
@@ -14808,6 +15240,7 @@ function isPipelinePhaseMode(v) {
|
|
|
14808
15240
|
const PREFERRED_AGENT_SETTING = "preferredAgent";
|
|
14809
15241
|
const AUDIO_NODE_SETTING = "audioNodeId";
|
|
14810
15242
|
const DEFAULT_BROKER_CALL_TIMEOUT_MS = 5e3;
|
|
15243
|
+
const RECONCILE_DEBOUNCE_MS = 200;
|
|
14811
15244
|
const AGENT_SETTINGS_KEY = "agentSettings";
|
|
14812
15245
|
const CAMERA_SETTINGS_KEY = "cameraSettings";
|
|
14813
15246
|
const TEMPLATES_KEY = "templates";
|
|
@@ -14959,6 +15392,12 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
14959
15392
|
loadShedState = /* @__PURE__ */ new Map();
|
|
14960
15393
|
/** Timer for the auto-resume sweep. */
|
|
14961
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;
|
|
14962
15401
|
initTimestamp = 0;
|
|
14963
15402
|
constructor() {
|
|
14964
15403
|
super({});
|
|
@@ -15017,6 +15456,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15017
15456
|
meta: { error: msg }
|
|
15018
15457
|
});
|
|
15019
15458
|
});
|
|
15459
|
+
this.scheduleReconcile();
|
|
15020
15460
|
}
|
|
15021
15461
|
);
|
|
15022
15462
|
this.watchCapability(["pipeline-executor", "analysis-pipeline"], {
|
|
@@ -15114,6 +15554,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15114
15554
|
});
|
|
15115
15555
|
}, PROFILE_SLOTS_DEBOUNCE_MS)
|
|
15116
15556
|
);
|
|
15557
|
+
this.scheduleReconcile();
|
|
15117
15558
|
}
|
|
15118
15559
|
);
|
|
15119
15560
|
this.unsubDeviceUnregistered = this.ctx.eventBus.subscribe(
|
|
@@ -15293,9 +15734,15 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15293
15734
|
}
|
|
15294
15735
|
]
|
|
15295
15736
|
};
|
|
15737
|
+
this.scheduleReconcile();
|
|
15296
15738
|
return {
|
|
15297
15739
|
providers: [
|
|
15298
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 },
|
|
15299
15746
|
{ capability: zonesCapability, provider: this.zonesProvider },
|
|
15300
15747
|
{ capability: zoneRulesCapability, provider: this.zoneRulesProvider },
|
|
15301
15748
|
{ capability: addonWidgetsSourceCapability, provider: widgetsProvider }
|
|
@@ -15420,6 +15867,10 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15420
15867
|
this.profileSlotTimers.clear();
|
|
15421
15868
|
this.profileSlotTimers = null;
|
|
15422
15869
|
}
|
|
15870
|
+
if (this.reconcileTimer !== null) {
|
|
15871
|
+
clearTimeout(this.reconcileTimer);
|
|
15872
|
+
this.reconcileTimer = null;
|
|
15873
|
+
}
|
|
15423
15874
|
this.unsubDeviceRegistered?.();
|
|
15424
15875
|
this.unsubDeviceRegistered = null;
|
|
15425
15876
|
this.unsubDeviceUnregistered?.();
|
|
@@ -15495,7 +15946,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15495
15946
|
);
|
|
15496
15947
|
this.cameraConfigs.set(runnerConfig.deviceId, runnerConfig);
|
|
15497
15948
|
const cfg = this.globalSettings ?? {};
|
|
15498
|
-
const minThreshold = typeof cfg.fpsMinThreshold === "number" ? cfg.fpsMinThreshold :
|
|
15949
|
+
const minThreshold = typeof cfg.fpsMinThreshold === "number" ? cfg.fpsMinThreshold : PipelineOrchestratorAddon.DEFAULT_FPS_MIN_THRESHOLD;
|
|
15499
15950
|
const windowMs = typeof cfg.fpsLowWindowMs === "number" && cfg.fpsLowWindowMs > 0 ? cfg.fpsLowWindowMs : PipelineOrchestratorAddon.DEFAULT_FPS_LOW_WINDOW_MS;
|
|
15500
15951
|
if (minThreshold > 0 && this.initTimestamp && Date.now() - this.initTimestamp > 3e4) {
|
|
15501
15952
|
const now = Date.now();
|
|
@@ -15938,7 +16389,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15938
16389
|
const api = ctx?.api;
|
|
15939
16390
|
if (!api) continue;
|
|
15940
16391
|
try {
|
|
15941
|
-
const load = await
|
|
16392
|
+
const load = await this.queryLocalLoadBounded(nodeId);
|
|
15942
16393
|
loads.push(load);
|
|
15943
16394
|
} catch (err) {
|
|
15944
16395
|
const msg = errMsg(err);
|
|
@@ -15963,6 +16414,53 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15963
16414
|
}
|
|
15964
16415
|
this.cachedAgentLoad = next;
|
|
15965
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
|
+
}
|
|
15966
16464
|
async readPreferredAgent(deviceId) {
|
|
15967
16465
|
const ctx = this.ctx;
|
|
15968
16466
|
if (!ctx?.settings) return null;
|
|
@@ -16260,6 +16758,44 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
16260
16758
|
"audio-analyzer ready — node now available for audio dispatch",
|
|
16261
16759
|
{ tags: { nodeId }, meta: { epoch: t.epoch } }
|
|
16262
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
|
+
}
|
|
16263
16799
|
}
|
|
16264
16800
|
/**
|
|
16265
16801
|
* Act on a `detection-pipeline` transition: on `ready` with a new
|
|
@@ -16331,6 +16867,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
16331
16867
|
});
|
|
16332
16868
|
});
|
|
16333
16869
|
}, 3e3);
|
|
16870
|
+
this.scheduleReconcile();
|
|
16334
16871
|
} catch (err) {
|
|
16335
16872
|
this.ctx.logger.debug("readiness seed+redispatch failed", {
|
|
16336
16873
|
tags: { nodeId },
|
|
@@ -16375,6 +16912,75 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
16375
16912
|
}
|
|
16376
16913
|
}
|
|
16377
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
|
+
}
|
|
16378
16984
|
// ── Capability bindings (cap methods) ────────────────────────────────
|
|
16379
16985
|
async getCapabilityBindings(input) {
|
|
16380
16986
|
const ctx = this.ctx;
|
|
@@ -17156,11 +17762,11 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17156
17762
|
key: "fpsMinThreshold",
|
|
17157
17763
|
type: "slider",
|
|
17158
17764
|
label: "Min FPS Threshold",
|
|
17159
|
-
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.",
|
|
17160
17766
|
min: 0,
|
|
17161
17767
|
max: 10,
|
|
17162
17768
|
step: 1,
|
|
17163
|
-
default:
|
|
17769
|
+
default: PipelineOrchestratorAddon.DEFAULT_FPS_MIN_THRESHOLD,
|
|
17164
17770
|
showValue: true,
|
|
17165
17771
|
unit: "fps"
|
|
17166
17772
|
},
|
|
@@ -17457,8 +18063,41 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17457
18063
|
}
|
|
17458
18064
|
]
|
|
17459
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
|
+
}
|
|
17460
18099
|
return {
|
|
17461
|
-
sections: [...baseSections, zonesSection]
|
|
18100
|
+
sections: [...baseSections, zonesSection, ...nativeObjectDetectionSections]
|
|
17462
18101
|
};
|
|
17463
18102
|
}
|
|
17464
18103
|
// `getCameraPipelineWithFallback` was a thin wrapper over
|
|
@@ -17519,11 +18158,25 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17519
18158
|
}
|
|
17520
18159
|
async applyDeviceSettingsPatch(input) {
|
|
17521
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
|
+
}
|
|
17522
18175
|
if (Object.keys(pipelinePatch).length > 0) {
|
|
17523
18176
|
await this.applyPipelinePatch(input.deviceId, pipelinePatch);
|
|
17524
18177
|
}
|
|
17525
|
-
if (Object.keys(
|
|
17526
|
-
await this.writeDeviceOrchestratorSettings(input.deviceId,
|
|
18178
|
+
if (Object.keys(orchestratorRest).length > 0) {
|
|
18179
|
+
await this.writeDeviceOrchestratorSettings(input.deviceId, orchestratorRest);
|
|
17527
18180
|
}
|
|
17528
18181
|
return { success: true };
|
|
17529
18182
|
}
|
|
@@ -17698,6 +18351,23 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17698
18351
|
return false;
|
|
17699
18352
|
}
|
|
17700
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
|
+
}
|
|
17701
18371
|
async resolveDeviceDetectionSettings(deviceId) {
|
|
17702
18372
|
const ctx = this.ctx;
|
|
17703
18373
|
if (!ctx?.settings) {
|
|
@@ -17762,6 +18432,8 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17762
18432
|
const detectionMode = typeof userDetectionMode === "string" && isPipelinePhaseMode(userDetectionMode) ? userDetectionMode : profile?.defaults.detectionMode ?? "on-motion";
|
|
17763
18433
|
const userAudioMode = raw["audioMode"];
|
|
17764
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;
|
|
17765
18437
|
return {
|
|
17766
18438
|
motionDetectionEnabled,
|
|
17767
18439
|
pipelineEnabled,
|
|
@@ -17772,7 +18444,8 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17772
18444
|
detectionFps: mustNumber("detectionFps"),
|
|
17773
18445
|
motionCooldownMs: mustNumber("motionCooldownMs"),
|
|
17774
18446
|
detectionMode,
|
|
17775
|
-
audioMode
|
|
18447
|
+
audioMode,
|
|
18448
|
+
onboardMotionDrivesAnalyzer
|
|
17776
18449
|
};
|
|
17777
18450
|
} catch (err) {
|
|
17778
18451
|
const msg = errMsg(err);
|
|
@@ -17880,7 +18553,8 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17880
18553
|
motionCooldownMs: resolved.motionCooldownMs,
|
|
17881
18554
|
audioStreamId: void 0,
|
|
17882
18555
|
detectionMode: resolved.detectionMode,
|
|
17883
|
-
audioMode: resolved.audioMode
|
|
18556
|
+
audioMode: resolved.audioMode,
|
|
18557
|
+
onboardMotionDrivesAnalyzer: resolved.onboardMotionDrivesAnalyzer
|
|
17884
18558
|
};
|
|
17885
18559
|
}
|
|
17886
18560
|
/**
|
|
@@ -17915,7 +18589,8 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17915
18589
|
audio: pipelineConfig.audio ?? null,
|
|
17916
18590
|
zones,
|
|
17917
18591
|
detectionMode: config2.detectionMode,
|
|
17918
|
-
audioMode: config2.audioMode
|
|
18592
|
+
audioMode: config2.audioMode,
|
|
18593
|
+
onboardMotionDrivesAnalyzer: config2.onboardMotionDrivesAnalyzer
|
|
17919
18594
|
};
|
|
17920
18595
|
let dispatchedNodeId = null;
|
|
17921
18596
|
try {
|
|
@@ -18100,6 +18775,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18100
18775
|
if (a.detectionFps !== b.detectionFps) return false;
|
|
18101
18776
|
if (a.motionCooldownMs !== b.motionCooldownMs) return false;
|
|
18102
18777
|
if (a.audioStreamId !== b.audioStreamId) return false;
|
|
18778
|
+
if (a.onboardMotionDrivesAnalyzer !== b.onboardMotionDrivesAnalyzer) return false;
|
|
18103
18779
|
if (a.motionSources.length !== b.motionSources.length) return false;
|
|
18104
18780
|
for (const s of a.motionSources) if (!b.motionSources.includes(s)) return false;
|
|
18105
18781
|
return true;
|
|
@@ -18189,9 +18865,20 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18189
18865
|
* across recoveries.
|
|
18190
18866
|
*/
|
|
18191
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;
|
|
18192
18879
|
enforceLoadManagement(deviceId, metrics) {
|
|
18193
18880
|
const cfg = this.globalSettings ?? {};
|
|
18194
|
-
const threshold = typeof cfg.fpsMinThreshold === "number" ? cfg.fpsMinThreshold :
|
|
18881
|
+
const threshold = typeof cfg.fpsMinThreshold === "number" ? cfg.fpsMinThreshold : PipelineOrchestratorAddon.DEFAULT_FPS_MIN_THRESHOLD;
|
|
18195
18882
|
const windowMs = typeof cfg.fpsLowWindowMs === "number" && cfg.fpsLowWindowMs > 0 ? cfg.fpsLowWindowMs : PipelineOrchestratorAddon.DEFAULT_FPS_LOW_WINDOW_MS;
|
|
18196
18883
|
if (threshold <= 0) return;
|
|
18197
18884
|
if (metrics.phase !== "active") return;
|
|
@@ -18311,7 +18998,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18311
18998
|
async handleInferenceResult(payload) {
|
|
18312
18999
|
const ctx = this.ctx;
|
|
18313
19000
|
if (!ctx) return;
|
|
18314
|
-
const { deviceId, frame, frameHandle } = payload;
|
|
19001
|
+
const { deviceId, frame, frameHandle, capturedAt } = payload;
|
|
18315
19002
|
if (frame.detections.length === 0) return;
|
|
18316
19003
|
this.ctx.eventBus.emit({
|
|
18317
19004
|
id: `detection-${deviceId}-${Date.now()}`,
|
|
@@ -18319,8 +19006,10 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18319
19006
|
source: { type: "device", id: deviceId, nodeId: "hub", addonId: "pipeline-orchestrator", deviceId },
|
|
18320
19007
|
timestamp: /* @__PURE__ */ new Date(),
|
|
18321
19008
|
// Forward the upstream shm-ring `frameHandle` so post-analysis
|
|
18322
|
-
// consumers (Task 8) can resolve the original frame zero-copy
|
|
18323
|
-
|
|
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 } : {} }
|
|
18324
19013
|
});
|
|
18325
19014
|
}
|
|
18326
19015
|
/**
|
|
@@ -18446,8 +19135,8 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18446
19135
|
}
|
|
18447
19136
|
try {
|
|
18448
19137
|
const byteLength = chunk.data.byteLength;
|
|
18449
|
-
const
|
|
18450
|
-
|
|
19138
|
+
const data = new Uint8Array(byteLength);
|
|
19139
|
+
data.set(
|
|
18451
19140
|
new Uint8Array(
|
|
18452
19141
|
chunk.data.buffer,
|
|
18453
19142
|
chunk.data.byteOffset,
|
|
@@ -18455,7 +19144,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18455
19144
|
)
|
|
18456
19145
|
);
|
|
18457
19146
|
const audioChunkInput = {
|
|
18458
|
-
data
|
|
19147
|
+
data,
|
|
18459
19148
|
sampleRate: chunk.sampleRate,
|
|
18460
19149
|
channels: chunk.channels,
|
|
18461
19150
|
timestamp: chunk.timestamp,
|