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 hdmiState = useVideoStore(state => state.hdmiState);
|
||||||
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
|
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
|
||||||
const isLoading = !hdmiError && !isPlaying;
|
const isLoading = !hdmiError && !isPlaying;
|
||||||
const isConnectionError = ["error", "failed", "disconnected"].includes(
|
const isConnectionError = ["error", "failed", "disconnected", "closed"].includes(
|
||||||
peerConnectionState || "",
|
peerConnectionState || "",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -130,10 +130,68 @@ export default function KvmIdRoute() {
|
||||||
const setDiskChannel = useRTCStore(state => state.setDiskChannel);
|
const setDiskChannel = useRTCStore(state => state.setDiskChannel);
|
||||||
const setRpcDataChannel = useRTCStore(state => state.setRpcDataChannel);
|
const setRpcDataChannel = useRTCStore(state => state.setRpcDataChannel);
|
||||||
const setTransceiver = useRTCStore(state => state.setTransceiver);
|
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 navigate = useNavigate();
|
||||||
const { otaState, setOtaState, setModalView } = useUpdateStore();
|
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(
|
const sdp = useCallback(
|
||||||
async (event: RTCPeerConnectionIceEvent, pc: RTCPeerConnection) => {
|
async (event: RTCPeerConnectionIceEvent, pc: RTCPeerConnection) => {
|
||||||
if (!pc) return;
|
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
|
// - 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
|
// Regardless, we should close the peer connection and let the useInterval handle reconnecting
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
pc?.close();
|
closePeerConnection();
|
||||||
console.error(`Error setting SDP - Status: ${res.status}}`, json);
|
console.error(`Error setting SDP - Status: ${res.status}}`, json);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -180,14 +238,20 @@ export default function KvmIdRoute() {
|
||||||
).catch(e => console.log(`Error setting remote description: ${e}`));
|
).catch(e => console.log(`Error setting remote description: ${e}`));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error setting SDP: ${error}`);
|
console.error(`Error setting SDP: ${error}`);
|
||||||
pc?.close();
|
closePeerConnection();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[navigate, params.id],
|
[closePeerConnection, navigate, params.id],
|
||||||
);
|
);
|
||||||
|
|
||||||
const connectWebRTC = useCallback(async () => {
|
const connectWebRTC = useCallback(async () => {
|
||||||
console.log("Attempting to connect WebRTC");
|
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({
|
const pc = new RTCPeerConnection({
|
||||||
// We only use STUN or TURN servers if we're in the cloud
|
// We only use STUN or TURN servers if we're in the cloud
|
||||||
...(isInCloud && iceConfig?.iceServers
|
...(isInCloud && iceConfig?.iceServers
|
||||||
|
@ -197,6 +261,11 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
// Set up event listeners and data channels
|
// Set up event listeners and data channels
|
||||||
pc.onconnectionstatechange = () => {
|
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);
|
setPeerConnectionState(pc.connectionState);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -236,16 +305,35 @@ export default function KvmIdRoute() {
|
||||||
setTransceiver,
|
setTransceiver,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// WebRTC connection management
|
useEffect(() => {
|
||||||
useInterval(() => {
|
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 (
|
if (
|
||||||
["connected", "connecting", "new"].includes(peerConnection?.connectionState ?? "")
|
["connected", "connecting", "new"].includes(peerConnection?.connectionState ?? "")
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (location.pathname.includes("other-session")) return;
|
|
||||||
|
// 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();
|
connectWebRTC();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [
|
||||||
|
connectWebRTC,
|
||||||
|
connectionFailed,
|
||||||
|
location.pathname,
|
||||||
|
peerConnection?.connectionState,
|
||||||
|
]);
|
||||||
|
|
||||||
// On boot, if the connection state is undefined, we connect to the WebRTC
|
// On boot, if the connection state is undefined, we connect to the WebRTC
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -431,7 +519,6 @@ export default function KvmIdRoute() {
|
||||||
}, [kvmTerminal, peerConnection, serialConsole]);
|
}, [kvmTerminal, peerConnection, serialConsole]);
|
||||||
|
|
||||||
const outlet = useOutlet();
|
const outlet = useOutlet();
|
||||||
const location = useLocation();
|
|
||||||
const onModalClose = useCallback(() => {
|
const onModalClose = useCallback(() => {
|
||||||
if (location.pathname !== "/other-session") navigateTo("/");
|
if (location.pathname !== "/other-session") navigateTo("/");
|
||||||
}, [navigateTo, location.pathname]);
|
}, [navigateTo, location.pathname]);
|
||||||
|
@ -516,10 +603,6 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="isolate"
|
className="isolate"
|
||||||
// onMouseMove={e => e.stopPropagation()}
|
|
||||||
// onMouseDown={e => e.stopPropagation()}
|
|
||||||
// onMouseUp={e => e.stopPropagation()}
|
|
||||||
// onPointerMove={e => e.stopPropagation()}
|
|
||||||
onKeyUp={e => e.stopPropagation()}
|
onKeyUp={e => e.stopPropagation()}
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -527,6 +610,7 @@ export default function KvmIdRoute() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal open={outlet !== null} onClose={onModalClose}>
|
<Modal open={outlet !== null} onClose={onModalClose}>
|
||||||
|
{/* The 'used by other session' modal needs to have access to the connectWebRTC function */}
|
||||||
<Outlet context={{ connectWebRTC }} />
|
<Outlet context={{ connectWebRTC }} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue