[WIP] Change playback latency spikes on Audio Output Quality changes

This commit is contained in:
Alex P 2025-09-08 22:55:19 +00:00
parent f873b50469
commit 89e68f5cdb
5 changed files with 79 additions and 61 deletions

View File

@ -284,6 +284,13 @@ func handleSetAudioQuality(c *gin.Context) {
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)

View File

@ -87,8 +87,10 @@ static volatile int playback_initialized = 0;
// Function to dynamically update Opus encoder parameters
int update_opus_encoder_params(int bitrate, int complexity, int vbr, int vbr_constraint,
int signal_type, int bandwidth, int dtx) {
if (!encoder || !capture_initialized) {
return -1; // Encoder not initialized
// This function is specifically for audio OUTPUT encoder parameters
// Only require playback initialization for audio output quality changes
if (!encoder || !playback_initialized) {
return -1; // Audio output encoder not initialized
}
// Update the static variables

View File

@ -204,10 +204,9 @@ func SetAudioQuality(quality AudioQuality) {
dtx = Config.AudioQualityMediumOpusDTX
}
// Restart audio output subprocess with new OPUS configuration
if supervisor := GetAudioOutputSupervisor(); supervisor != nil {
// Update audio output subprocess configuration dynamically without restart
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
logger.Info().Int("quality", int(quality)).Msg("restarting audio output subprocess with new quality settings")
logger.Info().Int("quality", int(quality)).Msg("updating audio output quality settings dynamically")
// Immediately boost adaptive buffer sizes to handle quality change frame burst
// This prevents "Message channel full, dropping frame" warnings during transitions
@ -218,56 +217,37 @@ func SetAudioQuality(quality AudioQuality) {
logger.Debug().Msg("boosted adaptive buffers for quality change")
}
// Set new OPUS configuration
// Set new OPUS configuration for future restarts
if supervisor := GetAudioOutputSupervisor(); supervisor != nil {
supervisor.SetOpusConfig(config.Bitrate*1000, complexity, vbr, signalType, bandwidth, dtx)
}
// Stop current subprocess
// Send dynamic configuration update to running audio output
vbrConstraint := Config.CGOOpusVBRConstraint
if err := updateOpusEncoderParams(config.Bitrate*1000, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx); err != nil {
logger.Warn().Err(err).Msg("failed to update OPUS encoder parameters dynamically")
// Fallback to subprocess restart if dynamic update fails
if supervisor := GetAudioOutputSupervisor(); supervisor != nil {
logger.Info().Msg("falling back to subprocess restart")
supervisor.Stop()
// Wait for supervisor to fully stop before starting again with timeout
// This prevents race conditions and audio breakage
stopTimeout := time.After(Config.QualityChangeSupervisorTimeout)
ticker := time.NewTicker(Config.QualityChangeTickerInterval)
defer ticker.Stop()
for {
select {
case <-stopTimeout:
logger.Warn().Msg("supervisor did not stop within 5s timeout, proceeding anyway")
goto startSupervisor
case <-ticker.C:
if !supervisor.IsRunning() {
goto startSupervisor
}
}
}
startSupervisor:
// Start subprocess with new configuration
if err := supervisor.Start(); err != nil {
logger.Error().Err(err).Msg("failed to restart audio output subprocess")
logger.Error().Err(err).Msg("failed to restart audio output subprocess after dynamic update failure")
}
}
} else {
logger.Info().Int("quality", int(quality)).Msg("audio output subprocess restarted successfully with new quality")
logger.Info().Msg("audio output quality updated dynamically")
// Reset audio input server stats after quality change
// Reset audio output stats after config update
// Allow adaptive buffer manager to naturally adjust buffer sizes
go func() {
time.Sleep(Config.QualityChangeSettleDelay) // Wait for quality change to settle
// Reset audio input server stats to clear persistent warnings
ResetGlobalAudioInputServerStats()
// Attempt recovery if microphone is still having issues
// Attempt recovery if there are still issues
time.Sleep(1 * time.Second)
RecoverGlobalAudioInputServer()
}()
}
} else {
// Fallback to dynamic update if supervisor is not available
vbrConstraint := Config.CGOOpusVBRConstraint
if err := updateOpusEncoderParams(config.Bitrate*1000, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx); err != nil {
logging.GetDefaultLogger().Error().Err(err).Msg("Failed to update OPUS encoder parameters")
}
}
}
}

View File

@ -54,6 +54,7 @@ import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
import { DeviceStatus } from "@routes/welcome-local";
import { SystemVersionInfo } from "@routes/devices.$id.settings.general.update";
import audioQualityService from "@/services/audioQualityService";
interface LocalLoaderResp {
authMode: "password" | "noPassword" | null;
@ -533,6 +534,11 @@ 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;

View File

@ -24,6 +24,7 @@ class AudioQualityService {
2: 'High',
3: 'Ultra'
};
private reconnectionCallback: (() => Promise<void>) | null = null;
/**
* Fetch audio quality presets from the backend
@ -96,12 +97,34 @@ class AudioQualityService {
}
/**
* Set audio quality
* 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
*/
async setAudioQuality(quality: number): Promise<boolean> {
try {
const response = await api.POST('/audio/quality', { quality });
return response.ok;
if (!response.ok) {
return false;
}
await this.replaceAudioTrack();
return true;
} catch (error) {
console.error('Failed to set audio quality:', error);
return false;