Stabilize iOS reconnect and screen audio sessions
Build and test / test (macos-latest) (push) Canceled after 0s
Build and test / test (ubuntu-24.04) (push) Canceled after 0s
Build and test / test (windows-latest) (push) Canceled after 0s
Build and test / apple-client (push) Canceled after 0s

This commit is contained in:
2026-09-26 20:30:24 +02:00
parent 5a2dddba89
commit 4cc13a27a0
11 changed files with 327 additions and 36 deletions
+33 -11
View File
@@ -51,6 +51,9 @@ internal sealed class AppModel
internal bool IsConnecting { get; private set; }
internal bool VoiceJoined => microphoneStream != 0;
internal bool ScreenSharing => broadcast?.IsActive == true;
#if DEBUG
internal bool ScreenRingMapped => broadcast?.IsMappingOpen == true;
#endif
internal bool IsBackgrounded => backgrounded;
internal float MicrophoneLevel { get; private set; }
internal string Status { get; private set; } = "Not connected";
@@ -162,7 +165,7 @@ internal sealed class AppModel
}
catch (Exception exception)
{
System.Diagnostics.Debug.WriteLine($"Connection failed: {exception}");
Console.Error.WriteLine($"VoiceCat connection setup failed: {exception}");
IsConnecting = false; Status = exception.Message;
if (ReferenceEquals(client, next)) { client = null; await StopSessionResourcesAsync(releaseAudio: !restoring); }
await next.DisposeAsync();
@@ -270,15 +273,35 @@ internal sealed class AppModel
if (microphoneStream != 0)
{
IosAudioEngine.Shared.StopMicrophone(); active.StopStream(microphoneStream); microphoneStream = 0;
await active.SubscribeVoiceAsync(false); MicrophoneLevel = 0; feedback.Play(SoundEvent.VoiceOff); Notify(); return;
if (!ScreenSharing) await active.SubscribeVoiceAsync(false);
MicrophoneLevel = 0; feedback.Play(SoundEvent.VoiceOff); Notify(); return;
}
if (AVFoundation.AVCaptureDevice.GetAuthorizationStatus(AVFoundation.AVAuthorizationMediaType.Audio) == AVFoundation.AVAuthorizationStatus.NotDetermined)
await AVFoundation.AVCaptureDevice.RequestAccessForMediaTypeAsync(AVFoundation.AVAuthorizationMediaType.Audio);
AVFoundation.AVAuthorizationStatus authorization = AVFoundation.AVCaptureDevice.GetAuthorizationStatus(AVFoundation.AVAuthorizationMediaType.Audio);
if (authorization == AVFoundation.AVAuthorizationStatus.NotDetermined)
authorization = await AVFoundation.AVCaptureDevice.RequestAccessForMediaTypeAsync(AVFoundation.AVAuthorizationMediaType.Audio)
? AVFoundation.AVAuthorizationStatus.Authorized : AVFoundation.AVAuthorizationStatus.Denied;
if (authorization != AVFoundation.AVAuthorizationStatus.Authorized)
throw new InvalidOperationException("Microphone access is required to join voice. Allow it in iOS Settings.");
VoiceSubscriptionResult subscribed = await active.SubscribeVoiceAsync();
if (!subscribed.Ok) throw new InvalidOperationException(subscribed.Error);
int channels = IosAudioRouter.Shared.CaptureChannels;
StreamInfo stream = await active.StartStreamAsync(StreamKind.StreamMic, "Microphone", channels);
microphoneStream = stream.StreamId; IosAudioEngine.Shared.StartMicrophone(stream.StreamId, channels); feedback.Play(SoundEvent.VoiceOn); Notify();
uint startedStream = 0;
try
{
int channels = IosAudioRouter.Shared.CaptureChannels;
StreamInfo stream = await active.StartStreamAsync(StreamKind.StreamMic, "Microphone", channels);
startedStream = stream.StreamId;
IosAudioEngine.Shared.StartMicrophone(stream.StreamId, channels);
microphoneStream = stream.StreamId; feedback.Play(SoundEvent.VoiceOn); Notify();
}
catch
{
IosAudioEngine.Shared.StopMicrophone();
if (startedStream != 0)
try { active.StopStream(startedStream); } catch (Exception exception) when (exception is IOException or InvalidOperationException or ObjectDisposedException) { }
if (!ScreenSharing)
try { await active.SubscribeVoiceAsync(false); } catch (Exception exception) when (exception is IOException or InvalidOperationException or ObjectDisposedException) { }
throw;
}
}
internal void ToggleScreenAudio()
@@ -390,10 +413,9 @@ internal sealed class AppModel
{
UIApplication.SharedApplication.BeginInvokeOnMainThread(() =>
{
// Presenting either system picker can replace or interrupt the app's audio session.
// Rebuild after the producer becomes active so playback and an existing mic tap are
// attached to the session that will remain in use for the broadcast.
if (ScreenSharing) IosAudioRouter.Shared.Recover("screen sharing started");
// Presenting either picker can change the audio route. Keep a healthy graph; if the
// picker stopped it, Recover resumes the same graph or rebuilds for changed hardware.
IosAudioRouter.Shared.Recover(ScreenSharing ? "screen sharing started" : "screen sharing stopped", force: false);
Notify();
});
}