fix(webapp): keep device presence alive via heartbeats

This commit is contained in:
2026-03-29 14:10:00 +00:00
parent 8ad4d56c21
commit 360e923987

View File

@@ -72,6 +72,7 @@ const DEFAULT_CAMERA_CONSTRAINTS = {
height: { ideal: 360, max: 540 }, height: { ideal: 360, max: 540 },
frameRate: { ideal: 15, max: 24 } frameRate: { ideal: 15, max: 24 }
}; };
const SOCKET_HEARTBEAT_INTERVAL_MS = 10_000;
const rtcConfig = { const rtcConfig = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
@@ -81,6 +82,7 @@ let initialized = false;
let initPromise = null; let initPromise = null;
let socket = null; let socket = null;
let pollInterval = null; let pollInterval = null;
let socketHeartbeatInterval = null;
let localCameraStream = null; let localCameraStream = null;
let activeMediaRecorder = null; let activeMediaRecorder = null;
let activeRecordingChunks = []; let activeRecordingChunks = [];
@@ -1414,10 +1416,34 @@ const handleCameraStreamRequest = async ({ streamId, requesterDeviceId }) => {
addActivity('Stream', 'Accepted stream request and started WebRTC offer'); addActivity('Stream', 'Accepted stream request and started WebRTC offer');
}; };
const stopSocketHeartbeat = () => {
if (socketHeartbeatInterval) {
clearInterval(socketHeartbeatInterval);
socketHeartbeatInterval = null;
}
};
const emitSocketHeartbeat = () => {
if (!socket?.connected) {
return;
}
socket.emit('heartbeat');
};
const startSocketHeartbeat = () => {
stopSocketHeartbeat();
emitSocketHeartbeat();
socketHeartbeatInterval = setInterval(() => {
emitSocketHeartbeat();
}, SOCKET_HEARTBEAT_INTERVAL_MS);
};
const connectSocket = () => { const connectSocket = () => {
const { deviceToken } = getAppState(); const { deviceToken } = getAppState();
if (!deviceToken) return; if (!deviceToken) return;
stopSocketHeartbeat();
if (socket) socket.disconnect(); if (socket) socket.disconnect();
socket = io(getBackendUrl(), { socket = io(getBackendUrl(), {
auth: { token: deviceToken }, auth: { token: deviceToken },
@@ -1425,6 +1451,7 @@ const connectSocket = () => {
}); });
socket.on('connect', () => { socket.on('connect', () => {
startSocketHeartbeat();
setAppState({ socketConnected: true }); setAppState({ socketConnected: true });
addActivity('System', 'Connected to realtime server'); addActivity('System', 'Connected to realtime server');
if (getAppState().device?.role === 'camera') { if (getAppState().device?.role === 'camera') {
@@ -1434,6 +1461,7 @@ const connectSocket = () => {
}); });
socket.on('disconnect', () => { socket.on('disconnect', () => {
stopSocketHeartbeat();
setAppState({ socketConnected: false }); setAppState({ socketConnected: false });
void stopLocalRecording(); void stopLocalRecording();
teardownPeerConnection(); teardownPeerConnection();
@@ -1443,6 +1471,7 @@ const connectSocket = () => {
socket.on('connect_error', (error) => { socket.on('connect_error', (error) => {
const message = error?.message || 'Realtime connection failed'; const message = error?.message || 'Realtime connection failed';
stopSocketHeartbeat();
setAppState({ socketConnected: false }); setAppState({ socketConnected: false });
addActivity('System', `Realtime connection failed: ${message}`); addActivity('System', `Realtime connection failed: ${message}`);
@@ -1675,6 +1704,7 @@ const startPolling = () => {
const cleanupConnectionState = async () => { const cleanupConnectionState = async () => {
stopPolling(); stopPolling();
stopSocketHeartbeat();
await stopLocalRecording(); await stopLocalRecording();
teardownPeerConnection(); teardownPeerConnection();
stopCameraPreview(); stopCameraPreview();