6.2.0 - Presentation axes on the native path
The native path now states the relation between its two time axes instead of leaving a host to infer it. Anyone compositing their own overlay onto native playback (libass, a burned-in badge) wants this one.
Added
-
Conversion between the source axis and the item axis, at any position. Cue times, chapter marks and
sourceTimeare source PTS.AVPlayerItem.currentTime()and its timebase read what the producer muxes into the segments. The two differ by the producer shift, and nothing published closed that gap exactly.player.presentationAxisMapdoes, in both directions, and is readable off the main actor. Requested by edde746 for frame-accurate libass rendering, with the measurement that showed the gap (#260).let map = player.presentationAxisMap map.itemSeconds(forSourceSeconds: cue.startTime) // stamp an overlay sample map.sourceSeconds(forItemSeconds: item.currentTime().seconds)
The shift is constant inside a producer epoch and steps at the edge, so the relation is a step function of item time rather than a scalar. That is why the surfaces that existed fell short:
playlistShiftSecondsholds only until the next epoch, since AVPlayer keeps presenting frames muxed under the previous shift for as long as its buffer lasts;clock.sourceTimeticks at ~10 Hz and a clock reading is not a frame boundary; andSegment.startPts/startSecondsis a genuine pair but about six seconds granular. -
Per-frame presentation times, on both axes at once.
setNativeVideoFrameTimeObserverreports aNativeVideoFrameTimefor every muxed video frame:source,item,segmentIndex,isKeyframe, and a producerepochso a consumer can drop entries whose segments a restart has since rewritten.player.setNativeVideoFrameTimeObserver { frame in // frame.source, frame.item, frame.segmentIndex, frame.isKeyframe, frame.epoch }
Called on the producer's pump thread, in decode order, so
sourceis not monotonic under B-frames. Sort before using the sequence as a frame-boundary list. It is not sorted in the engine because that would mean buffering inside the pump.The item value is the timestamp handed to the muxer, not a rescale of the source one. Those differ: the final-stage sanitizer repairs timestamps on SSAI splices whose ad creatives restart the source clock. It also cannot be read back off the packet after the write, since
av_interleaved_write_frametakes ownership and returns it blank, so the muxer hands it back instead. -
currentAVPlayerItempublishes alongsidecurrentAVPlayer. Items are swapped in place on every audio-track reload and episode autoplay, so a host holding an item or its timebase previously got no signal that it had gone stale.
Both conversion surfaces return nil rather than defaulting the shift to zero when no axis has been established. At the call site a defaulted shift is indistinguishable from a measured one, which is what #259 cost.
Fixed
-
A VOD producer restart no longer folds the whole timeline with its new shift. Each producer computes its own shift when its video gate opens, and a restart lands wherever the source seek lands, so a restart can change it. The shift history was rebuilt from scratch on every VOD restart, as one entry covering everything, so content the previous producer muxed, which AVPlayer can still hold in its buffer and put on screen, was folded with the incoming shift. The published playhead then leads or trails the picture by the difference for as long as that buffer lasts.
The history now records the item-axis position each producer starts writing from and keeps the entries below it; a backward restart drops only the entries it actually rewrites, since those describe bytes that no longer exist. Live program boundaries already worked this way and are unchanged.
In practice a scrub restart is unaffected, because AVPlayer flushes and lands above the seam. The case this changes is a restart without a seek: a backpressure wedge re-anchor, or an out-of-range fetch on a fast forward.
The shift-fold hypotheses in #65 were refuted by measurement at the time, and this does not revive them: that burst ran with an invariant shift, so the mechanism was inactive there.
Upgrading
.package(url: "https://github.com/superuser404notfound/AetherEngine", from: "6.2.0")Additive. No source change for consumers, and nothing to adopt unless you render your own overlay onto the native path.