mirror of https://github.com/jetkvm/kvm.git
[WIP] Change playback latency spikes on Audio Output Quality changes
This commit is contained in:
parent
f873b50469
commit
89e68f5cdb
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue