# Why Jellyfin subtitles delay playback

> Films with embedded subtitles took between 5.7 and 11.8 seconds to start in Tomo TV on Apple TV, because the player held while Jellyfin ran FFmpeg over the whole file before answering. The app now decodes the same packets it was already reading and cuts them into segments alongside the video. A CRLF quirk in Swift surfaced after real Aegisub files revealed styling that our synthetic fixtures never produced.

- ID: LAB-004
- Area: Playback engine · Subtitles
- Problem: Films with an embedded subtitle selected waited between 5.7 and 11.8 seconds before the first frame.
- Solution: The app decodes subtitle packets it was already reading and cuts them per segment, so they start with the picture.
- Status: Shipped in 2.2.6
- Published: 2026-10-05
- Author: Keiver Hernandez
- URL: https://keiver.dev/lab/jellyfin-subtitles-delay-playback
- Source: https://github.com/keiver/tomotv

Jellyfin subtitles are slow to start when the player has to wait on the server. In Tomo TV on Apple TV, picking an embedded text subtitle before pressing play produced a black screen that lasted between 5.7 and 11.8 seconds. The video itself played instantly without them. AVPlayer will not report a player item as ready until every selected media track is ready, and our subtitle track was not arriving from a fast source.

## Why Jellyfin subtitles are slow to start

Jellyfin exposes embedded text subtitles through an endpoint that returns a single WebVTT document. Before responding, Jellyfin runs FFmpeg over the whole file, demuxes the subtitle stream from beginning to end, and converts the result. That is a reasonable design for a subtitle download. In front of a play button, the player holds while the server reads the whole film. Measured across three days of server logs, the wait was 5.7 to 11.8 seconds per item. The video was not the cause and the network was not the cause.

## Subtitles cut on the video's own segment grid

Tomo TV's principle is that the server is a file host and the device does the decoding work. The on-device engine reads every packet in the file as it plays, subtitle packets included, and until this change the engine discarded them. Once we stopped discarding them, a decoder collects timed cues from each subtitle packet as the engine reads. Those cues are cut into short WebVTT segments on the same time grid as the video. AVPlayer gets its first subtitle segment at the same moment it gets its first video frame. The server is never asked about subtitles for an embedded track.

Before and after, the subtitle source changes like this:

```mermaid
flowchart LR
    A[Play pressed] --> B{subtitle source}
    B -- before --> C["Jellyfin subtitle endpoint<br/>FFmpeg reads whole file"]
    C --> D["AVPlayer waits<br/>5.7 to 11.8 s"]
    B -- after --> E["Engine read loop<br/>WebVTT segments"]
    E --> F[AVPlayer starts]
```

SubRip, mov_text, ASS and SSA formats all reach FFmpeg as ASS dialogue internally, so a single converter handles them. Bold, italic and underline are translated into the tags WebVTT supports. Positioning, animation and drawing commands that WebVTT does not carry are dropped cleanly.

## Swift reads CRLF as one Character

The synthetic tests passed. The first real ASS and SSA files from public test archives did not.

ASS stores style information in a script header. Parsing the header means splitting it into lines. We were splitting on `"\n"`. Swift reads a Windows line ending, `"\r\n"`, as a single `Character`. Splitting on `"\n"` found no line boundaries in a CRLF file and returned the whole header as one string. The style table came out empty, and every subtitle line rendered without bold or italic.

[packages/tomo-engine/ios/LocalRemuxer/AssToWebVTT.swift:220-224](https://github.com/keiver/tomotv/blob/8d75511f63afcc20a72d6c24ed98db2be02080df/packages/tomo-engine/ios/LocalRemuxer/AssToWebVTT.swift#L220-L224)

```swift
// isNewline, not "\n": Swift reads CRLF as ONE Character, so splitting a
// CRLF script on "\n" returns the whole header as a single line and the
// table comes out empty. Aegisub writes CRLF, which is most of the ASS
// in the world.
for raw in header.split(whereSeparator: \.isNewline) {
```

`\.isNewline` handles both `"\n"` and `"\r\n"`. It matters because Aegisub, a common subtitle editor, writes CRLF.

## Real files find bugs that synthetic fixtures miss

Both issues surfaced together. The CRLF split was one; the other was SSA's convention of marking an unresolved style with a leading asterisk. Our code matched that asterisk literally, so a file where every line used `*Default` found no style entry at all. Every fixture we had written was saved on a Mac with Unix line endings, and we had not written anything in that asterisk convention. Neither bug was reachable from a file we had made.

Two files from public test archives, one ASS and one SSA, caught both on the same day they were added. Synthetic fixtures let you test what you already know. Files someone else made with software you did not write find the corners you did not think of.

## Sidecar subtitle files use Jellyfin's URL

Not every subtitle is inside the container. A sidecar is a separate `.srt` or `.ass` file stored next to the video. It is not in the stream the engine reads, and serving it costs the server no extraction, so a sidecar keeps Jellyfin's own URL. The delay this work removed belonged to embedded tracks only. Image-format subtitles like PGS are a separate story covered in the [PGS report](/lab/pgs-subtitles-apple-tv-without-transcoding). For tracks that do reach the server, the rule stays the same: ask it only for what the device cannot produce itself.

---

If a player is waiting and the video is fine, look at the other tracks before you look at the file. And keep a few files in your test suite that someone else made with software you did not write. The whole app is at [Tomo TV](/lab/tomotv).

Drafted with Claude from Tomo TV's code and device measurements. Reviewed, edited and verified by Keiver Hernandez.

Cite as: Keiver Hernandez, "Why Jellyfin subtitles delay playback", keiver.dev/lab/jellyfin-subtitles-delay-playback, 2026-10-05.
