6.36.0 - Live recovery asks the producer, types a dead lease, and catches a renumbered clock
Drop-in from 6.35.0. No API change. Three live-recovery readings corrected, all from one device trace (tvOS 26.6, an MPEG-TS channel behind an Xtream aggregator that answers with a 302 to a one-slot media host).
The origin caused the freezes in that trace. What is fixed here is what the engine concluded from them: recovery ran about 12 s longer than it needed to and replayed eleven seconds the viewer had already seen.
The stall ladder now asks the producer before replacing the consumer's item
Stage 2 of the #65 ladder loads a fresh AVPlayerItem, which is the right tool for a consumer that died under a healthy producer and the wrong one for a producer starved by its origin. It gated on consumer fetch activity and on the position budget, and both are silent in either case.
In the trace it fired while the source was still re-resolving and no bytes were arriving. AVPlayer rejoined an unchanged local playlist at edge-minus-holdback, five seconds behind the frozen position, refetched the tail it had already shown and parked again, and only two grace windows later did the final rung ask the host to retune.
The count of segments the producer has finalized is the one fact that separates the two, and it was already the currency of two other budgets (the reopen cycle and the muxer rebuild). Stage 2 now consults it and skips straight to liveSourceReset when nothing has been finalized since the stall.
A session with no local producer at all, a remote HLS route AVPlayer fetches itself, reports nil rather than zero and keeps its old behaviour: the absence of a producer to ask is not an answer from one. The producer-driven wedge re-anchor and the item-death chain are deliberately unchanged; neither describes a starved producer.
407 from a pinned redirect target is a dead lease
[AVIOReader] pump gen=2 rejected response status=407 at offset 34545940 from the pinned target <media host>
[AVIOReader] pump conn ended at offset 34545940 status=407, reconnecting (streak=1 retryAfter=0.0s)
[AVIOReader] pump gen=3 rejected response status=407 at offset 34545940 from the pinned target <media host>
[AVIOReader] Dropped resolved URL cache (unproductive reconnect streak)
[AVIOReader] pump conn start gen=4 offset=34545940 open-ended re-resolving through the source
407 fell through the expiry, rate-limit and hard-error classifiers alike: no pin drop from the status, charged against the full mid-stream reconnect cap, and the pin dropped only afterwards by the unproductive-streak rule. So the attempt right after the refusal went back to the address that had just refused, and the move that works, the one on gen=4, cost an extra round trip to reach.
On a redirect chain a 407 cannot mean "authenticate to your proxy". The request went out direct, which is exactly why CFNetwork logs it as an unexpected proxy response, and a genuinely configured proxy is answered by URLSession's own auth challenge long before a status code reaches the reader. It means the pinned lease is gone or an interception answered in its place, and one re-resolve through the source is the only productive response.
407 now classifies as resolved-address expiry alongside 401/403/404/410, and 402 and 451 with it: an expired subscription or a geo-refusing edge node is the same shape, where the source still mints working targets and this one stopped being one. Rate-limit statuses (429/503/509) stay out, because there the origin is metering us, the pin is fine, and re-resolving spends the request there is no room for.
A source that renumbers its clock from zero is a restart, not a programme boundary
When a live origin restarts its stream from its ring buffer with raw dts back at zero, FFmpeg's 33-bit wrap correction adds 2^33, so what reaches the producer is a large FORWARD jump:
[HLSSegmentProducer] video timeline rebase (live): jumpTicks=8487014192 srcDts=8589934592 lastSrcDts=102920400 oldShift=100915200 newShift=8587927592 continuationDts=2007000
[HLSSegmentProducer] seg-15.m4s captured (764054 B) <- identical size to seg-6
[HLSSegmentProducer] seg-16.m4s captured (1044022 B) <- identical size to seg-7
isSourceReplay opened with guard jumpTicks < 0, so the classifier that raises .sourceReplay never looked at it. The restart was absorbed as a programme boundary behind an EXT-X-DISCONTINUITY and the session re-served eleven seconds it had already played.
The anchor is the part that is easy to get wrong. A rewind lands near the first dts this session saw, because the server restarted the programme. An axis reset lands near zero no matter where the session joined the ring: oldShift=100915200 puts that join 1121 s into the origin's axis, and the restart lands at raw 0, so testing the wrap-corrected value against the first-seen dts would have missed it by three orders of magnitude.
Both shapes are recognized now and both end the pump for a host retune. The axis reset requires no recent unplanned reconnect, because the origin renumbers on the connection it already holds (gen=1->1, reconnects=0 in the trace) and the signature carries its own discriminator: a boundary inside one transport stream keeps its PCR axis running, and a genuine 33-bit wrap after ~26.5 h arrives as a continuous correction rather than a jump over the discontinuity threshold. It is live-only, since a sequential origin's archive chunks legitimately open their own axis at zero (#368).
Not in this release
The fourth finding from the same trace, that a trickling origin holds both of the reader's stall detectors at "flowing" because both are zero-byte detectors rather than throughput detectors, is real and still open. Measured against a loopback origin that delivers 100 bytes once a second for 45 s on a held connection: the reader posts one slow read: 46874ms stallWaits=71(71signaled) reconnects=0, and the producer's own no-cut watchdog, which classifies source starvation on a 35 s window, fires 30 ms after that read returns, at 48 s.
Returning a short read instead of holding out for the full request moves that to 47 s. One second, because the watchdog runs between av_read_frame calls and av_read_frame does not return until a whole packet is assembled, which at that rate takes longer than the window no matter how short the reads underneath it are. So the reader's floor is not where the time goes: the no-cut watchdog is inline in the loop it watches, which makes it depend on exactly the call it is supposed to time out. Lifting it onto a timer, with an interrupt callback to bound av_read_frame, is its own change and is not in this release.
Reported by tschuegy (#405), from a Syravo device trace.
Full changelog: https://github.com/superuser404notfound/AetherEngine/blob/main/CHANGELOG.md
Diff: 6.35.0...6.36.0