fix(sim): prevent duplicate stream start loops and noisy recording fallback errors
This commit is contained in:
@@ -186,6 +186,9 @@ let sfuSendTransport = null;
|
|||||||
let sfuRecvTransport = null;
|
let sfuRecvTransport = null;
|
||||||
let sfuPublishedProducerId = null;
|
let sfuPublishedProducerId = null;
|
||||||
let sfuConsumedTrack = null;
|
let sfuConsumedTrack = null;
|
||||||
|
let streamRequestInFlight = false;
|
||||||
|
let hasAutoRequestedInitialStream = false;
|
||||||
|
const inflightCameraStreamCommands = new Set();
|
||||||
const rtcConfig = {
|
const rtcConfig = {
|
||||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
||||||
};
|
};
|
||||||
@@ -914,7 +917,14 @@ const finalizeRecordingForStream = async (streamSessionId, captureResult) => {
|
|||||||
if (recording?.id) {
|
if (recording?.id) {
|
||||||
try {
|
try {
|
||||||
if (!captureResult?.blob || captureResult.blob.size === 0) {
|
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', {
|
const uploadMeta = await API.request('/videos/upload-url', {
|
||||||
@@ -998,11 +1008,26 @@ const connectSocket = () => {
|
|||||||
try {
|
try {
|
||||||
if (payload.commandType === 'start_stream') {
|
if (payload.commandType === 'start_stream') {
|
||||||
const streamId = payload.payload.streamSessionId;
|
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();
|
const ready = await startCameraPreview();
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
throw new Error('Camera permission is required before streaming');
|
throw new Error('Camera permission is required before streaming');
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
await API.streams.accept(streamId);
|
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 API.streams.getPublishCreds(streamId);
|
||||||
await startLocalRecording();
|
await startLocalRecording();
|
||||||
if (isSfuMode()) {
|
if (isSfuMode()) {
|
||||||
@@ -1018,6 +1043,7 @@ const connectSocket = () => {
|
|||||||
addActivity('Stream', 'Accepted & Published');
|
addActivity('Stream', 'Accepted & Published');
|
||||||
// Auto-stop after 15s for simulation
|
// Auto-stop after 15s for simulation
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
const captureResult = await stopLocalRecording();
|
const captureResult = await stopLocalRecording();
|
||||||
await API.streams.end(streamId);
|
await API.streams.end(streamId);
|
||||||
await finalizeRecordingForStream(streamId, captureResult);
|
await finalizeRecordingForStream(streamId, captureResult);
|
||||||
@@ -1032,11 +1058,17 @@ const connectSocket = () => {
|
|||||||
teardownPeerConnection();
|
teardownPeerConnection();
|
||||||
store.update({ activeCameraDeviceId: null, activeStreamSessionId: null });
|
store.update({ activeCameraDeviceId: null, activeStreamSessionId: null });
|
||||||
addActivity('Stream', 'Ended auto-simulation');
|
addActivity('Stream', 'Ended auto-simulation');
|
||||||
|
} finally {
|
||||||
|
inflightCameraStreamCommands.delete(streamId);
|
||||||
|
}
|
||||||
}, 15000);
|
}, 15000);
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit('command:ack', { commandId: payload.commandId, status: 'acknowledged' });
|
socket.emit('command:ack', { commandId: payload.commandId, status: 'acknowledged' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (payload?.payload?.streamSessionId) {
|
||||||
|
inflightCameraStreamCommands.delete(payload.payload.streamSessionId);
|
||||||
|
}
|
||||||
socket.emit('command:ack', { commandId: payload.commandId, status: 'rejected', error: e.message });
|
socket.emit('command:ack', { commandId: payload.commandId, status: 'rejected', error: e.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1348,12 +1380,25 @@ const Actions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
requestStream: async (camId) => {
|
requestStream: async (camId) => {
|
||||||
|
if (streamRequestInFlight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const current = store.get();
|
||||||
|
if (current.activeStreamSessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
streamRequestInFlight = true;
|
||||||
try {
|
try {
|
||||||
store.update({ activeCameraDeviceId: camId });
|
store.update({ activeCameraDeviceId: camId });
|
||||||
Toast.show('Requesting Stream...', 'info');
|
Toast.show('Requesting Stream...', 'info');
|
||||||
await API.streams.request(camId);
|
await API.streams.request(camId);
|
||||||
// Socket will handle the rest ('stream:started')
|
// Socket will handle the rest ('stream:started')
|
||||||
} catch (e) { }
|
} catch (e) {
|
||||||
|
store.update({ activeCameraDeviceId: null });
|
||||||
|
} finally {
|
||||||
|
streamRequestInFlight = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
openRecording: async (recordingId) => {
|
openRecording: async (recordingId) => {
|
||||||
@@ -1469,7 +1514,8 @@ const render = (state) => {
|
|||||||
|
|
||||||
// 5. Client Mode Lists
|
// 5. Client Mode Lists
|
||||||
if (state.device?.role === 'client' && state.screen === 'home') {
|
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);
|
void Actions.requestStream(state.linkedCameras[0].cameraDeviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user