mirror of https://github.com/jetkvm/kvm.git
refactor: Improve WebRTC connection management and logging in KvmIdRoute
This commit is contained in:
parent
44ac37d11f
commit
eccc2c5a43
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
LoaderFunctionArgs,
|
LoaderFunctionArgs,
|
||||||
Outlet,
|
Outlet,
|
||||||
|
@ -146,17 +146,20 @@ export default function KvmIdRoute() {
|
||||||
const { otaState, setOtaState, setModalView } = useUpdateStore();
|
const { otaState, setOtaState, setModalView } = useUpdateStore();
|
||||||
|
|
||||||
const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
|
const [loadingMessage, setLoadingMessage] = useState("Connecting to device...");
|
||||||
const closePeerConnection = useCallback(
|
const cleanupAndStopReconnecting = useCallback(
|
||||||
function closePeerConnection() {
|
function cleanupAndStopReconnecting() {
|
||||||
console.log("Closing peer connection");
|
console.log("Closing peer connection");
|
||||||
|
|
||||||
setConnectionFailed(true);
|
setConnectionFailed(true);
|
||||||
|
if (peerConnection) {
|
||||||
|
setPeerConnectionState(peerConnection.connectionState);
|
||||||
|
}
|
||||||
connectionFailedRef.current = true;
|
connectionFailedRef.current = true;
|
||||||
|
|
||||||
peerConnection?.close();
|
peerConnection?.close();
|
||||||
signalingAttempts.current = 0;
|
signalingAttempts.current = 0;
|
||||||
},
|
},
|
||||||
[peerConnection],
|
[peerConnection, setPeerConnectionState],
|
||||||
);
|
);
|
||||||
|
|
||||||
// We need to track connectionFailed in a ref to avoid stale closure issues
|
// We need to track connectionFailed in a ref to avoid stale closure issues
|
||||||
|
@ -172,6 +175,7 @@ export default function KvmIdRoute() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
connectionFailedRef.current = connectionFailed;
|
connectionFailedRef.current = connectionFailed;
|
||||||
}, [connectionFailed]);
|
}, [connectionFailed]);
|
||||||
|
|
||||||
const signalingAttempts = useRef(0);
|
const signalingAttempts = useRef(0);
|
||||||
const setRemoteSessionDescription = useCallback(
|
const setRemoteSessionDescription = useCallback(
|
||||||
async function setRemoteSessionDescription(
|
async function setRemoteSessionDescription(
|
||||||
|
@ -182,11 +186,14 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription));
|
await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription));
|
||||||
console.log("Remote description set successfully");
|
console.log("[setRemoteSessionDescription] Remote description set successfully");
|
||||||
setLoadingMessage("Establishing secure connection...");
|
setLoadingMessage("Establishing secure connection...");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to set remote description:", error);
|
console.error(
|
||||||
closePeerConnection();
|
"[setRemoteSessionDescription] Failed to set remote description:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
cleanupAndStopReconnecting();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,26 +204,33 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
// When vivaldi has disabled "Broadcast IP for Best WebRTC Performance", this never connects
|
// When vivaldi has disabled "Broadcast IP for Best WebRTC Performance", this never connects
|
||||||
if (pc.sctp?.state === "connected") {
|
if (pc.sctp?.state === "connected") {
|
||||||
console.log("Remote description set");
|
console.log("[setRemoteSessionDescription] Remote description set");
|
||||||
clearInterval(checkInterval);
|
clearInterval(checkInterval);
|
||||||
|
setLoadingMessage("Connection established");
|
||||||
} else if (attempts >= 10) {
|
} else if (attempts >= 10) {
|
||||||
console.log("Failed to establish connection after 10 attempts");
|
console.log(
|
||||||
closePeerConnection();
|
"[setRemoteSessionDescription] Failed to establish connection after 10 attempts",
|
||||||
|
{
|
||||||
|
connectionState: pc.connectionState,
|
||||||
|
iceConnectionState: pc.iceConnectionState,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
cleanupAndStopReconnecting();
|
||||||
clearInterval(checkInterval);
|
clearInterval(checkInterval);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log("[setRemoteSessionDescription] Waiting for connection, state:", {
|
||||||
"Waiting for connection, state:",
|
connectionState: pc.connectionState,
|
||||||
pc.connectionState,
|
iceConnectionState: pc.iceConnectionState,
|
||||||
pc.iceConnectionState,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
[closePeerConnection],
|
[cleanupAndStopReconnecting],
|
||||||
);
|
);
|
||||||
|
|
||||||
const ignoreOffer = useRef(false);
|
const ignoreOffer = useRef(false);
|
||||||
const isSettingRemoteAnswerPending = useRef(false);
|
const isSettingRemoteAnswerPending = useRef(false);
|
||||||
|
const makingOffer = useRef(false);
|
||||||
|
|
||||||
const { sendMessage } = useWebSocket(
|
const { sendMessage } = useWebSocket(
|
||||||
isOnDevice
|
isOnDevice
|
||||||
|
@ -229,41 +243,54 @@ export default function KvmIdRoute() {
|
||||||
reconnectInterval: 1000,
|
reconnectInterval: 1000,
|
||||||
onReconnectStop: () => {
|
onReconnectStop: () => {
|
||||||
console.log("Reconnect stopped");
|
console.log("Reconnect stopped");
|
||||||
closePeerConnection();
|
cleanupAndStopReconnecting();
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldReconnect(event) {
|
shouldReconnect(event) {
|
||||||
console.log("shouldReconnect", event);
|
console.log("[Websocket] shouldReconnect", event);
|
||||||
|
// TODO: Why true?
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
onClose(event) {
|
onClose(event) {
|
||||||
console.log("onClose", event);
|
console.log("[Websocket] onClose", event);
|
||||||
|
// We don't want to close everything down, we wait for the reconnect to stop instead
|
||||||
},
|
},
|
||||||
|
|
||||||
onError(event) {
|
onError(event) {
|
||||||
console.log("onError", event);
|
console.log("[Websocket] onError", event);
|
||||||
|
// We don't want to close everything down, we wait for the reconnect to stop instead
|
||||||
},
|
},
|
||||||
onOpen(event) {
|
onOpen() {
|
||||||
console.log("onOpen", event);
|
console.log("[Websocket] onOpen");
|
||||||
console.log("signalingState", peerConnection?.signalingState);
|
|
||||||
setupPeerConnection();
|
setupPeerConnection();
|
||||||
},
|
},
|
||||||
|
|
||||||
onMessage: message => {
|
onMessage: message => {
|
||||||
if (message.data === "pong") return;
|
if (message.data === "pong") return;
|
||||||
if (!peerConnection) return;
|
if (!peerConnection) return;
|
||||||
console.log("Received WebSocket message:", message.data);
|
|
||||||
const parsedMessage = JSON.parse(message.data);
|
const parsedMessage = JSON.parse(message.data);
|
||||||
|
|
||||||
if (parsedMessage.type === "answer") {
|
if (parsedMessage.type === "answer") {
|
||||||
const polite = false;
|
console.log("[Websocket] Received answer");
|
||||||
const readyForOffer =
|
const readyForOffer =
|
||||||
|
// If we're making an offer, we don't want to accept an answer
|
||||||
!makingOffer &&
|
!makingOffer &&
|
||||||
|
// If the peer connection is stable or we're setting the remote answer pending, we're ready for an offer
|
||||||
(peerConnection?.signalingState === "stable" ||
|
(peerConnection?.signalingState === "stable" ||
|
||||||
isSettingRemoteAnswerPending.current);
|
isSettingRemoteAnswerPending.current);
|
||||||
const offerCollision = parsedMessage.type === "offer" && !readyForOffer;
|
|
||||||
|
|
||||||
ignoreOffer.current = !polite && offerCollision;
|
// If we're not ready for an offer, we don't want to accept an offer
|
||||||
|
ignoreOffer.current = parsedMessage.type === "offer" && !readyForOffer;
|
||||||
if (ignoreOffer.current) return;
|
if (ignoreOffer.current) return;
|
||||||
|
|
||||||
|
// Set so we don't accept an answer while we're setting the remote description
|
||||||
isSettingRemoteAnswerPending.current = parsedMessage.type == "answer";
|
isSettingRemoteAnswerPending.current = parsedMessage.type == "answer";
|
||||||
|
console.log(
|
||||||
|
"[Websocket] Setting remote answer pending",
|
||||||
|
isSettingRemoteAnswerPending.current,
|
||||||
|
);
|
||||||
|
|
||||||
const sd = atob(parsedMessage.data);
|
const sd = atob(parsedMessage.data);
|
||||||
const remoteSessionDescription = JSON.parse(sd);
|
const remoteSessionDescription = JSON.parse(sd);
|
||||||
|
@ -273,37 +300,42 @@ export default function KvmIdRoute() {
|
||||||
new RTCSessionDescription(remoteSessionDescription),
|
new RTCSessionDescription(remoteSessionDescription),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reset the remote answer pending flag
|
||||||
isSettingRemoteAnswerPending.current = false;
|
isSettingRemoteAnswerPending.current = false;
|
||||||
} else if (parsedMessage.type === "new-ice-candidate") {
|
} else if (parsedMessage.type === "new-ice-candidate") {
|
||||||
|
console.log("[Websocket] Received new-ice-candidate");
|
||||||
const candidate = parsedMessage.data;
|
const candidate = parsedMessage.data;
|
||||||
peerConnection.addIceCandidate(candidate);
|
peerConnection.addIceCandidate(candidate);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
connectionFailed ? false : true,
|
// Don't even retry once we declare failure
|
||||||
|
!connectionFailed,
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendWebRTCSignal = useCallback(
|
const sendWebRTCSignal = useCallback(
|
||||||
(type: string, data: any) => {
|
(type: string, data: unknown) => {
|
||||||
sendMessage(JSON.stringify({ type, data }));
|
// Second argument tells the library not to queue the message, and send it once the connection is established again.
|
||||||
|
// We have event handlers that handle the connection set up, so we don't need to queue the message.
|
||||||
|
sendMessage(JSON.stringify({ type, data }), false);
|
||||||
},
|
},
|
||||||
[sendMessage],
|
[sendMessage],
|
||||||
);
|
);
|
||||||
const makingOffer = useRef(false);
|
|
||||||
const setupPeerConnection = useCallback(async () => {
|
const setupPeerConnection = useCallback(async () => {
|
||||||
console.log("Setting up peer connection");
|
console.log("[setupPeerConnection] Setting up peer connection");
|
||||||
setConnectionFailed(false);
|
setConnectionFailed(false);
|
||||||
setLoadingMessage("Connecting to device...");
|
setLoadingMessage("Connecting to device...");
|
||||||
|
|
||||||
if (peerConnection?.signalingState === "stable") {
|
if (peerConnection?.signalingState === "stable") {
|
||||||
console.log("Peer connection already established");
|
console.log("[setupPeerConnection] Peer connection already established");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pc: RTCPeerConnection;
|
let pc: RTCPeerConnection;
|
||||||
try {
|
try {
|
||||||
console.log("Creating peer connection");
|
console.log("[setupPeerConnection] Creating peer connection");
|
||||||
setLoadingMessage("Creating peer connection...");
|
setLoadingMessage("Creating peer connection...");
|
||||||
pc = new RTCPeerConnection({
|
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
|
||||||
|
@ -311,26 +343,26 @@ export default function KvmIdRoute() {
|
||||||
? { iceServers: [iceConfig?.iceServers] }
|
? { iceServers: [iceConfig?.iceServers] }
|
||||||
: {}),
|
: {}),
|
||||||
});
|
});
|
||||||
|
setPeerConnectionState(pc.connectionState);
|
||||||
console.log("Peer connection created", pc);
|
console.log("[setupPeerConnection] Peer connection created", pc);
|
||||||
setLoadingMessage("Peer connection created");
|
setLoadingMessage("Setting up connection to device...");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error creating peer connection: ${e}`);
|
console.error(`[setupPeerConnection] Error creating peer connection: ${e}`);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
closePeerConnection();
|
cleanupAndStopReconnecting();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up event listeners and data channels
|
// Set up event listeners and data channels
|
||||||
pc.onconnectionstatechange = () => {
|
pc.onconnectionstatechange = () => {
|
||||||
console.log("Connection state changed", pc.connectionState);
|
console.log("[setupPeerConnection] Connection state changed", pc.connectionState);
|
||||||
setPeerConnectionState(pc.connectionState);
|
setPeerConnectionState(pc.connectionState);
|
||||||
};
|
};
|
||||||
|
|
||||||
pc.onnegotiationneeded = async () => {
|
pc.onnegotiationneeded = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("Creating offer");
|
console.log("[setupPeerConnection] Creating offer");
|
||||||
makingOffer.current = true;
|
makingOffer.current = true;
|
||||||
|
|
||||||
const offer = await pc.createOffer();
|
const offer = await pc.createOffer();
|
||||||
|
@ -338,8 +370,11 @@ export default function KvmIdRoute() {
|
||||||
const sd = btoa(JSON.stringify(pc.localDescription));
|
const sd = btoa(JSON.stringify(pc.localDescription));
|
||||||
sendWebRTCSignal("offer", { sd: sd });
|
sendWebRTCSignal("offer", { sd: sd });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error creating offer: ${e}`, new Date().toISOString());
|
console.error(
|
||||||
closePeerConnection();
|
`[setupPeerConnection] Error creating offer: ${e}`,
|
||||||
|
new Date().toISOString(),
|
||||||
|
);
|
||||||
|
cleanupAndStopReconnecting();
|
||||||
} finally {
|
} finally {
|
||||||
makingOffer.current = false;
|
makingOffer.current = false;
|
||||||
}
|
}
|
||||||
|
@ -369,8 +404,9 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
setPeerConnection(pc);
|
setPeerConnection(pc);
|
||||||
}, [
|
}, [
|
||||||
closePeerConnection,
|
cleanupAndStopReconnecting,
|
||||||
iceConfig?.iceServers,
|
iceConfig?.iceServers,
|
||||||
|
peerConnection?.signalingState,
|
||||||
sendWebRTCSignal,
|
sendWebRTCSignal,
|
||||||
setDiskChannel,
|
setDiskChannel,
|
||||||
setMediaMediaStream,
|
setMediaMediaStream,
|
||||||
|
@ -383,9 +419,9 @@ export default function KvmIdRoute() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (peerConnectionState === "failed") {
|
if (peerConnectionState === "failed") {
|
||||||
console.log("Connection failed, closing peer connection");
|
console.log("Connection failed, closing peer connection");
|
||||||
closePeerConnection();
|
cleanupAndStopReconnecting();
|
||||||
}
|
}
|
||||||
}, [peerConnectionState, closePeerConnection]);
|
}, [peerConnectionState, cleanupAndStopReconnecting]);
|
||||||
|
|
||||||
// Cleanup effect
|
// Cleanup effect
|
||||||
const clearInboundRtpStats = useRTCStore(state => state.clearInboundRtpStats);
|
const clearInboundRtpStats = useRTCStore(state => state.clearInboundRtpStats);
|
||||||
|
@ -601,6 +637,45 @@ export default function KvmIdRoute() {
|
||||||
[send, setScrollSensitivity],
|
[send, setScrollSensitivity],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ConnectionStatusElement = useMemo(() => {
|
||||||
|
const hasConnectionFailed =
|
||||||
|
connectionFailed || ["failed", "closed"].includes(peerConnectionState || "");
|
||||||
|
|
||||||
|
const isPeerConnectionLoading =
|
||||||
|
["connecting", "new"].includes(peerConnectionState || "") ||
|
||||||
|
peerConnection === null;
|
||||||
|
|
||||||
|
const isDisconnected = peerConnectionState === "disconnected";
|
||||||
|
|
||||||
|
const isOtherSession = location.pathname.includes("other-session");
|
||||||
|
|
||||||
|
if (isOtherSession) return null;
|
||||||
|
if (peerConnectionState === "connected") return null;
|
||||||
|
|
||||||
|
console.log("isDisconnected", isDisconnected);
|
||||||
|
if (isDisconnected) {
|
||||||
|
return <PeerConnectionDisconnectedOverlay show={true} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasConnectionFailed)
|
||||||
|
return (
|
||||||
|
<ConnectionFailedOverlay show={true} setupPeerConnection={setupPeerConnection} />
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isPeerConnectionLoading) {
|
||||||
|
return <LoadingConnectionOverlay show={true} text={loadingMessage} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [
|
||||||
|
connectionFailed,
|
||||||
|
loadingMessage,
|
||||||
|
location.pathname,
|
||||||
|
peerConnection,
|
||||||
|
peerConnectionState,
|
||||||
|
setupPeerConnection,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FeatureFlagProvider appVersion={appVersion}>
|
<FeatureFlagProvider appVersion={appVersion}>
|
||||||
{!outlet && otaState.updating && (
|
{!outlet && otaState.updating && (
|
||||||
|
@ -642,31 +717,7 @@ export default function KvmIdRoute() {
|
||||||
<div className="flex h-full w-full overflow-hidden">
|
<div className="flex h-full w-full overflow-hidden">
|
||||||
<div className="pointer-events-none fixed inset-0 isolate z-20 flex h-full w-full items-center justify-center">
|
<div className="pointer-events-none fixed inset-0 isolate z-20 flex h-full w-full items-center justify-center">
|
||||||
<div className="my-2 h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
<div className="my-2 h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
||||||
<LoadingConnectionOverlay
|
{!!ConnectionStatusElement && ConnectionStatusElement}
|
||||||
show={
|
|
||||||
!connectionFailed &&
|
|
||||||
peerConnectionState !== "disconnected" &&
|
|
||||||
(["connecting", "new"].includes(peerConnectionState || "") ||
|
|
||||||
peerConnection === null) &&
|
|
||||||
!location.pathname.includes("other-session")
|
|
||||||
}
|
|
||||||
text={loadingMessage}
|
|
||||||
/>
|
|
||||||
<ConnectionFailedOverlay
|
|
||||||
show={
|
|
||||||
(connectionFailed || peerConnectionState === "failed") &&
|
|
||||||
!location.pathname.includes("other-session")
|
|
||||||
}
|
|
||||||
setupPeerConnection={setupPeerConnection}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PeerConnectionDisconnectedOverlay
|
|
||||||
show={
|
|
||||||
peerConnectionState === "disconnected" &&
|
|
||||||
!location.pathname.includes("other-session")
|
|
||||||
}
|
|
||||||
setupPeerConnection={setupPeerConnection}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue