mirror of https://github.com/jetkvm/kvm.git
Compare commits
3 Commits
18f72700b6
...
5aeb4fafd1
Author | SHA1 | Date |
---|---|---|
|
5aeb4fafd1 | |
|
69168ff062 | |
|
0d7efe5c0e |
11
cloud.go
11
cloud.go
|
@ -7,13 +7,14 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"github.com/coder/websocket/wsjson"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket/wsjson"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/coder/websocket"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CloudRegisterRequest struct {
|
||||
|
@ -192,7 +193,11 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess
|
|||
return fmt.Errorf("google identity mismatch")
|
||||
}
|
||||
|
||||
session, err := newSession()
|
||||
session, err := newSession(SessionConfig{
|
||||
ICEServers: req.ICEServers,
|
||||
LocalIP: req.IP,
|
||||
IsCloud: true,
|
||||
})
|
||||
if err != nil {
|
||||
_ = wsjson.Write(context.Background(), c, gin.H{"error": err})
|
||||
return err
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"dev": "vite dev --mode=development",
|
||||
"build": "npm run build:prod",
|
||||
"build:device": "tsc && vite build --mode=device --emptyOutDir",
|
||||
"dev:device": "vite dev --mode=device",
|
||||
"build:prod": "tsc && vite build --mode=production",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
||||
},
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
useMountMediaStore,
|
||||
useUiStore,
|
||||
useSettingsStore,
|
||||
useVideoStore,
|
||||
} from "@/hooks/stores";
|
||||
import { MdOutlineContentPasteGo } from "react-icons/md";
|
||||
import Container from "@components/Container";
|
||||
|
@ -33,6 +34,7 @@ export default function Actionbar({
|
|||
state => state.remoteVirtualMediaState,
|
||||
);
|
||||
const developerMode = useSettingsStore(state => state.developerMode);
|
||||
const hdmiState = useVideoStore(state => state.hdmiState);
|
||||
|
||||
// This is the only way to get a reliable state change for the popover
|
||||
// at time of writing this there is no mount, or unmount event for the popover
|
||||
|
@ -247,6 +249,7 @@ export default function Actionbar({
|
|||
size="XS"
|
||||
theme="light"
|
||||
text="Fullscreen"
|
||||
disabled={hdmiState !== 'ready'}
|
||||
LeadingIcon={LuMaximize}
|
||||
onClick={() => requestFullscreen()}
|
||||
/>
|
||||
|
|
|
@ -30,6 +30,8 @@ export default function WebRTCVideo() {
|
|||
const {
|
||||
setClientSize: setVideoClientSize,
|
||||
setSize: setVideoSize,
|
||||
width: videoWidth,
|
||||
height: videoHeight,
|
||||
clientWidth: videoClientWidth,
|
||||
clientHeight: videoClientHeight,
|
||||
} = useVideoStore();
|
||||
|
@ -102,20 +104,43 @@ export default function WebRTCVideo() {
|
|||
const mouseMoveHandler = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!videoClientWidth || !videoClientHeight) return;
|
||||
const { buttons } = e;
|
||||
// Get the aspect ratios of the video element and the video stream
|
||||
const videoElementAspectRatio = videoClientWidth / videoClientHeight;
|
||||
const videoStreamAspectRatio = videoWidth / videoHeight;
|
||||
|
||||
// Clamp mouse position within the video boundaries
|
||||
const currMouseX = Math.min(Math.max(1, e.offsetX), videoClientWidth);
|
||||
const currMouseY = Math.min(Math.max(1, e.offsetY), videoClientHeight);
|
||||
// Calculate the effective video display area
|
||||
let effectiveWidth = videoClientWidth;
|
||||
let effectiveHeight = videoClientHeight;
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
|
||||
// Normalize mouse position to 0-32767 range (HID absolute coordinate system)
|
||||
const x = Math.round((currMouseX / videoClientWidth) * 32767);
|
||||
const y = Math.round((currMouseY / videoClientHeight) * 32767);
|
||||
if (videoElementAspectRatio > videoStreamAspectRatio) {
|
||||
// Pillarboxing: black bars on the left and right
|
||||
effectiveWidth = videoClientHeight * videoStreamAspectRatio;
|
||||
offsetX = (videoClientWidth - effectiveWidth) / 2;
|
||||
} else if (videoElementAspectRatio < videoStreamAspectRatio) {
|
||||
// Letterboxing: black bars on the top and bottom
|
||||
effectiveHeight = videoClientWidth / videoStreamAspectRatio;
|
||||
offsetY = (videoClientHeight - effectiveHeight) / 2;
|
||||
}
|
||||
|
||||
// Clamp mouse position within the effective video boundaries
|
||||
const clampedX = Math.min(Math.max(offsetX, e.offsetX), offsetX + effectiveWidth);
|
||||
const clampedY = Math.min(Math.max(offsetY, e.offsetY), offsetY + effectiveHeight);
|
||||
|
||||
// Map clamped mouse position to the video stream's coordinate system
|
||||
const relativeX = (clampedX - offsetX) / effectiveWidth;
|
||||
const relativeY = (clampedY - offsetY) / effectiveHeight;
|
||||
|
||||
// Convert to HID absolute coordinate system (0-32767 range)
|
||||
const x = Math.round(relativeX * 32767);
|
||||
const y = Math.round(relativeY * 32767);
|
||||
|
||||
// Send mouse movement
|
||||
const { buttons } = e;
|
||||
sendMouseMovement(x, y, buttons);
|
||||
},
|
||||
[sendMouseMovement, videoClientHeight, videoClientWidth],
|
||||
[sendMouseMovement, videoClientHeight, videoClientWidth, videoWidth, videoHeight],
|
||||
);
|
||||
|
||||
const mouseWheelHandler = useCallback(
|
||||
|
|
|
@ -2,13 +2,31 @@ import { defineConfig } from "vite";
|
|||
import react from "@vitejs/plugin-react-swc";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
declare const process: {
|
||||
env: {
|
||||
JETKVM_PROXY_URL: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default defineConfig(({ mode, command }) => {
|
||||
const isCloud = mode === "production";
|
||||
const onDevice = mode === "device";
|
||||
const { JETKVM_PROXY_URL } = process.env;
|
||||
|
||||
return {
|
||||
plugins: [tsconfigPaths(), react()],
|
||||
build: { outDir: isCloud ? "dist" : "../static" },
|
||||
server: { host: "0.0.0.0" },
|
||||
base: onDevice ? "/static" : "/",
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
proxy: JETKVM_PROXY_URL ? {
|
||||
'/me': JETKVM_PROXY_URL,
|
||||
'/device': JETKVM_PROXY_URL,
|
||||
'/webrtc': JETKVM_PROXY_URL,
|
||||
'/auth': JETKVM_PROXY_URL,
|
||||
'/storage': JETKVM_PROXY_URL,
|
||||
'/cloud': JETKVM_PROXY_URL,
|
||||
} : undefined
|
||||
},
|
||||
base: onDevice && command === 'build' ? "/static" : "/",
|
||||
};
|
||||
});
|
||||
|
|
8
web.go
8
web.go
|
@ -17,8 +17,10 @@ import (
|
|||
var staticFiles embed.FS
|
||||
|
||||
type WebRTCSessionRequest struct {
|
||||
Sd string `json:"sd"`
|
||||
OidcGoogle string `json:"OidcGoogle,omitempty"`
|
||||
Sd string `json:"sd"`
|
||||
OidcGoogle string `json:"OidcGoogle,omitempty"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
ICEServers []string `json:"iceServers,omitempty"`
|
||||
}
|
||||
|
||||
type SetPasswordRequest struct {
|
||||
|
@ -116,7 +118,7 @@ func handleWebRTCSession(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
session, err := newSession()
|
||||
session, err := newSession(SessionConfig{})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err})
|
||||
return
|
||||
|
|
33
webrtc.go
33
webrtc.go
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
|
@ -19,6 +20,12 @@ type Session struct {
|
|||
shouldUmountVirtualMedia bool
|
||||
}
|
||||
|
||||
type SessionConfig struct {
|
||||
ICEServers []string
|
||||
LocalIP string
|
||||
IsCloud bool
|
||||
}
|
||||
|
||||
func (s *Session) ExchangeOffer(offerStr string) (string, error) {
|
||||
b, err := base64.StdEncoding.DecodeString(offerStr)
|
||||
if err != nil {
|
||||
|
@ -61,9 +68,29 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) {
|
|||
return base64.StdEncoding.EncodeToString(localDescription), nil
|
||||
}
|
||||
|
||||
func newSession() (*Session, error) {
|
||||
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{{}},
|
||||
func newSession(config SessionConfig) (*Session, error) {
|
||||
webrtcSettingEngine := webrtc.SettingEngine{}
|
||||
iceServer := webrtc.ICEServer{}
|
||||
|
||||
if config.IsCloud {
|
||||
if config.ICEServers == nil {
|
||||
fmt.Printf("ICE Servers not provided by cloud")
|
||||
} else {
|
||||
iceServer.URLs = config.ICEServers
|
||||
fmt.Printf("Using ICE Servers provided by cloud: %v\n", iceServer.URLs)
|
||||
}
|
||||
|
||||
if config.LocalIP == "" || net.ParseIP(config.LocalIP) == nil {
|
||||
fmt.Printf("Local IP address %v not provided or invalid, won't set NAT1To1IPs\n", config.LocalIP)
|
||||
} else {
|
||||
webrtcSettingEngine.SetNAT1To1IPs([]string{config.LocalIP}, webrtc.ICECandidateTypeSrflx)
|
||||
fmt.Printf("Setting NAT1To1IPs to %s\n", config.LocalIP)
|
||||
}
|
||||
}
|
||||
|
||||
api := webrtc.NewAPI(webrtc.WithSettingEngine(webrtcSettingEngine))
|
||||
peerConnection, err := api.NewPeerConnection(webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{iceServer},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
Loading…
Reference in New Issue