Improve iOS voice stability and user audio controls
This commit is contained in:
@@ -6,7 +6,7 @@ using Channel = Voicecat.V1.Channel;
|
||||
|
||||
namespace VoiceCat.iOS;
|
||||
|
||||
internal sealed record ChatEntry(DateTime Timestamp, string Sender, string Text, bool Private);
|
||||
internal sealed record ChatEntry(DateTime Timestamp, uint SenderId, uint PeerUserId, string Sender, string Peer, string Text, bool Private);
|
||||
internal sealed record ActivityEntry(DateTime Timestamp, string Text);
|
||||
|
||||
internal sealed class AppModel
|
||||
@@ -104,6 +104,12 @@ internal sealed class AppModel
|
||||
{
|
||||
if (state == ClientConnectionState.Disconnected && next.ConnectionFailure is { } failure)
|
||||
Console.Error.WriteLine($"VoiceCat control connection failed: {failure}");
|
||||
if (state == ClientConnectionState.Disconnected)
|
||||
UIApplication.SharedApplication.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
try { await HandleConnectionLostAsync(next); }
|
||||
catch (Exception exception) { System.Diagnostics.Debug.WriteLine($"Reconnect cleanup failed: {exception}"); }
|
||||
});
|
||||
};
|
||||
client = next;
|
||||
try
|
||||
@@ -118,7 +124,8 @@ internal sealed class AppModel
|
||||
IosAudioEngine.Shared.StartListening(next);
|
||||
broadcast = new(); broadcast.Changed += BroadcastChanged; broadcast.Start(next);
|
||||
_ = PumpEventsAsync(next, lifetime.Token);
|
||||
levelTimer?.Dispose(); levelTimer = new(_ => PollAudio(), null, 50, 50);
|
||||
levelTimer?.Dispose(); levelTimer = new(_ =>
|
||||
UIApplication.SharedApplication.BeginInvokeOnMainThread(PollAudio), null, 50, 50);
|
||||
feedback.Play(SoundEvent.Login); feedback.Speak(restoring ? "Reconnected" : "Connected");
|
||||
if (restoring && restoreChannel != 0) await RestoreSessionAsync(next);
|
||||
Notify();
|
||||
@@ -127,7 +134,8 @@ internal sealed class AppModel
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Connection failed: {exception}");
|
||||
IsConnecting = false; Status = exception.Message;
|
||||
await next.DisposeAsync(); if (ReferenceEquals(client, next)) client = null;
|
||||
if (ReferenceEquals(client, next)) { client = null; await StopSessionResourcesAsync(); }
|
||||
await next.DisposeAsync();
|
||||
Notify();
|
||||
if (restoring && !explicitDisconnect) ScheduleReconnect();
|
||||
else throw;
|
||||
@@ -155,7 +163,10 @@ internal sealed class AppModel
|
||||
if (envelope.TextMessage is { } text)
|
||||
{
|
||||
User? sender = owner.Users.FirstOrDefault(user => user.Id == text.SenderId);
|
||||
messages.Add(new(DateTime.Now, sender?.Nickname ?? $"User {text.SenderId}", text.Body, text.Scope == TextScope.TextPrivate));
|
||||
bool privateMessage = text.Scope == TextScope.TextPrivate;
|
||||
uint peerUserId = privateMessage ? text.SenderId == SelfUserId ? text.TargetId : text.SenderId : 0;
|
||||
User? peer = privateMessage ? owner.Users.FirstOrDefault(user => user.Id == peerUserId) : null;
|
||||
messages.Add(new(DateTime.Now, text.SenderId, peerUserId, sender?.Nickname ?? $"User {text.SenderId}", peer?.Nickname ?? $"User {peerUserId}", text.Body, privateMessage));
|
||||
if (messages.Count > 500) messages.RemoveAt(0);
|
||||
bool self = text.SenderId == SelfUserId;
|
||||
feedback.Play(text.Scope == TextScope.TextPrivate
|
||||
@@ -174,15 +185,25 @@ internal sealed class AppModel
|
||||
catch (OperationCanceledException) { }
|
||||
finally
|
||||
{
|
||||
if (ReferenceEquals(client, owner) && !explicitDisconnect)
|
||||
{
|
||||
CaptureRestoreState(owner); await StopSessionResourcesAsync(); client = null; Status = "Connection lost";
|
||||
feedback.Play(SoundEvent.ConnectionLost); feedback.Speak("Connection lost, reconnecting"); UIApplication.SharedApplication.BeginInvokeOnMainThread(Notify);
|
||||
ScheduleReconnect();
|
||||
}
|
||||
await HandleConnectionLostAsync(owner);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleConnectionLostAsync(VoiceCatClient owner)
|
||||
{
|
||||
if (!ReferenceEquals(client, owner) || IsConnecting || explicitDisconnect) return;
|
||||
// A failed TLS reader changes State but leaves the event channel open until disposal.
|
||||
// Handle that state change directly so a Wi-Fi transition cannot strand this session.
|
||||
CaptureRestoreState(owner);
|
||||
client = null; Status = "Connection lost";
|
||||
try { await StopSessionResourcesAsync(); }
|
||||
catch (Exception exception) { System.Diagnostics.Debug.WriteLine($"Audio cleanup failed: {exception}"); }
|
||||
try { await owner.DisposeAsync(); }
|
||||
catch (Exception exception) { System.Diagnostics.Debug.WriteLine($"Connection cleanup failed: {exception}"); }
|
||||
feedback.Play(SoundEvent.ConnectionLost); feedback.Speak("Connection lost, reconnecting"); Notify();
|
||||
ScheduleReconnect();
|
||||
}
|
||||
|
||||
internal async Task JoinChannelAsync(uint channelId, string password = "")
|
||||
{
|
||||
VoiceCatClient active = client ?? throw new InvalidOperationException("Not connected.");
|
||||
@@ -258,7 +279,18 @@ internal sealed class AppModel
|
||||
{
|
||||
ServerProfile? profile = connectedProfile; if (profile is null || explicitDisconnect) return;
|
||||
int delay = Math.Min(1 << Math.Min(reconnectAttempt++, 5), 30);
|
||||
_ = Task.Run(async () => { try { await Task.Delay(TimeSpan.FromSeconds(delay), lifetime?.Token ?? default); await ConnectAsync(profile, restoring: true); } catch { } });
|
||||
CancellationToken token = lifetime?.Token ?? default;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try { await Task.Delay(TimeSpan.FromSeconds(delay), token); }
|
||||
catch (OperationCanceledException) { return; }
|
||||
UIApplication.SharedApplication.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
if (token.IsCancellationRequested || explicitDisconnect) return;
|
||||
try { await ConnectAsync(profile, restoring: true); }
|
||||
catch (Exception exception) { System.Diagnostics.Debug.WriteLine($"Reconnect failed: {exception}"); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void Notify() => Changed?.Invoke();
|
||||
@@ -274,7 +306,8 @@ internal sealed class AppModel
|
||||
|
||||
private void PollAudio()
|
||||
{
|
||||
VoiceCatClient? owner = client; uint stream = microphoneStream; if (owner is null || stream == 0) return;
|
||||
VoiceCatClient? owner = client; uint stream = microphoneStream;
|
||||
if (owner is null || owner.State != ClientConnectionState.Connected || stream == 0) return;
|
||||
(float level, bool talking) = owner.Audio.GetLocalLevel(stream); MicrophoneLevel = level;
|
||||
if (++diagnosticPolls >= 20)
|
||||
{
|
||||
@@ -283,8 +316,13 @@ internal sealed class AppModel
|
||||
Console.Error.WriteLine($"VC_AUDIO {IosAudioEngine.Shared.CaptureDiagnostics()} cycles={audio.Cycles} starved={audio.StarvedCycles} " +
|
||||
$"encoded={audio.EncodedPackets} packetDrops={audio.RejectedPackets} queued={audio.BufferedFrames}");
|
||||
}
|
||||
if (talking != lastTalking) { lastTalking = talking; owner.PublishStreamState(stream, talking); feedback.Play(talking ? SoundEvent.VoiceStart : SoundEvent.VoiceStop); }
|
||||
UIApplication.SharedApplication.BeginInvokeOnMainThread(Notify);
|
||||
if (talking != lastTalking)
|
||||
{
|
||||
try { owner.PublishStreamState(stream, talking); }
|
||||
catch (Exception exception) when (exception is IOException or InvalidOperationException or ObjectDisposedException) { return; }
|
||||
lastTalking = talking; feedback.Play(talking ? SoundEvent.VoiceStart : SoundEvent.VoiceStop);
|
||||
}
|
||||
Notify();
|
||||
}
|
||||
|
||||
private void HandleUserEvent(VoiceCatClient owner, UserEvent value)
|
||||
@@ -328,7 +366,7 @@ internal sealed class AppModel
|
||||
|
||||
private async Task StopSessionResourcesAsync()
|
||||
{
|
||||
levelTimer?.Dispose(); levelTimer = null; IosAudioEngine.Shared.Stop(); microphoneStream = 0; MicrophoneLevel = 0;
|
||||
levelTimer?.Dispose(); levelTimer = null; IosAudioEngine.Shared.Stop(); microphoneStream = 0; MicrophoneLevel = 0; lastTalking = false;
|
||||
if (broadcast is { } pump) { broadcast = null; pump.Changed -= BroadcastChanged; await pump.DisposeAsync(); }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user