# Captions stuck in one style, or lost when video is converted

Source: https://easeweb.dev/learn/caption-quality
Topics: Images and media > Captions stay readable and survive when video is converted (EN 301 549 7.1.3, 7.1.4, 7.1.5, 7.2.3)
The fix: Carry the captions and the described soundtrack through every conversion, as tracks of the stream. Then either let the browser draw the captions, so viewers' system caption settings apply, or give the player its own caption size, colour and background settings. Where a video's speech is in another language than its subtitles, also offer the subtitles spoken.
Test with: caption settings, screen off, keyboard
References: https://www.etsi.org/deliver/etsi_en/301500_301599/301549/03.02.01_60/en_301549v030201p.pdf https://bitvtest.de/pruefschritt/bitv-20-web/bitv-20-web-7-1-3-erhaltung-von-untertiteln https://bitvtest.de/pruefschritt/bitv-20-web/bitv-20-web-7-1-4-untertitel-anpassungen https://bitvtest.de/pruefschritt/bitv-20-web/bitv-20-web-7-1-5-gesprochene-untertitel https://www.w3.org/WAI/media/av/player/ https://developer.apple.com/documentation/http-live-streaming/hls-authoring-specification-for-apple-devices https://developer.mozilla.org/en-US/docs/Web/API/TextTrack/cuechange_event https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

The coffee roaster's brewing video has checked captions and a described soundtrack, and its branded player has CC and AD buttons beside the volume. Two things still go wrong.

The player draws the captions itself, so that they match the shop: 13 pixels, the brand's thin font, white, with no box behind them. Over the kitchen counter, which is light, they are hard to read. A viewer who set large yellow captions on black in their computer's caption settings gets the same small white text, because those settings style captions the browser draws, not text in the player's own box. The player has no caption settings.

Then the video plugin gained a "fast streaming" option, and the shop turned it on. It re-encodes every video into an adaptive stream that starts quickly on phones. The encoder took the video file and its soundtrack, but not the captions file or the described soundtrack uploaded with it. The streamed version plays, with nothing for the CC and AD buttons to turn on.

## Why it fails

WCAG says little about how captions look, and nothing about what a conversion keeps; WCAG's 1.2.2 only asks that captions exist, which the streamed version now fails too, as [videos without captions, or with unchecked automatic ones](/learn/video-captions) explains. These requirements are in EN 301 549, whose Table A.1 applies them to web pages for the EU's web accessibility directive:

- 7.1.3, Preservation of captioning: where a site transmits, converts or records video with sound, it keeps the caption data, so the captions can still be shown in sync. The site converts its uploads, so it must keep their captions.
- 7.2.3, Preservation of audio description: the same for the audio description.
- 7.1.4, Captions characteristics: where a site displays captions, viewers can adapt how they look to their own needs, such as their colours, font, size, background and outline. Captions drawn into the picture are exempt, as they can't be changed.
- 7.1.5, Spoken subtitles: where a site displays video with sound, it has a mode that speaks the captions, unless their text can't be read by software. German BITV testers apply it where the speech is not in the site's main language, subtitles in that language are offered, and no version of the video has a soundtrack in it: that is when a blind viewer needs the subtitles spoken. The brewing video's narrator speaks the captions' language, so reading them aloud would only repeat the narration, and BITV would not apply the clause here, though an auditor who reads the clause literally may still ask for it.

The streaming conversion fails 7.1.3 and 7.2.3. The player falls short of 7.1.4 for every viewer who needs a background behind the captions or their own caption style: it offers no setting, and their system caption settings don't reach it. A BITV tester, who also counts browser zoom and colour schemes, may still rate it met, since zoom enlarges these captions and a contrast theme recolours them; nothing gives them a background.

## Who is affected

Everyone sees the same small white text, and everyone gets the streamed version, but many viewers never turn captions on, so the shop doesn't notice.

## What automation and AI miss

No WCAG rule covers this, so axe and other WCAG scanners don't look; a page whose video has a captions track passes them even when the stream it plays has none. EaseWeb's audit lists every video for review, and for a player with its own controls it asks a person to check that viewers can change the captions' size, colours and background. It doesn't compare a stream with the file it came from, open a stream's manifest, or try caption settings. A code assistant asked to "make the captions match the brand" is likely to write a fixed font, size and colour, which is how this happened.

