Fix: USB Gadgets updates

This commit is contained in:
Alex P 2025-08-25 10:41:53 +00:00
parent bc53523fbb
commit 2afe2ca539
4 changed files with 56 additions and 4 deletions

View File

@ -23,6 +23,7 @@ const (
AudioEventMicrophoneMetrics AudioEventType = "microphone-metrics-update" AudioEventMicrophoneMetrics AudioEventType = "microphone-metrics-update"
AudioEventProcessMetrics AudioEventType = "audio-process-metrics" AudioEventProcessMetrics AudioEventType = "audio-process-metrics"
AudioEventMicProcessMetrics AudioEventType = "microphone-process-metrics" AudioEventMicProcessMetrics AudioEventType = "microphone-process-metrics"
AudioEventDeviceChanged AudioEventType = "audio-device-changed"
) )
// AudioEvent represents a WebSocket audio event // AudioEvent represents a WebSocket audio event
@ -73,6 +74,12 @@ type ProcessMetricsData struct {
ProcessName string `json:"process_name"` ProcessName string `json:"process_name"`
} }
// AudioDeviceChangedData represents audio device configuration change data
type AudioDeviceChangedData struct {
Enabled bool `json:"enabled"`
Reason string `json:"reason"`
}
// AudioEventSubscriber represents a WebSocket connection subscribed to audio events // AudioEventSubscriber represents a WebSocket connection subscribed to audio events
type AudioEventSubscriber struct { type AudioEventSubscriber struct {
conn *websocket.Conn conn *websocket.Conn
@ -164,6 +171,15 @@ func (aeb *AudioEventBroadcaster) BroadcastMicrophoneStateChanged(running, sessi
aeb.broadcast(event) aeb.broadcast(event)
} }
// BroadcastAudioDeviceChanged broadcasts audio device configuration changes
func (aeb *AudioEventBroadcaster) BroadcastAudioDeviceChanged(enabled bool, reason string) {
event := createAudioEvent(AudioEventDeviceChanged, AudioDeviceChangedData{
Enabled: enabled,
Reason: reason,
})
aeb.broadcast(event)
}
// sendInitialState sends current audio state to a new subscriber // sendInitialState sends current audio state to a new subscriber
func (aeb *AudioEventBroadcaster) sendInitialState(connectionID string) { func (aeb *AudioEventBroadcaster) sendInitialState(connectionID string) {
aeb.mutex.RLock() aeb.mutex.RLock()

View File

@ -15,6 +15,7 @@ import (
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
"go.bug.st/serial" "go.bug.st/serial"
"github.com/jetkvm/kvm/internal/audio"
"github.com/jetkvm/kvm/internal/usbgadget" "github.com/jetkvm/kvm/internal/usbgadget"
) )
@ -956,6 +957,11 @@ func rpcSetUsbDevices(usbDevices usbgadget.Devices) error {
if err := audioSupervisor.Start(); err != nil { if err := audioSupervisor.Start(); err != nil {
logger.Error().Err(err).Msg("failed to start audio supervisor") logger.Error().Err(err).Msg("failed to start audio supervisor")
// Don't return error here as USB reconfiguration was successful // Don't return error here as USB reconfiguration was successful
} else {
// Broadcast audio device change event to notify WebRTC session
broadcaster := audio.GetAudioEventBroadcaster()
broadcaster.BroadcastAudioDeviceChanged(true, "usb_reconfiguration")
logger.Info().Msg("broadcasted audio device change event after USB reconfiguration")
} }
} }
@ -999,6 +1005,11 @@ func rpcSetUsbDeviceState(device string, enabled bool) error {
logger.Info().Msg("starting audio processes due to audio device being enabled") logger.Info().Msg("starting audio processes due to audio device being enabled")
if err := audioSupervisor.Start(); err != nil { if err := audioSupervisor.Start(); err != nil {
logger.Error().Err(err).Msg("failed to start audio supervisor") logger.Error().Err(err).Msg("failed to start audio supervisor")
} else {
// Broadcast audio device change event to notify WebRTC session
broadcaster := audio.GetAudioEventBroadcaster()
broadcaster.BroadcastAudioDeviceChanged(true, "device_enabled")
logger.Info().Msg("broadcasted audio device change event after enabling audio device")
} }
} }
config.UsbDevices.Audio = enabled config.UsbDevices.Audio = enabled

View File

