Improve iOS voice stability and user audio controls
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-22 22:07:00 +02:00
parent 7b0c003ad4
commit ea76d5157a
15 changed files with 460 additions and 68 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ public sealed class AdaptivePcmBuffer
get => Volatile.Read(ref targetFrames) * 1000 / SampleRate;
set
{
if (value is not (20 or 40 or 60)) throw new ArgumentOutOfRangeException(nameof(value));
if (value is not (20 or 40 or 60 or 120)) throw new ArgumentOutOfRangeException(nameof(value));
Volatile.Write(ref targetFrames, value * SampleRate / 1000);
}
}
+23 -7
View File
@@ -48,16 +48,26 @@ internal sealed class ClientMediaTransport : IAsyncDisposable
{
while (!stop.IsCancellationRequested)
{
if (Environment.TickCount64 >= nextKeepalive)
try
{
if (!bound.Task.IsCompleted) socket.Send(binding, SocketFlags.None);
socket.Send(keepalive, SocketFlags.None);
nextKeepalive = Environment.TickCount64 + (bound.Task.IsCompleted ? 5000 : 250);
if (Environment.TickCount64 >= nextKeepalive)
{
if (!bound.Task.IsCompleted) socket.Send(binding, SocketFlags.None);
socket.Send(keepalive, SocketFlags.None);
nextKeepalive = Environment.TickCount64 + (bound.Task.IsCompleted ? 5000 : 250);
}
while (packets.TryRead(plain, out VoiceFrameHeader header, out int length))
{
int size = crypto.Encryptor.Encrypt(header, plain.AsSpan(0, length), packet);
socket.Send(packet.AsSpan(0, size), SocketFlags.None);
}
}
while (packets.TryRead(plain, out VoiceFrameHeader header, out int length))
catch (SocketException exception) when (IsTransientNetworkError(exception))
{
int size = crypto.Encryptor.Encrypt(header, plain.AsSpan(0, length), packet);
socket.Send(packet.AsSpan(0, size), SocketFlags.None);
// iOS can briefly lose its UDP route while Wi-Fi and cellular switch.
// Keep the sender and socket alive so the next route can carry media.
nextKeepalive = 0;
Thread.Sleep(100);
}
Thread.Sleep(1);
}
@@ -77,6 +87,8 @@ internal sealed class ClientMediaTransport : IAsyncDisposable
int length;
try { length = await socket.ReceiveAsync(packet, SocketFlags.None, stop.Token).ConfigureAwait(false); }
catch (SocketException exception) when (exception.SocketErrorCode is SocketError.ConnectionReset or SocketError.MessageSize) { continue; }
catch (SocketException exception) when (IsTransientNetworkError(exception))
{ await Task.Delay(100, stop.Token).ConfigureAwait(false); continue; }
if (!VoiceFrameHeader.TryRead(packet.AsSpan(0, length), out var candidate)) continue;
if (candidate.Type == MediaFrameType.Keepalive && length == VoiceFrameHeader.Size) { bound.TrySetResult(); continue; }
if (candidate.Type != MediaFrameType.Voice || candidate.Codec != 0 ||
@@ -89,6 +101,10 @@ internal sealed class ClientMediaTransport : IAsyncDisposable
finally { bound.TrySetCanceled(); stop.Cancel(); }
}
private static bool IsTransientNetworkError(SocketException exception) => exception.SocketErrorCode is
SocketError.NetworkDown or SocketError.NetworkUnreachable or SocketError.HostUnreachable or
SocketError.AddressNotAvailable or SocketError.NotConnected or SocketError.ConnectionReset;
public async ValueTask DisposeAsync()
{
stop.Cancel(); socket.Dispose();