## Before: captions drawn in one style

```js player.js
// The player hides the browser's captions and draws each cue itself.
const track = video.textTracks[0];
track.mode = 'hidden';
track.addEventListener('cuechange', () => {
  captionBox.textContent = track.activeCues[0]?.text ?? '';
});
```

```css player.css
.caption-box {
  position: absolute;
  bottom: 8%;
  width: 100%;
  font: 13px/1.3 "Brand Sans", sans-serif;
  color: #fff;
  text-align: center;
}
```

The captions are text, so they could be restyled, but the player offers no settings, and the viewer's system caption settings don't reach text in its own box. Thin white letters with no background are hard to read on a light picture. Browser zoom makes them bigger, along with the rest of the page, and a contrast theme changes their text colour, but nothing gives them a background.

## Before: captions and description lost in conversion

```m3u8 master.m3u8
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=2400000,RESOLUTION=1280x720
video/720p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
video/360p.m3u8
```

The stream's playlist lists two sizes of the picture with its soundtrack, and nothing else. The captions file and the described soundtrack uploaded with the original are not in it, so a player of this stream has no captions to show and no description to play.

## After: tracks kept through conversion, captions drawn by the browser

```m3u8 master.m3u8
#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,URI="audio/en.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="English, described",LANGUAGE="en",AUTOSELECT=YES,CHARACTERISTICS="public.accessibility.describes-video",URI="audio/en-described.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",LANGUAGE="en",AUTOSELECT=YES,CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",URI="subs/en.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=2400000,RESOLUTION=1280x720,AUDIO="audio",SUBTITLES="subs"
video/720p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360,AUDIO="audio",SUBTITLES="subs"
video/360p.m3u8
```

```js player.js
// The browser draws the captions, so the viewer's system caption style applies.
const track = video.textTracks[0];
track.mode = 'showing';
```

The encoding job now carries the captions as a subtitles rendition and the described soundtrack as a second audio rendition, marked as describing the video, so every player of the stream can offer both, in sync. With no box of its own, the player leaves the captions to the browser, which draws them in the viewer's system caption style in Safari and in Chromium-based browsers such as Chrome and Edge. Firefox doesn't read the system caption settings, on macOS at least, so its users there get the page's style, and viewers who don't know the setting exists won't find it from the player.

## After: tracks kept through conversion, and the player's own caption settings

```m3u8 master.m3u8
#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,URI="audio/en.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="English, described",LANGUAGE="en",AUTOSELECT=YES,CHARACTERISTICS="public.accessibility.describes-video",URI="audio/en-described.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",LANGUAGE="en",AUTOSELECT=YES,CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound",URI="subs/en.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=2400000,RESOLUTION=1280x720,AUDIO="audio",SUBTITLES="subs"
video/720p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360,AUDIO="audio",SUBTITLES="subs"
video/360p.m3u8
```

```html player.html
<fieldset class="caption-settings">
  <legend>Captions</legend>
  <label>Size
    <select data-style="--cap-size">
      <option value="1">100%</option>
      <option value="1.5">150%</option>
      <option value="2">200%</option>
    </select></label>
  <label>Text
    <select data-style="--cap-colour">
      <option value="#fff">White</option>
      <option value="#ffde3b">Yellow</option>
    </select></label>
  <label>Background
    <select data-style="--cap-back">
      <option value="rgb(0 0 0 / 0.8)">Black</option>
      <option value="transparent">None</option>
    </select></label>
</fieldset>
```

```js player.js
// Each setting sets a style on the caption box, and is kept for the next video.
for (const field of document.querySelectorAll('[data-style]')) {
  const key = `captions${field.dataset.style}`;
  field.value = localStorage.getItem(key) ?? field.value;
  const apply = () => {
    captionBox.style.setProperty(field.dataset.style, field.value);
    localStorage.setItem(key, field.value);
  };
  field.addEventListener('change', apply);
  apply();
}

const track = video.textTracks[0];
track.mode = 'hidden';
track.addEventListener('cuechange', () => {
  captionBox.textContent = track.activeCues[0]?.getCueAsHTML().textContent ?? '';
});
```

