@camera.ui/server 2.0.11 → 2.0.12
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/interface/assets/{404-COkoHYdK.js → 404-Dfyzm8bF.js} +1 -1
- package/dist/interface/assets/{About-qJICwbGH.js → About-B4Y3WP7T.js} +1 -1
- package/dist/interface/assets/{Adminpanel-D58GDcYK.js → Adminpanel-DSk1uh2x.js} +1 -1
- package/dist/interface/assets/Automation-CTSfLcmF.js +2 -0
- package/dist/interface/assets/{AutomationImport-jG2DzCAw.js → AutomationImport-B1P0T0Pe.js} +2 -2
- package/dist/interface/assets/AutomationStore-ZaaXPPbd.js +2 -0
- package/dist/interface/assets/AutomationStoreDetail-B2vc7mTZ.js +2 -0
- package/dist/interface/assets/Automations-Bvw-jTKi.js +2 -0
- package/dist/interface/assets/Camera-BxmJXk11.js +2 -0
- package/dist/interface/assets/{CameraConsole-BjtK8-Jv.js → CameraConsole-CVqgED5u.js} +1 -1
- package/dist/interface/assets/Cameras-DT62fcZK.js +652 -0
- package/dist/interface/assets/Camview-CqSckgLr.js +1 -0
- package/dist/interface/assets/Config-p9t9zgly.js +2 -0
- package/dist/interface/assets/{ConfigActionCameraControl-DjD6pSYf.js → ConfigActionCameraControl-BLROOU0q.js} +1 -1
- package/dist/interface/assets/{ConfigActionDelay-DGZlBe84.js → ConfigActionDelay-q0pOTOzh.js} +1 -1
- package/dist/interface/assets/{ConfigActionHttp-h5zpYYny.js → ConfigActionHttp-DiMXKRGz.js} +1 -1
- package/dist/interface/assets/ConfigActionImageInput-u5xX9t3-.js +1 -0
- package/dist/interface/assets/{ConfigActionMqtt-BnNmQUNe.js → ConfigActionMqtt-BkZl3uYq.js} +1 -1
- package/dist/interface/assets/ConfigActionNotification-DIkFNlKe.js +1 -0
- package/dist/interface/assets/{ConfigActionOutput-h2IebNMX.js → ConfigActionOutput-CJF0F4cT.js} +1 -1
- package/dist/interface/assets/{ConfigActionPlugin-B6pIqGWe.js → ConfigActionPlugin-rQeJ4umG.js} +1 -1
- package/dist/interface/assets/ConfigActionSensor-BWHpkoMs.js +1 -0
- package/dist/interface/assets/{ConfigActionSnapshot-Cs2TG5fZ.js → ConfigActionSnapshot-BlvQZl1R.js} +1 -1
- package/dist/interface/assets/{ConfigActionVariable-BPK-FHZ-.js → ConfigActionVariable-BMV-BDAk.js} +1 -1
- package/dist/interface/assets/{ConfigConditionIfElse-BNfUIXNh.js → ConfigConditionIfElse-C5geCT1S.js} +1 -1
- package/dist/interface/assets/ConfigConditionSensorState-8bIGEhQd.js +1 -0
- package/dist/interface/assets/{ConfigConditionSwitch-C7_-2l4Z.js → ConfigConditionSwitch-PwrCSnTQ.js} +1 -1
- package/dist/interface/assets/{ConfigConditionTime-DCTdB9RZ.js → ConfigConditionTime-DUt_dsr9.js} +1 -1
- package/dist/interface/assets/{ConfigSensorValueInput-Dt09C19b.js → ConfigSensorValueInput-CgZWxOMu.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerDetection-CVTb6TEu.js → ConfigTriggerDetection-D4UitpXI.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerGeofence-Cb62v6rf.js → ConfigTriggerGeofence-Ci-gkmjS.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerManual-agJhIaCy.js → ConfigTriggerManual-_PAVLJxz.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerMqtt-DWZ79tPv.js → ConfigTriggerMqtt-BtfXQ_Bh.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerSchedule-C38lrm2R.js → ConfigTriggerSchedule-CHyW4YQ1.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerSensor-BKXoFFFT.js → ConfigTriggerSensor-CZMplCYs.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerSystem-DqoMfULb.js → ConfigTriggerSystem-OOffe8rK.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerWebhook-DunygdEo.js → ConfigTriggerWebhook-Baul6M1Y.js} +1 -1
- package/dist/interface/assets/Console-MXiL0z_X.js +1 -0
- package/dist/interface/assets/{CuiActionButton-iaZkXylf.js → CuiActionButton-WFz5I80P.js} +1 -1
- package/dist/interface/assets/{CuiAvatar--d4yBFev.js → CuiAvatar-BUwQnIaI.js} +1 -1
- package/dist/interface/assets/{CuiBBoxPlayground-BuDfU2lQ.js → CuiBBoxPlayground-J7_8dCvo.js} +1 -1
- package/dist/interface/assets/CuiBottomSheet-BA-u9UtN.js +1 -0
- package/dist/interface/assets/{CuiBottomSheet-o1iNIub8.js → CuiBottomSheet-CzG0n4Oa.js} +1 -1
- package/dist/interface/assets/CuiCameraCard-GH-DR5_s.js +1 -0
- package/dist/interface/assets/{CuiCameraPipCard-NA4CctaN.js → CuiCameraPipCard-CL_wj05b.js} +1 -1
- package/dist/interface/assets/CuiCameraRecordings-BjErk9Gl.js +1 -0
- package/dist/interface/assets/CuiCameraShares-qrJhZMEt.js +1 -0
- package/dist/interface/assets/{CuiCameraSnapshot-Dp6cLkMa.js → CuiCameraSnapshot-Dn1QSGrr.js} +1 -1
- package/dist/interface/assets/CuiCameraTable-or8VpE34.js +2 -0
- package/dist/interface/assets/{CuiDataTable-BDb1Qvzm.js → CuiDataTable-CjXiUFiJ.js} +1 -1
- package/dist/interface/assets/CuiFloatingButton-BjpU1EHv.js +1 -0
- package/dist/interface/assets/CuiImage-BM4g6edi.css +1 -0
- package/dist/interface/assets/CuiImage-Bq3bxOOL.js +1 -0
- package/dist/interface/assets/CuiMenu-DtnIPhcr.js +1 -0
- package/dist/interface/assets/CuiPluginCard-DKNWUFpR.js +2 -0
- package/dist/interface/assets/{CuiPluginCategoryChip-R5WUBAUw.js → CuiPluginCategoryChip-cmfKx-dU.js} +1 -1
- package/dist/interface/assets/CuiPluginOAuthButton-C6FQiVkt.js +2 -0
- package/dist/interface/assets/CuiPluginTrustBadge-PVZMP1rI.js +1 -0
- package/dist/interface/assets/{CuiQRCode-CsxVDVaU.js → CuiQRCode-uQygC1dP.js} +1 -1
- package/dist/interface/assets/CuiRecordingsGrid-Cuwd4L67.js +1 -0
- package/dist/interface/assets/CuiSchema-CH0GXMWV.js +1 -0
- package/dist/interface/assets/{CuiTopNavbarItem-CIO_h1T0.js → CuiTopNavbarItem-nGcC2em4.js} +1 -1
- package/dist/interface/assets/{CuiTopbarSlot-C6t4NmW-.js → CuiTopbarSlot-B6UMqYZl.js} +1 -1
- package/dist/interface/assets/Faces-cpO5vJbp.js +1 -0
- package/dist/interface/assets/{FirstSteps-CP26tE1m.js → FirstSteps-BHTcbs9O.js} +1 -1
- package/dist/interface/assets/{Instances-Bdy0n3fB.js → Instances-BXiVG2Dm.js} +1 -1
- package/dist/interface/assets/Logs-hMg2QOGj.js +6 -0
- package/dist/interface/assets/{Menu-DX0KhBBs.js → Menu-BiPBCi8Z.js} +1 -1
- package/dist/interface/assets/{OAuthDeviceFlow-Cci9ex4G.js → OAuthDeviceFlow-Bia9UA3I.js} +1 -1
- package/dist/interface/assets/Plugin-DfRaHAsi.js +1 -0
- package/dist/interface/assets/{PluginConsole-Fffl7Yh_.js → PluginConsole-CRw4zTRM.js} +1 -1
- package/dist/interface/assets/PluginDetail-BYqji8yN.css +1 -0
- package/dist/interface/assets/PluginDetail-BqRoLZc7.js +2 -0
- package/dist/interface/assets/{PluginSchema-BNEfEDJU.js → PluginSchema-TtBJPR_R.js} +1 -1
- package/dist/interface/assets/PluginSearch-B_NYpJi3.js +2 -0
- package/dist/interface/assets/{PluginSearch-NuKgD1r3.css → PluginSearch-Bbp8D7BJ.css} +1 -1
- package/dist/interface/assets/PluginSearch-DnJgg6-6.js +1 -0
- package/dist/interface/assets/Plugins-DVFEGtj3.js +1 -0
- package/dist/interface/assets/Recordings-dwm6lAoJ.js +1 -0
- package/dist/interface/assets/{Settings-TkiQG2I9.js → Settings-BpBmrGpS.js} +1 -1
- package/dist/interface/assets/SettingsAccount-8Kh1uQqn.js +1 -0
- package/dist/interface/assets/SettingsAccount-CjgYe5mF.js +1 -0
- package/dist/interface/assets/SettingsAppearance-3i3Ix7j-.js +1 -0
- package/dist/interface/assets/{SettingsAppearance-CWWSQkIY.js → SettingsAppearance-EoBxclgt.js} +1 -1
- package/dist/interface/assets/SettingsBackup-Bpsbg6Pm.js +1 -0
- package/dist/interface/assets/{SettingsMqtt-CeurNe8m.js → SettingsMqtt-ZQolc9lR.js} +1 -1
- package/dist/interface/assets/SettingsNotifications-BOoMt1Er.js +2 -0
- package/dist/interface/assets/SettingsRecordings-BQhxLCgM.js +1 -0
- package/dist/interface/assets/SettingsRemote-Cvcmpdgb.js +1 -0
- package/dist/interface/assets/SettingsSystem-B-yhq40t.js +2 -0
- package/dist/interface/assets/SettingsUsers-CezygBk8.js +1 -0
- package/dist/interface/assets/Terminal-CDQJGwb_.js +4 -0
- package/dist/interface/assets/{VariableInput-BuXTKXXC.js → VariableInput-CnmqCHf7.js} +1 -1
- package/dist/interface/assets/{VariableSuggestions-BIPtw8GW.js → VariableSuggestions-D6IlwBqV.js} +1 -1
- package/dist/interface/assets/VersionsHandler-BHR1k9zz.js +7 -0
- package/dist/interface/assets/Workers-Bv-bWBC-.js +2 -0
- package/dist/interface/assets/{api-DudQz5TK.js → api-BhOwzvxK.js} +1 -1
- package/dist/interface/assets/api-Cea16mc8.js +2 -0
- package/dist/interface/assets/api-GMfqGYqQ.css +2 -0
- package/dist/interface/assets/{auth-BKlPnLfY.js → auth-CNPkdTXX.js} +1 -1
- package/dist/interface/assets/{automations-eQnuQOEs.js → automations-CHengR-B.js} +1 -1
- package/dist/interface/assets/{automations-Dtp3-KSy.js → automations-DhLADBhd.js} +1 -1
- package/dist/interface/assets/{cameras-BySMxwdP.js → cameras-DyF2T0be.js} +1 -1
- package/dist/interface/assets/{cameras.schema-DVnsQl9j.js → cameras.schema-DBzgU4m6.js} +1 -1
- package/dist/interface/assets/cloudHandoff-LxdvSi1a.js +1 -0
- package/dist/interface/assets/{config-CDCKrp2V.js → config-c7o4sJrQ.js} +1 -1
- package/dist/interface/assets/{circular-connection-Fq-Vqe3X.js → delete-outline-CbGU9XWz.js} +1 -1
- package/dist/interface/assets/{deviceId-O-kfs3nO.js → deviceId-BYu9Q8Ay.js} +1 -1
- package/dist/interface/assets/{hasPermission-DADXQ_dj.js → hasPermission-CgyWyOhQ.js} +1 -1
- package/dist/interface/assets/{instances-DVk8sRuN.js → instances-CRYtaTQS.js} +1 -1
- package/dist/interface/assets/main-CgXkx5UO.js +3 -0
- package/dist/interface/assets/{notifications-Dnh_ylVF.js → notifications-sejZCb0L.js} +1 -1
- package/dist/interface/assets/{plugins-CHRZiPTq.js → plugins-CCLFcD4c.js} +1 -1
- package/dist/interface/assets/round-restart-alt-PDSZur-0.js +1 -0
- package/dist/interface/assets/{router-M08TaIji.css → router-C7b6u3Zu.css} +1 -1
- package/dist/interface/assets/router-DkMUvvnP.js +2 -0
- package/dist/interface/assets/{routerState-CencmCOo.js → routerState-BR88YV1H.js} +1 -1
- package/dist/interface/assets/{server-zkNDKqjz.js → server-C7SETTy9.js} +1 -1
- package/dist/interface/assets/{shares-DooKpXt5.js → shares-Cy1SLL98.js} +1 -1
- package/dist/interface/assets/{theme-Dlx1uc6I.js → theme-DqbhRu7y.js} +1 -1
- package/dist/interface/assets/{types-DwLgLlNL.js → types-v8L66UlA.js} +1 -1
- package/dist/interface/assets/{ui-BajsbwJJ.js → ui-g5rCwxUE.js} +1 -1
- package/dist/interface/assets/{useAppVersion-Vx2rWjdR.js → useAppVersion-WfdoG9Ji.js} +1 -1
- package/dist/interface/assets/{useBackupRestore-CtDDTNYM.js → useBackupRestore-ootsFhdF.js} +1 -1
- package/dist/interface/assets/{useBiometricCredentials-DnEuKj2Y.js → useBiometricCredentials-Ca75xEru.js} +1 -1
- package/dist/interface/assets/{useCameraOptions-CurEfrEs.js → useCameraOptions-BpL2zbfV.js} +1 -1
- package/dist/interface/assets/{useCuiCameraDrawer-eH0m75Mk.js → useCuiCameraDrawer-BHUHw6W6.js} +1 -1
- package/dist/interface/assets/useCuiDialog-yVh5UWKF.js +2 -0
- package/dist/interface/assets/{useFileDownload-CaB5Myfa.js → useFileDownload-BcaNWndS.js} +1 -1
- package/dist/interface/assets/useLogsSocket-BRSsUNqv.js +5 -0
- package/dist/interface/assets/{useServerRestart-COGxcO4B.js → useServerRestart-PZhND5H_.js} +1 -1
- package/dist/interface/assets/{useSharedComposables-DC7dAxBt.js → useSharedComposables-USqd05AG.js} +1 -1
- package/dist/interface/assets/{useStreamStatus-Cr_bBYAe.js → useStreamStatus-nCx5ux1-.js} +1 -1
- package/dist/interface/assets/{useUpdateChannel-D_SN9IPN.js → useUpdateChannel-3eHk3KXZ.js} +1 -1
- package/dist/interface/assets/{useUserOptions-CtgFdv3y.js → useUserOptions-DVC4k8Zs.js} +1 -1
- package/dist/interface/assets/{users.schema-vs5FAhpT.js → users.schema-B4fEuoKZ.js} +1 -1
- package/dist/interface/assets/{vendor-cameraui-D5fd9Ars.js → vendor-cameraui-QuTJKoXE.js} +84 -23
- package/dist/interface/assets/vendor-core-CCcu_nmz.js +1 -0
- package/dist/interface/assets/{vendor-primevue-Ba4LruKV.js → vendor-primevue-BdB7McKg.js} +84 -145
- package/dist/interface/assets/view-grid-BWJS0bBD.js +1 -0
- package/dist/interface/index.html +47 -47
- package/dist/interface/sw.js +1 -1
- package/dist/server/package.json +6 -6
- package/dist/server/requirements.txt +1 -1
- package/dist/server/src/_camera_ui_tools/camera_ui_sdk/__init__.py +2 -0
- package/dist/server/src/_camera_ui_tools/camera_ui_sdk/camera/detection.py +3 -0
- package/dist/server/src/_camera_ui_tools/camera_ui_sdk/sensor/__init__.py +2 -0
- package/dist/server/src/_camera_ui_tools/camera_ui_sdk/sensor/ptz.py +45 -2
- package/dist/server/src/api/database/backfill.d.ts +5 -0
- package/dist/server/src/api/database/backfill.js +89 -0
- package/dist/server/src/api/database/backfill.js.map +1 -0
- package/dist/server/src/api/database/index.d.ts +1 -0
- package/dist/server/src/api/database/index.js +29 -43
- package/dist/server/src/api/database/index.js.map +1 -1
- package/dist/server/src/api/database/record-schemas.d.ts +231 -0
- package/dist/server/src/api/database/record-schemas.js +194 -0
- package/dist/server/src/api/database/record-schemas.js.map +1 -0
- package/dist/server/src/api/schemas/cameras.schema.d.ts +265 -1
- package/dist/server/src/api/schemas/cameras.schema.js +10 -0
- package/dist/server/src/api/schemas/cameras.schema.js.map +1 -1
- package/dist/server/src/api/schemas/remote.schema.d.ts +1 -1
- package/dist/server/src/api/services/cameras.service.d.ts +1 -0
- package/dist/server/src/api/services/cameras.service.js +18 -1
- package/dist/server/src/api/services/cameras.service.js.map +1 -1
- package/dist/server/src/api/services/plugins.service.js +15 -4
- package/dist/server/src/api/services/plugins.service.js.map +1 -1
- package/dist/server/src/api/websocket/nsp/main.js +3 -0
- package/dist/server/src/api/websocket/nsp/main.js.map +1 -1
- package/dist/server/src/camera/controller.js +1 -12
- package/dist/server/src/camera/controller.js.map +1 -1
- package/dist/server/src/camera/decoder/audio-loop.d.ts +25 -0
- package/dist/server/src/camera/decoder/audio-loop.js +163 -0
- package/dist/server/src/camera/decoder/audio-loop.js.map +1 -0
- package/dist/server/src/camera/decoder/cascade-manager.js +1 -4
- package/dist/server/src/camera/decoder/cascade-manager.js.map +1 -1
- package/dist/server/src/camera/decoder/detection-coordinator.d.ts +36 -97
- package/dist/server/src/camera/decoder/detection-coordinator.js +479 -1387
- package/dist/server/src/camera/decoder/detection-coordinator.js.map +1 -1
- package/dist/server/src/camera/decoder/detection-pipeline.d.ts +4 -0
- package/dist/server/src/camera/decoder/detection-pipeline.js +14 -24
- package/dist/server/src/camera/decoder/detection-pipeline.js.map +1 -1
- package/dist/server/src/camera/decoder/dwell-manager.js +1 -1
- package/dist/server/src/camera/decoder/dwell-manager.js.map +1 -1
- package/dist/server/src/camera/decoder/event-manager.js +15 -28
- package/dist/server/src/camera/decoder/event-manager.js.map +1 -1
- package/dist/server/src/camera/decoder/event-thumbnailer.d.ts +38 -0
- package/dist/server/src/camera/decoder/event-thumbnailer.js +192 -0
- package/dist/server/src/camera/decoder/event-thumbnailer.js.map +1 -0
- package/dist/server/src/camera/decoder/plugin-registry.d.ts +31 -0
- package/dist/server/src/camera/decoder/plugin-registry.js +114 -0
- package/dist/server/src/camera/decoder/plugin-registry.js.map +1 -0
- package/dist/server/src/camera/decoder/{ptz-autotracker.d.ts → ptz/autotracker.d.ts} +19 -0
- package/dist/server/src/camera/decoder/ptz/autotracker.js +537 -0
- package/dist/server/src/camera/decoder/ptz/autotracker.js.map +1 -0
- package/dist/server/src/camera/decoder/reconnect-backoff.d.ts +6 -0
- package/dist/server/src/camera/decoder/reconnect-backoff.js +17 -0
- package/dist/server/src/camera/decoder/reconnect-backoff.js.map +1 -0
- package/dist/server/src/camera/decoder/secondary-stage.d.ts +37 -0
- package/dist/server/src/camera/decoder/secondary-stage.js +445 -0
- package/dist/server/src/camera/decoder/secondary-stage.js.map +1 -0
- package/dist/server/src/camera/decoder/{audio-source.js → sources/audio-source.js} +2 -2
- package/dist/server/src/camera/decoder/sources/audio-source.js.map +1 -0
- package/dist/server/src/camera/decoder/{frame-source.js → sources/frame-source.js} +8 -11
- package/dist/server/src/camera/decoder/sources/frame-source.js.map +1 -0
- package/dist/server/src/camera/decoder/{snapshot-source.d.ts → sources/snapshot-source.d.ts} +2 -5
- package/dist/server/src/camera/decoder/{snapshot-source.js → sources/snapshot-source.js} +11 -15
- package/dist/server/src/camera/decoder/sources/snapshot-source.js.map +1 -0
- package/dist/server/src/camera/decoder/stationary-suppressor.d.ts +21 -0
- package/dist/server/src/camera/decoder/stationary-suppressor.js +141 -0
- package/dist/server/src/camera/decoder/stationary-suppressor.js.map +1 -0
- package/dist/server/src/camera/decoder/types.d.ts +6 -1
- package/dist/server/src/camera/decoder/types.js +1 -0
- package/dist/server/src/camera/decoder/types.js.map +1 -1
- package/dist/server/src/camera/decoder/worker.js +4 -7
- package/dist/server/src/camera/decoder/worker.js.map +1 -1
- package/dist/server/src/go2rtc/api/routes/discover.js +2 -1
- package/dist/server/src/go2rtc/api/routes/discover.js.map +1 -1
- package/dist/server/src/main.js +26 -3
- package/dist/server/src/main.js.map +1 -1
- package/dist/server/src/manager/downloadManager.js +0 -2
- package/dist/server/src/manager/downloadManager.js.map +1 -1
- package/dist/server/src/utils/npm/index.d.ts +10 -1
- package/dist/server/src/utils/npm/index.js +11 -1
- package/dist/server/src/utils/npm/index.js.map +1 -1
- package/dist/server/src/workers/capabilities/plugin-host.js +4 -1
- package/dist/server/src/workers/capabilities/plugin-host.js.map +1 -1
- package/package.json +6 -6
- package/dist/interface/assets/Automation-Dp_xaEqJ.js +0 -2
- package/dist/interface/assets/AutomationStore-4JFCinMU.js +0 -2
- package/dist/interface/assets/AutomationStoreDetail-yBtpTcZc.js +0 -2
- package/dist/interface/assets/Automations-BGsRPCnO.js +0 -2
- package/dist/interface/assets/Camera-C1_jdI_c.js +0 -2
- package/dist/interface/assets/Cameras-BmjWXAZ8.js +0 -652
- package/dist/interface/assets/Camview-wGaYaDmi.js +0 -1
- package/dist/interface/assets/Config-CuF6eGTD.js +0 -2
- package/dist/interface/assets/ConfigActionImageInput-xab5Mae-.js +0 -1
- package/dist/interface/assets/ConfigActionNotification-DGwL1eKM.js +0 -1
- package/dist/interface/assets/ConfigActionSensor-DBf7_RrD.js +0 -1
- package/dist/interface/assets/ConfigConditionSensorState-D1IWa7jK.js +0 -1
- package/dist/interface/assets/Console-DL4o8TCN.js +0 -1
- package/dist/interface/assets/CuiBottomSheet-DluJK839.js +0 -1
- package/dist/interface/assets/CuiCameraCard-CNkJcJNk.js +0 -1
- package/dist/interface/assets/CuiCameraRecordings-Dg_xNATQ.js +0 -1
- package/dist/interface/assets/CuiCameraShares-BSdoTLlP.js +0 -1
- package/dist/interface/assets/CuiCameraTable-16e0lAA5.js +0 -2
- package/dist/interface/assets/CuiFloatingButton-B64PYWE2.js +0 -1
- package/dist/interface/assets/CuiImage-Bxf7z2N4.css +0 -1
- package/dist/interface/assets/CuiImage-D_DDxN1t.js +0 -1
- package/dist/interface/assets/CuiMenu-BmgnEZun.js +0 -1
- package/dist/interface/assets/CuiPluginCard-C0GXkGJ3.js +0 -2
- package/dist/interface/assets/CuiPluginOAuthButton-CY3c3HRs.js +0 -2
- package/dist/interface/assets/CuiPluginTrustBadge-QRsixP01.js +0 -1
- package/dist/interface/assets/CuiRecordingsGrid-DycYYi7W.js +0 -1
- package/dist/interface/assets/CuiSchema-D1-Z2sTy.js +0 -1
- package/dist/interface/assets/Faces-QSA5E6EG.js +0 -1
- package/dist/interface/assets/Logs-Bs0C7Yu1.js +0 -6
- package/dist/interface/assets/Plugin-CG02A29i.js +0 -1
- package/dist/interface/assets/PluginDetail-BsIkPUY8.css +0 -1
- package/dist/interface/assets/PluginDetail-CRCMotvN.js +0 -2
- package/dist/interface/assets/PluginSearch-CwNCQedL.js +0 -2
- package/dist/interface/assets/PluginSearch-GD7qtALl.js +0 -1
- package/dist/interface/assets/Plugins-BNiQus_y.js +0 -1
- package/dist/interface/assets/Recordings-dY0k4i3E.js +0 -1
- package/dist/interface/assets/SettingsAccount-Cln0bZPI.js +0 -1
- package/dist/interface/assets/SettingsAccount-DvrdANbI.js +0 -1
- package/dist/interface/assets/SettingsAppearance-CVMAyxFr.js +0 -1
- package/dist/interface/assets/SettingsBackup-CZEKi84z.js +0 -1
- package/dist/interface/assets/SettingsNotifications-BBYcFlOe.js +0 -2
- package/dist/interface/assets/SettingsRecordings-8JRnUwvv.js +0 -1
- package/dist/interface/assets/SettingsRemote-BXLy9grM.js +0 -1
- package/dist/interface/assets/SettingsSystem-BwVJ2-Ws.js +0 -2
- package/dist/interface/assets/SettingsUsers-CMsX8YO6.js +0 -1
- package/dist/interface/assets/Terminal-CNAaV5h5.js +0 -4
- package/dist/interface/assets/VersionsHandler-0Ol7oxfg.js +0 -7
- package/dist/interface/assets/Workers-BYGXvih8.js +0 -2
- package/dist/interface/assets/api-BEwr0GLC.css +0 -2
- package/dist/interface/assets/api-CeJqvqFj.js +0 -2
- package/dist/interface/assets/cloudHandoff-DOYq2IS2.js +0 -1
- package/dist/interface/assets/main-DHUUsPUM.js +0 -3
- package/dist/interface/assets/round-restart-alt-BRh54UpI.js +0 -1
- package/dist/interface/assets/router-B6AZz5fb.js +0 -2
- package/dist/interface/assets/useCuiDialog-CGak2kyV.js +0 -2
- package/dist/interface/assets/useLogsSocket-B47qvN41.js +0 -5
- package/dist/interface/assets/vendor-core-D9amXF-u.js +0 -1
- package/dist/interface/assets/view-grid-jcP3VugM.js +0 -1
- package/dist/server/src/camera/decoder/audio-source.js.map +0 -1
- package/dist/server/src/camera/decoder/frame-source.js.map +0 -1
- package/dist/server/src/camera/decoder/ptz-autotracker.js +0 -342
- package/dist/server/src/camera/decoder/ptz-autotracker.js.map +0 -1
- package/dist/server/src/camera/decoder/snapshot-source.js.map +0 -1
- /package/dist/server/src/camera/decoder/{audio-source.d.ts → sources/audio-source.d.ts} +0 -0
- /package/dist/server/src/camera/decoder/{frame-source.d.ts → sources/frame-source.d.ts} +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable @stylistic/max-len */
|
|
2
1
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
2
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
3
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -8,154 +7,56 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
8
7
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
9
|
};
|
|
11
|
-
|
|
12
|
-
if (value !== null && value !== void 0) {
|
|
13
|
-
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
14
|
-
var dispose, inner;
|
|
15
|
-
if (async) {
|
|
16
|
-
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
17
|
-
dispose = value[Symbol.asyncDispose];
|
|
18
|
-
}
|
|
19
|
-
if (dispose === void 0) {
|
|
20
|
-
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
21
|
-
dispose = value[Symbol.dispose];
|
|
22
|
-
if (async) inner = dispose;
|
|
23
|
-
}
|
|
24
|
-
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
25
|
-
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
26
|
-
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
27
|
-
}
|
|
28
|
-
else if (async) {
|
|
29
|
-
env.stack.push({ async: true });
|
|
30
|
-
}
|
|
31
|
-
return value;
|
|
32
|
-
};
|
|
33
|
-
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
34
|
-
return function (env) {
|
|
35
|
-
function fail(e) {
|
|
36
|
-
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
37
|
-
env.hasError = true;
|
|
38
|
-
}
|
|
39
|
-
var r, s = 0;
|
|
40
|
-
function next() {
|
|
41
|
-
while (r = env.stack.pop()) {
|
|
42
|
-
try {
|
|
43
|
-
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
44
|
-
if (r.dispose) {
|
|
45
|
-
var result = r.dispose.call(r.value);
|
|
46
|
-
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
47
|
-
}
|
|
48
|
-
else s |= 1;
|
|
49
|
-
}
|
|
50
|
-
catch (e) {
|
|
51
|
-
fail(e);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
55
|
-
if (env.hasError) throw env.error;
|
|
56
|
-
}
|
|
57
|
-
return next();
|
|
58
|
-
};
|
|
59
|
-
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
60
|
-
var e = new Error(message);
|
|
61
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
62
|
-
});
|
|
10
|
+
import { PromiseTimeout, sleep } from '@camera.ui/common/utils';
|
|
63
11
|
import { isNoRespondersError, RPCClass, RPCMethod } from '@camera.ui/rpc';
|
|
64
12
|
import { SensorType } from '@camera.ui/sdk';
|
|
65
13
|
import { NamespaceManager } from '../../rpc/namespaces.js';
|
|
66
|
-
import { DETECTION_SENSOR_TYPES
|
|
14
|
+
import { DETECTION_SENSOR_TYPES } from '../sensors/types.js';
|
|
67
15
|
import { normalizeZone } from '../utils/filter.js';
|
|
68
|
-
import {
|
|
16
|
+
import { AudioDetectionLoop } from './audio-loop.js';
|
|
69
17
|
import { CascadeManager } from './cascade-manager.js';
|
|
70
18
|
import { DetectionPipeline } from './detection-pipeline.js';
|
|
71
19
|
import { DwellManager } from './dwell-manager.js';
|
|
72
20
|
import { DetectionEventManager } from './event-manager.js';
|
|
21
|
+
import { EventThumbnailer } from './event-thumbnailer.js';
|
|
73
22
|
import { FrameScaler } from './frame-scaler.js';
|
|
74
|
-
import {
|
|
75
|
-
import { PtzAutotracker } from './ptz
|
|
76
|
-
import {
|
|
77
|
-
import {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
export const STATIONARY_SPEED_THRESHOLD = 0.002;
|
|
23
|
+
import { isVideoInputSpec, PluginRegistry } from './plugin-registry.js';
|
|
24
|
+
import { PtzAutotracker } from './ptz/autotracker.js';
|
|
25
|
+
import { ReconnectBackoff } from './reconnect-backoff.js';
|
|
26
|
+
import { SecondaryStage } from './secondary-stage.js';
|
|
27
|
+
import { FrameSource } from './sources/frame-source.js';
|
|
28
|
+
import { StationarySuppressor } from './stationary-suppressor.js';
|
|
29
|
+
import { DETECT_TIMEOUT_MS, MOTION_WIDTH_MAP } from './types.js';
|
|
30
|
+
const DEFAULT_CASCADE_TIMEOUT = 10;
|
|
83
31
|
const OBJECT_DWELL_SECONDS = 2;
|
|
84
|
-
const
|
|
85
|
-
const EVENT_THUMB_MAX_WIDTH = 640;
|
|
86
|
-
const DETECTION_THUMB_MAX_WIDTH = 640;
|
|
87
|
-
const DETECTION_THUMB_MIN_CROP = 160;
|
|
88
|
-
const DETECTION_THUMB_HQ_MAX_WIDTH = 640;
|
|
89
|
-
const DETECTION_THUMB_HQ_MIN_CROP = 640;
|
|
90
|
-
const DETECTION_THUMB_HQ_QUALITY = 80;
|
|
91
|
-
const ATTRIBUTE_THUMB_MAX_WIDTH = 640;
|
|
92
|
-
const ATTRIBUTE_THUMB_MIN_CROP = 128;
|
|
93
|
-
const EVENT_THUMB_HQ_MAX_WIDTH = 960;
|
|
94
|
-
const EVENT_THUMB_HQ_QUALITY = 75;
|
|
95
|
-
const HQ_FRAME_MAX_AGE_MS = 500;
|
|
96
|
-
async function withTimeout(promise, ms, label) {
|
|
97
|
-
let timer;
|
|
98
|
-
const timeout = new Promise((_resolve, reject) => {
|
|
99
|
-
timer = setTimeout(() => reject(new Error(`${label} detection timed out after ${ms}ms`)), ms);
|
|
100
|
-
});
|
|
101
|
-
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
102
|
-
}
|
|
32
|
+
const SECONDARY_BBOX_TTL_MS = 2000;
|
|
103
33
|
let DetectionCoordinator = class DetectionCoordinator {
|
|
104
34
|
config;
|
|
105
35
|
proxy;
|
|
106
36
|
logger;
|
|
107
37
|
frameSource;
|
|
108
38
|
frameScaler;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
audioLoopRunning = false;
|
|
116
|
-
audioLoopPromise;
|
|
117
|
-
audioStopPromise;
|
|
118
|
-
audioReconnectCount = 0;
|
|
119
|
-
reconnectCount = 0;
|
|
120
|
-
maxReconnectCount = 10;
|
|
121
|
-
normalReconnectDelay = 10000;
|
|
122
|
-
maxReconnectDelay = 60000;
|
|
39
|
+
plugins = new PluginRegistry();
|
|
40
|
+
pipeline;
|
|
41
|
+
stationary;
|
|
42
|
+
secondaries;
|
|
43
|
+
thumbnailer;
|
|
44
|
+
audioLoop;
|
|
123
45
|
ptzAutotracker;
|
|
124
46
|
eventManager;
|
|
125
47
|
cascade = new CascadeManager();
|
|
126
48
|
dwell = new DwellManager();
|
|
127
|
-
|
|
49
|
+
videoBackoff = new ReconnectBackoff();
|
|
50
|
+
loopRunning = false;
|
|
51
|
+
loopPromise;
|
|
52
|
+
videoStopPromise;
|
|
128
53
|
adHocVideoLoop = false;
|
|
129
54
|
processingExternalSecondary = false;
|
|
130
|
-
hqSnapshotSource;
|
|
131
|
-
hqUpgradeInflight = false;
|
|
132
|
-
hqAspectWarned = false;
|
|
133
55
|
cascadeUnsubscribe;
|
|
134
56
|
dwellUnsubscribe;
|
|
135
|
-
seenStationaryTracks = new Set();
|
|
136
57
|
activeSensorTriggerTypes = new Map();
|
|
58
|
+
secondaryBboxSeen = new Map();
|
|
137
59
|
currentDetectionState = {};
|
|
138
|
-
get cascadeEnabled() {
|
|
139
|
-
return this.config.detectionSettings.cascadeDetection !== false;
|
|
140
|
-
}
|
|
141
|
-
get cascadeTimeoutSeconds() {
|
|
142
|
-
return this.config.detectionSettings.cascadeTimeout ?? DEFAULT_CASCADE_TIMEOUT;
|
|
143
|
-
}
|
|
144
|
-
get audioMinDecibels() {
|
|
145
|
-
return this.config.detectionSettings.audio.minDecibels;
|
|
146
|
-
}
|
|
147
|
-
get motionResolution() {
|
|
148
|
-
return this.config.detectionSettings.motion.resolution;
|
|
149
|
-
}
|
|
150
|
-
get targetFps() {
|
|
151
|
-
return this.frameSource.fps;
|
|
152
|
-
}
|
|
153
|
-
get videoAspectRatio() {
|
|
154
|
-
const [w, h] = this.config.interfaceSettings.aspectRatio.split(':').map(Number);
|
|
155
|
-
return w / h;
|
|
156
|
-
}
|
|
157
|
-
nvrProxy;
|
|
158
|
-
nvrProxyPromise;
|
|
159
60
|
constructor(config, proxy, logger) {
|
|
160
61
|
this.config = config;
|
|
161
62
|
this.proxy = proxy;
|
|
@@ -178,22 +79,29 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
178
79
|
this.frameSource = new FrameSource(frameSourceConfig, logger);
|
|
179
80
|
this.frameScaler = new FrameScaler(null, logger);
|
|
180
81
|
this.pipeline = new DetectionPipeline(config.zones, config.detectionSettings);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
this.hqSnapshotSource = new SnapshotSource({ url: hqUrl }, logger);
|
|
184
|
-
}
|
|
185
|
-
this.syncHqSnapshotSource();
|
|
82
|
+
this.stationary = new StationarySuppressor(logger);
|
|
83
|
+
this.secondaries = new SecondaryStage(this.plugins, this.pipeline, this.frameScaler, this.proxy, logger, () => !this.loopRunning);
|
|
186
84
|
if (config.lines.length > 0) {
|
|
187
85
|
this.pipeline.updateLines(config.lines, this.videoAspectRatio);
|
|
188
86
|
}
|
|
189
87
|
this.eventManager = new DetectionEventManager(config.cameraId, this.proxy, this.logger);
|
|
190
88
|
this.eventManager.onEventEnd(() => this.handleEventEnded());
|
|
89
|
+
this.thumbnailer = new EventThumbnailer({ frameSource: this.frameSource, frameScaler: this.frameScaler, eventManager: this.eventManager, logger }, config.availableSources);
|
|
90
|
+
this.thumbnailer.sync(this.config.frameWorkerSettings.hqSnapshots === true);
|
|
91
|
+
this.audioLoop = new AudioDetectionLoop({
|
|
92
|
+
cameraId: config.cameraId,
|
|
93
|
+
getPlugin: () => this.plugins.get(SensorType.Audio),
|
|
94
|
+
getStreamUrl: () => this.config.audioStreamUrl,
|
|
95
|
+
getMinDecibels: () => this.config.detectionSettings.audio.minDecibels,
|
|
96
|
+
onResult: (sensorId, result) => this.handleAudioLoopResult(sensorId, result),
|
|
97
|
+
}, logger);
|
|
191
98
|
this.ptzAutotracker = new PtzAutotracker({
|
|
192
99
|
logger: this.logger,
|
|
193
100
|
proxy: this.proxy,
|
|
194
101
|
cameraId: this.config.cameraId,
|
|
195
102
|
settings: this.config.ptzAutotrack,
|
|
196
|
-
|
|
103
|
+
getFps: () => this.targetFps,
|
|
104
|
+
onSuppressionActivated: () => this.handleSuppressionActivated(),
|
|
197
105
|
});
|
|
198
106
|
this.cascadeUnsubscribe = this.cascade.onChange((event) => {
|
|
199
107
|
if (event.type === 'activated') {
|
|
@@ -209,8 +117,7 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
209
117
|
this.dwellUnsubscribe = this.dwell.onChange((event) => {
|
|
210
118
|
const isSensorTrigger = event.sensorId.startsWith('trigger:');
|
|
211
119
|
if (event.state === 'activated') {
|
|
212
|
-
//
|
|
213
|
-
// Sensor triggers (contact/switch/light) manage their own properties.
|
|
120
|
+
// sensor triggers (contact/switch/light) manage their own detected/blocked
|
|
214
121
|
if (!isSensorTrigger) {
|
|
215
122
|
this.writeSensorProperties(event.sensorId, {
|
|
216
123
|
detected: true,
|
|
@@ -230,8 +137,204 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
230
137
|
}
|
|
231
138
|
});
|
|
232
139
|
}
|
|
140
|
+
updateZones(zones) {
|
|
141
|
+
this.config.zones = zones;
|
|
142
|
+
this.pipeline.updateZones(zones);
|
|
143
|
+
}
|
|
144
|
+
updateLines(lines) {
|
|
145
|
+
this.config.lines = lines;
|
|
146
|
+
this.pipeline.updateLines(lines, this.videoAspectRatio);
|
|
147
|
+
}
|
|
148
|
+
updateDetectionSettings(settings) {
|
|
149
|
+
this.config.detectionSettings = settings;
|
|
150
|
+
this.pipeline.updateSettings(settings);
|
|
151
|
+
}
|
|
152
|
+
updatePtzAutotrackSettings(settings) {
|
|
153
|
+
this.config.ptzAutotrack = settings;
|
|
154
|
+
this.ptzAutotracker.updateSettings(settings);
|
|
155
|
+
}
|
|
156
|
+
updateFrameWorkerSettings(settings) {
|
|
157
|
+
this.config.frameWorkerSettings = settings;
|
|
158
|
+
this.thumbnailer.sync(settings.hqSnapshots === true);
|
|
159
|
+
}
|
|
160
|
+
updateInterfaceSettings(settings) {
|
|
161
|
+
this.config.interfaceSettings = settings;
|
|
162
|
+
if (this.config.lines.length > 0) {
|
|
163
|
+
this.pipeline.updateLines(this.config.lines, this.videoAspectRatio);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async dispose() {
|
|
167
|
+
await this.stopVideoLoop();
|
|
168
|
+
await this.audioLoop.stop();
|
|
169
|
+
await this.thumbnailer.stop();
|
|
170
|
+
this.ptzAutotracker.dispose();
|
|
171
|
+
this.cascadeUnsubscribe?.();
|
|
172
|
+
this.dwellUnsubscribe?.();
|
|
173
|
+
this.cascade.dispose();
|
|
174
|
+
this.dwell.dispose();
|
|
175
|
+
this.eventManager.destroy();
|
|
176
|
+
this.adHocVideoLoop = false;
|
|
177
|
+
this.frameScaler.dispose();
|
|
178
|
+
this.pipeline.cleanup();
|
|
179
|
+
this.currentDetectionState = {};
|
|
180
|
+
}
|
|
181
|
+
async onSensorAdded(sensor) {
|
|
182
|
+
if (DETECTION_SENSOR_TYPES.has(sensor.sensorType) && sensor.requiresFrames) {
|
|
183
|
+
await this.registerDetectionPluginInternal(sensor);
|
|
184
|
+
}
|
|
185
|
+
if (sensor.sensorType === SensorType.PTZ) {
|
|
186
|
+
await this.ptzAutotracker.bind({
|
|
187
|
+
pluginId: sensor.pluginId,
|
|
188
|
+
sensorId: sensor.sensorId,
|
|
189
|
+
capabilities: sensor.capabilities,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async onSensorRemoved(sensorId) {
|
|
194
|
+
await this.removeDetectionPluginBySensor(sensorId);
|
|
195
|
+
this.ptzAutotracker.unbind(sensorId);
|
|
196
|
+
const dwellKey = `trigger:${sensorId}`;
|
|
197
|
+
this.activeSensorTriggerTypes.delete(dwellKey);
|
|
198
|
+
this.dwell.clear(dwellKey);
|
|
199
|
+
this.dwell.clear(sensorId);
|
|
200
|
+
}
|
|
201
|
+
onSensorCapabilitiesChanged(sensorId, capabilities) {
|
|
202
|
+
this.ptzAutotracker.setCapabilities(sensorId, capabilities);
|
|
203
|
+
}
|
|
204
|
+
async reportSensorWrite(sensorId, sensorType, properties) {
|
|
205
|
+
if (sensorType === SensorType.Object) {
|
|
206
|
+
const filtered = this.applyExternalDetectionFilters(sensorType, properties);
|
|
207
|
+
// presence feed keeps the autotracker's return-home timer alive even while
|
|
208
|
+
// suppression gates everything below
|
|
209
|
+
const externalDetections = filtered.detections ?? [];
|
|
210
|
+
this.ptzAutotracker.handlePresenceDetections(externalDetections);
|
|
211
|
+
if (this.ptzAutotracker.suppressionActive)
|
|
212
|
+
return;
|
|
213
|
+
this.ingestDetectionResult(SensorType.Object, sensorId, filtered);
|
|
214
|
+
this.eventManager.processResults(this.buildSnapshot());
|
|
215
|
+
if (!this.plugins.hasFrameBasedSecondary()) {
|
|
216
|
+
// re-shoot the event thumbnail: smart-camera reports arrive after the
|
|
217
|
+
// motion-start shot, often before the subject fully entered the frame
|
|
218
|
+
if (filtered.detected === true && this.eventManager.hasActiveEvent()) {
|
|
219
|
+
this.thumbnailer.fetchEventThumbnailAsync();
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (this.processingExternalSecondary)
|
|
224
|
+
return; // previous RPC still running
|
|
225
|
+
this.processingExternalSecondary = true;
|
|
226
|
+
try {
|
|
227
|
+
// processExternal only adapts the shape, external boxes get no real tracking
|
|
228
|
+
const rawExternal = filtered.detections ?? [];
|
|
229
|
+
const objectDetections = this.pipeline.processExternal(rawExternal);
|
|
230
|
+
const peeked = this.frameSource.peekLatestFrame();
|
|
231
|
+
if (peeked) {
|
|
232
|
+
try {
|
|
233
|
+
const results = { timestamp: Date.now() };
|
|
234
|
+
await this.runSecondariesAndThumbnails(peeked.frame, objectDetections, results);
|
|
235
|
+
this.ingestResultsForAllSecondaries(results);
|
|
236
|
+
const snapshot = this.buildSnapshot();
|
|
237
|
+
if (results.thumbnails && results.thumbnails.length > 0) {
|
|
238
|
+
snapshot.thumbnails = results.thumbnails;
|
|
239
|
+
}
|
|
240
|
+
this.eventManager.processResults(snapshot);
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
await peeked[Symbol.asyncDispose]();
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const fetched = await this.frameSource.fetchSnapshotFrame();
|
|
248
|
+
if (!fetched)
|
|
249
|
+
return;
|
|
250
|
+
try {
|
|
251
|
+
const results = { timestamp: Date.now() };
|
|
252
|
+
const sceneJpeg = await this.secondaries.detectFullFrame(fetched.frame, results);
|
|
253
|
+
this.ingestResultsForAllSecondaries(results);
|
|
254
|
+
const snapshot = this.buildSnapshot();
|
|
255
|
+
if (sceneJpeg)
|
|
256
|
+
snapshot.eventThumbnail = sceneJpeg;
|
|
257
|
+
const wantsEventThumb = this.eventManager.needsThumbnail() || this.snapshotWillStartEvent(snapshot);
|
|
258
|
+
this.eventManager.processResults(snapshot);
|
|
259
|
+
if (wantsEventThumb) {
|
|
260
|
+
this.thumbnailer.upgradeEventThumbnailAsync();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
await fetched[Symbol.asyncDispose]();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
this.processingExternalSecondary = false;
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (sensorType === SensorType.Motion) {
|
|
273
|
+
// frame-diff is garbage while the PTZ repositions
|
|
274
|
+
if (this.ptzAutotracker.suppressionActive)
|
|
275
|
+
return;
|
|
276
|
+
const filtered = this.applyExternalDetectionFilters(sensorType, properties);
|
|
277
|
+
this.ingestDetectionResult(SensorType.Motion, sensorId, filtered);
|
|
278
|
+
this.eventManager.processResults(this.buildSnapshot());
|
|
279
|
+
this.thumbnailer.fetchEventThumbnailAsync();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
// classifier is multi-provider, buffer keying needs the owning pluginId
|
|
283
|
+
let pluginId;
|
|
284
|
+
if (sensorType === SensorType.Classifier) {
|
|
285
|
+
pluginId = this.plugins.getAll(SensorType.Classifier).find((p) => p.sensorId === sensorId)?.pluginId;
|
|
286
|
+
}
|
|
287
|
+
const filtered = this.applyExternalDetectionFilters(sensorType, properties);
|
|
288
|
+
this.ingestDetectionResult(sensorType, sensorId, filtered, pluginId);
|
|
289
|
+
this.eventManager.processResults(this.buildSnapshot());
|
|
290
|
+
}
|
|
291
|
+
reportSensorTrigger(sensorId, triggerType, action, _sustained, timeoutSeconds) {
|
|
292
|
+
// namespaced so trigger dwells don't overwrite the sensor's own detected/blocked
|
|
293
|
+
const dwellKey = `trigger:${sensorId}`;
|
|
294
|
+
if (action === 'activate') {
|
|
295
|
+
this.activeSensorTriggerTypes.set(dwellKey, triggerType);
|
|
296
|
+
}
|
|
297
|
+
this.dwell.refresh(dwellKey, timeoutSeconds);
|
|
298
|
+
if (action === 'activate' && this.cascadeEnabled) {
|
|
299
|
+
this.cascade.triggerMomentary(this.cascadeTimeoutSeconds);
|
|
300
|
+
}
|
|
301
|
+
this.eventManager.processResults(this.buildSnapshot());
|
|
302
|
+
// no loop frame to encode from, fetch async (no-op if already captured)
|
|
303
|
+
if (action === 'activate') {
|
|
304
|
+
this.thumbnailer.fetchEventThumbnailAsync();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
reconcileSensorTriggers(activeSensorIds) {
|
|
308
|
+
const activeSet = new Set(activeSensorIds);
|
|
309
|
+
for (const dwellKey of this.activeSensorTriggerTypes.keys()) {
|
|
310
|
+
const sensorId = dwellKey.slice('trigger:'.length);
|
|
311
|
+
if (!activeSet.has(sensorId)) {
|
|
312
|
+
this.activeSensorTriggerTypes.delete(dwellKey);
|
|
313
|
+
this.dwell.clear(dwellKey);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
hasPlugin(sensorType) {
|
|
318
|
+
return this.plugins.has(sensorType);
|
|
319
|
+
}
|
|
320
|
+
get cascadeEnabled() {
|
|
321
|
+
return this.config.detectionSettings.cascadeDetection !== false;
|
|
322
|
+
}
|
|
323
|
+
get cascadeTimeoutSeconds() {
|
|
324
|
+
return this.config.detectionSettings.cascadeTimeout ?? DEFAULT_CASCADE_TIMEOUT;
|
|
325
|
+
}
|
|
326
|
+
get motionResolution() {
|
|
327
|
+
return this.config.detectionSettings.motion.resolution;
|
|
328
|
+
}
|
|
329
|
+
get targetFps() {
|
|
330
|
+
return this.frameSource.fps;
|
|
331
|
+
}
|
|
332
|
+
get videoAspectRatio() {
|
|
333
|
+
const [w, h] = this.config.interfaceSettings.aspectRatio.split(':').map(Number);
|
|
334
|
+
return w / h;
|
|
335
|
+
}
|
|
233
336
|
handleCascadeDeactivated() {
|
|
234
|
-
//
|
|
337
|
+
// clear cascade-gated buffer slots, keep motion/audio
|
|
235
338
|
const cs = this.currentDetectionState;
|
|
236
339
|
cs.object = undefined;
|
|
237
340
|
cs.face = undefined;
|
|
@@ -240,31 +343,46 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
240
343
|
cs.clip = undefined;
|
|
241
344
|
cs.lineCrossings = undefined;
|
|
242
345
|
cs.cascadeTriggered = undefined;
|
|
243
|
-
//
|
|
346
|
+
// publish empty detections so the UI clears stale bboxes
|
|
244
347
|
const clearTargets = [];
|
|
245
|
-
for (const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
348
|
+
for (const type of [SensorType.Object, SensorType.Face, SensorType.LicensePlate]) {
|
|
349
|
+
const plugin = this.plugins.get(type);
|
|
350
|
+
if (plugin)
|
|
351
|
+
clearTargets.push(plugin.sensorId);
|
|
249
352
|
}
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
for (const plugin of classifierPlugins.values()) {
|
|
253
|
-
clearTargets.push({ sensorId: plugin.sensorId, type: SensorType.Classifier });
|
|
254
|
-
}
|
|
353
|
+
for (const plugin of this.plugins.getAll(SensorType.Classifier)) {
|
|
354
|
+
clearTargets.push(plugin.sensorId);
|
|
255
355
|
}
|
|
256
|
-
for (const
|
|
356
|
+
for (const sensorId of clearTargets) {
|
|
257
357
|
this.writeSensorProperties(sensorId, { detected: false, detections: [] });
|
|
358
|
+
this.secondaryBboxSeen.delete(sensorId);
|
|
258
359
|
}
|
|
259
|
-
//
|
|
360
|
+
// segment ends here, the event itself continues while motion is active
|
|
260
361
|
this.eventManager.processResults(this.buildSnapshot());
|
|
261
362
|
}
|
|
262
363
|
handleEventEnded() {
|
|
263
|
-
this.
|
|
264
|
-
|
|
364
|
+
if (this.config.detectionSettings.object.suppressStatic ?? true) {
|
|
365
|
+
this.stationary.retainAcrossEvent((trackIds) => this.pipeline.retainTracks(trackIds));
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
this.pipeline.cleanup();
|
|
369
|
+
this.stationary.clear();
|
|
370
|
+
}
|
|
371
|
+
this.stationary.resetEventState();
|
|
265
372
|
this.activeSensorTriggerTypes.clear();
|
|
266
373
|
this.currentDetectionState = {};
|
|
267
374
|
}
|
|
375
|
+
handleAudioLoopResult(sensorId, result) {
|
|
376
|
+
// only the cascade trigger lives here, the audio dwell is handled in ingest
|
|
377
|
+
if (result.detected && this.cascadeEnabled) {
|
|
378
|
+
this.cascade.triggerMomentary(this.config.detectionSettings.audio.timeout);
|
|
379
|
+
}
|
|
380
|
+
this.ingestDetectionResult(SensorType.Audio, sensorId, {
|
|
381
|
+
detected: result.detected,
|
|
382
|
+
detections: result.detections ?? [],
|
|
383
|
+
...(result.decibels !== undefined ? { decibels: result.decibels } : {}),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
268
386
|
writeSensorProperties(sensorId, properties) {
|
|
269
387
|
const msg = {
|
|
270
388
|
sensorId,
|
|
@@ -314,10 +432,8 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
314
432
|
const raw = properties.detections;
|
|
315
433
|
if (!Array.isArray(raw) || raw.length === 0)
|
|
316
434
|
return properties;
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
// third-party plugins can still send box-less writes) — the zone filter
|
|
320
|
-
// and rust merge assume a box on every detection.
|
|
435
|
+
// older/third-party plugins can send box-less detections, but the zone
|
|
436
|
+
// filter and rust merge assume a box on every detection
|
|
321
437
|
const detections = raw.map((detection) => (detection.box ? detection : { ...detection, box: { x: 0, y: 0, width: 1, height: 1 } }));
|
|
322
438
|
let filtered;
|
|
323
439
|
switch (sensorType) {
|
|
@@ -333,7 +449,7 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
333
449
|
filtered = this.pipeline.runZoneFilter(detections);
|
|
334
450
|
break;
|
|
335
451
|
default:
|
|
336
|
-
return { ...properties, detections }; //
|
|
452
|
+
return { ...properties, detections }; // audio etc, no zone filter
|
|
337
453
|
}
|
|
338
454
|
return {
|
|
339
455
|
...properties,
|
|
@@ -341,13 +457,10 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
341
457
|
detected: filtered.length > 0,
|
|
342
458
|
};
|
|
343
459
|
}
|
|
344
|
-
// PTZ-suppression gating happens upstream
|
|
345
|
-
// belongs in the full pipeline or autotracker-only.
|
|
460
|
+
// PTZ-suppression gating happens upstream, callers decide what gets in here
|
|
346
461
|
ingestDetectionResult(sensorType, sensorId, properties, pluginId) {
|
|
347
462
|
this.updateBufferForType(sensorType, properties, pluginId);
|
|
348
463
|
const detected = properties.detected === true;
|
|
349
|
-
// Motion/Audio: dwell drives the detected/blocked/lastTriggered lifecycle
|
|
350
|
-
// entirely (see dwell.onChange in the constructor); motion also cascades.
|
|
351
464
|
if (sensorType === SensorType.Motion && detected) {
|
|
352
465
|
const motionTimeout = this.config.detectionSettings.motion.timeout;
|
|
353
466
|
this.dwell.refresh(sensorId, motionTimeout);
|
|
@@ -363,51 +476,25 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
363
476
|
}
|
|
364
477
|
}
|
|
365
478
|
else if (sensorType === SensorType.Object && detected) {
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
// afterwards they're in seenStationaryTracks and don't count, so parked
|
|
369
|
-
// cars/bikes can't keep events alive forever. Moving tracks always count.
|
|
370
|
-
// The set is cleared on cascade deactivate (event boundary).
|
|
479
|
+
// the dwell bridges single missed frames; anchored stationary tracks
|
|
480
|
+
// don't refresh it, so parked objects can't hold events open
|
|
371
481
|
const detections = properties.detections ?? [];
|
|
372
|
-
|
|
373
|
-
for (const t of detections) {
|
|
374
|
-
if (t.trackLost)
|
|
375
|
-
continue;
|
|
376
|
-
// No trackId = external smart-camera write (edge-triggered presence
|
|
377
|
-
// report, no tracking) — there is no speed/identity to judge
|
|
378
|
-
// stationarity by, so it always counts as active.
|
|
379
|
-
if (t.trackId === undefined) {
|
|
380
|
-
hasActiveTrack = true;
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
const isStationary = (t.trackSpeed ?? 0) < STATIONARY_SPEED_THRESHOLD;
|
|
384
|
-
if (isStationary) {
|
|
385
|
-
if (t.trackId !== undefined && !this.seenStationaryTracks.has(t.trackId)) {
|
|
386
|
-
this.seenStationaryTracks.add(t.trackId);
|
|
387
|
-
hasActiveTrack = true;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
else {
|
|
391
|
-
hasActiveTrack = true;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
if (hasActiveTrack) {
|
|
482
|
+
if (this.stationary.evaluate(detections)) {
|
|
395
483
|
this.dwell.refresh(sensorId, OBJECT_DWELL_SECONDS);
|
|
396
484
|
}
|
|
397
485
|
else {
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
// still gets the bboxes via the property publish below.
|
|
486
|
+
// clear detected + buffer so no segment opens, the UI still gets
|
|
487
|
+
// the bboxes via the property publish below
|
|
401
488
|
properties.detected = false;
|
|
402
489
|
if (this.currentDetectionState.object) {
|
|
403
490
|
this.currentDetectionState.object.detected = false;
|
|
404
491
|
this.currentDetectionState.object.detections = [];
|
|
405
492
|
}
|
|
493
|
+
this.stationary.logSuppressedOnce(detections);
|
|
406
494
|
}
|
|
407
495
|
}
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
// so strip them from the publish; dwell.onChange is the only writer.
|
|
496
|
+
// dwell owns detected/blocked/lastTriggered for these types, a no-detection
|
|
497
|
+
// frame would overwrite the dwell-set state, so strip before publishing
|
|
411
498
|
let publishProps = properties;
|
|
412
499
|
if (sensorType === SensorType.Motion || sensorType === SensorType.Audio || sensorType === SensorType.Object) {
|
|
413
500
|
const { detected: _d, blocked: _b, lastTriggered: _t, ...rest } = properties;
|
|
@@ -416,7 +503,16 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
416
503
|
if (Object.keys(publishProps).length > 0) {
|
|
417
504
|
this.writeSensorProperties(sensorId, publishProps);
|
|
418
505
|
}
|
|
419
|
-
|
|
506
|
+
if (sensorType === SensorType.Face || sensorType === SensorType.LicensePlate || sensorType === SensorType.Classifier) {
|
|
507
|
+
const detections = properties.detections ?? [];
|
|
508
|
+
if (detections.length > 0) {
|
|
509
|
+
this.secondaryBboxSeen.set(sensorId, Date.now());
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
this.secondaryBboxSeen.delete(sensorId);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// no EventManager tick here, processDetection flushes once per frame
|
|
420
516
|
}
|
|
421
517
|
updateBufferForType(sensorType, properties, pluginId) {
|
|
422
518
|
const cs = this.currentDetectionState;
|
|
@@ -453,381 +549,104 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
453
549
|
break;
|
|
454
550
|
}
|
|
455
551
|
}
|
|
456
|
-
|
|
457
|
-
this.
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
this.
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
this.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
await this.hqSnapshotSource?.stop();
|
|
486
|
-
this.ptzAutotracker.dispose();
|
|
487
|
-
this.cascadeUnsubscribe?.();
|
|
488
|
-
this.dwellUnsubscribe?.();
|
|
489
|
-
this.cascade.dispose();
|
|
490
|
-
this.dwell.dispose();
|
|
491
|
-
this.eventManager.destroy();
|
|
492
|
-
this.adHocVideoLoop = false;
|
|
493
|
-
this.frameScaler.dispose();
|
|
494
|
-
this.pipeline.cleanup();
|
|
495
|
-
this.currentDetectionState = {};
|
|
496
|
-
}
|
|
497
|
-
async onSensorAdded(sensor) {
|
|
498
|
-
if (DETECTION_SENSOR_TYPES.has(sensor.sensorType) && sensor.requiresFrames) {
|
|
499
|
-
await this.registerDetectionPluginInternal(sensor);
|
|
500
|
-
}
|
|
501
|
-
if (sensor.sensorType === SensorType.PTZ) {
|
|
502
|
-
await this.ptzAutotracker.bind({
|
|
503
|
-
pluginId: sensor.pluginId,
|
|
504
|
-
sensorId: sensor.sensorId,
|
|
505
|
-
capabilities: sensor.capabilities,
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
async onSensorRemoved(sensorId) {
|
|
510
|
-
await this.removeDetectionPluginBySensor(sensorId);
|
|
511
|
-
this.ptzAutotracker.unbind(sensorId);
|
|
512
|
-
const dwellKey = `trigger:${sensorId}`;
|
|
513
|
-
this.activeSensorTriggerTypes.delete(dwellKey);
|
|
514
|
-
this.dwell.clear(dwellKey);
|
|
515
|
-
this.dwell.clear(sensorId);
|
|
516
|
-
}
|
|
517
|
-
onSensorCapabilitiesChanged(sensorId, capabilities) {
|
|
518
|
-
this.ptzAutotracker.setCapabilities(sensorId, capabilities);
|
|
519
|
-
}
|
|
520
|
-
async registerDetectionPluginInternal(sensor) {
|
|
521
|
-
// Wait for any in-progress stop to complete before starting a new loop
|
|
522
|
-
if (this.videoStopPromise)
|
|
523
|
-
await this.videoStopPromise;
|
|
524
|
-
if (this.audioStopPromise)
|
|
525
|
-
await this.audioStopPromise;
|
|
526
|
-
const wasVideoNeeded = this.shouldVideoBeActive();
|
|
527
|
-
const wasAudioNeeded = this.shouldAudioBeActive();
|
|
528
|
-
const namespaces = NamespaceManager.sensorProviderNamespaces(sensor.pluginId, this.config.cameraId, sensor.sensorId);
|
|
529
|
-
const sensorProxy = this.proxy.createProxy(namespaces.sensorRpc);
|
|
530
|
-
const pluginInfo = {
|
|
531
|
-
pluginId: sensor.pluginId,
|
|
532
|
-
sensorId: sensor.sensorId,
|
|
533
|
-
sensorType: sensor.sensorType,
|
|
534
|
-
requiresFrames: sensor.requiresFrames,
|
|
535
|
-
modelSpec: sensor.modelSpec,
|
|
536
|
-
proxy: sensorProxy,
|
|
537
|
-
};
|
|
538
|
-
if (this.isMultiProvider(sensor.sensorType)) {
|
|
539
|
-
if (!this.multiPlugins.has(sensor.sensorType)) {
|
|
540
|
-
this.multiPlugins.set(sensor.sensorType, new Map());
|
|
541
|
-
}
|
|
542
|
-
const typePlugins = this.multiPlugins.get(sensor.sensorType);
|
|
543
|
-
if (typePlugins.has(sensor.pluginId))
|
|
544
|
-
return;
|
|
545
|
-
typePlugins.set(sensor.pluginId, pluginInfo);
|
|
546
|
-
this.logger.trace(`Multi-provider plugin registered: ${sensor.pluginId} for ${sensor.sensorType}`);
|
|
547
|
-
}
|
|
548
|
-
else {
|
|
549
|
-
if (this.singlePlugins.has(sensor.sensorType))
|
|
550
|
-
return;
|
|
551
|
-
this.singlePlugins.set(sensor.sensorType, pluginInfo);
|
|
552
|
-
this.logger.trace(`Plugin registered: ${sensor.pluginId} for ${sensor.sensorType}`);
|
|
553
|
-
}
|
|
554
|
-
if (!wasVideoNeeded && this.shouldVideoBeActive()) {
|
|
555
|
-
this.adHocVideoLoop = false;
|
|
556
|
-
this.startVideoLoop();
|
|
557
|
-
}
|
|
558
|
-
if (!wasAudioNeeded && this.shouldAudioBeActive()) {
|
|
559
|
-
this.startAudioLoop();
|
|
560
|
-
}
|
|
561
|
-
if (this.cascade.isActive) {
|
|
562
|
-
this.startAdHocVideoLoopIfNeeded();
|
|
563
|
-
}
|
|
552
|
+
async registerDetectionPluginInternal(sensor) {
|
|
553
|
+
if (this.videoStopPromise)
|
|
554
|
+
await this.videoStopPromise;
|
|
555
|
+
await this.audioLoop.waitForStop();
|
|
556
|
+
const wasVideoNeeded = this.plugins.shouldVideoBeActive();
|
|
557
|
+
const wasAudioNeeded = this.plugins.shouldAudioBeActive();
|
|
558
|
+
const namespaces = NamespaceManager.sensorProviderNamespaces(sensor.pluginId, this.config.cameraId, sensor.sensorId);
|
|
559
|
+
const sensorProxy = this.proxy.createProxy(namespaces.sensorRpc);
|
|
560
|
+
const registered = this.plugins.register({
|
|
561
|
+
pluginId: sensor.pluginId,
|
|
562
|
+
sensorId: sensor.sensorId,
|
|
563
|
+
sensorType: sensor.sensorType,
|
|
564
|
+
requiresFrames: sensor.requiresFrames,
|
|
565
|
+
modelSpec: sensor.modelSpec,
|
|
566
|
+
proxy: sensorProxy,
|
|
567
|
+
});
|
|
568
|
+
if (!registered)
|
|
569
|
+
return;
|
|
570
|
+
this.logger.trace(`Plugin registered: ${sensor.pluginId} for ${sensor.sensorType}`);
|
|
571
|
+
if (!wasVideoNeeded && this.plugins.shouldVideoBeActive()) {
|
|
572
|
+
this.adHocVideoLoop = false;
|
|
573
|
+
this.startVideoLoop();
|
|
574
|
+
}
|
|
575
|
+
if (!wasAudioNeeded && this.plugins.shouldAudioBeActive()) {
|
|
576
|
+
this.audioLoop.start();
|
|
577
|
+
}
|
|
578
|
+
if (this.cascade.isActive) {
|
|
579
|
+
this.startAdHocVideoLoopIfNeeded();
|
|
580
|
+
}
|
|
564
581
|
}
|
|
565
582
|
async removeDetectionPluginBySensor(sensorId) {
|
|
566
|
-
const wasVideoNeeded = this.shouldVideoBeActive();
|
|
567
|
-
const wasAudioNeeded = this.shouldAudioBeActive();
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (plugin.sensorId === sensorId) {
|
|
571
|
-
this.singlePlugins.delete(sensorType);
|
|
572
|
-
removed = true;
|
|
573
|
-
this.logger.trace(`Plugin unregistered: ${plugin.pluginId} (${sensorType})`);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
for (const [sensorType, typePlugins] of this.multiPlugins) {
|
|
577
|
-
for (const [pluginId, plugin] of typePlugins) {
|
|
578
|
-
if (plugin.sensorId === sensorId) {
|
|
579
|
-
typePlugins.delete(pluginId);
|
|
580
|
-
removed = true;
|
|
581
|
-
this.logger.trace(`Multi-provider plugin unregistered: ${pluginId} (${sensorType})`);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
if (typePlugins.size === 0)
|
|
585
|
-
this.multiPlugins.delete(sensorType);
|
|
586
|
-
}
|
|
587
|
-
if (!removed)
|
|
583
|
+
const wasVideoNeeded = this.plugins.shouldVideoBeActive();
|
|
584
|
+
const wasAudioNeeded = this.plugins.shouldAudioBeActive();
|
|
585
|
+
const removed = this.plugins.removeBySensor(sensorId);
|
|
586
|
+
if (removed.length === 0)
|
|
588
587
|
return;
|
|
589
|
-
|
|
588
|
+
for (const plugin of removed) {
|
|
589
|
+
this.logger.trace(`Plugin unregistered: ${plugin.pluginId} (${plugin.sensorType})`);
|
|
590
|
+
}
|
|
591
|
+
if (!this.plugins.has(SensorType.Object)) {
|
|
590
592
|
this.adHocVideoLoop = false;
|
|
591
593
|
}
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
if (this.adHocVideoLoop && !this.needsAdHocLoop()) {
|
|
594
|
+
// needsAdHocLoop, not shouldVideoBeActive: a loop still needed by a
|
|
595
|
+
// frame-based object plugin must survive plugin hot-reloads
|
|
596
|
+
if (this.adHocVideoLoop && !this.plugins.needsAdHocLoop()) {
|
|
595
597
|
this.adHocVideoLoop = false;
|
|
596
598
|
this.logger.debug('Stopping ad-hoc video loop — last frame-based consumer removed');
|
|
597
599
|
this.stopVideoLoop();
|
|
598
600
|
}
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
if (wasVideoNeeded && !this.shouldVideoBeActive() && !this.adHocVideoLoop) {
|
|
601
|
+
// await, otherwise a rapid re-register starts a second loop while the
|
|
602
|
+
// old one is still shutting down
|
|
603
|
+
if (wasVideoNeeded && !this.plugins.shouldVideoBeActive() && !this.adHocVideoLoop) {
|
|
602
604
|
await this.stopVideoLoop();
|
|
603
605
|
}
|
|
604
|
-
if (wasAudioNeeded && !this.shouldAudioBeActive()) {
|
|
605
|
-
await this.
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
async reportSensorWrite(sensorId, sensorType, properties) {
|
|
609
|
-
if (sensorType === SensorType.Object) {
|
|
610
|
-
const filtered = this.applyExternalDetectionFilters(sensorType, properties);
|
|
611
|
-
// Always feed the autotracker (even during suppression, so it can decide
|
|
612
|
-
// when to stop). External writes are presence-only — the presence channel
|
|
613
|
-
// holds the return-to-home timer without driving the motor.
|
|
614
|
-
const externalDetections = filtered.detections ?? [];
|
|
615
|
-
this.ptzAutotracker.handlePresenceDetections(externalDetections);
|
|
616
|
-
// During suppression tracker/pipeline/events stay gated — no stale
|
|
617
|
-
// bboxes or false events from the camera pose change.
|
|
618
|
-
if (this.ptzAutotracker.suppressionActive)
|
|
619
|
-
return;
|
|
620
|
-
this.ingestDetectionResult(SensorType.Object, sensorId, filtered);
|
|
621
|
-
this.eventManager.processResults(this.buildSnapshot());
|
|
622
|
-
if (!this.hasFrameBasedSecondary()) {
|
|
623
|
-
// Smart-camera objects arrive after the motion-start thumbnail was
|
|
624
|
-
// shot — usually before the subject fully entered the frame. Re-shoot
|
|
625
|
-
// so the event picture actually shows what was detected.
|
|
626
|
-
if (filtered.detected === true && this.eventManager.hasActiveEvent()) {
|
|
627
|
-
this.fetchEventThumbnailAsync();
|
|
628
|
-
}
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
if (this.processingExternalSecondary)
|
|
632
|
-
return; // busy-skip: previous RPC still running
|
|
633
|
-
this.processingExternalSecondary = true;
|
|
634
|
-
try {
|
|
635
|
-
// Wrap external boxes as TrackedDetection (shape adapter, no real
|
|
636
|
-
// tracking) so the secondary pipeline treats them like loop output.
|
|
637
|
-
const rawExternal = filtered.detections ?? [];
|
|
638
|
-
const objectDetections = this.pipeline.processExternal(rawExternal);
|
|
639
|
-
// Fast path: loop is running and has a frame → crop mode.
|
|
640
|
-
const peeked = this.frameSource.peekLatestFrame();
|
|
641
|
-
if (peeked) {
|
|
642
|
-
try {
|
|
643
|
-
const results = { timestamp: Date.now() };
|
|
644
|
-
await this.runSecondariesAndThumbnails(peeked.frame, objectDetections, results);
|
|
645
|
-
this.ingestResultsForAllSecondaries(results);
|
|
646
|
-
const snapshot = this.buildSnapshot();
|
|
647
|
-
if (results.thumbnails && results.thumbnails.length > 0) {
|
|
648
|
-
snapshot.thumbnails = results.thumbnails;
|
|
649
|
-
}
|
|
650
|
-
this.eventManager.processResults(snapshot);
|
|
651
|
-
}
|
|
652
|
-
finally {
|
|
653
|
-
await peeked[Symbol.asyncDispose]();
|
|
654
|
-
}
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
// Slow path: no loop frame yet → snapshot fetch, secondaries run
|
|
658
|
-
// full-frame (no per-detection crops).
|
|
659
|
-
const fetched = await this.frameSource.fetchSnapshotFrame();
|
|
660
|
-
if (!fetched)
|
|
661
|
-
return;
|
|
662
|
-
try {
|
|
663
|
-
const results = { timestamp: Date.now() };
|
|
664
|
-
const sceneJpeg = await this.runSecondariesFullFrame(fetched.frame, results);
|
|
665
|
-
this.ingestResultsForAllSecondaries(results);
|
|
666
|
-
const snapshot = this.buildSnapshot();
|
|
667
|
-
if (sceneJpeg)
|
|
668
|
-
snapshot.eventThumbnail = sceneJpeg;
|
|
669
|
-
const wantsEventThumb = this.eventManager.needsThumbnail() || this.snapshotWillStartEvent(snapshot);
|
|
670
|
-
this.eventManager.processResults(snapshot);
|
|
671
|
-
if (wantsEventThumb) {
|
|
672
|
-
this.upgradeEventThumbnailAsync();
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
finally {
|
|
676
|
-
await fetched[Symbol.asyncDispose]();
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
finally {
|
|
680
|
-
this.processingExternalSecondary = false;
|
|
681
|
-
}
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
if (sensorType === SensorType.Motion) {
|
|
685
|
-
// Fully gated during suppression — frame-diff is fake while the camera repositions.
|
|
686
|
-
if (this.ptzAutotracker.suppressionActive)
|
|
687
|
-
return;
|
|
688
|
-
const filtered = this.applyExternalDetectionFilters(sensorType, properties);
|
|
689
|
-
this.ingestDetectionResult(SensorType.Motion, sensorId, filtered);
|
|
690
|
-
this.eventManager.processResults(this.buildSnapshot());
|
|
691
|
-
this.fetchEventThumbnailAsync();
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
// External secondaries: direct ingest, no pixel work. Classifier (multi-
|
|
695
|
-
// provider) needs the owning pluginId for buffer keying.
|
|
696
|
-
let pluginId;
|
|
697
|
-
if (sensorType === SensorType.Classifier) {
|
|
698
|
-
const typePlugins = this.multiPlugins.get(SensorType.Classifier);
|
|
699
|
-
if (typePlugins) {
|
|
700
|
-
for (const plugin of typePlugins.values()) {
|
|
701
|
-
if (plugin.sensorId === sensorId) {
|
|
702
|
-
pluginId = plugin.pluginId;
|
|
703
|
-
break;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
const filtered = this.applyExternalDetectionFilters(sensorType, properties);
|
|
709
|
-
this.ingestDetectionResult(sensorType, sensorId, filtered, pluginId);
|
|
710
|
-
this.eventManager.processResults(this.buildSnapshot());
|
|
711
|
-
}
|
|
712
|
-
reportSensorTrigger(sensorId, triggerType, action, _sustained, timeoutSeconds) {
|
|
713
|
-
// Namespaced dwell key so trigger dwells don't overwrite the sensor's own
|
|
714
|
-
// detected/blocked properties (contact/switch/light manage those themselves).
|
|
715
|
-
const dwellKey = `trigger:${sensorId}`;
|
|
716
|
-
if (action === 'activate') {
|
|
717
|
-
this.activeSensorTriggerTypes.set(dwellKey, triggerType);
|
|
718
|
-
}
|
|
719
|
-
this.dwell.refresh(dwellKey, timeoutSeconds);
|
|
720
|
-
if (action === 'activate' && this.cascadeEnabled) {
|
|
721
|
-
this.cascade.triggerMomentary(this.cascadeTimeoutSeconds);
|
|
722
|
-
}
|
|
723
|
-
this.eventManager.processResults(this.buildSnapshot());
|
|
724
|
-
// Trigger-only path (doorbell, contact) has no loop frame to encode from —
|
|
725
|
-
// fetch the event thumb async; no-op if one was already captured.
|
|
726
|
-
if (action === 'activate') {
|
|
727
|
-
this.fetchEventThumbnailAsync();
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
reconcileSensorTriggers(activeSensorIds) {
|
|
731
|
-
const activeSet = new Set(activeSensorIds);
|
|
732
|
-
for (const dwellKey of this.activeSensorTriggerTypes.keys()) {
|
|
733
|
-
const sensorId = dwellKey.slice('trigger:'.length);
|
|
734
|
-
if (!activeSet.has(sensorId)) {
|
|
735
|
-
this.activeSensorTriggerTypes.delete(dwellKey);
|
|
736
|
-
this.dwell.clear(dwellKey);
|
|
737
|
-
}
|
|
606
|
+
if (wasAudioNeeded && !this.plugins.shouldAudioBeActive()) {
|
|
607
|
+
await this.audioLoop.stop();
|
|
738
608
|
}
|
|
739
609
|
}
|
|
740
|
-
|
|
610
|
+
handleSuppressionActivated() {
|
|
741
611
|
const cs = this.currentDetectionState;
|
|
742
612
|
cs.motion = undefined;
|
|
743
|
-
|
|
744
|
-
|
|
613
|
+
cs.object = undefined;
|
|
614
|
+
// motion is garbage during ego-motion and face/lpd inference pauses, their
|
|
615
|
+
// boxes would float frozen through the pan; object keeps publishing live
|
|
616
|
+
const motionPlugin = this.plugins.get(SensorType.Motion);
|
|
617
|
+
if (motionPlugin)
|
|
745
618
|
this.writeSensorProperties(motionPlugin.sensorId, { detections: [] });
|
|
619
|
+
for (const type of [SensorType.Face, SensorType.LicensePlate]) {
|
|
620
|
+
const plugin = this.plugins.get(type);
|
|
621
|
+
if (plugin)
|
|
622
|
+
this.clearSecondaryBboxes(plugin.sensorId);
|
|
746
623
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if (this.isMultiProvider(sensorType)) {
|
|
750
|
-
const typePlugins = this.multiPlugins.get(sensorType);
|
|
751
|
-
return typePlugins !== undefined && typePlugins.size > 0;
|
|
624
|
+
for (const plugin of this.plugins.getAll(SensorType.Classifier)) {
|
|
625
|
+
this.clearSecondaryBboxes(plugin.sensorId);
|
|
752
626
|
}
|
|
753
|
-
|
|
627
|
+
// image-space anchors don't survive a camera move, they re-form after
|
|
628
|
+
this.stationary.dropForCameraMove();
|
|
754
629
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
getPluginsForType(sensorType) {
|
|
759
|
-
if (this.isMultiProvider(sensorType)) {
|
|
760
|
-
const typePlugins = this.multiPlugins.get(sensorType);
|
|
761
|
-
return typePlugins ? Array.from(typePlugins.values()) : [];
|
|
762
|
-
}
|
|
763
|
-
const plugin = this.singlePlugins.get(sensorType);
|
|
764
|
-
return plugin ? [plugin] : [];
|
|
630
|
+
clearSecondaryBboxes(sensorId) {
|
|
631
|
+
this.writeSensorProperties(sensorId, { detections: [] });
|
|
632
|
+
this.secondaryBboxSeen.delete(sensorId);
|
|
765
633
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
for (const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
return allPlugins;
|
|
775
|
-
}
|
|
776
|
-
hasAnyPlugins() {
|
|
777
|
-
if (this.singlePlugins.size > 0)
|
|
778
|
-
return true;
|
|
779
|
-
for (const typePlugins of this.multiPlugins.values()) {
|
|
780
|
-
if (typePlugins.size > 0)
|
|
781
|
-
return true;
|
|
634
|
+
clearStaleSecondaryBboxes() {
|
|
635
|
+
if (this.secondaryBboxSeen.size === 0)
|
|
636
|
+
return;
|
|
637
|
+
const now = Date.now();
|
|
638
|
+
for (const [sensorId, ts] of this.secondaryBboxSeen) {
|
|
639
|
+
if (now - ts > SECONDARY_BBOX_TTL_MS) {
|
|
640
|
+
this.clearSecondaryBboxes(sensorId);
|
|
641
|
+
}
|
|
782
642
|
}
|
|
783
|
-
return false;
|
|
784
|
-
}
|
|
785
|
-
isRunning() {
|
|
786
|
-
return this.loopRunning;
|
|
787
|
-
}
|
|
788
|
-
isStreaming() {
|
|
789
|
-
return this.frameSource.isStreaming;
|
|
790
|
-
}
|
|
791
|
-
hasSecondaryModelSpec(spec) {
|
|
792
|
-
return spec !== undefined && 'triggerLabels' in spec && Array.isArray(spec.triggerLabels);
|
|
793
|
-
}
|
|
794
|
-
isVideoInputSpec(spec) {
|
|
795
|
-
return spec != null && typeof spec === 'object' && 'width' in spec && 'height' in spec && 'format' in spec;
|
|
796
|
-
}
|
|
797
|
-
isOnEdge(box) {
|
|
798
|
-
return box.x < 0.02 || box.y < 0.02 || box.x + box.width > 0.98 || box.y + box.height > 0.98;
|
|
799
|
-
}
|
|
800
|
-
isAudioModelSpec(spec) {
|
|
801
|
-
return spec !== undefined && 'input' in spec && typeof spec.input === 'object' && 'sampleRate' in spec.input;
|
|
802
|
-
}
|
|
803
|
-
isMultiProvider(sensorType) {
|
|
804
|
-
return SENSOR_TYPE_CONFIG[sensorType]?.multiProvider === true;
|
|
805
|
-
}
|
|
806
|
-
// Video stream is permanently needed when: (1) frame-based motion detection, or
|
|
807
|
-
// (2) audio plugin + object plugin for continuous audio-triggered cascade.
|
|
808
|
-
// External motion (requiresFrames=false) and sensor cascade triggers use an ad-hoc
|
|
809
|
-
// video loop — started on-demand, stopped when cascade ends. This prevents blocking
|
|
810
|
-
// event-based cameras (e.g. Ring) that stop firing events while the stream is consumed.
|
|
811
|
-
shouldVideoBeActive() {
|
|
812
|
-
const motionPlugin = this.singlePlugins.get(SensorType.Motion);
|
|
813
|
-
const objectPlugin = this.singlePlugins.get(SensorType.Object);
|
|
814
|
-
const audioPlugin = this.singlePlugins.get(SensorType.Audio);
|
|
815
|
-
if (motionPlugin?.requiresFrames)
|
|
816
|
-
return true;
|
|
817
|
-
if (audioPlugin && objectPlugin?.requiresFrames)
|
|
818
|
-
return true;
|
|
819
|
-
return false;
|
|
820
|
-
}
|
|
821
|
-
shouldAudioBeActive() {
|
|
822
|
-
const audioPlugin = this.singlePlugins.get(SensorType.Audio);
|
|
823
|
-
return audioPlugin?.requiresFrames === true;
|
|
824
643
|
}
|
|
825
644
|
startVideoLoop() {
|
|
826
645
|
if (this.loopRunning || this.videoStopPromise)
|
|
827
646
|
return;
|
|
828
647
|
this.logger.debug('Starting video detection loop');
|
|
829
648
|
this.loopRunning = true;
|
|
830
|
-
this.
|
|
649
|
+
this.videoBackoff.reset();
|
|
831
650
|
this.loopPromise = this.runDetectionLoop();
|
|
832
651
|
}
|
|
833
652
|
async stopVideoLoop() {
|
|
@@ -851,318 +670,27 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
851
670
|
this.videoStopPromise = undefined;
|
|
852
671
|
}
|
|
853
672
|
}
|
|
854
|
-
startAudioLoop() {
|
|
855
|
-
if (this.audioLoopRunning || this.audioStopPromise)
|
|
856
|
-
return;
|
|
857
|
-
this.logger.debug('Starting audio detection loop');
|
|
858
|
-
this.audioLoopRunning = true;
|
|
859
|
-
this.audioReconnectCount = 0;
|
|
860
|
-
this.audioLoopPromise = this.runAudioDetectionLoop();
|
|
861
|
-
}
|
|
862
|
-
async stopAudioLoop() {
|
|
863
|
-
if (!this.audioLoopRunning) {
|
|
864
|
-
if (this.audioStopPromise)
|
|
865
|
-
await this.audioStopPromise;
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
this.logger.debug('Stopping audio detection loop');
|
|
869
|
-
this.audioLoopRunning = false;
|
|
870
|
-
const doStop = async () => {
|
|
871
|
-
await this.audioSource?.stop();
|
|
872
|
-
await this.audioLoopPromise;
|
|
873
|
-
this.audioLoopPromise = undefined;
|
|
874
|
-
};
|
|
875
|
-
this.audioStopPromise = doStop();
|
|
876
|
-
try {
|
|
877
|
-
await this.audioStopPromise;
|
|
878
|
-
}
|
|
879
|
-
finally {
|
|
880
|
-
this.audioStopPromise = undefined;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
hasFrameBasedSecondary() {
|
|
884
|
-
const face = this.singlePlugins.get(SensorType.Face);
|
|
885
|
-
if (face?.requiresFrames)
|
|
886
|
-
return true;
|
|
887
|
-
const lpd = this.singlePlugins.get(SensorType.LicensePlate);
|
|
888
|
-
if (lpd?.requiresFrames)
|
|
889
|
-
return true;
|
|
890
|
-
const clip = this.singlePlugins.get(SensorType.Clip);
|
|
891
|
-
if (clip?.requiresFrames)
|
|
892
|
-
return true;
|
|
893
|
-
const classifiers = this.multiPlugins.get(SensorType.Classifier);
|
|
894
|
-
if (classifiers) {
|
|
895
|
-
for (const p of classifiers.values()) {
|
|
896
|
-
if (p.requiresFrames)
|
|
897
|
-
return true;
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
return false;
|
|
901
|
-
}
|
|
902
|
-
needsAdHocLoop() {
|
|
903
|
-
const obj = this.singlePlugins.get(SensorType.Object);
|
|
904
|
-
if (!obj)
|
|
905
|
-
return false;
|
|
906
|
-
if (obj.requiresFrames)
|
|
907
|
-
return true;
|
|
908
|
-
return this.hasFrameBasedSecondary();
|
|
909
|
-
}
|
|
910
673
|
startAdHocVideoLoopIfNeeded() {
|
|
911
|
-
if (!this.loopRunning && this.needsAdHocLoop()) {
|
|
674
|
+
if (!this.loopRunning && this.plugins.needsAdHocLoop()) {
|
|
912
675
|
this.adHocVideoLoop = true;
|
|
913
676
|
this.logger.debug('Starting ad-hoc video loop for sensor cascade trigger');
|
|
914
677
|
this.startVideoLoop();
|
|
915
678
|
}
|
|
916
679
|
}
|
|
917
680
|
stopAdHocVideoLoopIfIdle() {
|
|
918
|
-
if (this.adHocVideoLoop && !this.shouldVideoBeActive()) {
|
|
681
|
+
if (this.adHocVideoLoop && !this.plugins.shouldVideoBeActive()) {
|
|
919
682
|
this.adHocVideoLoop = false;
|
|
920
683
|
this.logger.debug('Stopping ad-hoc video loop — cascade ended');
|
|
921
684
|
this.stopVideoLoop();
|
|
922
685
|
}
|
|
923
686
|
}
|
|
924
|
-
fetchEventThumbnailAsync() {
|
|
925
|
-
void (async () => {
|
|
926
|
-
try {
|
|
927
|
-
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
928
|
-
try {
|
|
929
|
-
const hqJpeg = await this.hqSnapshotSource?.snapshotJpeg(EVENT_THUMB_HQ_MAX_WIDTH, EVENT_THUMB_HQ_QUALITY);
|
|
930
|
-
if (hqJpeg) {
|
|
931
|
-
this.eventManager.publishEventThumbnail(hqJpeg);
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
const handle = __addDisposableResource(env_1, await this.frameSource.fetchSnapshotFrame(), true);
|
|
935
|
-
if (!handle)
|
|
936
|
-
return;
|
|
937
|
-
// Large frames (plugin-native snapshots) can afford the HQ thumbnail width.
|
|
938
|
-
const maxWidth = handle.frame.width >= EVENT_THUMB_HQ_MAX_WIDTH * 2 ? EVENT_THUMB_HQ_MAX_WIDTH : EVENT_THUMB_MAX_WIDTH;
|
|
939
|
-
const jpeg = await this.frameScaler.frameToJPEG(handle.frame, maxWidth);
|
|
940
|
-
if (!jpeg)
|
|
941
|
-
return;
|
|
942
|
-
this.eventManager.publishEventThumbnail(jpeg);
|
|
943
|
-
}
|
|
944
|
-
catch (e_1) {
|
|
945
|
-
env_1.error = e_1;
|
|
946
|
-
env_1.hasError = true;
|
|
947
|
-
}
|
|
948
|
-
finally {
|
|
949
|
-
const result_1 = __disposeResources(env_1);
|
|
950
|
-
if (result_1)
|
|
951
|
-
await result_1;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
catch (e) {
|
|
955
|
-
this.logger.debug('event-thumb snapshot failed:', e);
|
|
956
|
-
}
|
|
957
|
-
})();
|
|
958
|
-
}
|
|
959
|
-
// Fallback: async snapshot replacement of a substream event thumbnail —
|
|
960
|
-
// only reached when the HQ source couldn't deliver a frame inline at event
|
|
961
|
-
// start (still connecting, buffer not warm yet).
|
|
962
|
-
upgradeEventThumbnailAsync() {
|
|
963
|
-
const source = this.hqSnapshotSource;
|
|
964
|
-
if (!source?.isRunning || !source.hasBuffer || this.hqUpgradeInflight)
|
|
965
|
-
return;
|
|
966
|
-
this.hqUpgradeInflight = true;
|
|
967
|
-
void (async () => {
|
|
968
|
-
try {
|
|
969
|
-
const jpeg = await source.snapshotJpeg(EVENT_THUMB_HQ_MAX_WIDTH, EVENT_THUMB_HQ_QUALITY);
|
|
970
|
-
if (jpeg && this.eventManager.hasActiveEvent()) {
|
|
971
|
-
this.eventManager.publishEventThumbnail(jpeg);
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
catch (error) {
|
|
975
|
-
this.logger.debug('Snapshot event thumbnail upgrade failed:', error);
|
|
976
|
-
}
|
|
977
|
-
finally {
|
|
978
|
-
this.hqUpgradeInflight = false;
|
|
979
|
-
}
|
|
980
|
-
})();
|
|
981
|
-
}
|
|
982
|
-
// Recent snapshot frame + its scaler (own hardware context); caller disposes
|
|
983
|
-
// the frame. With a reference size the aspect ratios must match — normalized
|
|
984
|
-
// boxes only transfer between streams showing the same sensor area.
|
|
985
|
-
async acquireHqFrame(reference) {
|
|
986
|
-
const source = this.hqSnapshotSource;
|
|
987
|
-
if (!source?.isRunning || !source.hasBuffer)
|
|
988
|
-
return null;
|
|
989
|
-
const scaler = source.scaler;
|
|
990
|
-
if (!scaler)
|
|
991
|
-
return null;
|
|
992
|
-
const frame = await source.getFrame(HQ_FRAME_MAX_AGE_MS);
|
|
993
|
-
if (!frame)
|
|
994
|
-
return null;
|
|
995
|
-
if (reference) {
|
|
996
|
-
const refAspect = reference.width / reference.height;
|
|
997
|
-
const hqAspect = frame.width / frame.height;
|
|
998
|
-
if (Math.abs(refAspect - hqAspect) / refAspect > 0.02) {
|
|
999
|
-
if (!this.hqAspectWarned) {
|
|
1000
|
-
this.hqAspectWarned = true;
|
|
1001
|
-
this.logger.warn(`[hq-thumb] aspect mismatch (detection ${reference.width}x${reference.height} vs snapshot ${frame.width}x${frame.height}) — detection crops stay on the substream`);
|
|
1002
|
-
}
|
|
1003
|
-
frame[Symbol.dispose]?.();
|
|
1004
|
-
return null;
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
return { frame, scaler };
|
|
1008
|
-
}
|
|
1009
|
-
resolveHqSourceUrl(sources) {
|
|
1010
|
-
if (!sources?.length)
|
|
1011
|
-
return undefined;
|
|
1012
|
-
const order = ['high-resolution', 'mid-resolution', 'low-resolution'];
|
|
1013
|
-
const byRole = new Map(sources.map((s) => [s.role, s]));
|
|
1014
|
-
// Detection consumes the LOWEST available role (worker: lowRes ?? midRes
|
|
1015
|
-
// ?? streamSource) — the HQ companion is only worth a session when a
|
|
1016
|
-
// strictly higher-resolution role exists.
|
|
1017
|
-
const detectionRole = [...order].reverse().find((role) => byRole.has(role));
|
|
1018
|
-
const hqRole = order.find((role) => byRole.has(role));
|
|
1019
|
-
if (!detectionRole || !hqRole || hqRole === detectionRole)
|
|
1020
|
-
return undefined;
|
|
1021
|
-
return byRole.get(hqRole).url;
|
|
1022
|
-
}
|
|
1023
|
-
syncHqSnapshotSource() {
|
|
1024
|
-
if (!this.hqSnapshotSource)
|
|
1025
|
-
return;
|
|
1026
|
-
const wanted = this.config.frameWorkerSettings.hqSnapshots === true;
|
|
1027
|
-
if (wanted && !this.hqSnapshotSource.isRunning) {
|
|
1028
|
-
this.hqSnapshotSource.start();
|
|
1029
|
-
}
|
|
1030
|
-
else if (!wanted && this.hqSnapshotSource.isRunning) {
|
|
1031
|
-
this.hqSnapshotSource.stop();
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
calculateDBFS(data, format) {
|
|
1035
|
-
let sumSquares = 0;
|
|
1036
|
-
let sampleCount = 0;
|
|
1037
|
-
if (format === 'float32') {
|
|
1038
|
-
const floatView = new Float32Array(data.buffer, data.byteOffset, data.byteLength / 4);
|
|
1039
|
-
sampleCount = floatView.length;
|
|
1040
|
-
for (let i = 0; i < sampleCount; i++)
|
|
1041
|
-
sumSquares += floatView[i] * floatView[i];
|
|
1042
|
-
}
|
|
1043
|
-
else {
|
|
1044
|
-
// PCM16: normalize [-32768, 32767] → [-1.0, 1.0]
|
|
1045
|
-
const int16View = new Int16Array(data.buffer, data.byteOffset, data.byteLength / 2);
|
|
1046
|
-
sampleCount = int16View.length;
|
|
1047
|
-
for (let i = 0; i < sampleCount; i++) {
|
|
1048
|
-
const normalized = int16View[i] / 32768;
|
|
1049
|
-
sumSquares += normalized * normalized;
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
if (sampleCount === 0)
|
|
1053
|
-
return -100;
|
|
1054
|
-
const rms = Math.sqrt(sumSquares / sampleCount);
|
|
1055
|
-
return rms > 0 ? 20 * Math.log10(Math.max(rms, 1e-10)) : -100;
|
|
1056
|
-
}
|
|
1057
|
-
async runAudioDetectionLoop() {
|
|
1058
|
-
const audioPlugin = this.singlePlugins.get(SensorType.Audio);
|
|
1059
|
-
if (!audioPlugin?.requiresFrames)
|
|
1060
|
-
return;
|
|
1061
|
-
const audioSpec = this.isAudioModelSpec(audioPlugin.modelSpec) ? audioPlugin.modelSpec : undefined;
|
|
1062
|
-
const sampleRate = audioSpec?.input.sampleRate ?? 16000;
|
|
1063
|
-
const channels = audioSpec?.input.channels ?? 1;
|
|
1064
|
-
const format = audioSpec?.input.format ?? 'float32';
|
|
1065
|
-
const samplesPerFrame = audioSpec?.input.samplesPerFrame;
|
|
1066
|
-
const audioConfig = {
|
|
1067
|
-
streamUrl: this.config.audioStreamUrl,
|
|
1068
|
-
sampleRate,
|
|
1069
|
-
channels,
|
|
1070
|
-
format,
|
|
1071
|
-
samplesPerFrame,
|
|
1072
|
-
};
|
|
1073
|
-
while (this.audioLoopRunning) {
|
|
1074
|
-
try {
|
|
1075
|
-
this.audioSource = new AudioSource(audioConfig, this.logger);
|
|
1076
|
-
await this.audioSource.start();
|
|
1077
|
-
this.logger.debug('Audio stream connected, processing audio frames...');
|
|
1078
|
-
this.audioReconnectCount = 0;
|
|
1079
|
-
let frameCount = 0;
|
|
1080
|
-
let lastAudioFrameId = -1;
|
|
1081
|
-
while (this.audioLoopRunning) {
|
|
1082
|
-
const snap = await this.audioSource.nextFrame(lastAudioFrameId);
|
|
1083
|
-
if (!snap)
|
|
1084
|
-
break; // source ended (stop or EOF)
|
|
1085
|
-
lastAudioFrameId = snap.id;
|
|
1086
|
-
const rawFrame = snap.frame;
|
|
1087
|
-
frameCount++;
|
|
1088
|
-
try {
|
|
1089
|
-
const frameData = rawFrame.data;
|
|
1090
|
-
if (!frameData?.[0])
|
|
1091
|
-
continue;
|
|
1092
|
-
const rawData = frameData[0];
|
|
1093
|
-
const dBFS = this.calculateDBFS(rawData, format);
|
|
1094
|
-
if (dBFS < this.audioMinDecibels)
|
|
1095
|
-
continue;
|
|
1096
|
-
const audioFrame = {
|
|
1097
|
-
cameraId: this.config.cameraId,
|
|
1098
|
-
data: rawData,
|
|
1099
|
-
sampleRate,
|
|
1100
|
-
channels,
|
|
1101
|
-
format,
|
|
1102
|
-
timestamp: Date.now(),
|
|
1103
|
-
};
|
|
1104
|
-
const result = await audioPlugin.proxy.detectAudio(audioFrame);
|
|
1105
|
-
if (!result)
|
|
1106
|
-
continue;
|
|
1107
|
-
// Frame-based audio detection: cascade trigger when detected (Motion's
|
|
1108
|
-
// dwell counterpart for audio is handled in ingestDetectionResult).
|
|
1109
|
-
if (result.detected && this.cascadeEnabled) {
|
|
1110
|
-
const audioTimeout = this.config.detectionSettings.audio.timeout;
|
|
1111
|
-
this.cascade.triggerMomentary(audioTimeout);
|
|
1112
|
-
}
|
|
1113
|
-
// Funnel through the unified ingest path — same as external audio writes.
|
|
1114
|
-
this.ingestDetectionResult(SensorType.Audio, audioPlugin.sensorId, {
|
|
1115
|
-
detected: result.detected,
|
|
1116
|
-
detections: result.detections ?? [],
|
|
1117
|
-
...(result.decibels !== undefined ? { decibels: result.decibels } : {}),
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
catch (error) {
|
|
1121
|
-
this.logger.error('Audio detection error:', error);
|
|
1122
|
-
}
|
|
1123
|
-
finally {
|
|
1124
|
-
try {
|
|
1125
|
-
rawFrame[Symbol.dispose]?.();
|
|
1126
|
-
}
|
|
1127
|
-
catch {
|
|
1128
|
-
// Best effort
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
const streamError = this.audioSource.lastError;
|
|
1133
|
-
await this.audioSource.stop();
|
|
1134
|
-
if (this.audioLoopRunning && streamError) {
|
|
1135
|
-
this.audioReconnectCount = Math.min(this.audioReconnectCount + 1, this.maxReconnectCount);
|
|
1136
|
-
const delay = this.audioReconnectCount >= this.maxReconnectCount ? this.maxReconnectDelay : this.normalReconnectDelay;
|
|
1137
|
-
this.logger.warn(`Audio stream ended with read error, reconnecting in ${delay / 1000}s: ${streamError.message}`);
|
|
1138
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1139
|
-
}
|
|
1140
|
-
else if (this.audioLoopRunning && frameCount === 0) {
|
|
1141
|
-
this.logger.debug('Audio stream ended without frames, waiting before reconnect...');
|
|
1142
|
-
await new Promise((resolve) => setTimeout(resolve, this.normalReconnectDelay));
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
catch (error) {
|
|
1146
|
-
if (!this.audioLoopRunning)
|
|
1147
|
-
break;
|
|
1148
|
-
await this.audioSource?.stop();
|
|
1149
|
-
this.audioReconnectCount = Math.min(this.audioReconnectCount + 1, this.maxReconnectCount);
|
|
1150
|
-
const delay = this.audioReconnectCount >= this.maxReconnectCount ? this.maxReconnectDelay : this.normalReconnectDelay;
|
|
1151
|
-
this.logger.warn(`Audio stream error, reconnecting in ${delay / 1000}s: ${error.message}`);
|
|
1152
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
await this.audioSource?.stop();
|
|
1156
|
-
this.audioSource = undefined;
|
|
1157
|
-
this.logger.debug('Audio detection loop ended');
|
|
1158
|
-
}
|
|
1159
687
|
async runDetectionLoop() {
|
|
1160
688
|
while (this.loopRunning) {
|
|
1161
689
|
try {
|
|
1162
690
|
await this.frameSource.start();
|
|
1163
691
|
this.frameScaler.updateHardwareContext(this.frameSource.hardwareContext);
|
|
1164
692
|
this.logger.debug('Stream connected, processing frames...');
|
|
1165
|
-
this.
|
|
693
|
+
this.videoBackoff.reset();
|
|
1166
694
|
let frameCount = 0;
|
|
1167
695
|
let lastFrameId = -1;
|
|
1168
696
|
while (this.loopRunning) {
|
|
@@ -1179,7 +707,7 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1179
707
|
snap.frame[Symbol.dispose]?.();
|
|
1180
708
|
}
|
|
1181
709
|
catch {
|
|
1182
|
-
//
|
|
710
|
+
// best-effort
|
|
1183
711
|
}
|
|
1184
712
|
}
|
|
1185
713
|
}
|
|
@@ -1187,14 +715,13 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1187
715
|
await this.frameSource.stop();
|
|
1188
716
|
this.frameScaler.clearCache();
|
|
1189
717
|
if (this.loopRunning && streamError) {
|
|
1190
|
-
|
|
1191
|
-
const delay = this.reconnectCount >= this.maxReconnectCount ? this.maxReconnectDelay : this.normalReconnectDelay;
|
|
718
|
+
const delay = this.videoBackoff.nextDelayMs();
|
|
1192
719
|
this.logger.warn(`Stream ended with read error, reconnecting in ${delay / 1000}s: ${streamError.message}`);
|
|
1193
|
-
await
|
|
720
|
+
await sleep(delay);
|
|
1194
721
|
}
|
|
1195
722
|
else if (this.loopRunning && frameCount === 0) {
|
|
1196
723
|
this.logger.debug('Stream ended without frames, waiting before reconnect...');
|
|
1197
|
-
await
|
|
724
|
+
await sleep(this.videoBackoff.idleDelayMs);
|
|
1198
725
|
}
|
|
1199
726
|
}
|
|
1200
727
|
catch (error) {
|
|
@@ -1202,10 +729,9 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1202
729
|
break;
|
|
1203
730
|
await this.frameSource.stop();
|
|
1204
731
|
this.frameScaler.clearCache();
|
|
1205
|
-
|
|
1206
|
-
const delay = this.reconnectCount >= this.maxReconnectCount ? this.maxReconnectDelay : this.normalReconnectDelay;
|
|
732
|
+
const delay = this.videoBackoff.nextDelayMs();
|
|
1207
733
|
this.logger.warn(`Stream error, reconnecting in ${delay / 1000}s: ${error.message}`);
|
|
1208
|
-
await
|
|
734
|
+
await sleep(delay);
|
|
1209
735
|
}
|
|
1210
736
|
}
|
|
1211
737
|
await this.frameSource.stop();
|
|
@@ -1213,7 +739,7 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1213
739
|
this.logger.debug('Detection loop ended');
|
|
1214
740
|
}
|
|
1215
741
|
async processRawFrame(rawFrame) {
|
|
1216
|
-
if (!this.loopRunning || !this.
|
|
742
|
+
if (!this.loopRunning || !this.plugins.hasAny())
|
|
1217
743
|
return;
|
|
1218
744
|
try {
|
|
1219
745
|
await this.processDetection(rawFrame);
|
|
@@ -1227,21 +753,18 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1227
753
|
let motionDetected = false;
|
|
1228
754
|
let objectDetections = [];
|
|
1229
755
|
const results = { timestamp: t0 };
|
|
1230
|
-
//
|
|
1231
|
-
// expire during the cascade window.
|
|
756
|
+
// keeps dead tracks from expiring during the cascade window
|
|
1232
757
|
if (this.cascade.isActive) {
|
|
1233
758
|
this.pipeline.refreshReid();
|
|
1234
759
|
}
|
|
1235
|
-
// PTZ
|
|
1236
|
-
//
|
|
1237
|
-
//
|
|
1238
|
-
// commands on its own isPtzMoving state.
|
|
760
|
+
// while the PTZ repositions: motion and object keep running so the
|
|
761
|
+
// background model and track ids survive the pan, but nothing below
|
|
762
|
+
// reaches dwell/events until the settle window ends
|
|
1239
763
|
const ptzSuppressed = this.ptzAutotracker.suppressionActive;
|
|
1240
|
-
//
|
|
1241
|
-
//
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
if (motionPlugin?.requiresFrames && !ptzSuppressed) {
|
|
764
|
+
// motion runs every frame and re-arms the cascade; the cascade gates
|
|
765
|
+
// object detection, not motion
|
|
766
|
+
const motionPlugin = this.plugins.get(SensorType.Motion);
|
|
767
|
+
if (motionPlugin?.requiresFrames) {
|
|
1245
768
|
const motionFrame = await this.scaleForMotion(rawFrame);
|
|
1246
769
|
if (!this.loopRunning)
|
|
1247
770
|
return;
|
|
@@ -1250,13 +773,18 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1250
773
|
const result = await motionPlugin.proxy.detectMotion(motionFrame);
|
|
1251
774
|
if (!this.loopRunning)
|
|
1252
775
|
return;
|
|
1253
|
-
if (
|
|
776
|
+
if (ptzSuppressed) {
|
|
777
|
+
// fed for the background model only, results discarded: frame-diff
|
|
778
|
+
// is garbage during ego-motion, but skipping frames would leave a
|
|
779
|
+
// stale background that lights up the whole scene after the move
|
|
780
|
+
}
|
|
781
|
+
else if (result.detections.length > 0) {
|
|
1254
782
|
const filtered = this.pipeline.runMergeAndZoneFilter(result.detections);
|
|
1255
783
|
motionDetected = filtered.length > 0;
|
|
1256
784
|
results.motion = { ...result, detections: filtered };
|
|
1257
785
|
}
|
|
1258
786
|
else {
|
|
1259
|
-
//
|
|
787
|
+
// record empty so the UI clears stale bboxes
|
|
1260
788
|
results.motion = { detected: false, detections: [] };
|
|
1261
789
|
}
|
|
1262
790
|
}
|
|
@@ -1268,22 +796,20 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1268
796
|
}
|
|
1269
797
|
}
|
|
1270
798
|
else if (this.cascade.isActive) {
|
|
1271
|
-
//
|
|
799
|
+
// no frame-based motion plugin, the cascade is the only motion-equivalent signal
|
|
1272
800
|
motionDetected = true;
|
|
1273
801
|
results.cascadeTriggered = true;
|
|
1274
802
|
}
|
|
1275
|
-
//
|
|
1276
|
-
// own flag is ignored on purpose, zone filtering wins.
|
|
803
|
+
// detected is re-derived after zone filtering, the plugin's own flag is ignored
|
|
1277
804
|
if (results.motion && motionPlugin) {
|
|
1278
805
|
this.ingestDetectionResult(SensorType.Motion, motionPlugin.sensorId, {
|
|
1279
806
|
detected: motionDetected,
|
|
1280
807
|
detections: results.motion.detections,
|
|
1281
808
|
});
|
|
1282
809
|
}
|
|
1283
|
-
//
|
|
1284
|
-
//
|
|
1285
|
-
|
|
1286
|
-
const objectPlugin = this.singlePlugins.get(SensorType.Object);
|
|
810
|
+
// object is cascade-gated, but keeps running during PTZ suppression so
|
|
811
|
+
// the tracker holds its lock across pans
|
|
812
|
+
const objectPlugin = this.plugins.get(SensorType.Object);
|
|
1287
813
|
const shouldDetectObjects = objectPlugin?.requiresFrames === true && (ptzSuppressed || !this.cascadeEnabled || this.cascade.isActive);
|
|
1288
814
|
if (shouldDetectObjects) {
|
|
1289
815
|
const objectFrame = await this.scaleForObject(rawFrame);
|
|
@@ -1291,17 +817,24 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1291
817
|
return;
|
|
1292
818
|
if (objectFrame) {
|
|
1293
819
|
try {
|
|
1294
|
-
const result = await
|
|
820
|
+
const result = await PromiseTimeout(objectPlugin.proxy.detectObjects(objectFrame), DETECT_TIMEOUT_MS, undefined, `Object detection timed out after ${DETECT_TIMEOUT_MS}ms`);
|
|
1295
821
|
if (!this.loopRunning)
|
|
1296
822
|
return;
|
|
1297
|
-
//
|
|
1298
|
-
// state; the
|
|
1299
|
-
// pans (track ids survive instead of LOST → REACQUIRE).
|
|
823
|
+
// run the pipeline even on empty frames so Norfair advances its
|
|
824
|
+
// Kalman state; the pose delta keeps predictions stable across pans
|
|
1300
825
|
const poseDelta = this.ptzAutotracker.consumePoseDelta();
|
|
1301
826
|
const pipelineResult = this.pipeline.process(result.detections, objectFrame, poseDelta);
|
|
1302
|
-
//
|
|
1303
|
-
|
|
1304
|
-
|
|
827
|
+
// track id churn is invisible in the event log without this
|
|
828
|
+
if (pipelineResult.created.length > 0 || pipelineResult.removed.length > 0) {
|
|
829
|
+
const born = pipelineResult.created.map((id) => {
|
|
830
|
+
const t = pipelineResult.tracked.find((d) => d.trackId === id);
|
|
831
|
+
return `${t?.label ?? 'unknown'}#${id}`;
|
|
832
|
+
});
|
|
833
|
+
const died = pipelineResult.removed.map((id) => `#${id}${this.stationary.isAnchored(id) ? ' (anchored!)' : ''}`);
|
|
834
|
+
this.logger.trace(`[tracker] ${born.length ? `born: ${born.join(', ')}` : ''}${born.length && died.length ? ' — ' : ''}${died.length ? `died: ${died.join(', ')}` : ''}`);
|
|
835
|
+
}
|
|
836
|
+
// extrapolated (Kalman-only) tracks smooth single-frame misses in
|
|
837
|
+
// the UI, but must not float off-frame as ghost bboxes
|
|
1305
838
|
const visibleTracks = pipelineResult.tracked.filter((t) => {
|
|
1306
839
|
if (!t.trackLost)
|
|
1307
840
|
return true;
|
|
@@ -1313,9 +846,8 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1313
846
|
results.object = { ...result, detections: visibleTracks };
|
|
1314
847
|
if (pipelineResult.crossings.length > 0)
|
|
1315
848
|
results.lineCrossings = pipelineResult.crossings;
|
|
1316
|
-
//
|
|
1317
|
-
//
|
|
1318
|
-
// churn. It gates motor decisions on its own isPtzMoving flag.
|
|
849
|
+
// incl. extrapolated tracks, otherwise a single missed detector
|
|
850
|
+
// frame flips the autotracker into LOST/REACQUIRE churn
|
|
1319
851
|
this.ptzAutotracker.handleObjectDetections(pipelineResult.tracked);
|
|
1320
852
|
}
|
|
1321
853
|
catch (error) {
|
|
@@ -1325,14 +857,25 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1325
857
|
}
|
|
1326
858
|
}
|
|
1327
859
|
}
|
|
860
|
+
// tracker and autotracker are fed, everything event-facing stays quiet
|
|
861
|
+
if (ptzSuppressed) {
|
|
862
|
+
// object boxes stay live in the UI through the move, bbox publish only
|
|
863
|
+
if (objectPlugin && results.object) {
|
|
864
|
+
this.writeSensorProperties(objectPlugin.sensorId, { detections: results.object.detections });
|
|
865
|
+
}
|
|
866
|
+
// keep an already-running object dwell alive so a chase doesn't end its
|
|
867
|
+
// own event, but never activate it from suppressed frames
|
|
868
|
+
if (objectPlugin && objectDetections.length > 0 && this.dwell.isActive(objectPlugin.sensorId)) {
|
|
869
|
+
this.dwell.refresh(objectPlugin.sensorId, OBJECT_DWELL_SECONDS);
|
|
870
|
+
}
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
1328
873
|
if (results.lineCrossings && results.lineCrossings.length > 0) {
|
|
1329
874
|
for (const c of results.lineCrossings) {
|
|
1330
|
-
this.logger.
|
|
875
|
+
this.logger.trace(`Line crossing: ${c.label}#${c.trackId} crossed "${c.lineName}" dir=${c.direction} conf=${c.confidence.toFixed(2)}`);
|
|
1331
876
|
}
|
|
1332
|
-
// Buffer line crossings so the EventManager snapshot picks them up
|
|
1333
877
|
this.currentDetectionState.lineCrossings = results.lineCrossings;
|
|
1334
878
|
}
|
|
1335
|
-
// Same rule as motion: detected is re-derived from post-pipeline detections.
|
|
1336
879
|
if (results.object && objectPlugin) {
|
|
1337
880
|
this.ingestDetectionResult(SensorType.Object, objectPlugin.sensorId, {
|
|
1338
881
|
detected: objectDetections.length > 0,
|
|
@@ -1341,227 +884,67 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1341
884
|
}
|
|
1342
885
|
if (!this.loopRunning)
|
|
1343
886
|
return;
|
|
1344
|
-
// 3. Secondary detectors + per-detection thumbnails. Result-collection
|
|
1345
|
-
// only — the shared ingest below keeps loop tick and external-RPC path
|
|
1346
|
-
// on the same helper.
|
|
1347
887
|
if (objectDetections.length > 0) {
|
|
1348
888
|
await this.runSecondariesAndThumbnails(rawFrame, objectDetections, results);
|
|
1349
889
|
}
|
|
1350
890
|
if (!this.loopRunning)
|
|
1351
891
|
return;
|
|
1352
|
-
// 4. Unified ingest.
|
|
1353
892
|
this.ingestResultsForAllSecondaries(results);
|
|
1354
|
-
|
|
1355
|
-
//
|
|
893
|
+
this.clearStaleSecondaryBboxes();
|
|
894
|
+
// one flush per frame, after detectors and thumbnails, so segment-start
|
|
895
|
+
// messages already carry them
|
|
1356
896
|
if (!this.loopRunning)
|
|
1357
897
|
return;
|
|
1358
898
|
const snapshot = this.buildSnapshot();
|
|
1359
899
|
if (results.thumbnails && results.thumbnails.length > 0) {
|
|
1360
900
|
snapshot.thumbnails = results.thumbnails;
|
|
1361
901
|
}
|
|
1362
|
-
//
|
|
1363
|
-
//
|
|
1364
|
-
// the 'start' message carries it inline). Attaching is harmless either
|
|
1365
|
-
// way; EventManager only consumes it while needsEventThumbnail is true.
|
|
1366
|
-
// Full frame prefers the snapshot source (no aspect constraint needed).
|
|
902
|
+
// pre-encode the event thumbnail when this frame starts an event, so the
|
|
903
|
+
// start message carries it inline; attaching is harmless otherwise
|
|
1367
904
|
const wantsEventThumb = this.eventManager.needsThumbnail() || this.snapshotWillStartEvent(snapshot);
|
|
1368
905
|
let hqThumbAttached = false;
|
|
1369
906
|
if (wantsEventThumb) {
|
|
1370
907
|
try {
|
|
1371
|
-
const
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
if (jpeg) {
|
|
1376
|
-
snapshot.eventThumbnail = jpeg;
|
|
1377
|
-
hqThumbAttached = true;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
if (!snapshot.eventThumbnail) {
|
|
1381
|
-
const jpeg = await this.frameScaler.frameToJPEG(rawFrame, EVENT_THUMB_MAX_WIDTH);
|
|
1382
|
-
if (jpeg)
|
|
1383
|
-
snapshot.eventThumbnail = jpeg;
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
finally {
|
|
1387
|
-
hq?.frame[Symbol.dispose]?.();
|
|
1388
|
-
}
|
|
908
|
+
const thumb = await this.thumbnailer.captureEventThumbnail(rawFrame);
|
|
909
|
+
if (thumb.jpeg)
|
|
910
|
+
snapshot.eventThumbnail = thumb.jpeg;
|
|
911
|
+
hqThumbAttached = thumb.fromHq;
|
|
1389
912
|
}
|
|
1390
913
|
catch (error) {
|
|
1391
914
|
this.logger.error('Event thumbnail generation error:', error);
|
|
1392
915
|
}
|
|
1393
916
|
}
|
|
1394
917
|
this.eventManager.processResults(snapshot);
|
|
1395
|
-
//
|
|
1396
|
-
// covers the window where the HQ source wasn't ready at event start.
|
|
918
|
+
// covers the window where the HQ source wasn't ready at event start
|
|
1397
919
|
if (wantsEventThumb && !hqThumbAttached) {
|
|
1398
|
-
this.upgradeEventThumbnailAsync();
|
|
920
|
+
this.thumbnailer.upgradeEventThumbnailAsync();
|
|
1399
921
|
}
|
|
1400
922
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
if (snapshot.motion?.detected)
|
|
1405
|
-
return true;
|
|
1406
|
-
if (snapshot.audio?.detected)
|
|
1407
|
-
return true;
|
|
1408
|
-
if (snapshot.cascadeTriggered)
|
|
1409
|
-
return true;
|
|
1410
|
-
if ((snapshot.sensorTriggers?.length ?? 0) > 0)
|
|
1411
|
-
return true;
|
|
1412
|
-
if ((snapshot.lineCrossings?.length ?? 0) > 0)
|
|
1413
|
-
return true;
|
|
1414
|
-
return false;
|
|
1415
|
-
}
|
|
1416
|
-
async resolveFaceIdentities(faces) {
|
|
1417
|
-
const withEmbeddings = faces.filter((f) => f.embedding?.length);
|
|
1418
|
-
if (!withEmbeddings.length)
|
|
1419
|
-
return;
|
|
1420
|
-
const facePlugin = this.singlePlugins.get(SensorType.Face);
|
|
1421
|
-
const embeddingModel = facePlugin?.modelSpec?.embeddingModel;
|
|
1422
|
-
if (!embeddingModel)
|
|
1423
|
-
return;
|
|
1424
|
-
const nvr = await this.getNvrProxy();
|
|
1425
|
-
if (!nvr)
|
|
923
|
+
async runSecondariesAndThumbnails(rawFrame, objectDetections, results) {
|
|
924
|
+
objectDetections = this.stationary.excludeSealed(objectDetections);
|
|
925
|
+
if (objectDetections.length === 0)
|
|
1426
926
|
return;
|
|
927
|
+
await this.secondaries.detect(rawFrame, objectDetections, results);
|
|
1427
928
|
try {
|
|
1428
|
-
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
withEmbeddings[i].identity = matches[i].identity;
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
catch (error) {
|
|
1437
|
-
if (!this.loopRunning || isNoRespondersError(error))
|
|
1438
|
-
return;
|
|
1439
|
-
this.logger.error('Could not resolve face identities:', error);
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
async scaleForMotion(rawFrame) {
|
|
1443
|
-
const motionPlugin = this.singlePlugins.get(SensorType.Motion);
|
|
1444
|
-
if (!motionPlugin?.requiresFrames)
|
|
1445
|
-
return undefined;
|
|
1446
|
-
const maxWidth = MOTION_WIDTH_MAP[this.motionResolution];
|
|
1447
|
-
const scaled = await this.frameScaler.scaleProportional(rawFrame, maxWidth, 'gray');
|
|
1448
|
-
return scaled ? this.frameScaler.toVideoFrameData(scaled) : undefined;
|
|
1449
|
-
}
|
|
1450
|
-
async scaleForObject(rawFrame) {
|
|
1451
|
-
const objectPlugin = this.singlePlugins.get(SensorType.Object);
|
|
1452
|
-
if (!objectPlugin?.requiresFrames)
|
|
1453
|
-
return undefined;
|
|
1454
|
-
const inputSpec = objectPlugin.modelSpec?.input;
|
|
1455
|
-
if (!this.isVideoInputSpec(inputSpec))
|
|
1456
|
-
return undefined;
|
|
1457
|
-
const scaled = await this.frameScaler.scaleToSpec(rawFrame, inputSpec);
|
|
1458
|
-
return scaled ? this.frameScaler.toVideoFrameData(scaled) : undefined;
|
|
1459
|
-
}
|
|
1460
|
-
async prepareSecondaryRegions(rawFrame, objectDetections) {
|
|
1461
|
-
const result = new Map();
|
|
1462
|
-
const consumers = [];
|
|
1463
|
-
const facePlugin = this.singlePlugins.get(SensorType.Face);
|
|
1464
|
-
if (facePlugin && this.hasSecondaryModelSpec(facePlugin.modelSpec) && this.isVideoInputSpec(facePlugin.modelSpec.input)) {
|
|
1465
|
-
consumers.push({ key: 'face', triggerLabels: facePlugin.modelSpec.triggerLabels, input: facePlugin.modelSpec.input });
|
|
1466
|
-
}
|
|
1467
|
-
const lpdPlugin = this.singlePlugins.get(SensorType.LicensePlate);
|
|
1468
|
-
if (lpdPlugin && this.hasSecondaryModelSpec(lpdPlugin.modelSpec) && this.isVideoInputSpec(lpdPlugin.modelSpec.input)) {
|
|
1469
|
-
consumers.push({ key: 'lpd', triggerLabels: lpdPlugin.modelSpec.triggerLabels, input: lpdPlugin.modelSpec.input });
|
|
1470
|
-
}
|
|
1471
|
-
const clipPlugin = this.singlePlugins.get(SensorType.Clip);
|
|
1472
|
-
if (clipPlugin && this.hasSecondaryModelSpec(clipPlugin.modelSpec) && this.isVideoInputSpec(clipPlugin.modelSpec.input)) {
|
|
1473
|
-
consumers.push({ key: 'clip', triggerLabels: clipPlugin.modelSpec.triggerLabels, input: clipPlugin.modelSpec.input });
|
|
1474
|
-
}
|
|
1475
|
-
const classifierPlugins = this.multiPlugins.get(SensorType.Classifier);
|
|
1476
|
-
if (classifierPlugins) {
|
|
1477
|
-
for (const [pluginId, classifierPlugin] of classifierPlugins) {
|
|
1478
|
-
if (this.hasSecondaryModelSpec(classifierPlugin.modelSpec) && this.isVideoInputSpec(classifierPlugin.modelSpec.input)) {
|
|
1479
|
-
consumers.push({ key: `classifier:${pluginId}`, triggerLabels: classifierPlugin.modelSpec.triggerLabels, input: classifierPlugin.modelSpec.input });
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
if (consumers.length === 0)
|
|
1484
|
-
return result;
|
|
1485
|
-
for (const detection of objectDetections) {
|
|
1486
|
-
const detLabel = detection.label.toLowerCase();
|
|
1487
|
-
const targets = [];
|
|
1488
|
-
for (const consumer of consumers) {
|
|
1489
|
-
if (consumer.triggerLabels.length === 0 || consumer.triggerLabels.some((l) => l.toLowerCase() === detLabel)) {
|
|
1490
|
-
targets.push({ key: consumer.key, width: consumer.input.width, height: consumer.input.height, format: consumer.input.format });
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
if (targets.length === 0)
|
|
1494
|
-
continue;
|
|
1495
|
-
// Consumers with identical dimensions share one scaled region
|
|
1496
|
-
const uniqueTargets = [];
|
|
1497
|
-
const dupeMap = new Map(); // sizeKey → [consumerKey, ...]
|
|
1498
|
-
for (const t of targets) {
|
|
1499
|
-
const sizeKey = `${t.width}x${t.height}_${t.format}`;
|
|
1500
|
-
const existing = dupeMap.get(sizeKey);
|
|
1501
|
-
if (existing) {
|
|
1502
|
-
existing.push(t.key);
|
|
1503
|
-
}
|
|
1504
|
-
else {
|
|
1505
|
-
dupeMap.set(sizeKey, [t.key]);
|
|
1506
|
-
uniqueTargets.push(t);
|
|
1507
|
-
}
|
|
929
|
+
// detectors ran on the substream frame, thumbnails prefer the HQ frame
|
|
930
|
+
const hq = await this.thumbnailer.acquireHqFrame({ width: rawFrame.width, height: rawFrame.height });
|
|
931
|
+
try {
|
|
932
|
+
results.thumbnails = await this.secondaries.generateThumbnails(hq?.frame ?? rawFrame, hq?.scaler ?? this.frameScaler, hq !== null, objectDetections, results);
|
|
1508
933
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
const primaryKey = consumerKeys[0];
|
|
1512
|
-
const region = regions.get(primaryKey);
|
|
1513
|
-
if (!region)
|
|
1514
|
-
continue;
|
|
1515
|
-
for (const key of consumerKeys) {
|
|
1516
|
-
const list = result.get(key);
|
|
1517
|
-
if (list) {
|
|
1518
|
-
list.push(region);
|
|
1519
|
-
}
|
|
1520
|
-
else {
|
|
1521
|
-
result.set(key, [region]);
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
934
|
+
finally {
|
|
935
|
+
hq?.frame[Symbol.dispose]?.();
|
|
1524
936
|
}
|
|
1525
937
|
}
|
|
1526
|
-
return result;
|
|
1527
|
-
}
|
|
1528
|
-
async runSecondaryDetection(type, results, fn) {
|
|
1529
|
-
let detections = undefined;
|
|
1530
|
-
try {
|
|
1531
|
-
detections = await fn();
|
|
1532
|
-
}
|
|
1533
938
|
catch (error) {
|
|
1534
|
-
|
|
1535
|
-
return;
|
|
1536
|
-
const logType = type === SensorType.Face ? 'Face' : type === SensorType.LicensePlate ? 'License plate' : type === SensorType.Clip ? 'CLIP' : 'Classifier';
|
|
1537
|
-
this.logger.error(`${logType} detection error:`, error);
|
|
1538
|
-
}
|
|
1539
|
-
if (!detections)
|
|
1540
|
-
return;
|
|
1541
|
-
if (type === SensorType.Face) {
|
|
1542
|
-
results.face = detections;
|
|
1543
|
-
await this.resolveFaceIdentities(results.face.detections);
|
|
1544
|
-
}
|
|
1545
|
-
else if (type === SensorType.LicensePlate) {
|
|
1546
|
-
results.licensePlate = detections;
|
|
1547
|
-
}
|
|
1548
|
-
else if (type === SensorType.Clip) {
|
|
1549
|
-
results.clip = detections;
|
|
1550
|
-
const clipPlugin = this.singlePlugins.get(SensorType.Clip);
|
|
1551
|
-
results.clipEmbeddingModel = clipPlugin?.modelSpec?.embeddingModel;
|
|
1552
|
-
}
|
|
1553
|
-
else if (type === SensorType.Classifier) {
|
|
1554
|
-
results.classifiers ??= {};
|
|
1555
|
-
for (const { pluginId, result } of detections) {
|
|
1556
|
-
results.classifiers[pluginId] = result;
|
|
1557
|
-
}
|
|
1558
|
-
if (Object.keys(results.classifiers).length === 0)
|
|
1559
|
-
delete results.classifiers;
|
|
939
|
+
this.logger.error('Thumbnail generation error:', error);
|
|
1560
940
|
}
|
|
1561
941
|
}
|
|
1562
942
|
ingestResultsForAllSecondaries(results) {
|
|
1563
943
|
if (results.face) {
|
|
1564
|
-
|
|
944
|
+
if (results.faceEmbeddingModel) {
|
|
945
|
+
this.currentDetectionState.faceEmbeddingModel = results.faceEmbeddingModel;
|
|
946
|
+
}
|
|
947
|
+
const facePlugin = this.plugins.get(SensorType.Face);
|
|
1565
948
|
if (facePlugin) {
|
|
1566
949
|
this.ingestDetectionResult(SensorType.Face, facePlugin.sensorId, {
|
|
1567
950
|
detected: results.face.detected,
|
|
@@ -1570,7 +953,7 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1570
953
|
}
|
|
1571
954
|
}
|
|
1572
955
|
if (results.licensePlate) {
|
|
1573
|
-
const lpdPlugin = this.
|
|
956
|
+
const lpdPlugin = this.plugins.get(SensorType.LicensePlate);
|
|
1574
957
|
if (lpdPlugin) {
|
|
1575
958
|
this.ingestDetectionResult(SensorType.LicensePlate, lpdPlugin.sensorId, {
|
|
1576
959
|
detected: results.licensePlate.detected,
|
|
@@ -1579,345 +962,54 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1579
962
|
}
|
|
1580
963
|
}
|
|
1581
964
|
if (results.classifiers) {
|
|
1582
|
-
const
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
detections: classifierResult.detections,
|
|
1590
|
-
}, pluginId);
|
|
1591
|
-
}
|
|
965
|
+
for (const [pluginId, classifierResult] of Object.entries(results.classifiers)) {
|
|
966
|
+
const plugin = this.plugins.getAll(SensorType.Classifier).find((p) => p.pluginId === pluginId);
|
|
967
|
+
if (plugin) {
|
|
968
|
+
this.ingestDetectionResult(SensorType.Classifier, plugin.sensorId, {
|
|
969
|
+
detected: classifierResult.detected,
|
|
970
|
+
detections: classifierResult.detections,
|
|
971
|
+
}, pluginId);
|
|
1592
972
|
}
|
|
1593
973
|
}
|
|
1594
974
|
}
|
|
1595
975
|
if (results.clip) {
|
|
1596
|
-
//
|
|
976
|
+
// clip has no sensor, embeddings flow through the buffer only
|
|
1597
977
|
this.currentDetectionState.clip = results.clip;
|
|
1598
978
|
this.currentDetectionState.clipEmbeddingModel = results.clipEmbeddingModel;
|
|
1599
979
|
}
|
|
1600
980
|
}
|
|
1601
|
-
|
|
1602
|
-
if (
|
|
1603
|
-
return;
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
}
|
|
1616
|
-
try {
|
|
1617
|
-
// Detectors consumed the substream frame above; images destined for the
|
|
1618
|
-
// event (chips, scene) prefer the snapshot frame when available.
|
|
1619
|
-
const hq = await this.acquireHqFrame({ width: rawFrame.width, height: rawFrame.height });
|
|
1620
|
-
try {
|
|
1621
|
-
results.thumbnails = await this.generateThumbnails(hq?.frame ?? rawFrame, hq?.scaler ?? this.frameScaler, hq !== null, objectDetections, results);
|
|
1622
|
-
}
|
|
1623
|
-
finally {
|
|
1624
|
-
hq?.frame[Symbol.dispose]?.();
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
catch (error) {
|
|
1628
|
-
this.logger.error('Thumbnail generation error:', error);
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
async runSecondariesFullFrame(rawFrame, results) {
|
|
1632
|
-
const regionMap = await this.prepareFullFrameRegions(rawFrame);
|
|
1633
|
-
if (regionMap.size > 0) {
|
|
1634
|
-
await Promise.allSettled([
|
|
1635
|
-
this.runSecondaryDetection(SensorType.Face, results, () => this.runFaceDetection(regionMap.get('face') ?? [])),
|
|
1636
|
-
this.runSecondaryDetection(SensorType.LicensePlate, results, () => this.runLicensePlateDetection(regionMap.get('lpd') ?? [])),
|
|
1637
|
-
this.runSecondaryDetection(SensorType.Classifier, results, () => this.runClassifierDetections(regionMap)),
|
|
1638
|
-
this.runSecondaryDetection(SensorType.Clip, results, () => this.runClipDetection(regionMap.get('clip') ?? [])),
|
|
1639
|
-
]);
|
|
1640
|
-
if (results.face?.detections.length) {
|
|
1641
|
-
const facePlugin = this.singlePlugins.get(SensorType.Face);
|
|
1642
|
-
results.faceEmbeddingModel = facePlugin?.modelSpec?.embeddingModel;
|
|
1643
|
-
this.currentDetectionState.faceEmbeddingModel = results.faceEmbeddingModel;
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
try {
|
|
1647
|
-
return await this.frameScaler.frameToJPEG(rawFrame, EVENT_THUMB_MAX_WIDTH);
|
|
1648
|
-
}
|
|
1649
|
-
catch (error) {
|
|
1650
|
-
this.logger.debug('Full-frame scene thumbnail failed:', error);
|
|
1651
|
-
return null;
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
async prepareFullFrameRegions(rawFrame) {
|
|
1655
|
-
const result = new Map();
|
|
1656
|
-
const consumers = [];
|
|
1657
|
-
const facePlugin = this.singlePlugins.get(SensorType.Face);
|
|
1658
|
-
if (facePlugin?.requiresFrames && this.hasSecondaryModelSpec(facePlugin.modelSpec) && this.isVideoInputSpec(facePlugin.modelSpec.input)) {
|
|
1659
|
-
consumers.push({ key: 'face', triggerLabels: facePlugin.modelSpec.triggerLabels, input: facePlugin.modelSpec.input });
|
|
1660
|
-
}
|
|
1661
|
-
const lpdPlugin = this.singlePlugins.get(SensorType.LicensePlate);
|
|
1662
|
-
if (lpdPlugin?.requiresFrames && this.hasSecondaryModelSpec(lpdPlugin.modelSpec) && this.isVideoInputSpec(lpdPlugin.modelSpec.input)) {
|
|
1663
|
-
consumers.push({ key: 'lpd', triggerLabels: lpdPlugin.modelSpec.triggerLabels, input: lpdPlugin.modelSpec.input });
|
|
1664
|
-
}
|
|
1665
|
-
const clipPlugin = this.singlePlugins.get(SensorType.Clip);
|
|
1666
|
-
if (clipPlugin?.requiresFrames && this.hasSecondaryModelSpec(clipPlugin.modelSpec) && this.isVideoInputSpec(clipPlugin.modelSpec.input)) {
|
|
1667
|
-
consumers.push({ key: 'clip', triggerLabels: clipPlugin.modelSpec.triggerLabels, input: clipPlugin.modelSpec.input });
|
|
1668
|
-
}
|
|
1669
|
-
const classifierPlugins = this.multiPlugins.get(SensorType.Classifier);
|
|
1670
|
-
if (classifierPlugins) {
|
|
1671
|
-
for (const [pluginId, plugin] of classifierPlugins) {
|
|
1672
|
-
if (plugin.requiresFrames && this.hasSecondaryModelSpec(plugin.modelSpec) && this.isVideoInputSpec(plugin.modelSpec.input)) {
|
|
1673
|
-
consumers.push({ key: `classifier:${pluginId}`, triggerLabels: plugin.modelSpec.triggerLabels, input: plugin.modelSpec.input });
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
if (consumers.length === 0)
|
|
1678
|
-
return result;
|
|
1679
|
-
const frameWidth = rawFrame.width;
|
|
1680
|
-
const frameHeight = rawFrame.height;
|
|
1681
|
-
const fullFrameDetection = {
|
|
1682
|
-
label: 'person',
|
|
1683
|
-
confidence: 1.0,
|
|
1684
|
-
box: { x: 0, y: 0, width: 1, height: 1 },
|
|
1685
|
-
};
|
|
1686
|
-
for (const consumer of consumers) {
|
|
1687
|
-
const scaled = await this.frameScaler.scaleToSpec(rawFrame, consumer.input);
|
|
1688
|
-
if (!scaled)
|
|
1689
|
-
continue;
|
|
1690
|
-
const region = {
|
|
1691
|
-
frame: this.frameScaler.toVideoFrameData(scaled, `fullframe:${consumer.key}`),
|
|
1692
|
-
detection: fullFrameDetection,
|
|
1693
|
-
offset: { x: 0, y: 0 },
|
|
1694
|
-
cropSize: { width: frameWidth, height: frameHeight },
|
|
1695
|
-
originalSize: { width: frameWidth, height: frameHeight },
|
|
1696
|
-
};
|
|
1697
|
-
result.set(consumer.key, [region]);
|
|
1698
|
-
}
|
|
1699
|
-
return result;
|
|
1700
|
-
}
|
|
1701
|
-
prepareSecondaryFrames(croppedRegions) {
|
|
1702
|
-
return croppedRegions.map((r, i) => ({ ...r.frame, id: String(i), label: r.detection.label }));
|
|
1703
|
-
}
|
|
1704
|
-
async runFaceDetection(croppedRegions) {
|
|
1705
|
-
const facePlugin = this.singlePlugins.get(SensorType.Face);
|
|
1706
|
-
if (!facePlugin || croppedRegions.length === 0)
|
|
1707
|
-
return undefined;
|
|
1708
|
-
const batchResults = await withTimeout(facePlugin.proxy.detectFaces(this.prepareSecondaryFrames(croppedRegions)), DETECT_TIMEOUT_MS, 'Face');
|
|
1709
|
-
const allFaces = [];
|
|
1710
|
-
for (let i = 0; i < batchResults.length; i++) {
|
|
1711
|
-
const parentTrackId = 'trackId' in croppedRegions[i].detection ? croppedRegions[i].detection.trackId : undefined;
|
|
1712
|
-
for (const face of batchResults[i].detections) {
|
|
1713
|
-
const transformed = this.transformBoxToOriginal(face.box, croppedRegions[i]);
|
|
1714
|
-
allFaces.push({ ...face, box: transformed, parentTrackId });
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
if (allFaces.length === 0)
|
|
1718
|
-
return { detected: false, detections: [] };
|
|
1719
|
-
// NMS only — zone filter is skipped because face crops come from
|
|
1720
|
-
// object detections that already passed zone filtering in process().
|
|
1721
|
-
const deduped = this.pipeline.runNms(allFaces);
|
|
1722
|
-
return { detected: deduped.length > 0, detections: deduped };
|
|
981
|
+
snapshotWillStartEvent(snapshot) {
|
|
982
|
+
if (this.eventManager.hasActiveEvent())
|
|
983
|
+
return false;
|
|
984
|
+
if (snapshot.motion?.detected)
|
|
985
|
+
return true;
|
|
986
|
+
if (snapshot.audio?.detected)
|
|
987
|
+
return true;
|
|
988
|
+
if (snapshot.cascadeTriggered)
|
|
989
|
+
return true;
|
|
990
|
+
if ((snapshot.sensorTriggers?.length ?? 0) > 0)
|
|
991
|
+
return true;
|
|
992
|
+
if ((snapshot.lineCrossings?.length ?? 0) > 0)
|
|
993
|
+
return true;
|
|
994
|
+
return false;
|
|
1723
995
|
}
|
|
1724
|
-
async
|
|
1725
|
-
const
|
|
1726
|
-
if (!
|
|
996
|
+
async scaleForMotion(rawFrame) {
|
|
997
|
+
const motionPlugin = this.plugins.get(SensorType.Motion);
|
|
998
|
+
if (!motionPlugin?.requiresFrames)
|
|
1727
999
|
return undefined;
|
|
1728
|
-
const
|
|
1729
|
-
const
|
|
1730
|
-
|
|
1731
|
-
const parentTrackId = 'trackId' in croppedRegions[i].detection ? croppedRegions[i].detection.trackId : undefined;
|
|
1732
|
-
for (const plate of batchResults[i].detections) {
|
|
1733
|
-
allPlates.push({ ...plate, box: this.transformBoxToOriginal(plate.box, croppedRegions[i]), parentTrackId });
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
if (allPlates.length === 0)
|
|
1737
|
-
return { detected: false, detections: [] };
|
|
1738
|
-
// NMS only — zone filter skipped (crops from already-filtered object boxes).
|
|
1739
|
-
const deduped = this.pipeline.runNms(allPlates);
|
|
1740
|
-
return { detected: deduped.length > 0, detections: deduped };
|
|
1741
|
-
}
|
|
1742
|
-
runClassifierDetections(regionMap) {
|
|
1743
|
-
const classifierPlugins = this.multiPlugins.get(SensorType.Classifier);
|
|
1744
|
-
if (!classifierPlugins || classifierPlugins.size === 0)
|
|
1745
|
-
return [];
|
|
1746
|
-
const promises = [];
|
|
1747
|
-
for (const [pluginId, classifierPlugin] of classifierPlugins) {
|
|
1748
|
-
const croppedRegions = regionMap.get(`classifier:${pluginId}`);
|
|
1749
|
-
if (!croppedRegions || croppedRegions.length === 0)
|
|
1750
|
-
continue;
|
|
1751
|
-
const runClassifier = async () => {
|
|
1752
|
-
const batchResults = await withTimeout(classifierPlugin.proxy.detectClassifications(this.prepareSecondaryFrames(croppedRegions)), DETECT_TIMEOUT_MS, 'Classifier');
|
|
1753
|
-
const allDetections = [];
|
|
1754
|
-
for (let i = 0; i < batchResults.length; i++) {
|
|
1755
|
-
const parentTrackId = 'trackId' in croppedRegions[i].detection ? croppedRegions[i].detection.trackId : undefined;
|
|
1756
|
-
for (const detection of batchResults[i].detections) {
|
|
1757
|
-
allDetections.push({ ...detection, box: this.transformBoxToOriginal(detection.box, croppedRegions[i]), parentTrackId });
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
if (allDetections.length === 0)
|
|
1761
|
-
return undefined;
|
|
1762
|
-
const deduped = this.pipeline.runNms(allDetections);
|
|
1763
|
-
if (deduped.length === 0)
|
|
1764
|
-
return undefined;
|
|
1765
|
-
return { pluginId, result: { detected: true, detections: deduped } };
|
|
1766
|
-
};
|
|
1767
|
-
promises.push(runClassifier());
|
|
1768
|
-
}
|
|
1769
|
-
return promises;
|
|
1000
|
+
const maxWidth = MOTION_WIDTH_MAP[this.motionResolution];
|
|
1001
|
+
const scaled = await this.frameScaler.scaleProportional(rawFrame, maxWidth, 'gray');
|
|
1002
|
+
return scaled ? this.frameScaler.toVideoFrameData(scaled) : undefined;
|
|
1770
1003
|
}
|
|
1771
|
-
async
|
|
1772
|
-
const
|
|
1773
|
-
if (!
|
|
1774
|
-
return undefined;
|
|
1775
|
-
const batchResults = await withTimeout(clipPlugin.proxy.detectEmbeddings(this.prepareSecondaryFrames(croppedRegions)), DETECT_TIMEOUT_MS, 'CLIP');
|
|
1776
|
-
const allEmbeddings = [];
|
|
1777
|
-
for (let i = 0; i < batchResults.length; i++) {
|
|
1778
|
-
if (batchResults[i].embeddings && batchResults[i].embeddings.length > 0) {
|
|
1779
|
-
const parentTrackId = 'trackId' in croppedRegions[i].detection ? croppedRegions[i].detection.trackId : undefined;
|
|
1780
|
-
for (const emb of batchResults[i].embeddings) {
|
|
1781
|
-
const transformed = this.transformBoxToOriginal(emb.box, croppedRegions[i]);
|
|
1782
|
-
allEmbeddings.push({ ...emb, box: transformed, parentTrackId });
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
if (allEmbeddings.length === 0)
|
|
1004
|
+
async scaleForObject(rawFrame) {
|
|
1005
|
+
const objectPlugin = this.plugins.get(SensorType.Object);
|
|
1006
|
+
if (!objectPlugin?.requiresFrames)
|
|
1787
1007
|
return undefined;
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
}
|
|
1791
|
-
async generateThumbnails(sourceFrame, scaler, usingHq, objectDetections, results) {
|
|
1792
|
-
const thumbnails = [];
|
|
1793
|
-
const attributePadding = usingHq ? 0.4 : 0.75;
|
|
1794
|
-
const detectionMaxWidth = usingHq ? DETECTION_THUMB_HQ_MAX_WIDTH : DETECTION_THUMB_MAX_WIDTH;
|
|
1795
|
-
const detectionMinCrop = usingHq ? DETECTION_THUMB_HQ_MIN_CROP : DETECTION_THUMB_MIN_CROP;
|
|
1796
|
-
const detectionQuality = usingHq ? DETECTION_THUMB_HQ_QUALITY : undefined;
|
|
1797
|
-
// Object thumbnails — objectDetections are TrackedDetection[] when tracker ran
|
|
1798
|
-
const objectCrops = await scaler.cropToJPEG(sourceFrame, objectDetections, {
|
|
1799
|
-
maxWidth: detectionMaxWidth,
|
|
1800
|
-
padding: 0.3,
|
|
1801
|
-
minCrop: detectionMinCrop,
|
|
1802
|
-
quality: detectionQuality,
|
|
1803
|
-
});
|
|
1804
|
-
for (const crop of objectCrops) {
|
|
1805
|
-
const detection = objectDetections[crop.index];
|
|
1806
|
-
if (detection) {
|
|
1807
|
-
thumbnails.push({
|
|
1808
|
-
label: detection.label,
|
|
1809
|
-
score: detection.confidence,
|
|
1810
|
-
jpeg: crop.jpeg,
|
|
1811
|
-
area: detection.box.width * detection.box.height,
|
|
1812
|
-
onEdge: this.isOnEdge(detection.box),
|
|
1813
|
-
trackId: 'trackId' in detection ? detection.trackId : undefined,
|
|
1814
|
-
speed: 'trackSpeed' in detection ? detection.trackSpeed : undefined,
|
|
1815
|
-
});
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
if (results.face?.detections) {
|
|
1819
|
-
const faceCrops = await scaler.cropToJPEG(sourceFrame, results.face.detections, {
|
|
1820
|
-
maxWidth: ATTRIBUTE_THUMB_MAX_WIDTH,
|
|
1821
|
-
padding: attributePadding,
|
|
1822
|
-
minCrop: ATTRIBUTE_THUMB_MIN_CROP,
|
|
1823
|
-
});
|
|
1824
|
-
for (const crop of faceCrops) {
|
|
1825
|
-
const detection = results.face.detections[crop.index];
|
|
1826
|
-
if (detection) {
|
|
1827
|
-
// Inline on the detection so enrichSegment can persist unknown
|
|
1828
|
-
// faces to the face store.
|
|
1829
|
-
detection.thumbnail ??= crop.jpeg;
|
|
1830
|
-
thumbnails.push({
|
|
1831
|
-
label: `face:${detection.identity ?? 'unknown'}`,
|
|
1832
|
-
score: detection.confidence,
|
|
1833
|
-
jpeg: crop.jpeg,
|
|
1834
|
-
area: detection.box.width * detection.box.height,
|
|
1835
|
-
onEdge: this.isOnEdge(detection.box),
|
|
1836
|
-
});
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
if (results.licensePlate?.detections) {
|
|
1841
|
-
const plateCrops = await scaler.cropToJPEG(sourceFrame, results.licensePlate.detections, {
|
|
1842
|
-
maxWidth: ATTRIBUTE_THUMB_MAX_WIDTH,
|
|
1843
|
-
padding: attributePadding,
|
|
1844
|
-
minCrop: ATTRIBUTE_THUMB_MIN_CROP,
|
|
1845
|
-
});
|
|
1846
|
-
for (const crop of plateCrops) {
|
|
1847
|
-
const detection = results.licensePlate.detections[crop.index];
|
|
1848
|
-
if (detection) {
|
|
1849
|
-
thumbnails.push({
|
|
1850
|
-
label: `plate:${detection.plateText ?? 'unknown'}`,
|
|
1851
|
-
score: detection.confidence,
|
|
1852
|
-
jpeg: crop.jpeg,
|
|
1853
|
-
area: detection.box.width * detection.box.height,
|
|
1854
|
-
onEdge: this.isOnEdge(detection.box),
|
|
1855
|
-
});
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
if (results.classifiers) {
|
|
1860
|
-
for (const classifierResult of Object.values(results.classifiers)) {
|
|
1861
|
-
const clsCrops = await scaler.cropToJPEG(sourceFrame, classifierResult.detections, {
|
|
1862
|
-
maxWidth: detectionMaxWidth,
|
|
1863
|
-
padding: 0.3,
|
|
1864
|
-
minCrop: detectionMinCrop,
|
|
1865
|
-
quality: detectionQuality,
|
|
1866
|
-
});
|
|
1867
|
-
for (const crop of clsCrops) {
|
|
1868
|
-
const detection = classifierResult.detections[crop.index];
|
|
1869
|
-
if (detection) {
|
|
1870
|
-
thumbnails.push({
|
|
1871
|
-
label: `class:${detection.label}`,
|
|
1872
|
-
score: detection.confidence,
|
|
1873
|
-
jpeg: crop.jpeg,
|
|
1874
|
-
area: detection.box.width * detection.box.height,
|
|
1875
|
-
onEdge: this.isOnEdge(detection.box),
|
|
1876
|
-
});
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
return thumbnails;
|
|
1882
|
-
}
|
|
1883
|
-
transformBoxToOriginal(box, region) {
|
|
1884
|
-
// box coordinates are normalized (0-1) relative to the scaled frame (e.g. 320x320),
|
|
1885
|
-
// but must be mapped back through the original crop dimensions (before scaling)
|
|
1886
|
-
const pixelX = box.x * region.cropSize.width + region.offset.x;
|
|
1887
|
-
const pixelY = box.y * region.cropSize.height + region.offset.y;
|
|
1888
|
-
const pixelW = box.width * region.cropSize.width;
|
|
1889
|
-
const pixelH = box.height * region.cropSize.height;
|
|
1890
|
-
return {
|
|
1891
|
-
x: pixelX / region.originalSize.width,
|
|
1892
|
-
y: pixelY / region.originalSize.height,
|
|
1893
|
-
width: pixelW / region.originalSize.width,
|
|
1894
|
-
height: pixelH / region.originalSize.height,
|
|
1895
|
-
};
|
|
1896
|
-
}
|
|
1897
|
-
getNvrProxy() {
|
|
1898
|
-
if (this.nvrProxy)
|
|
1899
|
-
return Promise.resolve(this.nvrProxy);
|
|
1900
|
-
this.nvrProxyPromise ??= this.resolveNvrProxy().finally(() => {
|
|
1901
|
-
this.nvrProxyPromise = undefined;
|
|
1902
|
-
});
|
|
1903
|
-
return this.nvrProxyPromise;
|
|
1904
|
-
}
|
|
1905
|
-
async resolveNvrProxy() {
|
|
1906
|
-
try {
|
|
1907
|
-
const coreManagerProxy = this.proxy.createProxy(NamespaceManager.coreManagerNamespaces().coreManagerRpc);
|
|
1908
|
-
const plugin = await coreManagerProxy.getPlugin('@camera.ui/camera-ui-nvr');
|
|
1909
|
-
if (!plugin) {
|
|
1910
|
-
return undefined;
|
|
1911
|
-
}
|
|
1912
|
-
const ns = NamespaceManager.pluginNamespaces(plugin.id);
|
|
1913
|
-
const connection = this.proxy.createProxy(ns.pluginChildRpc, { isolatedConnection: true });
|
|
1914
|
-
this.nvrProxy = connection.proxy;
|
|
1915
|
-
return this.nvrProxy;
|
|
1916
|
-
}
|
|
1917
|
-
catch (error) {
|
|
1918
|
-
this.logger.error('NVR proxy resolution failed:', error);
|
|
1008
|
+
const inputSpec = objectPlugin.modelSpec?.input;
|
|
1009
|
+
if (!isVideoInputSpec(inputSpec))
|
|
1919
1010
|
return undefined;
|
|
1920
|
-
|
|
1011
|
+
const scaled = await this.frameScaler.scaleToSpec(rawFrame, inputSpec);
|
|
1012
|
+
return scaled ? this.frameScaler.toVideoFrameData(scaled) : undefined;
|
|
1921
1013
|
}
|
|
1922
1014
|
};
|
|
1923
1015
|
__decorate([
|