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
|
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
|
// Convert int to AudioQuality type
|
||||||
quality := audio.AudioQuality(req.Quality)
|
quality := audio.AudioQuality(req.Quality)
|
||||||
|
|
||||||
|
|
|
@ -87,8 +87,10 @@ static volatile int playback_initialized = 0;
|
||||||
// Function to dynamically update Opus encoder parameters
|
// Function to dynamically update Opus encoder parameters
|
||||||
int update_opus_encoder_params(int bitrate, int complexity, int vbr, int vbr_constraint,
|
int update_opus_encoder_params(int bitrate, int complexity, int vbr, int vbr_constraint,
|
||||||
int signal_type, int bandwidth, int dtx) {
|
int signal_type, int bandwidth, int dtx) {
|
||||||
if (!encoder || !capture_initialized) {
|
// This function is specifically for audio OUTPUT encoder parameters
|
||||||
return -1; // Encoder not initialized
|
// Only require playback initialization for audio output quality changes
|
||||||
|
if (!encoder || !playback_initialized) {
|
||||||
|
return -1; // Audio output encoder not initialized
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the static variables
|
// Update the static variables
|
||||||
|
|
|
@ -204,10 +204,9 @@ func SetAudioQuality(quality AudioQuality) {
|
||||||
dtx = Config.AudioQualityMediumOpusDTX
|
dtx = Config.AudioQualityMediumOpusDTX
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart audio output subprocess with new OPUS configuration
|
// Update audio output subprocess configuration dynamically without restart
|
||||||
if supervisor := GetAudioOutputSupervisor(); supervisor != nil {
|
|
||||||
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
|
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
|
// Immediately boost adaptive buffer sizes to handle quality change frame burst
|
||||||
// This prevents "Message channel full, dropping frame" warnings during transitions
|
// 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")
|
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)
|
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()
|
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 {
|
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 {
|
} 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
|
// Allow adaptive buffer manager to naturally adjust buffer sizes
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(Config.QualityChangeSettleDelay) // Wait for quality change to settle
|
time.Sleep(Config.QualityChangeSettleDelay) // Wait for quality change to settle
|
||||||
// Reset audio input server stats to clear persistent warnings
|
// Reset audio input server stats to clear persistent warnings
|
||||||
ResetGlobalAudioInputServerStats()
|
ResetGlobalAudioInputServerStats()
|
||||||
// Attempt recovery if microphone is still having issues
|
// Attempt recovery if there are still issues
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
RecoverGlobalAudioInputServer()
|
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 { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
|
||||||
import { DeviceStatus } from "@routes/welcome-local";
|
import { DeviceStatus } from "@routes/welcome-local";
|
||||||
import { SystemVersionInfo } from "@routes/devices.$id.settings.general.update";
|
import { SystemVersionInfo } from "@routes/devices.$id.settings.general.update";
|
||||||
|
import audioQualityService from "@/services/audioQualityService";
|
||||||
|
|
||||||
interface LocalLoaderResp {
|
interface LocalLoaderResp {
|
||||||
authMode: "password" | "noPassword" | null;
|
authMode: "password" | "noPassword" | null;
|
||||||
|
@ -533,6 +534,11 @@ export default function KvmIdRoute() {
|
||||||
};
|
};
|
||||||
}, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView]);
|
}, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView]);
|
||||||
|
|
||||||
|
// Register callback with audioQualityService
|
||||||
|
useEffect(() => {
|
||||||
|
audioQualityService.setReconnectionCallback(setupPeerConnection);
|
||||||
|
}, [setupPeerConnection]);
|
||||||
|
|
||||||
// TURN server usage detection
|
// TURN server usage detection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (peerConnectionState !== "connected") return;
|
if (peerConnectionState !== "connected") return;
|
||||||
|
|
|
@ -24,6 +24,7 @@ class AudioQualityService {
|
||||||
2: 'High',
|
2: 'High',
|
||||||
3: 'Ultra'
|
3: 'Ultra'
|
||||||
};
|
};
|
||||||
|
private reconnectionCallback: (() => Promise<void>) | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch audio quality presets from the backend
|
* 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> {
|
async setAudioQuality(quality: number): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const response = await api.POST('/audio/quality', { quality });
|
const response = await api.POST('/audio/quality', { quality });
|
||||||
return response.ok;
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.replaceAudioTrack();
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to set audio quality:', error);
|
console.error('Failed to set audio quality:', error);
|
||||||
return false;
|
return false;
|
||||||
|
|
Loading…
Reference in New Issue