Skip to content

Selecting an embedded text subtitle doubles the bytes read from a remote source (#151 forward prefetch), MKV and MP4 alike #1015

Closed

Description

@samimt55

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

  1. superuser404notfound commented on Nov 10, 2026

    @superuser404notfound
    Owner

    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 = 0 now also stops the forward prefetch, for text and bitmap tracks. Embedded subtitles then come from the main reader alone. It still starts when softwareSubtitleDelaySeconds goes negative (so your .whenAdvancing case 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.

  2. added a commit that references this issue on Nov 10, 2026
  3. superuser404notfound commented on Nov 10, 2026

    @superuser404notfound
    Owner

    #1017 is merged, so the change is on main now (0624eb9) and will be in the next release. The test branch is gone; please test against main instead.

  4. samimt55 commented on Nov 10, 2026

    @samimt55
    ContributorAuthor

    Thanks for the quick fix. I ran the same Jellyfin setup as in the report against main at 0624eb9 (tvOS 27 simulator, Jellyfin 12.4 on the LAN with static=true, 60 s of playback from 30 s, bytes from SourceNetworkDiagnostics):

    Source Subtitles off Embedded SRT, reach 60 (default) Embedded SRT, reach 0
    MKV 112.5 MB 244.9 MB 107.0 MB
    MP4 (mov_text) 107.4 MB 242.1 MB 110.8 MB

    With reach 0 no #151 line appears at all, and every cue arrived on time in all four subtitle runs: 14 of 14 cues between 34 and 86 s were in subtitleCues at 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 logged cancelled (reason=noAdvanceReach).

    Thanks again, this covers our case.

  5. superuser404notfound commented on Nov 10, 2026

    @superuser404notfound
    Owner

    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.

  6. superuser404notfound commented on Nov 10, 2026

    @superuser404notfound
    Owner

    Released in 7.62.1 (015e6067). Sodalite and AetherPlayer are bumped to it.

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions