@camera.ui/server 2.0.10 → 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-ChR2IVti.js → 404-Dfyzm8bF.js} +1 -1
- package/dist/interface/assets/{About-CDQ55crC.js → About-B4Y3WP7T.js} +1 -1
- package/dist/interface/assets/{Adminpanel-CiJGwx7_.js → Adminpanel-DSk1uh2x.js} +1 -1
- package/dist/interface/assets/Automation-CTSfLcmF.js +2 -0
- package/dist/interface/assets/AutomationImport-B1P0T0Pe.js +2 -0
- 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-dx9vB5ht.js → CameraConsole-CVqgED5u.js} +2 -2
- 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-CrWmGvdE.js → ConfigActionCameraControl-BLROOU0q.js} +1 -1
- package/dist/interface/assets/{ConfigActionDelay-1kd69ysm.js → ConfigActionDelay-q0pOTOzh.js} +1 -1
- package/dist/interface/assets/{ConfigActionHttp-XY-r_hEX.js → ConfigActionHttp-DiMXKRGz.js} +1 -1
- package/dist/interface/assets/ConfigActionImageInput-u5xX9t3-.js +1 -0
- package/dist/interface/assets/{ConfigActionMqtt-DsX-q9M_.js → ConfigActionMqtt-BkZl3uYq.js} +1 -1
- package/dist/interface/assets/ConfigActionNotification-DIkFNlKe.js +1 -0
- package/dist/interface/assets/{ConfigActionOutput-DMYPThT1.js → ConfigActionOutput-CJF0F4cT.js} +1 -1
- package/dist/interface/assets/{ConfigActionPlugin-rCaB0HjK.js → ConfigActionPlugin-rQeJ4umG.js} +1 -1
- package/dist/interface/assets/ConfigActionSensor-BWHpkoMs.js +1 -0
- package/dist/interface/assets/{ConfigActionSnapshot-8vrwtN10.js → ConfigActionSnapshot-BlvQZl1R.js} +1 -1
- package/dist/interface/assets/{ConfigActionVariable-Dy8ZnhCO.js → ConfigActionVariable-BMV-BDAk.js} +1 -1
- package/dist/interface/assets/{ConfigConditionIfElse-DK9Z6-kN.js → ConfigConditionIfElse-C5geCT1S.js} +1 -1
- package/dist/interface/assets/ConfigConditionSensorState-8bIGEhQd.js +1 -0
- package/dist/interface/assets/{ConfigConditionSwitch-_Gxu6OYc.js → ConfigConditionSwitch-PwrCSnTQ.js} +1 -1
- package/dist/interface/assets/{ConfigConditionTime-Ce8AortY.js → ConfigConditionTime-DUt_dsr9.js} +1 -1
- package/dist/interface/assets/{ConfigSensorValueInput-BIiQ8NN6.js → ConfigSensorValueInput-CgZWxOMu.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerDetection-CCCi-Tof.js → ConfigTriggerDetection-D4UitpXI.js} +1 -1
- package/dist/interface/assets/ConfigTriggerGeofence-Ci-gkmjS.js +1 -0
- package/dist/interface/assets/{ConfigTriggerManual-DZPdLXoe.js → ConfigTriggerManual-_PAVLJxz.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerMqtt-4Gp-qhCg.js → ConfigTriggerMqtt-BtfXQ_Bh.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerSchedule-DQX6czW4.js → ConfigTriggerSchedule-CHyW4YQ1.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerSensor-CLnC2yvE.js → ConfigTriggerSensor-CZMplCYs.js} +1 -1
- package/dist/interface/assets/{ConfigTriggerSystem-Gae5o59O.js → ConfigTriggerSystem-OOffe8rK.js} +1 -1
- package/dist/interface/assets/ConfigTriggerWebhook-Baul6M1Y.js +1 -0
- package/dist/interface/assets/Console-MXiL0z_X.js +1 -0
- package/dist/interface/assets/{CuiActionButton-BAlSwXzv.js → CuiActionButton-WFz5I80P.js} +1 -1
- package/dist/interface/assets/{CuiAvatar-DeBKq6dT.js → CuiAvatar-BUwQnIaI.js} +1 -1
- package/dist/interface/assets/{CuiBBoxPlayground-yYOBCXTq.js → CuiBBoxPlayground-J7_8dCvo.js} +1 -1
- package/dist/interface/assets/CuiBottomSheet-BA-u9UtN.js +1 -0
- package/dist/interface/assets/{CuiBottomSheet-udULz0wI.js → CuiBottomSheet-CzG0n4Oa.js} +1 -1
- package/dist/interface/assets/CuiCameraCard-GH-DR5_s.js +1 -0
- package/dist/interface/assets/{CuiCameraPipCard-BxbUK-93.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-Dn1QSGrr.js +1 -0
- package/dist/interface/assets/CuiCameraTable-or8VpE34.js +2 -0
- package/dist/interface/assets/{CuiDataTable-CB7aMmp9.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-yMnZpSGr.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-D9QNUEmx.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--MOi9UUw.js → CuiTopNavbarItem-nGcC2em4.js} +1 -1
- package/dist/interface/assets/{CuiTopbarSlot-Bcw46PzA.js → CuiTopbarSlot-B6UMqYZl.js} +1 -1
- package/dist/interface/assets/Faces-cpO5vJbp.js +1 -0
- package/dist/interface/assets/FirstSteps-BHTcbs9O.js +1 -0
- package/dist/interface/assets/Instances-BXiVG2Dm.js +1 -0
- package/dist/interface/assets/Logs-hMg2QOGj.js +6 -0
- package/dist/interface/assets/{Menu-GbuCaA_6.js → Menu-BiPBCi8Z.js} +1 -1
- package/dist/interface/assets/OAuthDeviceFlow-Bia9UA3I.js +1 -0
- package/dist/interface/assets/Plugin-DfRaHAsi.js +1 -0
- package/dist/interface/assets/{PluginConsole-CLGkDl1F.js → PluginConsole-CRw4zTRM.js} +2 -2
- package/dist/interface/assets/PluginDetail-BYqji8yN.css +1 -0
- package/dist/interface/assets/PluginDetail-BqRoLZc7.js +2 -0
- package/dist/interface/assets/{PluginSchema-Dy9kDPML.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-is528I5i2.js → Settings-BpBmrGpS.js} +1 -1
- package/dist/interface/assets/SettingsAccount-8Kh1uQqn.js +1 -0
- package/dist/interface/assets/{SettingsAccount-Y7hpH8Y3.css → SettingsAccount-CBJ_h7eM.css} +1 -1
- package/dist/interface/assets/SettingsAccount-CjgYe5mF.js +1 -0
- package/dist/interface/assets/SettingsAppearance-3i3Ix7j-.js +1 -0
- package/dist/interface/assets/{SettingsAppearance-CGaG9lml.js → SettingsAppearance-EoBxclgt.js} +1 -1
- package/dist/interface/assets/SettingsBackup-Bpsbg6Pm.js +1 -0
- package/dist/interface/assets/SettingsMqtt-ZQolc9lR.js +1 -0
- 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/SettingsRemote-DYld6-1F.css +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-DW39Zo4d.js → VariableInput-CnmqCHf7.js} +1 -1
- package/dist/interface/assets/{VariableSuggestions-DwhFBKzJ.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/{Workers-E6LM_yAl.css → Workers-DmcQx0L8.css} +1 -1
- package/dist/interface/assets/{api-Br3Jmdj6.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-CNPkdTXX.js +1 -0
- package/dist/interface/assets/{automations-Dd-6vvka.js → automations-CHengR-B.js} +1 -1
- package/dist/interface/assets/automations-DhLADBhd.js +1 -0
- package/dist/interface/assets/bell-outline-CHhIZudN.js +1 -0
- package/dist/interface/assets/{cameras-DCMMwuui.js → cameras-DyF2T0be.js} +1 -1
- package/dist/interface/assets/{cameras.schema-99F6GE0u.js → cameras.schema-DBzgU4m6.js} +1 -1
- package/dist/interface/assets/cloudHandoff-LxdvSi1a.js +1 -0
- package/dist/interface/assets/{config-BiEjYldh.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-sq3mLBKa.js → deviceId-BYu9Q8Ay.js} +1 -1
- package/dist/interface/assets/{hasPermission-Dm18Z17u.js → hasPermission-CgyWyOhQ.js} +1 -1
- package/dist/interface/assets/instances-CRYtaTQS.js +1 -0
- package/dist/interface/assets/main-CgXkx5UO.js +3 -0
- package/dist/interface/assets/nodeDefinitions-DUlwYSC1.js +1 -0
- package/dist/interface/assets/{notifications-D87Lhlir.js → notifications-sejZCb0L.js} +1 -1
- package/dist/interface/assets/plugins-CCLFcD4c.js +1 -0
- package/dist/interface/assets/round-restart-alt-PDSZur-0.js +1 -0
- package/dist/interface/assets/{router-qrIK2v9X.css → router-C7b6u3Zu.css} +1 -1
- package/dist/interface/assets/router-DkMUvvnP.js +2 -0
- package/dist/interface/assets/{routerState-B08WcvfU.js → routerState-BR88YV1H.js} +1 -1
- package/dist/interface/assets/{server-D7bl4wSy.js → server-C7SETTy9.js} +1 -1
- package/dist/interface/assets/shares-Cy1SLL98.js +1 -0
- package/dist/interface/assets/{theme-BG6n3TRA.js → theme-DqbhRu7y.js} +1 -1
- package/dist/interface/assets/transit-connection-variant-DfztpRMW.js +1 -0
- package/dist/interface/assets/types-v8L66UlA.js +1 -0
- package/dist/interface/assets/{ui-Cv0M3koY.js → ui-g5rCwxUE.js} +1 -1
- package/dist/interface/assets/{useAppVersion-DCmhFeIs.js → useAppVersion-WfdoG9Ji.js} +1 -1
- package/dist/interface/assets/useBackupRestore-ootsFhdF.js +1 -0
- package/dist/interface/assets/{useBiometricCredentials-Tq1nEG8V.js → useBiometricCredentials-Ca75xEru.js} +1 -1
- package/dist/interface/assets/useCameraOptions-BpL2zbfV.js +1 -0
- package/dist/interface/assets/{useCuiCameraDrawer-BcAYizqy.js → useCuiCameraDrawer-BHUHw6W6.js} +1 -1
- package/dist/interface/assets/useCuiDialog-yVh5UWKF.js +2 -0
- package/dist/interface/assets/{useFileDownload-C5quSdZR.js → useFileDownload-BcaNWndS.js} +1 -1
- package/dist/interface/assets/useLogsSocket-BRSsUNqv.js +5 -0
- package/dist/interface/assets/useLogsSocket-CoA-Nq-_.css +1 -0
- package/dist/interface/assets/{useServerRestart-DnT0ZOYm.js → useServerRestart-PZhND5H_.js} +1 -1
- package/dist/interface/assets/{useSharedComposables-ffwWlpSL.js → useSharedComposables-USqd05AG.js} +1 -1
- package/dist/interface/assets/{useStreamStatus-CD18kTqt.js → useStreamStatus-nCx5ux1-.js} +1 -1
- package/dist/interface/assets/{useUpdateChannel-DbBXpySY.js → useUpdateChannel-3eHk3KXZ.js} +1 -1
- package/dist/interface/assets/{useUserOptions-Bb0ACtxn.js → useUserOptions-DVC4k8Zs.js} +1 -1
- package/dist/interface/assets/{users.schema-BGeyXreN.js → users.schema-B4fEuoKZ.js} +1 -1
- package/dist/interface/assets/vendor-cameraui-QuTJKoXE.js +2039 -0
- package/dist/interface/assets/vendor-core-CCcu_nmz.js +1 -0
- package/dist/interface/assets/{vendor-primevue-DB14kOh6.js → vendor-primevue-BdB7McKg.js} +84 -145
- package/dist/interface/assets/view-grid-BWJS0bBD.js +1 -0
- package/dist/interface/index.html +49 -48
- package/dist/interface/sw.js +1 -1
- package/dist/server/package.json +10 -10
- 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/base.py +10 -2
- package/dist/server/src/_camera_ui_tools/camera_ui_sdk/sensor/ptz.py +45 -2
- package/dist/server/src/api/controllers/instances.controller.d.ts +2 -2
- package/dist/server/src/api/controllers/instances.controller.js +7 -7
- package/dist/server/src/api/controllers/instances.controller.js.map +1 -1
- package/dist/server/src/api/controllers/plugins.controller.js +1 -1
- package/dist/server/src/api/controllers/plugins.controller.js.map +1 -1
- 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/database/types.d.ts +8 -0
- package/dist/server/src/api/routes/instances.routes.js +2 -1
- package/dist/server/src/api/routes/instances.routes.js.map +1 -1
- 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/config.schema.d.ts +2 -0
- package/dist/server/src/api/schemas/config.schema.js +1 -0
- package/dist/server/src/api/schemas/config.schema.js.map +1 -1
- package/dist/server/src/api/schemas/instances.schema.d.ts +4 -0
- package/dist/server/src/api/schemas/instances.schema.js +5 -0
- package/dist/server/src/api/schemas/instances.schema.js.map +1 -1
- package/dist/server/src/api/schemas/remote.schema.d.ts +1 -1
- package/dist/server/src/api/services/backupScheduler.service.js +1 -1
- package/dist/server/src/api/services/backupScheduler.service.js.map +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/instances.service.d.ts +12 -3
- package/dist/server/src/api/services/instances.service.js +123 -36
- package/dist/server/src/api/services/instances.service.js.map +1 -1
- package/dist/server/src/api/services/plugins.service.js +29 -6
- package/dist/server/src/api/services/plugins.service.js.map +1 -1
- package/dist/server/src/api/services/virtualsensors.service.js +2 -2
- package/dist/server/src/api/services/virtualsensors.service.js.map +1 -1
- package/dist/server/src/api/types/index.d.ts +4 -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/api/websocket/nsp/server.d.ts +1 -0
- package/dist/server/src/api/websocket/nsp/server.js +3 -1
- package/dist/server/src/api/websocket/nsp/server.js.map +1 -1
- package/dist/server/src/camera/controller.d.ts +2 -1
- package/dist/server/src/camera/controller.js +19 -16
- 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 +37 -97
- package/dist/server/src/camera/decoder/detection-coordinator.js +492 -1369
- 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.d.ts → sources/frame-source.d.ts} +4 -0
- package/dist/server/src/camera/decoder/{frame-source.js → sources/frame-source.js} +30 -12
- 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.d.ts +1 -0
- package/dist/server/src/camera/decoder/worker.js +11 -7
- package/dist/server/src/camera/decoder/worker.js.map +1 -1
- package/dist/server/src/camera/index.d.ts +5 -1
- package/dist/server/src/camera/index.js.map +1 -1
- package/dist/server/src/camera/sensors/controller.js +4 -3
- package/dist/server/src/camera/sensors/controller.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/go2rtc/api/routes/snapshot.d.ts +3 -3
- package/dist/server/src/go2rtc/api/routes/snapshot.js +1 -1
- package/dist/server/src/go2rtc/api/routes/snapshot.js.map +1 -1
- package/dist/server/src/go2rtc/types.d.ts +3 -0
- package/dist/server/src/go2rtc/types.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/mqtt/bridge.js +1 -1
- package/dist/server/src/mqtt/bridge.js.map +1 -1
- package/dist/server/src/plugins/schema.js +1 -1
- package/dist/server/src/plugins/schema.js.map +1 -1
- package/dist/server/src/remote/services/cloudflare-managed.js +16 -5
- package/dist/server/src/remote/services/cloudflare-managed.js.map +1 -1
- package/dist/server/src/remote/services/cloudflare.d.ts +2 -2
- package/dist/server/src/remote/services/cloudflare.js +16 -17
- package/dist/server/src/remote/services/cloudflare.js.map +1 -1
- package/dist/server/src/remote/services/cloudflaredBinary.d.ts +2 -0
- package/dist/server/src/remote/services/cloudflaredBinary.js +20 -0
- package/dist/server/src/remote/services/cloudflaredBinary.js.map +1 -0
- package/dist/server/src/remote/services/index.d.ts +1 -0
- package/dist/server/src/remote/services/index.js +20 -5
- package/dist/server/src/remote/services/index.js.map +1 -1
- package/dist/server/src/remote/services/urlReadiness.d.ts +11 -0
- package/dist/server/src/remote/services/urlReadiness.js +129 -0
- package/dist/server/src/remote/services/urlReadiness.js.map +1 -0
- package/dist/server/src/rpc/interfaces/device.d.ts +6 -0
- package/dist/server/src/services/config/defaults.js +1 -0
- package/dist/server/src/services/config/defaults.js.map +1 -1
- package/dist/server/src/services/config/types.d.ts +1 -0
- package/dist/server/src/utils/npm/index.d.ts +11 -2
- package/dist/server/src/utils/npm/index.js +14 -4
- package/dist/server/src/utils/npm/index.js.map +1 -1
- package/dist/server/src/utils/pythonInstaller.js +3 -3
- package/dist/server/src/utils/pythonInstaller.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 +10 -10
- package/dist/interface/assets/Automation-Kt6pl4MS.js +0 -2
- package/dist/interface/assets/AutomationImport-DWQkfeuR.js +0 -2
- package/dist/interface/assets/AutomationStore-FbQdggSq.js +0 -2
- package/dist/interface/assets/AutomationStoreDetail-D15G_MQy.js +0 -2
- package/dist/interface/assets/Automations-urwh-nzl.js +0 -2
- package/dist/interface/assets/Camera-Brp9vdg2.js +0 -2
- package/dist/interface/assets/Cameras-DDut77Xv.js +0 -652
- package/dist/interface/assets/Camview-ChimIRuL.js +0 -1
- package/dist/interface/assets/Config-Cy5j6yf_.js +0 -2
- package/dist/interface/assets/ConfigActionImageInput-DJovQd4V.js +0 -1
- package/dist/interface/assets/ConfigActionNotification-BM2X-K-D.js +0 -1
- package/dist/interface/assets/ConfigActionSensor-gxpOX2DC.js +0 -1
- package/dist/interface/assets/ConfigConditionSensorState-BaFj9WYl.js +0 -1
- package/dist/interface/assets/ConfigTriggerGeofence-DOxaQSWM.js +0 -1
- package/dist/interface/assets/ConfigTriggerWebhook-Czyo2Bs-.js +0 -1
- package/dist/interface/assets/Console-DBJHlPg2.js +0 -1
- package/dist/interface/assets/CuiBottomSheet-B1jcfI0o.js +0 -1
- package/dist/interface/assets/CuiCameraCard-HEi3Hoe-.js +0 -1
- package/dist/interface/assets/CuiCameraRecordings-C5bd4VAf.js +0 -1
- package/dist/interface/assets/CuiCameraShares-BNjnm9yr.js +0 -1
- package/dist/interface/assets/CuiCameraSnapshot-C-iPOB3g.js +0 -1
- package/dist/interface/assets/CuiCameraTable-CDpfm8fi.js +0 -2
- package/dist/interface/assets/CuiFloatingButton-DdI-E8dh.js +0 -1
- package/dist/interface/assets/CuiImage-Bxf7z2N4.css +0 -1
- package/dist/interface/assets/CuiImage-DBQAYfn1.js +0 -1
- package/dist/interface/assets/CuiMenu--tB8FqMF.js +0 -1
- package/dist/interface/assets/CuiPluginCard-Cb0XSLIr.js +0 -2
- package/dist/interface/assets/CuiPluginOAuthButton-CuppMPTq.js +0 -2
- package/dist/interface/assets/CuiPluginTrustBadge-Bcnptf84.js +0 -1
- package/dist/interface/assets/CuiRecordingsGrid-DlKUfbeS.js +0 -1
- package/dist/interface/assets/CuiSchema-B6q285jE.js +0 -1
- package/dist/interface/assets/Faces-DtJmIku2.js +0 -1
- package/dist/interface/assets/FirstSteps-CsMSK_YI.js +0 -1
- package/dist/interface/assets/Instances-BTNzUfGz.js +0 -1
- package/dist/interface/assets/Logs-Bic2YIta.js +0 -6
- package/dist/interface/assets/OAuthDeviceFlow-DwSJHrqc.js +0 -1
- package/dist/interface/assets/Plugin-B8656Gzo.js +0 -1
- package/dist/interface/assets/PluginDetail-BsIkPUY8.css +0 -1
- package/dist/interface/assets/PluginDetail-DS8wUbYr.js +0 -2
- package/dist/interface/assets/PluginSearch-DBdp5Ifo.js +0 -1
- package/dist/interface/assets/PluginSearch-DFtTwd_R.js +0 -2
- package/dist/interface/assets/Plugins-C_kdqAp4.js +0 -1
- package/dist/interface/assets/Recordings-Cz4uWsI9.js +0 -1
- package/dist/interface/assets/SettingsAccount-CALY2rH0.js +0 -1
- package/dist/interface/assets/SettingsAccount-_zSgGuBE.js +0 -1
- package/dist/interface/assets/SettingsAppearance-z9tBty3q.js +0 -1
- package/dist/interface/assets/SettingsBackup-JI_xqcVS.js +0 -1
- package/dist/interface/assets/SettingsMqtt-B_tzet_5.js +0 -1
- package/dist/interface/assets/SettingsNotifications-FWWveQMH.js +0 -2
- package/dist/interface/assets/SettingsRecordings-D-S7rofE.js +0 -1
- package/dist/interface/assets/SettingsRemote-CVuDUBMM.js +0 -1
- package/dist/interface/assets/SettingsRemote-T38YVHal.css +0 -1
- package/dist/interface/assets/SettingsSystem-J7ZT7oRr.js +0 -2
- package/dist/interface/assets/SettingsUsers-DvuiYjNg.js +0 -1
- package/dist/interface/assets/Terminal-CNzw8wk3.js +0 -4
- package/dist/interface/assets/VersionsHandler-CsXazSWK.js +0 -7
- package/dist/interface/assets/Workers-BXH8IccG.js +0 -2
- package/dist/interface/assets/api-C31uUd3g.css +0 -2
- package/dist/interface/assets/api-DR6W2XWy.js +0 -2
- package/dist/interface/assets/auth-BJf6VFls.js +0 -1
- package/dist/interface/assets/automations-Dt5J3kkY.js +0 -1
- package/dist/interface/assets/cloudHandoff-Yc4FJv_l.js +0 -1
- package/dist/interface/assets/instances-MscZjRfc.js +0 -1
- package/dist/interface/assets/main-4pJkMgxg.js +0 -3
- package/dist/interface/assets/nodeDefinitions-5BjF3SJ8.js +0 -1
- package/dist/interface/assets/plugins-VeCjoaEi.js +0 -1
- package/dist/interface/assets/round-restart-alt-CcJGggln.js +0 -1
- package/dist/interface/assets/router-BFG1ClJo.js +0 -2
- package/dist/interface/assets/shares-D5xo0sht.js +0 -1
- package/dist/interface/assets/transit-connection-variant-CXkATe8I.js +0 -1
- package/dist/interface/assets/types-Dyn_Sb2s.js +0 -1
- package/dist/interface/assets/useBackupRestore-B-yCV4DL.js +0 -1
- package/dist/interface/assets/useCameraOptions-B2lIc9JO.js +0 -1
- package/dist/interface/assets/useCuiDialog-DLoFzodG.js +0 -2
- package/dist/interface/assets/useLogsSocket-4F2-d9fa.css +0 -1
- package/dist/interface/assets/useLogsSocket-B1sCzRXK.js +0 -5
- package/dist/interface/assets/vendor-cameraui-DDdtrYBn.js +0 -1978
- package/dist/interface/assets/vendor-core-CZuWsjWr.js +0 -1
- package/dist/interface/assets/view-grid-DRwNJzxD.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
|
@@ -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;
|
|
@@ -165,25 +66,42 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
165
66
|
snapshotUrl: config.snapshotUrl,
|
|
166
67
|
fps: this.config.frameWorkerSettings.fps,
|
|
167
68
|
};
|
|
69
|
+
if (config.controllerSnapshotSourceId) {
|
|
70
|
+
const sourceId = config.controllerSnapshotSourceId;
|
|
71
|
+
const controllerProxy = this.proxy.createProxy(NamespaceManager.cameraNamespaces(config.cameraId).cameraControllerRpc);
|
|
72
|
+
frameSourceConfig.snapshotProvider = async () => {
|
|
73
|
+
const jpeg = await controllerProxy.snapshot(sourceId, true);
|
|
74
|
+
if (!jpeg || jpeg.byteLength === 0)
|
|
75
|
+
return null;
|
|
76
|
+
return Buffer.from(jpeg);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
168
79
|
this.frameSource = new FrameSource(frameSourceConfig, logger);
|
|
169
80
|
this.frameScaler = new FrameScaler(null, logger);
|
|
170
81
|
this.pipeline = new DetectionPipeline(config.zones, config.detectionSettings);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
this.hqSnapshotSource = new SnapshotSource({ url: hqUrl }, logger);
|
|
174
|
-
}
|
|
175
|
-
this.syncHqSnapshotSource();
|
|
82
|
+
this.stationary = new StationarySuppressor(logger);
|
|
83
|
+
this.secondaries = new SecondaryStage(this.plugins, this.pipeline, this.frameScaler, this.proxy, logger, () => !this.loopRunning);
|
|
176
84
|
if (config.lines.length > 0) {
|
|
177
85
|
this.pipeline.updateLines(config.lines, this.videoAspectRatio);
|
|
178
86
|
}
|
|
179
87
|
this.eventManager = new DetectionEventManager(config.cameraId, this.proxy, this.logger);
|
|
180
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);
|
|
181
98
|
this.ptzAutotracker = new PtzAutotracker({
|
|
182
99
|
logger: this.logger,
|
|
183
100
|
proxy: this.proxy,
|
|
184
101
|
cameraId: this.config.cameraId,
|
|
185
102
|
settings: this.config.ptzAutotrack,
|
|
186
|
-
|
|
103
|
+
getFps: () => this.targetFps,
|
|
104
|
+
onSuppressionActivated: () => this.handleSuppressionActivated(),
|
|
187
105
|
});
|
|
188
106
|
this.cascadeUnsubscribe = this.cascade.onChange((event) => {
|
|
189
107
|
if (event.type === 'activated') {
|
|
@@ -199,8 +117,7 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
199
117
|
this.dwellUnsubscribe = this.dwell.onChange((event) => {
|
|
200
118
|
const isSensorTrigger = event.sensorId.startsWith('trigger:');
|
|
201
119
|
if (event.state === 'activated') {
|
|
202
|
-
//
|
|
203
|
-
// Sensor triggers (contact/switch/light) manage their own properties.
|
|
120
|
+
// sensor triggers (contact/switch/light) manage their own detected/blocked
|
|
204
121
|
if (!isSensorTrigger) {
|
|
205
122
|
this.writeSensorProperties(event.sensorId, {
|
|
206
123
|
detected: true,
|
|
@@ -220,8 +137,204 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
220
137
|
}
|
|
221
138
|
});
|
|
222
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
|
+
}
|
|
223
336
|
handleCascadeDeactivated() {
|
|
224
|
-
//
|
|
337
|
+
// clear cascade-gated buffer slots, keep motion/audio
|
|
225
338
|
const cs = this.currentDetectionState;
|
|
226
339
|
cs.object = undefined;
|
|
227
340
|
cs.face = undefined;
|
|
@@ -230,31 +343,46 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
230
343
|
cs.clip = undefined;
|
|
231
344
|
cs.lineCrossings = undefined;
|
|
232
345
|
cs.cascadeTriggered = undefined;
|
|
233
|
-
//
|
|
346
|
+
// publish empty detections so the UI clears stale bboxes
|
|
234
347
|
const clearTargets = [];
|
|
235
|
-
for (const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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);
|
|
239
352
|
}
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
for (const plugin of classifierPlugins.values()) {
|
|
243
|
-
clearTargets.push({ sensorId: plugin.sensorId, type: SensorType.Classifier });
|
|
244
|
-
}
|
|
353
|
+
for (const plugin of this.plugins.getAll(SensorType.Classifier)) {
|
|
354
|
+
clearTargets.push(plugin.sensorId);
|
|
245
355
|
}
|
|
246
|
-
for (const
|
|
356
|
+
for (const sensorId of clearTargets) {
|
|
247
357
|
this.writeSensorProperties(sensorId, { detected: false, detections: [] });
|
|
358
|
+
this.secondaryBboxSeen.delete(sensorId);
|
|
248
359
|
}
|
|
249
|
-
//
|
|
360
|
+
// segment ends here, the event itself continues while motion is active
|
|
250
361
|
this.eventManager.processResults(this.buildSnapshot());
|
|
251
362
|
}
|
|
252
363
|
handleEventEnded() {
|
|
253
|
-
this.
|
|
254
|
-
|
|
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();
|
|
255
372
|
this.activeSensorTriggerTypes.clear();
|
|
256
373
|
this.currentDetectionState = {};
|
|
257
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
|
+
}
|
|
258
386
|
writeSensorProperties(sensorId, properties) {
|
|
259
387
|
const msg = {
|
|
260
388
|
sensorId,
|
|
@@ -301,9 +429,12 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
301
429
|
return result;
|
|
302
430
|
}
|
|
303
431
|
applyExternalDetectionFilters(sensorType, properties) {
|
|
304
|
-
const
|
|
305
|
-
if (!Array.isArray(
|
|
432
|
+
const raw = properties.detections;
|
|
433
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
306
434
|
return properties;
|
|
435
|
+
// older/third-party plugins can send box-less detections, but the zone
|
|
436
|
+
// filter and rust merge assume a box on every detection
|
|
437
|
+
const detections = raw.map((detection) => (detection.box ? detection : { ...detection, box: { x: 0, y: 0, width: 1, height: 1 } }));
|
|
307
438
|
let filtered;
|
|
308
439
|
switch (sensorType) {
|
|
309
440
|
case SensorType.Face:
|
|
@@ -318,7 +449,7 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
318
449
|
filtered = this.pipeline.runZoneFilter(detections);
|
|
319
450
|
break;
|
|
320
451
|
default:
|
|
321
|
-
return properties; //
|
|
452
|
+
return { ...properties, detections }; // audio etc, no zone filter
|
|
322
453
|
}
|
|
323
454
|
return {
|
|
324
455
|
...properties,
|
|
@@ -326,13 +457,10 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
326
457
|
detected: filtered.length > 0,
|
|
327
458
|
};
|
|
328
459
|
}
|
|
329
|
-
// PTZ-suppression gating happens upstream
|
|
330
|
-
// belongs in the full pipeline or autotracker-only.
|
|
460
|
+
// PTZ-suppression gating happens upstream, callers decide what gets in here
|
|
331
461
|
ingestDetectionResult(sensorType, sensorId, properties, pluginId) {
|
|
332
462
|
this.updateBufferForType(sensorType, properties, pluginId);
|
|
333
463
|
const detected = properties.detected === true;
|
|
334
|
-
// Motion/Audio: dwell drives the detected/blocked/lastTriggered lifecycle
|
|
335
|
-
// entirely (see dwell.onChange in the constructor); motion also cascades.
|
|
336
464
|
if (sensorType === SensorType.Motion && detected) {
|
|
337
465
|
const motionTimeout = this.config.detectionSettings.motion.timeout;
|
|
338
466
|
this.dwell.refresh(sensorId, motionTimeout);
|
|
@@ -348,44 +476,25 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
348
476
|
}
|
|
349
477
|
}
|
|
350
478
|
else if (sensorType === SensorType.Object && detected) {
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
// afterwards they're in seenStationaryTracks and don't count, so parked
|
|
354
|
-
// cars/bikes can't keep events alive forever. Moving tracks always count.
|
|
355
|
-
// 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
|
|
356
481
|
const detections = properties.detections ?? [];
|
|
357
|
-
|
|
358
|
-
for (const t of detections) {
|
|
359
|
-
if (t.trackLost)
|
|
360
|
-
continue;
|
|
361
|
-
const isStationary = (t.trackSpeed ?? 0) < STATIONARY_SPEED_THRESHOLD;
|
|
362
|
-
if (isStationary) {
|
|
363
|
-
if (t.trackId !== undefined && !this.seenStationaryTracks.has(t.trackId)) {
|
|
364
|
-
this.seenStationaryTracks.add(t.trackId);
|
|
365
|
-
hasActiveTrack = true;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
hasActiveTrack = true;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
if (hasActiveTrack) {
|
|
482
|
+
if (this.stationary.evaluate(detections)) {
|
|
373
483
|
this.dwell.refresh(sensorId, OBJECT_DWELL_SECONDS);
|
|
374
484
|
}
|
|
375
485
|
else {
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// 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
|
|
379
488
|
properties.detected = false;
|
|
380
489
|
if (this.currentDetectionState.object) {
|
|
381
490
|
this.currentDetectionState.object.detected = false;
|
|
382
491
|
this.currentDetectionState.object.detections = [];
|
|
383
492
|
}
|
|
493
|
+
this.stationary.logSuppressedOnce(detections);
|
|
384
494
|
}
|
|
385
495
|
}
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
// 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
|
|
389
498
|
let publishProps = properties;
|
|
390
499
|
if (sensorType === SensorType.Motion || sensorType === SensorType.Audio || sensorType === SensorType.Object) {
|
|
391
500
|
const { detected: _d, blocked: _b, lastTriggered: _t, ...rest } = properties;
|
|
@@ -394,7 +503,16 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
394
503
|
if (Object.keys(publishProps).length > 0) {
|
|
395
504
|
this.writeSensorProperties(sensorId, publishProps);
|
|
396
505
|
}
|
|
397
|
-
|
|
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
|
|
398
516
|
}
|
|
399
517
|
updateBufferForType(sensorType, properties, pluginId) {
|
|
400
518
|
const cs = this.currentDetectionState;
|
|
@@ -431,374 +549,104 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
431
549
|
break;
|
|
432
550
|
}
|
|
433
551
|
}
|
|
434
|
-
|
|
435
|
-
this.
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
this.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
this.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
await this.hqSnapshotSource?.stop();
|
|
464
|
-
this.ptzAutotracker.dispose();
|
|
465
|
-
this.cascadeUnsubscribe?.();
|
|
466
|
-
this.dwellUnsubscribe?.();
|
|
467
|
-
this.cascade.dispose();
|
|
468
|
-
this.dwell.dispose();
|
|
469
|
-
this.eventManager.destroy();
|
|
470
|
-
this.adHocVideoLoop = false;
|
|
471
|
-
this.frameScaler.dispose();
|
|
472
|
-
this.pipeline.cleanup();
|
|
473
|
-
this.currentDetectionState = {};
|
|
474
|
-
}
|
|
475
|
-
async onSensorAdded(sensor) {
|
|
476
|
-
if (DETECTION_SENSOR_TYPES.has(sensor.sensorType) && sensor.requiresFrames) {
|
|
477
|
-
await this.registerDetectionPluginInternal(sensor);
|
|
478
|
-
}
|
|
479
|
-
if (sensor.sensorType === SensorType.PTZ) {
|
|
480
|
-
await this.ptzAutotracker.bind({
|
|
481
|
-
pluginId: sensor.pluginId,
|
|
482
|
-
sensorId: sensor.sensorId,
|
|
483
|
-
capabilities: sensor.capabilities,
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
async onSensorRemoved(sensorId) {
|
|
488
|
-
await this.removeDetectionPluginBySensor(sensorId);
|
|
489
|
-
this.ptzAutotracker.unbind(sensorId);
|
|
490
|
-
const dwellKey = `trigger:${sensorId}`;
|
|
491
|
-
this.activeSensorTriggerTypes.delete(dwellKey);
|
|
492
|
-
this.dwell.clear(dwellKey);
|
|
493
|
-
this.dwell.clear(sensorId);
|
|
494
|
-
}
|
|
495
|
-
onSensorCapabilitiesChanged(sensorId, capabilities) {
|
|
496
|
-
this.ptzAutotracker.setCapabilities(sensorId, capabilities);
|
|
497
|
-
}
|
|
498
|
-
async registerDetectionPluginInternal(sensor) {
|
|
499
|
-
// Wait for any in-progress stop to complete before starting a new loop
|
|
500
|
-
if (this.videoStopPromise)
|
|
501
|
-
await this.videoStopPromise;
|
|
502
|
-
if (this.audioStopPromise)
|
|
503
|
-
await this.audioStopPromise;
|
|
504
|
-
const wasVideoNeeded = this.shouldVideoBeActive();
|
|
505
|
-
const wasAudioNeeded = this.shouldAudioBeActive();
|
|
506
|
-
const namespaces = NamespaceManager.sensorProviderNamespaces(sensor.pluginId, this.config.cameraId, sensor.sensorId);
|
|
507
|
-
const sensorProxy = this.proxy.createProxy(namespaces.sensorRpc);
|
|
508
|
-
const pluginInfo = {
|
|
509
|
-
pluginId: sensor.pluginId,
|
|
510
|
-
sensorId: sensor.sensorId,
|
|
511
|
-
sensorType: sensor.sensorType,
|
|
512
|
-
requiresFrames: sensor.requiresFrames,
|
|
513
|
-
modelSpec: sensor.modelSpec,
|
|
514
|
-
proxy: sensorProxy,
|
|
515
|
-
};
|
|
516
|
-
if (this.isMultiProvider(sensor.sensorType)) {
|
|
517
|
-
if (!this.multiPlugins.has(sensor.sensorType)) {
|
|
518
|
-
this.multiPlugins.set(sensor.sensorType, new Map());
|
|
519
|
-
}
|
|
520
|
-
const typePlugins = this.multiPlugins.get(sensor.sensorType);
|
|
521
|
-
if (typePlugins.has(sensor.pluginId))
|
|
522
|
-
return;
|
|
523
|
-
typePlugins.set(sensor.pluginId, pluginInfo);
|
|
524
|
-
this.logger.trace(`Multi-provider plugin registered: ${sensor.pluginId} for ${sensor.sensorType}`);
|
|
525
|
-
}
|
|
526
|
-
else {
|
|
527
|
-
if (this.singlePlugins.has(sensor.sensorType))
|
|
528
|
-
return;
|
|
529
|
-
this.singlePlugins.set(sensor.sensorType, pluginInfo);
|
|
530
|
-
this.logger.trace(`Plugin registered: ${sensor.pluginId} for ${sensor.sensorType}`);
|
|
531
|
-
}
|
|
532
|
-
if (!wasVideoNeeded && this.shouldVideoBeActive()) {
|
|
533
|
-
this.adHocVideoLoop = false;
|
|
534
|
-
this.startVideoLoop();
|
|
535
|
-
}
|
|
536
|
-
if (!wasAudioNeeded && this.shouldAudioBeActive()) {
|
|
537
|
-
this.startAudioLoop();
|
|
538
|
-
}
|
|
539
|
-
if (this.cascade.isActive) {
|
|
540
|
-
this.startAdHocVideoLoopIfNeeded();
|
|
541
|
-
}
|
|
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
|
+
}
|
|
542
581
|
}
|
|
543
582
|
async removeDetectionPluginBySensor(sensorId) {
|
|
544
|
-
const wasVideoNeeded = this.shouldVideoBeActive();
|
|
545
|
-
const wasAudioNeeded = this.shouldAudioBeActive();
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (plugin.sensorId === sensorId) {
|
|
549
|
-
this.singlePlugins.delete(sensorType);
|
|
550
|
-
removed = true;
|
|
551
|
-
this.logger.trace(`Plugin unregistered: ${plugin.pluginId} (${sensorType})`);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
for (const [sensorType, typePlugins] of this.multiPlugins) {
|
|
555
|
-
for (const [pluginId, plugin] of typePlugins) {
|
|
556
|
-
if (plugin.sensorId === sensorId) {
|
|
557
|
-
typePlugins.delete(pluginId);
|
|
558
|
-
removed = true;
|
|
559
|
-
this.logger.trace(`Multi-provider plugin unregistered: ${pluginId} (${sensorType})`);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
if (typePlugins.size === 0)
|
|
563
|
-
this.multiPlugins.delete(sensorType);
|
|
564
|
-
}
|
|
565
|
-
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)
|
|
566
587
|
return;
|
|
567
|
-
|
|
588
|
+
for (const plugin of removed) {
|
|
589
|
+
this.logger.trace(`Plugin unregistered: ${plugin.pluginId} (${plugin.sensorType})`);
|
|
590
|
+
}
|
|
591
|
+
if (!this.plugins.has(SensorType.Object)) {
|
|
568
592
|
this.adHocVideoLoop = false;
|
|
569
593
|
}
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
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()) {
|
|
573
597
|
this.adHocVideoLoop = false;
|
|
574
598
|
this.logger.debug('Stopping ad-hoc video loop — last frame-based consumer removed');
|
|
575
599
|
this.stopVideoLoop();
|
|
576
600
|
}
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
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) {
|
|
580
604
|
await this.stopVideoLoop();
|
|
581
605
|
}
|
|
582
|
-
if (wasAudioNeeded && !this.shouldAudioBeActive()) {
|
|
583
|
-
await this.
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
async reportSensorWrite(sensorId, sensorType, properties) {
|
|
587
|
-
if (sensorType === SensorType.Object) {
|
|
588
|
-
const filtered = this.applyExternalDetectionFilters(sensorType, properties);
|
|
589
|
-
// Always feed the autotracker (even during suppression, so it can decide
|
|
590
|
-
// when to stop). External writes are presence-only — the presence channel
|
|
591
|
-
// holds the return-to-home timer without driving the motor.
|
|
592
|
-
const externalDetections = filtered.detections ?? [];
|
|
593
|
-
this.ptzAutotracker.handlePresenceDetections(externalDetections);
|
|
594
|
-
// During suppression tracker/pipeline/events stay gated — no stale
|
|
595
|
-
// bboxes or false events from the camera pose change.
|
|
596
|
-
if (this.ptzAutotracker.suppressionActive)
|
|
597
|
-
return;
|
|
598
|
-
this.ingestDetectionResult(SensorType.Object, sensorId, filtered);
|
|
599
|
-
this.eventManager.processResults(this.buildSnapshot());
|
|
600
|
-
if (!this.hasFrameBasedSecondary())
|
|
601
|
-
return;
|
|
602
|
-
if (this.processingExternalSecondary)
|
|
603
|
-
return; // busy-skip: previous RPC still running
|
|
604
|
-
this.processingExternalSecondary = true;
|
|
605
|
-
try {
|
|
606
|
-
// Wrap external boxes as TrackedDetection (shape adapter, no real
|
|
607
|
-
// tracking) so the secondary pipeline treats them like loop output.
|
|
608
|
-
const rawExternal = filtered.detections ?? [];
|
|
609
|
-
const objectDetections = this.pipeline.processExternal(rawExternal);
|
|
610
|
-
// Fast path: loop is running and has a frame → crop mode.
|
|
611
|
-
const peeked = this.frameSource.peekLatestFrame();
|
|
612
|
-
if (peeked) {
|
|
613
|
-
try {
|
|
614
|
-
const results = { timestamp: Date.now() };
|
|
615
|
-
await this.runSecondariesAndThumbnails(peeked.frame, objectDetections, results);
|
|
616
|
-
this.ingestResultsForAllSecondaries(results);
|
|
617
|
-
const snapshot = this.buildSnapshot();
|
|
618
|
-
if (results.thumbnails && results.thumbnails.length > 0) {
|
|
619
|
-
snapshot.thumbnails = results.thumbnails;
|
|
620
|
-
}
|
|
621
|
-
this.eventManager.processResults(snapshot);
|
|
622
|
-
}
|
|
623
|
-
finally {
|
|
624
|
-
await peeked[Symbol.asyncDispose]();
|
|
625
|
-
}
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
// Slow path: no loop frame yet → snapshot fetch, secondaries run
|
|
629
|
-
// full-frame (no per-detection crops).
|
|
630
|
-
const fetched = await this.frameSource.fetchSnapshotFrame();
|
|
631
|
-
if (!fetched)
|
|
632
|
-
return;
|
|
633
|
-
try {
|
|
634
|
-
const results = { timestamp: Date.now() };
|
|
635
|
-
const sceneJpeg = await this.runSecondariesFullFrame(fetched.frame, results);
|
|
636
|
-
this.ingestResultsForAllSecondaries(results);
|
|
637
|
-
const snapshot = this.buildSnapshot();
|
|
638
|
-
if (sceneJpeg)
|
|
639
|
-
snapshot.eventThumbnail = sceneJpeg;
|
|
640
|
-
const wantsEventThumb = this.eventManager.needsThumbnail() || this.snapshotWillStartEvent(snapshot);
|
|
641
|
-
this.eventManager.processResults(snapshot);
|
|
642
|
-
if (wantsEventThumb) {
|
|
643
|
-
this.upgradeEventThumbnailAsync();
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
finally {
|
|
647
|
-
await fetched[Symbol.asyncDispose]();
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
finally {
|
|
651
|
-
this.processingExternalSecondary = false;
|
|
652
|
-
}
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
if (sensorType === SensorType.Motion) {
|
|
656
|
-
// Fully gated during suppression — frame-diff is fake while the camera repositions.
|
|
657
|
-
if (this.ptzAutotracker.suppressionActive)
|
|
658
|
-
return;
|
|
659
|
-
const filtered = this.applyExternalDetectionFilters(sensorType, properties);
|
|
660
|
-
this.ingestDetectionResult(SensorType.Motion, sensorId, filtered);
|
|
661
|
-
this.eventManager.processResults(this.buildSnapshot());
|
|
662
|
-
this.fetchEventThumbnailAsync();
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
// External secondaries: direct ingest, no pixel work. Classifier (multi-
|
|
666
|
-
// provider) needs the owning pluginId for buffer keying.
|
|
667
|
-
let pluginId;
|
|
668
|
-
if (sensorType === SensorType.Classifier) {
|
|
669
|
-
const typePlugins = this.multiPlugins.get(SensorType.Classifier);
|
|
670
|
-
if (typePlugins) {
|
|
671
|
-
for (const plugin of typePlugins.values()) {
|
|
672
|
-
if (plugin.sensorId === sensorId) {
|
|
673
|
-
pluginId = plugin.pluginId;
|
|
674
|
-
break;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
const filtered = this.applyExternalDetectionFilters(sensorType, properties);
|
|
680
|
-
this.ingestDetectionResult(sensorType, sensorId, filtered, pluginId);
|
|
681
|
-
this.eventManager.processResults(this.buildSnapshot());
|
|
682
|
-
}
|
|
683
|
-
reportSensorTrigger(sensorId, triggerType, action, _sustained, timeoutSeconds) {
|
|
684
|
-
// Namespaced dwell key so trigger dwells don't overwrite the sensor's own
|
|
685
|
-
// detected/blocked properties (contact/switch/light manage those themselves).
|
|
686
|
-
const dwellKey = `trigger:${sensorId}`;
|
|
687
|
-
if (action === 'activate') {
|
|
688
|
-
this.activeSensorTriggerTypes.set(dwellKey, triggerType);
|
|
689
|
-
}
|
|
690
|
-
this.dwell.refresh(dwellKey, timeoutSeconds);
|
|
691
|
-
if (action === 'activate' && this.cascadeEnabled) {
|
|
692
|
-
this.cascade.triggerMomentary(this.cascadeTimeoutSeconds);
|
|
693
|
-
}
|
|
694
|
-
this.eventManager.processResults(this.buildSnapshot());
|
|
695
|
-
// Trigger-only path (doorbell, contact) has no loop frame to encode from —
|
|
696
|
-
// fetch the event thumb async; no-op if one was already captured.
|
|
697
|
-
if (action === 'activate') {
|
|
698
|
-
this.fetchEventThumbnailAsync();
|
|
606
|
+
if (wasAudioNeeded && !this.plugins.shouldAudioBeActive()) {
|
|
607
|
+
await this.audioLoop.stop();
|
|
699
608
|
}
|
|
700
609
|
}
|
|
701
|
-
|
|
702
|
-
const activeSet = new Set(activeSensorIds);
|
|
703
|
-
for (const dwellKey of this.activeSensorTriggerTypes.keys()) {
|
|
704
|
-
const sensorId = dwellKey.slice('trigger:'.length);
|
|
705
|
-
if (!activeSet.has(sensorId)) {
|
|
706
|
-
this.activeSensorTriggerTypes.delete(dwellKey);
|
|
707
|
-
this.dwell.clear(dwellKey);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
clearMotionStateForSuppression() {
|
|
610
|
+
handleSuppressionActivated() {
|
|
712
611
|
const cs = this.currentDetectionState;
|
|
713
612
|
cs.motion = undefined;
|
|
714
|
-
|
|
715
|
-
|
|
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)
|
|
716
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);
|
|
717
623
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
if (this.isMultiProvider(sensorType)) {
|
|
721
|
-
const typePlugins = this.multiPlugins.get(sensorType);
|
|
722
|
-
return typePlugins !== undefined && typePlugins.size > 0;
|
|
723
|
-
}
|
|
724
|
-
return this.singlePlugins.has(sensorType);
|
|
725
|
-
}
|
|
726
|
-
getPlugin(sensorType) {
|
|
727
|
-
return this.singlePlugins.get(sensorType);
|
|
728
|
-
}
|
|
729
|
-
getPluginsForType(sensorType) {
|
|
730
|
-
if (this.isMultiProvider(sensorType)) {
|
|
731
|
-
const typePlugins = this.multiPlugins.get(sensorType);
|
|
732
|
-
return typePlugins ? Array.from(typePlugins.values()) : [];
|
|
624
|
+
for (const plugin of this.plugins.getAll(SensorType.Classifier)) {
|
|
625
|
+
this.clearSecondaryBboxes(plugin.sensorId);
|
|
733
626
|
}
|
|
734
|
-
|
|
735
|
-
|
|
627
|
+
// image-space anchors don't survive a camera move, they re-form after
|
|
628
|
+
this.stationary.dropForCameraMove();
|
|
736
629
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
allPlugins.push(plugin);
|
|
741
|
-
for (const typePlugins of this.multiPlugins.values()) {
|
|
742
|
-
for (const plugin of typePlugins.values())
|
|
743
|
-
allPlugins.push(plugin);
|
|
744
|
-
}
|
|
745
|
-
return allPlugins;
|
|
630
|
+
clearSecondaryBboxes(sensorId) {
|
|
631
|
+
this.writeSensorProperties(sensorId, { detections: [] });
|
|
632
|
+
this.secondaryBboxSeen.delete(sensorId);
|
|
746
633
|
}
|
|
747
|
-
|
|
748
|
-
if (this.
|
|
749
|
-
return
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
+
}
|
|
753
642
|
}
|
|
754
|
-
return false;
|
|
755
|
-
}
|
|
756
|
-
isRunning() {
|
|
757
|
-
return this.loopRunning;
|
|
758
|
-
}
|
|
759
|
-
isStreaming() {
|
|
760
|
-
return this.frameSource.isStreaming;
|
|
761
|
-
}
|
|
762
|
-
hasSecondaryModelSpec(spec) {
|
|
763
|
-
return spec !== undefined && 'triggerLabels' in spec && Array.isArray(spec.triggerLabels);
|
|
764
|
-
}
|
|
765
|
-
isVideoInputSpec(spec) {
|
|
766
|
-
return spec != null && typeof spec === 'object' && 'width' in spec && 'height' in spec && 'format' in spec;
|
|
767
|
-
}
|
|
768
|
-
isOnEdge(box) {
|
|
769
|
-
return box.x < 0.02 || box.y < 0.02 || box.x + box.width > 0.98 || box.y + box.height > 0.98;
|
|
770
|
-
}
|
|
771
|
-
isAudioModelSpec(spec) {
|
|
772
|
-
return spec !== undefined && 'input' in spec && typeof spec.input === 'object' && 'sampleRate' in spec.input;
|
|
773
|
-
}
|
|
774
|
-
isMultiProvider(sensorType) {
|
|
775
|
-
return SENSOR_TYPE_CONFIG[sensorType]?.multiProvider === true;
|
|
776
|
-
}
|
|
777
|
-
// Video stream is permanently needed when: (1) frame-based motion detection, or
|
|
778
|
-
// (2) audio plugin + object plugin for continuous audio-triggered cascade.
|
|
779
|
-
// External motion (requiresFrames=false) and sensor cascade triggers use an ad-hoc
|
|
780
|
-
// video loop — started on-demand, stopped when cascade ends. This prevents blocking
|
|
781
|
-
// event-based cameras (e.g. Ring) that stop firing events while the stream is consumed.
|
|
782
|
-
shouldVideoBeActive() {
|
|
783
|
-
const motionPlugin = this.singlePlugins.get(SensorType.Motion);
|
|
784
|
-
const objectPlugin = this.singlePlugins.get(SensorType.Object);
|
|
785
|
-
const audioPlugin = this.singlePlugins.get(SensorType.Audio);
|
|
786
|
-
if (motionPlugin?.requiresFrames)
|
|
787
|
-
return true;
|
|
788
|
-
if (audioPlugin && objectPlugin?.requiresFrames)
|
|
789
|
-
return true;
|
|
790
|
-
return false;
|
|
791
|
-
}
|
|
792
|
-
shouldAudioBeActive() {
|
|
793
|
-
const audioPlugin = this.singlePlugins.get(SensorType.Audio);
|
|
794
|
-
return audioPlugin?.requiresFrames === true;
|
|
795
643
|
}
|
|
796
644
|
startVideoLoop() {
|
|
797
645
|
if (this.loopRunning || this.videoStopPromise)
|
|
798
646
|
return;
|
|
799
647
|
this.logger.debug('Starting video detection loop');
|
|
800
648
|
this.loopRunning = true;
|
|
801
|
-
this.
|
|
649
|
+
this.videoBackoff.reset();
|
|
802
650
|
this.loopPromise = this.runDetectionLoop();
|
|
803
651
|
}
|
|
804
652
|
async stopVideoLoop() {
|
|
@@ -822,316 +670,27 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
822
670
|
this.videoStopPromise = undefined;
|
|
823
671
|
}
|
|
824
672
|
}
|
|
825
|
-
startAudioLoop() {
|
|
826
|
-
if (this.audioLoopRunning || this.audioStopPromise)
|
|
827
|
-
return;
|
|
828
|
-
this.logger.debug('Starting audio detection loop');
|
|
829
|
-
this.audioLoopRunning = true;
|
|
830
|
-
this.audioReconnectCount = 0;
|
|
831
|
-
this.audioLoopPromise = this.runAudioDetectionLoop();
|
|
832
|
-
}
|
|
833
|
-
async stopAudioLoop() {
|
|
834
|
-
if (!this.audioLoopRunning) {
|
|
835
|
-
if (this.audioStopPromise)
|
|
836
|
-
await this.audioStopPromise;
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
this.logger.debug('Stopping audio detection loop');
|
|
840
|
-
this.audioLoopRunning = false;
|
|
841
|
-
const doStop = async () => {
|
|
842
|
-
await this.audioSource?.stop();
|
|
843
|
-
await this.audioLoopPromise;
|
|
844
|
-
this.audioLoopPromise = undefined;
|
|
845
|
-
};
|
|
846
|
-
this.audioStopPromise = doStop();
|
|
847
|
-
try {
|
|
848
|
-
await this.audioStopPromise;
|
|
849
|
-
}
|
|
850
|
-
finally {
|
|
851
|
-
this.audioStopPromise = undefined;
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
hasFrameBasedSecondary() {
|
|
855
|
-
const face = this.singlePlugins.get(SensorType.Face);
|
|
856
|
-
if (face?.requiresFrames)
|
|
857
|
-
return true;
|
|
858
|
-
const lpd = this.singlePlugins.get(SensorType.LicensePlate);
|
|
859
|
-
if (lpd?.requiresFrames)
|
|
860
|
-
return true;
|
|
861
|
-
const clip = this.singlePlugins.get(SensorType.Clip);
|
|
862
|
-
if (clip?.requiresFrames)
|
|
863
|
-
return true;
|
|
864
|
-
const classifiers = this.multiPlugins.get(SensorType.Classifier);
|
|
865
|
-
if (classifiers) {
|
|
866
|
-
for (const p of classifiers.values()) {
|
|
867
|
-
if (p.requiresFrames)
|
|
868
|
-
return true;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
return false;
|
|
872
|
-
}
|
|
873
|
-
needsAdHocLoop() {
|
|
874
|
-
const obj = this.singlePlugins.get(SensorType.Object);
|
|
875
|
-
if (!obj)
|
|
876
|
-
return false;
|
|
877
|
-
if (obj.requiresFrames)
|
|
878
|
-
return true;
|
|
879
|
-
return this.hasFrameBasedSecondary();
|
|
880
|
-
}
|
|
881
673
|
startAdHocVideoLoopIfNeeded() {
|
|
882
|
-
if (!this.loopRunning && this.needsAdHocLoop()) {
|
|
674
|
+
if (!this.loopRunning && this.plugins.needsAdHocLoop()) {
|
|
883
675
|
this.adHocVideoLoop = true;
|
|
884
676
|
this.logger.debug('Starting ad-hoc video loop for sensor cascade trigger');
|
|
885
677
|
this.startVideoLoop();
|
|
886
678
|
}
|
|
887
679
|
}
|
|
888
680
|
stopAdHocVideoLoopIfIdle() {
|
|
889
|
-
if (this.adHocVideoLoop && !this.shouldVideoBeActive()) {
|
|
681
|
+
if (this.adHocVideoLoop && !this.plugins.shouldVideoBeActive()) {
|
|
890
682
|
this.adHocVideoLoop = false;
|
|
891
683
|
this.logger.debug('Stopping ad-hoc video loop — cascade ended');
|
|
892
684
|
this.stopVideoLoop();
|
|
893
685
|
}
|
|
894
686
|
}
|
|
895
|
-
fetchEventThumbnailAsync() {
|
|
896
|
-
void (async () => {
|
|
897
|
-
try {
|
|
898
|
-
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
899
|
-
try {
|
|
900
|
-
const hqJpeg = await this.hqSnapshotSource?.snapshotJpeg(EVENT_THUMB_HQ_MAX_WIDTH, EVENT_THUMB_HQ_QUALITY);
|
|
901
|
-
if (hqJpeg) {
|
|
902
|
-
this.eventManager.publishEventThumbnail(hqJpeg);
|
|
903
|
-
return;
|
|
904
|
-
}
|
|
905
|
-
const handle = __addDisposableResource(env_1, await this.frameSource.fetchSnapshotFrame(), true);
|
|
906
|
-
if (!handle)
|
|
907
|
-
return;
|
|
908
|
-
const jpeg = await this.frameScaler.frameToJPEG(handle.frame, EVENT_THUMB_MAX_WIDTH);
|
|
909
|
-
if (!jpeg)
|
|
910
|
-
return;
|
|
911
|
-
this.eventManager.publishEventThumbnail(jpeg);
|
|
912
|
-
}
|
|
913
|
-
catch (e_1) {
|
|
914
|
-
env_1.error = e_1;
|
|
915
|
-
env_1.hasError = true;
|
|
916
|
-
}
|
|
917
|
-
finally {
|
|
918
|
-
const result_1 = __disposeResources(env_1);
|
|
919
|
-
if (result_1)
|
|
920
|
-
await result_1;
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
catch (e) {
|
|
924
|
-
this.logger.debug('event-thumb snapshot failed:', e);
|
|
925
|
-
}
|
|
926
|
-
})();
|
|
927
|
-
}
|
|
928
|
-
// Fallback: async snapshot replacement of a substream event thumbnail —
|
|
929
|
-
// only reached when the HQ source couldn't deliver a frame inline at event
|
|
930
|
-
// start (still connecting, buffer not warm yet).
|
|
931
|
-
upgradeEventThumbnailAsync() {
|
|
932
|
-
const source = this.hqSnapshotSource;
|
|
933
|
-
if (!source?.isRunning || !source.hasBuffer || this.hqUpgradeInflight)
|
|
934
|
-
return;
|
|
935
|
-
this.hqUpgradeInflight = true;
|
|
936
|
-
void (async () => {
|
|
937
|
-
try {
|
|
938
|
-
const jpeg = await source.snapshotJpeg(EVENT_THUMB_HQ_MAX_WIDTH, EVENT_THUMB_HQ_QUALITY);
|
|
939
|
-
if (jpeg && this.eventManager.hasActiveEvent()) {
|
|
940
|
-
this.eventManager.publishEventThumbnail(jpeg);
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
catch (error) {
|
|
944
|
-
this.logger.debug('Snapshot event thumbnail upgrade failed:', error);
|
|
945
|
-
}
|
|
946
|
-
finally {
|
|
947
|
-
this.hqUpgradeInflight = false;
|
|
948
|
-
}
|
|
949
|
-
})();
|
|
950
|
-
}
|
|
951
|
-
// Recent snapshot frame + its scaler (own hardware context); caller disposes
|
|
952
|
-
// the frame. With a reference size the aspect ratios must match — normalized
|
|
953
|
-
// boxes only transfer between streams showing the same sensor area.
|
|
954
|
-
async acquireHqFrame(reference) {
|
|
955
|
-
const source = this.hqSnapshotSource;
|
|
956
|
-
if (!source?.isRunning || !source.hasBuffer)
|
|
957
|
-
return null;
|
|
958
|
-
const scaler = source.scaler;
|
|
959
|
-
if (!scaler)
|
|
960
|
-
return null;
|
|
961
|
-
const frame = await source.getFrame(HQ_FRAME_MAX_AGE_MS);
|
|
962
|
-
if (!frame)
|
|
963
|
-
return null;
|
|
964
|
-
if (reference) {
|
|
965
|
-
const refAspect = reference.width / reference.height;
|
|
966
|
-
const hqAspect = frame.width / frame.height;
|
|
967
|
-
if (Math.abs(refAspect - hqAspect) / refAspect > 0.02) {
|
|
968
|
-
if (!this.hqAspectWarned) {
|
|
969
|
-
this.hqAspectWarned = true;
|
|
970
|
-
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`);
|
|
971
|
-
}
|
|
972
|
-
frame[Symbol.dispose]?.();
|
|
973
|
-
return null;
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
return { frame, scaler };
|
|
977
|
-
}
|
|
978
|
-
resolveHqSourceUrl(sources) {
|
|
979
|
-
if (!sources?.length)
|
|
980
|
-
return undefined;
|
|
981
|
-
const order = ['high-resolution', 'mid-resolution', 'low-resolution'];
|
|
982
|
-
const byRole = new Map(sources.map((s) => [s.role, s]));
|
|
983
|
-
// Detection consumes the LOWEST available role (worker: lowRes ?? midRes
|
|
984
|
-
// ?? streamSource) — the HQ companion is only worth a session when a
|
|
985
|
-
// strictly higher-resolution role exists.
|
|
986
|
-
const detectionRole = [...order].reverse().find((role) => byRole.has(role));
|
|
987
|
-
const hqRole = order.find((role) => byRole.has(role));
|
|
988
|
-
if (!detectionRole || !hqRole || hqRole === detectionRole)
|
|
989
|
-
return undefined;
|
|
990
|
-
return byRole.get(hqRole).url;
|
|
991
|
-
}
|
|
992
|
-
syncHqSnapshotSource() {
|
|
993
|
-
if (!this.hqSnapshotSource)
|
|
994
|
-
return;
|
|
995
|
-
const wanted = this.config.frameWorkerSettings.hqSnapshots === true;
|
|
996
|
-
if (wanted && !this.hqSnapshotSource.isRunning) {
|
|
997
|
-
this.hqSnapshotSource.start();
|
|
998
|
-
}
|
|
999
|
-
else if (!wanted && this.hqSnapshotSource.isRunning) {
|
|
1000
|
-
void this.hqSnapshotSource.stop();
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
calculateDBFS(data, format) {
|
|
1004
|
-
let sumSquares = 0;
|
|
1005
|
-
let sampleCount = 0;
|
|
1006
|
-
if (format === 'float32') {
|
|
1007
|
-
const floatView = new Float32Array(data.buffer, data.byteOffset, data.byteLength / 4);
|
|
1008
|
-
sampleCount = floatView.length;
|
|
1009
|
-
for (let i = 0; i < sampleCount; i++)
|
|
1010
|
-
sumSquares += floatView[i] * floatView[i];
|
|
1011
|
-
}
|
|
1012
|
-
else {
|
|
1013
|
-
// PCM16: normalize [-32768, 32767] → [-1.0, 1.0]
|
|
1014
|
-
const int16View = new Int16Array(data.buffer, data.byteOffset, data.byteLength / 2);
|
|
1015
|
-
sampleCount = int16View.length;
|
|
1016
|
-
for (let i = 0; i < sampleCount; i++) {
|
|
1017
|
-
const normalized = int16View[i] / 32768;
|
|
1018
|
-
sumSquares += normalized * normalized;
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
if (sampleCount === 0)
|
|
1022
|
-
return -100;
|
|
1023
|
-
const rms = Math.sqrt(sumSquares / sampleCount);
|
|
1024
|
-
return rms > 0 ? 20 * Math.log10(Math.max(rms, 1e-10)) : -100;
|
|
1025
|
-
}
|
|
1026
|
-
async runAudioDetectionLoop() {
|
|
1027
|
-
const audioPlugin = this.singlePlugins.get(SensorType.Audio);
|
|
1028
|
-
if (!audioPlugin?.requiresFrames)
|
|
1029
|
-
return;
|
|
1030
|
-
const audioSpec = this.isAudioModelSpec(audioPlugin.modelSpec) ? audioPlugin.modelSpec : undefined;
|
|
1031
|
-
const sampleRate = audioSpec?.input.sampleRate ?? 16000;
|
|
1032
|
-
const channels = audioSpec?.input.channels ?? 1;
|
|
1033
|
-
const format = audioSpec?.input.format ?? 'float32';
|
|
1034
|
-
const samplesPerFrame = audioSpec?.input.samplesPerFrame;
|
|
1035
|
-
const audioConfig = {
|
|
1036
|
-
streamUrl: this.config.audioStreamUrl,
|
|
1037
|
-
sampleRate,
|
|
1038
|
-
channels,
|
|
1039
|
-
format,
|
|
1040
|
-
samplesPerFrame,
|
|
1041
|
-
};
|
|
1042
|
-
while (this.audioLoopRunning) {
|
|
1043
|
-
try {
|
|
1044
|
-
this.audioSource = new AudioSource(audioConfig, this.logger);
|
|
1045
|
-
await this.audioSource.start();
|
|
1046
|
-
this.logger.debug('Audio stream connected, processing audio frames...');
|
|
1047
|
-
this.audioReconnectCount = 0;
|
|
1048
|
-
let frameCount = 0;
|
|
1049
|
-
let lastAudioFrameId = -1;
|
|
1050
|
-
while (this.audioLoopRunning) {
|
|
1051
|
-
const snap = await this.audioSource.nextFrame(lastAudioFrameId);
|
|
1052
|
-
if (!snap)
|
|
1053
|
-
break; // source ended (stop or EOF)
|
|
1054
|
-
lastAudioFrameId = snap.id;
|
|
1055
|
-
const rawFrame = snap.frame;
|
|
1056
|
-
frameCount++;
|
|
1057
|
-
try {
|
|
1058
|
-
const frameData = rawFrame.data;
|
|
1059
|
-
if (!frameData?.[0])
|
|
1060
|
-
continue;
|
|
1061
|
-
const rawData = frameData[0];
|
|
1062
|
-
const dBFS = this.calculateDBFS(rawData, format);
|
|
1063
|
-
if (dBFS < this.audioMinDecibels)
|
|
1064
|
-
continue;
|
|
1065
|
-
const audioFrame = {
|
|
1066
|
-
cameraId: this.config.cameraId,
|
|
1067
|
-
data: rawData,
|
|
1068
|
-
sampleRate,
|
|
1069
|
-
channels,
|
|
1070
|
-
format,
|
|
1071
|
-
timestamp: Date.now(),
|
|
1072
|
-
};
|
|
1073
|
-
const result = await audioPlugin.proxy.detectAudio(audioFrame);
|
|
1074
|
-
if (!result)
|
|
1075
|
-
continue;
|
|
1076
|
-
// Frame-based audio detection: cascade trigger when detected (Motion's
|
|
1077
|
-
// dwell counterpart for audio is handled in ingestDetectionResult).
|
|
1078
|
-
if (result.detected && this.cascadeEnabled) {
|
|
1079
|
-
const audioTimeout = this.config.detectionSettings.audio.timeout;
|
|
1080
|
-
this.cascade.triggerMomentary(audioTimeout);
|
|
1081
|
-
}
|
|
1082
|
-
// Funnel through the unified ingest path — same as external audio writes.
|
|
1083
|
-
this.ingestDetectionResult(SensorType.Audio, audioPlugin.sensorId, {
|
|
1084
|
-
detected: result.detected,
|
|
1085
|
-
detections: result.detections ?? [],
|
|
1086
|
-
...(result.decibels !== undefined ? { decibels: result.decibels } : {}),
|
|
1087
|
-
});
|
|
1088
|
-
}
|
|
1089
|
-
catch (error) {
|
|
1090
|
-
this.logger.error('Audio detection error:', error);
|
|
1091
|
-
}
|
|
1092
|
-
finally {
|
|
1093
|
-
try {
|
|
1094
|
-
rawFrame[Symbol.dispose]?.();
|
|
1095
|
-
}
|
|
1096
|
-
catch {
|
|
1097
|
-
// Best effort
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
const streamError = this.audioSource.lastError;
|
|
1102
|
-
await this.audioSource.stop();
|
|
1103
|
-
if (this.audioLoopRunning && streamError) {
|
|
1104
|
-
this.audioReconnectCount = Math.min(this.audioReconnectCount + 1, this.maxReconnectCount);
|
|
1105
|
-
const delay = this.audioReconnectCount >= this.maxReconnectCount ? this.maxReconnectDelay : this.normalReconnectDelay;
|
|
1106
|
-
this.logger.warn(`Audio stream ended with read error, reconnecting in ${delay / 1000}s: ${streamError.message}`);
|
|
1107
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1108
|
-
}
|
|
1109
|
-
else if (this.audioLoopRunning && frameCount === 0) {
|
|
1110
|
-
this.logger.debug('Audio stream ended without frames, waiting before reconnect...');
|
|
1111
|
-
await new Promise((resolve) => setTimeout(resolve, this.normalReconnectDelay));
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
catch (error) {
|
|
1115
|
-
if (!this.audioLoopRunning)
|
|
1116
|
-
break;
|
|
1117
|
-
await this.audioSource?.stop();
|
|
1118
|
-
this.audioReconnectCount = Math.min(this.audioReconnectCount + 1, this.maxReconnectCount);
|
|
1119
|
-
const delay = this.audioReconnectCount >= this.maxReconnectCount ? this.maxReconnectDelay : this.normalReconnectDelay;
|
|
1120
|
-
this.logger.warn(`Audio stream error, reconnecting in ${delay / 1000}s: ${error.message}`);
|
|
1121
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
await this.audioSource?.stop();
|
|
1125
|
-
this.audioSource = undefined;
|
|
1126
|
-
this.logger.debug('Audio detection loop ended');
|
|
1127
|
-
}
|
|
1128
687
|
async runDetectionLoop() {
|
|
1129
688
|
while (this.loopRunning) {
|
|
1130
689
|
try {
|
|
1131
690
|
await this.frameSource.start();
|
|
1132
691
|
this.frameScaler.updateHardwareContext(this.frameSource.hardwareContext);
|
|
1133
692
|
this.logger.debug('Stream connected, processing frames...');
|
|
1134
|
-
this.
|
|
693
|
+
this.videoBackoff.reset();
|
|
1135
694
|
let frameCount = 0;
|
|
1136
695
|
let lastFrameId = -1;
|
|
1137
696
|
while (this.loopRunning) {
|
|
@@ -1148,7 +707,7 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1148
707
|
snap.frame[Symbol.dispose]?.();
|
|
1149
708
|
}
|
|
1150
709
|
catch {
|
|
1151
|
-
//
|
|
710
|
+
// best-effort
|
|
1152
711
|
}
|
|
1153
712
|
}
|
|
1154
713
|
}
|
|
@@ -1156,14 +715,13 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1156
715
|
await this.frameSource.stop();
|
|
1157
716
|
this.frameScaler.clearCache();
|
|
1158
717
|
if (this.loopRunning && streamError) {
|
|
1159
|
-
|
|
1160
|
-
const delay = this.reconnectCount >= this.maxReconnectCount ? this.maxReconnectDelay : this.normalReconnectDelay;
|
|
718
|
+
const delay = this.videoBackoff.nextDelayMs();
|
|
1161
719
|
this.logger.warn(`Stream ended with read error, reconnecting in ${delay / 1000}s: ${streamError.message}`);
|
|
1162
|
-
await
|
|
720
|
+
await sleep(delay);
|
|
1163
721
|
}
|
|
1164
722
|
else if (this.loopRunning && frameCount === 0) {
|
|
1165
723
|
this.logger.debug('Stream ended without frames, waiting before reconnect...');
|
|
1166
|
-
await
|
|
724
|
+
await sleep(this.videoBackoff.idleDelayMs);
|
|
1167
725
|
}
|
|
1168
726
|
}
|
|
1169
727
|
catch (error) {
|
|
@@ -1171,10 +729,9 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1171
729
|
break;
|
|
1172
730
|
await this.frameSource.stop();
|
|
1173
731
|
this.frameScaler.clearCache();
|
|
1174
|
-
|
|
1175
|
-
const delay = this.reconnectCount >= this.maxReconnectCount ? this.maxReconnectDelay : this.normalReconnectDelay;
|
|
732
|
+
const delay = this.videoBackoff.nextDelayMs();
|
|
1176
733
|
this.logger.warn(`Stream error, reconnecting in ${delay / 1000}s: ${error.message}`);
|
|
1177
|
-
await
|
|
734
|
+
await sleep(delay);
|
|
1178
735
|
}
|
|
1179
736
|
}
|
|
1180
737
|
await this.frameSource.stop();
|
|
@@ -1182,7 +739,7 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1182
739
|
this.logger.debug('Detection loop ended');
|
|
1183
740
|
}
|
|
1184
741
|
async processRawFrame(rawFrame) {
|
|
1185
|
-
if (!this.loopRunning || !this.
|
|
742
|
+
if (!this.loopRunning || !this.plugins.hasAny())
|
|
1186
743
|
return;
|
|
1187
744
|
try {
|
|
1188
745
|
await this.processDetection(rawFrame);
|
|
@@ -1196,21 +753,18 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1196
753
|
let motionDetected = false;
|
|
1197
754
|
let objectDetections = [];
|
|
1198
755
|
const results = { timestamp: t0 };
|
|
1199
|
-
//
|
|
1200
|
-
// expire during the cascade window.
|
|
756
|
+
// keeps dead tracks from expiring during the cascade window
|
|
1201
757
|
if (this.cascade.isActive) {
|
|
1202
758
|
this.pipeline.refreshReid();
|
|
1203
759
|
}
|
|
1204
|
-
// PTZ
|
|
1205
|
-
//
|
|
1206
|
-
//
|
|
1207
|
-
// 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
|
|
1208
763
|
const ptzSuppressed = this.ptzAutotracker.suppressionActive;
|
|
1209
|
-
//
|
|
1210
|
-
//
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
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) {
|
|
1214
768
|
const motionFrame = await this.scaleForMotion(rawFrame);
|
|
1215
769
|
if (!this.loopRunning)
|
|
1216
770
|
return;
|
|
@@ -1219,13 +773,18 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1219
773
|
const result = await motionPlugin.proxy.detectMotion(motionFrame);
|
|
1220
774
|
if (!this.loopRunning)
|
|
1221
775
|
return;
|
|
1222
|
-
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) {
|
|
1223
782
|
const filtered = this.pipeline.runMergeAndZoneFilter(result.detections);
|
|
1224
783
|
motionDetected = filtered.length > 0;
|
|
1225
784
|
results.motion = { ...result, detections: filtered };
|
|
1226
785
|
}
|
|
1227
786
|
else {
|
|
1228
|
-
//
|
|
787
|
+
// record empty so the UI clears stale bboxes
|
|
1229
788
|
results.motion = { detected: false, detections: [] };
|
|
1230
789
|
}
|
|
1231
790
|
}
|
|
@@ -1237,22 +796,20 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1237
796
|
}
|
|
1238
797
|
}
|
|
1239
798
|
else if (this.cascade.isActive) {
|
|
1240
|
-
//
|
|
799
|
+
// no frame-based motion plugin, the cascade is the only motion-equivalent signal
|
|
1241
800
|
motionDetected = true;
|
|
1242
801
|
results.cascadeTriggered = true;
|
|
1243
802
|
}
|
|
1244
|
-
//
|
|
1245
|
-
// own flag is ignored on purpose, zone filtering wins.
|
|
803
|
+
// detected is re-derived after zone filtering, the plugin's own flag is ignored
|
|
1246
804
|
if (results.motion && motionPlugin) {
|
|
1247
805
|
this.ingestDetectionResult(SensorType.Motion, motionPlugin.sensorId, {
|
|
1248
806
|
detected: motionDetected,
|
|
1249
807
|
detections: results.motion.detections,
|
|
1250
808
|
});
|
|
1251
809
|
}
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1254
|
-
|
|
1255
|
-
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);
|
|
1256
813
|
const shouldDetectObjects = objectPlugin?.requiresFrames === true && (ptzSuppressed || !this.cascadeEnabled || this.cascade.isActive);
|
|
1257
814
|
if (shouldDetectObjects) {
|
|
1258
815
|
const objectFrame = await this.scaleForObject(rawFrame);
|
|
@@ -1260,17 +817,24 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1260
817
|
return;
|
|
1261
818
|
if (objectFrame) {
|
|
1262
819
|
try {
|
|
1263
|
-
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`);
|
|
1264
821
|
if (!this.loopRunning)
|
|
1265
822
|
return;
|
|
1266
|
-
//
|
|
1267
|
-
// state; the
|
|
1268
|
-
// 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
|
|
1269
825
|
const poseDelta = this.ptzAutotracker.consumePoseDelta();
|
|
1270
826
|
const pipelineResult = this.pipeline.process(result.detections, objectFrame, poseDelta);
|
|
1271
|
-
//
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
|
1274
838
|
const visibleTracks = pipelineResult.tracked.filter((t) => {
|
|
1275
839
|
if (!t.trackLost)
|
|
1276
840
|
return true;
|
|
@@ -1282,9 +846,8 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1282
846
|
results.object = { ...result, detections: visibleTracks };
|
|
1283
847
|
if (pipelineResult.crossings.length > 0)
|
|
1284
848
|
results.lineCrossings = pipelineResult.crossings;
|
|
1285
|
-
//
|
|
1286
|
-
//
|
|
1287
|
-
// 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
|
|
1288
851
|
this.ptzAutotracker.handleObjectDetections(pipelineResult.tracked);
|
|
1289
852
|
}
|
|
1290
853
|
catch (error) {
|
|
@@ -1294,14 +857,25 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1294
857
|
}
|
|
1295
858
|
}
|
|
1296
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
|
+
}
|
|
1297
873
|
if (results.lineCrossings && results.lineCrossings.length > 0) {
|
|
1298
874
|
for (const c of results.lineCrossings) {
|
|
1299
|
-
this.logger.
|
|
875
|
+
this.logger.trace(`Line crossing: ${c.label}#${c.trackId} crossed "${c.lineName}" dir=${c.direction} conf=${c.confidence.toFixed(2)}`);
|
|
1300
876
|
}
|
|
1301
|
-
// Buffer line crossings so the EventManager snapshot picks them up
|
|
1302
877
|
this.currentDetectionState.lineCrossings = results.lineCrossings;
|
|
1303
878
|
}
|
|
1304
|
-
// Same rule as motion: detected is re-derived from post-pipeline detections.
|
|
1305
879
|
if (results.object && objectPlugin) {
|
|
1306
880
|
this.ingestDetectionResult(SensorType.Object, objectPlugin.sensorId, {
|
|
1307
881
|
detected: objectDetections.length > 0,
|
|
@@ -1310,227 +884,67 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1310
884
|
}
|
|
1311
885
|
if (!this.loopRunning)
|
|
1312
886
|
return;
|
|
1313
|
-
// 3. Secondary detectors + per-detection thumbnails. Result-collection
|
|
1314
|
-
// only — the shared ingest below keeps loop tick and external-RPC path
|
|
1315
|
-
// on the same helper.
|
|
1316
887
|
if (objectDetections.length > 0) {
|
|
1317
888
|
await this.runSecondariesAndThumbnails(rawFrame, objectDetections, results);
|
|
1318
889
|
}
|
|
1319
890
|
if (!this.loopRunning)
|
|
1320
891
|
return;
|
|
1321
|
-
// 4. Unified ingest.
|
|
1322
892
|
this.ingestResultsForAllSecondaries(results);
|
|
1323
|
-
|
|
1324
|
-
//
|
|
893
|
+
this.clearStaleSecondaryBboxes();
|
|
894
|
+
// one flush per frame, after detectors and thumbnails, so segment-start
|
|
895
|
+
// messages already carry them
|
|
1325
896
|
if (!this.loopRunning)
|
|
1326
897
|
return;
|
|
1327
898
|
const snapshot = this.buildSnapshot();
|
|
1328
899
|
if (results.thumbnails && results.thumbnails.length > 0) {
|
|
1329
900
|
snapshot.thumbnails = results.thumbnails;
|
|
1330
901
|
}
|
|
1331
|
-
//
|
|
1332
|
-
//
|
|
1333
|
-
// the 'start' message carries it inline). Attaching is harmless either
|
|
1334
|
-
// way; EventManager only consumes it while needsEventThumbnail is true.
|
|
1335
|
-
// 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
|
|
1336
904
|
const wantsEventThumb = this.eventManager.needsThumbnail() || this.snapshotWillStartEvent(snapshot);
|
|
1337
905
|
let hqThumbAttached = false;
|
|
1338
906
|
if (wantsEventThumb) {
|
|
1339
907
|
try {
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
if (jpeg) {
|
|
1345
|
-
snapshot.eventThumbnail = jpeg;
|
|
1346
|
-
hqThumbAttached = true;
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
if (!snapshot.eventThumbnail) {
|
|
1350
|
-
const jpeg = await this.frameScaler.frameToJPEG(rawFrame, EVENT_THUMB_MAX_WIDTH);
|
|
1351
|
-
if (jpeg)
|
|
1352
|
-
snapshot.eventThumbnail = jpeg;
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
finally {
|
|
1356
|
-
hq?.frame[Symbol.dispose]?.();
|
|
1357
|
-
}
|
|
908
|
+
const thumb = await this.thumbnailer.captureEventThumbnail(rawFrame);
|
|
909
|
+
if (thumb.jpeg)
|
|
910
|
+
snapshot.eventThumbnail = thumb.jpeg;
|
|
911
|
+
hqThumbAttached = thumb.fromHq;
|
|
1358
912
|
}
|
|
1359
913
|
catch (error) {
|
|
1360
914
|
this.logger.error('Event thumbnail generation error:', error);
|
|
1361
915
|
}
|
|
1362
916
|
}
|
|
1363
917
|
this.eventManager.processResults(snapshot);
|
|
1364
|
-
//
|
|
1365
|
-
// 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
|
|
1366
919
|
if (wantsEventThumb && !hqThumbAttached) {
|
|
1367
|
-
this.upgradeEventThumbnailAsync();
|
|
920
|
+
this.thumbnailer.upgradeEventThumbnailAsync();
|
|
1368
921
|
}
|
|
1369
922
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
if (snapshot.motion?.detected)
|
|
1374
|
-
return true;
|
|
1375
|
-
if (snapshot.audio?.detected)
|
|
1376
|
-
return true;
|
|
1377
|
-
if (snapshot.cascadeTriggered)
|
|
1378
|
-
return true;
|
|
1379
|
-
if ((snapshot.sensorTriggers?.length ?? 0) > 0)
|
|
1380
|
-
return true;
|
|
1381
|
-
if ((snapshot.lineCrossings?.length ?? 0) > 0)
|
|
1382
|
-
return true;
|
|
1383
|
-
return false;
|
|
1384
|
-
}
|
|
1385
|
-
async resolveFaceIdentities(faces) {
|
|
1386
|
-
const withEmbeddings = faces.filter((f) => f.embedding?.length);
|
|
1387
|
-
if (!withEmbeddings.length)
|
|
1388
|
-
return;
|
|
1389
|
-
const facePlugin = this.singlePlugins.get(SensorType.Face);
|
|
1390
|
-
const embeddingModel = facePlugin?.modelSpec?.embeddingModel;
|
|
1391
|
-
if (!embeddingModel)
|
|
1392
|
-
return;
|
|
1393
|
-
const nvr = await this.getNvrProxy();
|
|
1394
|
-
if (!nvr)
|
|
923
|
+
async runSecondariesAndThumbnails(rawFrame, objectDetections, results) {
|
|
924
|
+
objectDetections = this.stationary.excludeSealed(objectDetections);
|
|
925
|
+
if (objectDetections.length === 0)
|
|
1395
926
|
return;
|
|
927
|
+
await this.secondaries.detect(rawFrame, objectDetections, results);
|
|
1396
928
|
try {
|
|
1397
|
-
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
withEmbeddings[i].identity = matches[i].identity;
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
catch (error) {
|
|
1406
|
-
if (!this.loopRunning || isNoRespondersError(error))
|
|
1407
|
-
return;
|
|
1408
|
-
this.logger.error('Could not resolve face identities:', error);
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
async scaleForMotion(rawFrame) {
|
|
1412
|
-
const motionPlugin = this.singlePlugins.get(SensorType.Motion);
|
|
1413
|
-
if (!motionPlugin?.requiresFrames)
|
|
1414
|
-
return undefined;
|
|
1415
|
-
const maxWidth = MOTION_WIDTH_MAP[this.motionResolution];
|
|
1416
|
-
const scaled = await this.frameScaler.scaleProportional(rawFrame, maxWidth, 'gray');
|
|
1417
|
-
return scaled ? this.frameScaler.toVideoFrameData(scaled) : undefined;
|
|
1418
|
-
}
|
|
1419
|
-
async scaleForObject(rawFrame) {
|
|
1420
|
-
const objectPlugin = this.singlePlugins.get(SensorType.Object);
|
|
1421
|
-
if (!objectPlugin?.requiresFrames)
|
|
1422
|
-
return undefined;
|
|
1423
|
-
const inputSpec = objectPlugin.modelSpec?.input;
|
|
1424
|
-
if (!this.isVideoInputSpec(inputSpec))
|
|
1425
|
-
return undefined;
|
|
1426
|
-
const scaled = await this.frameScaler.scaleToSpec(rawFrame, inputSpec);
|
|
1427
|
-
return scaled ? this.frameScaler.toVideoFrameData(scaled) : undefined;
|
|
1428
|
-
}
|
|
1429
|
-
async prepareSecondaryRegions(rawFrame, objectDetections) {
|
|
1430
|
-
const result = new Map();
|
|
1431
|
-
const consumers = [];
|
|
1432
|
-
const facePlugin = this.singlePlugins.get(SensorType.Face);
|
|
1433
|
-
if (facePlugin && this.hasSecondaryModelSpec(facePlugin.modelSpec) && this.isVideoInputSpec(facePlugin.modelSpec.input)) {
|
|
1434
|
-
consumers.push({ key: 'face', triggerLabels: facePlugin.modelSpec.triggerLabels, input: facePlugin.modelSpec.input });
|
|
1435
|
-
}
|
|
1436
|
-
const lpdPlugin = this.singlePlugins.get(SensorType.LicensePlate);
|
|
1437
|
-
if (lpdPlugin && this.hasSecondaryModelSpec(lpdPlugin.modelSpec) && this.isVideoInputSpec(lpdPlugin.modelSpec.input)) {
|
|
1438
|
-
consumers.push({ key: 'lpd', triggerLabels: lpdPlugin.modelSpec.triggerLabels, input: lpdPlugin.modelSpec.input });
|
|
1439
|
-
}
|
|
1440
|
-
const clipPlugin = this.singlePlugins.get(SensorType.Clip);
|
|
1441
|
-
if (clipPlugin && this.hasSecondaryModelSpec(clipPlugin.modelSpec) && this.isVideoInputSpec(clipPlugin.modelSpec.input)) {
|
|
1442
|
-
consumers.push({ key: 'clip', triggerLabels: clipPlugin.modelSpec.triggerLabels, input: clipPlugin.modelSpec.input });
|
|
1443
|
-
}
|
|
1444
|
-
const classifierPlugins = this.multiPlugins.get(SensorType.Classifier);
|
|
1445
|
-
if (classifierPlugins) {
|
|
1446
|
-
for (const [pluginId, classifierPlugin] of classifierPlugins) {
|
|
1447
|
-
if (this.hasSecondaryModelSpec(classifierPlugin.modelSpec) && this.isVideoInputSpec(classifierPlugin.modelSpec.input)) {
|
|
1448
|
-
consumers.push({ key: `classifier:${pluginId}`, triggerLabels: classifierPlugin.modelSpec.triggerLabels, input: classifierPlugin.modelSpec.input });
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
if (consumers.length === 0)
|
|
1453
|
-
return result;
|
|
1454
|
-
for (const detection of objectDetections) {
|
|
1455
|
-
const detLabel = detection.label.toLowerCase();
|
|
1456
|
-
const targets = [];
|
|
1457
|
-
for (const consumer of consumers) {
|
|
1458
|
-
if (consumer.triggerLabels.length === 0 || consumer.triggerLabels.some((l) => l.toLowerCase() === detLabel)) {
|
|
1459
|
-
targets.push({ key: consumer.key, width: consumer.input.width, height: consumer.input.height, format: consumer.input.format });
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
if (targets.length === 0)
|
|
1463
|
-
continue;
|
|
1464
|
-
// Consumers with identical dimensions share one scaled region
|
|
1465
|
-
const uniqueTargets = [];
|
|
1466
|
-
const dupeMap = new Map(); // sizeKey → [consumerKey, ...]
|
|
1467
|
-
for (const t of targets) {
|
|
1468
|
-
const sizeKey = `${t.width}x${t.height}_${t.format}`;
|
|
1469
|
-
const existing = dupeMap.get(sizeKey);
|
|
1470
|
-
if (existing) {
|
|
1471
|
-
existing.push(t.key);
|
|
1472
|
-
}
|
|
1473
|
-
else {
|
|
1474
|
-
dupeMap.set(sizeKey, [t.key]);
|
|
1475
|
-
uniqueTargets.push(t);
|
|
1476
|
-
}
|
|
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);
|
|
1477
933
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
const primaryKey = consumerKeys[0];
|
|
1481
|
-
const region = regions.get(primaryKey);
|
|
1482
|
-
if (!region)
|
|
1483
|
-
continue;
|
|
1484
|
-
for (const key of consumerKeys) {
|
|
1485
|
-
const list = result.get(key);
|
|
1486
|
-
if (list) {
|
|
1487
|
-
list.push(region);
|
|
1488
|
-
}
|
|
1489
|
-
else {
|
|
1490
|
-
result.set(key, [region]);
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
934
|
+
finally {
|
|
935
|
+
hq?.frame[Symbol.dispose]?.();
|
|
1493
936
|
}
|
|
1494
937
|
}
|
|
1495
|
-
return result;
|
|
1496
|
-
}
|
|
1497
|
-
async runSecondaryDetection(type, results, fn) {
|
|
1498
|
-
let detections = undefined;
|
|
1499
|
-
try {
|
|
1500
|
-
detections = await fn();
|
|
1501
|
-
}
|
|
1502
938
|
catch (error) {
|
|
1503
|
-
|
|
1504
|
-
return;
|
|
1505
|
-
const logType = type === SensorType.Face ? 'Face' : type === SensorType.LicensePlate ? 'License plate' : type === SensorType.Clip ? 'CLIP' : 'Classifier';
|
|
1506
|
-
this.logger.error(`${logType} detection error:`, error);
|
|
1507
|
-
}
|
|
1508
|
-
if (!detections)
|
|
1509
|
-
return;
|
|
1510
|
-
if (type === SensorType.Face) {
|
|
1511
|
-
results.face = detections;
|
|
1512
|
-
await this.resolveFaceIdentities(results.face.detections);
|
|
1513
|
-
}
|
|
1514
|
-
else if (type === SensorType.LicensePlate) {
|
|
1515
|
-
results.licensePlate = detections;
|
|
1516
|
-
}
|
|
1517
|
-
else if (type === SensorType.Clip) {
|
|
1518
|
-
results.clip = detections;
|
|
1519
|
-
const clipPlugin = this.singlePlugins.get(SensorType.Clip);
|
|
1520
|
-
results.clipEmbeddingModel = clipPlugin?.modelSpec?.embeddingModel;
|
|
1521
|
-
}
|
|
1522
|
-
else if (type === SensorType.Classifier) {
|
|
1523
|
-
results.classifiers ??= {};
|
|
1524
|
-
for (const { pluginId, result } of detections) {
|
|
1525
|
-
results.classifiers[pluginId] = result;
|
|
1526
|
-
}
|
|
1527
|
-
if (Object.keys(results.classifiers).length === 0)
|
|
1528
|
-
delete results.classifiers;
|
|
939
|
+
this.logger.error('Thumbnail generation error:', error);
|
|
1529
940
|
}
|
|
1530
941
|
}
|
|
1531
942
|
ingestResultsForAllSecondaries(results) {
|
|
1532
943
|
if (results.face) {
|
|
1533
|
-
|
|
944
|
+
if (results.faceEmbeddingModel) {
|
|
945
|
+
this.currentDetectionState.faceEmbeddingModel = results.faceEmbeddingModel;
|
|
946
|
+
}
|
|
947
|
+
const facePlugin = this.plugins.get(SensorType.Face);
|
|
1534
948
|
if (facePlugin) {
|
|
1535
949
|
this.ingestDetectionResult(SensorType.Face, facePlugin.sensorId, {
|
|
1536
950
|
detected: results.face.detected,
|
|
@@ -1539,7 +953,7 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1539
953
|
}
|
|
1540
954
|
}
|
|
1541
955
|
if (results.licensePlate) {
|
|
1542
|
-
const lpdPlugin = this.
|
|
956
|
+
const lpdPlugin = this.plugins.get(SensorType.LicensePlate);
|
|
1543
957
|
if (lpdPlugin) {
|
|
1544
958
|
this.ingestDetectionResult(SensorType.LicensePlate, lpdPlugin.sensorId, {
|
|
1545
959
|
detected: results.licensePlate.detected,
|
|
@@ -1548,345 +962,54 @@ let DetectionCoordinator = class DetectionCoordinator {
|
|
|
1548
962
|
}
|
|
1549
963
|
}
|
|
1550
964
|
if (results.classifiers) {
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
detections: classifierResult.detections,
|
|
1559
|
-
}, pluginId);
|
|
1560
|
-
}
|
|
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);
|
|
1561
972
|
}
|
|
1562
973
|
}
|
|
1563
974
|
}
|
|
1564
975
|
if (results.clip) {
|
|
1565
|
-
//
|
|
976
|
+
// clip has no sensor, embeddings flow through the buffer only
|
|
1566
977
|
this.currentDetectionState.clip = results.clip;
|
|
1567
978
|
this.currentDetectionState.clipEmbeddingModel = results.clipEmbeddingModel;
|
|
1568
979
|
}
|
|
1569
980
|
}
|
|
1570
|
-
|
|
1571
|
-
if (
|
|
1572
|
-
return;
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
}
|
|
1585
|
-
try {
|
|
1586
|
-
// Detectors consumed the substream frame above; images destined for the
|
|
1587
|
-
// event (chips, scene) prefer the snapshot frame when available.
|
|
1588
|
-
const hq = await this.acquireHqFrame({ width: rawFrame.width, height: rawFrame.height });
|
|
1589
|
-
try {
|
|
1590
|
-
results.thumbnails = await this.generateThumbnails(hq?.frame ?? rawFrame, hq?.scaler ?? this.frameScaler, hq !== null, objectDetections, results);
|
|
1591
|
-
}
|
|
1592
|
-
finally {
|
|
1593
|
-
hq?.frame[Symbol.dispose]?.();
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
catch (error) {
|
|
1597
|
-
this.logger.error('Thumbnail generation error:', error);
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
async runSecondariesFullFrame(rawFrame, results) {
|
|
1601
|
-
const regionMap = await this.prepareFullFrameRegions(rawFrame);
|
|
1602
|
-
if (regionMap.size > 0) {
|
|
1603
|
-
await Promise.allSettled([
|
|
1604
|
-
this.runSecondaryDetection(SensorType.Face, results, () => this.runFaceDetection(regionMap.get('face') ?? [])),
|
|
1605
|
-
this.runSecondaryDetection(SensorType.LicensePlate, results, () => this.runLicensePlateDetection(regionMap.get('lpd') ?? [])),
|
|
1606
|
-
this.runSecondaryDetection(SensorType.Classifier, results, () => this.runClassifierDetections(regionMap)),
|
|
1607
|
-
this.runSecondaryDetection(SensorType.Clip, results, () => this.runClipDetection(regionMap.get('clip') ?? [])),
|
|
1608
|
-
]);
|
|
1609
|
-
if (results.face?.detections.length) {
|
|
1610
|
-
const facePlugin = this.singlePlugins.get(SensorType.Face);
|
|
1611
|
-
results.faceEmbeddingModel = facePlugin?.modelSpec?.embeddingModel;
|
|
1612
|
-
this.currentDetectionState.faceEmbeddingModel = results.faceEmbeddingModel;
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
try {
|
|
1616
|
-
return await this.frameScaler.frameToJPEG(rawFrame, EVENT_THUMB_MAX_WIDTH);
|
|
1617
|
-
}
|
|
1618
|
-
catch (error) {
|
|
1619
|
-
this.logger.debug('Full-frame scene thumbnail failed:', error);
|
|
1620
|
-
return null;
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
async prepareFullFrameRegions(rawFrame) {
|
|
1624
|
-
const result = new Map();
|
|
1625
|
-
const consumers = [];
|
|
1626
|
-
const facePlugin = this.singlePlugins.get(SensorType.Face);
|
|
1627
|
-
if (facePlugin?.requiresFrames && this.hasSecondaryModelSpec(facePlugin.modelSpec) && this.isVideoInputSpec(facePlugin.modelSpec.input)) {
|
|
1628
|
-
consumers.push({ key: 'face', triggerLabels: facePlugin.modelSpec.triggerLabels, input: facePlugin.modelSpec.input });
|
|
1629
|
-
}
|
|
1630
|
-
const lpdPlugin = this.singlePlugins.get(SensorType.LicensePlate);
|
|
1631
|
-
if (lpdPlugin?.requiresFrames && this.hasSecondaryModelSpec(lpdPlugin.modelSpec) && this.isVideoInputSpec(lpdPlugin.modelSpec.input)) {
|
|
1632
|
-
consumers.push({ key: 'lpd', triggerLabels: lpdPlugin.modelSpec.triggerLabels, input: lpdPlugin.modelSpec.input });
|
|
1633
|
-
}
|
|
1634
|
-
const clipPlugin = this.singlePlugins.get(SensorType.Clip);
|
|
1635
|
-
if (clipPlugin?.requiresFrames && this.hasSecondaryModelSpec(clipPlugin.modelSpec) && this.isVideoInputSpec(clipPlugin.modelSpec.input)) {
|
|
1636
|
-
consumers.push({ key: 'clip', triggerLabels: clipPlugin.modelSpec.triggerLabels, input: clipPlugin.modelSpec.input });
|
|
1637
|
-
}
|
|
1638
|
-
const classifierPlugins = this.multiPlugins.get(SensorType.Classifier);
|
|
1639
|
-
if (classifierPlugins) {
|
|
1640
|
-
for (const [pluginId, plugin] of classifierPlugins) {
|
|
1641
|
-
if (plugin.requiresFrames && this.hasSecondaryModelSpec(plugin.modelSpec) && this.isVideoInputSpec(plugin.modelSpec.input)) {
|
|
1642
|
-
consumers.push({ key: `classifier:${pluginId}`, triggerLabels: plugin.modelSpec.triggerLabels, input: plugin.modelSpec.input });
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
if (consumers.length === 0)
|
|
1647
|
-
return result;
|
|
1648
|
-
const frameWidth = rawFrame.width;
|
|
1649
|
-
const frameHeight = rawFrame.height;
|
|
1650
|
-
const fullFrameDetection = {
|
|
1651
|
-
label: 'person',
|
|
1652
|
-
confidence: 1.0,
|
|
1653
|
-
box: { x: 0, y: 0, width: 1, height: 1 },
|
|
1654
|
-
};
|
|
1655
|
-
for (const consumer of consumers) {
|
|
1656
|
-
const scaled = await this.frameScaler.scaleToSpec(rawFrame, consumer.input);
|
|
1657
|
-
if (!scaled)
|
|
1658
|
-
continue;
|
|
1659
|
-
const region = {
|
|
1660
|
-
frame: this.frameScaler.toVideoFrameData(scaled, `fullframe:${consumer.key}`),
|
|
1661
|
-
detection: fullFrameDetection,
|
|
1662
|
-
offset: { x: 0, y: 0 },
|
|
1663
|
-
cropSize: { width: frameWidth, height: frameHeight },
|
|
1664
|
-
originalSize: { width: frameWidth, height: frameHeight },
|
|
1665
|
-
};
|
|
1666
|
-
result.set(consumer.key, [region]);
|
|
1667
|
-
}
|
|
1668
|
-
return result;
|
|
1669
|
-
}
|
|
1670
|
-
prepareSecondaryFrames(croppedRegions) {
|
|
1671
|
-
return croppedRegions.map((r, i) => ({ ...r.frame, id: String(i), label: r.detection.label }));
|
|
1672
|
-
}
|
|
1673
|
-
async runFaceDetection(croppedRegions) {
|
|
1674
|
-
const facePlugin = this.singlePlugins.get(SensorType.Face);
|
|
1675
|
-
if (!facePlugin || croppedRegions.length === 0)
|
|
1676
|
-
return undefined;
|
|
1677
|
-
const batchResults = await withTimeout(facePlugin.proxy.detectFaces(this.prepareSecondaryFrames(croppedRegions)), DETECT_TIMEOUT_MS, 'Face');
|
|
1678
|
-
const allFaces = [];
|
|
1679
|
-
for (let i = 0; i < batchResults.length; i++) {
|
|
1680
|
-
const parentTrackId = 'trackId' in croppedRegions[i].detection ? croppedRegions[i].detection.trackId : undefined;
|
|
1681
|
-
for (const face of batchResults[i].detections) {
|
|
1682
|
-
const transformed = this.transformBoxToOriginal(face.box, croppedRegions[i]);
|
|
1683
|
-
allFaces.push({ ...face, box: transformed, parentTrackId });
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
if (allFaces.length === 0)
|
|
1687
|
-
return { detected: false, detections: [] };
|
|
1688
|
-
// NMS only — zone filter is skipped because face crops come from
|
|
1689
|
-
// object detections that already passed zone filtering in process().
|
|
1690
|
-
const deduped = this.pipeline.runNms(allFaces);
|
|
1691
|
-
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;
|
|
1692
995
|
}
|
|
1693
|
-
async
|
|
1694
|
-
const
|
|
1695
|
-
if (!
|
|
996
|
+
async scaleForMotion(rawFrame) {
|
|
997
|
+
const motionPlugin = this.plugins.get(SensorType.Motion);
|
|
998
|
+
if (!motionPlugin?.requiresFrames)
|
|
1696
999
|
return undefined;
|
|
1697
|
-
const
|
|
1698
|
-
const
|
|
1699
|
-
|
|
1700
|
-
const parentTrackId = 'trackId' in croppedRegions[i].detection ? croppedRegions[i].detection.trackId : undefined;
|
|
1701
|
-
for (const plate of batchResults[i].detections) {
|
|
1702
|
-
allPlates.push({ ...plate, box: this.transformBoxToOriginal(plate.box, croppedRegions[i]), parentTrackId });
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
if (allPlates.length === 0)
|
|
1706
|
-
return { detected: false, detections: [] };
|
|
1707
|
-
// NMS only — zone filter skipped (crops from already-filtered object boxes).
|
|
1708
|
-
const deduped = this.pipeline.runNms(allPlates);
|
|
1709
|
-
return { detected: deduped.length > 0, detections: deduped };
|
|
1710
|
-
}
|
|
1711
|
-
runClassifierDetections(regionMap) {
|
|
1712
|
-
const classifierPlugins = this.multiPlugins.get(SensorType.Classifier);
|
|
1713
|
-
if (!classifierPlugins || classifierPlugins.size === 0)
|
|
1714
|
-
return [];
|
|
1715
|
-
const promises = [];
|
|
1716
|
-
for (const [pluginId, classifierPlugin] of classifierPlugins) {
|
|
1717
|
-
const croppedRegions = regionMap.get(`classifier:${pluginId}`);
|
|
1718
|
-
if (!croppedRegions || croppedRegions.length === 0)
|
|
1719
|
-
continue;
|
|
1720
|
-
const runClassifier = async () => {
|
|
1721
|
-
const batchResults = await withTimeout(classifierPlugin.proxy.detectClassifications(this.prepareSecondaryFrames(croppedRegions)), DETECT_TIMEOUT_MS, 'Classifier');
|
|
1722
|
-
const allDetections = [];
|
|
1723
|
-
for (let i = 0; i < batchResults.length; i++) {
|
|
1724
|
-
const parentTrackId = 'trackId' in croppedRegions[i].detection ? croppedRegions[i].detection.trackId : undefined;
|
|
1725
|
-
for (const detection of batchResults[i].detections) {
|
|
1726
|
-
allDetections.push({ ...detection, box: this.transformBoxToOriginal(detection.box, croppedRegions[i]), parentTrackId });
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
if (allDetections.length === 0)
|
|
1730
|
-
return undefined;
|
|
1731
|
-
const deduped = this.pipeline.runNms(allDetections);
|
|
1732
|
-
if (deduped.length === 0)
|
|
1733
|
-
return undefined;
|
|
1734
|
-
return { pluginId, result: { detected: true, detections: deduped } };
|
|
1735
|
-
};
|
|
1736
|
-
promises.push(runClassifier());
|
|
1737
|
-
}
|
|
1738
|
-
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;
|
|
1739
1003
|
}
|
|
1740
|
-
async
|
|
1741
|
-
const
|
|
1742
|
-
if (!
|
|
1743
|
-
return undefined;
|
|
1744
|
-
const batchResults = await withTimeout(clipPlugin.proxy.detectEmbeddings(this.prepareSecondaryFrames(croppedRegions)), DETECT_TIMEOUT_MS, 'CLIP');
|
|
1745
|
-
const allEmbeddings = [];
|
|
1746
|
-
for (let i = 0; i < batchResults.length; i++) {
|
|
1747
|
-
if (batchResults[i].embeddings && batchResults[i].embeddings.length > 0) {
|
|
1748
|
-
const parentTrackId = 'trackId' in croppedRegions[i].detection ? croppedRegions[i].detection.trackId : undefined;
|
|
1749
|
-
for (const emb of batchResults[i].embeddings) {
|
|
1750
|
-
const transformed = this.transformBoxToOriginal(emb.box, croppedRegions[i]);
|
|
1751
|
-
allEmbeddings.push({ ...emb, box: transformed, parentTrackId });
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
if (allEmbeddings.length === 0)
|
|
1004
|
+
async scaleForObject(rawFrame) {
|
|
1005
|
+
const objectPlugin = this.plugins.get(SensorType.Object);
|
|
1006
|
+
if (!objectPlugin?.requiresFrames)
|
|
1756
1007
|
return undefined;
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
}
|
|
1760
|
-
async generateThumbnails(sourceFrame, scaler, usingHq, objectDetections, results) {
|
|
1761
|
-
const thumbnails = [];
|
|
1762
|
-
const attributePadding = usingHq ? 0.4 : 0.75;
|
|
1763
|
-
const detectionMaxWidth = usingHq ? DETECTION_THUMB_HQ_MAX_WIDTH : DETECTION_THUMB_MAX_WIDTH;
|
|
1764
|
-
const detectionMinCrop = usingHq ? DETECTION_THUMB_HQ_MIN_CROP : DETECTION_THUMB_MIN_CROP;
|
|
1765
|
-
const detectionQuality = usingHq ? DETECTION_THUMB_HQ_QUALITY : undefined;
|
|
1766
|
-
// Object thumbnails — objectDetections are TrackedDetection[] when tracker ran
|
|
1767
|
-
const objectCrops = await scaler.cropToJPEG(sourceFrame, objectDetections, {
|
|
1768
|
-
maxWidth: detectionMaxWidth,
|
|
1769
|
-
padding: 0.3,
|
|
1770
|
-
minCrop: detectionMinCrop,
|
|
1771
|
-
quality: detectionQuality,
|
|
1772
|
-
});
|
|
1773
|
-
for (const crop of objectCrops) {
|
|
1774
|
-
const detection = objectDetections[crop.index];
|
|
1775
|
-
if (detection) {
|
|
1776
|
-
thumbnails.push({
|
|
1777
|
-
label: detection.label,
|
|
1778
|
-
score: detection.confidence,
|
|
1779
|
-
jpeg: crop.jpeg,
|
|
1780
|
-
area: detection.box.width * detection.box.height,
|
|
1781
|
-
onEdge: this.isOnEdge(detection.box),
|
|
1782
|
-
trackId: 'trackId' in detection ? detection.trackId : undefined,
|
|
1783
|
-
speed: 'trackSpeed' in detection ? detection.trackSpeed : undefined,
|
|
1784
|
-
});
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
if (results.face?.detections) {
|
|
1788
|
-
const faceCrops = await scaler.cropToJPEG(sourceFrame, results.face.detections, {
|
|
1789
|
-
maxWidth: ATTRIBUTE_THUMB_MAX_WIDTH,
|
|
1790
|
-
padding: attributePadding,
|
|
1791
|
-
minCrop: ATTRIBUTE_THUMB_MIN_CROP,
|
|
1792
|
-
});
|
|
1793
|
-
for (const crop of faceCrops) {
|
|
1794
|
-
const detection = results.face.detections[crop.index];
|
|
1795
|
-
if (detection) {
|
|
1796
|
-
// Inline on the detection so enrichSegment can persist unknown
|
|
1797
|
-
// faces to the face store.
|
|
1798
|
-
detection.thumbnail ??= crop.jpeg;
|
|
1799
|
-
thumbnails.push({
|
|
1800
|
-
label: `face:${detection.identity ?? 'unknown'}`,
|
|
1801
|
-
score: detection.confidence,
|
|
1802
|
-
jpeg: crop.jpeg,
|
|
1803
|
-
area: detection.box.width * detection.box.height,
|
|
1804
|
-
onEdge: this.isOnEdge(detection.box),
|
|
1805
|
-
});
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
if (results.licensePlate?.detections) {
|
|
1810
|
-
const plateCrops = await scaler.cropToJPEG(sourceFrame, results.licensePlate.detections, {
|
|
1811
|
-
maxWidth: ATTRIBUTE_THUMB_MAX_WIDTH,
|
|
1812
|
-
padding: attributePadding,
|
|
1813
|
-
minCrop: ATTRIBUTE_THUMB_MIN_CROP,
|
|
1814
|
-
});
|
|
1815
|
-
for (const crop of plateCrops) {
|
|
1816
|
-
const detection = results.licensePlate.detections[crop.index];
|
|
1817
|
-
if (detection) {
|
|
1818
|
-
thumbnails.push({
|
|
1819
|
-
label: `plate:${detection.plateText ?? 'unknown'}`,
|
|
1820
|
-
score: detection.confidence,
|
|
1821
|
-
jpeg: crop.jpeg,
|
|
1822
|
-
area: detection.box.width * detection.box.height,
|
|
1823
|
-
onEdge: this.isOnEdge(detection.box),
|
|
1824
|
-
});
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
if (results.classifiers) {
|
|
1829
|
-
for (const classifierResult of Object.values(results.classifiers)) {
|
|
1830
|
-
const clsCrops = await scaler.cropToJPEG(sourceFrame, classifierResult.detections, {
|
|
1831
|
-
maxWidth: detectionMaxWidth,
|
|
1832
|
-
padding: 0.3,
|
|
1833
|
-
minCrop: detectionMinCrop,
|
|
1834
|
-
quality: detectionQuality,
|
|
1835
|
-
});
|
|
1836
|
-
for (const crop of clsCrops) {
|
|
1837
|
-
const detection = classifierResult.detections[crop.index];
|
|
1838
|
-
if (detection) {
|
|
1839
|
-
thumbnails.push({
|
|
1840
|
-
label: `class:${detection.label}`,
|
|
1841
|
-
score: detection.confidence,
|
|
1842
|
-
jpeg: crop.jpeg,
|
|
1843
|
-
area: detection.box.width * detection.box.height,
|
|
1844
|
-
onEdge: this.isOnEdge(detection.box),
|
|
1845
|
-
});
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
return thumbnails;
|
|
1851
|
-
}
|
|
1852
|
-
transformBoxToOriginal(box, region) {
|
|
1853
|
-
// box coordinates are normalized (0-1) relative to the scaled frame (e.g. 320x320),
|
|
1854
|
-
// but must be mapped back through the original crop dimensions (before scaling)
|
|
1855
|
-
const pixelX = box.x * region.cropSize.width + region.offset.x;
|
|
1856
|
-
const pixelY = box.y * region.cropSize.height + region.offset.y;
|
|
1857
|
-
const pixelW = box.width * region.cropSize.width;
|
|
1858
|
-
const pixelH = box.height * region.cropSize.height;
|
|
1859
|
-
return {
|
|
1860
|
-
x: pixelX / region.originalSize.width,
|
|
1861
|
-
y: pixelY / region.originalSize.height,
|
|
1862
|
-
width: pixelW / region.originalSize.width,
|
|
1863
|
-
height: pixelH / region.originalSize.height,
|
|
1864
|
-
};
|
|
1865
|
-
}
|
|
1866
|
-
getNvrProxy() {
|
|
1867
|
-
if (this.nvrProxy)
|
|
1868
|
-
return Promise.resolve(this.nvrProxy);
|
|
1869
|
-
this.nvrProxyPromise ??= this.resolveNvrProxy().finally(() => {
|
|
1870
|
-
this.nvrProxyPromise = undefined;
|
|
1871
|
-
});
|
|
1872
|
-
return this.nvrProxyPromise;
|
|
1873
|
-
}
|
|
1874
|
-
async resolveNvrProxy() {
|
|
1875
|
-
try {
|
|
1876
|
-
const coreManagerProxy = this.proxy.createProxy(NamespaceManager.coreManagerNamespaces().coreManagerRpc);
|
|
1877
|
-
const plugin = await coreManagerProxy.getPlugin('@camera.ui/camera-ui-nvr');
|
|
1878
|
-
if (!plugin) {
|
|
1879
|
-
return undefined;
|
|
1880
|
-
}
|
|
1881
|
-
const ns = NamespaceManager.pluginNamespaces(plugin.id);
|
|
1882
|
-
const connection = this.proxy.createProxy(ns.pluginChildRpc, { isolatedConnection: true });
|
|
1883
|
-
this.nvrProxy = connection.proxy;
|
|
1884
|
-
return this.nvrProxy;
|
|
1885
|
-
}
|
|
1886
|
-
catch (error) {
|
|
1887
|
-
this.logger.error('NVR proxy resolution failed:', error);
|
|
1008
|
+
const inputSpec = objectPlugin.modelSpec?.input;
|
|
1009
|
+
if (!isVideoInputSpec(inputSpec))
|
|
1888
1010
|
return undefined;
|
|
1889
|
-
|
|
1011
|
+
const scaled = await this.frameScaler.scaleToSpec(rawFrame, inputSpec);
|
|
1012
|
+
return scaled ? this.frameScaler.toVideoFrameData(scaled) : undefined;
|
|
1890
1013
|
}
|
|
1891
1014
|
};
|
|
1892
1015
|
__decorate([
|