Add adaptive packet loss handling
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 16:54:49 +02:00
parent 7c4708c3de
commit 7b0c003ad4
35 changed files with 518 additions and 60 deletions
+34 -4
View File
@@ -39,6 +39,8 @@ public sealed partial class VoiceCatClient : IAsyncDisposable
private long nextRequest;
private AuthResult? authentication;
private ServerHello? hello;
private uint adaptiveLossChannel;
private int adaptiveLossPercent = -1;
private ClientConnectionState state;
public event Action<ClientConnectionState>? ConnectionStateChanged;
@@ -47,6 +49,7 @@ public sealed partial class VoiceCatClient : IAsyncDisposable
public Task Completion => reader;
public AuthResult? Authentication { get { lock (stateGate) return authentication?.Clone(); } }
public ServerHello? ServerHello { get { lock (stateGate) return hello?.Clone(); } }
public bool SupportsAdaptivePacketLoss { get { lock (stateGate) return hello?.Features.Contains(ProtocolFeatures.AdaptivePacketLoss) == true; } }
public IReadOnlyList<Channel> Channels { get { lock (stateGate) return channels.Values.Select(c => c.Clone()).ToArray(); } }
public IReadOnlyList<User> Users { get { lock (stateGate) return users.Values.Select(u => u.Clone()).ToArray(); } }
public bool TryReadEvent(out Envelope? envelope) => events.Reader.TryRead(out envelope);
@@ -94,7 +97,9 @@ public sealed partial class VoiceCatClient : IAsyncDisposable
pins.Pin(host, port, certificatePin);
}
reader = ReadAsync(control, connectionLifetime.Token);
Envelope response = await RequestAsync(new() { ClientHello = new() { ProtoVersion = 2, ClientName = clientName, ClientVersion = clientVersion } }, token).ConfigureAwait(false);
var clientHello = new ClientHello { ProtoVersion = 2, ClientName = clientName, ClientVersion = clientVersion };
clientHello.Features.Add(ProtocolFeatures.AdaptivePacketLoss);
Envelope response = await RequestAsync(new() { ClientHello = clientHello }, token).ConfigureAwait(false);
if (response.ServerHello?.ProtoVersion != 2) throw new IOException("Unsupported server protocol.");
lock (stateGate) hello = response.ServerHello.Clone();
keepalive = KeepaliveAsync(connectionLifetime.Token);
@@ -147,7 +152,14 @@ public sealed partial class VoiceCatClient : IAsyncDisposable
var info = new StreamInfo { StreamId = response.StreamId, Ssrc = response.Ssrc, Kind = kind, Audio = response.EffectiveAudio.Clone(), Label = label };
try
{
lock (stateGate) { if (State != ClientConnectionState.Connected) throw new InvalidOperationException("Client disconnected during stream negotiation."); Audio.AddLocalStream(info, captureChannels); localStreams[info.StreamId] = info; }
lock (stateGate)
{
if (State != ClientConnectionState.Connected) throw new InvalidOperationException("Client disconnected during stream negotiation.");
Audio.AddLocalStream(info, captureChannels); localStreams[info.StreamId] = info;
User? self = authentication is null ? null : users.GetValueOrDefault(authentication.Self.Id, authentication.Self);
if (self is not null && self.ChannelId == adaptiveLossChannel && adaptiveLossPercent >= 0)
Audio.SetExpectedPacketLoss(adaptiveLossPercent);
}
return info.Clone();
}
catch { if (State == ClientConnectionState.Connected) Send(new() { StreamStop = new() { StreamId = info.StreamId } }); throw; }
@@ -226,13 +238,31 @@ public sealed partial class VoiceCatClient : IAsyncDisposable
if (message.ChannelEvent is not null)
{
if (message.ChannelEvent.Kind == ChannelEvent.Types.Kind.Deleted) channels.Remove(message.ChannelEvent.DeletedId);
else if (message.ChannelEvent.Channel is not null) channels[message.ChannelEvent.Channel.Id] = message.ChannelEvent.Channel.Clone();
else if (message.ChannelEvent.Channel is not null)
{
PacketLossMode previousMode = channels.GetValueOrDefault(message.ChannelEvent.Channel.Id)?.Audio.PacketLossMode
?? PacketLossMode.PacketLossManual;
channels[message.ChannelEvent.Channel.Id] = message.ChannelEvent.Channel.Clone();
User? self = authentication is null ? null : users.GetValueOrDefault(authentication.Self.Id, authentication.Self);
if (self?.ChannelId == message.ChannelEvent.Channel.Id && previousMode != message.ChannelEvent.Channel.Audio.PacketLossMode)
{ adaptiveLossChannel = 0; adaptiveLossPercent = -1; }
}
}
if (message.UserEvent is not null)
{
if (message.UserEvent.Kind == UserEvent.Types.Kind.Left) users.Remove(message.UserEvent.LeftId);
else if (message.UserEvent.User is not null) users[message.UserEvent.User.Id] = message.UserEvent.User.Clone();
}
if (message.PacketLossUpdate is not null && authentication is not null)
{
User self = users.GetValueOrDefault(authentication.Self.Id, authentication.Self);
if (self.ChannelId == message.PacketLossUpdate.ChannelId)
{
adaptiveLossChannel = self.ChannelId;
adaptiveLossPercent = checked((int)message.PacketLossUpdate.AppliedPercent);
Audio.SetExpectedPacketLoss(checked((int)message.PacketLossUpdate.AppliedPercent));
}
}
if (authentication is not null && (message.ServerState is not null || message.UserEvent is not null))
{
User self = users.GetValueOrDefault(authentication.Self.Id, authentication.Self);
@@ -273,7 +303,7 @@ public sealed partial class VoiceCatClient : IAsyncDisposable
media = null;
mediaCrypto?.Dispose(); mediaCrypto = null;
connectionLifetime?.Dispose(); connectionLifetime = null;
lock (stateGate) { authentication = null; hello = null; channels.Clear(); users.Clear(); }
lock (stateGate) { authentication = null; hello = null; channels.Clear(); users.Clear(); adaptiveLossChannel = 0; adaptiveLossPercent = -1; }
lock (stateGate)
{
foreach (var id in localStreams.Keys) Audio.RemoveLocalStream(id);