6.5.6 - A VOD outlives the end AVPlayer calls
Reported by @edde746 on #287. One correction to the report is load-bearing for where the defect turned out to be: the served playlist is not video-anchored. Its EXTINF sum equals the container duration to the millisecond, so duration and the manifest already agreed, and the early end comes from the consumer rather than from what the engine advertises.
Fixed
- A VOD whose audio track outlives its video no longer ends when the picture runs out. AVPlayer fires
didPlayToEndTimethe moment its video renderer runs dry, and the engine forwarded that as an organic finish. On the reporter's dual-audio BDRip, whose selected English AAC runs 53 s past the last video sample, the item stopped 53 s early; because.endedis terminal, the audio-only tail was then unreachable for the rest of the session, and a host that rebuilt the session at the boundary got the same instant end again. An end that lands more than a second inside the range AVPlayer itself still reports as seekable is now refused: the item is re-seeked to the same position and resumed, which re-arms the renderers and plays the tail out to an organic end at the real duration with nothing dropped.
Measurement
Synthesized MKV, 60 s HEVC video and 113 s AAC audio, direct-played through the loopback path under aetherctl play. The bad arm reproduced in every run here rather than the reporter's 1 in 6:
| fixture | 6.5.5 | 6.5.6 |
|---|---|---|
| 53 s tail (duration 113.02) | ends 60.03 | 112.97, organic |
| 8 s tail (duration 68.02) | ends 60.03 | 67.98, organic |
| 2 s tail (duration 62.02) | ends 60.03 | 61.98, organic |
| equal-length A/V (duration 60.02) | 59.97 | 59.97, path never armed |
| resume at the boundary (60.03) | 112.97 |
One recovery attempt each. The 2 s and 8 s arms are what rule out the tail length as the trigger: all three end at the same 60.03, which is where the video runs out.
The served playlist for the 53 s fixture is 14 x 4.004 + 56.967 = 113.023, the container duration exactly. The final segment is one long slot spanning the audio-only tail, because the keyframe planner ends it at sourceDurationSeconds.
Why the seekable range and not the loaded one
At the instant of the premature end AVPlayer has already trimmed loadedTimeRanges back to the exhaustion point, [22.34, 60.01] against a playhead of 60.03, while seekableTimeRanges still reports [0, 113.02]. So the loaded range corroborates the mistake and only the seekable range refutes it. This is the opposite of #169, where the loaded range is the discriminator, and it is also why the condition is hard to state honestly from outside the engine: everything AVPlayer exposes about buffering at that moment agrees with the early end.
play() without the seek was measured as well: the rate returns to 1.0 and the clock stays frozen at the boundary indefinitely. The re-seek is load-bearing.
Bounds
Suppressing a terminal event is bounded on four sides: at most three recoveries per item, each requiring the playhead to have moved at least 0.5 s since the last one, live sessions excluded, and a shortfall threshold deliberately above endOfMediaEpsilonSeconds so the #169 tail park owns the last half second undisturbed. A real serve failure never reaches this path, failing the item with failedToPlayToEndTime / -12889 instead. A source that genuinely cannot continue costs one re-seek and then completes exactly as it did before.
The transient pause AVPlayer takes across the re-seek is swallowed, so a consumer never sees the session bounce through .paused.
Verification line
[NativeAVPlayerHost] #<n> AE#287 premature end: playhead=<t>s duration=<d>s seekableEnd=<s>s loadedEnd=<l>s; <x>s of the presentation lies past the end AVPlayer reported, re-seeking in place (attempt <k>)
1462 tests in 217 suites, 11 of them new. Strict Swift concurrency clean, tvOS and iOS builds green.
Tooling
aetherctl play gains --start-position, the resume anchor serve already had. The reporter's hard-park arm, a rebuilt session opening exactly at the video-exhaustion boundary, is not reproducible without it.
Compatibility
No API changes.