@ -8,7 +8,8 @@ export type AudioEventType =
| 'microphone-state-changed' | 'microphone-state-changed'
| 'microphone-metrics-update' | 'microphone-metrics-update'
| 'audio-process-metrics' | 'audio-process-metrics'
| 'microphone-process-metrics'; | 'microphone-process-metrics'
| 'audio-device-changed';
// Audio event data interfaces // Audio event data interfaces
export interface AudioMuteData { export interface AudioMuteData {
@ -48,10 +49,15 @@ export interface ProcessMetricsData {
process_name: string; process_name: string;
} }
export interface AudioDeviceChangedData {
enabled: boolean;
reason: string;
}
// Audio event structure // Audio event structure
export interface AudioEvent { export interface AudioEvent {
type: AudioEventType; type: AudioEventType;
data: AudioMuteData | AudioMetricsData | MicrophoneStateData | MicrophoneMetricsData | ProcessMetricsData; data: AudioMuteData | AudioMetricsData | MicrophoneStateData | MicrophoneMetricsData | ProcessMetricsData | AudioDeviceChangedData;
} }
// Hook return type // Hook return type
@ -72,6 +78,9 @@ export interface UseAudioEventsReturn {
audioProcessMetrics: ProcessMetricsData | null; audioProcessMetrics: ProcessMetricsData | null;
microphoneProcessMetrics: ProcessMetricsData | null; microphoneProcessMetrics: ProcessMetricsData | null;
// Device change events
onAudioDeviceChanged?: (data: AudioDeviceChangedData) => void;
// Manual subscription control // Manual subscription control
subscribe: () => void; subscribe: () => void;
unsubscribe: () => void; unsubscribe: () => void;
@ -84,7 +93,7 @@ const globalSubscriptionState = {
connectionId: null as string | null connectionId: null as string | null
}; };
export function useAudioEvents(): UseAudioEventsReturn { export function useAudioEvents(onAudioDeviceChanged?: (data: AudioDeviceChangedData) => void): UseAudioEventsReturn {
// State for audio data // State for audio data
const [audioMuted, setAudioMuted] = useState<boolean | null>(null); const [audioMuted, setAudioMuted] = useState<boolean | null>(null);
const [audioMetrics, setAudioMetrics] = useState<AudioMetricsData | null>(null); const [audioMetrics, setAudioMetrics] = useState<AudioMetricsData | null>(null);
@ -244,6 +253,15 @@ export function useAudioEvents(): UseAudioEventsReturn {
break; break;
} }
case 'audio-device-changed': {
const deviceChangedData = audioEvent.data as AudioDeviceChangedData;
console.log('[AudioEvents] Audio device changed:', deviceChangedData);
if (onAudioDeviceChanged) {
onAudioDeviceChanged(deviceChangedData);
}
break;
}
default: default:
// Ignore other message types (WebRTC signaling, etc.) // Ignore other message types (WebRTC signaling, etc.)
break; break;
@ -256,7 +274,7 @@ export function useAudioEvents(): UseAudioEventsReturn {
} }
} }
} }
}, [lastMessage]); }, [lastMessage, onAudioDeviceChanged]);
// Auto-subscribe when connected // Auto-subscribe when connected
useEffect(() => { useEffect(() => {
@ -309,6 +327,9 @@ export function useAudioEvents(): UseAudioEventsReturn {
audioProcessMetrics, audioProcessMetrics,
microphoneProcessMetrics, microphoneProcessMetrics,
// Device change events
onAudioDeviceChanged,
// Manual subscription control // Manual subscription control
subscribe, subscribe,
unsubscribe, unsubscribe,

View File

@ -34,6 +34,7 @@ import {
VideoState, VideoState,
} from "@/hooks/stores"; } from "@/hooks/stores";
import { useMicrophone } from "@/hooks/useMicrophone"; import { useMicrophone } from "@/hooks/useMicrophone";
import { useAudioEvents } from "@/hooks/useAudioEvents";
import WebRTCVideo from "@components/WebRTCVideo"; import WebRTCVideo from "@components/WebRTCVideo";
import { checkAuth, isInCloud, isOnDevice } from "@/main"; import { checkAuth, isInCloud, isOnDevice } from "@/main";
import DashboardNavbar from "@components/Header"; import DashboardNavbar from "@components/Header";
@ -655,6 +656,9 @@ export default function KvmIdRoute() {
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel); const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
const { send } = useJsonRpc(onJsonRpcRequest); const { send } = useJsonRpc(onJsonRpcRequest);
// Use audio events hook without device change handler to avoid subscription loops
useAudioEvents();
useEffect(() => { useEffect(() => {
if (rpcDataChannel?.readyState !== "open") return; if (rpcDataChannel?.readyState !== "open") return;
send("getVideoState", {}, (resp: JsonRpcResponse) => { send("getVideoState", {}, (resp: JsonRpcResponse) => {