mirror of https://github.com/jetkvm/kvm.git
				
				
				
			Improve connection error handling (#284)
* feat(WebRTC): enhance connection management with connection failures after X attempts or a certain time * refactor(WebRTC): simplify WebRTCVideo component and enhance connection error handling * fix(WebRTC): extend connection timeout from 1 second to 60 seconds for improved error handling
This commit is contained in:
		
							parent
							
								
									0a7847c5ab
								
							
						
					
					
						commit
						5d7d4db4aa
					
				|  | @ -47,7 +47,7 @@ export default function WebRTCVideo() { | |||
|   const hdmiState = useVideoStore(state => state.hdmiState); | ||||
|   const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState); | ||||
|   const isLoading = !hdmiError && !isPlaying; | ||||
|   const isConnectionError = ["error", "failed", "disconnected"].includes( | ||||
|   const isConnectionError = ["error", "failed", "disconnected", "closed"].includes( | ||||
|     peerConnectionState || "", | ||||
|   ); | ||||
| 
 | ||||
|  |  | |||
|  | @ -130,10 +130,68 @@ export default function KvmIdRoute() { | |||
|   const setDiskChannel = useRTCStore(state => state.setDiskChannel); | ||||
|   const setRpcDataChannel = useRTCStore(state => state.setRpcDataChannel); | ||||
|   const setTransceiver = useRTCStore(state => state.setTransceiver); | ||||
|   const location = useLocation(); | ||||
| 
 | ||||
|   const [connectionAttempts, setConnectionAttempts] = useState(0); | ||||
| 
 | ||||
|   const [startedConnectingAt, setStartedConnectingAt] = useState<Date | null>(null); | ||||
|   const [connectedAt, setConnectedAt] = useState<Date | null>(null); | ||||
| 
 | ||||
|   const [connectionFailed, setConnectionFailed] = useState(false); | ||||
| 
 | ||||
|   const navigate = useNavigate(); | ||||
|   const { otaState, setOtaState, setModalView } = useUpdateStore(); | ||||
| 
 | ||||
|   const closePeerConnection = useCallback( | ||||
|     function closePeerConnection() { | ||||
|       peerConnection?.close(); | ||||
|       // "closed" is a valid RTCPeerConnection state according to the WebRTC spec
 | ||||
|       // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionState#closed
 | ||||
|       // However, the onconnectionstatechange event doesn't fire when close() is called manually
 | ||||
|       // So we need to explicitly update our state to maintain consistency
 | ||||
|       // I don't know why this is happening, but this is the best way I can think of to handle it
 | ||||
|       setPeerConnectionState("closed"); | ||||
|     }, | ||||
|     [peerConnection, setPeerConnectionState], | ||||
|   ); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const connectionAttemptsThreshold = 30; | ||||
|     if (connectionAttempts > connectionAttemptsThreshold) { | ||||
|       console.log(`Connection failed after ${connectionAttempts} attempts.`); | ||||
|       setConnectionFailed(true); | ||||
|       closePeerConnection(); | ||||
|     } | ||||
|   }, [connectionAttempts, closePeerConnection]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     // Skip if already connected
 | ||||
|     if (connectedAt) return; | ||||
| 
 | ||||
|     // Skip if connection is declared as failed
 | ||||
|     if (connectionFailed) return; | ||||
| 
 | ||||
|     const interval = setInterval(() => { | ||||
|       console.log("Checking connection status"); | ||||
| 
 | ||||
|       // Skip if connection hasn't started
 | ||||
|       if (!startedConnectingAt) return; | ||||
| 
 | ||||
|       const elapsedTime = Math.floor( | ||||
|         new Date().getTime() - startedConnectingAt.getTime(), | ||||
|       ); | ||||
| 
 | ||||
|       // Fail connection if it's been over X seconds since we started connecting
 | ||||
|       if (elapsedTime > 60 * 1000) { | ||||
|         console.error(`Connection failed after ${elapsedTime} ms.`); | ||||
|         setConnectionFailed(true); | ||||
|         closePeerConnection(); | ||||
|       } | ||||
|     }, 1000); | ||||
| 
 | ||||
|     return () => clearInterval(interval); | ||||
|   }, [closePeerConnection, connectedAt, connectionFailed, startedConnectingAt]); | ||||
| 
 | ||||
