Compare commits

..

1 Commits

Author SHA1 Message Date
Brandon Tuttle 4a6e02ae7d
Merge ccfd63b84f into 15768ee0ab 2025-02-11 09:16:21 -05:00
7 changed files with 20 additions and 101 deletions

View File

@ -7,14 +7,13 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"time"
"github.com/coder/websocket/wsjson" "github.com/coder/websocket/wsjson"
"time"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/coder/websocket"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/coder/websocket"
) )
type CloudRegisterRequest struct { type CloudRegisterRequest struct {
@ -193,11 +192,7 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess
return fmt.Errorf("google identity mismatch") return fmt.Errorf("google identity mismatch")
} }
session, err := newSession(SessionConfig{ session, err := newSession()
ICEServers: req.ICEServers,
LocalIP: req.IP,
IsCloud: true,
})
if err != nil { if err != nil {
_ = wsjson.Write(context.Background(), c, gin.H{"error": err}) _ = wsjson.Write(context.Background(), c, gin.H{"error": err})
return err return err

View File

@ -10,7 +10,6 @@
"dev": "vite dev --mode=development", "dev": "vite dev --mode=development",
"build": "npm run build:prod", "build": "npm run build:prod",
"build:device": "tsc && vite build --mode=device --emptyOutDir", "build:device": "tsc && vite build --mode=device --emptyOutDir",
"dev:device": "vite dev --mode=device",
"build:prod": "tsc && vite build --mode=production", "build:prod": "tsc && vite build --mode=production",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
}, },

View File

@ -4,7 +4,6 @@ import {
useMountMediaStore, useMountMediaStore,
useUiStore, useUiStore,
useSettingsStore, useSettingsStore,
useVideoStore,
} from "@/hooks/stores"; } from "@/hooks/stores";
import { MdOutlineContentPasteGo } from "react-icons/md"; import { MdOutlineContentPasteGo } from "react-icons/md";
import Container from "@components/Container"; import Container from "@components/Container";
@ -34,7 +33,6 @@ export default function Actionbar({
state => state.remoteVirtualMediaState, state => state.remoteVirtualMediaState,
); );
const developerMode = useSettingsStore(state => state.developerMode); 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 // 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 // at time of writing this there is no mount, or unmount event for the popover
@ -249,7 +247,6 @@ export default function Actionbar({
size="XS" size="XS"
theme="light" theme="light"
text="Fullscreen" text="Fullscreen"
disabled={hdmiState !== 'ready'}
LeadingIcon={LuMaximize} LeadingIcon={LuMaximize}
onClick={() => requestFullscreen()} onClick={() => requestFullscreen()}
/> />

View File

@ -30,8 +30,6 @@ export default function WebRTCVideo() {
const { const {
setClientSize: setVideoClientSize, setClientSize: setVideoClientSize,
setSize: setVideoSize, setSize: setVideoSize,
width: videoWidth,
height: videoHeight,
clientWidth: videoClientWidth, clientWidth: videoClientWidth,
clientHeight: videoClientHeight, clientHeight: videoClientHeight,
} = useVideoStore(); } = useVideoStore();
@ -104,43 +102,20 @@ export default function WebRTCVideo() {
const mouseMoveHandler = useCallback( const mouseMoveHandler = useCallback(
(e: MouseEvent) => { (e: MouseEvent) => {
if (!videoClientWidth || !videoClientHeight) return; if (!videoClientWidth || !videoClientHeight) return;
// Get the aspect ratios of the video element and the video stream const { buttons } = e;
const videoElementAspectRatio = videoClientWidth / videoClientHeight;
const videoStreamAspectRatio = videoWidth / videoHeight;
// Calculate the effective video display area // Clamp mouse position within the video boundaries
let effectiveWidth = videoClientWidth; const currMouseX = Math.min(Math.max(1, e.offsetX), videoClientWidth);
let effectiveHeight = videoClientHeight; const currMouseY = Math.min(Math.max(1, e.offsetY), videoClientHeight);
let offsetX = 0;
let offsetY = 0;
if (videoElementAspectRatio > videoStreamAspectRatio) { // Normalize mouse position to 0-32767 range (HID absolute coordinate system)
// Pillarboxing: black bars on the left and right const x = Math.round((currMouseX / videoClientWidth) * 32767);
effectiveWidth = videoClientHeight * videoStreamAspectRatio; const y = Math.round((currMouseY / videoClientHeight) * 32767);
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 // Send mouse movement
const { buttons } = e;
sendMouseMovement(x, y, buttons); sendMouseMovement(x, y, buttons);
}, },
[sendMouseMovement, videoClientHeight, videoClientWidth, videoWidth, videoHeight], [sendMouseMovement, videoClientHeight, videoClientWidth],
); );
const mouseWheelHandler = useCallback( const mouseWheelHandler = useCallback(

View File

@ -2,31 +2,13 @@ import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths";
declare const process: { export default defineConfig(({ mode }) => {
env: {
JETKVM_PROXY_URL: string;
};
};
export default defineConfig(({ mode, command }) => {
const isCloud = mode === "production"; const isCloud = mode === "production";
const onDevice = mode === "device"; const onDevice = mode === "device";
const { JETKVM_PROXY_URL } = process.env;
return { return {
plugins: [tsconfigPaths(), react()], plugins: [tsconfigPaths(), react()],
build: { outDir: isCloud ? "dist" : "../static" }, build: { outDir: isCloud ? "dist" : "../static" },
server: { server: { host: "0.0.0.0" },
host: "0.0.0.0", base: onDevice ? "/static" : "/",
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
View File

@ -17,10 +17,8 @@ import (
var staticFiles embed.FS var staticFiles embed.FS
type WebRTCSessionRequest struct { type WebRTCSessionRequest struct {
Sd string `json:"sd"` Sd string `json:"sd"`
OidcGoogle string `json:"OidcGoogle,omitempty"` OidcGoogle string `json:"OidcGoogle,omitempty"`
IP string `json:"ip,omitempty"`
ICEServers []string `json:"iceServers,omitempty"`
} }
type SetPasswordRequest struct { type SetPasswordRequest struct {
@ -118,7 +116,7 @@ func handleWebRTCSession(c *gin.Context) {
return return
} }
session, err := newSession(SessionConfig{}) session, err := newSession()
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err}) c.JSON(http.StatusInternalServerError, gin.H{"error": err})
return return

View File

@ -4,7 +4,6 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"strings" "strings"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
@ -20,12 +19,6 @@ type Session struct {
shouldUmountVirtualMedia bool shouldUmountVirtualMedia bool
} }
type SessionConfig struct {
ICEServers []string
LocalIP string
IsCloud bool
}
func (s *Session) ExchangeOffer(offerStr string) (string, error) { func (s *Session) ExchangeOffer(offerStr string) (string, error) {
b, err := base64.StdEncoding.DecodeString(offerStr) b, err := base64.StdEncoding.DecodeString(offerStr)
if err != nil { if err != nil {
@ -68,29 +61,9 @@ func (s *Session) ExchangeOffer(offerStr string) (string, error) {
return base64.StdEncoding.EncodeToString(localDescription), nil return base64.StdEncoding.EncodeToString(localDescription), nil
} }
func newSession(config SessionConfig) (*Session, error) { func newSession() (*Session, error) {
webrtcSettingEngine := webrtc.SettingEngine{} peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
iceServer := webrtc.ICEServer{} ICEServers: []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 { if err != nil {
return nil, err return nil, err