using System.Net.Sockets;
using System.Runtime.InteropServices;
namespace RemSound.Core;
///
/// qWAVE flow attachment for RemSound's outbound UDP socket. Asks Windows' built-in QoS
/// service (Quality Windows Audio/Video Experience, qwave.dll) to prioritise our
/// audio packets ahead of best-effort traffic on the local hop.
///
/// What this actually buys us:
///
/// - The NIC scheduler sends our packets first when there's contention with other
/// outbound traffic (browser downloads, video uploads, OS-level background
/// transfers). Locally, our audio always goes first.
/// - Windows marks the outbound packets with DSCP bits (Voice = 46/EF) that user-mode
/// code is normally not allowed to set. The marking propagates across routers that
/// honour DSCP (most consumer kit on a LAN does).
/// - On Wi-Fi, the DSCP marking maps to WMM Voice access category — gives our packets
/// the shortest medium-contention window the standard defines. Real win on a busy
/// access point with other Wi-Fi clients fighting for airtime.
///
///
/// What it doesn't buy us: priority across the public internet. Most ISPs strip or rewrite
/// DSCP at the network edge, so qWAVE markings rarely survive past your local ISP's first
/// hop. LAN and same-house Wi-Fi: tangible benefit. Across the internet: neutral. We do
/// this anyway because the LAN/Wi-Fi half is genuine and there's no cost to attaching the
/// flow on every launch — always-on.
///
/// Lifecycle: attach to the socket once after it's bound, detach on Dispose. The qWAVE
/// flow handle is per-process-per-socket; closing it cleanly releases the OS-side flow
/// state. If anything fails (qwave.dll missing on a stripped-down Windows image, QoS
/// service disabled), the methods log and return false — the socket continues to work
/// without prioritisation.
///
public sealed class NetworkPriority : IDisposable
{
private IntPtr qosHandle = IntPtr.Zero;
private uint flowId;
private Socket? attachedSocket;
private bool flowAdded;
/// Attach the supplied socket to a Voice-priority qWAVE flow. Returns true on
/// success. On failure (logged via ) the socket is
/// untouched and continues to work without prioritisation — the caller does not need
/// to special-case the failure path. The socket must already be bound.
public bool TryAttach(Socket socket, Action? onDiagnostic = null)
{
if (attachedSocket is not null) return true; // already attached
try
{
var version = new QOS_VERSION { MajorVersion = 1, MinorVersion = 0 };
if (!QOSCreateHandle(ref version, out qosHandle))
{
var err = Marshal.GetLastWin32Error();
onDiagnostic?.Invoke($"qwave: QOSCreateHandle failed (win32={err})");
qosHandle = IntPtr.Zero;
return false;
}
// No DestAddr (IntPtr.Zero) — flow applies to any destination from this socket.
// QOS_NON_ADAPTIVE_FLOW = don't let qWAVE adjust our priority downward if it
// thinks the link is congested; we want consistent Voice priority always.
uint id = 0;
if (!QOSAddSocketToFlow(qosHandle, socket.Handle, IntPtr.Zero,
QOS_TRAFFIC_TYPE_VOICE, QOS_NON_ADAPTIVE_FLOW, ref id))
{
var err = Marshal.GetLastWin32Error();
onDiagnostic?.Invoke($"qwave: QOSAddSocketToFlow failed (win32={err})");
QOSCloseHandle(qosHandle);
qosHandle = IntPtr.Zero;
return false;
}
flowId = id;
attachedSocket = socket;
flowAdded = true;
onDiagnostic?.Invoke($"qwave: attached send socket to Voice-priority flow (flowId={flowId})");
return true;
}
catch (Exception ex)
{
onDiagnostic?.Invoke($"qwave: attach threw {ex.GetType().Name}: {ex.Message}");
if (qosHandle != IntPtr.Zero)
{
try { QOSCloseHandle(qosHandle); } catch { /* ignore */ }
qosHandle = IntPtr.Zero;
}
return false;
}
}
public void Dispose()
{
try
{
if (flowAdded && attachedSocket is not null && qosHandle != IntPtr.Zero)
{
QOSRemoveSocketFromFlow(qosHandle, attachedSocket.Handle, flowId, 0);
}
if (qosHandle != IntPtr.Zero)
{
QOSCloseHandle(qosHandle);
}
}
catch { /* shutdown is best-effort */ }
finally
{
flowAdded = false;
attachedSocket = null;
qosHandle = IntPtr.Zero;
flowId = 0;
}
}
// === Native interop ===
// qWAVE traffic-type ordering (higher = more priority): BestEffort < Background <
// ExcellentEffort < AudioVideo < Voice < Control. We pick Voice rather than
// AudioVideo because Voice has the most aggressive jitter requirements in qWAVE's
// model, which matches RemSound's sub-50 ms expectations.
private const int QOS_TRAFFIC_TYPE_VOICE = 4;
private const uint QOS_NON_ADAPTIVE_FLOW = 0x2;
[StructLayout(LayoutKind.Sequential)]
private struct QOS_VERSION
{
public ushort MajorVersion;
public ushort MinorVersion;
}
[DllImport("qwave.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool QOSCreateHandle(ref QOS_VERSION Version, out IntPtr QOSHandle);
[DllImport("qwave.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool QOSCloseHandle(IntPtr QOSHandle);
[DllImport("qwave.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool QOSAddSocketToFlow(IntPtr QOSHandle, IntPtr Socket,
IntPtr DestAddr, int TrafficType, uint Flags, ref uint FlowID);
[DllImport("qwave.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool QOSRemoveSocketFromFlow(IntPtr QOSHandle, IntPtr Socket,
uint FlowID, uint Flags);
}