fix(core): grow UDP socket buffers — fixes 4K/5K video freezing on one frame
The data-plane UDP sockets used the OS default buffer (~208 KB on Linux, similar
on macOS), which is smaller than a single high-resolution frame burst: a
5120×1440 keyframe is ~130 packets the encode|send thread hands to sendmmsg at
once. The burst overflows the buffer — EAGAIN on the host send (now dropped, was
fatal) or a silent drop on the client recv — and because the data plane runs
infinite-GOP, one lost frame breaks every subsequent reference and the decode
freezes on the last good frame until an RFI refresh that may never catch up.
Symptom: connect at 5120×1440, see ONE frame, then a frozen image (audio + input
keep working — those ride QUIC, not this socket).
Set SO_SNDBUF/SO_RCVBUF to 8 MB (clamped by the OS to net.core.{w,r}mem_max on
Linux / kern.ipc.maxsockbuf on macOS); warn if the grant lands far below target so
an undersized host is diagnosable. The client side matters most — the SAME
UdpTransport backs the Apple client's data plane via the C ABI, and macOS grants
multi-MB buffers without any sysctl, so a rebuilt client stops losing frames.
Validated live, bazzite→client at 5120×1440: was 1319/1500 frames (12% loss →
freeze), now 1500/1500 @60 and 5279/5279 @240 (split-encode active), zero
mismatches, p50 1.9–3.4 ms. Host send buffer was still capped at 416 KB and lost
nothing — the loss was purely the client recv buffer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,14 +19,44 @@ pub struct UdpTransport {
|
||||
}
|
||||
|
||||
impl UdpTransport {
|
||||
/// Target kernel socket-buffer size. A high-resolution frame is a burst (a 5120×1440
|
||||
/// keyframe is ~130 packets the send thread hands to `sendmmsg` at once); the default
|
||||
/// UDP buffer (~208 KB on Linux) overflows on it, which EAGAINs the host send (dropping
|
||||
/// packets) or drops on the client recv — and with infinite-GOP a single lost frame
|
||||
/// freezes the decode until the next RFI refresh. Requested large; the OS clamps to
|
||||
/// `net.core.{wmem,rmem}_max` (Linux) / `kern.ipc.maxsockbuf` (macOS).
|
||||
const TARGET_SOCKBUF: usize = 8 * 1024 * 1024;
|
||||
|
||||
/// Bind `local` and `connect` to `peer`, so `send`/`recv` need no address and the
|
||||
/// kernel filters to this peer. Non-blocking, matching the [`Transport`] contract.
|
||||
pub fn connect(local: &str, peer: &str) -> std::io::Result<Self> {
|
||||
let socket = UdpSocket::bind(local)?;
|
||||
socket.connect(peer)?;
|
||||
Self::grow_buffers(&socket);
|
||||
socket.set_nonblocking(true)?;
|
||||
Ok(UdpTransport { socket })
|
||||
}
|
||||
|
||||
/// Best-effort grow of SO_SNDBUF/SO_RCVBUF (see [`TARGET_SOCKBUF`]). A failure isn't fatal
|
||||
/// (the stream just runs lossier); a grant far below the request means the OS cap is too
|
||||
/// low for clean 4K/5K streaming, so warn once with the knob to raise.
|
||||
fn grow_buffers(socket: &UdpSocket) {
|
||||
let sock = socket2::SockRef::from(socket);
|
||||
let _ = sock.set_send_buffer_size(Self::TARGET_SOCKBUF);
|
||||
let _ = sock.set_recv_buffer_size(Self::TARGET_SOCKBUF);
|
||||
// The kernel reports back the (possibly clamped, Linux-doubled) granted size.
|
||||
let granted = sock
|
||||
.send_buffer_size()
|
||||
.unwrap_or(0)
|
||||
.min(sock.recv_buffer_size().unwrap_or(0));
|
||||
if granted < Self::TARGET_SOCKBUF / 4 {
|
||||
tracing::warn!(
|
||||
granted_kb = granted / 1024,
|
||||
"UDP socket buffer capped well below target — high-resolution streaming may drop \
|
||||
frames; raise net.core.wmem_max / net.core.rmem_max (Linux) for clean 4K/5K"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for UdpTransport {
|
||||
|
||||
Reference in New Issue
Block a user