Skip to content
HLS & DASH

How to Add Subtitles to HLS and DASH Streams (WebVTT Guide)

How to add subtitles to HLS and DASH: convert SRT to WebVTT, write subtitle playlists, set default and forced tracks, and test captions on every device.

How to Add Subtitles to HLS and DASH Streams (WebVTT Guide)
On this page 14 sections

Captions make video easier to follow, easier to find and easier to use in noisy or quiet places. Many viewers watch with the sound off on phones, and for deaf and hard-of-hearing viewers, captions are essential. If your video streams as HLS or DASH, adding them properly takes a few specific steps. This guide shows how to add subtitles to HLS, how to do the same in DASH, and how to avoid the sync and display problems that trip people up.

Subtitles, captions and the formats involved

A quick vocabulary check first.

  • Subtitles usually show dialogue, often translated into another language.
  • Closed captions (CC) show dialogue plus sound descriptions such as “[door slams]” or “[upbeat music]”, aimed at viewers who cannot hear the audio.
  • Forced subtitles appear even when the viewer has subtitles off, for example to translate a sign or a short line in another language.

Common formats:

  • SRT (.srt): the simplest and most common file format. Numbered cues with times and text. Not supported directly by HLS.
  • WebVTT (.vtt): the web standard for text tracks, and the format HLS uses. Similar to SRT, with a header and support for positioning and styling.
  • TTML / IMSC1: an XML format used by broadcasters and in DASH.
  • CEA-608 and CEA-708: captions embedded inside the video stream itself, common in broadcast and supported by HLS players.

Step 1: convert SRT to WebVTT

Most caption tools export SRT. Converting to WebVTT is simple:

  1. Add a WEBVTT line at the top, followed by a blank line.
  2. Change the millisecond separator in timestamps from a comma to a full stop: 00:01:02,500 becomes 00:01:02.500.
  3. Optionally remove the cue numbers; WebVTT does not need them.

You can do this in a text editor, with FFmpeg (ffmpeg -i captions.srt captions.vtt), or with our free subtitle converter, which runs in your browser and handles both directions.

Check the result for three things: UTF-8 encoding so accented characters survive, no overlapping cues, and line lengths that stay readable, ideally two lines of up to about 42 characters.

Step 2: add the subtitle track to your HLS master playlist

HLS treats subtitles as an alternative rendition, just like an extra audio language. You add an #EXT-X-MEDIA line for each language, and link the group to each video rendition.

#EXTM3U
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,URI="subs/en/index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español",LANGUAGE="es",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,URI="subs/es/index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=5350000,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2",SUBTITLES="subs"
1080p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3210000,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2",SUBTITLES="subs"
720p/index.m3u8

What the attributes mean:

  • GROUP-ID ties the subtitle tracks together; each video rendition references it with SUBTITLES="subs".
  • NAME is what viewers see in the player menu. Use the language’s own name.
  • LANGUAGE is an ISO language code, used by devices to auto-select captions that match system settings.
  • DEFAULT=YES marks the track to show if the viewer has not chosen. Only one track per group should be default.
  • AUTOSELECT=YES lets the player choose the track automatically based on the viewer’s settings.
  • FORCED=YES is only for forced-narrative tracks.

Step 3: create the subtitle media playlist

Each subtitle track needs its own media playlist. For on-demand video, the simplest version points to a single WebVTT file covering the whole video:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:3600
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:3600.0,
en.vtt
#EXT-X-ENDLIST

Set the target duration and #EXTINF to the length of the video in seconds. Many players accept this. For the widest compatibility, and always for live streams, the WebVTT is split into segments matching the video segment length, each listed in the playlist like video segments.

Segmented WebVTT and X-TIMESTAMP-MAP

When WebVTT is segmented, each file starts with a header that maps caption times to the video’s timeline:

WEBVTT
X-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000

The MPEGTS value is the presentation timestamp of the video at the moment the captions’ local time is zero, on a 90 kHz clock. If it is wrong or missing, captions drift or appear at the wrong time. Packagers write this automatically; hand-made files are where it usually goes wrong.

Letting a packager do it

Rather than writing playlists by hand, most teams let a packager create them. With Shaka Packager, you add the text file as another input:

packager \
  in=video_1080.mp4,stream=video,output=1080.mp4 \
  in=audio.mp4,stream=audio,output=audio.mp4 \
  in=en.vtt,stream=text,language=en,output=subs_en.vtt,hls_group_id=subs,hls_name=English \
  --hls_master_playlist_output master.m3u8 --mpd_output manifest.mpd

This produces an HLS master with the subtitle group and a DASH manifest with a text adaptation set, from the same files. FFmpeg’s HLS output can also segment WebVTT subtitles when you map a subtitle stream. If you are building the rest of the pipeline with FFmpeg, see our guide to converting MP4 to HLS.

Adding subtitles to DASH

In DASH, subtitles are an adaptation set in the MPD:

<AdaptationSet contentType="text" mimeType="text/vtt" lang="en">
  <Role schemeIdUri="urn:mpeg:dash:role:2011" value="subtitle"/>
  <Representation id="sub-en" bandwidth="256">
    <BaseURL>subs/en.vtt</BaseURL>
  </Representation>
</AdaptationSet>

For broadcast-style workflows, DASH often uses IMSC1 (TTML) text inside fMP4 segments, with a mimeType of application/mp4 and codecs stpp. Use Role values of subtitle or caption so players label them correctly. Our explainer on MPEG-DASH covers adaptation sets in more detail.

Embedded captions (CEA-608/708)

Broadcast workflows often carry captions inside the video stream. HLS supports them; declare them in the master playlist with TYPE=CLOSED-CAPTIONS and INSTREAM-ID="CC1", and reference the group with CLOSED-CAPTIONS="cc" on each stream. If your source has embedded captions and you do not declare them, some players still show them and others do not, which leads to confusing support tickets.

Testing on real devices

Captions behave differently across players, so test on the devices your audience uses:

  • Safari on iPhone and Mac reads HLS subtitles natively and honours system caption styling.
  • Web players such as hls.js, Shaka Player and Video.js show a captions menu when the playlist declares subtitle groups. Try our HLS player for a quick check.
  • Android via ExoPlayer/Media3 supports WebVTT and embedded captions.
  • Smart TVs vary most; test at least one.

Check that the right track is default, that switching languages works, that text sits clear of on-screen graphics, and that timing stays in sync to the final minute.

Common problems

Captions do not appear at all. The video renditions do not reference the subtitle group, or the subtitle playlist URL is wrong. Check the SUBTITLES attribute on every #EXT-X-STREAM-INF line.

Captions drift out of sync. Wrong X-TIMESTAMP-MAP, or captions timed against a different edit of the video.

Garbled characters. The file is not UTF-8. Re-save it with UTF-8 encoding.

Two tracks marked default. Players behave unpredictably. Keep one default per group.

Captions blocked by CORS. Subtitle files are fetched like any other resource, so they need the same CORS headers as your segments. Our HLS fixes guide covers the header setup.

Writing captions people can actually read

The technical setup is only half the job. Badly written captions are frustrating even when they are perfectly in sync. A few habits make a real difference.

Keep each caption to one or two lines. Long blocks force viewers to read instead of watch, and they cover the picture. Break lines at natural points in the sentence, not in the middle of a name or a phrase. Keep captions on screen long enough to read: roughly a second for every three or four words is a comfortable pace, and nothing should flash by in under a second.

Identify speakers when it is not obvious who is talking, for example when someone speaks off screen. In closed captions, describe sounds that matter to the story or the lesson, such as “[phone buzzing]” or “[laughter]”, but do not narrate every background noise.

Match the audio closely. Viewers who can hear a little notice when captions paraphrase heavily. Clean up filler words like “um” and false starts, but keep the meaning and tone.

Automatic speech recognition has become very good, and it is a sensible starting point for most creators. Always review the output, though. Technical terms, product names and accents are where machines still slip, and a single wrong word in a technical lesson, such as “codex” for “codec”, undermines trust in the whole video.

Captions and SEO

Search engines cannot watch video, but they can read text. A transcript published on the same page as the video gives search engines far more to understand than a title and description alone. Many creators publish the cleaned-up caption text as a readable transcript below the player, or turn it into a supporting article. Captions also improve watch time on social platforms where videos autoplay silently, which indirectly helps discovery.

For a course or membership site, captions have one more benefit: students can search inside lessons. If your player or platform supports transcript search, it is worth switching on.

Where to go next

Once captions are working, check the rest of your stream with the M3U8 analyzer so the subtitle groups, codecs and bandwidth values all line up, and make sure your encoding settings leave room for caption-safe margins in the picture.

The short version

Convert to WebVTT. Declare each language once. Link it from every rendition. Test it on a phone. That is most of the job.

Then ask a viewer who relies on captions to try it. Their feedback is worth more than any checklist.

Summary

To add subtitles to HLS, convert SRT to WebVTT, add an #EXT-X-MEDIA subtitle entry per language in the master playlist, reference the group from every video rendition, and point each entry at a subtitle media playlist. For live streams and maximum compatibility, segment the WebVTT with a correct X-TIMESTAMP-MAP. In DASH, add a text adaptation set. Let a packager do the heavy lifting, then test on real devices. Your viewers, and your search rankings, will thank you.

Frequently asked questions

Can HLS use SRT subtitles?

No. HLS expects WebVTT for text subtitles, or captions embedded in the video as CEA-608/708. Convert SRT to WebVTT first.

What is the difference between subtitles and closed captions?

Subtitles usually translate or transcribe dialogue. Closed captions also describe sounds, music and speaker changes for deaf and hard-of-hearing viewers. Technically they can use the same formats.

Why are my subtitles out of sync in HLS?

Usually a missing or wrong X-TIMESTAMP-MAP header in segmented WebVTT files, or captions timed against a version of the video with a different start point.

Keep reading