|   const sdp = useCallback( | ||||
|     async (event: RTCPeerConnectionIceEvent, pc: RTCPeerConnection) => { | ||||
|       if (!pc) return; | ||||
|  | @ -169,7 +227,7 @@ export default function KvmIdRoute() { | |||
|           // - In device mode, the device api would timeout, the fetch would throw an error, therefore the catch block would be hit
 | ||||
|           // Regardless, we should close the peer connection and let the useInterval handle reconnecting
 | ||||
|           if (!res.ok) { | ||||
|             pc?.close(); | ||||
|             closePeerConnection(); | ||||
|             console.error(`Error setting SDP - Status: ${res.status}}`, json); | ||||
|             return; | ||||
|           } | ||||
|  | @ -180,14 +238,20 @@ export default function KvmIdRoute() { | |||
|         ).catch(e => console.log(`Error setting remote description: ${e}`)); | ||||
|       } catch (error) { | ||||
|         console.error(`Error setting SDP: ${error}`); | ||||
|         pc?.close(); | ||||
|         closePeerConnection(); | ||||
|       } | ||||
|     }, | ||||
|     [navigate, params.id], | ||||
|     [closePeerConnection, navigate, params.id], | ||||
|   ); | ||||
| 
 | ||||
|   const connectWebRTC = useCallback(async () => { | ||||
|     console.log("Attempting to connect WebRTC"); | ||||
| 
 | ||||
|     // Track connection status to detect failures and show error overlay
 | ||||
|     setConnectionAttempts(x => x + 1); | ||||
|     setStartedConnectingAt(new Date()); | ||||
|     setConnectedAt(null); | ||||
| 
 | ||||
|     const pc = new RTCPeerConnection({ | ||||
|       // We only use STUN or TURN servers if we're in the cloud
 | ||||
|       ...(isInCloud && iceConfig?.iceServers | ||||
|  | @ -197,6 +261,11 @@ export default function KvmIdRoute() { | |||
| 
 | ||||
|     // Set up event listeners and data channels
 | ||||
|     pc.onconnectionstatechange = () => { | ||||
|       // If the connection state is connected, we reset the connection attempts.
 | ||||
|       if (pc.connectionState === "connected") { | ||||
|         setConnectionAttempts(0); | ||||
|         setConnectedAt(new Date()); | ||||
|       } | ||||
|       setPeerConnectionState(pc.connectionState); | ||||
|     }; | ||||
| 
 | ||||
|  | @ -236,16 +305,35 @@ export default function KvmIdRoute() { | |||
|     setTransceiver, | ||||
|   ]); | ||||
| 
 | ||||
|   // WebRTC connection management
 | ||||
|   useInterval(() => { | ||||
|   useEffect(() => { | ||||
|     console.log("Attempting to connect WebRTC"); | ||||
| 
 | ||||
|     // If we're in an other session, we don't need to connect
 | ||||
|     if (location.pathname.includes("other-session")) return; | ||||
| 
 | ||||
|     // If we're already connected or connecting, we don't need to connect
 | ||||
|     if ( | ||||
|       ["connected", "connecting", "new"].includes(peerConnection?.connectionState ?? "") | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
|     if (location.pathname.includes("other-session")) return; | ||||
|     connectWebRTC(); | ||||
|   }, 3000); | ||||
| 
 | ||||
|     // In certain cases, we want to never connect again. This happens when we've tried for a long time and failed
 | ||||
|     if (connectionFailed) { | ||||
|       console.log("Connection failed. We won't attempt to connect again."); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const interval = setInterval(() => { | ||||
|       connectWebRTC(); | ||||
|     }, 3000); | ||||
|     return () => clearInterval(interval); | ||||
|   }, [ | ||||
|     connectWebRTC, | ||||
|     connectionFailed, | ||||
|     location.pathname, | ||||
|     peerConnection?.connectionState, | ||||
|   ]); | ||||
| 
 | ||||
|   // On boot, if the connection state is undefined, we connect to the WebRTC
 | ||||
|   useEffect(() => { | ||||
|  | @ -431,7 +519,6 @@ export default function KvmIdRoute() { | |||
|   }, [kvmTerminal, peerConnection, serialConsole]); | ||||
| 
 | ||||
|   const outlet = useOutlet(); | ||||
|   const location = useLocation(); | ||||
|   const onModalClose = useCallback(() => { | ||||
|     if (location.pathname !== "/other-session") navigateTo("/"); | ||||
|   }, [navigateTo, location.pathname]); | ||||
|  | @ -516,10 +603,6 @@ export default function KvmIdRoute() { | |||
| 
 | ||||
|       <div | ||||
|         className="isolate" | ||||
|         // onMouseMove={e => e.stopPropagation()}
 | ||||
|         // onMouseDown={e => e.stopPropagation()}
 | ||||
|         // onMouseUp={e => e.stopPropagation()}
 | ||||
|         // onPointerMove={e => e.stopPropagation()}
 | ||||
|         onKeyUp={e => e.stopPropagation()} | ||||
|         onKeyDown={e => { | ||||
|           e.stopPropagation(); | ||||
|  | @ -527,6 +610,7 @@ export default function KvmIdRoute() { | |||
|         }} | ||||
|       > | ||||
|         <Modal open={outlet !== null} onClose={onModalClose}> | ||||
|           {/* The 'used by other session' modal needs to have access to the connectWebRTC function */} | ||||
|           <Outlet context={{ connectWebRTC }} /> | ||||
|         </Modal> | ||||
|       </div> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue