mirror of https://github.com/jetkvm/kvm.git
Compare commits
5 Commits
ae3f5fba93
...
ce1c4028a4
Author | SHA1 | Date |
---|---|---|
|
ce1c4028a4 | |
|
ff3727b1fe | |
|
d415afcea9 | |
|
368c1eea90 | |
|
f49c405509 |
4
Makefile
4
Makefile
|
@ -1,5 +1,5 @@
|
||||||
VERSION_DEV := 0.3.7-dev$(shell date +%Y%m%d%H%M)
|
VERSION_DEV := 0.3.8-dev$(shell date +%Y%m%d%H%M)
|
||||||
VERSION := 0.3.6
|
VERSION := 0.3.7
|
||||||
|
|
||||||
hash_resource:
|
hash_resource:
|
||||||
@shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256
|
@shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -21,6 +21,7 @@ require (
|
||||||
github.com/pion/webrtc/v4 v4.0.0
|
github.com/pion/webrtc/v4 v4.0.0
|
||||||
github.com/pojntfx/go-nbd v0.3.2
|
github.com/pojntfx/go-nbd v0.3.2
|
||||||
github.com/psanford/httpreadat v0.1.0
|
github.com/psanford/httpreadat v0.1.0
|
||||||
|
github.com/ulikunitz/xz v0.5.12
|
||||||
github.com/vishvananda/netlink v1.3.0
|
github.com/vishvananda/netlink v1.3.0
|
||||||
go.bug.st/serial v1.6.2
|
go.bug.st/serial v1.6.2
|
||||||
golang.org/x/crypto v0.28.0
|
golang.org/x/crypto v0.28.0
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -142,6 +142,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||||
|
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
||||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
|
|
|
@ -285,11 +285,6 @@ export default function WebRTCVideo() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const prev = useHidStore.getState();
|
const prev = useHidStore.getState();
|
||||||
|
|
||||||
// if (document.activeElement?.id !== "videoFocusTrap") {
|
|
||||||
// console.log("KEYUP: Not focusing on the video", document.activeElement);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
setIsNumLockActive(e.getModifierState("NumLock"));
|
setIsNumLockActive(e.getModifierState("NumLock"));
|
||||||
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
||||||
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
||||||
|
@ -336,6 +331,18 @@ export default function WebRTCVideo() {
|
||||||
[keyDownHandler, keyUpHandler, resetKeyboardState, sendKeyboardEvent],
|
[keyDownHandler, keyUpHandler, resetKeyboardState, sendKeyboardEvent],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const videoKeyUpHandler = useCallback((e: KeyboardEvent) => {
|
||||||
|
// In fullscreen mode in chrome & safari, the space key is used to pause/play the video
|
||||||
|
// there is no way to prevent this, so we need to simply force play the video when it's paused.
|
||||||
|
// Fix only works in chrome based browsers.
|
||||||
|
if (e.code === "Space") {
|
||||||
|
if (videoElm.current?.paused == true) {
|
||||||
|
console.log("Force playing video");
|
||||||
|
videoElm.current?.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
function setupVideoEventListeners() {
|
function setupVideoEventListeners() {
|
||||||
let videoElmRefValue = null;
|
let videoElmRefValue = null;
|
||||||
|
@ -347,7 +354,7 @@ export default function WebRTCVideo() {
|
||||||
videoElmRefValue.addEventListener("mousemove", mouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("mousemove", mouseMoveHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("pointerdown", mouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("pointerdown", mouseMoveHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("pointerup", mouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("pointerup", mouseMoveHandler, { signal });
|
||||||
|
videoElmRefValue.addEventListener("keyup", videoKeyUpHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, { signal });
|
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, { signal });
|
||||||
videoElmRefValue.addEventListener(
|
videoElmRefValue.addEventListener(
|
||||||
"contextmenu",
|
"contextmenu",
|
||||||
|
@ -364,7 +371,13 @@ export default function WebRTCVideo() {
|
||||||
if (videoElmRefValue) abortController.abort();
|
if (videoElmRefValue) abortController.abort();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[mouseMoveHandler, resetMousePosition, onVideoPlaying, mouseWheelHandler],
|
[
|
||||||
|
mouseMoveHandler,
|
||||||
|
resetMousePosition,
|
||||||
|
onVideoPlaying,
|
||||||
|
mouseWheelHandler,
|
||||||
|
videoKeyUpHandler,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
|
@ -410,7 +423,7 @@ export default function WebRTCVideo() {
|
||||||
}, [sendKeyboardEvent, setDisableVideoFocusTrap, sidebarView]);
|
}, [sendKeyboardEvent, setDisableVideoFocusTrap, sidebarView]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid w-full h-full grid-rows-layout">
|
<div className="grid h-full w-full grid-rows-layout">
|
||||||
<div className="min-h-[39.5px]">
|
<div className="min-h-[39.5px]">
|
||||||
<fieldset disabled={peerConnectionState !== "connected"}>
|
<fieldset disabled={peerConnectionState !== "connected"}>
|
||||||
<Actionbar
|
<Actionbar
|
||||||
|
@ -427,18 +440,18 @@ export default function WebRTCVideo() {
|
||||||
<div className="relative h-full">
|
<div className="relative h-full">
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"absolute inset-0 bg-blue-50/40 dark:bg-slate-800/40 opacity-80",
|
"absolute inset-0 bg-blue-50/40 opacity-80 dark:bg-slate-800/40",
|
||||||
"[background-image:radial-gradient(theme(colors.blue.300)_0.5px,transparent_0.5px),radial-gradient(theme(colors.blue.300)_0.5px,transparent_0.5px)] dark:[background-image:radial-gradient(theme(colors.slate.700)_0.5px,transparent_0.5px),radial-gradient(theme(colors.slate.700)_0.5px,transparent_0.5px)]",
|
"[background-image:radial-gradient(theme(colors.blue.300)_0.5px,transparent_0.5px),radial-gradient(theme(colors.blue.300)_0.5px,transparent_0.5px)] dark:[background-image:radial-gradient(theme(colors.slate.700)_0.5px,transparent_0.5px),radial-gradient(theme(colors.slate.700)_0.5px,transparent_0.5px)]",
|
||||||
"[background-position:0_0,10px_10px]",
|
"[background-position:0_0,10px_10px]",
|
||||||
"[background-size:20px_20px]",
|
"[background-size:20px_20px]",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex h-full flex-col">
|
||||||
<div className="relative flex-grow overflow-hidden">
|
<div className="relative flex-grow overflow-hidden">
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex h-full flex-col">
|
||||||
<div className="grid flex-grow overflow-hidden grid-rows-bodyFooter">
|
<div className="grid flex-grow grid-rows-bodyFooter overflow-hidden">
|
||||||
<div className="relative flex items-center justify-center mx-4 my-2 overflow-hidden">
|
<div className="relative mx-4 my-2 flex items-center justify-center overflow-hidden">
|
||||||
<div className="relative flex items-center justify-center w-full h-full">
|
<div className="relative flex h-full w-full items-center justify-center">
|
||||||
<video
|
<video
|
||||||
ref={videoElm}
|
ref={videoElm}
|
||||||
autoPlay={true}
|
autoPlay={true}
|
||||||
|
@ -454,14 +467,14 @@ export default function WebRTCVideo() {
|
||||||
{
|
{
|
||||||
"cursor-none": settings.isCursorHidden,
|
"cursor-none": settings.isCursorHidden,
|
||||||
"opacity-0": isLoading || isConnectionError || hdmiError,
|
"opacity-0": isLoading || isConnectionError || hdmiError,
|
||||||
"animate-slideUpFade border border-slate-800/30 dark:border-slate-300/20 opacity-0 shadow":
|
"animate-slideUpFade border border-slate-800/30 opacity-0 shadow dark:border-slate-300/20":
|
||||||
isPlaying,
|
isPlaying,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{ animationDuration: "500ms" }}
|
style={{ animationDuration: "500ms" }}
|
||||||
className="absolute inset-0 flex items-center justify-center opacity-0 pointer-events-none animate-slideUpFade"
|
className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center opacity-0"
|
||||||
>
|
>
|
||||||
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
<div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
|
||||||
<LoadingOverlay show={isLoading} />
|
<LoadingOverlay show={isLoading} />
|
||||||
|
|
|
@ -239,6 +239,7 @@ export default function SettingsSidebar() {
|
||||||
}
|
}
|
||||||
|
|
||||||
setBacklightSettings(settings);
|
setBacklightSettings(settings);
|
||||||
|
handleBacklightSettingsSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBacklightSettingsSave = () => {
|
const handleBacklightSettingsSave = () => {
|
||||||
|
@ -362,7 +363,7 @@ export default function SettingsSidebar() {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
setUsbEmulationEnabled(resp.result as boolean);
|
setUsbEmulationEnabled(resp.result as boolean);
|
||||||
});
|
});
|
||||||
}, [getCloudState, send, setDeveloperMode, setHideCursor, setJiggler]);
|
}, [getCloudState, send, setBacklightSettings, setDeveloperMode, setHideCursor, setJiggler]);
|
||||||
|
|
||||||
const getDevice = useCallback(async () => {
|
const getDevice = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -900,12 +901,6 @@ export default function SettingsSidebar() {
|
||||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||||
The display will wake up when the connection state changes, or when touched.
|
The display will wake up when the connection state changes, or when touched.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
|
||||||
size="SM"
|
|
||||||
theme="primary"
|
|
||||||
text="Save Display Settings"
|
|
||||||
onClick={handleBacklightSettingsSave}
|
|
||||||
/>
|
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
<div className="pb-2 space-y-4">
|
<div className="pb-2 space-y-4">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
|
"github.com/ulikunitz/xz"
|
||||||
)
|
)
|
||||||
|
|
||||||
const massStorageName = "mass_storage.usb0"
|
const massStorageName = "mass_storage.usb0"
|
||||||
|
@ -269,16 +270,48 @@ func rpcMountWithStorage(filename string, mode VirtualMediaMode) error {
|
||||||
return fmt.Errorf("failed to get file info: %w", err)
|
return fmt.Errorf("failed to get file info: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = setMassStorageImage(fullPath)
|
// Handle XZ compressed images
|
||||||
|
if strings.HasSuffix(filename, ".img.xz") {
|
||||||
|
logger.Info("Mounting compressed XZ image")
|
||||||
|
|
||||||
|
// Create temporary file for decompressed image
|
||||||
|
decompressedPath := filepath.Join(imagesFolder, strings.TrimSuffix(filename, ".xz")+".temp")
|
||||||
|
defer os.Remove(decompressedPath) // Clean up temp file after mounting
|
||||||
|
|
||||||
|
err = decompressXZImage(fullPath, decompressedPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decompress XZ image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount the decompressed image
|
||||||
|
err = mountImage(decompressedPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to mount decompressed image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentVirtualMediaState = &VirtualMediaState{
|
||||||
|
Source: Storage,
|
||||||
|
Mode: mode,
|
||||||
|
Filename: filename,
|
||||||
|
Size: fileInfo.Size(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle regular images
|
||||||
|
err = mountImage(fullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to set mass storage image: %w", err)
|
return fmt.Errorf("failed to set mass storage image: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentVirtualMediaState = &VirtualMediaState{
|
currentVirtualMediaState = &VirtualMediaState{
|
||||||
Source: Storage,
|
Source: Storage,
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
Filename: filename,
|
Filename: filename,
|
||||||
Size: fileInfo.Size(),
|
Size: fileInfo.Size(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -556,3 +589,30 @@ func handleUploadHttp(c *gin.Context) {
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Upload completed"})
|
c.JSON(http.StatusOK, gin.H{"message": "Upload completed"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add this helper function to handle XZ decompression
|
||||||
|
func decompressXZImage(sourcePath, destPath string) error {
|
||||||
|
sourceFile, err := os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open source file: %w", err)
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
|
||||||
|
reader, err := xz.NewReader(sourceFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create XZ reader: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
destFile, err := os.Create(destPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create destination file: %w", err)
|
||||||
|
}
|
||||||
|
defer destFile.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(destFile, reader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decompress file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue