What happened?
Subtitles drop out for 20 to 40 seconds at a time and then come back on their own, in the middle of otherwise healthy playback. Four reported instances so far.
The cause is that the retained cue store travels about one drain-lead window ahead of the playhead and stays there, so the region the viewer is actually in never gets decoded. The overlay is correct to draw nothing. There's just nothing covering the clock.
Steps to reproduce
Nothing exotic in the call sequence, it's the steady state that drifts.
- Load a long file (the reporting asset runs about 25 minutes) with an SRT track, direct play, default LoadOptions
- Let it play without touching the transport for a minute or two
- Subtitles stop appearing, while decoding stays healthy
- Seek, or anything that nudges the transport, and they come back for a while before drifting off again
A reporter found step 4 on his own by pressing up to show the progress bar.
AetherEngine version or commit SHA
6.42.0
Host app
Custom / my own integration
Platform
tvOS
OS version
tvOS 26.6
Device / chip
Apple TV 4K 2nd Generation
Playback path
Software (dav1d) host
Source media (for playback bugs)
SRT, both embedded and external, same behavior either way. Direct play from Jellyfin over LAN, no transcode.
The video side doesn't seem to matter, and I don't think ffprobe output would tell you much here since the failure is in the cue store rather than the media. The one relevant property is cue density: the reporting asset's track runs about one cue every 6 seconds.
Error codes / log lines
2.5.0 t=30s subCues=12 prefetch=park prefetchLead=74.5s pumpParked=1
2.5.1 t=30s subCues=9 prefetch=park prefetchLead=74.7s pumpParked=1
2.5.1 t=60s subCues=13 prefetch=yield prefetchLead=-215.8s pumpParked=1
outcome=harvestHole gapAt=954.12
(No thrown errors and no AVFoundation codes. The engine already spots the result and names the gap, it just doesn't repair it)
Anything else
subtitleCueRetentionSeconds is 300 and the track on the reporting asset runs about one cue every 6 seconds so a window over the playhead should hold something like 50 cues. The memprobe lines show it holding a fraction of that, and holding it in the wrong place.
2.5.0 t=30s subCues=12 prefetch=park prefetchLead=74.5s pumpParked=1
2.5.1 t=30s subCues=9 prefetch=park prefetchLead=74.7s pumpParked=1
2.5.1 t=60s subCues=13 prefetch=yield prefetchLead=-215.8s pumpParked=1
At the t=30s probe the playhead is 880.1 and the store's nine cues are 852.375, 856.083, 859.666, 862.166, 871.333, 875.250, 925.166, 929.041 and 940.083. Six of them ended before 877 and the other three don't start until 925. Nothing covers 880. That's the blank screen, and the count matches the probe exactly. The lead of each delivered cue over the playhead climbs and then pins at the drain lead:
cue 852.375 playhead 852.000 lead 0.375
cue 871.333 playhead 852.200 lead 19.133
cue 925.166 playhead 865.600 lead 59.566
cue 940.083 playhead 880.100 lead 59.983
cue 969.333 playhead 909.600 lead 59.733
cue 1015.916 playhead 956.000 lead 59.916
subtitleDrainLeadSeconds is 60, so the harvest settles exactly one lead window ahead and stays there. The only cues in the store are whatever the forward prefetcher picked up at its frontier and the region the viewer is in never gets decoded.The engine already spots the result. One delivery tick reports outcome=harvestHole gapAt=954.12. It names the gap but doesn't repair it.
A seek bumps seekGen and kicks off a reason=reconstruction drain that re-harvests around the playhead, republishing cues that do cover it. The lead resets to somewhere between -11s and +37s and then climbs back to 60. That lines up with what a reporter noticed on his own: pressing up to show the progress bar brings subtitles back for a few seconds. Anything that nudges the transport re-anchors the drain window onto the playhead, then the pipeline races ahead again.
prefetchLead hitting -215.8s with prefetch=yield is the other end of it. The prefetcher ends up 216 seconds behind the playhead and then stops doing useful work, harvesting 42 packets in 30 seconds. Once it's behind and yielding, nothing fills the hole. Whatever re-anchors it after a seek looks like the behavior the steady state needs.
I couldn't work out from the logs alone why the lead goes negative. That part probably needs the prefetcher internals or a device.
Reproduced on host builds carrying engine 6.42.0 and I checked 6.68.4 before filing. The three governing constants are unchanged, harvestHole is still named without being repaired, and the only subtitle commits since 6.42.0 are the metered origin prefetch hold, the PGS placeholder log text, the live TARGETDURATION seal and the module renames. So I don't think this is already fixed, though I haven't run 6.68.4 on a device to confirm.
d596461b already says the thing this needs, for a different reason:
A paced or single-slot origin keeps the pump's tap-fed subtitles and yields the speculative far-ahead window.
That's the right trade here too. It's gated on isPaced or requiresSerialRequests, so it only fires when an origin has been handing out 429s. A plain LAN Jellyfin server is neither, which may be why this hasn't turned up in your own testing. Preferring playhead-local cues when the retained window doesn't cover the playhead looks like the same rule under a second condition rather than a new mechanism.
What happened?
Subtitles drop out for 20 to 40 seconds at a time and then come back on their own, in the middle of otherwise healthy playback. Four reported instances so far.
The cause is that the retained cue store travels about one drain-lead window ahead of the playhead and stays there, so the region the viewer is actually in never gets decoded. The overlay is correct to draw nothing. There's just nothing covering the clock.
Steps to reproduce
Nothing exotic in the call sequence, it's the steady state that drifts.
A reporter found step 4 on his own by pressing up to show the progress bar.
AetherEngine version or commit SHA
6.42.0
Host app
Custom / my own integration
Platform
tvOS
OS version
tvOS 26.6
Device / chip
Apple TV 4K 2nd Generation
Playback path
Software (dav1d) host
Source media (for playback bugs)
SRT, both embedded and external, same behavior either way. Direct play from Jellyfin over LAN, no transcode.
The video side doesn't seem to matter, and I don't think ffprobe output would tell you much here since the failure is in the cue store rather than the media. The one relevant property is cue density: the reporting asset's track runs about one cue every 6 seconds.
Error codes / log lines
Anything else
subtitleCueRetentionSecondsis 300 and the track on the reporting asset runs about one cue every 6 seconds so a window over the playhead should hold something like 50 cues. The memprobe lines show it holding a fraction of that, and holding it in the wrong place.At the
t=30sprobe the playhead is 880.1 and the store's nine cues are 852.375, 856.083, 859.666, 862.166, 871.333, 875.250, 925.166, 929.041 and 940.083. Six of them ended before 877 and the other three don't start until 925. Nothing covers 880. That's the blank screen, and the count matches the probe exactly. The lead of each delivered cue over the playhead climbs and then pins at the drain lead:subtitleDrainLeadSecondsis 60, so the harvest settles exactly one lead window ahead and stays there. The only cues in the store are whatever the forward prefetcher picked up at its frontier and the region the viewer is in never gets decoded.The engine already spots the result. One delivery tick reportsoutcome=harvestHole gapAt=954.12. It names the gap but doesn't repair it.A seek bumps
seekGenand kicks off areason=reconstructiondrain that re-harvests around the playhead, republishing cues that do cover it. The lead resets to somewhere between -11s and +37s and then climbs back to 60. That lines up with what a reporter noticed on his own: pressing up to show the progress bar brings subtitles back for a few seconds. Anything that nudges the transport re-anchors the drain window onto the playhead, then the pipeline races ahead again.prefetchLeadhitting -215.8s withprefetch=yieldis the other end of it. The prefetcher ends up 216 seconds behind the playhead and then stops doing useful work, harvesting 42 packets in 30 seconds. Once it's behind and yielding, nothing fills the hole. Whatever re-anchors it after a seek looks like the behavior the steady state needs.I couldn't work out from the logs alone why the lead goes negative. That part probably needs the prefetcher internals or a device.
Reproduced on host builds carrying engine 6.42.0 and I checked 6.68.4 before filing. The three governing constants are unchanged,
harvestHoleis still named without being repaired, and the only subtitle commits since 6.42.0 are the metered origin prefetch hold, the PGS placeholder log text, the live TARGETDURATION seal and the module renames. So I don't think this is already fixed, though I haven't run 6.68.4 on a device to confirm.d596461balready says the thing this needs, for a different reason:That's the right trade here too. It's gated on
isPacedorrequiresSerialRequests, so it only fires when an origin has been handing out 429s. A plain LAN Jellyfin server is neither, which may be why this hasn't turned up in your own testing. Preferring playhead-local cues when the retained window doesn't cover the playhead looks like the same rule under a second condition rather than a new mechanism.