diff --git a/WebApp/src/lib/app/controller.js b/WebApp/src/lib/app/controller.js index f7e5f81..5772f36 100644 --- a/WebApp/src/lib/app/controller.js +++ b/WebApp/src/lib/app/controller.js @@ -766,6 +766,23 @@ const clearClientStream = () => { 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) => Boolean(streamSessionId && (remoteStreams.has(streamSessionId) || streamTimers.has(streamSessionId))); @@ -1700,13 +1717,6 @@ const pollClientData = async () => { recordings: recs.recordings || [], linkedCameras }); - - for (const link of linkedCameras) { - if (!requestedStreams.has(link.cameraDeviceId)) { - requestedStreams.add(link.cameraDeviceId); - void actions.requestStream(link.cameraDeviceId); - } - } }; const startPolling = () => { @@ -2109,17 +2119,22 @@ const actions = { async requestStream(cameraDeviceId) { try { + requestedStreams.add(cameraDeviceId); await api.streams.request(cameraDeviceId); } catch (error) { + requestedStreams.delete(cameraDeviceId); pushToast(error.message || 'Failed to request stream', 'error'); } }, - selectCamera(cameraDeviceId) { + async selectCamera(cameraDeviceId) { const currentState = getAppState(); const sessions = currentState.cameraSessions || {}; const existingSessionId = sessions[cameraDeviceId] || null; const reusableSessionId = hasReusableClientStreamSession(existingSessionId) ? existingSessionId : null; + const previousActiveStreamSessionId = currentState.activeStreamSessionId; + const isSwitchingStreams = + Boolean(previousActiveStreamSessionId) && previousActiveStreamSessionId !== reusableSessionId; if (currentState.activeCameraDeviceId !== cameraDeviceId || currentState.activeStreamSessionId !== reusableSessionId) { clearClientStream(); @@ -2131,6 +2146,10 @@ const actions = { openLinkedCameraMenuId: null }); + if (isSwitchingStreams) { + void endClientStreamSession(previousActiveStreamSessionId, { teardown: false }); + } + if (reusableSessionId) { attachClientStreamToElement(); setClientStreamMode(remoteStreams.has(reusableSessionId) ? 'video' : 'connecting'); @@ -2138,12 +2157,14 @@ const actions = { } setClientStreamMode('connecting'); - void actions.requestStream(cameraDeviceId); + await actions.requestStream(cameraDeviceId); }, closeStreamViewer() { + const streamSessionId = getAppState().activeStreamSessionId; setAppState({ activeCameraDeviceId: null, activeStreamSessionId: null }); clearClientStream(); + void endClientStreamSession(streamSessionId, { teardown: false }); }, async openRecording(recordingId) {