Skip to content

The retained subtitle window travels ahead of the playhead so a drain that keeps pace still leaves nothing to draw #496

Description

@RadicalMuffinMan

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.

  1. Load a long file (the reporting asset runs about 25 minutes) with an SRT track, direct play, default LoadOptions
  2. Let it play without touching the transport for a minute or two
  3. Subtitles stop appearing, while decoding stays healthy
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions