Try to fix background glitching over long periods of time
This commit is contained in:
@@ -18,6 +18,11 @@ internal sealed class IosAudioRouter
|
||||
internal static IosAudioRouter Shared { get; } = new();
|
||||
private readonly NSUserDefaults defaults = NSUserDefaults.StandardUserDefaults;
|
||||
private bool applying;
|
||||
private System.Threading.Timer? watchdog;
|
||||
private long lastRenderCallbacks = -1;
|
||||
private int watchdogMisses;
|
||||
private bool watchdogTicking;
|
||||
private bool interrupted;
|
||||
internal event Action? Changed;
|
||||
internal IosAudioPreset Preset { get; private set; } = IosAudioPreset.VoiceChat;
|
||||
internal IosBluetoothMode BluetoothMode { get; private set; } = IosBluetoothMode.HfpVoice;
|
||||
@@ -117,6 +122,7 @@ internal sealed class IosAudioRouter
|
||||
$"preferred={session.PreferredInput?.PortName ?? "default"} dataSource={session.InputDataSource?.DataSourceName ?? "default"} " +
|
||||
$"pattern={session.InputDataSource?.SelectedPolarPattern.ToString() ?? "default"}");
|
||||
session.OverrideOutputAudioPort(ForceSpeaker && BluetoothMode != IosBluetoothMode.BuiltInMicA2dp ? AVAudioSessionPortOverride.Speaker : AVAudioSessionPortOverride.None, out _); RefreshRoutes();
|
||||
ResetWatchdog(); EnsureWatchdog();
|
||||
}
|
||||
finally { applying = false; }
|
||||
}
|
||||
@@ -210,10 +216,14 @@ internal sealed class IosAudioRouter
|
||||
}
|
||||
|
||||
private void Set(string key, string? value) { if (value is null) defaults.RemoveObject(key); else defaults.SetString(value, key); }
|
||||
internal void Deactivate() => AVAudioSession.SharedInstance().SetActive(false, AVAudioSessionSetActiveOptions.NotifyOthersOnDeactivation, out _);
|
||||
internal void Deactivate()
|
||||
{
|
||||
watchdog?.Dispose(); watchdog = null; ResetWatchdog();
|
||||
AVAudioSession.SharedInstance().SetActive(false, AVAudioSessionSetActiveOptions.NotifyOthersOnDeactivation, out _);
|
||||
}
|
||||
internal void EnsureAudio(string reason)
|
||||
{
|
||||
if (!IosAudioEngine.Shared.IsConnected) return;
|
||||
if (!IosAudioEngine.Shared.IsConnected || interrupted) return;
|
||||
try { IosAudioEngine.Shared.EnsureRunning(); }
|
||||
catch (Exception exception) { System.Diagnostics.Debug.WriteLine($"Audio recovery ({reason}) failed: {exception}"); }
|
||||
}
|
||||
@@ -221,13 +231,47 @@ internal sealed class IosAudioRouter
|
||||
internal void Recover(string reason)
|
||||
{
|
||||
RefreshRoutes();
|
||||
if (!IosAudioEngine.Shared.IsConnected) return;
|
||||
if (!IosAudioEngine.Shared.IsConnected || interrupted) return;
|
||||
UIApplication.SharedApplication.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
try { IosAudioEngine.Shared.Reconfigure(); }
|
||||
catch (Exception exception) { System.Diagnostics.Debug.WriteLine($"Audio recovery ({reason}) failed: {exception}"); }
|
||||
});
|
||||
}
|
||||
|
||||
// The render callback is the only clock for the device-clocked pipeline, so a graph that
|
||||
// stops while the app is backgrounded freezes capture, mix and send with no notification to
|
||||
// recover from. Poll for that and rebuild; a failed rebuild is retried on the next tick.
|
||||
private void EnsureWatchdog()
|
||||
{
|
||||
watchdog ??= new System.Threading.Timer(_ => UIApplication.SharedApplication.BeginInvokeOnMainThread(TickWatchdog),
|
||||
null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
private void ResetWatchdog() { lastRenderCallbacks = -1; watchdogMisses = 0; }
|
||||
|
||||
// An interruption that ends while the app is suspended never delivers its Ended notification.
|
||||
internal void ResumeForeground() { interrupted = false; ResetWatchdog(); Recover("foreground"); }
|
||||
|
||||
private void TickWatchdog()
|
||||
{
|
||||
if (watchdogTicking) return;
|
||||
watchdogTicking = true;
|
||||
try
|
||||
{
|
||||
if (!IosAudioEngine.Shared.IsConnected || interrupted || applying) { ResetWatchdog(); return; }
|
||||
long callbacks = IosAudioEngine.Shared.RenderCallbacks;
|
||||
bool stalled = !IosAudioEngine.Shared.IsRunning || callbacks == lastRenderCallbacks;
|
||||
lastRenderCallbacks = callbacks;
|
||||
if (!stalled) { watchdogMisses = 0; return; }
|
||||
// One missed tick can be a route change already rebuilding the graph.
|
||||
if (++watchdogMisses < 2) return;
|
||||
ResetWatchdog();
|
||||
try { IosAudioEngine.Shared.Reconfigure(); }
|
||||
catch (Exception exception) { System.Diagnostics.Debug.WriteLine($"Audio watchdog rebuild failed: {exception}"); }
|
||||
}
|
||||
finally { watchdogTicking = false; }
|
||||
}
|
||||
private void HandleRouteChange(NSNotification note)
|
||||
{
|
||||
NSNumber? value = note.UserInfo?[new NSString("AVAudioSessionRouteChangeReasonKey")] as NSNumber;
|
||||
@@ -243,6 +287,10 @@ internal sealed class IosAudioRouter
|
||||
{
|
||||
NSNumber? type = note.UserInfo?[new NSString("AVAudioSessionInterruptionTypeKey")] as NSNumber;
|
||||
AVAudioSessionInterruptionType interruption = (AVAudioSessionInterruptionType)(type?.UInt32Value ?? 0);
|
||||
if (interruption == AVAudioSessionInterruptionType.Ended) Recover("interruption ended");
|
||||
// The system stops the graph on Began and SetActive fails until the interruption clears,
|
||||
// so suppress recovery until Ended and let the watchdog retry if that rebuild fails.
|
||||
if (interruption == AVAudioSessionInterruptionType.Began) { interrupted = true; ResetWatchdog(); return; }
|
||||
if (interruption != AVAudioSessionInterruptionType.Ended) return;
|
||||
interrupted = false; ResetWatchdog(); Recover("interruption ended");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user