How should live latency be measured and maintained with AVPlayer (HLS / LL-HLS)?

We keep live playback at a consistent distance from the live edge using small playback rate adjustments, with a target range based on recommendedTimeOffsetFromLive. Since the live edge is not exposed by AVPlayer, we currently fall back to seekableTimeRanges.end as our best approximation.

  1. What should be treated as the live edge, and how should the current live latency be measured?
  2. Is rate adjustment the appropriate way to hold a target latency? While playing above 1.0x, the playhead can reach the seekable end, at which point AVPlayerItemDidPlayToEndTime fires and halts the live stream. How can we guard against ?
  3. Does any of this differ between regular HLS and LL-HLS?

A clear statement of the intended contract here would resolve a lot of uncertainty.

Thanks in advance.

1.What should be treated as the live edge, and how should the current live latency be measured?

seekableTimeRanges.end is a good working approximation of the live edge — it represents the furthest point in the stream the player can seek to

Live latency at any moment is therefore (Note:  Live latency is playhead distance from Live Edge)

##  let latency = playerItem.seekableTimeRanges.last
   .map { CMTimeRangeGetEnd($0.timeRangeValue) }
   .map { $0 - playerItem.currentTime() }

2. Is rate > 1.0 the right mechanism? What happens when the playhead reaches the end?

Yes — playing above 1.0x to reduce live latency is a supported pattern.

The concern about AVPlayerItemDidPlayToEndTimeNotification firing does not apply here. For live streams (duration == kCMTimeIndefinite) this notification is only posted if forwardPlaybackEndTime is explicitly set on the item. Reaching seekableTimeRanges.end during live playback at rate > 1.0 does not trigger it.

from iOS/macOS 26.4 onwards, when the playhead reaches the live edge at rate > 1.0, the player automatically steps the rate back to 1.0, no guard needed.

3. Differences between regular HLS and LL-HLS

The fundamental model is the same for both — seekableTimeRanges, configuredTimeOffsetFromLive, recommendedTimeOffsetFromLive, and the rate > 1.0 pattern all apply.

The differences are in scale: seekableTimeRanges.end granularity Regular HLS : Advances by full segment. LL-HLS : Advances by partial segment

For LL-HLS, seekableTimeRanges.end moves forward continuously at part traget intervals, so the rate > 1.0 catch-up converges smoothly. For regular HLS, the boundary advances in segment-sized steps, so the playhead catches up in larger increments.

Summary :- the intended contract

  1. Live latency = seekableTimeRanges.end − currentTime(). This is slightly conservative relative to the true live edge but is the closest approximation.
  2. Target latency :- set via configuredTimeOffsetFromLive. Follow recommendedTimeOffsetFromLive for network-adaptive targets.
  3. Catch-up :- playing at rate > 1.0 is supported. From (iOS/macOS 26.4+) the player automatically steps back to 1.0 when the playhead reaches the live edge,. No additional guard is needed.
  4. AVPlayerItemDidPlayToEndTimeNotification does not fire during normal live playback. Only fires if forwardPlaybackEndTime s explicitly set.
  5. LL-HLS vs regular HLS :- same API surface, different numeric scales. Always use recommendedTimeOffsetFromLive rather than a hardcoded offset.

when it comes to live streaming latency, it's all about how quickly you can get the video from the source to the viewer's screen. Think of it like a river – the shorter and faster the flow, the lower the latency!For HLS (HTTP Live Streaming) and especially LL-HLS (Low-Latency HLS), there are a few key things to consider:Chunk Size: This is like the size of the water droplets in our river analogy. Smaller chunks mean faster delivery, reducing latency. LL-HLS specifically

I would separate distance from AVPlayer’s live edge from true end-to-end live latency.

  1. What should be treated as the live edge?

From the public AVFoundation API, seekableTimeRanges.end is the practical reference for the player-visible live edge.

The current offset from that edge can therefore be measured as:

let currentTime = playerItem.currentTime()
let seekableEnd = playerItem.seekableTimeRanges.last?.timeRangeValue.end

let offsetFromLive =
    seekableEnd.map { CMTimeGetSeconds($0 - currentTime) }

I would describe this value as distance to the seekable edge, rather than absolute live latency.

recommendedTimeOffsetFromLive should be treated as AVPlayer’s recommended target offset from live, not as a measurement of the current offset.

If the stream contains correctly aligned EXT-X-PROGRAM-DATE-TIME, currentDate() can additionally be used to estimate wall-clock latency:

wallClockLatency = now - currentDate()

That is a different metric from distance to the seekable edge.

  1. Is playback-rate adjustment appropriate for maintaining the target offset?

Yes. A temporary playback rate above 1.0 is a supported way to catch up toward live.

AVPlayer exposes AVPlayer.RateDidChangeReason.playheadReachedLiveEdge, which indicates that a rate greater than 1.0 was automatically changed back to 1.0 when the playhead reached the live edge.

A control loop should still avoid continuously driving the player all the way to the seekable boundary. A small deadband around the target works better:

offset > target + tolerance
    -> slightly increase playback rate

offset within target ± tolerance
    -> rate = 1.0

offset < target - tolerance
    -> do not continue catching up

For example:

let error = offsetFromLive - targetOffset

if error > tolerance {
    player.rate = 1.03
} else {
    player.rate = 1.0
}

I would not use AVPlayerItemDidPlayToEndTime as a live-edge signal.

For an ongoing live playlist without EXT-X-ENDLIST, with no intentional forwardPlaybackEndTime, receiving AVPlayerItemDidPlayToEndTime should be treated as something to investigate separately rather than normal live-edge behavior.

configuredTimeOffsetFromLive and automaticallyPreservesTimeOffsetFromLive also solve different parts of the problem:

  • configuredTimeOffsetFromLive defines the desired offset when starting or seeking to live.
  • automaticallyPreservesTimeOffsetFromLive helps preserve the existing relative position through buffering.
  • Neither exposes the continuously updated current live latency.
  1. Regular HLS vs LL-HLS

I would use the same AVFoundation-level model for both:

current player-relative offset
    =
seekableTimeRanges.end - currentTime

The main difference is how the live window advances.

With regular HLS, the seekable edge generally advances as complete segments become available.

With LL-HLS, partial segments allow that edge to advance at a finer cadence and permit a much smaller practical live offset.

I would therefore avoid calculating the desired latency directly from segment duration or PART-TARGET. Let recommendedTimeOffsetFromLive provide AVPlayer’s recommended target, and use the seekable range only to measure the current position relative to the player-visible live edge.

For telemetry, I would keep these as separate metrics:

distanceToSeekableEdge
wallClockLatency
recommendedTimeOffsetFromLive
How should live latency be measured and maintained with AVPlayer (HLS / LL-HLS)?
 
 
Q