A VOD outlives the end AVPlayer calls while its own seekable range runs on (#287) - #291
Merged
Merged
Conversation
…able range runs on An MKV whose selected audio track outruns its video track ends tens of seconds early: AVPlayer fires didPlayToEndTime the moment the video renderer runs dry, and the engine forwards that as organic completion. `.ended` is terminal, so the audio-only tail is unreachable for the rest of the session and a host retry loop that rebuilds at the boundary can never escape it (AE#287). Reproduced deterministically on macOS with a synthesized 60 s-video / 113 s-audio MKV: the end fires at 60.03 s of a 113.02 s presentation, and identically for a 2 s, an 8 s and a 53 s tail, so the trigger is the video exhaustion and not the length of the tail. The playlist is not the culprit: its EXTINF sum equals the container duration to the millisecond (14 x 4.004 + 56.967 = 113.023). The engine cannot talk AVPlayer out of that call, so it stops repeating it. An end that lands more than a second inside the range AVPlayer itself still reports as seekable is refused, and the item is re-seeked to the SAME position and resumed. That re-arms the renderers and the tail plays out to an organic end at the real duration with nothing dropped; all three fixtures now finish at their true end after exactly one recovery, and an equal-length file never enters the path. The witness is the seekable range, not the loaded one that carries #169: 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), so it corroborates the mistake, while seekableTimeRanges still reports [0, 113.02]. `play()` without the seek was measured too and leaves the clock frozen at the boundary indefinitely. Recoveries are capped and each one must move the playhead, so a source that genuinely cannot continue costs one re-seek and then completes exactly as it does today. `aetherctl play` gains `--start-position`, the resume anchor the reporter's hard park needs to be reproducible at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HX5zV3Fcf7Nzq4DYGdXvQE
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the ~53 s early stop reported in #287.
What happens
An MKV whose selected audio track outruns its video track ends tens of seconds early. AVPlayer fires
didPlayToEndTimethe moment its video renderer runs dry, and the engine forwards that as organic completion..endedis terminal (#63/#164), so the audio-only tail is unreachable for the rest of the session, and a host that rebuilds the session at the boundary gets the same instant end again.Measured, not inferred
Reproduced deterministically on macOS with
aetherctl playon a synthesized MKV (60 s HEVC video, 113 s AAC audio):didPlayToEndTimeTwo things this rules out. The playlist is not video-anchored: its EXTINF sum equals the container duration to the millisecond, so the presentation the engine advertises is self-consistent. And the tail length is irrelevant, so the trigger is the video exhaustion itself.
The fix
The engine cannot talk AVPlayer out of that call, so it stops repeating it. An end that lands more than a second inside the range AVPlayer itself still reports as seekable is refused; the item is re-seeked to the SAME position and resumed. That re-arms the renderers, and the tail plays out to an organic end at the real duration with nothing dropped.
The witness is the seekable range, not the loaded one that carries #169. At the instant of the premature end AVPlayer has already trimmed
loadedTimeRangesback to the exhaustion point ([22.34, 60.01]against a playhead of 60.03), so the loaded range corroborates the mistake instead of refuting it, whileseekableTimeRangesstill reports[0, 113.02].play()without the seek was measured too: it leaves the clock frozen at the boundary indefinitely.Bounds: recoveries are capped at 3 per item and each must have moved the playhead, so a source that genuinely cannot continue costs one re-seek and then completes exactly as it does today. A live session fails the guard twice over (explicit flag, and its seekable end is the live edge). A real serve failure never reaches this path, failing the item with
failedToPlayToEndTime/ -12889 instead.The transient pause AVPlayer takes across the re-seek is swallowed, so the engine never bounces through
.pausedfor something the viewer must not notice.Verification
Each recovering fixture takes exactly one attempt.
PrematureEndOfItemTests, written red first)aetherctl playgains--start-position, the resume anchor the reporter's hard-park arm needs to be reproducible at all.🤖 Generated with Claude Code
https://claude.ai/code/session_01HX5zV3Fcf7Nzq4DYGdXvQE