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:
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.
// 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. 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.