Add local motion detector engine

This commit is contained in:
2026-03-08 12:40:00 +00:00
parent 3c2ea7fd75
commit 72903a97eb
2 changed files with 304 additions and 0 deletions

View File

@@ -3,6 +3,7 @@
import { io } from 'socket.io-client';
import { api } from './api';
import { createMotionDetector } from './motion-detector';
import { getAppState, patchAppState, resetAppState, setAppState } from './store';
const PAGE_PATHS = {
@@ -79,6 +80,7 @@ let activeRecordingChunks = [];
let activeRecordingStartedAt = null;
let activeRecordingStreamSessionId = null;
let lastMotionEventId = null;
let motionDetector = null;
let cameraVideoElement = null;
let clientVideoElement = null;
@@ -357,6 +359,7 @@ const attachCameraStreamToElement = () => {
if (localCameraStream) {
void cameraVideoElement.play().catch(() => {});
}
applyMotionDetectionReadiness();
};
const attachClientStreamToElement = () => {
@@ -466,6 +469,78 @@ const stopCameraPreview = () => {
cameraVideoElement.srcObject = null;
}
setAppState({ cameraPreviewReady: false });
applyMotionDetectionReadiness();
};
const updateMotionDetectionRuntime = (updates) => {
patchAppState((state) => ({
motionDetection: {
...state.motionDetection,
...updates
}
}));
};
const ensureMotionDetector = () => {
if (motionDetector) {
return motionDetector;
}
motionDetector = createMotionDetector({
getSourceElement: () => cameraVideoElement,
getConfig: () => getAppState().motionDetection,
onUpdate: ({ state, score, activeMotion, activeSince }) => {
const current = getAppState().motionDetection;
const nextState = state === 'cooldown' && !activeMotion ? 'monitoring' : state;
updateMotionDetectionRuntime({
state: nextState,
score,
lastTriggeredAt: activeMotion
? current.lastTriggeredAt ?? (activeSince ? new Date(activeSince).toISOString() : new Date().toISOString())
: current.lastTriggeredAt
});
}
});
return motionDetector;
};
const shouldRunMotionDetector = () => {
if (typeof document !== 'undefined' && document.visibilityState !== 'visible') {
return false;
}
const state = getAppState();
return Boolean(
state.device?.role === 'camera' &&
state.motionDetection?.enabled &&
state.cameraPreviewReady &&
localCameraStream &&
cameraVideoElement
);
};
const applyMotionDetectionReadiness = () => {
const detector = ensureMotionDetector();
if (shouldRunMotionDetector()) {
if (!detector.isRunning()) {
detector.start();
addActivity('Motion Detection', 'Detector monitoring started');
}
return;
}
if (detector.isRunning()) {
detector.stop('idle');
addActivity('Motion Detection', 'Detector monitoring paused');
return;
}
updateMotionDetectionRuntime({ state: 'idle', score: 0 });
};
const onVisibilityChange = () => {
applyMotionDetectionReadiness();
};
const clearClientStream = () => {
@@ -1049,6 +1124,7 @@ const connectSocket = () => {
if (getAppState().device?.role === 'camera') {
void startCameraPreview();
}
applyMotionDetectionReadiness();
});
socket.on('disconnect', () => {
@@ -1056,6 +1132,7 @@ const connectSocket = () => {
void stopLocalRecording();
teardownPeerConnection();
setAppState({ activeCameraDeviceId: null, activeStreamSessionId: null });
applyMotionDetectionReadiness();
});
socket.on('command:received', async (payload) => {
@@ -1281,6 +1358,9 @@ const cleanupConnectionState = async () => {
await stopLocalRecording();
teardownPeerConnection();
stopCameraPreview();
if (motionDetector?.isRunning()) {
motionDetector.stop('idle');
}
if (socket) {
socket.disconnect();
socket = null;
@@ -1327,6 +1407,9 @@ const init = async () => {
if (navigator.mediaDevices?.addEventListener) {
navigator.mediaDevices.addEventListener('devicechange', onMediaDeviceChange);
}
if (typeof document !== 'undefined') {
document.addEventListener('visibilitychange', onVisibilityChange);
}
const saved = localStorage.getItem('mobileSimDevice');
if (saved) {
@@ -1365,6 +1448,7 @@ const init = async () => {
enforceRouteForSession();
void refreshCameraInputDevices();
applyMotionDetectionReadiness();
window.addEventListener('beforeunload', () => {
void cleanupConnectionState();
@@ -1383,6 +1467,9 @@ const destroy = async () => {
if (navigator.mediaDevices?.removeEventListener) {
navigator.mediaDevices.removeEventListener('devicechange', onMediaDeviceChange);
}
if (typeof document !== 'undefined') {
document.removeEventListener('visibilitychange', onVisibilityChange);
}
initialized = false;
await cleanupConnectionState();
};
@@ -1585,6 +1672,7 @@ const actions = {
const motionDetection = updateMotionDetectionState({ enabled: Boolean(enabled) });
pushToast(motionDetection.enabled ? 'Automatic detection armed' : 'Automatic detection paused', 'info');
addActivity('Motion Detection', motionDetection.enabled ? 'Detector armed' : 'Detector paused');
applyMotionDetectionReadiness();
},
setMotionDetectionProfile(profile) {
@@ -1592,6 +1680,7 @@ const actions = {
const motionDetection = updateMotionDetectionState({ profile: nextProfile.profile });
pushToast(`${nextProfile.label} profile selected`, 'success');
addActivity('Motion Detection', `Profile set to ${motionDetection.profile}`);
applyMotionDetectionReadiness();
},
setMotionDetectionDebug(debug) {