Skip to content

Live join: hasFirstFrameReadyForDisplay and .playing fire 1.5–2.8 s before the rate rolls (MinimizeStalls), and no join option covers the wait #440

Description

@cmcpherson274

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):

  1. 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).
  2. 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

  1. Serve a live MPEG-TS stream (Jellyfin /Videos/<id>/stream.ts, h264 + AAC) and load it
    with isLive: true, liveJoinProfile: .fastZap, native/loopback route.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions