From ef652ea7e56402d631b5a68405da916a0fe1fbbe Mon Sep 17 00:00:00 2001 From: Matiss Jurevics Date: Tue, 17 Feb 2026 16:15:00 +0000 Subject: [PATCH] fix(sim): prevent duplicate stream start loops and noisy recording fallback errors --- Backend/public/mobile-sim.js | 80 ++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/Backend/public/mobile-sim.js b/Backend/public/mobile-sim.js index 1c7d330..00da2f1 100644 --- a/Backend/public/mobile-sim.js +++ b/Backend/public/mobile-sim.js @@ -186,6 +186,9 @@ let sfuSendTransport = null; let sfuRecvTransport = null; let sfuPublishedProducerId = null; let sfuConsumedTrack = null; +let streamRequestInFlight = false; +let hasAutoRequestedInitialStream = false; +const inflightCameraStreamCommands = new Set(); const rtcConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }], }; @@ -914,7 +917,14 @@ const finalizeRecordingForStream = async (streamSessionId, captureResult) => { if (recording?.id) { try { if (!captureResult?.blob || captureResult.blob.size === 0) { - throw new Error('No captured video blob to upload'); + const fallbackObjectKey = `sim/${streamSessionId}/${Date.now()}.webm`; + await API.events.finalizeRecording(recording.id, { + objectKey: fallbackObjectKey, + durationSeconds: captureResult?.durationSeconds ?? 15, + sizeBytes: captureResult?.blob?.size ?? 0, + }); + addActivity('Recording', 'No local blob; finalized with simulator fallback'); + return true; } const uploadMeta = await API.request('/videos/upload-url', { @@ -998,11 +1008,26 @@ const connectSocket = () => { try { if (payload.commandType === 'start_stream') { const streamId = payload.payload.streamSessionId; + if (inflightCameraStreamCommands.has(streamId)) { + addActivity('Stream', `Duplicate start ignored for ${streamId.substring(0, 8)}`); + socket.emit('command:ack', { commandId: payload.commandId, status: 'acknowledged' }); + return; + } + inflightCameraStreamCommands.add(streamId); + const ready = await startCameraPreview(); if (!ready) { throw new Error('Camera permission is required before streaming'); } - await API.streams.accept(streamId); + try { + await API.streams.accept(streamId); + } catch (error) { + const message = error?.message || ''; + if (!message.includes('status 409')) { + throw error; + } + addActivity('Stream', 'Accept already handled, continuing publish setup'); + } await API.streams.getPublishCreds(streamId); await startLocalRecording(); if (isSfuMode()) { @@ -1018,25 +1043,32 @@ const connectSocket = () => { addActivity('Stream', 'Accepted & Published'); // Auto-stop after 15s for simulation setTimeout(async () => { - const captureResult = await stopLocalRecording(); - await API.streams.end(streamId); - await finalizeRecordingForStream(streamId, captureResult); - stopFrameRelay(); - if (!isSfuMode() && socket && payload.sourceDeviceId) { - socket.emit('webrtc:signal', { - toDeviceId: payload.sourceDeviceId, - streamSessionId: streamId, - signalType: 'hangup', - }); + try { + const captureResult = await stopLocalRecording(); + await API.streams.end(streamId); + await finalizeRecordingForStream(streamId, captureResult); + stopFrameRelay(); + if (!isSfuMode() && socket && payload.sourceDeviceId) { + socket.emit('webrtc:signal', { + toDeviceId: payload.sourceDeviceId, + streamSessionId: streamId, + signalType: 'hangup', + }); + } + teardownPeerConnection(); + store.update({ activeCameraDeviceId: null, activeStreamSessionId: null }); + addActivity('Stream', 'Ended auto-simulation'); + } finally { + inflightCameraStreamCommands.delete(streamId); } - teardownPeerConnection(); - store.update({ activeCameraDeviceId: null, activeStreamSessionId: null }); - addActivity('Stream', 'Ended auto-simulation'); }, 15000); } socket.emit('command:ack', { commandId: payload.commandId, status: 'acknowledged' }); } catch (e) { + if (payload?.payload?.streamSessionId) { + inflightCameraStreamCommands.delete(payload.payload.streamSessionId); + } socket.emit('command:ack', { commandId: payload.commandId, status: 'rejected', error: e.message }); } }); @@ -1348,12 +1380,25 @@ const Actions = { }, requestStream: async (camId) => { + if (streamRequestInFlight) { + return; + } + const current = store.get(); + if (current.activeStreamSessionId) { + return; + } + + streamRequestInFlight = true; try { store.update({ activeCameraDeviceId: camId }); Toast.show('Requesting Stream...', 'info'); await API.streams.request(camId); // Socket will handle the rest ('stream:started') - } catch (e) { } + } catch (e) { + store.update({ activeCameraDeviceId: null }); + } finally { + streamRequestInFlight = false; + } }, openRecording: async (recordingId) => { @@ -1469,7 +1514,8 @@ const render = (state) => { // 5. Client Mode Lists if (state.device?.role === 'client' && state.screen === 'home') { - if (!state.activeCameraDeviceId && state.linkedCameras.length > 0) { + if (!hasAutoRequestedInitialStream && !state.activeCameraDeviceId && state.linkedCameras.length > 0 && !streamRequestInFlight) { + hasAutoRequestedInitialStream = true; void Actions.requestStream(state.linkedCameras[0].cameraDeviceId); }