浏览代码

Fixed audio lag behind bug

TC 4 天之前
父节点
当前提交
6663c77201

二进制
dezukvmd/dezukvmd


+ 28 - 1
dezukvmd/ipkvm.go

@@ -12,6 +12,7 @@ import (
 
 	"github.com/gorilla/csrf"
 	"imuslab.com/dezukvm/dezukvmd/mod/dezukvm"
+	"imuslab.com/dezukvm/dezukvmd/mod/utils"
 )
 
 var (
@@ -90,11 +91,12 @@ func init_ipkvm_mode() error {
 
 	csrfMiddleware := csrf.Protect(
 		[]byte(nodeUUID),
-		csrf.CookieName("dezukvm-csrf"),
+		csrf.CookieName("dezukvm_csrf_token"),
 		csrf.Secure(false),
 		csrf.Path("/"),
 		csrf.SameSite(csrf.SameSiteLaxMode),
 	)
+
 	err = http.ListenAndServe(":9000", csrfMiddleware(listeningServerMux))
 	return err
 }
@@ -116,6 +118,31 @@ func register_ipkvm_apis(mux *http.ServeMux) {
 		dezukvmManager.HandleHIDEvents(w, r, instanceUUID)
 	})
 
+	mux.HandleFunc("/api/v1/mass_storage/switch", func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != http.MethodPost {
+			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			return
+		}
+		instanceUUID, err := utils.PostPara(r, "uuid")
+		if err != nil {
+			http.Error(w, "Missing or invalid uuid parameter", http.StatusBadRequest)
+			return
+		}
+		side, err := utils.PostPara(r, "side")
+		if err != nil {
+			http.Error(w, "Missing or invalid side parameter", http.StatusBadRequest)
+			return
+		}
+		switch side {
+		case "kvm":
+			dezukvmManager.HandleMassStorageSideSwitch(w, r, instanceUUID, true)
+		case "remote":
+			dezukvmManager.HandleMassStorageSideSwitch(w, r, instanceUUID, false)
+		default:
+			http.Error(w, "Invalid side parameter", http.StatusBadRequest)
+		}
+	})
+
 	mux.HandleFunc("/api/v1/instances", func(w http.ResponseWriter, r *http.Request) {
 		if r.Method == http.MethodGet {
 			dezukvmManager.HandleListInstances(w, r)

+ 26 - 0
dezukvmd/mod/dezukvm/handlers.go

@@ -32,6 +32,32 @@ func (d *DezukVM) HandleHIDEvents(w http.ResponseWriter, r *http.Request, instan
 	targetInstance.usbKVMController.HIDWebSocketHandler(w, r)
 }
 
+// HandleMassStorageSideSwitch handles the request to switch the USB mass storage side.
+// there is only two state for the USB mass storage side, KVM side or Remote side.
+// isKvmSide = true means switch to KVM side, otherwise switch to Remote side.
+func (d *DezukVM) HandleMassStorageSideSwitch(w http.ResponseWriter, r *http.Request, instanceUuid string, isKvmSide bool) {
+	targetInstance, err := d.GetInstanceByUUID(instanceUuid)
+	if err != nil {
+		http.Error(w, "Instance with specified UUID not found", http.StatusNotFound)
+		return
+	}
+	if targetInstance.auxMCUController == nil {
+		http.Error(w, "Auxiliary MCU controller not initialized or missing", http.StatusInternalServerError)
+		return
+	}
+	if isKvmSide {
+		err = targetInstance.auxMCUController.SwitchUSBToKVM()
+	} else {
+		err = targetInstance.auxMCUController.SwitchUSBToRemote()
+	}
+	if err != nil {
+		http.Error(w, "Failed to switch USB mass storage side: "+err.Error(), http.StatusInternalServerError)
+		return
+	}
+	w.WriteHeader(http.StatusOK)
+	w.Write([]byte("OK"))
+}
+
 func (d *DezukVM) HandleListInstances(w http.ResponseWriter, r *http.Request) {
 	instances := []map[string]interface{}{}
 	for _, instance := range d.UsbKvmInstance {

+ 105 - 0
dezukvmd/mod/utils/utils.go

@@ -0,0 +1,105 @@
+package utils
+
+import (
+	"errors"
+	"net/http"
+	"strconv"
+	"strings"
+)
+
+// Response related
+func SendTextResponse(w http.ResponseWriter, msg string) {
+	w.Write([]byte(msg))
+}
+
+// Send JSON response, with an extra json header
+func SendJSONResponse(w http.ResponseWriter, json string) {
+	w.Header().Set("Content-Type", "application/json")
+	w.Write([]byte(json))
+}
+
+func SendErrorResponse(w http.ResponseWriter, errMsg string) {
+	w.Header().Set("Content-Type", "application/json")
+	w.Write([]byte("{\"error\":\"" + errMsg + "\"}"))
+}
+
+func SendOK(w http.ResponseWriter) {
+	w.Header().Set("Content-Type", "application/json")
+	w.Write([]byte("\"OK\""))
+}
+
+// Get GET parameter
+func GetPara(r *http.Request, key string) (string, error) {
+	// Get first value from the URL query
+	value := r.URL.Query().Get(key)
+	if len(value) == 0 {
+		return "", errors.New("invalid " + key + " given")
+	}
+	return value, nil
+}
+
+// Get GET paramter as boolean, accept 1 or true
+func GetBool(r *http.Request, key string) (bool, error) {
+	x, err := GetPara(r, key)
+	if err != nil {
+		return false, err
+	}
+
+	// Convert to lowercase and trim spaces just once to compare
+	switch strings.ToLower(strings.TrimSpace(x)) {
+	case "1", "true", "on":
+		return true, nil
+	case "0", "false", "off":
+		return false, nil
+	}
+
+	return false, errors.New("invalid boolean given")
+}
+
+// Get POST parameter
+func PostPara(r *http.Request, key string) (string, error) {
+	// Try to parse the form
+	if err := r.ParseForm(); err != nil {
+		return "", err
+	}
+	// Get first value from the form
+	x := r.Form.Get(key)
+	if len(x) == 0 {
+		return "", errors.New("invalid " + key + " given")
+	}
+	return x, nil
+}
+
+// Get POST paramter as boolean, accept 1 or true
+func PostBool(r *http.Request, key string) (bool, error) {
+	x, err := PostPara(r, key)
+	if err != nil {
+		return false, err
+	}
+
+	// Convert to lowercase and trim spaces just once to compare
+	switch strings.ToLower(strings.TrimSpace(x)) {
+	case "1", "true", "on":
+		return true, nil
+	case "0", "false", "off":
+		return false, nil
+	}
+
+	return false, errors.New("invalid boolean given")
+}
+
+// Get POST paramter as int
+func PostInt(r *http.Request, key string) (int, error) {
+	x, err := PostPara(r, key)
+	if err != nil {
+		return 0, err
+	}
+
+	x = strings.TrimSpace(x)
+	rx, err := strconv.Atoi(x)
+	if err != nil {
+		return 0, err
+	}
+
+	return rx, nil
+}

+ 23 - 24
dezukvmd/www/index.html

@@ -2,12 +2,12 @@
 <html lang="en">
     <head>
         <meta charset="UTF-8">
-        <title>DezuKVM | Dashboard</title>
+        <title>Dashboard | DezuKVM</title>
         <meta name="csrf_token" content="">
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <meta name="dezukvm.csrf.token" content="{{.csrfToken}}">
         <link rel="icon" type="image/png" href="/favicon.png">
-        <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
+        <script src="js/jquery-3.7.1.min.js"></script>
         <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.4/semantic.min.css" integrity="sha512-ySrYzxj+EI1e9xj/kRYqeDL5l1wW0IWY8pzHNTIZ+vc1D3Z14UDNPbwup4yOUmlRemYjgUXsUZ/xvCQU2ThEAw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
         <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.4/semantic.min.js" integrity="sha512-Y/wIVu+S+XJsDL7I+nL50kAVFLMqSdvuLqF2vMoRqiMkmvcqFjEpEgeu6Rx8tpZXKp77J8OUpMKy0m3jLYhbbw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
         
@@ -24,23 +24,23 @@
                 height: 100vh;
             }
             .sidebar {
-                width: 100px;
-                background: #f4f5f7; /* Changed from #222 to a whitish grey */
-                color: #222; /* Changed from #fff to dark text */
+                width: 64px;
+                background: #f4f5f7;
+                color: #222;
                 display: flex;
                 flex-direction: column;
                 justify-content: space-between;
                 border-right: 1px solid #e0e1e2;
             }
             .sidebar .menu-top {
-                padding: 32px 0 0 0;
+                padding: 1em 0 0 0;
                 display: flex;
                 flex-direction: column;
                 align-items: center;
             }
             .sidebar .logo {
-                width: 64px;
-                height: 64px;
+                width: 44px;
+                height: 44px;
                 margin-bottom: 32px;
                 border-radius: 12px;
                 display: flex;
@@ -57,42 +57,42 @@
                 width: 100%;
             }
             .sidebar .menu-options .item {
-                padding: 16px 32px;
+                padding: 0.8em 0.4em;
                 cursor: pointer;
                 transition: background 0.2s;
                 font-size: 1.1em;
                 color: #222;
+                text-align: center;
             }
             .sidebar .menu-options .item:hover,
             .sidebar .menu-options .item.active {
                 background: #e9ecef; /* Lighter hover/active background */
             }
             .sidebar .menu-bottom {
-                background: #f0f1f3; /* Lighter than sidebar */
+                background: #2b2b2b; /* Lighter than sidebar */
                 padding: 24px 0 24px 0;
                 display: flex;
                 flex-direction: column;
                 align-items: center;
             }
             .sidebar .menu-bottom .item {
-                color: #555;
-                padding: 12px 32px;
+                color: #eee;
+                padding: 0.8em 0.4em;
                 width: 100%;
-                text-align: center;
+                text-align: center !important;
                 cursor: pointer;
                 border-radius: 6px;
                 margin-bottom: 8px;
                 transition: background 0.2s, color 0.2s;
             }
             .sidebar .menu-bottom .item:hover {
-                background: #e9ecef;
-                color: #222;
+                background: #444;
+                color: #fff;
             }
             .content {
                 flex: 1;
                 background: #f9fafb;
-                padding: 32px;
-                overflow-y: auto;
+                overflow-y: hidden;
             }
             @media (prefers-color-scheme: dark) {
                 body, .content {
@@ -104,7 +104,7 @@
                     color: #fff;
                 }
                 .sidebar .menu-bottom {
-                    background: #222;
+                    background: #f0f1f3;
                 }
                 .sidebar .menu-options .item:hover,
                 .sidebar .menu-options .item.active {
@@ -122,11 +122,10 @@
                             <img src="img/logo.png" alt="Logo">
                         </div>
                         <div class="menu-options">
-                            <div class="item active">Dashboard</div>
-                            <div class="item">Servers</div>
-                            <div class="item">Storage</div>
-                            <div class="item">Network</div>
-                            <div class="item">Users</div>
+                            <div class="item"><i class="ui server icon"></i></div>
+                            <div class="active item"><i class="ui desktop icon"></i></div>
+                            <div class="item"><i class="ui folder icon"></i></div>
+                            <div class="item"><i class="ui plug icon"></i></div>
                         </div>
                     </div>
                 </div>
@@ -136,7 +135,7 @@
                 </div>
             </nav>
             <main class="content">
-                <!-- Main content goes here -->
+                <iframe src="viewport.html#3f858f1f-93a0-4aa7-a231-5934022e5911" style="width: 100%; height: 100vh; border: none;"></iframe>
             </main>
         </div>
     </body>

+ 10 - 14
dezukvmd/www/js/kvmevt.js

@@ -12,6 +12,7 @@ let protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
 let port = window.location.port ? window.location.port : (protocol === 'wss' ? 443 : 80);
 let hidSocketURL = `${protocol}://${window.location.hostname}:${port}/api/v1/hid/{uuid}/events`;
 let audioSocketURL = `${protocol}://${window.location.hostname}:${port}/api/v1/stream/{uuid}/audio`;
+
 let mouseMoveAbsolute = true; // Set to true for absolute mouse coordinates, false for relative
 let mouseIsOutside = false; //Mouse is outside capture element
 let audioFrontendStarted = false; //Audio frontend has been started
@@ -22,6 +23,7 @@ if (window.location.hash.length > 1){
     kvmDeviceUUID = window.location.hash.substring(1);
     hidSocketURL = hidSocketURL.replace("{uuid}", kvmDeviceUUID);
     audioSocketURL = audioSocketURL.replace("{uuid}", kvmDeviceUUID);
+    massStorageSwitchURL = massStorageSwitchURL.replace("{uuid}", kvmDeviceUUID);
 }
 
 $(document).ready(function() {
@@ -30,7 +32,7 @@ $(document).ready(function() {
     startHidWebSocket();
 });
 
-/* Stream endpoint */
+/* Initiate API endpoint */
 function setStreamingSource(deviceUUID) {
     let videoStreamURL = `/api/v1/stream/${deviceUUID}/video`
     let videoElement = document.getElementById("remoteCapture");
@@ -303,7 +305,7 @@ function startAudioWebSocket(quality="standard") {
     };
 
 
-    const MAX_AUDIO_QUEUE = 8;
+    const MAX_AUDIO_QUEUE = 4;
     let PCM_SAMPLE_RATE;
     if (quality == "high"){
         PCM_SAMPLE_RATE = 48000; // Use 48kHz for high quality
@@ -349,24 +351,13 @@ function startAudioWebSocket(quality="standard") {
         console.error("Audio WebSocket error", e);
     };
 
-    // Periodically resync scheduledTime to avoid drift
-    let lastResync = 0;
-    const RESYNC_INTERVAL = 60; // seconds
-
     function scheduleAudioPlayback() {
         if (!audioContext || audioQueue.length === 0) return;
 
-        // Resync scheduledTime every RESYNC_INTERVAL seconds to avoid drift
+        // Use audioContext.currentTime to schedule buffers back-to-back
         if (scheduledTime < audioContext.currentTime) {
             scheduledTime = audioContext.currentTime;
         }
-        if (scheduledTime - audioContext.currentTime > RESYNC_INTERVAL) {
-            scheduledTime = audioContext.currentTime;
-        }
-        if (audioContext.currentTime - lastResync > RESYNC_INTERVAL) {
-            scheduledTime = audioContext.currentTime;
-            lastResync = audioContext.currentTime;
-        }
 
         while (audioQueue.length > 0) {
             let floatBuf = audioQueue.shift();
@@ -378,6 +369,11 @@ function startAudioWebSocket(quality="standard") {
                     channelData[i] = floatBuf[i * 2 + ch];
                 }
             }
+
+            if (scheduledTime - audioContext.currentTime > 0.2) {
+                console.warn("Audio buffer too far ahead, discarding frame");
+                continue;
+            }
             let source = audioContext.createBufferSource();
             source.buffer = buffer;
             source.connect(audioContext.destination);

+ 48 - 50
dezukvmd/www/js/viewport.js

@@ -1,32 +1,42 @@
 /*
     viewport.js
 */
+let massStorageSwitchURL = "/api/v1/mass_storage/switch"; //side accept kvm or remote
 
-function cjax(object){
-    let csrf_token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
-    $.ajax({
-        url: object.url,
-        type: object.type || 'POST',
-        data: object.data || {},
-        headers: {
-            'X-CSRF-Token': csrf_token
-        },
-        success: object.success,
-        error: object.error
-    });
-}
-
-// Add cjax as a jQuery method
-$.cjax = cjax;
+// CSRF-protected AJAX function
+$.cjax = function(payload){
+    let requireTokenMethod = ["POST", "PUT", "DELETE"];
+    if (requireTokenMethod.includes(payload.method) || requireTokenMethod.includes(payload.type)){
+        //csrf token is required
+        let csrfToken = document.getElementsByTagName("meta")["dezukvm.csrf.token"].getAttribute("content");
+        payload.headers = {
+            "dezukvm_csrf_token": csrfToken,
+        }
+    }
 
+    $.ajax(payload);
+}
 
+$(document).ready(function() {
+    // Check if the user has opted out of seeing the audio tips
+    if (localStorage.getItem('dontShowAudioTipsAgain') === 'true') {
+        $('#audioTips').remove();
+    }
+});
 
+/* Mass Storage Switch */
 function switchMassStorageToRemote(){
     $.cjax({
-        url: '/aux/switchusbremote',
-        type: 'GET',
+        url: massStorageSwitchURL,
+        type: 'POST',
+        data: {
+            side: 'remote',
+            uuid: kvmDeviceUUID
+        },
         success: function(response) {
-            //alert('Mass Storage switched to Remote successfully.');
+            if (response.error) {
+                alert('Error switching Mass Storage to Remote: ' + response.error);
+            }
         },
         error: function(xhr, status, error) {
             alert('Error switching Mass Storage to Remote: ' + error);
@@ -36,10 +46,16 @@ function switchMassStorageToRemote(){
 
 function switchMassStorageToKvm(){
     $.cjax({
-        url: '/aux/switchusbkvm',
-        type: 'GET',
+        url: massStorageSwitchURL,
+        type: 'POST',
+        data: {
+            side: 'kvm',
+            uuid: kvmDeviceUUID
+        },
         success: function(response) {
-            //alert('Mass Storage switched to KVM successfully.');
+            if (response.error) {
+                alert('Error switching Mass Storage to KVM: ' + response.error);
+            }
         },
         error: function(xhr, status, error) {
             alert('Error switching Mass Storage to KVM: ' + error);
@@ -47,6 +63,16 @@ function switchMassStorageToKvm(){
     });
 }
 
+
+/*
+    UI elements and events
+*/
+
+function handleDontShowAudioTipsAgain(){
+    localStorage.setItem('dontShowAudioTipsAgain', 'true');
+    $('#audioTips').remove();
+}
+
 function toggleFullScreen(){
     let elem = document.documentElement;
     if (!document.fullscreenElement) {
@@ -71,31 +97,3 @@ function toggleFullScreen(){
         }
     }
 }
-
-
-function measureMJPEGfps(imgId, callback, intervalMs = 1000) {
-    let frameCount = 0;
-    let lastSrc = '';
-    const img = document.getElementById(imgId);
-
-    if (!img) {
-        console.error('Image element not found');
-        return;
-    }
-
-    // Listen for src changes (for MJPEG, the src stays the same, but the image data updates)
-    img.addEventListener('load', () => {
-        frameCount++;
-    });
-
-    // Periodically report FPS
-    setInterval(() => {
-        callback(frameCount);
-        frameCount = 0;
-    }, intervalMs);
-}
-
-// Example usage:
-measureMJPEGfps('remoteCapture', function(fps) {
-    document.title = `${fps} fps| DezuKVM`;
-});

+ 7 - 0
dezukvmd/www/viewport.css

@@ -31,4 +31,11 @@ body {
     display: flex;
     gap: 10px;
     z-index: 1000;
+}
+
+#audioTips{
+    position: fixed;
+    bottom: 2em;
+    right: 2em;
+    z-index: 1000;
 }

+ 12 - 1
dezukvmd/www/viewport.html

@@ -6,8 +6,10 @@
     <meta name="description" content="dezuKVM Management Interface">
     <meta name="author" content="imuslab">
     <meta name="dezukvm.csrf.token" content="{{.csrfToken}}">
-    <title>Connecting | DezuKVM</title>
+    <title>Streaming | DezuKVM</title>
     <script src="js/jquery-3.7.1.min.js"></script>
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.4/semantic.min.css" integrity="sha512-ySrYzxj+EI1e9xj/kRYqeDL5l1wW0IWY8pzHNTIZ+vc1D3Z14UDNPbwup4yOUmlRemYjgUXsUZ/xvCQU2ThEAw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.4/semantic.min.js" integrity="sha512-Y/wIVu+S+XJsDL7I+nL50kAVFLMqSdvuLqF2vMoRqiMkmvcqFjEpEgeu6Rx8tpZXKp77J8OUpMKy0m3jLYhbbw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
     <link rel="stylesheet" href="viewport.css">
     <link rel="icon" type="image/png" href="/favicon.png">
 </head>
@@ -19,6 +21,15 @@
         <button id="btnFullScreen" onclick="switchMassStorageToKvm()">Switch Storage to KVM</button>
         <button id="btnFullScreen" onclick="switchMassStorageToRemote()">Switch Storage to Remote</button>
     </div>
+    <div id="audioTips" class="ui left aligned message">
+        <div class="content">
+            <div class="header">
+                <i class="volume up icon"></i> Start Audio Stream
+            </div>
+            <p>Click the remote desktop to start audio streaming</p>
+            <button onclick="handleDontShowAudioTipsAgain();" class="ui right floated mini basic button">Don't show again</button>
+        </div>
+    </div>
     <script src="js/viewport.js"></script>
     <script src="js/kvmevt.js"></script>
 </body>