@camstack/system 1.2.263 → 1.2.264
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/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/camera-grid/addon.d.ts +20 -0
- package/dist/builtins/camera-grid/grid-activity-mirror.d.ts +32 -0
- package/dist/builtins/camera-grid/grid-activity.d.ts +50 -0
- package/dist/builtins/camera-grid/grid-camera-device.d.ts +11 -1
- package/dist/builtins/camera-grid/grid-canvas.d.ts +83 -0
- package/dist/builtins/camera-grid/grid-compositor.d.ts +36 -0
- package/dist/builtins/camera-grid/grid-detection-mapping.d.ts +31 -0
- package/dist/builtins/camera-grid/grid-tile-feed.d.ts +38 -0
- package/dist/builtins/camera-grid/grid-tile-plan.d.ts +32 -0
- package/dist/builtins/camera-grid/index.js +416 -7
- package/dist/builtins/camera-grid/index.mjs +416 -7
- package/dist/builtins/camera-grid/silence-analysis.d.ts +7 -0
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-6hiXbrNY.js → dist-BhYweLVr.js} +74 -15
- package/dist/{dist-CQ7ItBIG.mjs → dist-CO2O_ddo.mjs} +61 -8
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{retired-settings-keys-CCXmaFTK.mjs → retired-settings-keys-BnnHWyfj.mjs} +1 -1
- package/dist/{retired-settings-keys-AmmGrCp1.js → retired-settings-keys-CKLI8VmE.js} +1 -1
- package/package.json +4 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $t as streamCatalogCapability, At as motionCapability, Jt as snapshotCapability, Pn as EventCategory, U as cameraGridLayoutCapability, Y as declarationOwnerNodeId, an as errMsg, b as MOTION_CLOSE_AFTER_MS, bn as createEvent, dn as DeviceFeature, f as DERIVED_CAMERA_SILENCED_CAP_NAMES, i as BaseDevice, on as buildFfmpegArgs, p as DeclaredDevices, pn as DeviceType, sn as BaseAddon, wn as hydrateSchema } from "../../dist-CO2O_ddo.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { FfmpegProcess } from "@camstack/types/node";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -192,6 +192,10 @@ var GridCameraDevice = class extends BaseDevice {
|
|
|
192
192
|
getSnapshot: async ({ deviceId }) => activeRuntime().getSnapshot(deviceId),
|
|
193
193
|
invalidateCache: async () => {}
|
|
194
194
|
});
|
|
195
|
+
this.ctx.registerNativeCap(motionCapability, {
|
|
196
|
+
getStatus: async ({ deviceId }) => activeRuntime().motionStatus(deviceId),
|
|
197
|
+
isDetected: async ({ deviceId }) => activeRuntime().motionStatus(deviceId).detected
|
|
198
|
+
});
|
|
195
199
|
this.markOnline(true);
|
|
196
200
|
}
|
|
197
201
|
catalog() {
|
|
@@ -395,6 +399,330 @@ function normalizeGridRows(raw, mintId) {
|
|
|
395
399
|
};
|
|
396
400
|
}
|
|
397
401
|
//#endregion
|
|
402
|
+
//#region src/builtins/camera-grid/grid-activity-mirror.ts
|
|
403
|
+
/**
|
|
404
|
+
* What a grid says about ITSELF, kept from what its sources said.
|
|
405
|
+
*
|
|
406
|
+
* ## Why the grid answers at all
|
|
407
|
+
*
|
|
408
|
+
* A grid is a camera, and the viewer already knows how to light a camera: the
|
|
409
|
+
* motion ring keys off `motion.on-motion-changed` filtered by `deviceId`, the
|
|
410
|
+
* audio one off `pipeline.audio-inference-result`. So the grid does not need a
|
|
411
|
+
* new channel, a new cap or a line of app code — it needs to answer those
|
|
412
|
+
* questions about its own device id. The operator set both rules: motion is on
|
|
413
|
+
* if AT LEAST ONE source has it, and the audio level is the LOUDEST of them.
|
|
414
|
+
*
|
|
415
|
+
* It is derived from the SOURCES' events and never from the composed picture.
|
|
416
|
+
* Analysing the composite would be a second motion authority for cameras that
|
|
417
|
+
* already have one, and a decode this addon does not pay today.
|
|
418
|
+
*
|
|
419
|
+
* ## The falling edge is the whole difficulty
|
|
420
|
+
*
|
|
421
|
+
* Bus events are telemetry and may be dropped (D8/D11). A lost `detected:
|
|
422
|
+
* false` would leave a grid lit for ever, and the grid has no phase machine to
|
|
423
|
+
* produce a falling edge of its own — a real camera's comes from the pipeline
|
|
424
|
+
* runner, which a grid deliberately does not run.
|
|
425
|
+
*
|
|
426
|
+
* So a source's motion is remembered WITH ITS TIME and expires on its own. That
|
|
427
|
+
* is D49's shape: in-memory state refreshed off the event path, never a gate
|
|
428
|
+
* that re-reads something fallible in order to decide. A dropped clear costs a
|
|
429
|
+
* ring that stays lit until the expiry instead of for ever, and a subject that
|
|
430
|
+
* is genuinely still there keeps renewing it.
|
|
431
|
+
*
|
|
432
|
+
* The expiry is `MOTION_CLOSE_AFTER_MS`, imported and not re-spelled: it is the
|
|
433
|
+
* system's one answer to "how long after the last push is a camera still
|
|
434
|
+
* moving", and a second number here would be a second authority disagreeing
|
|
435
|
+
* with the first on exactly the cameras that make up the grid.
|
|
436
|
+
*/
|
|
437
|
+
/**
|
|
438
|
+
* How long an audio reading counts as current.
|
|
439
|
+
*
|
|
440
|
+
* The analyzer emits one per window per camera, so a source that has gone quiet
|
|
441
|
+
* simply stops arriving — there is no "silence" event to wait for. Without a
|
|
442
|
+
* window the loudest reading of the day would be the grid's level for ever.
|
|
443
|
+
* Chosen to span several analysis windows so an ordinary gap does not blink the
|
|
444
|
+
* level away, and to be far shorter than a person notices a stale ring.
|
|
445
|
+
*/
|
|
446
|
+
var AUDIO_FRESH_MS = 5e3;
|
|
447
|
+
var GridActivityMirror = class {
|
|
448
|
+
motion = /* @__PURE__ */ new Map();
|
|
449
|
+
audio = /* @__PURE__ */ new Map();
|
|
450
|
+
lastDetectedAt = null;
|
|
451
|
+
/** One source's motion push. `detected: false` is as important as `true`. */
|
|
452
|
+
noteMotion(sourceDeviceId, detected, atMs) {
|
|
453
|
+
this.motion.set(sourceDeviceId, {
|
|
454
|
+
detected,
|
|
455
|
+
atMs
|
|
456
|
+
});
|
|
457
|
+
if (detected) this.lastDetectedAt = Math.max(this.lastDetectedAt ?? 0, atMs);
|
|
458
|
+
}
|
|
459
|
+
/** One source's audio window. `dbfs` is the level, on the analyzer's scale. */
|
|
460
|
+
noteAudio(sourceDeviceId, dbfs, atMs) {
|
|
461
|
+
this.audio.set(sourceDeviceId, {
|
|
462
|
+
dbfs,
|
|
463
|
+
atMs
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Drop every source this grid no longer composes.
|
|
468
|
+
*
|
|
469
|
+
* A cell removed while its camera was moving would otherwise hold the grid
|
|
470
|
+
* lit until the expiry, for a picture the source is not even in.
|
|
471
|
+
*/
|
|
472
|
+
keepOnly(sourceDeviceIds) {
|
|
473
|
+
const keep = new Set(sourceDeviceIds);
|
|
474
|
+
const motionIds = Array.from(this.motion.keys());
|
|
475
|
+
const audioIds = Array.from(this.audio.keys());
|
|
476
|
+
for (const id of motionIds) if (!keep.has(id)) this.motion.delete(id);
|
|
477
|
+
for (const id of audioIds) if (!keep.has(id)) this.audio.delete(id);
|
|
478
|
+
}
|
|
479
|
+
/** At least one source moving, as of `nowMs`. */
|
|
480
|
+
motionAt(nowMs) {
|
|
481
|
+
let detected = false;
|
|
482
|
+
for (const state of this.motion.values()) {
|
|
483
|
+
if (!state.detected) continue;
|
|
484
|
+
if (nowMs - state.atMs > 15e3) continue;
|
|
485
|
+
detected = true;
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
detected,
|
|
490
|
+
lastDetectedAt: this.lastDetectedAt
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* The loudest CURRENT reading, or `null` when nothing fresh has been heard.
|
|
495
|
+
*
|
|
496
|
+
* Null and not `-Infinity`: "nobody has reported a level" is a different
|
|
497
|
+
* answer from "it is silent", and a number here would have the viewer draw a
|
|
498
|
+
* level for a grid whose sources are not being analysed at all.
|
|
499
|
+
*/
|
|
500
|
+
loudestAt(nowMs) {
|
|
501
|
+
let loudest = null;
|
|
502
|
+
for (const state of this.audio.values()) {
|
|
503
|
+
if (nowMs - state.atMs > AUDIO_FRESH_MS) continue;
|
|
504
|
+
if (loudest === null || state.dbfs > loudest) loudest = state.dbfs;
|
|
505
|
+
}
|
|
506
|
+
return loudest;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
//#endregion
|
|
510
|
+
//#region src/builtins/camera-grid/grid-detection-mapping.ts
|
|
511
|
+
/**
|
|
512
|
+
* Where this detection lands on the canvas — once per cell that shows it.
|
|
513
|
+
*
|
|
514
|
+
* Empty is a real answer: the subject is in a part of the source this grid does
|
|
515
|
+
* not display, or the frame arrived with no size to measure against. Neither is
|
|
516
|
+
* a reason to guess.
|
|
517
|
+
*/
|
|
518
|
+
function mapDetectionToCanvas(input) {
|
|
519
|
+
const { width: frameW, height: frameH } = input.sourceFrame;
|
|
520
|
+
if (frameW <= 0 || frameH <= 0) return [];
|
|
521
|
+
const left = input.bbox.x / frameW;
|
|
522
|
+
const top = input.bbox.y / frameH;
|
|
523
|
+
const right = (input.bbox.x + input.bbox.width) / frameW;
|
|
524
|
+
const bottom = (input.bbox.y + input.bbox.height) / frameH;
|
|
525
|
+
const out = [];
|
|
526
|
+
for (const { source, cell } of input.cells) {
|
|
527
|
+
if (source.width <= 0 || source.height <= 0) continue;
|
|
528
|
+
const l = Math.max(left, source.x);
|
|
529
|
+
const t = Math.max(top, source.y);
|
|
530
|
+
const r = Math.min(right, source.x + source.width);
|
|
531
|
+
const b = Math.min(bottom, source.y + source.height);
|
|
532
|
+
if (r <= l || b <= t) continue;
|
|
533
|
+
const cx = (l - source.x) / source.width;
|
|
534
|
+
const cy = (t - source.y) / source.height;
|
|
535
|
+
const cw = (r - l) / source.width;
|
|
536
|
+
const ch = (b - t) / source.height;
|
|
537
|
+
out.push({
|
|
538
|
+
x: (cell.x + cx * cell.width) * input.canvas.width,
|
|
539
|
+
y: (cell.y + cy * cell.height) * input.canvas.height,
|
|
540
|
+
width: cw * cell.width * input.canvas.width,
|
|
541
|
+
height: ch * cell.height * input.canvas.height
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
return out;
|
|
545
|
+
}
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/builtins/camera-grid/grid-activity.ts
|
|
548
|
+
/**
|
|
549
|
+
* The grid's own motion / audio / detection, forwarded from its sources.
|
|
550
|
+
*
|
|
551
|
+
* ## Why the grid speaks a camera's language instead of a new one
|
|
552
|
+
*
|
|
553
|
+
* The operator asked for the grid's tile to light up in the viewer exactly as a
|
|
554
|
+
* camera's card does, with NO app change. The app already knows how: it filters
|
|
555
|
+
* `motion.on-motion-changed` by `deviceId` for the motion ring,
|
|
556
|
+
* `pipeline.audio-inference-result` for the audio one, and `detection.result`
|
|
557
|
+
* for the detection one. So the grid answers those three questions about its
|
|
558
|
+
* own device id, in the same shapes. Nothing new is invented, and nothing in
|
|
559
|
+
* the viewer is touched.
|
|
560
|
+
*
|
|
561
|
+
* The rules are the operator's: motion if AT LEAST ONE source has it, and the
|
|
562
|
+
* LOUDEST source's level for audio.
|
|
563
|
+
*
|
|
564
|
+
* ## What speaking a camera's language costs, and why it is safe here
|
|
565
|
+
*
|
|
566
|
+
* Those categories have other listeners, and emitting on them makes the grid a
|
|
567
|
+
* motion source for the whole system. Checked, one by one, before shipping:
|
|
568
|
+
* the recorder's event capture, the HomeKit and Alexa exports and the Home
|
|
569
|
+
* Assistant export are all per-device and do nothing until an operator enables
|
|
570
|
+
* them for this camera — and if an operator DOES export a grid to HomeKit, a
|
|
571
|
+
* grid reporting motion is correct, not a surprise.
|
|
572
|
+
*
|
|
573
|
+
* The one listener with no gate of its own was `pipeline-analytics`, which
|
|
574
|
+
* would have opened a motion EPISODE per grid, duplicating the rows its sources
|
|
575
|
+
* already write. Its gate turned out to be the `pipeline-analytics` WRAPPER
|
|
576
|
+
* BINDING, so the answer is the one the operator gave: a derived camera is
|
|
577
|
+
* created with the analytics wrappers OFF (`grid-analytics-defaults.ts`).
|
|
578
|
+
* That is a binding, not a new flag, so an operator who wants a grid analysed
|
|
579
|
+
* can simply turn it back on.
|
|
580
|
+
*
|
|
581
|
+
* ## Detection carries boxes whether or not anyone wanted them
|
|
582
|
+
*
|
|
583
|
+
* `detection.result` is the same event the viewer draws BOXES from, so a grid
|
|
584
|
+
* that reports detections reports boxes. Sending the source's own pixel box
|
|
585
|
+
* would draw it against the composed canvas at confidently wrong coordinates,
|
|
586
|
+
* so every box is carried through the cell geometry first
|
|
587
|
+
* (`grid-detection-mapping.ts`) and lands where the subject actually is.
|
|
588
|
+
*/
|
|
589
|
+
/**
|
|
590
|
+
* One mirror per grid, and the emission that follows an update.
|
|
591
|
+
*
|
|
592
|
+
* Deliberately NOT one mirror per source: the question is always "is THIS grid
|
|
593
|
+
* lit", and a source shared by two grids answers it twice, differently, because
|
|
594
|
+
* the other cells differ.
|
|
595
|
+
*/
|
|
596
|
+
var GridActivityForwarder = class {
|
|
597
|
+
deps;
|
|
598
|
+
mirrors = /* @__PURE__ */ new Map();
|
|
599
|
+
constructor(deps) {
|
|
600
|
+
this.deps = deps;
|
|
601
|
+
}
|
|
602
|
+
/** A source camera's motion push. */
|
|
603
|
+
onSourceMotion(sourceDeviceId, detected, atMs) {
|
|
604
|
+
for (const grid of this.deps.gridsForSource(sourceDeviceId)) {
|
|
605
|
+
const mirror = this.mirrorFor(grid);
|
|
606
|
+
const before = mirror.motionAt(atMs).detected;
|
|
607
|
+
mirror.noteMotion(sourceDeviceId, detected, atMs);
|
|
608
|
+
const after = mirror.motionAt(atMs);
|
|
609
|
+
if (after.detected === before) continue;
|
|
610
|
+
this.emitMotion(grid.deviceId, after, atMs);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/** A source camera's audio window. */
|
|
614
|
+
onSourceAudio(sourceDeviceId, dbfs, atMs) {
|
|
615
|
+
for (const grid of this.deps.gridsForSource(sourceDeviceId)) {
|
|
616
|
+
const mirror = this.mirrorFor(grid);
|
|
617
|
+
mirror.noteAudio(sourceDeviceId, dbfs, atMs);
|
|
618
|
+
const loudest = mirror.loudestAt(atMs);
|
|
619
|
+
if (loudest === null) continue;
|
|
620
|
+
this.emitAudio(grid.deviceId, loudest, atMs);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/** A source camera's detection frame, with its boxes in SOURCE pixels. */
|
|
624
|
+
onSourceDetections(frame) {
|
|
625
|
+
for (const grid of this.deps.gridsForSource(frame.deviceId)) {
|
|
626
|
+
const cells = grid.cellsForSource(frame.deviceId);
|
|
627
|
+
if (cells.length === 0) continue;
|
|
628
|
+
const detections = [];
|
|
629
|
+
for (const detection of frame.detections) for (const bbox of mapDetectionToCanvas({
|
|
630
|
+
bbox: detection.bbox,
|
|
631
|
+
sourceFrame: {
|
|
632
|
+
width: frame.width,
|
|
633
|
+
height: frame.height
|
|
634
|
+
},
|
|
635
|
+
canvas: grid.canvas,
|
|
636
|
+
cells
|
|
637
|
+
})) {
|
|
638
|
+
const { refinedBbox: _refined, mask: _mask, ...rest } = detection;
|
|
639
|
+
detections.push({
|
|
640
|
+
...rest,
|
|
641
|
+
bbox
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
if (detections.length === 0) continue;
|
|
645
|
+
this.emitDetections(grid, frame, detections);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/** The grid's own motion answer, for `motion.getStatus`. */
|
|
649
|
+
motionStatusFor(deviceId, nowMs) {
|
|
650
|
+
return this.mirrors.get(deviceId)?.motionAt(nowMs) ?? {
|
|
651
|
+
detected: false,
|
|
652
|
+
lastDetectedAt: null
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
/** Drop the mirror of a grid that no longer exists. */
|
|
656
|
+
forget(deviceId) {
|
|
657
|
+
this.mirrors.delete(deviceId);
|
|
658
|
+
}
|
|
659
|
+
mirrorFor(grid) {
|
|
660
|
+
let mirror = this.mirrors.get(grid.deviceId);
|
|
661
|
+
if (mirror === void 0) {
|
|
662
|
+
mirror = new GridActivityMirror();
|
|
663
|
+
this.mirrors.set(grid.deviceId, mirror);
|
|
664
|
+
}
|
|
665
|
+
mirror.keepOnly(grid.sourceDeviceIds);
|
|
666
|
+
return mirror;
|
|
667
|
+
}
|
|
668
|
+
source(deviceId) {
|
|
669
|
+
return {
|
|
670
|
+
type: "device",
|
|
671
|
+
id: deviceId,
|
|
672
|
+
addonId: this.deps.addonId,
|
|
673
|
+
deviceId
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
emitMotion(deviceId, state, atMs) {
|
|
677
|
+
const payload = {
|
|
678
|
+
deviceId,
|
|
679
|
+
detected: state.detected,
|
|
680
|
+
timestamp: atMs,
|
|
681
|
+
source: "onboard"
|
|
682
|
+
};
|
|
683
|
+
this.deps.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, this.source(deviceId), payload));
|
|
684
|
+
this.deps.logger.debug("camera grid: motion forwarded", {
|
|
685
|
+
tags: { deviceId },
|
|
686
|
+
meta: { detected: state.detected }
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
emitAudio(deviceId, dbfs, atMs) {
|
|
690
|
+
const payload = {
|
|
691
|
+
deviceId,
|
|
692
|
+
frame: {
|
|
693
|
+
kind: "audio-window",
|
|
694
|
+
windowId: `grid-${String(deviceId)}-${String(atMs)}`,
|
|
695
|
+
deviceId,
|
|
696
|
+
timestamp: atMs,
|
|
697
|
+
startMs: atMs,
|
|
698
|
+
endMs: atMs,
|
|
699
|
+
level: {
|
|
700
|
+
rms: 0,
|
|
701
|
+
dbfs
|
|
702
|
+
},
|
|
703
|
+
detections: []
|
|
704
|
+
},
|
|
705
|
+
nodeId: "hub"
|
|
706
|
+
};
|
|
707
|
+
this.deps.eventBus.emit(createEvent(EventCategory.PipelineAudioInferenceResult, this.source(deviceId), payload));
|
|
708
|
+
}
|
|
709
|
+
emitDetections(grid, sourceFrame, detections) {
|
|
710
|
+
const payload = {
|
|
711
|
+
frame: {
|
|
712
|
+
kind: "frame",
|
|
713
|
+
frameId: `grid-${String(grid.deviceId)}-${sourceFrame.frameId}`,
|
|
714
|
+
deviceId: grid.deviceId,
|
|
715
|
+
timestamp: sourceFrame.timestamp,
|
|
716
|
+
width: grid.canvas.width,
|
|
717
|
+
height: grid.canvas.height,
|
|
718
|
+
detections
|
|
719
|
+
},
|
|
720
|
+
analysisResults: []
|
|
721
|
+
};
|
|
722
|
+
this.deps.eventBus.emit(createEvent(EventCategory.DetectionResult, this.source(grid.deviceId), payload));
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
//#endregion
|
|
398
726
|
//#region src/builtins/camera-grid/grid-wire-format.ts
|
|
399
727
|
/** What the relay answers with, and what the sampler must demux. */
|
|
400
728
|
var GRID_WIRE_CONTENT_TYPE = "video/x-flv";
|
|
@@ -1397,11 +1725,14 @@ var GridStreamSession = class {
|
|
|
1397
1725
|
* for a grid must win, and a reconcile that re-asserted every pass would silently
|
|
1398
1726
|
* overrule them once a minute.
|
|
1399
1727
|
*/
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1728
|
+
/**
|
|
1729
|
+
* The list lives in `@camstack/types` now, and this is an alias rather than a
|
|
1730
|
+
* second copy. There WERE two — this one and the Terminal addon's — and they
|
|
1731
|
+
* had drifted: neither named `pipeline-analytics`, the one cap with no gate of
|
|
1732
|
+
* its own, which is what a grid forwarding its sources' motion would have used
|
|
1733
|
+
* to write a duplicate motion episode per event.
|
|
1734
|
+
*/
|
|
1735
|
+
var GRID_SILENCED_CAP_NAMES = DERIVED_CAMERA_SILENCED_CAP_NAMES;
|
|
1405
1736
|
/**
|
|
1406
1737
|
* Resolve the addon currently providing `capName` for this device, from the
|
|
1407
1738
|
* device's OWN bindings. `listBindableCapsForDeviceType` answers for the device
|
|
@@ -1778,6 +2109,7 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
1778
2109
|
* — that only changes when an operator changes it.
|
|
1779
2110
|
*/
|
|
1780
2111
|
planByInstanceId = /* @__PURE__ */ new Map();
|
|
2112
|
+
activity = null;
|
|
1781
2113
|
snapshots = null;
|
|
1782
2114
|
frameSampler = null;
|
|
1783
2115
|
ffmpegBinaryPath = "ffmpeg";
|
|
@@ -1806,11 +2138,29 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
1806
2138
|
sampleFrame: async (session) => this.sampleFrame(session),
|
|
1807
2139
|
store: new GridLastFrameFileStore({ dataDir: this.ctx.dataDir })
|
|
1808
2140
|
});
|
|
2141
|
+
this.activity = new GridActivityForwarder({
|
|
2142
|
+
eventBus: this.ctx.eventBus,
|
|
2143
|
+
logger: this.ctx.logger.child("activity"),
|
|
2144
|
+
addonId: this.ctx.id,
|
|
2145
|
+
gridsForSource: (sourceDeviceId) => this.gridsForSource(sourceDeviceId),
|
|
2146
|
+
now: () => Date.now()
|
|
2147
|
+
});
|
|
2148
|
+
this.subscribeSourceActivity();
|
|
1809
2149
|
installGridCameraRuntime({
|
|
1810
2150
|
descriptorsFor: (instanceId) => this.descriptorsFor(instanceId),
|
|
1811
2151
|
getLayout: (deviceId) => this.gridViewFor(deviceId),
|
|
1812
2152
|
saveLayout: async (patch) => this.saveGridLayout(patch),
|
|
1813
|
-
getSnapshot: async (deviceId) => this.getSnapshot(deviceId)
|
|
2153
|
+
getSnapshot: async (deviceId) => this.getSnapshot(deviceId),
|
|
2154
|
+
motionStatus: (deviceId) => {
|
|
2155
|
+
const state = this.activity?.motionStatusFor(deviceId, Date.now()) ?? {
|
|
2156
|
+
detected: false,
|
|
2157
|
+
lastDetectedAt: null
|
|
2158
|
+
};
|
|
2159
|
+
return {
|
|
2160
|
+
...state,
|
|
2161
|
+
autoClearAfterMs: state.detected ? MOTION_CLOSE_AFTER_MS : null
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
1814
2164
|
});
|
|
1815
2165
|
await this.reconcile().catch((error) => {
|
|
1816
2166
|
this.ctx.logger.warn("camera grid: initial reconciliation failed — will retry", { meta: { error: errMsg(error) } });
|
|
@@ -1947,6 +2297,64 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
1947
2297
|
}
|
|
1948
2298
|
}
|
|
1949
2299
|
/**
|
|
2300
|
+
* Listen to the SOURCES, so the grid can say the same things about itself.
|
|
2301
|
+
*
|
|
2302
|
+
* Filtered by source id inside the handler rather than by subscribing per
|
|
2303
|
+
* camera: the bus filter is per category, the set of sources changes whenever
|
|
2304
|
+
* an operator drags a cell, and a per-camera subscription would have to be
|
|
2305
|
+
* torn down and rebuilt on every layout save.
|
|
2306
|
+
*
|
|
2307
|
+
* Every handler is TOTAL — a malformed payload is ignored, never thrown out
|
|
2308
|
+
* of. These are telemetry events on a shared bus (D8/D11), and a throw here
|
|
2309
|
+
* would take the grid's forwarding down for every grid because one camera
|
|
2310
|
+
* emitted something unexpected.
|
|
2311
|
+
*/
|
|
2312
|
+
subscribeSourceActivity() {
|
|
2313
|
+
this.subscribe({ category: EventCategory.MotionOnMotionChanged }, (event) => {
|
|
2314
|
+
const data = event.data;
|
|
2315
|
+
if (typeof data.deviceId !== "number" || typeof data.detected !== "boolean") return;
|
|
2316
|
+
const at = typeof data.timestamp === "number" ? data.timestamp : Date.now();
|
|
2317
|
+
this.activity?.onSourceMotion(data.deviceId, data.detected, at);
|
|
2318
|
+
});
|
|
2319
|
+
this.subscribe({ category: EventCategory.PipelineAudioInferenceResult }, (event) => {
|
|
2320
|
+
const data = event.data;
|
|
2321
|
+
const dbfs = data.frame?.level?.dbfs;
|
|
2322
|
+
if (typeof data.deviceId !== "number" || typeof dbfs !== "number") return;
|
|
2323
|
+
this.activity?.onSourceAudio(data.deviceId, dbfs, Date.now());
|
|
2324
|
+
});
|
|
2325
|
+
this.subscribe({ category: EventCategory.DetectionResult }, (event) => {
|
|
2326
|
+
const frame = event.data.frame;
|
|
2327
|
+
if (frame === void 0 || frame.kind !== "frame") return;
|
|
2328
|
+
this.activity?.onSourceDetections(frame);
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Which grids show this camera. Empty for the overwhelming majority of
|
|
2333
|
+
* events, which is why it is the FIRST thing every handler asks.
|
|
2334
|
+
*/
|
|
2335
|
+
gridsForSource(sourceDeviceId) {
|
|
2336
|
+
const targets = [];
|
|
2337
|
+
for (const [instanceId, deviceId] of this.deviceIdByInstanceId) {
|
|
2338
|
+
const instance = this.instanceById(instanceId);
|
|
2339
|
+
if (!instance || !instance.enabled) continue;
|
|
2340
|
+
if (sourceDeviceId === deviceId) continue;
|
|
2341
|
+
if (instance.cells.filter((cell) => cell.deviceId === sourceDeviceId).length === 0) continue;
|
|
2342
|
+
targets.push({
|
|
2343
|
+
deviceId,
|
|
2344
|
+
canvas: {
|
|
2345
|
+
width: instance.width,
|
|
2346
|
+
height: instance.height
|
|
2347
|
+
},
|
|
2348
|
+
cellsForSource: (id) => instance.cells.filter((cell) => cell.deviceId === id).map((cell) => ({
|
|
2349
|
+
source: cell.source,
|
|
2350
|
+
cell: cell.cell
|
|
2351
|
+
})),
|
|
2352
|
+
sourceDeviceIds: [...new Set(instance.cells.map((cell) => cell.deviceId))]
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
return targets;
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
1950
2358
|
* Which stream each grid reads each of its sources at, asked of the BROKER.
|
|
1951
2359
|
*
|
|
1952
2360
|
* `listAllProfileSlots` is the one authority on "does 615 have a low
|
|
@@ -2130,6 +2538,7 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2130
2538
|
}
|
|
2131
2539
|
for (const instanceId of [...this.deviceIdByInstanceId.keys()]) {
|
|
2132
2540
|
if (live.has(instanceId)) continue;
|
|
2541
|
+
this.activity?.forget(this.deviceIdByInstanceId.get(instanceId) ?? 0);
|
|
2133
2542
|
this.deviceIdByInstanceId.delete(instanceId);
|
|
2134
2543
|
this.planByInstanceId.delete(instanceId);
|
|
2135
2544
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { AddonApi, IScopedLogger } from '@camstack/types';
|
|
2
|
+
/**
|
|
3
|
+
* The list lives in `@camstack/types` now, and this is an alias rather than a
|
|
4
|
+
* second copy. There WERE two — this one and the Terminal addon's — and they
|
|
5
|
+
* had drifted: neither named `pipeline-analytics`, the one cap with no gate of
|
|
6
|
+
* its own, which is what a grid forwarding its sources' motion would have used
|
|
7
|
+
* to write a duplicate motion episode per event.
|
|
8
|
+
*/
|
|
2
9
|
export declare const GRID_SILENCED_CAP_NAMES: readonly string[];
|
|
3
10
|
export interface SilenceAnalysisDeps {
|
|
4
11
|
readonly api: AddonApi;
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-BhYweLVr.js");
|
|
7
7
|
const require_formatter = require("../../formatter-DqAKDlvN.js");
|
|
8
8
|
//#region src/builtins/console-logging/console-destination.ts
|
|
9
9
|
var LEVEL_RANK = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ct as logDestinationCapability,
|
|
1
|
+
import { Ct as logDestinationCapability, sn as BaseAddon } from "../../dist-CO2O_ddo.mjs";
|
|
2
2
|
import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
|
|
3
3
|
//#region src/builtins/console-logging/console-destination.ts
|
|
4
4
|
var LEVEL_RANK = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
3
|
-
const require_dist = require("../../dist-
|
|
3
|
+
const require_dist = require("../../dist-BhYweLVr.js");
|
|
4
4
|
let node_crypto = require("node:crypto");
|
|
5
5
|
let node_fs_promises = require("node:fs/promises");
|
|
6
6
|
node_fs_promises = require_chunk.__toESM(node_fs_promises);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { G as coreBlockAddonId, K as coreBlockIdFromAddonId,
|
|
1
|
+
import { G as coreBlockAddonId, K as coreBlockIdFromAddonId, an as errMsg, c as CORE_BLOCKS_ADDON_ID, l as CORE_BLOCK_ADDON_PREFIX, p as DeclaredDevices, q as coreBlocksCapability, sn as BaseAddon, u as CoreBlockSchema } from "../../dist-CO2O_ddo.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
7
|
-
const require_retired_settings_keys = require("../../retired-settings-keys-
|
|
6
|
+
const require_dist = require("../../dist-BhYweLVr.js");
|
|
7
|
+
const require_retired_settings_keys = require("../../retired-settings-keys-CKLI8VmE.js");
|
|
8
8
|
let node_crypto = require("node:crypto");
|
|
9
9
|
let _camstack_types_node = require("@camstack/types/node");
|
|
10
10
|
//#region src/builtins/device-manager/adoption-job-engine.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $ as deviceStateCapability,
|
|
2
|
-
import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-
|
|
1
|
+
import { $ as deviceStateCapability, D as STREAM_PROFILE_META, H as buildStreamParamsConfigSchema, Ht as runtimeStatePolicyFor, Nn as sleep$1, Nt as parseStreamParamsFormPatch, Pn as EventCategory, Q as deviceManagerCapability, St as locationIconKey, Tn as isDeviceConfigCap, an as errMsg, dn as DeviceFeature, et as deviceStatusCapability, fn as DeviceRole, gn as WELL_KNOWN_TAB_MAP, it as enumerateSchemaFields, jt as normalizeUnit, m as DeviceStatusSchema, o as CAMERA_SWITCH_ORDER, pn as DeviceType, qt as siteZoneKey, rt as enumerateItemArrayFields, s as CAP_NAMES_WITH_STATUS, sn as BaseAddon, t as ALL_CAPABILITY_DEFINITIONS, v as LocationIconIdSchema, zt as resolveContainerPrimaryChild } from "../../dist-CO2O_ddo.mjs";
|
|
2
|
+
import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-BnnHWyfj.mjs";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { canonicalDeviceFingerprint } from "@camstack/types/node";
|
|
5
5
|
//#region src/builtins/device-manager/adoption-job-engine.ts
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-BhYweLVr.js");
|
|
7
7
|
//#region src/builtins/doorbell/trigger-engine.ts
|
|
8
8
|
/**
|
|
9
9
|
* Pure trigger logic for the virtual-doorbell builtin — edge detection over
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as bareAddonId,
|
|
1
|
+
import { B as bareAddonId, Pn as EventCategory, T as SOURCE_DEVICE_TYPES, an as errMsg, at as evaluateSensorEdge, bn as createEvent, gt as isSourceCap, ht as isSameAddonId, pn as DeviceType, sn as BaseAddon, tt as doorbellCapability, w as SOURCE_CAPS } from "../../dist-CO2O_ddo.mjs";
|
|
2
2
|
//#region src/builtins/doorbell/trigger-engine.ts
|
|
3
3
|
/**
|
|
4
4
|
* Pure trigger logic for the virtual-doorbell builtin — edge detection over
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-BhYweLVr.js");
|
|
7
7
|
const require_formatter = require("../../formatter-DqAKDlvN.js");
|
|
8
8
|
//#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
|
|
9
9
|
var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ct as logDestinationCapability,
|
|
1
|
+
import { Ct as logDestinationCapability, sn as BaseAddon } from "../../dist-CO2O_ddo.mjs";
|
|
2
2
|
import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
|
|
3
3
|
//#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
|
|
4
4
|
var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
require("../../chunk-Cek0wNdY.js");
|
|
3
|
-
const require_dist = require("../../dist-
|
|
3
|
+
const require_dist = require("../../dist-BhYweLVr.js");
|
|
4
4
|
//#region src/builtins/liveness-monitor/liveness-checks.ts
|
|
5
5
|
var NO_DEVICES = "liveness:no-devices";
|
|
6
6
|
var ALL_OFFLINE = "liveness:all-devices-offline";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as RecordingStatusSchema,
|
|
1
|
+
import { C as RecordingStatusSchema, Pn as EventCategory, an as errMsg, bn as createEvent, sn as BaseAddon } from "../../dist-CO2O_ddo.mjs";
|
|
2
2
|
//#region src/builtins/liveness-monitor/liveness-checks.ts
|
|
3
3
|
var NO_DEVICES = "liveness:no-devices";
|
|
4
4
|
var ALL_OFFLINE = "liveness:all-devices-offline";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-BhYweLVr.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
node_crypto = require_chunk.__toESM(node_crypto);
|
|
9
9
|
let crypto$1 = require("crypto");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
|
|
2
|
-
import { N as UserRecordSchema, O as ScopedTokenSchema, R as authProviderCapability,
|
|
2
|
+
import { N as UserRecordSchema, O as ScopedTokenSchema, R as authProviderCapability, r as ApiKeyRecordSchema, sn as BaseAddon, tn as userManagementCapability } from "../../dist-CO2O_ddo.mjs";
|
|
3
3
|
import * as crypto$2 from "node:crypto";
|
|
4
4
|
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
5
5
|
import nodeCrypto from "crypto";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
3
|
-
const require_dist = require("../../dist-
|
|
3
|
+
const require_dist = require("../../dist-BhYweLVr.js");
|
|
4
4
|
const require_tls = require("../../tls-BFs4PzoW.js");
|
|
5
5
|
let node_fs_promises = require("node:fs/promises");
|
|
6
6
|
let node_path = require("node:path");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Pn as EventCategory, sn as BaseAddon, xt as localNetworkCapability } from "../../dist-CO2O_ddo.mjs";
|
|
2
2
|
import { S as readLanHttpState, _ as DEFAULT_LAN_HTTP_PORT, a as writeTlsMode, i as writeExtraSans, l as reissueTlsLeaf, n as readTlsAccessStatus, o as validateUploadedTls, r as readTlsMode, s as ensureTlsCert, t as readExtraSans, y as applyLanHttp } from "../../tls-2xbgg78V.mjs";
|
|
3
3
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-BhYweLVr.js");
|
|
7
7
|
//#region src/builtins/loki-logging/loki-payload.ts
|
|
8
8
|
/**
|
|
9
9
|
* Loki rejects a label name that is not a valid Prometheus label
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ct as logDestinationCapability,
|
|
1
|
+
import { Ct as logDestinationCapability, sn as BaseAddon } from "../../dist-CO2O_ddo.mjs";
|
|
2
2
|
//#region src/builtins/loki-logging/loki-payload.ts
|
|
3
3
|
/**
|
|
4
4
|
* Loki rejects a label name that is not a valid Prometheus label
|