Session: hydraheadipad v0.2.92, fluffy-dumpling-87 at 10.110.15.197, experience: rupelmonde-castle-viewer, 1920x1080 at 25000 kbps, H.264.
About 1 minute into the stream, the decoder queue began overflowing repeatedly. Each overflow immediately triggered an IDR frame request. New IDR frames arrived into an already-full queue, causing another overflow, which triggered another IDR request — a self-reinforcing loop lasting about 10 seconds across at least 11 overflow events.
Queue size limit: In VideoDepacketizer.c, the decode unit queue is initialized with a hard-coded bound of 15:
LbqInitializeLinkedBlockingQueue(&decodeUnitQueue, 15);
When LbqOfferQueueItem() finds currentSize == sizeBound it returns LBQ_BOUND_EXCEEDED.
Overflow recovery path (reassembleFrame in VideoDepacketizer.c, lines 506–525):
When LBQ_BOUND_EXCEEDED is returned, the code:
Flushing the queue should free space for the incoming IDR frame — but because the flush and the incoming IDR arrive asynchronously, the queue can already be partially refilled with P-frames by the time the IDR arrives. If the decoder is too slow to consume frames (e.g. because iOS VideoToolbox is falling behind at 25 Mbps), the queue fills again immediately after the IDR arrives, producing another overflow, another flush, another IDR request — the loop observed in the log.
Bitrate is hardcoded at 25000 kbps regardless of routing:
ContentView.swift line 90:
private func bitrateFor(host: String) -> Int {
25000
}
HeadConfig.swift line 51–55 shows StreamConfig.bitrateKbps returns 25000 for WireGuard hosts (10.10.x.x) and 150000 for LAN — but ContentView.swift overrides this with its own bitrateFor() that always returns 25000. The session log confirms 25000 kbps was used. Body host 10.110.15.197 is a LAN address (not 10.10.x.x WireGuard), so StreamConfig.bitrateKbps would have returned 150000 — ContentView.swift's override is what caps it to 25000.
At 1920x1080 H.264 60fps, 25000 kbps is a heavy load for iOS VideoToolbox. If the decoder falls behind (even transiently), frames accumulate in the 15-slot queue faster than they are consumed, and the overflow loop triggers.
HydraStreamSession.stop() was called at 11:48:47 and logged 'sending /cancel'. The server did not terminate until 11:50:22 — 95 seconds later. In that window the ENet control channel showed: 'Failed to send ENet control packet', 'Loss Stats: Transaction failed: 60', 'ENet peer is already disconnected'.
sendCancelToSunshine in HydraStreamSession.m (lines 205–220):
The /cancel request is fire-and-forget. It is sent with a 3-second timeout, and the completion block only logs the response — it does not take any action on failure (no retry, no fallback, no error propagation):
req.timeoutInterval = 3;
[[session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *resp, NSError *error) {
Log(LOG_D, @Sunshine