@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.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);
|
|
@@ -13149,7 +13436,8 @@ const PackageUpdateSchema = object({
|
|
|
13149
13436
|
currentVersion: string(),
|
|
13150
13437
|
latestVersion: string(),
|
|
13151
13438
|
category: _enum(["addon", "core"]),
|
|
13152
|
-
requiresRestart: boolean()
|
|
13439
|
+
requiresRestart: boolean(),
|
|
13440
|
+
isSystem: boolean()
|
|
13153
13441
|
});
|
|
13154
13442
|
const PackageVersionInfoSchema = object({
|
|
13155
13443
|
version: string(),
|
|
@@ -13182,6 +13470,42 @@ const UpdateFrameworkPackageResultSchema = object({
|
|
|
13182
13470
|
/** Ms-epoch the server scheduled its self-restart. */
|
|
13183
13471
|
restartingAt: number()
|
|
13184
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
|
+
});
|
|
13185
13509
|
const FrameworkPackageStatusSchema = object({
|
|
13186
13510
|
packageName: string(),
|
|
13187
13511
|
currentVersion: string(),
|
|
@@ -13319,7 +13643,7 @@ const CustomActionInputSchema = object({
|
|
|
13319
13643
|
getLastRestart: method(
|
|
13320
13644
|
_void(),
|
|
13321
13645
|
object({
|
|
13322
|
-
kind: _enum(["framework-update", "manual", "system"]),
|
|
13646
|
+
kind: _enum(["framework-update", "manual", "system", "framework-bulk-update"]),
|
|
13323
13647
|
packageName: string().optional(),
|
|
13324
13648
|
fromVersion: string().optional(),
|
|
13325
13649
|
toVersion: string().optional(),
|
|
@@ -13409,11 +13733,70 @@ const CustomActionInputSchema = object({
|
|
|
13409
13733
|
updateFrameworkPackage: method(
|
|
13410
13734
|
object({
|
|
13411
13735
|
packageName: string().min(1),
|
|
13412
|
-
version: string().optional()
|
|
13736
|
+
version: string().optional(),
|
|
13737
|
+
deferRestart: boolean().optional()
|
|
13413
13738
|
}),
|
|
13414
13739
|
UpdateFrameworkPackageResultSchema,
|
|
13415
13740
|
{ kind: "mutation", auth: "admin" }
|
|
13416
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
|
+
),
|
|
13417
13800
|
getVersions: method(
|
|
13418
13801
|
object({ name: string() }),
|
|
13419
13802
|
array(PackageVersionInfoSchema).readonly()
|
|
@@ -13541,6 +13924,7 @@ var EventCategory = /* @__PURE__ */ ((EventCategory2) => {
|
|
|
13541
13924
|
EventCategory2["StreamBrokerOnCamStreamDemand"] = "stream-broker.onCamStreamDemand";
|
|
13542
13925
|
EventCategory2["StreamBrokerOnCamStreamIdle"] = "stream-broker.onCamStreamIdle";
|
|
13543
13926
|
EventCategory2["StreamBrokerOnRequestStreamSourceRefresh"] = "stream-broker.onRequestStreamSourceRefresh";
|
|
13927
|
+
EventCategory2["StreamParamsChanged"] = "stream-params.changed";
|
|
13544
13928
|
EventCategory2["DeviceStateChanged"] = "device.state-changed";
|
|
13545
13929
|
EventCategory2["BatteryOnStatusChanged"] = "battery.onStatusChanged";
|
|
13546
13930
|
EventCategory2["DoorbellOnPressed"] = "doorbell.onPressed";
|
|
@@ -13588,6 +13972,7 @@ var EventCategory = /* @__PURE__ */ ((EventCategory2) => {
|
|
|
13588
13972
|
EventCategory2["NetworkTunnelStarted"] = "network.tunnel.started";
|
|
13589
13973
|
EventCategory2["NetworkTunnelStopped"] = "network.tunnel.stopped";
|
|
13590
13974
|
EventCategory2["LocalNetworkChanged"] = "network.local.changed";
|
|
13975
|
+
EventCategory2["MeshNetworkChanged"] = "network.mesh.changed";
|
|
13591
13976
|
EventCategory2["BackupCompleted"] = "backup.completed";
|
|
13592
13977
|
EventCategory2["BackupRestored"] = "backup.restored";
|
|
13593
13978
|
EventCategory2["NotificationDispatched"] = "notification.dispatched";
|
|
@@ -13598,6 +13983,7 @@ var EventCategory = /* @__PURE__ */ ((EventCategory2) => {
|
|
|
13598
13983
|
EventCategory2["DeviceAwake"] = "device.awake";
|
|
13599
13984
|
EventCategory2["DeviceSleeping"] = "device.sleeping";
|
|
13600
13985
|
EventCategory2["RetentionCleanup"] = "retention.cleanup";
|
|
13986
|
+
EventCategory2["AddonsBulkUpdateProgress"] = "addons.bulk-update-progress";
|
|
13601
13987
|
return EventCategory2;
|
|
13602
13988
|
})(EventCategory || {});
|
|
13603
13989
|
function isEvent(event2, category) {
|
|
@@ -13750,7 +14136,7 @@ class BaseAddon {
|
|
|
13750
14136
|
}
|
|
13751
14137
|
// ── Settings schemas (override to provide UI) ─────────────────────────
|
|
13752
14138
|
/** Override to provide global-level settings UI schema. */
|
|
13753
|
-
globalSettingsSchema() {
|
|
14139
|
+
globalSettingsSchema(_cap) {
|
|
13754
14140
|
return null;
|
|
13755
14141
|
}
|
|
13756
14142
|
/** Override to provide device-level settings UI schema. */
|
|
@@ -13764,8 +14150,8 @@ class BaseAddon {
|
|
|
13764
14150
|
// blob and every addon used exactly one of them; the distinction was
|
|
13765
14151
|
// never semantically load-bearing. `global` won because it was the
|
|
13766
14152
|
// widely-used one and the name reads naturally (per-node addon config).
|
|
13767
|
-
async getGlobalSettings(overlay) {
|
|
13768
|
-
const schema = this.globalSettingsSchema();
|
|
14153
|
+
async getGlobalSettings(overlay, cap) {
|
|
14154
|
+
const schema = this.globalSettingsSchema(cap);
|
|
13769
14155
|
if (!schema) return { sections: [] };
|
|
13770
14156
|
const raw = await this._ctx?.settings?.readAddonStore() ?? {};
|
|
13771
14157
|
return hydrateSchema(schema, overlay ? { ...raw, ...overlay } : raw);
|
|
@@ -14184,6 +14570,14 @@ class ReadinessRegistry {
|
|
|
14184
14570
|
cleanup();
|
|
14185
14571
|
reject(new ReadinessTimeoutError(capName, scope, this.now() - start));
|
|
14186
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();
|
|
14187
14581
|
const onAbort = () => {
|
|
14188
14582
|
if (settled) return;
|
|
14189
14583
|
settled = true;
|
|
@@ -14194,6 +14588,7 @@ class ReadinessRegistry {
|
|
|
14194
14588
|
function cleanup() {
|
|
14195
14589
|
unsubscribe();
|
|
14196
14590
|
if (timer !== null) clearTimeout(timer);
|
|
14591
|
+
clearInterval(pendingLogTimer);
|
|
14197
14592
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
14198
14593
|
}
|
|
14199
14594
|
});
|
|
@@ -14747,6 +15142,8 @@ function balanceAudio(input) {
|
|
|
14747
15142
|
}
|
|
14748
15143
|
const POLL_INTERVAL_MS = 200;
|
|
14749
15144
|
const PULL_MAX_COUNT = 8;
|
|
15145
|
+
const RESUBSCRIBE_AFTER_FAILURES = 2;
|
|
15146
|
+
const RESUBSCRIBE_THROTTLE_TICKS = 5;
|
|
14750
15147
|
async function startAudioChunkPoller(options) {
|
|
14751
15148
|
const { api, brokerId, tag, onChunk, logger } = options;
|
|
14752
15149
|
let subscriptionId;
|
|
@@ -14764,6 +15161,19 @@ async function startAudioChunkPoller(options) {
|
|
|
14764
15161
|
}
|
|
14765
15162
|
let stopped = false;
|
|
14766
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
|
+
};
|
|
14767
15177
|
const tick = async () => {
|
|
14768
15178
|
if (stopped) return;
|
|
14769
15179
|
try {
|
|
@@ -14771,14 +15181,24 @@ async function startAudioChunkPoller(options) {
|
|
|
14771
15181
|
subscriptionId,
|
|
14772
15182
|
maxCount: PULL_MAX_COUNT
|
|
14773
15183
|
});
|
|
15184
|
+
if (consecutiveFailures > 0) {
|
|
15185
|
+
logger.info("audio-chunk poller: stream recovered", { meta: { brokerId, subscriptionId } });
|
|
15186
|
+
}
|
|
15187
|
+
consecutiveFailures = 0;
|
|
14774
15188
|
for (const chunk of chunks) {
|
|
14775
15189
|
if (stopped) break;
|
|
14776
15190
|
await onChunk(chunk);
|
|
14777
15191
|
}
|
|
14778
15192
|
} catch (err) {
|
|
14779
|
-
|
|
14780
|
-
|
|
14781
|
-
|
|
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
|
+
}
|
|
14782
15202
|
}
|
|
14783
15203
|
if (!stopped) {
|
|
14784
15204
|
timer = setTimeout(() => void tick(), POLL_INTERVAL_MS);
|
|
@@ -14799,6 +15219,18 @@ async function startAudioChunkPoller(options) {
|
|
|
14799
15219
|
});
|
|
14800
15220
|
};
|
|
14801
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
|
+
}
|
|
14802
15234
|
const PHASE_MODE_VALUES = /* @__PURE__ */ new Set([
|
|
14803
15235
|
"disabled",
|
|
14804
15236
|
"always-on",
|
|
@@ -14810,6 +15242,7 @@ function isPipelinePhaseMode(v) {
|
|
|
14810
15242
|
const PREFERRED_AGENT_SETTING = "preferredAgent";
|
|
14811
15243
|
const AUDIO_NODE_SETTING = "audioNodeId";
|
|
14812
15244
|
const DEFAULT_BROKER_CALL_TIMEOUT_MS = 5e3;
|
|
15245
|
+
const RECONCILE_DEBOUNCE_MS = 200;
|
|
14813
15246
|
const AGENT_SETTINGS_KEY = "agentSettings";
|
|
14814
15247
|
const CAMERA_SETTINGS_KEY = "cameraSettings";
|
|
14815
15248
|
const TEMPLATES_KEY = "templates";
|
|
@@ -14961,6 +15394,12 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
14961
15394
|
loadShedState = /* @__PURE__ */ new Map();
|
|
14962
15395
|
/** Timer for the auto-resume sweep. */
|
|
14963
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;
|
|
14964
15403
|
initTimestamp = 0;
|
|
14965
15404
|
constructor() {
|
|
14966
15405
|
super({});
|
|
@@ -15019,6 +15458,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15019
15458
|
meta: { error: msg }
|
|
15020
15459
|
});
|
|
15021
15460
|
});
|
|
15461
|
+
this.scheduleReconcile();
|
|
15022
15462
|
}
|
|
15023
15463
|
);
|
|
15024
15464
|
this.watchCapability(["pipeline-executor", "analysis-pipeline"], {
|
|
@@ -15116,6 +15556,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15116
15556
|
});
|
|
15117
15557
|
}, PROFILE_SLOTS_DEBOUNCE_MS)
|
|
15118
15558
|
);
|
|
15559
|
+
this.scheduleReconcile();
|
|
15119
15560
|
}
|
|
15120
15561
|
);
|
|
15121
15562
|
this.unsubDeviceUnregistered = this.ctx.eventBus.subscribe(
|
|
@@ -15295,9 +15736,15 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15295
15736
|
}
|
|
15296
15737
|
]
|
|
15297
15738
|
};
|
|
15739
|
+
this.scheduleReconcile();
|
|
15298
15740
|
return {
|
|
15299
15741
|
providers: [
|
|
15300
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 },
|
|
15301
15748
|
{ capability: zonesCapability, provider: this.zonesProvider },
|
|
15302
15749
|
{ capability: zoneRulesCapability, provider: this.zoneRulesProvider },
|
|
15303
15750
|
{ capability: addonWidgetsSourceCapability, provider: widgetsProvider }
|
|
@@ -15422,6 +15869,10 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15422
15869
|
this.profileSlotTimers.clear();
|
|
15423
15870
|
this.profileSlotTimers = null;
|
|
15424
15871
|
}
|
|
15872
|
+
if (this.reconcileTimer !== null) {
|
|
15873
|
+
clearTimeout(this.reconcileTimer);
|
|
15874
|
+
this.reconcileTimer = null;
|
|
15875
|
+
}
|
|
15425
15876
|
this.unsubDeviceRegistered?.();
|
|
15426
15877
|
this.unsubDeviceRegistered = null;
|
|
15427
15878
|
this.unsubDeviceUnregistered?.();
|
|
@@ -15497,7 +15948,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15497
15948
|
);
|
|
15498
15949
|
this.cameraConfigs.set(runnerConfig.deviceId, runnerConfig);
|
|
15499
15950
|
const cfg = this.globalSettings ?? {};
|
|
15500
|
-
const minThreshold = typeof cfg.fpsMinThreshold === "number" ? cfg.fpsMinThreshold :
|
|
15951
|
+
const minThreshold = typeof cfg.fpsMinThreshold === "number" ? cfg.fpsMinThreshold : PipelineOrchestratorAddon.DEFAULT_FPS_MIN_THRESHOLD;
|
|
15501
15952
|
const windowMs = typeof cfg.fpsLowWindowMs === "number" && cfg.fpsLowWindowMs > 0 ? cfg.fpsLowWindowMs : PipelineOrchestratorAddon.DEFAULT_FPS_LOW_WINDOW_MS;
|
|
15502
15953
|
if (minThreshold > 0 && this.initTimestamp && Date.now() - this.initTimestamp > 3e4) {
|
|
15503
15954
|
const now = Date.now();
|
|
@@ -15940,7 +16391,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15940
16391
|
const api = ctx?.api;
|
|
15941
16392
|
if (!api) continue;
|
|
15942
16393
|
try {
|
|
15943
|
-
const load = await
|
|
16394
|
+
const load = await this.queryLocalLoadBounded(nodeId);
|
|
15944
16395
|
loads.push(load);
|
|
15945
16396
|
} catch (err) {
|
|
15946
16397
|
const msg = errMsg(err);
|
|
@@ -15965,6 +16416,53 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
15965
16416
|
}
|
|
15966
16417
|
this.cachedAgentLoad = next;
|
|
15967
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
|
+
}
|
|
15968
16466
|
async readPreferredAgent(deviceId) {
|
|
15969
16467
|
const ctx = this.ctx;
|
|
15970
16468
|
if (!ctx?.settings) return null;
|
|
@@ -16262,6 +16760,44 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
16262
16760
|
"audio-analyzer ready — node now available for audio dispatch",
|
|
16263
16761
|
{ tags: { nodeId }, meta: { epoch: t.epoch } }
|
|
16264
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
|
+
}
|
|
16265
16801
|
}
|
|
16266
16802
|
/**
|
|
16267
16803
|
* Act on a `detection-pipeline` transition: on `ready` with a new
|
|
@@ -16333,6 +16869,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
16333
16869
|
});
|
|
16334
16870
|
});
|
|
16335
16871
|
}, 3e3);
|
|
16872
|
+
this.scheduleReconcile();
|
|
16336
16873
|
} catch (err) {
|
|
16337
16874
|
this.ctx.logger.debug("readiness seed+redispatch failed", {
|
|
16338
16875
|
tags: { nodeId },
|
|
@@ -16377,6 +16914,75 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
16377
16914
|
}
|
|
16378
16915
|
}
|
|
16379
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
|
+
}
|
|
16380
16986
|
// ── Capability bindings (cap methods) ────────────────────────────────
|
|
16381
16987
|
async getCapabilityBindings(input) {
|
|
16382
16988
|
const ctx = this.ctx;
|
|
@@ -17158,11 +17764,11 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17158
17764
|
key: "fpsMinThreshold",
|
|
17159
17765
|
type: "slider",
|
|
17160
17766
|
label: "Min FPS Threshold",
|
|
17161
|
-
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.",
|
|
17162
17768
|
min: 0,
|
|
17163
17769
|
max: 10,
|
|
17164
17770
|
step: 1,
|
|
17165
|
-
default:
|
|
17771
|
+
default: PipelineOrchestratorAddon.DEFAULT_FPS_MIN_THRESHOLD,
|
|
17166
17772
|
showValue: true,
|
|
17167
17773
|
unit: "fps"
|
|
17168
17774
|
},
|
|
@@ -17459,8 +18065,41 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17459
18065
|
}
|
|
17460
18066
|
]
|
|
17461
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
|
+
}
|
|
17462
18101
|
return {
|
|
17463
|
-
sections: [...baseSections, zonesSection]
|
|
18102
|
+
sections: [...baseSections, zonesSection, ...nativeObjectDetectionSections]
|
|
17464
18103
|
};
|
|
17465
18104
|
}
|
|
17466
18105
|
// `getCameraPipelineWithFallback` was a thin wrapper over
|
|
@@ -17521,11 +18160,25 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17521
18160
|
}
|
|
17522
18161
|
async applyDeviceSettingsPatch(input) {
|
|
17523
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
|
+
}
|
|
17524
18177
|
if (Object.keys(pipelinePatch).length > 0) {
|
|
17525
18178
|
await this.applyPipelinePatch(input.deviceId, pipelinePatch);
|
|
17526
18179
|
}
|
|
17527
|
-
if (Object.keys(
|
|
17528
|
-
await this.writeDeviceOrchestratorSettings(input.deviceId,
|
|
18180
|
+
if (Object.keys(orchestratorRest).length > 0) {
|
|
18181
|
+
await this.writeDeviceOrchestratorSettings(input.deviceId, orchestratorRest);
|
|
17529
18182
|
}
|
|
17530
18183
|
return { success: true };
|
|
17531
18184
|
}
|
|
@@ -17700,6 +18353,23 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17700
18353
|
return false;
|
|
17701
18354
|
}
|
|
17702
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
|
+
}
|
|
17703
18373
|
async resolveDeviceDetectionSettings(deviceId) {
|
|
17704
18374
|
const ctx = this.ctx;
|
|
17705
18375
|
if (!ctx?.settings) {
|
|
@@ -17764,6 +18434,8 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17764
18434
|
const detectionMode = typeof userDetectionMode === "string" && isPipelinePhaseMode(userDetectionMode) ? userDetectionMode : profile?.defaults.detectionMode ?? "on-motion";
|
|
17765
18435
|
const userAudioMode = raw["audioMode"];
|
|
17766
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;
|
|
17767
18439
|
return {
|
|
17768
18440
|
motionDetectionEnabled,
|
|
17769
18441
|
pipelineEnabled,
|
|
@@ -17774,7 +18446,8 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17774
18446
|
detectionFps: mustNumber("detectionFps"),
|
|
17775
18447
|
motionCooldownMs: mustNumber("motionCooldownMs"),
|
|
17776
18448
|
detectionMode,
|
|
17777
|
-
audioMode
|
|
18449
|
+
audioMode,
|
|
18450
|
+
onboardMotionDrivesAnalyzer
|
|
17778
18451
|
};
|
|
17779
18452
|
} catch (err) {
|
|
17780
18453
|
const msg = errMsg(err);
|
|
@@ -17882,7 +18555,8 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17882
18555
|
motionCooldownMs: resolved.motionCooldownMs,
|
|
17883
18556
|
audioStreamId: void 0,
|
|
17884
18557
|
detectionMode: resolved.detectionMode,
|
|
17885
|
-
audioMode: resolved.audioMode
|
|
18558
|
+
audioMode: resolved.audioMode,
|
|
18559
|
+
onboardMotionDrivesAnalyzer: resolved.onboardMotionDrivesAnalyzer
|
|
17886
18560
|
};
|
|
17887
18561
|
}
|
|
17888
18562
|
/**
|
|
@@ -17917,7 +18591,8 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
17917
18591
|
audio: pipelineConfig.audio ?? null,
|
|
17918
18592
|
zones,
|
|
17919
18593
|
detectionMode: config2.detectionMode,
|
|
17920
|
-
audioMode: config2.audioMode
|
|
18594
|
+
audioMode: config2.audioMode,
|
|
18595
|
+
onboardMotionDrivesAnalyzer: config2.onboardMotionDrivesAnalyzer
|
|
17921
18596
|
};
|
|
17922
18597
|
let dispatchedNodeId = null;
|
|
17923
18598
|
try {
|
|
@@ -18102,6 +18777,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18102
18777
|
if (a.detectionFps !== b.detectionFps) return false;
|
|
18103
18778
|
if (a.motionCooldownMs !== b.motionCooldownMs) return false;
|
|
18104
18779
|
if (a.audioStreamId !== b.audioStreamId) return false;
|
|
18780
|
+
if (a.onboardMotionDrivesAnalyzer !== b.onboardMotionDrivesAnalyzer) return false;
|
|
18105
18781
|
if (a.motionSources.length !== b.motionSources.length) return false;
|
|
18106
18782
|
for (const s of a.motionSources) if (!b.motionSources.includes(s)) return false;
|
|
18107
18783
|
return true;
|
|
@@ -18191,9 +18867,20 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18191
18867
|
* across recoveries.
|
|
18192
18868
|
*/
|
|
18193
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;
|
|
18194
18881
|
enforceLoadManagement(deviceId, metrics) {
|
|
18195
18882
|
const cfg = this.globalSettings ?? {};
|
|
18196
|
-
const threshold = typeof cfg.fpsMinThreshold === "number" ? cfg.fpsMinThreshold :
|
|
18883
|
+
const threshold = typeof cfg.fpsMinThreshold === "number" ? cfg.fpsMinThreshold : PipelineOrchestratorAddon.DEFAULT_FPS_MIN_THRESHOLD;
|
|
18197
18884
|
const windowMs = typeof cfg.fpsLowWindowMs === "number" && cfg.fpsLowWindowMs > 0 ? cfg.fpsLowWindowMs : PipelineOrchestratorAddon.DEFAULT_FPS_LOW_WINDOW_MS;
|
|
18198
18885
|
if (threshold <= 0) return;
|
|
18199
18886
|
if (metrics.phase !== "active") return;
|
|
@@ -18313,7 +19000,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18313
19000
|
async handleInferenceResult(payload) {
|
|
18314
19001
|
const ctx = this.ctx;
|
|
18315
19002
|
if (!ctx) return;
|
|
18316
|
-
const { deviceId, frame, frameHandle } = payload;
|
|
19003
|
+
const { deviceId, frame, frameHandle, capturedAt } = payload;
|
|
18317
19004
|
if (frame.detections.length === 0) return;
|
|
18318
19005
|
this.ctx.eventBus.emit({
|
|
18319
19006
|
id: `detection-${deviceId}-${Date.now()}`,
|
|
@@ -18321,8 +19008,10 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18321
19008
|
source: { type: "device", id: deviceId, nodeId: "hub", addonId: "pipeline-orchestrator", deviceId },
|
|
18322
19009
|
timestamp: /* @__PURE__ */ new Date(),
|
|
18323
19010
|
// Forward the upstream shm-ring `frameHandle` so post-analysis
|
|
18324
|
-
// consumers (Task 8) can resolve the original frame zero-copy
|
|
18325
|
-
|
|
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 } : {} }
|
|
18326
19015
|
});
|
|
18327
19016
|
}
|
|
18328
19017
|
/**
|
|
@@ -18448,8 +19137,8 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18448
19137
|
}
|
|
18449
19138
|
try {
|
|
18450
19139
|
const byteLength = chunk.data.byteLength;
|
|
18451
|
-
const
|
|
18452
|
-
|
|
19140
|
+
const data = new Uint8Array(byteLength);
|
|
19141
|
+
data.set(
|
|
18453
19142
|
new Uint8Array(
|
|
18454
19143
|
chunk.data.buffer,
|
|
18455
19144
|
chunk.data.byteOffset,
|
|
@@ -18457,7 +19146,7 @@ class PipelineOrchestratorAddon extends BaseAddon {
|
|
|
18457
19146
|
)
|
|
18458
19147
|
);
|
|
18459
19148
|
const audioChunkInput = {
|
|
18460
|
-
data
|
|
19149
|
+
data,
|
|
18461
19150
|
sampleRate: chunk.sampleRate,
|
|
18462
19151
|
channels: chunk.channels,
|
|
18463
19152
|
timestamp: chunk.timestamp,
|