Wire automatic motion event lifecycle
This commit is contained in:
@@ -81,6 +81,8 @@ let activeRecordingStartedAt = null;
|
||||
let activeRecordingStreamSessionId = null;
|
||||
let lastMotionEventId = null;
|
||||
let motionDetector = null;
|
||||
let detectorMotionActive = false;
|
||||
let autoMotionTransitionInFlight = false;
|
||||
|
||||
let cameraVideoElement = null;
|
||||
let clientVideoElement = null;
|
||||
@@ -481,6 +483,38 @@ const updateMotionDetectionRuntime = (updates) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const isAutoMotionEventActive = () => {
|
||||
const state = getAppState();
|
||||
return Boolean(state.isMotionActive && state.activeMotionSource === 'auto' && lastMotionEventId);
|
||||
};
|
||||
|
||||
const getMotionDetectionPauseReason = () => {
|
||||
if (typeof document !== 'undefined' && document.visibilityState !== 'visible') {
|
||||
return 'page hidden';
|
||||
}
|
||||
|
||||
const state = getAppState();
|
||||
if (state.device?.role !== 'camera') {
|
||||
return 'device is not a camera';
|
||||
}
|
||||
if (!state.motionDetection?.enabled) {
|
||||
return 'detector disarmed';
|
||||
}
|
||||
if (!state.socketConnected) {
|
||||
return 'realtime connection offline';
|
||||
}
|
||||
if (!state.cameraPreviewReady) {
|
||||
return 'camera preview unavailable';
|
||||
}
|
||||
if (!localCameraStream) {
|
||||
return 'camera stream unavailable';
|
||||
}
|
||||
if (!cameraVideoElement) {
|
||||
return 'preview element unavailable';
|
||||
}
|
||||
return 'idle';
|
||||
};
|
||||
|
||||
const ensureMotionDetector = () => {
|
||||
if (motionDetector) {
|
||||
return motionDetector;
|
||||
@@ -492,13 +526,22 @@ const ensureMotionDetector = () => {
|
||||
onUpdate: ({ state, score, activeMotion, activeSince }) => {
|
||||
const current = getAppState().motionDetection;
|
||||
const nextState = state === 'cooldown' && !activeMotion ? 'monitoring' : state;
|
||||
const motionBecameActive = activeMotion && !detectorMotionActive;
|
||||
const motionBecameInactive = !activeMotion && detectorMotionActive;
|
||||
detectorMotionActive = activeMotion;
|
||||
updateMotionDetectionRuntime({
|
||||
state: nextState,
|
||||
score,
|
||||
lastTriggeredAt: activeMotion
|
||||
? current.lastTriggeredAt ?? (activeSince ? new Date(activeSince).toISOString() : new Date().toISOString())
|
||||
lastTriggeredAt: motionBecameActive
|
||||
? activeSince
|
||||
? new Date(activeSince).toISOString()
|
||||
: new Date().toISOString()
|
||||
: current.lastTriggeredAt
|
||||
});
|
||||
|
||||
if (motionBecameActive || motionBecameInactive) {
|
||||
void syncAutoMotionLifecycle({ activeMotion });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -514,6 +557,7 @@ const shouldRunMotionDetector = () => {
|
||||
return Boolean(
|
||||
state.device?.role === 'camera' &&
|
||||
state.motionDetection?.enabled &&
|
||||
state.socketConnected &&
|
||||
state.cameraPreviewReady &&
|
||||
localCameraStream &&
|
||||
cameraVideoElement
|
||||
@@ -530,9 +574,15 @@ const applyMotionDetectionReadiness = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const pauseReason = getMotionDetectionPauseReason();
|
||||
detectorMotionActive = false;
|
||||
if (isAutoMotionEventActive()) {
|
||||
void syncAutoMotionLifecycle({ activeMotion: false });
|
||||
}
|
||||
|
||||
if (detector.isRunning()) {
|
||||
detector.stop('idle');
|
||||
addActivity('Motion Detection', 'Detector monitoring paused');
|
||||
addActivity('Motion Detection', `Detector monitoring paused (${pauseReason})`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1094,6 +1144,107 @@ const uploadStandaloneMotionRecording = async (captureResult) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getMotionStartPayload = (source = 'manual') =>
|
||||
source === 'auto'
|
||||
? { title: 'Automatic Motion', triggeredBy: 'auto_motion' }
|
||||
: { title: 'Simulated Motion', triggeredBy: 'motion' };
|
||||
|
||||
const startMotionEvent = async ({ source = 'manual' } = {}) => {
|
||||
if (getAppState().isMotionActive || lastMotionEventId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await api.events.startMotion(getMotionStartPayload(source));
|
||||
await startCameraPreview();
|
||||
await startLocalRecording();
|
||||
lastMotionEventId = response.event.id;
|
||||
|
||||
const startedAt = new Date().toISOString();
|
||||
setAppState({
|
||||
isMotionActive: true,
|
||||
activeMotionSource: source
|
||||
});
|
||||
|
||||
if (source === 'auto') {
|
||||
updateMotionDetectionRuntime({ lastTriggeredAt: startedAt });
|
||||
}
|
||||
|
||||
pushToast(source === 'auto' ? 'Automatic motion event started' : 'Motion Event Started', 'success');
|
||||
addActivity(
|
||||
'Motion',
|
||||
source === 'auto' ? `Automatic motion event started (${response.event.id})` : `Started event ${response.event.id}`
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
const endMotionEvent = async ({ source = 'manual' } = {}) => {
|
||||
if (!lastMotionEventId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const eventId = lastMotionEventId;
|
||||
const streamSessionId = activeRecordingStreamSessionId;
|
||||
|
||||
if (streamSessionId) {
|
||||
await api.streams.end(streamSessionId);
|
||||
addActivity('Stream', `Ended stream ${streamSessionId}`);
|
||||
} else if (activeMediaRecorder?.state === 'recording') {
|
||||
const captureResult = await stopLocalRecording();
|
||||
await uploadStandaloneMotionRecording(captureResult);
|
||||
}
|
||||
|
||||
await api.events.endMotion(eventId);
|
||||
lastMotionEventId = null;
|
||||
setAppState({
|
||||
isMotionActive: false,
|
||||
activeMotionSource: null
|
||||
});
|
||||
|
||||
pushToast(source === 'auto' ? 'Automatic motion event ended' : 'Motion Ended', 'success');
|
||||
addActivity(
|
||||
'Motion',
|
||||
source === 'auto' ? `Automatic motion event ended (${eventId})` : `Ended event ${eventId}`
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
const syncAutoMotionLifecycle = async ({ activeMotion }) => {
|
||||
if (autoMotionTransitionInFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeMotion) {
|
||||
if (getAppState().isMotionActive || lastMotionEventId) {
|
||||
return;
|
||||
}
|
||||
|
||||
autoMotionTransitionInFlight = true;
|
||||
try {
|
||||
await startMotionEvent({ source: 'auto' });
|
||||
} catch (error) {
|
||||
console.error('Failed to auto-start motion event', error);
|
||||
pushToast(error.message || 'Failed to start automatic motion event', 'error');
|
||||
} finally {
|
||||
autoMotionTransitionInFlight = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAutoMotionEventActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
autoMotionTransitionInFlight = true;
|
||||
try {
|
||||
await endMotionEvent({ source: 'auto' });
|
||||
} catch (error) {
|
||||
console.error('Failed to auto-end motion event', error);
|
||||
pushToast(error.message || 'Failed to end automatic motion event', 'error');
|
||||
} finally {
|
||||
autoMotionTransitionInFlight = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCameraStreamRequest = async ({ streamId, requesterDeviceId }) => {
|
||||
if (!streamId || !requesterDeviceId) {
|
||||
throw new Error('Missing stream request context');
|
||||
@@ -1606,13 +1757,7 @@ const actions = {
|
||||
|
||||
async startMotion() {
|
||||
try {
|
||||
const response = await api.events.startMotion();
|
||||
await startCameraPreview();
|
||||
await startLocalRecording();
|
||||
lastMotionEventId = response.event.id;
|
||||
setAppState({ isMotionActive: true });
|
||||
pushToast('Motion Event Started', 'success');
|
||||
addActivity('Motion', `Started event ${response.event.id}`);
|
||||
await startMotionEvent({ source: 'manual' });
|
||||
} catch (error) {
|
||||
pushToast(error.message || 'Failed to start motion', 'error');
|
||||
}
|
||||
@@ -1621,19 +1766,7 @@ const actions = {
|
||||
async endMotion() {
|
||||
if (!lastMotionEventId) return;
|
||||
try {
|
||||
const streamSessionId = activeRecordingStreamSessionId;
|
||||
if (streamSessionId) {
|
||||
await api.streams.end(streamSessionId);
|
||||
addActivity('Stream', `Ended stream ${streamSessionId}`);
|
||||
} else if (activeMediaRecorder?.state === 'recording') {
|
||||
const captureResult = await stopLocalRecording();
|
||||
await uploadStandaloneMotionRecording(captureResult);
|
||||
}
|
||||
await api.events.endMotion(lastMotionEventId);
|
||||
lastMotionEventId = null;
|
||||
setAppState({ isMotionActive: false });
|
||||
pushToast('Motion Ended', 'success');
|
||||
addActivity('Motion', 'Ended event');
|
||||
await endMotionEvent({ source: 'manual' });
|
||||
} catch (error) {
|
||||
pushToast(error.message || 'Failed to end motion', 'error');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user