fix(webapp): avoid overlapping client stream sessions

This commit is contained in:
2026-04-01 14:00:00 +01:00
parent 2044754666
commit 8c877c0e85

View File

@@ -766,6 +766,23 @@ const clearClientStream = () => {
setClientStreamMode('none'); setClientStreamMode('none');
}; };
const endClientStreamSession = async (streamSessionId, { teardown = true } = {}) => {
if (!streamSessionId) return;
try {
await api.streams.end(streamSessionId);
} catch (error) {
const message = error?.message || '';
if (!/cannot be ended|not found/i.test(message)) {
console.warn('Failed ending client stream session', error);
}
}
if (teardown) {
teardownPeerConnection(streamSessionId);
}
};
const hasReusableClientStreamSession = (streamSessionId) => const hasReusableClientStreamSession = (streamSessionId) =>
Boolean(streamSessionId && (remoteStreams.has(streamSessionId) || streamTimers.has(streamSessionId))); Boolean(streamSessionId && (remoteStreams.has(streamSessionId) || streamTimers.has(streamSessionId)));
@@ -1700,13 +1717,6 @@ const pollClientData = async () => {
recordings: recs.recordings || [], recordings: recs.recordings || [],
linkedCameras linkedCameras
}); });
for (const link of linkedCameras) {
if (!requestedStreams.has(link.cameraDeviceId)) {
requestedStreams.add(link.cameraDeviceId);
void actions.requestStream(link.cameraDeviceId);
}
}
}; };
const startPolling = () => { const startPolling = () => {
@@ -2109,17 +2119,22 @@ const actions = {
async requestStream(cameraDeviceId) { async requestStream(cameraDeviceId) {
try { try {
requestedStreams.add(cameraDeviceId);
await api.streams.request(cameraDeviceId); await api.streams.request(cameraDeviceId);
} catch (error) { } catch (error) {
requestedStreams.delete(cameraDeviceId);
pushToast(error.message || 'Failed to request stream', 'error'); pushToast(error.message || 'Failed to request stream', 'error');
} }
}, },
selectCamera(cameraDeviceId) { async selectCamera(cameraDeviceId) {
const currentState = getAppState(); const currentState = getAppState();
const sessions = currentState.cameraSessions || {}; const sessions = currentState.cameraSessions || {};
const existingSessionId = sessions[cameraDeviceId] || null; const existingSessionId = sessions[cameraDeviceId] || null;
const reusableSessionId = hasReusableClientStreamSession(existingSessionId) ? existingSessionId : null; const reusableSessionId = hasReusableClientStreamSession(existingSessionId) ? existingSessionId : null;
const previousActiveStreamSessionId = currentState.activeStreamSessionId;
const isSwitchingStreams =
Boolean(previousActiveStreamSessionId) && previousActiveStreamSessionId !== reusableSessionId;
if (currentState.activeCameraDeviceId !== cameraDeviceId || currentState.activeStreamSessionId !== reusableSessionId) { if (currentState.activeCameraDeviceId !== cameraDeviceId || currentState.activeStreamSessionId !== reusableSessionId) {
clearClientStream(); clearClientStream();
@@ -2131,6 +2146,10 @@ const actions = {
openLinkedCameraMenuId: null openLinkedCameraMenuId: null
}); });
if (isSwitchingStreams) {
void endClientStreamSession(previousActiveStreamSessionId, { teardown: false });
}
if (reusableSessionId) { if (reusableSessionId) {
attachClientStreamToElement(); attachClientStreamToElement();
setClientStreamMode(remoteStreams.has(reusableSessionId) ? 'video' : 'connecting'); setClientStreamMode(remoteStreams.has(reusableSessionId) ? 'video' : 'connecting');
@@ -2138,12 +2157,14 @@ const actions = {
} }
setClientStreamMode('connecting'); setClientStreamMode('connecting');
void actions.requestStream(cameraDeviceId); await actions.requestStream(cameraDeviceId);
}, },
closeStreamViewer() { closeStreamViewer() {
const streamSessionId = getAppState().activeStreamSessionId;
setAppState({ activeCameraDeviceId: null, activeStreamSessionId: null }); setAppState({ activeCameraDeviceId: null, activeStreamSessionId: null });
clearClientStream(); clearClientStream();
void endClientStreamSession(streamSessionId, { teardown: false });
}, },
async openRecording(recordingId) { async openRecording(recordingId) {