Description
## Stream context
Session: hydraheadipad v0.2.92, fluffy-dumpling-87 at 10.110.15.197, experience: rupelmonde-castle-viewer, 1920x1080 at 25000 kbps, H.264.
---
## Anomaly 1 — Video decode unit queue overflow storm (11:48:22–11:48:32)
### What the log shows
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.
### What the code shows
**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:
1. Sets waitingForIdrFrame = true
2. Drops the frame that just failed to enqueue
3. Flushes all frames already in the queue (LbqFlushQueueItems)
4. Calls LiRequestIdrFrame()
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.
### Possible fixes to consider
- Reduce the queue bound below 15 to trigger the overflow/flush earlier, giving the decoder less backlog to drain before accepting the IDR frame — though this increases sensitivity to short decoder stalls.
- After an overflow, back off IDR requests (e.g. wait one RTT before requesting again) rather than requesting immediately on every subsequent overflow.
- Investigate whether 25000 kbps for LAN H.264 at 1080p60 is sustainable on the target iPad model, and whether reducing to 15000–20000 kbps eliminates decoder stalls under normal load.
- The bitrateFor() override in ContentView.swift appears to shadow the per-routing logic in HeadConfig.swift. Clarify which one should be authoritative.
---
## Anomaly 2 — 95-second gap between stop() (11:48:47) and server termination (11:50:22)
### What the log shows
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'.
### What the code shows
**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