# PGS subtitles on Apple TV without transcoding

> Tomo TV decodes PGS, VobSub, DVB and XSUB on the Apple TV without help from the server. The engine reads each subtitle stream while demuxing the file, writing display sets to disk as PNGs. A React Native overlay draws them over the native player, inside the content area the controls avoid.

- ID: LAB-002
- Area: Playback engine · Image subtitles
- Problem: PGS and other picture subtitle formats have no renderer in AVPlayer, so every file with them required a full server transcode to burn the subtitles into the picture.
- Solution: The on-device engine decodes bitmap subtitle streams into timed PNGs, which the app draws over the native player. The video and audio are stream-copied, untouched.
- Status: In production
- Published: 2026-10-05
- Updated: 2026-10-17
- Author: Keiver Hernandez
- URL: https://keiver.dev/lab/pgs-subtitles-apple-tv-without-transcoding
- Source: https://github.com/keiver/tomotv

Picture subtitle formats (PGS on Blu-ray remuxes, DVD subpictures, DVB graphics, XSUB) have no renderer in AVPlayer. On Plex and Jellyfin, that gap sends the server into a full re-encode to paint the subtitle images into the video. Tomo TV handles PGS subtitles on Apple TV without transcoding: the on-device engine decodes them to timed images, and the app draws them over Apple's own player. The video and audio streams never change.

![A yellow picture subtitle reading Murderers! Murderers! over a film playing in Tomo TV on Apple TV](/lab/pgs-subtitles-apple-tv-without-transcoding/pgs-subtitle.webp "A PGS subtitle decoded on the Apple TV and drawn over Apple's player. The server only sent the file.")

## PGS subtitles and the renderer AVPlayer does not have

AVKit handles text subtitles natively (WebVTT, SubRip and similar formats). Bitmap formats are different. PGS, DVD subpictures, DVB graphics and XSUB carry pictures, not text. AVPlayer has no built-in way to draw them. Before the current approach, files with only picture subtitles triggered a server request to burn the bitmaps into the video. That meant a full re-encode for every play, trading the original picture for subtitles.

The FFmpeg build inside the on-device engine already decodes all four formats. We use those decoders directly rather than routing subtitles through the server.

## Decode to timed events, not time ranges

The engine decodes picture subtitle streams while it reads the container, writing each display set to disk as a PNG and an event with a timestamp.

The data model is events, not time ranges. Each display set supersedes the previous one, and a set with no composition objects is an erase: the format's signal that nothing should be on screen. Storing start and end times would require back-filling the end from the next event, which breaks on a seek. Absolute source timestamps keep cue timing correct when the engine restarts its output timeline after a seek.

The overlay fetches events from the engine's local server and polls every few seconds while the engine is still reading, stopping when the track is marked complete.

The engine processes each subtitle stream alongside the video and audio. The video and audio take a direct path to the player, untouched.

```mermaid
flowchart LR
  A[Container file] --> B[Image subtitle stream]
  B --> C[On-device decoder]
  C --> D[Timed PNG events]
  D --> E[Overlay drawn over AVKit]
  A --> F[Video and audio]
  F --> G[Stream copied to player]
```

## Draw inside the player, under the controls

The overlay sits as a child of the video component. React Native Video places child views inside AVPlayerViewController's content overlay view, which Apple documents as the layer between the video content and the player controls.

The overlay does not trust that layering alone. It reads AVKit's own guide for where its chrome will not cover the picture, `unobscuredContentGuide`. When the controls appear, each image is lifted only far enough to clear the bottom of that guide. Lifting by the minimum, rather than the whole image height, keeps a display set that spans multiple rectangles (dialogue and an on-screen sign, for instance) in its correct relative layout.

![A Blu-ray picture subtitle over a film in Tomo TV on Apple TV, with the player controls and the subtitle menu open](/lab/pgs-subtitles-apple-tv-without-transcoding/pgs-subtitle-over-player.webp "The same frame with the controls up. The subtitle is lifted just clear of the title and scrub bar, and the subtitle menu covers its end because the overlay sits under the player's chrome.")

## Three AVKit surprises with subtitle tracks

Building this feature exposed three behaviors that showed up only on a device.

**Identical display titles collapse.** Jellyfin assigns the same display name to every untagged PGS track. A disc with many PGS tracks and no language or title metadata sends back identical labels from the server. Resolving a viewer's pick by comparing display names collapsed all of them onto one stream and drew the wrong subtitles silently. The app now uses each rendition's ordinal in AVFoundation's legible group. It refuses to proceed when counts disagree or more than one track reports selected.

**FORCED=YES loses the track.** AVKit treats a forced rendition as something it applies on the viewer's behalf, not something the viewer picks. Its actual behavior, measured across three files differing only in that attribute: the track is withheld from the picker and also not applied. Every subtitle rendition is written with FORCED=NO, whatever the source flags say; DEFAULT and AUTOSELECT carry the intent without hiding the track.

**An empty CC option.** Without a CLOSED-CAPTIONS attribute on the HLS variant entry, AVPlayer cannot rule out in-band captions and offers an empty "CC" entry in the track list. RFC 8216 defines CLOSED-CAPTIONS=NONE to declare there are none. Both playlist generators now include it.

## The subtitle canvas is not the video size

PGS embeds its own canvas dimensions, and they do not always match the video. The overlay maps each image's coordinates through the scale and offset that fit the video into the screen, including any letterbox bars. When a stream omits its canvas dimensions, the video's own size serves as the fallback.

---

Decoding bitmap subtitles on the device removes a whole class of server transcodes for files that were lossless everywhere else. The three AVKit behaviors above are edge cases in HLS playlist authoring where silence was read as permission: declare attributes explicitly and measure every behavior on a device. Text subtitles have their own story in [why Jellyfin subtitles delay playback](/lab/jellyfin-subtitles-delay-playback), and the engine's other trick for the scrub bar is in [Apple TV scrub thumbnails without trickplay](/lab/apple-tv-scrub-thumbnails-without-trickplay). The app itself 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, "PGS subtitles on Apple TV without transcoding", keiver.dev/lab/pgs-subtitles-apple-tv-without-transcoding, 2026-10-05.
