Skip to content

6.2.0 - Presentation axes on the native path

Choose a tag to compare

@superuser404notfound superuser404notfound released this 30 Jul 19:29

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 sourceTime are 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.presentationAxisMap does, 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: playlistShiftSeconds holds only until the next epoch, since AVPlayer keeps presenting frames muxed under the previous shift for as long as its buffer lasts; clock.sourceTime ticks at ~10 Hz and a clock reading is not a frame boundary; and Segment.startPts / startSeconds is a genuine pair but about six seconds granular.

  • Per-frame presentation times, on both axes at once. setNativeVideoFrameTimeObserver reports a NativeVideoFrameTime for every muxed video frame: source, item, segmentIndex, isKeyframe, and a producer epoch so 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 source is 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_frame takes ownership and returns it blank, so the muxer hands it back instead.

  • currentAVPlayerItem publishes alongside currentAVPlayer. 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.