```css player.css
.caption-box {
  position: absolute;
  bottom: 8%;
  left: 50%;
  translate: -50%;
  max-width: 90%;
  padding: 0.1em 0.4em;
  font: calc(var(--cap-size) * 1.25rem) / 1.3 system-ui, sans-serif;
  color: var(--cap-colour);
  background: var(--cap-back);
  white-space: pre-line;
  text-align: center;
}
```

The stream keeps its tracks as in the other fix. The player keeps drawing its own captions, but viewers now choose their size, text colour and background, in every browser, starting from white on a near-black box that is readable on any picture. The choice is kept for the next video. Put these settings in the player's Settings menu, beside the caption language.

## Which option to use

Both keep the tracks through conversion, and both let viewers restyle the captions. Use the player's own settings: they work in every browser, sit where viewers look for them, and start from a readable style, while the browser's drawing leaves Firefox users, on macOS at least, with one style and asks everyone else to know about a setting deep in their operating system. Letting the browser draw is the smaller change, and a fair choice for a player that uses the browser's own controls.

## Implementation decisions

Check every route a video takes after upload:

- A streaming encoder, as here: map the captions file to a subtitles rendition and the described soundtrack to an audio rendition in the job's settings, for every language, and check the output manifest lists them. DASH manifests carry them as text and audio adaptation sets, with roles for captions and description.
- A video platform, such as YouTube or Vimeo: upload the captions file to the video there, and check the embed offers it; re-uploading a new cut loses the old captions unless you add them again.
- A file made for social media, or a shorter edit: it is a new video, so it needs its own captions, cut to its timing; captions drawn into the picture go wherever the file goes, where a separate captions file may not.
- A recording the site makes, such as a webinar or a support call: save the live captions with it, and offer them with the recording.

Keep what the captions' look says. EN 301 549 notes that position, colour and style can carry meaning, such as a caption moved to the top so it doesn't cover text on screen, or a colour for each speaker. A conversion that keeps the words but drops the positions should be fixed, and viewer settings should change the size and colours without moving captions back over what they were avoiding.

Full screen: when a player draws its own captions, put the player's container into full screen with `requestFullscreen()`, not the video, or the captions box is left behind. Browser-drawn captions follow the video by themselves.

Videos in another language: when a video's speech is in another language than its subtitles, such as an interview in Spanish with English subtitles, a blind viewer who doesn't understand Spanish needs the subtitles spoken. The better way is a spoken-subtitles soundtrack, which EN 301 549 suggests delivering as a separate audio track, offered like the described soundtrack. A player can also offer a Read subtitles aloud option, off by default, that speaks each cue with speech synthesis: a `SpeechSynthesisUtterance` whose `lang` comes from the track, with the previous cue cancelled when the next starts so it never falls behind, and with the original speech turned down while it talks. Speech synthesis depends on the voices the device has for the language. There is no need to offer either for a video whose speech is in the captions' language: it only repeats the narration over itself.

Captions drawn into the picture are exempt from 7.1.4 and 7.1.5, because nothing can change or read them; that is the reason not to use them alone, not a way around these clauses. [Video players that hide captions in a settings menu](/learn/caption-controls) covers where the CC and AD buttons go.

## Verify the fix

1. Play every converted version of a video, the stream, the platform embed and any edit, beside the original: every captions language and the audio description are offered, and stay in sync with the speech.
2. Choose large yellow captions on black in the player's settings, and in the system caption settings (Windows: Accessibility, Captions; macOS: Accessibility, Hearing, Captions, which macOS 27 calls Subtitles and Captioning). Check the captions follow the player's settings in every browser you support, and the system's wherever the browser draws them; on macOS, untick Allow video to override, or a page's own caption style can win.
3. If the site plays videos whose speech is in another language than their subtitles, play one with the screen covered and turn on its spoken subtitles: every subtitle is spoken in the subtitles' language as it comes up, or a spoken-subtitles soundtrack plays in its place.
4. In full screen, check the captions still show in the chosen style, and use the caption settings with only the keyboard.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat whenever the encoder, the player or the video platform changes.

## Limits

This is a starting pattern, not a guarantee for every application. Settings only help when the captions are right and complete, and some viewers need settings beyond these three, such as an outline around the letters or a different font. Test the complete journey with the people who will watch, in the players and platforms the video really plays in.

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
