What happened?
On a live loopback session, hasFirstFrameReadyForDisplay and state == .playing both go true while AVPlayer is still in waitingToPlay(.toMinimizeStalls) — the presented first frame then HOLDS STATIC for 1.55–2.81 s before rate rolls, and the host has no lever over that wait.
Measured on real hardware across 11 consecutive live tunes (raw-TS /stream.ts from Jellyfin
10.11, h264 1080p59.94 + AAC, loopback route, liveJoinProfile = .fastZap), millisecond
logs + HDMI capture at 10 fps:
- Your own host log shows the shape on every tune, e.g.:
#3 layer.isReadyForDisplay=true t+1.10s →
#3 timeControlStatus=waitingToPlay reason=AVPlayerWaitingWhileEvaluatingBufferingRateReason →
#3 timeControlStatus=waitingToPlay reason=AVPlayerWaitingToMinimizeStallsReason →
#3 timeControlStatus=playing reason=- t+3.91s — 2.54 s between first frame and rate roll.
- 9 of the 11 tunes held 1.55–2.81 s; on glass the frame is bit-static through the window
(max per-pixel delta ≤ 1 at 10 fps on 192×108 gray), i.e. a real user-visible freeze, not
a reporting artifact.
- The engine's clock advances through the window and
state reads .playing, so every
start signal a host can key chrome on fires seconds before motion. We shipped a
host-side hold keyed on currentAVPlayerItem.timebase rate (works), but the WAIT itself
stands: warm channel-change press-to-motion measures 6.4–6.6 s against our 5–7 s product
bar — at the ceiling, and the wait is the largest single share we cannot touch.
- Not a fix:
preferredForwardBufferDuration = 2.0 on the published item, applied before
the roll — measured inert (A/B on device, five warm zaps each way; identical
press-to-motion distributions). The wait is rate evaluation, not buffer-target-bound.
The engine sets neither automaticallyWaitsToMinimizeStalls nor uses
playImmediately(atRate:) anywhere (checked at both 6.21.0 and cb3baaf/6.50.1), so
AVPlayer's default policy decides the join tail on a path whose whole point
(.fastZap) is join latency.
Two asks, either resolves it (both would be better):
- A join option to shorten/skip the wait on live loopback —
playImmediately-class, or
exposure of automaticallyWaitsToMinimizeStalls scoped to the live join (the
.fastZap doc already prices "one early -16832 or a short rebuffer" as an acceptable
trade; this is the same trade's other half).
- Roll-aligned signaling: a published edge for "rate actually rolling" (we derive it from
the item timebase today; a first-class signal would let hosts key chrome honestly
without reaching into the item).
Steps to reproduce
- Serve a live MPEG-TS stream (Jellyfin
/Videos/<id>/stream.ts, h264 + AAC) and load it
with isLive: true, liveJoinProfile: .fastZap, native/loopback route.
- Watch the NativeAVPlayerHost transitions:
layer.isReadyForDisplay=true →
waitingToPlay(EvaluatingBufferingRate) → waitingToPlay(ToMinimizeStalls) →
playing 1.5–2.8 s later. The presented frame is static for that whole span.
- Optionally point an external capture at the output: the frame freeze is on-glass, with
every host-visible start signal already fired.
AetherEngine version or commit SHA
Measured at 6.21.0 (87868c1); code re-checked at 6.50.1 (cb3baaf) — no
waits/playImmediately handling exists there either, so the mechanism is presumed
unchanged (the measurement itself is 6.21.0).
Host app
Custom / my own integration
Platform
tvOS
OS version
tvOS 26.5
Device / chip
Apple TV 4K (3rd generation), AppleTV14,1 — real hardware, not simulator. HDMI capture
(Elgato 4K X) used for the on-glass verification.
Playback path
Native AVPlayer
Source media (for playback bugs)
Live TV channel via Jellyfin 10.11 raw MPEG-TS (/stream.ts): h264 High 1920×1080
59.94fps progressive + AAC-LC stereo. Reproduces on every channel of this shape we
tried (7 distinct channels, 11 tunes).
Error codes / log lines
No errors — that is the point: the hold is silent. Representative sequence quoted above;
full millisecond logs of all 11 tunes plus the 10 fps frame-difference tables available
on request.
Anything else
The freeze cost us a P1 in our own device battery: our chrome dropped its spinner on
your start signals ~2.7 s before motion, which read as "stuck" to a viewer. We work
around it host-side now, but every AetherEngine host keying on
hasFirstFrameReadyForDisplay/.playing for live will inherit the same trap.
What happened?
On a live loopback session,
hasFirstFrameReadyForDisplayandstate == .playingboth go true while AVPlayer is still inwaitingToPlay(.toMinimizeStalls)— the presented first frame then HOLDS STATIC for 1.55–2.81 s before rate rolls, and the host has no lever over that wait.Measured on real hardware across 11 consecutive live tunes (raw-TS
/stream.tsfrom Jellyfin10.11, h264 1080p59.94 + AAC, loopback route,
liveJoinProfile = .fastZap), millisecondlogs + HDMI capture at 10 fps:
#3 layer.isReadyForDisplay=true t+1.10s→#3 timeControlStatus=waitingToPlay reason=AVPlayerWaitingWhileEvaluatingBufferingRateReason→#3 timeControlStatus=waitingToPlay reason=AVPlayerWaitingToMinimizeStallsReason→#3 timeControlStatus=playing reason=- t+3.91s— 2.54 s between first frame and rate roll.(max per-pixel delta ≤ 1 at 10 fps on 192×108 gray), i.e. a real user-visible freeze, not
a reporting artifact.
statereads.playing, so everystart signal a host can key chrome on fires seconds before motion. We shipped a
host-side hold keyed on
currentAVPlayerItem.timebaserate (works), but the WAIT itselfstands: warm channel-change press-to-motion measures 6.4–6.6 s against our 5–7 s product
bar — at the ceiling, and the wait is the largest single share we cannot touch.
preferredForwardBufferDuration = 2.0on the published item, applied beforethe roll — measured inert (A/B on device, five warm zaps each way; identical
press-to-motion distributions). The wait is rate evaluation, not buffer-target-bound.
The engine sets neither
automaticallyWaitsToMinimizeStallsnor usesplayImmediately(atRate:)anywhere (checked at both 6.21.0 and cb3baaf/6.50.1), soAVPlayer's default policy decides the join tail on a path whose whole point
(
.fastZap) is join latency.Two asks, either resolves it (both would be better):
playImmediately-class, orexposure of
automaticallyWaitsToMinimizeStallsscoped to the live join (the.fastZapdoc already prices "one early -16832 or a short rebuffer" as an acceptabletrade; this is the same trade's other half).
the item timebase today; a first-class signal would let hosts key chrome honestly
without reaching into the item).
Steps to reproduce
/Videos/<id>/stream.ts, h264 + AAC) and load itwith
isLive: true,liveJoinProfile: .fastZap, native/loopback route.layer.isReadyForDisplay=true→waitingToPlay(EvaluatingBufferingRate)→waitingToPlay(ToMinimizeStalls)→playing1.5–2.8 s later. The presented frame is static for that whole span.every host-visible start signal already fired.
AetherEngine version or commit SHA
Measured at 6.21.0 (87868c1); code re-checked at 6.50.1 (cb3baaf) — no
waits/playImmediately handling exists there either, so the mechanism is presumed
unchanged (the measurement itself is 6.21.0).
Host app
Custom / my own integration
Platform
tvOS
OS version
tvOS 26.5
Device / chip
Apple TV 4K (3rd generation), AppleTV14,1 — real hardware, not simulator. HDMI capture
(Elgato 4K X) used for the on-glass verification.
Playback path
Native AVPlayer
Source media (for playback bugs)
Live TV channel via Jellyfin 10.11 raw MPEG-TS (
/stream.ts): h264 High 1920×108059.94fps progressive + AAC-LC stereo. Reproduces on every channel of this shape we
tried (7 distinct channels, 11 tunes).
Error codes / log lines
No errors — that is the point: the hold is silent. Representative sequence quoted above;
full millisecond logs of all 11 tunes plus the 10 fps frame-difference tables available
on request.
Anything else
The freeze cost us a P1 in our own device battery: our chrome dropped its spinner on
your start signals ~2.7 s before motion, which read as "stuck" to a viewer. We work
around it host-side now, but every AetherEngine host keying on
hasFirstFrameReadyForDisplay/.playingfor live will inherit the same trap.