Skip to content

6.68.0 - A source that is late is not a source that has stopped

Choose a tag to compare

@superuser404notfound superuser404notfound released this 05 Sep 07:49
· 580 commits to main since this release

Two live-path fixes reported from the field, and a host-requested item handover contributed as a pull request. Both fixes are about a decision taken from the wrong reading: one spends a commitment the viewer pays for, the other keeps an offset that belongs to somebody else's run.

A source that is late is not a source that has stopped (AE#446)

When a live source stops delivering, the remaining window is served as a finished asset, because ENDLIST is the only thing measured to keep AVPlayer fetching a playlist whose tail has stopped moving. That decision fired the instant the source missed its cadence, on the same 1.5 x TARGETDURATION threshold that withdraws the blocking-reload advert.

The two do not cost the same. Withdrawing the advert is free and reversible. An item that has read an ENDLIST never reloads its playlist again, so the source coming back can only be expressed as an item swap, and the viewer pays for that swap with a seam at the end of the runway. An irreversible commitment was sized by a threshold chosen for a withdrawal that costs nothing.

Reported on a 1 s-segment stack, where TARGETDURATION 2 puts that threshold at 3.0 s: a 3.006 s stall in the source read closed a window that still listed 14 s of runway in front of the consumer, the source delivered again 0.6 s later where an item that no longer polls cannot see it, and the session played out its runway and swapped 17 s after that, for 0.18 to 0.20 s of rebuffering the outage never required.

The close now waits for 3 x TARGETDURATION of silence, bounded at both ends because both bounds are real:

  • the runway left in front of the consumer (2 x TARGETDURATION): a consumer that walks off the end of an open window gets no didPlayToEndTime to hand the session a controlled swap. It stalls at an edge that is not moving and later rejoins at edge-minus-HOLD-BACK on its own, which round 2 measured as a 117.76 s forward step. Below that floor the old timing is exactly right.
  • the producer's own 35 s patience with a source that cuts nothing: past it the read is given up, and a window not closed by then never would be. That is reachable at the large TARGETDURATION a bursty relay seals from its arrival cadence, where 3 x TD would land on the far side of the exit.

The ceiling being spent is measured rather than assumed. With the close suppressed and the advert withdrawn, which is the state this window is always in past the patience threshold, AVPlayer kept fetching the resident runway for 77 s past a freeze at TARGETDURATION 6, about 13 target durations: 20 fetches, 13 -12888 lines across 20 polls, and it stopped on the last segment the window listed rather than on patience.

Measured on the same command in both arms (aetherctl live --no-blocking-reload --realtime --preroll 0 --rewind-before-freeze 60 --freeze-after 100, 68 s of runway):

arm before after
12 s gap ENDLIST at 10.2 s of silence, consumer at 7 of 25, session committed to a swap at the end of its runway no ENDLIST, no swap, largest step 1.10 s, playing throughout
40 s outage ENDLIST at 10.2 s, played out, rejoined at the place it held, 0 segments skipped ENDLIST at 20.2 s, played out, rejoined at the place it held, 0 segments skipped

A shallow window is unchanged on purpose: at 12 s of runway with TARGETDURATION 6 both arms close at the same moment, because there the floor decides rather than the clock. The freeze leg has its own verdict for the new outcome (VERDICT: live-freeze gap absorbed), so a run where nothing was committed is no longer read as a run with a missing rejoin, and both ends of a late episode are now stated in the log, including the one where the source comes back and nothing was ever closed.

Reported by @Simpendaal.

A seek landing reads the axis its own run carries (AE#481)

The AE#418 axis is published once, at the advertised start of the segment whose gate re-aimed below its boundary, and it then stood for the whole timeline above that seam. The picture says the offset is narrower than that: it belongs to the run that segment opened.

A seek that opens a new run at a segment the producer wrote on its planned position lands on a source-true stretch, and nothing on this side looked. Measured with play --picture-probe and the re-anchoring seek last, capErr sat at +9.017 from the landing to the end of the session, so every cue placed from the clock was 9 s early, permanently. Ten rounds of #418 never saw it standing because a seek burst heals it inside a second: only a session whose last re-anchoring seek is also its last seek keeps the error.

A landing now takes the reading itself, anchored on the segment the local server answered first after the seek, which is the one whose content opens the run the landing sits in. Same build, same arms: capErr 9.037 to the end of the session before, 0.037 after, and no correction moves the picture anywhere else (+0.008 to +0.017 across every publication).

The capErr line also states its own quantum now. It differences a frame-grid value against the engine's continuous clock, so anything below one frame is the sub-frame phase of the sampling instant rather than an accuracy, and the line said nothing about that while printing three decimals either way.

A host can request the in-place item handover (AE#158)

prepareForItemReplacement() asks for the handover that keeps a PiP window alive across a native to native load(). It was gated on pictureInPictureActive alone, so a host that mounts the engine's own AVPlayerLayer still took the nil-item gap on every next-episode transition, and on tvOS that gap can leave the layer black while the successor's audio and clock run.

The request is one-shot: consumed by the next load(), cancelled by stop(), ignored when the outgoing backend is not native. When the item is kept, the native host retires the outgoing session's publishers before the engine subscribes for the successor, so a previous episode's EOF, readiness, rate and clock are not replayed into the new session, and main-actor hops queued by the outgoing item's KVO drop on their session guard instead of writing into the successor. Layer readiness is the one that matters there: the handover leaves the outgoing item mounted on the layer, so that observer is the one that goes on reporting through the gap. The nativeRemoteHLS bypass consumes the handover too; it used to drop the item to nil across a native to native load even while PiP was active.

Contributed by @Quick104.

Full changes: CHANGELOG.md, diff 6.67.2...6.68.0.