feat(webapp): remove frame fallback from runtime stream path
This commit is contained in:
@@ -39,12 +39,6 @@ let activeRecordingChunks = [];
|
||||
let activeRecordingStartedAt = null;
|
||||
let activeRecordingStreamSessionId = null;
|
||||
let lastMotionEventId = null;
|
||||
let frameRelayTimer = null;
|
||||
let frameRelayStartTimer = null;
|
||||
let frameCanvas = null;
|
||||
let frameContext = null;
|
||||
let hasWebrtcEverConnected = false;
|
||||
let webrtcConnected = false;
|
||||
|
||||
let cameraVideoElement = null;
|
||||
let clientVideoElement = null;
|
||||
@@ -153,10 +147,9 @@ const setClientStreamMode = (mode) => {
|
||||
if (mode === 'unavailable') clientPlaceholderText = 'Stream unavailable';
|
||||
if (mode === 'none') clientPlaceholderText = 'Select a camera to view';
|
||||
|
||||
patchAppState((state) => ({
|
||||
patchAppState(() => ({
|
||||
clientStreamMode: mode,
|
||||
clientPlaceholderText,
|
||||
clientFallbackFrame: mode === 'image' ? state.clientFallbackFrame : ''
|
||||
clientPlaceholderText
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -417,60 +410,6 @@ const closeRecordingModal = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const stopFrameRelay = () => {
|
||||
if (frameRelayStartTimer) {
|
||||
clearTimeout(frameRelayStartTimer);
|
||||
frameRelayStartTimer = null;
|
||||
}
|
||||
if (frameRelayTimer) {
|
||||
clearInterval(frameRelayTimer);
|
||||
frameRelayTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startFrameRelay = async (streamSessionId, toDeviceId) => {
|
||||
if (!socket || !streamSessionId || !toDeviceId) return;
|
||||
if (hasWebrtcEverConnected) return;
|
||||
|
||||
const ready = await startCameraPreview();
|
||||
if (!ready) {
|
||||
throw new Error('Camera permission is required before streaming');
|
||||
}
|
||||
|
||||
if (!cameraVideoElement) return;
|
||||
|
||||
stopFrameRelay();
|
||||
frameRelayTimer = setInterval(() => {
|
||||
if (webrtcConnected || hasWebrtcEverConnected) return;
|
||||
if (
|
||||
!socket ||
|
||||
cameraVideoElement.readyState < 2 ||
|
||||
!cameraVideoElement.videoWidth ||
|
||||
!cameraVideoElement.videoHeight
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frameCanvas) {
|
||||
frameCanvas = document.createElement('canvas');
|
||||
frameContext = frameCanvas.getContext('2d');
|
||||
}
|
||||
if (!frameCanvas || !frameContext) return;
|
||||
|
||||
frameCanvas.width = cameraVideoElement.videoWidth;
|
||||
frameCanvas.height = cameraVideoElement.videoHeight;
|
||||
frameContext.drawImage(cameraVideoElement, 0, 0, frameCanvas.width, frameCanvas.height);
|
||||
const frame = frameCanvas.toDataURL('image/jpeg', 0.6);
|
||||
|
||||
socket.emit('stream:frame', {
|
||||
toDeviceId,
|
||||
streamSessionId,
|
||||
frame,
|
||||
capturedAt: new Date().toISOString()
|
||||
});
|
||||
}, 600);
|
||||
};
|
||||
|
||||
const getPreferredRecordingMimeType = () => {
|
||||
if (typeof MediaRecorder === 'undefined') return '';
|
||||
const preferredTypes = ['video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm'];
|
||||
@@ -672,8 +611,6 @@ const teardownPeerConnection = (streamSessionId) => {
|
||||
pendingCandidatesMap.clear();
|
||||
connectedPeers.clear();
|
||||
setConnectedStreamSessionIds();
|
||||
webrtcConnected = false;
|
||||
hasWebrtcEverConnected = false;
|
||||
clearClientStream();
|
||||
return;
|
||||
}
|
||||
@@ -752,11 +689,6 @@ const ensurePeerConnection = async ({ streamSessionId, targetDeviceId, asCamera
|
||||
addActivity('WebRTC', `Peer connected for ${streamSessionId}`);
|
||||
connectedPeers.add(streamSessionId);
|
||||
setConnectedStreamSessionIds();
|
||||
if (asCamera) {
|
||||
webrtcConnected = true;
|
||||
hasWebrtcEverConnected = true;
|
||||
stopFrameRelay();
|
||||
}
|
||||
} else if (
|
||||
connection.connectionState === 'failed' ||
|
||||
connection.connectionState === 'disconnected' ||
|
||||
@@ -765,15 +697,9 @@ const ensurePeerConnection = async ({ streamSessionId, targetDeviceId, asCamera
|
||||
addActivity('WebRTC', `Peer ${connection.connectionState} for ${streamSessionId}`);
|
||||
connectedPeers.delete(streamSessionId);
|
||||
setConnectedStreamSessionIds();
|
||||
if (asCamera) {
|
||||
if (!hasWebrtcEverConnected) webrtcConnected = false;
|
||||
if (connection.connectionState === 'failed' || connection.connectionState === 'closed') {
|
||||
hasWebrtcEverConnected = false;
|
||||
}
|
||||
}
|
||||
if (getAppState().device?.role === 'client' && getAppState().activeStreamSessionId === streamSessionId) {
|
||||
if (connection.connectionState === 'failed' || connection.connectionState === 'closed') {
|
||||
clearClientStream();
|
||||
setClientStreamMode('unavailable');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -940,6 +866,23 @@ const uploadStandaloneMotionRecording = async (captureResult) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCameraStreamRequest = async ({ streamId, requesterDeviceId }) => {
|
||||
if (!streamId || !requesterDeviceId) {
|
||||
throw new Error('Missing stream request context');
|
||||
}
|
||||
|
||||
const ready = await startCameraPreview();
|
||||
if (!ready) {
|
||||
throw new Error('Camera permission is required before streaming');
|
||||
}
|
||||
|
||||
activeRecordingStreamSessionId = streamId;
|
||||
await api.streams.accept(streamId);
|
||||
await startLocalRecording();
|
||||
await startOfferToClient(streamId, requesterDeviceId);
|
||||
addActivity('Stream', 'Accepted stream request and started WebRTC offer');
|
||||
};
|
||||
|
||||
const connectSocket = () => {
|
||||
const { deviceToken } = getAppState();
|
||||
if (!deviceToken) return;
|
||||
@@ -957,7 +900,6 @@ const connectSocket = () => {
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
setAppState({ socketConnected: false });
|
||||
stopFrameRelay();
|
||||
void stopLocalRecording();
|
||||
teardownPeerConnection();
|
||||
setAppState({ activeCameraDeviceId: null, activeStreamSessionId: null });
|
||||
@@ -968,25 +910,10 @@ const connectSocket = () => {
|
||||
|
||||
try {
|
||||
if (payload.commandType === 'start_stream') {
|
||||
const streamId = payload.payload.streamSessionId;
|
||||
const ready = await startCameraPreview();
|
||||
if (!ready) {
|
||||
throw new Error('Camera permission is required before streaming');
|
||||
}
|
||||
activeRecordingStreamSessionId = streamId;
|
||||
await api.streams.accept(streamId);
|
||||
await api.streams.getPublishCreds(streamId);
|
||||
await startLocalRecording();
|
||||
if (payload.sourceDeviceId) {
|
||||
await startOfferToClient(streamId, payload.sourceDeviceId);
|
||||
frameRelayStartTimer = setTimeout(() => {
|
||||
if (!webrtcConnected && !hasWebrtcEverConnected) {
|
||||
void startFrameRelay(streamId, payload.sourceDeviceId);
|
||||
}
|
||||
}, 2500);
|
||||
addActivity('Stream', 'Accepted & Published');
|
||||
}
|
||||
|
||||
await handleCameraStreamRequest({
|
||||
streamId: payload.payload.streamSessionId,
|
||||
requesterDeviceId: payload.sourceDeviceId
|
||||
});
|
||||
socket.emit('command:ack', { commandId: payload.commandId, status: 'acknowledged' });
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -994,6 +921,20 @@ const connectSocket = () => {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('stream:requested', async (payload) => {
|
||||
if (getAppState().device?.role !== 'camera') return;
|
||||
|
||||
try {
|
||||
await handleCameraStreamRequest({
|
||||
streamId: payload.streamSessionId,
|
||||
requesterDeviceId: payload.requesterDeviceId
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed handling direct stream request', error);
|
||||
pushToast(error.message || 'Failed to accept stream request', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('motion:detected', (payload) => {
|
||||
const cameraDeviceId = payload.cameraDeviceId || payload.deviceId;
|
||||
addActivity('Motion', `${getCameraLabel(cameraDeviceId)} has detected movement`);
|
||||
@@ -1018,37 +959,17 @@ const connectSocket = () => {
|
||||
setClientStreamMode('connecting');
|
||||
}
|
||||
|
||||
try {
|
||||
await api.streams.getSubscribeCreds(payload.streamSessionId);
|
||||
streamTimers.set(
|
||||
payload.streamSessionId,
|
||||
setTimeout(() => {
|
||||
if (!remoteStreams.has(payload.streamSessionId)) {
|
||||
addActivity('Stream', `No remote video track received for ${payload.streamSessionId}`);
|
||||
if (getAppState().activeStreamSessionId === payload.streamSessionId) {
|
||||
setClientStreamMode('unavailable');
|
||||
}
|
||||
streamTimers.set(
|
||||
payload.streamSessionId,
|
||||
setTimeout(() => {
|
||||
if (!remoteStreams.has(payload.streamSessionId)) {
|
||||
addActivity('Stream', `No remote video track received for ${payload.streamSessionId}`);
|
||||
if (getAppState().activeStreamSessionId === payload.streamSessionId) {
|
||||
setClientStreamMode('unavailable');
|
||||
}
|
||||
}, 6000)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Stream connect failed', error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('stream:frame', (payload) => {
|
||||
if (connectedPeers.has(payload.streamSessionId)) return;
|
||||
if (!payload?.frame) return;
|
||||
|
||||
if (streamTimers.has(payload.streamSessionId)) {
|
||||
clearTimeout(streamTimers.get(payload.streamSessionId));
|
||||
streamTimers.delete(payload.streamSessionId);
|
||||
}
|
||||
|
||||
if (payload.streamSessionId === getAppState().activeStreamSessionId) {
|
||||
setAppState({ clientFallbackFrame: payload.frame });
|
||||
setClientStreamMode('image');
|
||||
}
|
||||
}
|
||||
}, 6000)
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('stream:ended', async (payload) => {
|
||||
@@ -1204,7 +1125,6 @@ const startPolling = () => {
|
||||
|
||||
const cleanupConnectionState = async () => {
|
||||
stopPolling();
|
||||
stopFrameRelay();
|
||||
await stopLocalRecording();
|
||||
teardownPeerConnection();
|
||||
stopCameraPreview();
|
||||
@@ -1603,10 +1523,7 @@ const actions = {
|
||||
openLinkedCameraMenuId: null
|
||||
});
|
||||
|
||||
if (!requestedStreams.has(cameraDeviceId)) {
|
||||
requestedStreams.add(cameraDeviceId);
|
||||
void actions.requestStream(cameraDeviceId);
|
||||
}
|
||||
void actions.requestStream(cameraDeviceId);
|
||||
|
||||
attachClientStreamToElement();
|
||||
if (!getAppState().activeStreamSessionId) {
|
||||
|
||||
Reference in New Issue
Block a user