mirror of https://github.com/jetkvm/kvm.git
[WIP] Cleanup: PR Cleanup
This commit is contained in:
parent
274854b198
commit
cd87aa499c
|
@ -2,10 +2,8 @@ package kvm
|
|||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jetkvm/kvm/internal/audio"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/rs/zerolog"
|
||||
|
@ -30,6 +28,16 @@ func ensureAudioControlService() *audio.AudioControlService {
|
|||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Set up RPC callback functions for the audio package
|
||||
audio.SetRPCCallbacks(
|
||||
func() *audio.AudioControlService { return audioControlService },
|
||||
func() audio.AudioConfig { return audioControlService.GetCurrentAudioQuality() },
|
||||
func(quality audio.AudioQuality) error {
|
||||
audioControlService.SetAudioQuality(quality)
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
return audioControlService
|
||||
}
|
||||
|
@ -129,94 +137,6 @@ func GetCurrentAudioQuality() audio.AudioConfig {
|
|||
return audioControlService.GetCurrentAudioQuality()
|
||||
}
|
||||
|
||||
// handleAudioMute handles POST /audio/mute requests
|
||||
func handleAudioMute(c *gin.Context) {
|
||||
type muteReq struct {
|
||||
Muted bool `json:"muted"`
|
||||
}
|
||||
var req muteReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": "invalid request"})
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if req.Muted {
|
||||
err = MuteAudioOutput()
|
||||
} else {
|
||||
err = UnmuteAudioOutput()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": "audio mute state updated",
|
||||
"muted": req.Muted,
|
||||
})
|
||||
}
|
||||
|
||||
// handleMicrophoneStart handles POST /microphone/start requests
|
||||
func handleMicrophoneStart(c *gin.Context) {
|
||||
err := StartMicrophone()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// handleMicrophoneStop handles POST /microphone/stop requests
|
||||
func handleMicrophoneStop(c *gin.Context) {
|
||||
err := StopMicrophone()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// handleMicrophoneMute handles POST /microphone/mute requests
|
||||
func handleMicrophoneMute(c *gin.Context) {
|
||||
var req struct {
|
||||
Muted bool `json:"muted"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if req.Muted {
|
||||
err = StopMicrophone()
|
||||
} else {
|
||||
err = StartMicrophone()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// handleMicrophoneReset handles POST /microphone/reset requests
|
||||
func handleMicrophoneReset(c *gin.Context) {
|
||||
err := ResetMicrophone()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// handleSubscribeAudioEvents handles WebSocket audio event subscription
|
||||
func handleSubscribeAudioEvents(connectionID string, wsCon *websocket.Conn, runCtx context.Context, l *zerolog.Logger) {
|
||||
ensureAudioControlService()
|
||||
|
@ -228,57 +148,3 @@ func handleUnsubscribeAudioEvents(connectionID string, l *zerolog.Logger) {
|
|||
ensureAudioControlService()
|
||||
audioControlService.UnsubscribeFromAudioEvents(connectionID, l)
|
||||
}
|
||||
|
||||
// handleAudioStatus handles GET requests for audio status
|
||||
func handleAudioStatus(c *gin.Context) {
|
||||
ensureAudioControlService()
|
||||
|
||||
status := audioControlService.GetAudioStatus()
|
||||
c.JSON(200, status)
|
||||
}
|
||||
|
||||
// handleAudioQuality handles GET requests for audio quality presets
|
||||
func handleAudioQuality(c *gin.Context) {
|
||||
presets := GetAudioQualityPresets()
|
||||
current := GetCurrentAudioQuality()
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"presets": presets,
|
||||
"current": current,
|
||||
})
|
||||
}
|
||||
|
||||
// handleSetAudioQuality handles POST requests to set audio quality
|
||||
func handleSetAudioQuality(c *gin.Context) {
|
||||
var req struct {
|
||||
Quality int `json:"quality"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if audio output is active before attempting quality change
|
||||
// This prevents race conditions where quality changes are attempted before initialization
|
||||
if !IsAudioOutputActive() {
|
||||
c.JSON(503, gin.H{"error": "audio output not active - please wait for initialization to complete"})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert int to AudioQuality type
|
||||
quality := audio.AudioQuality(req.Quality)
|
||||
|
||||
// Set the audio quality using global convenience function
|
||||
if err := SetAudioQuality(quality); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Return the updated configuration
|
||||
current := GetCurrentAudioQuality()
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"config": current,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
package audio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// RPC wrapper functions for audio control
|
||||
// These functions bridge the RPC layer to the AudioControlService
|
||||
|
||||
// These variables will be set by the main package to provide access to the global service
|
||||
var (
|
||||
getAudioControlServiceFunc func() *AudioControlService
|
||||
getAudioQualityFunc func() AudioConfig
|
||||
setAudioQualityFunc func(AudioQuality) error
|
||||
)
|
||||
|
||||
// SetRPCCallbacks sets the callback functions for RPC operations
|
||||
func SetRPCCallbacks(
|
||||
getService func() *AudioControlService,
|
||||
getQuality func() AudioConfig,
|
||||
setQuality func(AudioQuality) error,
|
||||
) {
|
||||
getAudioControlServiceFunc = getService
|
||||
getAudioQualityFunc = getQuality
|
||||
setAudioQualityFunc = setQuality
|
||||
}
|
||||
|
||||
// RPCAudioMute handles audio mute/unmute RPC requests
|
||||
func RPCAudioMute(muted bool) error {
|
||||
if getAudioControlServiceFunc == nil {
|
||||
return fmt.Errorf("audio control service not available")
|
||||
}
|
||||
service := getAudioControlServiceFunc()
|
||||
if service == nil {
|
||||
return fmt.Errorf("audio control service not initialized")
|
||||
}
|
||||
return service.MuteAudio(muted)
|
||||
}
|
||||
|
||||
// RPCAudioQuality handles audio quality change RPC requests
|
||||
func RPCAudioQuality(quality int) (map[string]any, error) {
|
||||
if getAudioQualityFunc == nil || setAudioQualityFunc == nil {
|
||||
return nil, fmt.Errorf("audio quality functions not available")
|
||||
}
|
||||
|
||||
// Convert int to AudioQuality type
|
||||
audioQuality := AudioQuality(quality)
|
||||
|
||||
// Get current audio quality configuration
|
||||
currentConfig := getAudioQualityFunc()
|
||||
|
||||
// Set new quality if different
|
||||
if currentConfig.Quality != audioQuality {
|
||||
err := setAudioQualityFunc(audioQuality)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set audio quality: %w", err)
|
||||
}
|
||||
// Get updated config after setting
|
||||
newConfig := getAudioQualityFunc()
|
||||
return map[string]any{"config": newConfig}, nil
|
||||
}
|
||||
|
||||
// Return current config if no change needed
|
||||
return map[string]any{"config": currentConfig}, nil
|
||||
}
|
||||
|
||||
// RPCMicrophoneStart handles microphone start RPC requests
|
||||
func RPCMicrophoneStart() error {
|
||||
if getAudioControlServiceFunc == nil {
|
||||
return fmt.Errorf("audio control service not available")
|
||||
}
|
||||
service := getAudioControlServiceFunc()
|
||||
if service == nil {
|
||||
return fmt.Errorf("audio control service not initialized")
|
||||
}
|
||||
return service.StartMicrophone()
|
||||
}
|
||||
|
||||
// RPCMicrophoneStop handles microphone stop RPC requests
|
||||
func RPCMicrophoneStop() error {
|
||||
if getAudioControlServiceFunc == nil {
|
||||
return fmt.Errorf("audio control service not available")
|
||||
}
|
||||
service := getAudioControlServiceFunc()
|
||||
if service == nil {
|
||||
return fmt.Errorf("audio control service not initialized")
|
||||
}
|
||||
return service.StopMicrophone()
|
||||
}
|
||||
|
||||
// RPCAudioStatus handles audio status RPC requests (read-only)
|
||||
func RPCAudioStatus() (map[string]interface{}, error) {
|
||||
if getAudioControlServiceFunc == nil {
|
||||
return nil, fmt.Errorf("audio control service not available")
|
||||
}
|
||||
service := getAudioControlServiceFunc()
|
||||
if service == nil {
|
||||
return nil, fmt.Errorf("audio control service not initialized")
|
||||
}
|
||||
return service.GetAudioStatus(), nil
|
||||
}
|
||||
|
||||
// RPCAudioQualityPresets handles audio quality presets RPC requests (read-only)
|
||||
func RPCAudioQualityPresets() (map[string]any, error) {
|
||||
if getAudioControlServiceFunc == nil || getAudioQualityFunc == nil {
|
||||
return nil, fmt.Errorf("audio control service not available")
|
||||
}
|
||||
service := getAudioControlServiceFunc()
|
||||
if service == nil {
|
||||
return nil, fmt.Errorf("audio control service not initialized")
|
||||
}
|
||||
|
||||
presets := service.GetAudioQualityPresets()
|
||||
current := getAudioQualityFunc()
|
||||
|
||||
return map[string]any{
|
||||
"presets": presets,
|
||||
"current": current,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RPCMicrophoneStatus handles microphone status RPC requests (read-only)
|
||||
func RPCMicrophoneStatus() (map[string]interface{}, error) {
|
||||
if getAudioControlServiceFunc == nil {
|
||||
return nil, fmt.Errorf("audio control service not available")
|
||||
}
|
||||
service := getAudioControlServiceFunc()
|
||||
if service == nil {
|
||||
return nil, fmt.Errorf("audio control service not initialized")
|
||||
}
|
||||
return service.GetMicrophoneStatus(), nil
|
||||
}
|
||||
|
||||
// RPCMicrophoneReset handles microphone reset RPC requests
|
||||
func RPCMicrophoneReset() error {
|
||||
if getAudioControlServiceFunc == nil {
|
||||
return fmt.Errorf("audio control service not available")
|
||||
}
|
||||
service := getAudioControlServiceFunc()
|
||||
if service == nil {
|
||||
return fmt.Errorf("audio control service not initialized")
|
||||
}
|
||||
return service.ResetMicrophone()
|
||||
}
|
||||
|
||||
// RPCMicrophoneMute handles microphone mute RPC requests
|
||||
func RPCMicrophoneMute(muted bool) error {
|
||||
if getAudioControlServiceFunc == nil {
|
||||
return fmt.Errorf("audio control service not available")
|
||||
}
|
||||
service := getAudioControlServiceFunc()
|
||||
if service == nil {
|
||||
return fmt.Errorf("audio control service not initialized")
|
||||
}
|
||||
return service.MuteMicrophone(muted)
|
||||
}
|
46
jsonrpc.go
46
jsonrpc.go
|
@ -1339,6 +1339,43 @@ func rpcDoExecuteKeyboardMacro(ctx context.Context, macro []hidrpc.KeyboardMacro
|
|||
return nil
|
||||
}
|
||||
|
||||
// Audio control RPC handlers - delegated to audio package
|
||||
func rpcAudioMute(muted bool) error {
|
||||
return audio.RPCAudioMute(muted)
|
||||
}
|
||||
|
||||
func rpcAudioQuality(quality int) (map[string]any, error) {
|
||||
return audio.RPCAudioQuality(quality)
|
||||
}
|
||||
|
||||
func rpcMicrophoneStart() error {
|
||||
return audio.RPCMicrophoneStart()
|
||||
}
|
||||
|
||||
func rpcMicrophoneStop() error {
|
||||
return audio.RPCMicrophoneStop()
|
||||
}
|
||||
|
||||
func rpcAudioStatus() (map[string]interface{}, error) {
|
||||
return audio.RPCAudioStatus()
|
||||
}
|
||||
|
||||
func rpcAudioQualityPresets() (map[string]any, error) {
|
||||
return audio.RPCAudioQualityPresets()
|
||||
}
|
||||
|
||||
func rpcMicrophoneStatus() (map[string]interface{}, error) {
|
||||
return audio.RPCMicrophoneStatus()
|
||||
}
|
||||
|
||||
func rpcMicrophoneReset() error {
|
||||
return audio.RPCMicrophoneReset()
|
||||
}
|
||||
|
||||
func rpcMicrophoneMute(muted bool) error {
|
||||
return audio.RPCMicrophoneMute(muted)
|
||||
}
|
||||
|
||||
var rpcHandlers = map[string]RPCHandler{
|
||||
"ping": {Func: rpcPing},
|
||||
"reboot": {Func: rpcReboot, Params: []string{"force"}},
|
||||
|
@ -1388,6 +1425,15 @@ var rpcHandlers = map[string]RPCHandler{
|
|||
"isUpdatePending": {Func: rpcIsUpdatePending},
|
||||
"getUsbEmulationState": {Func: rpcGetUsbEmulationState},
|
||||
"setUsbEmulationState": {Func: rpcSetUsbEmulationState, Params: []string{"enabled"}},
|
||||
"audioMute": {Func: rpcAudioMute, Params: []string{"muted"}},
|
||||
"audioQuality": {Func: rpcAudioQuality, Params: []string{"quality"}},
|
||||
"audioStatus": {Func: rpcAudioStatus},
|
||||
"audioQualityPresets": {Func: rpcAudioQualityPresets},
|
||||
"microphoneStart": {Func: rpcMicrophoneStart},
|
||||
"microphoneStop": {Func: rpcMicrophoneStop},
|
||||
"microphoneStatus": {Func: rpcMicrophoneStatus},
|
||||
"microphoneReset": {Func: rpcMicrophoneReset},
|
||||
"microphoneMute": {Func: rpcMicrophoneMute, Params: []string{"muted"}},
|
||||
"getUsbConfig": {Func: rpcGetUsbConfig},
|
||||
"setUsbConfig": {Func: rpcSetUsbConfig, Params: []string{"usbConfig"}},
|
||||
"checkMountUrl": {Func: rpcCheckMountUrl, Params: []string{"url"}},
|
||||
|
|
|
@ -6,13 +6,13 @@ import { Fragment, useCallback, useRef } from "react";
|
|||
import { CommandLineIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
import { Button } from "@components/Button";
|
||||
import Container from "@components/Container";
|
||||
import {
|
||||
useHidStore,
|
||||
useMountMediaStore,
|
||||
useSettingsStore,
|
||||
useUiStore,
|
||||
} from "@/hooks/stores";
|
||||
import Container from "@components/Container";
|
||||
import { cx } from "@/cva.config";
|
||||
import PasteModal from "@/components/popovers/PasteModal";
|
||||
import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index";
|
||||
|
|
|
@ -11,6 +11,8 @@ import { cva } from "@/cva.config";
|
|||
|
||||
import Card from "./Card";
|
||||
|
||||
|
||||
|
||||
export interface ComboboxOption {
|
||||
value: string;
|
||||
label: string;
|
||||
|
|
|
@ -4,6 +4,8 @@ import { GridCard } from "@/components/Card";
|
|||
|
||||
import { cx } from "../cva.config";
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
IconElm?: React.FC<{ className: string | undefined }>;
|
||||
headline: string;
|
||||
|
|
|
@ -4,20 +4,22 @@ import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/1
|
|||
import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
|
||||
import { LuMonitorSmartphone } from "react-icons/lu";
|
||||
|
||||
import USBStateStatus from "@components/USBStateStatus";
|
||||
import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard";
|
||||
import Container from "@/components/Container";
|
||||
import Card from "@/components/Card";
|
||||
import { useHidStore, useRTCStore, useUserStore } from "@/hooks/stores";
|
||||
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||
import USBStateStatus from "@components/USBStateStatus";
|
||||
import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard";
|
||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||
|
||||
import api from "../api";
|
||||
import { isOnDevice } from "../main";
|
||||
import api from "../api";
|
||||
|
||||
import { LinkButton } from "./Button";
|
||||
|
||||
|
||||
|
||||
interface NavbarProps {
|
||||
isLoggedIn: boolean;
|
||||
primaryLinks?: { title: string; to: string }[];
|
||||
|
|
|
@ -7,6 +7,7 @@ import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
|||
import { InputFieldWithLabel } from "./InputField";
|
||||
import { SelectMenuBasic } from "./SelectMenuBasic";
|
||||
|
||||
|
||||
export interface JigglerConfig {
|
||||
inactivity_limit_seconds: number;
|
||||
jitter_percentage: number;
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import React, { JSX } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
|
||||
import FieldLabel from "@/components/FieldLabel";
|
||||
import { cva } from "@/cva.config";
|
||||
|
||||
import Card from "./Card";
|
||||
|
||||
|
||||
|
||||
type SelectMenuProps = Pick<
|
||||
JSX.IntrinsicElements["select"],
|
||||
"disabled" | "onChange" | "name" | "value"
|
||||
|
|
|
@ -8,11 +8,13 @@ import { WebglAddon } from "@xterm/addon-webgl";
|
|||
import { Unicode11Addon } from "@xterm/addon-unicode11";
|
||||
import { ClipboardAddon } from "@xterm/addon-clipboard";
|
||||
|
||||
|
||||
import { cx } from "@/cva.config";
|
||||
import { AvailableTerminalTypes, useUiStore } from "@/hooks/stores";
|
||||
|
||||
import { Button } from "./Button";
|
||||
|
||||
|
||||
const isWebGl2Supported = !!document.createElement("canvas").getContext("webgl2");
|
||||
|
||||
// Terminal theme configuration
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from "react";
|
||||
|
||||
import { cx } from "@/cva.config";
|
||||
import KeyboardAndMouseConnectedIcon from "@/assets/keyboard-and-mouse-connected.png";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import StatusCard from "@components/StatusCards";
|
||||
import { cx } from "@/cva.config";
|
||||
import KeyboardAndMouseConnectedIcon from "@/assets/keyboard-and-mouse-connected.png";
|
||||
import { USBStates } from "@/hooks/stores";
|
||||
|
||||
type StatusProps = Record<
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
import { cx } from "@/cva.config";
|
||||
|
||||
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
||||
|
@ -6,6 +7,7 @@ import { Button } from "./Button";
|
|||
import { GridCard } from "./Card";
|
||||
import LoadingSpinner from "./LoadingSpinner";
|
||||
|
||||
|
||||
export default function UpdateInProgressStatusCard() {
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
||||
|
|
|
@ -4,12 +4,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||
import Keyboard from "react-simple-keyboard";
|
||||
import { LuKeyboard } from "react-icons/lu";
|
||||
|
||||
import Card from "@components/Card";
|
||||
// eslint-disable-next-line import/order
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
|
||||
import "react-simple-keyboard/build/css/index.css";
|
||||
|
||||
import Card from "@components/Card";
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import DetachIconRaw from "@/assets/detach-icon.svg";
|
||||
import { cx } from "@/cva.config";
|
||||
import { useHidStore, useUiStore } from "@/hooks/stores";
|
||||
|
|
|
@ -3,8 +3,8 @@ import { useResizeObserver } from "usehooks-ts";
|
|||
|
||||
import VirtualKeyboard from "@components/VirtualKeyboard";
|
||||
import Actionbar from "@components/ActionBar";
|
||||
import MacroBar from "@/components/MacroBar";
|
||||
import InfoBar from "@components/InfoBar";
|
||||
import MacroBar from "@/components/MacroBar";
|
||||
import notifications from "@/notifications";
|
||||
import useKeyboard from "@/hooks/useKeyboard";
|
||||
import { cx } from "@/cva.config";
|
||||
|
@ -23,6 +23,7 @@ import {
|
|||
PointerLockBar,
|
||||
} from "./VideoOverlay";
|
||||
|
||||
|
||||
// Type for microphone error
|
||||
interface MicrophoneError {
|
||||
type: 'permission' | 'device' | 'network' | 'unknown';
|
||||
|
|
|
@ -9,6 +9,7 @@ import LoadingSpinner from "@/components/LoadingSpinner";
|
|||
|
||||
import { JsonRpcResponse, useJsonRpc } from "../../hooks/useJsonRpc";
|
||||
|
||||
|
||||
const LONG_PRESS_DURATION = 3000; // 3 seconds for long press
|
||||
|
||||
interface ATXState {
|
||||
|
|
|
@ -4,11 +4,11 @@ import { useCallback, useEffect, useState } from "react";
|
|||
import { Button } from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import FieldLabel from "@components/FieldLabel";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import {SelectMenuBasic} from "@components/SelectMenuBasic";
|
||||
import notifications from "@/notifications";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
|
||||
interface DCPowerState {
|
||||
isOn: boolean;
|
||||
|
|
|
@ -4,10 +4,10 @@ import { useEffect, useState } from "react";
|
|||
import { Button } from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import { useUiStore } from "@/hooks/stores";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
|
||||
interface SerialSettings {
|
||||
baudRate: string;
|
||||
|
|
|
@ -5,7 +5,8 @@ import { Button } from "@components/Button";
|
|||
import { cx } from "@/cva.config";
|
||||
import { useAudioDevices } from "@/hooks/useAudioDevices";
|
||||
import { useAudioEvents } from "@/hooks/useAudioEvents";
|
||||
import api from "@/api";
|
||||
import { useJsonRpc, JsonRpcResponse } from "@/hooks/useJsonRpc";
|
||||
import { useRTCStore } from "@/hooks/stores";
|
||||
import notifications from "@/notifications";
|
||||
import audioQualityService from "@/services/audioQualityService";
|
||||
|
||||
|
@ -64,6 +65,17 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
|
|||
isConnected: wsConnected
|
||||
} = useAudioEvents();
|
||||
|
||||
// RPC for device communication (works both locally and via cloud)
|
||||
const { rpcDataChannel } = useRTCStore();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
// Initialize audio quality service with RPC for cloud compatibility
|
||||
useEffect(() => {
|
||||
if (send) {
|
||||
audioQualityService.setRpcSend(send);
|
||||
}
|
||||
}, [send]);
|
||||
|
||||
// WebSocket-only implementation - no fallback polling
|
||||
|
||||
// Microphone state from props (keeping hook for legacy device operations)
|
||||
|
@ -146,21 +158,22 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
|
|||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
if (isMuted) {
|
||||
// Unmute: Start audio output process and notify backend
|
||||
const resp = await api.POST("/audio/mute", { muted: false });
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Failed to unmute audio: ${resp.status}`);
|
||||
}
|
||||
// WebSocket will handle the state update automatically
|
||||
} else {
|
||||
// Mute: Stop audio output process and notify backend
|
||||
const resp = await api.POST("/audio/mute", { muted: true });
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Failed to mute audio: ${resp.status}`);
|
||||
}
|
||||
// WebSocket will handle the state update automatically
|
||||
// Use RPC for device communication - works for both local and cloud
|
||||
if (rpcDataChannel?.readyState !== "open") {
|
||||
throw new Error("Device connection not available");
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
send("audioMute", { muted: !isMuted }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
reject(new Error(resp.error.message));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// WebSocket will handle the state update automatically
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to toggle audio mute";
|
||||
notifications.error(errorMessage);
|
||||
|
@ -172,13 +185,27 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
|
|||
const handleQualityChange = async (quality: number) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const resp = await api.POST("/audio/quality", { quality });
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
setCurrentConfig(data.config);
|
||||
// Use RPC for device communication - works for both local and cloud
|
||||
if (rpcDataChannel?.readyState !== "open") {
|
||||
throw new Error("Device connection not available");
|
||||
}
|
||||
} catch {
|
||||
// Failed to change audio quality
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
send("audioQuality", { quality }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
reject(new Error(resp.error.message));
|
||||
} else {
|
||||
// Update local state with response
|
||||
if ("result" in resp && resp.result && typeof resp.result === 'object' && 'config' in resp.result) {
|
||||
setCurrentConfig(resp.result.config as AudioConfig);
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Failed to change audio quality";
|
||||
notifications.error(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
@ -196,17 +223,44 @@ export default function AudioControlPopover({ microphone }: AudioControlPopoverP
|
|||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Use RPC for device communication - works for both local and cloud
|
||||
if (rpcDataChannel?.readyState !== "open") {
|
||||
throw new Error("Device connection not available");
|
||||
}
|
||||
|
||||
if (isMicrophoneActiveFromHook) {
|
||||
// Disable: Stop microphone subprocess AND remove WebRTC tracks
|
||||
// Disable: Stop microphone subprocess via RPC AND remove WebRTC tracks locally
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
send("microphoneStop", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
reject(new Error(resp.error.message));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Also stop local WebRTC stream
|
||||
const result = await stopMicrophone();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error?.message || "Failed to stop microphone");
|
||||
console.warn("Local microphone stop failed:", result.error?.message);
|
||||
}
|
||||
} else {
|
||||
// Enable: Start microphone subprocess AND add WebRTC tracks
|
||||
// Enable: Start microphone subprocess via RPC AND add WebRTC tracks locally
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
send("microphoneStart", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
reject(new Error(resp.error.message));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Also start local WebRTC stream
|
||||
const result = await startMicrophone();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error?.message || "Failed to start microphone");
|
||||
throw new Error(result.error?.message || "Failed to start local microphone");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { LuPower, LuTerminal, LuPlugZap } from "react-icons/lu";
|
||||
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import Card, { GridCard } from "@components/Card";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { ATXPowerControl } from "@components/extensions/ATXPowerControl";
|
||||
import { DCPowerControl } from "@components/extensions/DCPowerControl";
|
||||
import { SerialConsole } from "@components/extensions/SerialConsole";
|
||||
import { Button } from "@components/Button";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
interface Extension {
|
||||
|
|
|
@ -10,9 +10,9 @@ import { useLocation } from "react-router";
|
|||
|
||||
import { Button } from "@components/Button";
|
||||
import Card, { GridCard } from "@components/Card";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { formatters } from "@/utils";
|
||||
import { RemoteVirtualMediaState, useMountMediaStore } from "@/hooks/stores";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import notifications from "@/notifications";
|
||||
|
|
|
@ -3,17 +3,17 @@ import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { LuCornerDownLeft } from "react-icons/lu";
|
||||
|
||||
import { Button } from "@components/Button";
|
||||
import { GridCard } from "@components/Card";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
import { cx } from "@/cva.config";
|
||||
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import useKeyboard, { type MacroStep } from "@/hooks/useKeyboard";
|
||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||
import notifications from "@/notifications";
|
||||
import { Button } from "@components/Button";
|
||||
import { GridCard } from "@components/Card";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
|
||||
// uint32 max value / 4
|
||||
const pasteMaxLength = 1073741824;
|
||||
|
|
|
@ -11,6 +11,8 @@ import EmptyStateCard from "./EmptyStateCard";
|
|||
import DeviceList, { StoredDevice } from "./DeviceList";
|
||||
import AddDeviceForm from "./AddDeviceForm";
|
||||
|
||||
|
||||
|
||||
export default function WakeOnLanModal() {
|
||||
const [storedDevices, setStoredDevices] = useState<StoredDevice[]>([]);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { useInterval } from "usehooks-ts";
|
||||
|
||||
|
||||
import SidebarHeader from "@/components/SidebarHeader";
|
||||
import { useRTCStore, useUiStore } from "@/hooks/stores";
|
||||
import { someIterable } from "@/utils";
|
||||
|
||||
import { createChartArray, Metric } from "../Metric";
|
||||
import { SettingsSectionHeader } from "../SettingsSectionHeader";
|
||||
import { createChartArray, Metric } from "../Metric";
|
||||
|
||||
|
||||
export default function ConnectionStatsSidebar() {
|
||||
const { sidebarView, setSidebarView } = useUiStore();
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
|
||||
import { devWarn } from '../utils/debug';
|
||||
|
||||
|
||||
|
||||
// Define the JsonRpc types for better type checking
|
||||
interface JsonRpcResponse {
|
||||
jsonrpc: string;
|
||||
|
|
|
@ -4,6 +4,9 @@ import useWebSocket, { ReadyState } from 'react-use-websocket';
|
|||
import { devError, devWarn } from '../utils/debug';
|
||||
import { NETWORK_CONFIG } from '../config/constants';
|
||||
|
||||
import { JsonRpcResponse, useJsonRpc } from './useJsonRpc';
|
||||
import { useRTCStore } from './stores';
|
||||
|
||||
// Audio event types matching the backend
|
||||
export type AudioEventType =
|
||||
| 'audio-mute-changed'
|
||||
|
@ -63,18 +66,34 @@ export function useAudioEvents(onAudioDeviceChanged?: (data: AudioDeviceChangedD
|
|||
const [audioMuted, setAudioMuted] = useState<boolean | null>(null);
|
||||
const [microphoneState, setMicrophoneState] = useState<MicrophoneStateData | null>(null);
|
||||
|
||||
// Fetch initial audio status
|
||||
// Get RTC store and JSON RPC functionality
|
||||
const { rpcDataChannel } = useRTCStore();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
// Fetch initial audio status using RPC for cloud compatibility
|
||||
const fetchInitialAudioStatus = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch('/audio/status');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAudioMuted(data.muted);
|
||||
}
|
||||
} catch (error) {
|
||||
devError('Failed to fetch initial audio status:', error);
|
||||
// Early return if RPC data channel is not open
|
||||
if (rpcDataChannel?.readyState !== "open") {
|
||||
devWarn('RPC connection not available for initial audio status, skipping');
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve) => {
|
||||
send("audioStatus", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
devError('RPC audioStatus failed:', resp.error);
|
||||
} else if ("result" in resp) {
|
||||
const data = resp.result as { muted: boolean };
|
||||
setAudioMuted(data.muted);
|
||||
}
|
||||
resolve(); // Continue regardless of result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
devError('Failed to fetch initial audio status via RPC:', error);
|
||||
}
|
||||
}, [rpcDataChannel?.readyState, send]);
|
||||
|
||||
// Local subscription state
|
||||
const [isLocallySubscribed, setIsLocallySubscribed] = useState(false);
|
||||
|
@ -253,10 +272,13 @@ export function useAudioEvents(onAudioDeviceChanged?: (data: AudioDeviceChangedD
|
|||
}
|
||||
}, [readyState]);
|
||||
|
||||
// Fetch initial audio status on component mount
|
||||
// Fetch initial audio status on component mount - but only when RPC is ready
|
||||
useEffect(() => {
|
||||
fetchInitialAudioStatus();
|
||||
}, [fetchInitialAudioStatus]);
|
||||
// Only fetch when RPC data channel is open and ready
|
||||
if (rpcDataChannel?.readyState === "open") {
|
||||
fetchInitialAudioStatus();
|
||||
}
|
||||
}, [fetchInitialAudioStatus, rpcDataChannel?.readyState]);
|
||||
|
||||
// Cleanup on component unmount
|
||||
useEffect(() => {
|
||||
|
|
|
@ -17,6 +17,8 @@ import {
|
|||
unmarshalHidRpcMessage,
|
||||
} from "./hidRpc";
|
||||
|
||||
|
||||
|
||||
const KEEPALIVE_MESSAGE = new KeypressKeepAliveMessage();
|
||||
|
||||
interface sendMessageParams {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useRTCStore, useSettingsStore } from "@/hooks/stores";
|
||||
import api from "@/api";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { devLog, devInfo, devWarn, devError, devOnly } from "@/utils/debug";
|
||||
import { AUDIO_CONFIG } from "@/config/constants";
|
||||
|
||||
|
@ -21,9 +21,29 @@ export function useMicrophone() {
|
|||
setMicrophoneActive,
|
||||
isMicrophoneMuted,
|
||||
setMicrophoneMuted,
|
||||
rpcDataChannel,
|
||||
} = useRTCStore();
|
||||
|
||||
const { microphoneWasEnabled, setMicrophoneWasEnabled } = useSettingsStore();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
// RPC helper functions to replace HTTP API calls
|
||||
const rpcMicrophoneStart = useCallback((): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (rpcDataChannel?.readyState !== "open") {
|
||||
reject(new Error("Device connection not available"));
|
||||
return;
|
||||
}
|
||||
|
||||
send("microphoneStart", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
reject(new Error(resp.error.message));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [rpcDataChannel?.readyState, send]);
|
||||
|
||||
const microphoneStreamRef = useRef<MediaStream | null>(null);
|
||||
|
||||
|
@ -60,8 +80,6 @@ export function useMicrophone() {
|
|||
|
||||
// Cleanup function to stop microphone stream
|
||||
const stopMicrophoneStream = useCallback(async () => {
|
||||
// Cleaning up microphone stream
|
||||
|
||||
if (microphoneStreamRef.current) {
|
||||
microphoneStreamRef.current.getTracks().forEach((track: MediaStreamTrack) => {
|
||||
track.stop();
|
||||
|
@ -106,37 +124,52 @@ export function useMicrophone() {
|
|||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.GET("/microphone/status", {});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const backendRunning = data.running;
|
||||
|
||||
// Only sync if there's a significant state difference and we're not in a transition
|
||||
if (backendRunning !== isMicrophoneActive) {
|
||||
devInfo(`Syncing microphone state: backend=${backendRunning}, frontend=${isMicrophoneActive}`);
|
||||
|
||||
// If backend is running but frontend thinks it's not, just update frontend state
|
||||
if (backendRunning && !isMicrophoneActive) {
|
||||
devLog("Backend running, updating frontend state to active");
|
||||
setMicrophoneActive(true);
|
||||
}
|
||||
// If backend is not running but frontend thinks it is, clean up and update state
|
||||
else if (!backendRunning && isMicrophoneActive) {
|
||||
devLog("Backend not running, cleaning up frontend state");
|
||||
setMicrophoneActive(false);
|
||||
// Only clean up stream if we actually have one
|
||||
if (microphoneStreamRef.current) {
|
||||
devLog("Cleaning up orphaned stream");
|
||||
await stopMicrophoneStream();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
devWarn("Failed to sync microphone state:", error);
|
||||
// Early return if RPC data channel is not ready
|
||||
if (rpcDataChannel?.readyState !== "open") {
|
||||
devWarn("RPC connection not available for microphone sync, skipping");
|
||||
return;
|
||||
}
|
||||
}, [isMicrophoneActive, setMicrophoneActive, stopMicrophoneStream]);
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
send("microphoneStatus", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
devError("RPC microphone status failed:", resp.error);
|
||||
reject(new Error(resp.error.message));
|
||||
} else if ("result" in resp) {
|
||||
const data = resp.result as { running: boolean };
|
||||
const backendRunning = data.running;
|
||||
|
||||
// Only sync if there's a significant state difference and we're not in a transition
|
||||
if (backendRunning !== isMicrophoneActive) {
|
||||
devInfo(`Syncing microphone state: backend=${backendRunning}, frontend=${isMicrophoneActive}`);
|
||||
|
||||
// If backend is running but frontend thinks it's not, just update frontend state
|
||||
if (backendRunning && !isMicrophoneActive) {
|
||||
devLog("Backend running, updating frontend state to active");
|
||||
setMicrophoneActive(true);
|
||||
}
|
||||
// If backend is not running but frontend thinks it is, clean up and update state
|
||||
else if (!backendRunning && isMicrophoneActive) {
|
||||
devLog("Backend not running, cleaning up frontend state");
|
||||
setMicrophoneActive(false);
|
||||
// Only clean up stream if we actually have one
|
||||
if (microphoneStreamRef.current) {
|
||||
stopMicrophoneStream();
|
||||
}
|
||||
setMicrophoneMuted(false);
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error("Invalid response"));
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
devError("Error syncing microphone state:", error);
|
||||
}
|
||||
}, [isMicrophoneActive, setMicrophoneActive, setMicrophoneMuted, stopMicrophoneStream, rpcDataChannel?.readyState, send]);
|
||||
|
||||
// Start microphone stream
|
||||
const startMicrophone = useCallback(async (deviceId?: string): Promise<{ success: boolean; error?: MicrophoneError }> => {
|
||||
|
@ -169,8 +202,6 @@ export function useMicrophone() {
|
|||
audio: audioConstraints
|
||||
});
|
||||
|
||||
// Microphone stream created successfully
|
||||
|
||||
// Store the stream in both ref and store
|
||||
microphoneStreamRef.current = stream;
|
||||
setMicrophoneStream(stream);
|
||||
|
@ -286,78 +317,54 @@ export function useMicrophone() {
|
|||
// Notify backend that microphone is started
|
||||
devLog("Notifying backend about microphone start...");
|
||||
|
||||
// Retry logic for backend failures
|
||||
// Retry logic for backend failures
|
||||
let backendSuccess = false;
|
||||
let lastError: Error | string | null = null;
|
||||
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
// If this is a retry, first try to reset the backend microphone state
|
||||
if (attempt > 1) {
|
||||
devLog(`Backend start attempt ${attempt}, first trying to reset backend state...`);
|
||||
try {
|
||||
// Try the new reset endpoint first
|
||||
const resetResp = await api.POST("/microphone/reset", {});
|
||||
if (resetResp.ok) {
|
||||
devLog("Backend reset successful");
|
||||
} else {
|
||||
// Fallback to stop
|
||||
await api.POST("/microphone/stop", {});
|
||||
}
|
||||
// If this is a retry, first try to reset the backend microphone state
|
||||
if (attempt > 1) {
|
||||
devLog(`Backend start attempt ${attempt}, first trying to reset backend state...`);
|
||||
try {
|
||||
// Use RPC for reset (cloud-compatible)
|
||||
if (rpcDataChannel?.readyState === "open") {
|
||||
await new Promise<void>((resolve) => {
|
||||
send("microphoneReset", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
devWarn("RPC microphone reset failed:", resp.error);
|
||||
// Try stop as fallback
|
||||
send("microphoneStop", {}, (stopResp: JsonRpcResponse) => {
|
||||
if ("error" in stopResp) {
|
||||
devWarn("RPC microphone stop also failed:", stopResp.error);
|
||||
}
|
||||
resolve(); // Continue even if both fail
|
||||
});
|
||||
} else {
|
||||
devLog("RPC microphone reset successful");
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
// Wait a bit for the backend to reset
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
} catch (resetError) {
|
||||
devWarn("Failed to reset backend state:", resetError);
|
||||
} else {
|
||||
devWarn("RPC connection not available for reset");
|
||||
}
|
||||
} catch (resetError) {
|
||||
devWarn("Failed to reset backend state:", resetError);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await rpcMicrophoneStart();
|
||||
devLog(`Backend RPC microphone start successful (attempt ${attempt})`);
|
||||
backendSuccess = true;
|
||||
break; // Exit the retry loop on success
|
||||
} catch (rpcError) {
|
||||
lastError = `Backend RPC error: ${rpcError instanceof Error ? rpcError.message : 'Unknown error'}`;
|
||||
devError(`Backend microphone start failed with RPC error: ${lastError} (attempt ${attempt})`);
|
||||
|
||||
const backendResp = await api.POST("/microphone/start", {});
|
||||
devLog(`Backend response status (attempt ${attempt}):`, backendResp.status, "ok:", backendResp.ok);
|
||||
|
||||
if (!backendResp.ok) {
|
||||
lastError = `Backend returned status ${backendResp.status}`;
|
||||
devError(`Backend microphone start failed with status: ${backendResp.status} (attempt ${attempt})`);
|
||||
|
||||
// For 500 errors, try again after a short delay
|
||||
if (backendResp.status === 500 && attempt < 3) {
|
||||
devLog(`Retrying backend start in 500ms (attempt ${attempt + 1}/3)...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Success!
|
||||
const responseData = await backendResp.json();
|
||||
devLog("Backend response data:", responseData);
|
||||
if (responseData.status === "already running") {
|
||||
devInfo("Backend microphone was already running");
|
||||
|
||||
// If we're on the first attempt and backend says "already running",
|
||||
// but frontend thinks it's not active, this might be a stuck state
|
||||
if (attempt === 1 && !isMicrophoneActive) {
|
||||
devWarn("Backend reports 'already running' but frontend is not active - possible stuck state");
|
||||
devLog("Attempting to reset backend state and retry...");
|
||||
|
||||
try {
|
||||
const resetResp = await api.POST("/microphone/reset", {});
|
||||
if (resetResp.ok) {
|
||||
devLog("Backend reset successful, retrying start...");
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
continue; // Retry the start
|
||||
}
|
||||
} catch (resetError) {
|
||||
devWarn("Failed to reset stuck backend state:", resetError);
|
||||
}
|
||||
}
|
||||
}
|
||||
devLog("Backend microphone start successful");
|
||||
backendSuccess = true;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : String(error);
|
||||
devError(`Backend microphone start threw error (attempt ${attempt}):`, error);
|
||||
|
||||
// For network errors, try again after a short delay
|
||||
// For RPC errors, try again after a short delay
|
||||
if (attempt < 3) {
|
||||
devLog(`Retrying backend start in 500ms (attempt ${attempt + 1}/3)...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
@ -414,8 +421,6 @@ export function useMicrophone() {
|
|||
setIsStarting(false);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
// Failed to start microphone
|
||||
|
||||
let micError: MicrophoneError;
|
||||
if (error instanceof Error) {
|
||||
if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') {
|
||||
|
@ -446,7 +451,7 @@ export function useMicrophone() {
|
|||
setIsStarting(false);
|
||||
return { success: false, error: micError };
|
||||
}
|
||||
}, [peerConnection, setMicrophoneStream, setMicrophoneSender, setMicrophoneActive, setMicrophoneMuted, setMicrophoneWasEnabled, stopMicrophoneStream, isMicrophoneActive, isMicrophoneMuted, microphoneStream, isStarting, isStopping, isToggling]);
|
||||
}, [peerConnection, setMicrophoneStream, setMicrophoneSender, setMicrophoneActive, setMicrophoneMuted, setMicrophoneWasEnabled, stopMicrophoneStream, isMicrophoneActive, isMicrophoneMuted, microphoneStream, isStarting, isStopping, isToggling, rpcMicrophoneStart, rpcDataChannel?.readyState, send]);
|
||||
|
||||
|
||||
|
||||
|
@ -463,10 +468,22 @@ export function useMicrophone() {
|
|||
// First stop the stream
|
||||
await stopMicrophoneStream();
|
||||
|
||||
// Then notify backend that microphone is stopped
|
||||
// Then notify backend that microphone is stopped using RPC
|
||||
try {
|
||||
await api.POST("/microphone/stop", {});
|
||||
devLog("Backend notified about microphone stop");
|
||||
if (rpcDataChannel?.readyState === "open") {
|
||||
await new Promise<void>((resolve) => {
|
||||
send("microphoneStop", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
devWarn("RPC microphone stop failed:", resp.error);
|
||||
} else {
|
||||
devLog("Backend notified about microphone stop via RPC");
|
||||
}
|
||||
resolve(); // Continue regardless of result
|
||||
});
|
||||
});
|
||||
} else {
|
||||
devWarn("RPC connection not available for microphone stop");
|
||||
}
|
||||
} catch (error) {
|
||||
devWarn("Failed to notify backend about microphone stop:", error);
|
||||
}
|
||||
|
@ -494,7 +511,7 @@ export function useMicrophone() {
|
|||
}
|
||||
};
|
||||
}
|
||||
}, [stopMicrophoneStream, syncMicrophoneState, setMicrophoneActive, setMicrophoneMuted, setMicrophoneWasEnabled, isStarting, isStopping, isToggling]);
|
||||
}, [stopMicrophoneStream, syncMicrophoneState, setMicrophoneActive, setMicrophoneMuted, setMicrophoneWasEnabled, isStarting, isStopping, isToggling, rpcDataChannel?.readyState, send]);
|
||||
|
||||
// Toggle microphone mute
|
||||
const toggleMicrophoneMute = useCallback(async (): Promise<{ success: boolean; error?: MicrophoneError }> => {
|
||||
|
@ -569,9 +586,22 @@ export function useMicrophone() {
|
|||
|
||||
setMicrophoneMuted(newMutedState);
|
||||
|
||||
// Notify backend about mute state
|
||||
// Notify backend about mute state using RPC
|
||||
try {
|
||||
await api.POST("/microphone/mute", { muted: newMutedState });
|
||||
if (rpcDataChannel?.readyState === "open") {
|
||||
await new Promise<void>((resolve) => {
|
||||
send("microphoneMute", { muted: newMutedState }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
devWarn("RPC microphone mute failed:", resp.error);
|
||||
} else {
|
||||
devLog("Backend notified about microphone mute via RPC");
|
||||
}
|
||||
resolve(); // Continue regardless of result
|
||||
});
|
||||
});
|
||||
} else {
|
||||
devWarn("RPC connection not available for microphone mute");
|
||||
}
|
||||
} catch (error) {
|
||||
devWarn("Failed to notify backend about microphone mute:", error);
|
||||
}
|
||||
|
@ -589,7 +619,7 @@ export function useMicrophone() {
|
|||
}
|
||||
};
|
||||
}
|
||||
}, [microphoneStream, isMicrophoneActive, isMicrophoneMuted, setMicrophoneMuted, isStarting, isStopping, isToggling]);
|
||||
}, [microphoneStream, isMicrophoneActive, isMicrophoneMuted, setMicrophoneMuted, isStarting, isStopping, isToggling, rpcDataChannel?.readyState, send]);
|
||||
|
||||
|
||||
|
||||
|
@ -612,6 +642,12 @@ export function useMicrophone() {
|
|||
// Sync state on mount and auto-restore microphone if it was enabled before page reload
|
||||
useEffect(() => {
|
||||
const autoRestoreMicrophone = async () => {
|
||||
// Wait for RPC connection to be ready before attempting any operations
|
||||
if (rpcDataChannel?.readyState !== "open") {
|
||||
devLog("RPC connection not ready for microphone auto-restore, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
// First sync the current state
|
||||
await syncMicrophoneState();
|
||||
|
||||
|
@ -631,8 +667,10 @@ export function useMicrophone() {
|
|||
}
|
||||
};
|
||||
|
||||
autoRestoreMicrophone();
|
||||
}, [syncMicrophoneState, microphoneWasEnabled, isMicrophoneActive, peerConnection, startMicrophone]);
|
||||
// Add a delay to ensure RTC connection is fully established
|
||||
const timer = setTimeout(autoRestoreMicrophone, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [syncMicrophoneState, microphoneWasEnabled, isMicrophoneActive, peerConnection, startMicrophone, rpcDataChannel?.readyState]);
|
||||
|
||||
// Cleanup on unmount - use ref to avoid dependency on stopMicrophoneStream
|
||||
useEffect(() => {
|
||||
|
|
|
@ -10,9 +10,6 @@ import {
|
|||
} from "react-router";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/16/solid";
|
||||
|
||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||
import api from "@/api";
|
||||
import Root from "@/root";
|
||||
import Card from "@components/Card";
|
||||
import EmptyCard from "@components/EmptyCard";
|
||||
import NotFoundPage from "@components/NotFoundPage";
|
||||
|
@ -28,6 +25,9 @@ import DeviceIdRename from "@routes/devices.$id.rename";
|
|||
import DevicesRoute from "@routes/devices";
|
||||
import SettingsIndexRoute from "@routes/devices.$id.settings._index";
|
||||
import SettingsAccessIndexRoute from "@routes/devices.$id.settings.access._index";
|
||||
import Root from "@/root";
|
||||
import api from "@/api";
|
||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||
import Notifications from "@/notifications";
|
||||
const SignupRoute = lazy(() => import("@routes/signup"));
|
||||
const LoginRoute = lazy(() => import("@routes/login"));
|
||||
|
|
|
@ -6,9 +6,9 @@ import { Button, LinkButton } from "@components/Button";
|
|||
import Card from "@components/Card";
|
||||
import { CardHeader } from "@components/CardHeader";
|
||||
import DashboardNavbar from "@components/Header";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import { User } from "@/hooks/stores";
|
||||
import { checkAuth } from "@/main";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import { CLOUD_API } from "@/ui.config";
|
||||
|
||||
interface LoaderData {
|
||||
|
|
|
@ -9,12 +9,12 @@ import { PlusCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/20/sol
|
|||
import { TrashIcon } from "@heroicons/react/16/solid";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
import Card, { GridCard } from "@/components/Card";
|
||||
import { Button } from "@components/Button";
|
||||
import AutoHeight from "@components/AutoHeight";
|
||||
import Card, { GridCard } from "@/components/Card";
|
||||
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||
import { formatters } from "@/utils";
|
||||
import AutoHeight from "@components/AutoHeight";
|
||||
import { InputFieldWithLabel } from "@/components/InputField";
|
||||
import DebianIcon from "@/assets/debian-icon.png";
|
||||
import UbuntuIcon from "@/assets/ubuntu-icon.png";
|
||||
|
@ -25,16 +25,17 @@ import NetBootIcon from "@/assets/netboot-icon.svg";
|
|||
import Fieldset from "@/components/Fieldset";
|
||||
import { DEVICE_API } from "@/ui.config";
|
||||
|
||||
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
|
||||
import notifications from "../notifications";
|
||||
import { isOnDevice } from "../main";
|
||||
import { cx } from "../cva.config";
|
||||
import {
|
||||
MountMediaState,
|
||||
RemoteVirtualMediaState,
|
||||
useMountMediaStore,
|
||||
useRTCStore,
|
||||
} from "../hooks/stores";
|
||||
import { cx } from "../cva.config";
|
||||
import { isOnDevice } from "../main";
|
||||
import notifications from "../notifications";
|
||||
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
|
||||
|
||||
|
||||
export default function MountRoute() {
|
||||
const navigate = useNavigate();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useNavigate, useOutletContext } from "react-router";
|
||||
|
||||
import { GridCard } from "@/components/Card";
|
||||
import { Button } from "@components/Button";
|
||||
import { GridCard } from "@/components/Card";
|
||||
import LogoBlue from "@/assets/logo-blue.svg";
|
||||
import LogoWhite from "@/assets/logo-white.svg";
|
||||
|
||||
|
|
|
@ -7,13 +7,14 @@ import Card from "@components/Card";
|
|||
import { CardHeader } from "@components/CardHeader";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import DashboardNavbar from "@components/Header";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import { User } from "@/hooks/stores";
|
||||
import { checkAuth } from "@/main";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import { CLOUD_API } from "@/ui.config";
|
||||
|
||||
import api from "../api";
|
||||
|
||||
|
||||
interface LoaderData {
|
||||
device: { id: string; name: string; user: { googleId: string } };
|
||||
user: User;
|
||||
|
|
|
@ -3,8 +3,9 @@ import type { LoaderFunction } from "react-router";
|
|||
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import api from "@/api";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
import api from "@/api";
|
||||
import { GridCard } from "@/components/Card";
|
||||
import { Button, LinkButton } from "@/components/Button";
|
||||
import { InputFieldWithLabel } from "@/components/InputField";
|
||||
|
@ -15,11 +16,12 @@ import notifications from "@/notifications";
|
|||
import { DEVICE_API } from "@/ui.config";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { isOnDevice } from "@/main";
|
||||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
|
||||
import { LocalDevice } from "./devices.$id";
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
import { CloudState } from "./adopt";
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
import { LocalDevice } from "./devices.$id";
|
||||
|
||||
|
||||
|
||||
export interface TLSState {
|
||||
mode: "self-signed" | "custom" | "disabled";
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
import { useState , useEffect } from "react";
|
||||
|
||||
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
|
||||
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||
|
@ -12,6 +13,7 @@ import { useDeviceStore } from "../hooks/stores";
|
|||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
|
||||
export default function SettingsGeneralRoute() {
|
||||
const { send } = useJsonRpc();
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useNavigate } from "react-router";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { Button } from "@components/Button";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
|
||||
export default function SettingsGeneralRebootRoute() {
|
||||
const navigate = useNavigate();
|
||||
|
|
|
@ -2,9 +2,9 @@ import { useLocation, useNavigate } from "react-router";
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
import { Button } from "@components/Button";
|
||||
import Card from "@/components/Card";
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { Button } from "@components/Button";
|
||||
import { UpdateState, useUpdateStore } from "@/hooks/stores";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
|
|
|
@ -2,15 +2,16 @@ import { useEffect } from "react";
|
|||
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { SettingsItem } from "@routes/devices.$id.settings";
|
||||
import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
|
||||
|
||||
import notifications from "../notifications";
|
||||
import { UsbInfoSetting } from "../components/UsbInfoSetting";
|
||||
import { FeatureFlag } from "../components/FeatureFlag";
|
||||
|
||||
|
||||
export default function SettingsHardwareRoute() {
|
||||
const { send } = useJsonRpc();
|
||||
const settings = useSettingsStore();
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { useCallback, useEffect } from "react";
|
||||
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { useSettingsStore } from "@/hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { Checkbox } from "@/components/Checkbox";
|
||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
|
||||
export default function SettingsKeyboardRoute() {
|
||||
const { setKeyboardLayout } = useSettingsStore();
|
||||
const { showPressedKeys, setShowPressedKeys } = useSettingsStore();
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import { CheckCircleIcon } from "@heroicons/react/16/solid";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import MouseIcon from "@/assets/mouse-icon.svg";
|
||||
import PointingFinger from "@/assets/pointing-finger.svg";
|
||||
import { GridCard } from "@/components/Card";
|
||||
import { Checkbox } from "@/components/Checkbox";
|
||||
import { useSettingsStore } from "@/hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { JigglerSetting } from "@components/JigglerSetting";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useSettingsStore } from "@/hooks/stores";
|
||||
import { Checkbox } from "@/components/Checkbox";
|
||||
import { GridCard } from "@/components/Card";
|
||||
import PointingFinger from "@/assets/pointing-finger.svg";
|
||||
import MouseIcon from "@/assets/mouse-icon.svg";
|
||||
|
||||
import { cx } from "../cva.config";
|
||||
import notifications from "../notifications";
|
||||
import SettingsNestedSection from "../components/SettingsNestedSection";
|
||||
import notifications from "../notifications";
|
||||
import { cx } from "../cva.config";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
|
|
|
@ -3,6 +3,15 @@ import dayjs from "dayjs";
|
|||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { LuEthernetPort } from "react-icons/lu";
|
||||
|
||||
import { Button } from "@components/Button";
|
||||
import { GridCard } from "@components/Card";
|
||||
import InputField, { InputFieldWithLabel } from "@components/InputField";
|
||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||
import Fieldset from "@/components/Fieldset";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import notifications from "@/notifications";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import {
|
||||
IPv4Mode,
|
||||
IPv6Mode,
|
||||
|
@ -13,20 +22,11 @@ import {
|
|||
TimeSyncMode,
|
||||
useNetworkStateStore,
|
||||
} from "@/hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { Button } from "@components/Button";
|
||||
import { GridCard } from "@components/Card";
|
||||
import InputField, { InputFieldWithLabel } from "@components/InputField";
|
||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||
import Fieldset from "@/components/Fieldset";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
import Ipv6NetworkCard from "../components/Ipv6NetworkCard";
|
||||
import EmptyCard from "../components/EmptyCard";
|
||||
import AutoHeight from "../components/AutoHeight";
|
||||
import DhcpLeaseCard from "../components/DhcpLeaseCard";
|
||||
import AutoHeight from "../components/AutoHeight";
|
||||
import EmptyCard from "../components/EmptyCard";
|
||||
import Ipv6NetworkCard from "../components/Ipv6NetworkCard";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useResizeObserver } from "usehooks-ts";
|
||||
|
||||
|
||||
import Card from "@/components/Card";
|
||||
import { LinkButton } from "@/components/Button";
|
||||
import { FeatureFlag } from "@/components/FeatureFlag";
|
||||
|
@ -23,6 +24,7 @@ import { useUiStore } from "@/hooks/stores";
|
|||
|
||||
import { cx } from "../cva.config";
|
||||
|
||||
|
||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||
export default function SettingsRoute() {
|
||||
const location = useLocation();
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import { Button } from "@/components/Button";
|
||||
import { TextAreaWithLabel } from "@/components/TextArea";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { useSettingsStore } from "@/hooks/stores";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import Fieldset from "@components/Fieldset";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
import { SettingsItem } from "./devices.$id.settings";
|
||||
|
||||
|
||||
const defaultEdid =
|
||||
"00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b";
|
||||
const edids = [
|
||||
|
|
|
@ -13,6 +13,7 @@ import { CLOUD_API } from "@/ui.config";
|
|||
|
||||
import api from "../api";
|
||||
|
||||
|
||||
const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
|
||||
await checkAuth();
|
||||
const res = await fetch(`${CLOUD_API}/devices/${params.id}`, {
|
||||
|
|
|
@ -15,6 +15,9 @@ import { FocusTrap } from "focus-trap-react";
|
|||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import useWebSocket from "react-use-websocket";
|
||||
|
||||
import WebRTCVideo from "@components/WebRTCVideo";
|
||||
import DashboardNavbar from "@components/Header";
|
||||
import { DeviceStatus } from "@routes/welcome-local";
|
||||
import { CLOUD_API, DEVICE_API } from "@/ui.config";
|
||||
import api from "@/api";
|
||||
import { checkAuth, isInCloud, isOnDevice } from "@/main";
|
||||
|
@ -36,11 +39,6 @@ import {
|
|||
} from "@/hooks/stores";
|
||||
import { useMicrophone } from "@/hooks/useMicrophone";
|
||||
import { useAudioEvents } from "@/hooks/useAudioEvents";
|
||||
import WebRTCVideo from "@components/WebRTCVideo";
|
||||
import DashboardNavbar from "@components/Header";
|
||||
const ConnectionStatsSidebar = lazy(() => import('@/components/sidebar/connectionStats'));
|
||||
const Terminal = lazy(() => import('@components/Terminal'));
|
||||
const UpdateInProgressStatusCard = lazy(() => import("@/components/UpdateInProgressStatusCard"));
|
||||
import Modal from "@/components/Modal";
|
||||
import { JsonRpcRequest, JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import {
|
||||
|
@ -50,10 +48,12 @@ import {
|
|||
} from "@/components/VideoOverlay";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
|
||||
import { DeviceStatus } from "@routes/welcome-local";
|
||||
import audioQualityService from "@/services/audioQualityService";
|
||||
import { useVersion } from "@/hooks/useVersion";
|
||||
|
||||
const ConnectionStatsSidebar = lazy(() => import('@/components/sidebar/connectionStats'));
|
||||
const Terminal = lazy(() => import('@components/Terminal'));
|
||||
const UpdateInProgressStatusCard = lazy(() => import("@/components/UpdateInProgressStatusCard"));
|
||||
|
||||
interface LocalLoaderResp {
|
||||
authMode: "password" | "noPassword" | null;
|
||||
}
|
||||
|
@ -573,11 +573,6 @@ export default function KvmIdRoute() {
|
|||
};
|
||||
}, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView]);
|
||||
|
||||
// Register callback with audioQualityService
|
||||
useEffect(() => {
|
||||
audioQualityService.setReconnectionCallback(setupPeerConnection);
|
||||
}, [setupPeerConnection]);
|
||||
|
||||
// TURN server usage detection
|
||||
useEffect(() => {
|
||||
if (peerConnectionState !== "connected") return;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import GridBackground from "@components/GridBackground";
|
||||
import { LinkButton } from "@/components/Button";
|
||||
import SimpleNavbar from "@/components/SimpleNavbar";
|
||||
import Container from "@/components/Container";
|
||||
import GridBackground from "@components/GridBackground";
|
||||
|
||||
export default function DevicesAlreadyAdopted() {
|
||||
return (
|
||||
|
|
|
@ -18,6 +18,9 @@ import ExtLink from "../components/ExtLink";
|
|||
|
||||
import { DeviceStatus } from "./welcome-local";
|
||||
|
||||
|
||||
|
||||
|
||||
const loader: LoaderFunction = async () => {
|
||||
const res = await api
|
||||
.GET(`${DEVICE_API}/device/status`)
|
||||
|
|
|
@ -5,9 +5,9 @@ import { useState } from "react";
|
|||
import GridBackground from "@components/GridBackground";
|
||||
import Container from "@components/Container";
|
||||
import { Button } from "@components/Button";
|
||||
import LogoBlueIcon from "@/assets/logo-blue.png";
|
||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||
import { DEVICE_API } from "@/ui.config";
|
||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||
import LogoBlueIcon from "@/assets/logo-blue.png";
|
||||
|
||||
import { GridCard } from "../components/Card";
|
||||
import { cx } from "../cva.config";
|
||||
|
@ -15,6 +15,7 @@ import api from "../api";
|
|||
|
||||
import { DeviceStatus } from "./welcome-local";
|
||||
|
||||
|
||||
const loader: LoaderFunction = async () => {
|
||||
const res = await api
|
||||
.GET(`${DEVICE_API}/device/status`)
|
||||
|
|
|
@ -16,6 +16,8 @@ import api from "../api";
|
|||
|
||||
import { DeviceStatus } from "./welcome-local";
|
||||
|
||||
|
||||
|
||||
const loader: LoaderFunction = async () => {
|
||||
const res = await api
|
||||
.GET(`${DEVICE_API}/device/status`)
|
||||
|
|
|
@ -14,6 +14,7 @@ import { DEVICE_API } from "@/ui.config";
|
|||
|
||||
import api from "../api";
|
||||
|
||||
|
||||
export interface DeviceStatus {
|
||||
isSetup: boolean;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import api from '@/api';
|
||||
import { JsonRpcResponse } from '@/hooks/useJsonRpc';
|
||||
|
||||
interface AudioConfig {
|
||||
Quality: number;
|
||||
|
@ -15,6 +15,8 @@ interface AudioQualityResponse {
|
|||
presets: QualityPresets;
|
||||
}
|
||||
|
||||
type RpcSendFunction = (method: string, params: Record<string, unknown>, callback: (resp: JsonRpcResponse) => void) => void;
|
||||
|
||||
class AudioQualityService {
|
||||
private audioPresets: QualityPresets | null = null;
|
||||
private microphonePresets: QualityPresets | null = null;
|
||||
|
@ -24,24 +26,44 @@ class AudioQualityService {
|
|||
2: 'High',
|
||||
3: 'Ultra'
|
||||
};
|
||||
private reconnectionCallback: (() => Promise<void>) | null = null;
|
||||
private rpcSend: RpcSendFunction | null = null;
|
||||
|
||||
/**
|
||||
* Fetch audio quality presets from the backend
|
||||
* Set RPC send function for cloud compatibility
|
||||
*/
|
||||
setRpcSend(rpcSend: RpcSendFunction): void {
|
||||
this.rpcSend = rpcSend;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch audio quality presets using RPC (cloud-compatible)
|
||||
*/
|
||||
async fetchAudioQualityPresets(): Promise<AudioQualityResponse | null> {
|
||||
if (!this.rpcSend) {
|
||||
console.error('RPC not available for audio quality presets');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.GET('/audio/quality');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.audioPresets = data.presets;
|
||||
this.updateQualityLabels(data.presets);
|
||||
return data;
|
||||
}
|
||||
return await new Promise<AudioQualityResponse | null>((resolve) => {
|
||||
this.rpcSend!("audioQualityPresets", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
console.error('RPC audio quality presets failed:', resp.error);
|
||||
resolve(null);
|
||||
} else if ("result" in resp) {
|
||||
const data = resp.result as AudioQualityResponse;
|
||||
this.audioPresets = data.presets;
|
||||
this.updateQualityLabels(data.presets);
|
||||
resolve(data);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch audio quality presets:', error);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -80,34 +102,25 @@ class AudioQualityService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set reconnection callback for WebRTC reset
|
||||
*/
|
||||
setReconnectionCallback(callback: () => Promise<void>): void {
|
||||
this.reconnectionCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger audio track replacement using backend's track replacement mechanism
|
||||
*/
|
||||
private async replaceAudioTrack(): Promise<void> {
|
||||
if (this.reconnectionCallback) {
|
||||
await this.reconnectionCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set audio quality with track replacement
|
||||
* Set audio quality using RPC (cloud-compatible)
|
||||
*/
|
||||
async setAudioQuality(quality: number): Promise<boolean> {
|
||||
if (!this.rpcSend) {
|
||||
console.error('RPC not available for audio quality change');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.POST('/audio/quality', { quality });
|
||||
|
||||
if (!response.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.replaceAudioTrack();
|
||||
return true;
|
||||
return await new Promise<boolean>((resolve) => {
|
||||
this.rpcSend!("audioQuality", { quality }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
console.error('RPC audio quality change failed:', resp.error);
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to set audio quality:', error);
|
||||
return false;
|
||||
|
|
10
web.go
10
web.go
|
@ -184,16 +184,6 @@ func setupRouter() *gin.Engine {
|
|||
protected.PUT("/auth/password-local", handleUpdatePassword)
|
||||
protected.DELETE("/auth/local-password", handleDeletePassword)
|
||||
protected.POST("/storage/upload", handleUploadHttp)
|
||||
|
||||
// Audio handlers
|
||||
protected.GET("/audio/status", handleAudioStatus)
|
||||
protected.POST("/audio/mute", handleAudioMute)
|
||||
protected.GET("/audio/quality", handleAudioQuality)
|
||||
protected.POST("/audio/quality", handleSetAudioQuality)
|
||||
protected.POST("/microphone/start", handleMicrophoneStart)
|
||||
protected.POST("/microphone/stop", handleMicrophoneStop)
|
||||
protected.POST("/microphone/mute", handleMicrophoneMute)
|
||||
protected.POST("/microphone/reset", handleMicrophoneReset)
|
||||
}
|
||||
|
||||
// Catch-all route for SPA
|
||||
|
|
Loading…
Reference in New Issue