Skip to content

A 5.000 s fastZap live join reaches readyToPlay and then places nothing: the item holds no loaded range at all, sustained #509

Description

@superuser404notfound

Split out of #447, which stays closed: the seal in that report is correct, as @AttiK22 says
in #447 (comment),
and none of the four terms #447 fixed can reach this. The failure is one step past every
gate that issue is about, so it gets its own thread.

Reported by @AttiK22 on 6.66.0, identical on iOS 26.6.1 and tvOS 26.5. Xtream raw
MPEG-TS over HTTP, h264 1080p50 + AAC, dispatch: codec=27 -> native (loopback),
liveJoinProfile = .fastZap, liveJoinStartsImmediately = true,
maxConcurrentSourceRequests = 1. A channel cutting 5.000 s segments reaches readyToPlay
and then never presents. The playhead does not move for the whole session.

What the capture already says, and it is sharper than it reads

Three of those fields are engine-internal and worth stating exactly, because two of them
were doing more work in that report than they can carry.

[LagDiag] clk=95258.48 dclk=0.00 tcs=waiting rate=1.00 wait=AVPlayerWaitingToMinimizeStallsReason
          fwd=- keepUp=n empty=n drop=0+0 stall=0 ready=n thermal=nominal net=6.3 restarts=0
  • fwd=- is the finding. It is item.loadedTimeRanges.last?.timeRangeValue, and the
    only way it prints - is an EMPTY array. So this is not a thin buffer or a starved one.
    Twenty seconds in, the item holds no loaded range at all.
  • empty=n does not contradict that. isPlaybackBufferEmpty is AVPlayer's answer about
    the ITEM, and an item that has fetched but placed nothing reads exactly this way (AE#418:
    a fetch is not a placement). The AE#440 line's ahead 0.00s, empty=false is the same pair
    seen from the other sampler.
  • ready=n is playerLayer.isReadyForDisplay, not the item status, so it agrees:
    nothing has been placed to display.
  • net=6.3 means an access-log entry exists, i.e. bytes did transfer. Whatever is wrong
    is not that AVPlayer asked for nothing.
  • One correction to the report: size=1920x1080 fps=50.0 decoder=VideoToolbox H264 (HW) is
    published by the engine's own probe of the upstream TS (AetherEngine+Probe
    activeVideoDecoderLabel), not by AVPlayer. It is not evidence that AVPlayer parsed a
    segment.

Put together: the item fetched, and placed nothing on its axis, and kept doing that for
twenty seconds while the loopback answered.
That is a much narrower shape than "the join
starts late", and it is not a shape any depth gate can produce.

The first-serve path is not the discriminator

The reported correlation is over two channels: the one served AT the holdback takes the
plain path and wedges, the one served BELOW it takes the fastZap bounded start grace and
plays. Downstream of waitForFirstLiveSegment those two paths differ in the log note and in
nothing else. Both call sealLiveTargetDuration on the same value, and every playlist after
the first is built the same way.

Measured rather than argued. Synthetic 1080p50 H.264 at 6 Mbps + AAC, GOP 250 (so 5.000 s
segments), --fast-zap, liveJoinStartsImmediately on by default, five arms:

arm origin first serve outcome
A 1x after a 15 s backlog 3 segments / 15.000s >= 15.000s holdback (TARGETDURATION 5s) plays, timeControlStatus=playing t+2.53s
C 150 s backlog, then 1x same line, served after 0.058 s plays at t+0.24s
BIG as A, item axis at 95258.00 s same line plays at t+0.24s
R18 sustained 1.8x, 60 s same line plays, ends 49 s behind live, never holds
COMBO sustained 1.6x + axis 95258.00 same line plays through 60 s

So the exact first-serve line quoted in the report, 3 segments / 15.000s >= 15.000s holdback (TARGETDURATION 5s), is reproducible here five ways and joins every time. Serving
at exactly 3 x TARGETDURATION is a boundary, but it is not this.

Two suspects that arm R18 and arm BIG exist to kill, because both were worth taking
seriously:

  • A live edge that outruns wall clock. The report's own intermittency points at a race
    (startup routed +946ms wedges, +11182ms plays), and the producer really is running
    ahead: 1820 written packets over 20.07 s is about 1.59x realtime measured against a
    paced arm here. aetherctl live could not drive that shape at all, since --realtime is
    exactly 1x and an unpaced fixture hands over one burst and stops, so --realtime-rate X
    now holds the origin at X times wall clock for the whole run (f3775c1). At 1.6x and 1.8x
    sustained the join is fine. The viewer falls further and further behind live, which is its
    own honesty question, but the rate rolls and stays rolled.
  • The item axis near the 33-bit PTS wrap. clk=95258.48 is 185 s below 2^33 / 90000 = 95443.72 s, and at 90 kHz that is 8.573e9 ticks, past UINT32_MAX. A 32-bit tfdt would
    have explained every observable in this report exactly. Arm BIG and arm COMBO put the
    fixture's own axis there (prodShift=95258.00s) and both play, so the axis survives that
    value end to end.
  • The pump is healthy on the reported numbers too: packetsRead=3501 packetsWritten=1820 is
    a ratio of 1.923, against 1.937 / 1.937 / 1.938 in three healthy runs here.

What ships now, because the next capture should not need this thread

ahead 0.00s is two different facts and the line printed one word for both.
contiguousBufferedEnd returns the playhead itself whenever no range touches it, so an item
that has placed NOTHING and an item holding media somewhere ELSE collapse onto the same
number, with empty=false true in both. Those need opposite investigations. The
discriminating fact was already in the capture, in a 1 Hz verbose line from a different log
that nobody reads against this one.

The refusal and the witness account now name which zero they measured (1ad185c):

AE#440 live join: leaving the stall-avoidance wait alone (buffer ahead 0.00s, empty=true,
  floor 1.50s); the item holds no loaded range at all, so nothing has been placed on its
  axis since it was mounted (playhead 0.00s)

and, where a range exists but not at the playhead, the count and the signed distance to the
nearest one, which is the reading that separates "the fetch never landed" from "the item and
the playlist disagree about where the media is". Only when the cushion reads zero; a real
depth still leads with the number the decision is taken on.

What would decide it

Not released yet. Three things from a failing session, all of which the existing build
already produces:

  1. Every [HLSLocalServer] line, with timestamps. The report quotes one 200 for
    /seg6.mp4. What matters is the whole sequence: whether init.mp4 was fetched, which
    segment indices were asked for in what order, and whether any request was held rather than
    answered. An item parked at the start of the window that only ever asks for the newest
    segment is a different bug from one that fetches seg0 and places nothing.
  2. item.errorLog() and item.error at the moment the watchdog fires. restarts=0 and
    stall=0 say the engine saw nothing; AVFoundation may have.
  3. One A/B with LoadOptions.liveBlockingReload = false. That is the arm that separates
    "AVPlayer stopped fetching because it is in low-latency blocking-reload mode" from
    everything else, and it is a one-line change on the host side.

And if the #EXT-X-MAP line in a failing session ever names init1.mp4 rather than
init.mp4, say so: an init version change inside the first window would be a third mechanism
again and is worth ruling in or out before anything else.

Thanks for the capture. The three logs in it are what made the first pass possible at all,
and the correction above about size= / decoder= is the only thing in it that was reading
further than it can.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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