6.10.0 - software presentation timebase and per-frame times
Two additive surfaces on the software path, useful only together: the clock a host paces an overlay against, and the frame boundaries it lands on. Drop-in from 6.9.0, no source change on the consumer side.
Added
-
softwarePresentationTimebasehands out the render synchronizer's timebase. That synchronizer is the master clock for the audio renderer and the video display layer both, and it is created unconditionally, so the timebase exists for the whole session including on a source with no audio track. nil on every other path. -
setSoftwareVideoFrameTimeObserverreports every frame the software path enqueues:player.softwarePresentationTimebase // CMTimebase?, source axis player.setSoftwareVideoFrameTimeObserver { frame in frame.presentation // CMTime, source axis frame.generation // UInt64, moves on every renderer flush }
Both read the source axis, the one the engine's subtitle cues already live on, so a host compositing a bitmap overlay (libass and friends) converts nothing between the cue times, the frame boundaries and the clock.
Why SoftwareVideoFrameTime rather than NativeVideoFrameTime
Three of the native type's five fields answer questions this path does not have. source and item are two axes because the native path muxes what it demuxes and the two differ; here the engine enqueues the source timestamp unchanged. segmentIndex indexes segments this path never produces. epoch keys a producer restart that never happens.
Filling those with placeholders would put values in fields a consumer cannot tell apart from measurements, so the new type carries what this path knows. generation keeps the job epoch actually does: entries recorded under an older generation describe frames the compositor has discarded, which is what a seek does. That matters concretely on a backward seek, where the same timestamp comes round again and the timestamps alone cannot separate the two runs.
Where the reports come from
The handover to the compositor, not the admission into the reorder buffer. A frame refused for an unschedulable timestamp (#298), skipped after a seek, or lost to a failed sample-buffer creation never reaches that point, so it is never reported as a boundary that exists. Being past the reorder buffer also means these arrive in ascending presentation order, unlike the native path's decode-order stream, which a consumer has to sort.
Verification
aetherctl play --frame-times installs the observer before load() (the documented usage: the engine re-arms each new host with it) and reads the timebase. VP9 session over a rate-limited origin, with a seek at t=12:
timebase: present, time=0.000s rate=0.00
t=12 ... ft=30 ftLast=12.200s ftGen=0 ooo=0 tb=12.153s
SEEKLANDED target=6.01 in 2ms
t=13 ... ft=32 ftLast=7.067s ftGen=1 ooo=0 tb=7.015s
frame times: frames=490 outOfOrder=0 generations=[0, 1]
ooo counts reports arriving out of presentation order, so the ascending-order guarantee is measured over 490 frames rather than asserted. The roughly 50 ms between ftLast and the timebase is the decode cushion, and its size is why the two have to be on one axis.
Requested by @edde746 in #311, with the file and line references for both halves already in the report.
Scope
Software path only. The remote-HLS bypass has the same symptom for a different reason: AVPlayer owns decode there, so the engine never sees a frame and has nothing to report. That was explicitly out of scope in the request and stays out here.