mirror of https://github.com/jetkvm/kvm.git
feat: Add app version header and update WebRTC signaling endpoint
This commit is contained in:
parent
b9c4f4a24b
commit
443cf5d029
1
cloud.go
1
cloud.go
|
@ -268,6 +268,7 @@ func runWebsocketClient() error {
|
||||||
|
|
||||||
header := http.Header{}
|
header := http.Header{}
|
||||||
header.Set("X-Device-ID", GetDeviceID())
|
header.Set("X-Device-ID", GetDeviceID())
|
||||||
|
header.Set("X-App-Version", builtAppVersion)
|
||||||
header.Set("Authorization", "Bearer "+config.CloudToken)
|
header.Set("Authorization", "Bearer "+config.CloudToken)
|
||||||
dialCtx, cancelDial := context.WithTimeout(context.Background(), CloudWebSocketConnectTimeout)
|
dialCtx, cancelDial := context.WithTimeout(context.Background(), CloudWebSocketConnectTimeout)
|
||||||
|
|
||||||
|
|
|
@ -44,16 +44,16 @@ import UpdateInProgressStatusCard from "../components/UpdateInProgressStatusCard
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import Modal from "../components/Modal";
|
import Modal from "../components/Modal";
|
||||||
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
||||||
import { FeatureFlagProvider } from "../providers/FeatureFlagProvider";
|
|
||||||
import notifications from "../notifications";
|
|
||||||
import {
|
import {
|
||||||
ConnectionFailedOverlay,
|
ConnectionFailedOverlay,
|
||||||
LoadingConnectionOverlay,
|
LoadingConnectionOverlay,
|
||||||
PeerConnectionDisconnectedOverlay,
|
PeerConnectionDisconnectedOverlay,
|
||||||
} from "../components/VideoOverlay";
|
} from "../components/VideoOverlay";
|
||||||
|
import { FeatureFlagProvider } from "../providers/FeatureFlagProvider";
|
||||||
|
import notifications from "../notifications";
|
||||||
|
|
||||||
import { SystemVersionInfo } from "./devices.$id.settings.general.update";
|
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
|
import { SystemVersionInfo } from "./devices.$id.settings.general.update";
|
||||||
|
|
||||||
interface LocalLoaderResp {
|
interface LocalLoaderResp {
|
||||||
authMode: "password" | "noPassword" | null;
|
authMode: "password" | "noPassword" | null;
|
||||||
|
@ -140,6 +140,8 @@ export default function KvmIdRoute() {
|
||||||
const setTransceiver = useRTCStore(state => state.setTransceiver);
|
const setTransceiver = useRTCStore(state => state.setTransceiver);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isLegacySignalingEnabled = useRef(false);
|
||||||
|
|
||||||
const [connectionFailed, setConnectionFailed] = useState(false);
|
const [connectionFailed, setConnectionFailed] = useState(false);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -234,11 +236,10 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
console.log("isondevice", isOnDevice);
|
const { sendMessage, getWebSocket } = useWebSocket(
|
||||||
const { sendMessage } = useWebSocket(
|
|
||||||
isOnDevice
|
isOnDevice
|
||||||
? `${wsProtocol}//${window.location.host}/webrtc/signaling`
|
? `${wsProtocol}//${window.location.host}/webrtc/signaling/client`
|
||||||
: `${CLOUD_API.replace("http", "ws")}/webrtc/signaling?id=${params.id}`,
|
: `${CLOUD_API.replace("http", "ws")}/webrtc/signaling/client?id=${params.id}`,
|
||||||
{
|
{
|
||||||
heartbeat: true,
|
heartbeat: true,
|
||||||
retryOnError: true,
|
retryOnError: true,
|
||||||
|
@ -266,15 +267,45 @@ export default function KvmIdRoute() {
|
||||||
},
|
},
|
||||||
onOpen() {
|
onOpen() {
|
||||||
console.log("[Websocket] onOpen");
|
console.log("[Websocket] onOpen");
|
||||||
setupPeerConnection();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onMessage: message => {
|
onMessage: message => {
|
||||||
if (message.data === "pong") return;
|
if (message.data === "pong") return;
|
||||||
if (!peerConnection) return;
|
|
||||||
|
/*
|
||||||
|
Currently the signaling process is as follows:
|
||||||
|
After open, the other side will send a `device-metadata` message with the device version
|
||||||
|
If the device version is not set, we can assume the device is using the legacy signaling
|
||||||
|
Otherwise, we can assume the device is using the new signaling
|
||||||
|
|
||||||
|
If the device is using the legacy signaling, we close the websocket connection
|
||||||
|
and use the legacy HTTPSignaling function to get the remote session description
|
||||||
|
|
||||||
|
If the device is using the new signaling, we don't need to do anything special, but continue to use the websocket connection
|
||||||
|
to chat with the other peer about the connection
|
||||||
|
*/
|
||||||
|
|
||||||
const parsedMessage = JSON.parse(message.data);
|
const parsedMessage = JSON.parse(message.data);
|
||||||
|
if (parsedMessage.type === "device-metadata") {
|
||||||
|
const { deviceVersion } = parsedMessage.data;
|
||||||
|
console.log("[Websocket] Received device-metadata message");
|
||||||
|
console.log("[Websocket] Device version", deviceVersion);
|
||||||
|
// If the device version is not set, we can assume the device is using the legacy signaling
|
||||||
|
if (!deviceVersion) {
|
||||||
|
console.log("[Websocket] Device is using legacy signaling");
|
||||||
|
|
||||||
|
// Now we don't need the websocket connection anymore, as we've established that we need to use the legacy signaling
|
||||||
|
// which does everything over HTTP(at least from the perspective of the client)
|
||||||
|
isLegacySignalingEnabled.current = true;
|
||||||
|
getWebSocket()?.close();
|
||||||
|
} else {
|
||||||
|
console.log("[Websocket] Device is using new signaling");
|
||||||
|
isLegacySignalingEnabled.current = false;
|
||||||
|
}
|
||||||
|
setupPeerConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!peerConnection) return;
|
||||||
if (parsedMessage.type === "answer") {
|
if (parsedMessage.type === "answer") {
|
||||||
console.log("[Websocket] Received answer");
|
console.log("[Websocket] Received answer");
|
||||||
const readyForOffer =
|
const readyForOffer =
|
||||||
|
@ -314,7 +345,7 @@ export default function KvmIdRoute() {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Don't even retry once we declare failure
|
// Don't even retry once we declare failure
|
||||||
!connectionFailed,
|
!connectionFailed && isLegacySignalingEnabled.current === false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendWebRTCSignal = useCallback(
|
const sendWebRTCSignal = useCallback(
|
||||||
|
@ -326,6 +357,42 @@ export default function KvmIdRoute() {
|
||||||
[sendMessage],
|
[sendMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const legacyHTTPSignaling = useCallback(
|
||||||
|
async (pc: RTCPeerConnection) => {
|
||||||
|
const sd = btoa(JSON.stringify(pc.localDescription));
|
||||||
|
|
||||||
|
// Legacy mode == UI in cloud with updated code connecting to older device version.
|
||||||
|
// In device mode, old devices wont server this JS, and on newer devices legacy mode wont be enabled
|
||||||
|
const sessionUrl = `${CLOUD_API}/webrtc/session`;
|
||||||
|
|
||||||
|
console.log("Trying to get remote session description");
|
||||||
|
setLoadingMessage(
|
||||||
|
`Getting remote session description... ${signalingAttempts.current > 0 ? `(attempt ${signalingAttempts.current + 1})` : ""}`,
|
||||||
|
);
|
||||||
|
const res = await api.POST(sessionUrl, {
|
||||||
|
sd,
|
||||||
|
// When on device, we don't need to specify the device id, as it's already known
|
||||||
|
...(isOnDevice ? {} : { id: params.id }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
if (res.status === 401) return navigate(isOnDevice ? "/login-local" : "/login");
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error("Error getting SDP", { status: res.status, json });
|
||||||
|
cleanupAndStopReconnecting();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Successfully got Remote Session Description. Setting.");
|
||||||
|
setLoadingMessage("Setting remote session description...");
|
||||||
|
|
||||||
|
const decodedSd = atob(json.sd);
|
||||||
|
const parsedSd = JSON.parse(decodedSd);
|
||||||
|
setRemoteSessionDescription(pc, new RTCSessionDescription(parsedSd));
|
||||||
|
},
|
||||||
|
[cleanupAndStopReconnecting, navigate, params.id, setRemoteSessionDescription],
|
||||||
|
);
|
||||||
|
|
||||||
const setupPeerConnection = useCallback(async () => {
|
const setupPeerConnection = useCallback(async () => {
|
||||||
console.log("[setupPeerConnection] Setting up peer connection");
|
console.log("[setupPeerConnection] Setting up peer connection");
|
||||||
setConnectionFailed(false);
|
setConnectionFailed(false);
|
||||||
|
@ -346,6 +413,7 @@ export default function KvmIdRoute() {
|
||||||
? { iceServers: [iceConfig?.iceServers] }
|
? { iceServers: [iceConfig?.iceServers] }
|
||||||
: {}),
|
: {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
setPeerConnectionState(pc.connectionState);
|
setPeerConnectionState(pc.connectionState);
|
||||||
console.log("[setupPeerConnection] Peer connection created", pc);
|
console.log("[setupPeerConnection] Peer connection created", pc);
|
||||||
setLoadingMessage("Setting up connection to device...");
|
setLoadingMessage("Setting up connection to device...");
|
||||||
|
@ -371,7 +439,12 @@ export default function KvmIdRoute() {
|
||||||
const offer = await pc.createOffer();
|
const offer = await pc.createOffer();
|
||||||
await pc.setLocalDescription(offer);
|
await pc.setLocalDescription(offer);
|
||||||
const sd = btoa(JSON.stringify(pc.localDescription));
|
const sd = btoa(JSON.stringify(pc.localDescription));
|
||||||
sendWebRTCSignal("offer", { sd: sd });
|
const isNewSignalingEnabled = isLegacySignalingEnabled.current === false;
|
||||||
|
if (isNewSignalingEnabled) {
|
||||||
|
sendWebRTCSignal("offer", { sd: sd });
|
||||||
|
} else {
|
||||||
|
console.log("Legacy signanling. Waiting for ICE Gathering to complete...");
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(
|
||||||
`[setupPeerConnection] Error creating offer: ${e}`,
|
`[setupPeerConnection] Error creating offer: ${e}`,
|
||||||
|
@ -389,6 +462,22 @@ export default function KvmIdRoute() {
|
||||||
sendWebRTCSignal("new-ice-candidate", candidate);
|
sendWebRTCSignal("new-ice-candidate", candidate);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pc.onicegatheringstatechange = event => {
|
||||||
|
const pc = event.currentTarget as RTCPeerConnection;
|
||||||
|
if (pc.iceGatheringState === "complete") {
|
||||||
|
console.log("ICE Gathering completed");
|
||||||
|
setLoadingMessage("ICE Gathering completed");
|
||||||
|
|
||||||
|
if (isLegacySignalingEnabled.current) {
|
||||||
|
// We can now start the https/ws connection to get the remote session description from the KVM device
|
||||||
|
legacyHTTPSignaling(pc);
|
||||||
|
}
|
||||||
|
} else if (pc.iceGatheringState === "gathering") {
|
||||||
|
console.log("ICE Gathering Started");
|
||||||
|
setLoadingMessage("Gathering ICE candidates...");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pc.ontrack = function (event) {
|
pc.ontrack = function (event) {
|
||||||
setMediaMediaStream(event.streams[0]);
|
setMediaMediaStream(event.streams[0]);
|
||||||
};
|
};
|
||||||
|
@ -409,6 +498,7 @@ export default function KvmIdRoute() {
|
||||||
}, [
|
}, [
|
||||||
cleanupAndStopReconnecting,
|
cleanupAndStopReconnecting,
|
||||||
iceConfig?.iceServers,
|
iceConfig?.iceServers,
|
||||||
|
legacyHTTPSignaling,
|
||||||
peerConnection?.signalingState,
|
peerConnection?.signalingState,
|
||||||
sendWebRTCSignal,
|
sendWebRTCSignal,
|
||||||
setDiskChannel,
|
setDiskChannel,
|
||||||
|
@ -552,10 +642,6 @@ export default function KvmIdRoute() {
|
||||||
});
|
});
|
||||||
}, [rpcDataChannel?.readyState, send, setHdmiState]);
|
}, [rpcDataChannel?.readyState, send, setHdmiState]);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-expect-error
|
|
||||||
window.send = send;
|
|
||||||
|
|
||||||
// When the update is successful, we need to refresh the client javascript and show a success modal
|
// When the update is successful, we need to refresh the client javascript and show a success modal
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (queryParams.get("updateSuccess")) {
|
if (queryParams.get("updateSuccess")) {
|
||||||
|
@ -654,8 +740,6 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
if (isOtherSession) return null;
|
if (isOtherSession) return null;
|
||||||
if (peerConnectionState === "connected") return null;
|
if (peerConnectionState === "connected") return null;
|
||||||
|
|
||||||
console.log("isDisconnected", isDisconnected);
|
|
||||||
if (isDisconnected) {
|
if (isDisconnected) {
|
||||||
return <PeerConnectionDisconnectedOverlay show={true} />;
|
return <PeerConnectionDisconnectedOverlay show={true} />;
|
||||||
}
|
}
|
||||||
|
|
6
web.go
6
web.go
|
@ -12,6 +12,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/coder/websocket"
|
||||||
|
"github.com/coder/websocket/wsjson"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
|
@ -98,7 +99,7 @@ func setupRouter() *gin.Engine {
|
||||||
protected := r.Group("/")
|
protected := r.Group("/")
|
||||||
protected.Use(protectedMiddleware())
|
protected.Use(protectedMiddleware())
|
||||||
{
|
{
|
||||||
protected.GET("/webrtc/signaling", handleLocalWebRTCSignal)
|
protected.GET("/webrtc/signaling/client", handleLocalWebRTCSignal)
|
||||||
protected.POST("/cloud/register", handleCloudRegister)
|
protected.POST("/cloud/register", handleCloudRegister)
|
||||||
protected.GET("/cloud/state", handleCloudState)
|
protected.GET("/cloud/state", handleCloudState)
|
||||||
protected.GET("/device", handleDevice)
|
protected.GET("/device", handleDevice)
|
||||||
|
@ -143,6 +144,9 @@ func handleLocalWebRTCSignal(c *gin.Context) {
|
||||||
|
|
||||||
// Now use conn for websocket operations
|
// Now use conn for websocket operations
|
||||||
defer wsCon.Close(websocket.StatusNormalClosure, "")
|
defer wsCon.Close(websocket.StatusNormalClosure, "")
|
||||||
|
|
||||||
|
wsjson.Write(context.Background(), wsCon, gin.H{"type": "device-metadata", "data": gin.H{"deviceVersion": builtAppVersion}})
|
||||||
|
|
||||||
err = handleWebRTCSignalWsMessages(wsCon, false, source)
|
err = handleWebRTCSignalWsMessages(wsCon, false, source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
|
Loading…
Reference in New Issue