I would separate distance from AVPlayer’s live edge from true end-to-end live latency.
- 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.
- 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.
- 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