Repository navigation
A VOD source whose audio ends before its video stops at the audio's end on the loopback route: the rest of the picture is never shown #847
Description
What happens
A Matroska VOD file whose audio tracks end 25 s before its video reaches .ended where the audio stops. The remaining video, silent end credits, never plays.
Other players show the tail in silence. This file is a hybrid: the video is a BBC iPlayer WEB-DL and the audio comes from the HD Blu-ray, which is probably why their lengths differ. Hybrids like this are common, so a gap between the end of the audio and the end of the video isn't unusual.
The file
HEVC 2160p 25 fps HLG, TrueHD 7.1 Atmos (index 1, selected) and AC-3 5.1 (index 2), MKV, about 10.4 GB. ffprobe on the last packets:
| Stream | Last packet pts |
|---|---|
| video (v:0) | 3473.44 s (+0.04) |
| TrueHD (a:0) | 3448.487 s |
| AC-3 (a:1) | 3448.48 s (+0.032) |
| format duration | 3473.52 s |
The video has no gap in its last 75 s (1878 packets from 3398.4 s). The frames after 3448.5 s are real picture: end credit cards.
Environment
AetherEngine 7.44.0, tvOS 27.0, Apple TV 4K (2nd gen). Native loopback route, audioDelivery == .bridged (TrueHD → FLAC channel bridge), default LoadOptions apart from host display settings. The AC-3 track ends at the same place, so choosing it doesn't help.
To reproduce
Play any VOD file whose audio ends well before its video, and seek to about 15 s before the audio's end. state becomes .ended at the audio's end, not the video's.
What the engine logs
Played into the end credits (TrueHD selected), with EngineLog forwarded to the console. Segment fetches left out:
[HLSSegmentProducer] pump finished: reason=eof packetsRead=108979 packetsWritten=2838 lastError=0 elapsed=59836ms cacheCount=38
[NativeAVPlayerHost] #1 didPlayToEndTime
[NativeAVPlayerHost] #1 AE#287 premature end: playhead=3448.474s duration=3473.520s seekableEnd=3473.520s loadedEnd=3448.417s; 25.0s of the presentation lies past the end AVPlayer reported, re-seeking in place (attempt 1)
[NativeAVPlayerHost] #1 rate=0.0
[NativeAVPlayerHost] #1 timeControlStatus=paused reason=- t+101.59s
[HLSVideoEngine] declareTarget backward jump 866 -> 862: resident through seg866 (march front 866), no restart
[NativeAVPlayerHost] #1 didPlayToEndTime
[NativeAVPlayerHost] seek completion target=3448.47 actual=3448.47 finished=true landed=true current=true
[NativeAVPlayerHost] #1 rate=1.0
[NativeAVPlayerHost] #1 AE#287 resumed: rate=1.0 t=3448.473s
state became .ended at 3448 s, between the second didPlayToEndTime and AE#287 resumed. No #169 tail-park line.
What the log shows
- The producer isn't stopping early. It reached whole-file EOF about 40 s before the playhead got there, so the segments after 3448.5 s exist, with video and no audio.
- AVPlayer stops loading where the audio stops.
loadedEnd=3448.417sis the audio's last sample, whileseekableEndis the full 3473.52. AVPlayer appears to count only the stretch where every muxed track has samples as loaded, and it firesdidPlayToEndTimewhen the audio renderer runs dry. - AE#287 is the mirror case. Its doc comment (
NativeAVPlayerHost.swift:1884) covers audio outlasting video, where the in-place re-seek re-arms the renderers. Here the re-seek can't help, because nothing after the seek point has audio. AVPlayer firesdidPlayToEndTimeagain before the seek even completes. - That second end latches
.endedwhile the first recovery is still running. The re-entrantrecoverFromPrematureEnd()sees the same playhead, soprematureEndRecoveryMinProgressSeconds(0.5 s,AetherEngine.swift:3482, 3527-3528) refuses it anddidReachEndis set (NativeAVPlayerHost.swift:882-884). The first recovery then logsresumed, but.endedis terminal.
A possible fix
Pad the audio with silence through the end of the video, so every muxed segment carries audio and AVPlayer's loaded range runs to the end:
- On the bridged path, an
AudioBridgecall writing zeroed PCM into the FIFO, continuing fromnextEncoderPTS, called before each cut once the audio is known to have ended, and at EOF beforebridge.flush(). - "Audio has ended" could come from an audio end learned by the first pump to reach EOF (the producer reads well ahead), with a gap heuristic as a fallback. A producer restarted inside the tail would need the learned value.
- Stream copy (E-AC-3) has no encoder to make silence, so it may need a separate approach, or none at first.
Rebuilding the session video-only at the premature end also works in principle, but costs a rebuffer at the credits and doesn't cover seeking into the tail.
Expected
Playback continues, silent, to the end of the longest selected stream (3473.4 s here), then .ended.
Activity