Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EPIC, WIP] feat: Implement voice receive #154

Open
wants to merge 19 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace Lavalink4NET.Experiments.Receive.Connections;

// Based on https://source.dot.net/#Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets/src/Servers/Kestrel/shared/CorrelationIdGenerator.cs,f22660215e7e9131

internal static class CorrelationIdGenerator
{
private static ReadOnlySpan<char> Encode32Chars => "0123456789ABCDEFGHIJKLMNOPQRSTUV";
private static long _lastId = DateTime.UtcNow.Ticks;

public static string GetNextId() => GenerateId(Interlocked.Increment(ref _lastId));

private static string GenerateId(long id) => string.Create(13, id, (buffer, value) =>
{
buffer[12] = Encode32Chars[(int)(value & 31)];
buffer[11] = Encode32Chars[(int)((value >> 5) & 31)];
buffer[10] = Encode32Chars[(int)((value >> 10) & 31)];
buffer[9] = Encode32Chars[(int)((value >> 15) & 31)];
buffer[8] = Encode32Chars[(int)((value >> 20) & 31)];
buffer[7] = Encode32Chars[(int)((value >> 25) & 31)];
buffer[6] = Encode32Chars[(int)((value >> 30) & 31)];
buffer[5] = Encode32Chars[(int)((value >> 35) & 31)];
buffer[4] = Encode32Chars[(int)((value >> 40) & 31)];
buffer[3] = Encode32Chars[(int)((value >> 45) & 31)];
buffer[2] = Encode32Chars[(int)((value >> 50) & 31)];
buffer[1] = Encode32Chars[(int)((value >> 55) & 31)];
buffer[0] = Encode32Chars[(int)((value >> 60) & 31)];
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Lavalink4NET.Experiments.Receive.Connections.Discovery;

using System.Net;
using System.Net.Sockets;

internal interface IIpDiscoveryService
{
ValueTask<IPEndPoint?> DiscoverExternalAddressAsync(Socket socket, uint ssrc, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
namespace Lavalink4NET.Experiments.Receive.Connections.Discovery;

using System.Buffers;
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
using System.Text;

internal sealed class IpDiscoveryService : IIpDiscoveryService
{
public async ValueTask<IPEndPoint?> DiscoverExternalAddressAsync(Socket socket, uint ssrc, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ArgumentNullException.ThrowIfNull(socket);

using var periodicTimer = new PeriodicTimer(TimeSpan.FromSeconds(1));

try
{
do
{
// discover external address
var address = await DiscoverExternalAddressSingleAsync(
socket: socket,
ssrc: ssrc,
cancellationToken: cancellationToken)
.ConfigureAwait(false);

if (address is not null)
{
// got response!
return address;
}
}
while (await periodicTimer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false));
}
catch (OperationCanceledException)
{
}

// no attempts left, give up or cancellation requested
return null;
}

private async ValueTask<IPEndPoint?> DiscoverExternalAddressSingleAsync(Socket socket, uint ssrc, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

ArgumentNullException.ThrowIfNull(socket);

// rent a buffer from the shared buffer array pool with a minimum size of 74 bytes (can
// hold the request).
var pooledBuffer = ArrayPool<byte>.Shared.Rent(74);
var buffer = pooledBuffer.AsMemory(0, 74);

try
{
// encode payload data
BinaryPrimitives.WriteUInt16BigEndian(buffer.Span[0..], 0x01); // Request Payload Type
BinaryPrimitives.WriteUInt16BigEndian(buffer.Span[2..], 70); // encoded payload size (always 70)
BinaryPrimitives.WriteUInt32BigEndian(buffer.Span[4..], ssrc); // encode the client's SSRC (big-endian)

// send payload
await socket
.SendAsync(buffer, SocketFlags.None, cancellationToken)
.ConfigureAwait(false);

var startTime = DateTimeOffset.UtcNow;

while (!cancellationToken.IsCancellationRequested)
{
var receiveResult = await socket
.ReceiveFromAsync(buffer, SocketFlags.None, new IPEndPoint(IPAddress.Any, 0), cancellationToken: cancellationToken)
.ConfigureAwait(false);

if (receiveResult.ReceivedBytes is not 74) // Total Length
{
continue;
}

var payloadType = BinaryPrimitives.ReadUInt16BigEndian(buffer.Span[0..]);
var encodedSize = BinaryPrimitives.ReadUInt16BigEndian(buffer.Span[2..]);
var ssrcValue = BinaryPrimitives.ReadUInt32BigEndian(buffer.Span[4..]);

// validate header
if (payloadType is 0x02 && encodedSize is 70 && ssrcValue == ssrc)
{
var addressSpan = buffer[8..64];
var addressTerminatorOffset = addressSpan.Span.IndexOf((byte)0);
var addressLength = addressTerminatorOffset is -1 ? 64 : addressTerminatorOffset;
var address = Encoding.ASCII.GetString(addressSpan.Span[..addressLength]);
var port = BinaryPrimitives.ReadUInt16BigEndian(buffer.Span[72..]);

return new IPEndPoint(IPAddress.Parse(address), port);
}
}
}
finally
{
// return buffer to pool
ArrayPool<byte>.Shared.Return(pooledBuffer);
}

// timeout exceeded
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Lavalink4NET.Experiments.Receive.Connections.Features;

using Microsoft.AspNetCore.Connections.Features;

internal sealed record class ConnectionIdFeature : IConnectionIdFeature
{
public required string ConnectionId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Lavalink4NET.Experiments.Receive.Connections.Features;

internal sealed record class ConnectionLabelFeature(string Label) : IConnectionLabelFeature;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Lavalink4NET.Experiments.Receive.Connections.Features;

internal sealed record class GuildIdFeature(ulong GuildId) : IGuildIdFeature;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Lavalink4NET.Experiments.Receive.Connections.Features;

public interface IConnectionLabelFeature
{
string Label { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Lavalink4NET.Experiments.Receive.Connections.Features;

public interface IGuildIdFeature
{
ulong GuildId { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Lavalink4NET.Experiments.Receive.Connections.Features;

public interface IVoiceGatewayVersionFeature
{
int Version { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Lavalink4NET.Experiments.Receive.Connections.Features;

internal sealed record class VoiceGatewayVersionFeature(int Version) : IVoiceGatewayVersionFeature;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Lavalink4NET.Experiments.Receive.Connections;

using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Lavalink4NET.Experiments.Receive.Connections.Payloads;

internal interface IVoiceConnectionHandle
{
ValueTask<IPEndPoint> SelectProtocolAsync(SelectProtocolPayload selectProtocolPayload, CancellationToken cancellationToken = default);

ValueTask SetSessionDescriptionAsync(SessionDescriptionPayload sessionDescriptionPayload, CancellationToken cancellationToken = default);

ValueTask<IPEndPoint> SetReadyAsync(ReadyPayload readyPayload, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Lavalink4NET.Experiments.Receive.Connections;

internal interface IVoiceConnectionHandler
{
ValueTask ProcessAsync(
VoiceConnectionContext connectionContext,
IVoiceConnectionHandle connectionHandle,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Lavalink4NET.Experiments.Receive.Connections;

using Lavalink4NET.Experiments.Receive.Connections.Payloads;

internal interface IVoiceProtocolHandler
{
ValueTask<PayloadReadResult> ReadAsync(
VoiceConnectionContext connectionContext,
CancellationToken cancellationToken = default);

ValueTask WriteAsync(
VoiceConnectionContext connectionContext,
IVoicePayload payload,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Lavalink4NET.Experiments.Receive.Connections;

using System.Net.WebSockets;

internal sealed record class PayloadReadCloseResult(
WebSocketCloseStatus CloseStatus,
string? CloseStatusDescription,
bool ByRemote = false);
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Lavalink4NET.Experiments.Receive.Connections;

using System.Diagnostics.CodeAnalysis;
using System.Net.WebSockets;
using Lavalink4NET.Experiments.Receive.Connections.Payloads;

internal readonly record struct PayloadReadResult
{
private readonly object? _value;

public PayloadReadResult(IVoicePayload payload)
{
ArgumentNullException.ThrowIfNull(payload);

_value = payload;
}

public PayloadReadResult(WebSocketCloseStatus closeStatus, string? closeStatusDescription, bool byRemote = false)
{
_value = new PayloadReadCloseResult(closeStatus, closeStatusDescription, byRemote);
}

public IVoicePayload? Payload => _value as IVoicePayload;

public WebSocketCloseStatus CloseStatus => _value is PayloadReadCloseResult closeResult
? closeResult.CloseStatus
: WebSocketCloseStatus.Empty;

public string? CloseStatusDescription => _value is PayloadReadCloseResult closeResult
? closeResult.CloseStatusDescription
: null;

public bool ByRemote => _value is PayloadReadCloseResult closeResult && closeResult.ByRemote;

[MemberNotNullWhen(true, nameof(Payload))]
public bool IsSuccess => _value is IVoicePayload;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Lavalink4NET.Experiments.Receive.Connections.Payloads.Converters;

using System.Text.Json;
using System.Text.Json.Serialization;

internal sealed class HeartbeatIntervalJsonConverter : JsonConverter<TimeSpan>
{
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return TimeSpan.FromMilliseconds(reader.GetDouble());
}

public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
{
writer.WriteNumberValue(value.TotalMilliseconds);
}
}
Loading
Loading