Repository navigation
Description
Hi, thanks for the secondary subtitle channel, it works very well for us. One thing we measured while testing it: as soon as an embedded subtitle is selected on a remote (HTTP) source, the session reads roughly twice as much from the origin, whatever the container.
What happens. Selecting an embedded subtitle starts the #151 forward prefetch: a second, subtitle-only side demuxer on the same URL (reader label prefetch) that keeps the packet store filled to playhead + 75 s (subtitleDrainLeadSeconds 60 + subtitleForwardPrefetchLeadMarginSeconds 15). The subtitle packets are interleaved with the video, so this reader downloads the video bytes a second time. The source already says so, next to sideReaderLinkGate in AetherEngine.swift (7.59.0, lines 1626 to 1627): "On Matroska a side reader is a second full copy of the stream". The link gate keeps the video path first, which is good, but the extra bytes are still read.
Measurements (7.59.0, tvOS 27 simulator, a 4-minute 1080p H.264 file at 6.4 Mbit/s with three SRT tracks, served over HTTP by a Jellyfin 12.4 server on the LAN with static=true, 60 s of playback from 30 s, bytes summed from SourceNetworkDiagnostics completed and abandoned events):
| Source | Subtitles | Bytes from the origin |
|---|---|---|
| MKV | off | 104.5 MB |
| MKV | embedded SRT selected | 243.6 MB |
| MKV | embedded SRT, maximumSubtitleOffsetSeconds = 0 |
246.5 MB |
| MKV | same track as an external sidecar (the server extracts it, 3.8 KB) | 106.0 MB |
MP4 (same streams, mov_text, faststart) |
off | 106.6 MB |
| MP4 | embedded mov_text selected |
245.1 MB |
The log shows #151 forward prefetch started: streams=[3, 4, 5] pacing=0 anchor=3 startAt=30.00s lead=75.0s and six to eight prefetch conn start windows of 32 MB during the minute. On a real remote 4K HEVC MKV over the internet (Apple TV 4K, 3rd generation) it was 479 MB in the first minute with subtitles off against 934 MB with one embedded SRT selected.
Why it matters for us. Our app applies the subtitle delay itself (setSoftwareSubtitleDelay and our own overlay), and most of the time the delay is zero or positive, which the pump's own harvest already covers. On metered or slower connections the doubled read is the largest single cost of turning subtitles on. As far as we can see there is no public switch: shouldRunSubtitleForwardPrefetch only looks at live, embedded drain targets and the source, and the hold only applies to paced or one-request origins.
Proposal. A load option (correctable mid-session if possible), for example:
public enum SubtitleForwardPrefetch: Sendable { case automatic, off, whenAdvancing }
LoadOptions.subtitleForwardPrefetch = .automatic // today's behaviour, the default.off: never start the forward prefetch; embedded subtitles come from the pump's harvest only. A host that accepts no advance offset, or only small ones, loses nothing..whenAdvancing: start it only while the subtitle delay is negative (softwareSubtitleDelaySeconds < 0), with the lead sized to that delay rather than a fixed 75 s, and stop it when the delay returns to zero or above.
Either would let a host keep one connection per session for text subtitles. We are happy to test a branch on the same setup and report back with the same numbers.
Activity
superuser404notfound commentedon Nov 10, 2026
Thanks for the measurements, they made this easy to pin down.
PR #1017 uses the switch you already tried instead of a new option:
maximumSubtitleOffsetSeconds = 0now also stops the forward prefetch, for text and bitmap tracks. Embedded subtitles then come from the main reader alone. It still starts whensoftwareSubtitleDelaySecondsgoes negative (so your.whenAdvancingcase is covered), or when the OCR worker is armed, and both directions work mid-session. A shorter lead would not have helped: the reader is sequential, so it only reads the same bytes later.On my side, over HTTP with a 720p MKV and one embedded SRT, 40 s of playback read 92.9 MB with the default reach and 53.8 MB with reach 0 (53.4 MB with subtitles off), and every cue still arrived on time. If you can run your Jellyfin setup against the branch
fix/issue-1015-prefetch-off-at-zero-reach, I'd be glad to see your numbers.Merge pull request #1017 from superuser404notfound/fix/issue-1015-pre…
superuser404notfound commentedon Nov 10, 2026
#1017 is merged, so the change is on
mainnow (0624eb9) and will be in the next release. The test branch is gone; please test againstmaininstead.samimt55 commentedon Nov 10, 2026
Thanks for the quick fix. I ran the same Jellyfin setup as in the report against
mainat 0624eb9 (tvOS 27 simulator, Jellyfin 12.4 on the LAN withstatic=true, 60 s of playback from 30 s, bytes fromSourceNetworkDiagnostics):mov_text)With reach 0 no
#151line appears at all, and every cue arrived on time in all four subtitle runs: 14 of 14 cues between 34 and 86 s were insubtitleCuesat the first 100 ms sample inside their display window.The negative-delay path also works mid-session. With reach 0, setting
setSoftwareSubtitleDelay(-20)after 8 s logged#151 forward prefetch started ... startAt=37.57s lead=75.0s, the cue due at playhead + 20 s was already held (cues reached 56 s ahead of the playhead), and setting the delay back to 0 loggedcancelled (reason=noAdvanceReach).Thanks again, this covers our case.
superuser404notfound commentedon Nov 10, 2026
Thanks for running the full matrix, MKV and MP4 both landing at the subtitles-off level with every cue on time is exactly the result I was hoping for, and the mid-session check of the negative-delay path is a nice extra.
The change ships in the next AetherEngine release (0624eb9 on
main). Since this covers your case, I'm closing this one.chore(release): prep 7.62.1 - version sweep, changelog folds #1015, #929
superuser404notfound commentedon Nov 10, 2026
Released in 7.62.1 (
015e6067). Sodalite and AetherPlayer are bumped to it.fix(subtitles): a zero offset reach stops the forward prefetch's seco…