Fix: make sure audio output enable / disable doesn't need a refresh in order for audio to become audible again

This commit is contained in:
Alex P 2025-09-20 21:07:41 +00:00
parent 439f57c3c8
commit a84f63c0c4
4 changed files with 40 additions and 27 deletions

View File

@ -20,6 +20,7 @@ import (
"github.com/coder/websocket"
"github.com/gin-gonic/gin"
"github.com/jetkvm/kvm/internal/audio"
"github.com/rs/zerolog"
)
@ -480,6 +481,16 @@ func handleSessionRequest(
cancelKeyboardMacro()
currentSession = session
// Set up audio relay callback to get current session's audio track
// This is needed for audio output to work after enable/disable cycles
audio.SetCurrentSessionCallback(func() audio.AudioTrackWriter {
if currentSession != nil {
return currentSession.AudioTrack
}
return nil
})
_ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd})
return nil
}

36
main.go
View File

@ -21,16 +21,6 @@ var (
audioSupervisor *audio.AudioOutputSupervisor
)
// runAudioServer is now handled by audio.RunAudioOutputServer
// This function is kept for backward compatibility but delegates to the audio package
func runAudioServer() {
err := audio.RunAudioOutputServer()
if err != nil {
logger.Error().Err(err).Msg("audio output server failed")
os.Exit(1)
}
}
func startAudioSubprocess() error {
// Initialize validation cache for optimal performance
audio.InitValidationCache()
@ -47,14 +37,14 @@ func startAudioSubprocess() error {
audio.SetAudioInputSupervisor(audioInputSupervisor)
// Set default OPUS configuration for audio input supervisor (low quality for single-core RV1106)
config := audio.Config
audioConfig := audio.Config
audioInputSupervisor.SetOpusConfig(
config.AudioQualityLowInputBitrate*1000, // Convert kbps to bps
config.AudioQualityLowOpusComplexity,
config.AudioQualityLowOpusVBR,
config.AudioQualityLowOpusSignalType,
config.AudioQualityLowOpusBandwidth,
config.AudioQualityLowOpusDTX,
audioConfig.AudioQualityLowInputBitrate*1000, // Convert kbps to bps
audioConfig.AudioQualityLowOpusComplexity,
audioConfig.AudioQualityLowOpusVBR,
audioConfig.AudioQualityLowOpusSignalType,
audioConfig.AudioQualityLowOpusBandwidth,
audioConfig.AudioQualityLowOpusDTX,
)
// Note: Audio input supervisor is NOT started here - it will be started on-demand
@ -110,6 +100,12 @@ func startAudioSubprocess() error {
},
)
// Check if USB audio device is enabled before starting audio processes
if config.UsbDevices == nil || !config.UsbDevices.Audio {
logger.Info().Msg("USB audio device disabled - skipping audio supervisor startup")
return nil
}
// Start the supervisor
if err := audioSupervisor.Start(); err != nil {
return fmt.Errorf("failed to start audio supervisor: %w", err)
@ -137,7 +133,11 @@ func Main(audioServer bool, audioInputServer bool) {
// If running as audio server, only initialize audio processing
if isAudioServer {
runAudioServer()
err := audio.RunAudioOutputServer()
if err != nil {
logger.Error().Err(err).Msg("audio output server failed")
os.Exit(1)
}
return
}

View File

@ -113,14 +113,12 @@ export function useMicrophone() {
// Debounce sync calls to prevent race conditions
const now = Date.now();
if (now - lastSyncRef.current < AUDIO_CONFIG.SYNC_DEBOUNCE_MS) {
devLog("Skipping sync - too frequent");
return;
}
lastSyncRef.current = now;
// Don't sync if we're in the middle of starting the microphone
if (isStartingRef.current) {
devLog("Skipping sync - microphone is starting");
return;
}
@ -197,7 +195,6 @@ export function useMicrophone() {
audioConstraints.deviceId = { exact: deviceId };
}
devLog("Requesting microphone with constraints:", audioConstraints);
const stream = await navigator.mediaDevices.getUserMedia({
audio: audioConstraints
});
@ -265,7 +262,6 @@ export function useMicrophone() {
}
// Notify backend that microphone is started
devLog("Notifying backend about microphone start...");
// Retry logic for backend failures
let backendSuccess = false;
@ -274,7 +270,6 @@ export function useMicrophone() {
for (let attempt = 1; attempt <= 3; attempt++) {
// 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") {
@ -290,7 +285,6 @@ export function useMicrophone() {
resolve(); // Continue even if both fail
});
} else {
devLog("RPC microphone reset successful");
resolve();
}
});
@ -315,7 +309,6 @@ export function useMicrophone() {
// 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));
continue;
}
@ -556,7 +549,6 @@ export function useMicrophone() {
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;
}
@ -565,7 +557,6 @@ export function useMicrophone() {
// If microphone was enabled before page reload and is not currently active, restore it
if (microphoneWasEnabled && !isMicrophoneActive && peerConnection) {
devLog("Auto-restoring microphone after page reload");
try {
const result = await startMicrophone();
if (result.success) {

11
web.go
View File

@ -20,6 +20,7 @@ import (
gin_logger "github.com/gin-contrib/logger"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/jetkvm/kvm/internal/audio"
"github.com/jetkvm/kvm/internal/logging"
"github.com/pion/webrtc/v4"
"github.com/prometheus/client_golang/prometheus"
@ -233,6 +234,16 @@ func handleWebRTCSession(c *gin.Context) {
cancelKeyboardMacro()
currentSession = session
// Set up audio relay callback to get current session's audio track
// This is needed for audio output to work after enable/disable cycles
audio.SetCurrentSessionCallback(func() audio.AudioTrackWriter {
if currentSession != nil {
return currentSession.AudioTrack
}
return nil
})
c.JSON(http.StatusOK, gin.H{"sd": sd})
}