Technical

HLS Subtitles: How Segmented WebVTT Actually Works

Diagram of an HLS subtitle track showing three WebVTT segments whose local cue times are converted to media time by an X-TIMESTAMP-MAP header of LOCAL 00:00:00.000 mapped to MPEGTS 900000, or ten seconds.

Why do WebVTT subtitles drift in an HLS stream?

Because HLS splits subtitles into segments whose cue times are local to the file, not the media timeline. Each segment carries an X-TIMESTAMP-MAP header tying a cue time to a 90 kHz MPEG-2 timestamp. Drop that header, or concatenate segments without applying it, and every cue lands at the wrong time.

A subtitle file delivered to a streaming platform stops being a file. The packager cuts it into pieces, writes a playlist that indexes them, and from that point the player never sees the document you authored.

That matters because most sync faults in streamed subtitles are not authoring faults at all. The caption file was correct when it left the editor; the packaging stage lost the relationship between cue time and media time.

This guide covers how a subtitle rendition is declared, what a subtitle segment actually contains, what X-TIMESTAMP-MAP does, and which faults come from which stage.

Why HLS Segments Subtitles at All

HTTP Live Streaming is built around small, independently fetchable chunks of media. That is what lets a player change bitrate mid-programme, start at an arbitrary point, and recover from a failed request without reloading anything else.

Subtitles are a rendition like any other, so they are cut on the same timeline as the video. A viewer who joins a ninety-minute programme at the eighty-minute mark should not have to download eighty minutes of text they will never see.

Two consequences follow, and between them they explain nearly every streaming subtitle fault:

  • Each segment has to stand alone. A player may fetch segment 47 without having seen segments 1 to 46, so the segment must carry everything needed to render it.
  • Cue times have to be reconcilable with the media timeline. The text says 00:00:02.000, and something has to tell the player what that means in terms of the video it is already playing.

What an HLS Subtitle Track Is Made Of

Four things, in a chain. Each one is a common place for a delivery to break.

The four components of an HLS subtitle track and what each one does
Component What it is What breaks here
Multivariant playlistThe top-level .m3u8 that lists variants and renditionsSubtitle group never referenced, so no menu entry appears
Subtitle media playlistOne .m3u8 per language, listing segments and durationsDurations that do not match the video timeline
Subtitle segmentsThe .vtt files, or fragmented MP4 fragmentsMissing timestamp map, dropped cues, empty gaps
Initialisation sectionThe WebVTT header line, or an EXT-X-MAP init segmentfMP4 segments delivered with no init segment

The second edition of the HLS specification renames the top-level playlist from master playlist to Multivariant Playlist. Both names are still in wide use, and tooling documentation has not caught up evenly, so expect to see them used interchangeably in delivery specifications.

How Subtitles Are Declared in the Playlist

A subtitle rendition is declared with an EXT-X-MEDIA tag, and then claimed by each variant through a SUBTITLES attribute pointing at the group.

#EXTM3U
#EXT-X-VERSION:7
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog",URI="subs/en/index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=3000000,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1280x720,SUBTITLES="subs",CLOSED-CAPTIONS=NONE
video/720p/index.m3u8

Every tag occupies exactly one line. The example above is wrapped for reading only; a line break inside an attribute list produces a playlist that most players reject outright.

Attributes used on an EXT-X-MEDIA tag for a subtitle rendition
Attribute Purpose Notes
GROUP-IDName of the rendition groupMust match the SUBTITLES value on the variant tags
LANGUAGERFC 5646 language tagWhat the player uses to preselect a track
NAMEHuman-readable menu labelRequired, and shown to the viewer as written
DEFAULTPlay this rendition if nothing else appliesAt most one per group should be YES
AUTOSELECTMay be chosen on language preference aloneCannot be NO when DEFAULT is YES
FORCEDForced-narrative subtitles onlyValid only on TYPE=SUBTITLES
CHARACTERISTICSAccessibility signallingComma-separated Uniform Type Identifiers
URIThe subtitle media playlistRequired for subtitles, unlike some other rendition types

The CLOSED-CAPTIONS attribute on the variant tag is a separate question from subtitles entirely. It refers to CEA-608 and CEA-708 data carried inside the video elementary stream, which our guide to embedding 608 and 708 captions covers. Setting it to NONE declares that no such data exists, and if you set it at all it has to be set on every variant.

Inside the Subtitle Media Playlist

The subtitle media playlist has the same shape as a video one, minus the codec information.

#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:6
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:6.00000,
seg0.vtt
#EXTINF:6.00000,
seg1.vtt
#EXTINF:6.00000,
seg2.vtt
#EXT-X-ENDLIST

Nothing requires subtitle segments to be the same length as video segments, but making them the same length is the sane default. Apple's authoring guidance for its own devices settles on six-second media segments, and matching that keeps the two playlists the same length, which turns a missing subtitle segment into an obvious off-by-one rather than a subtle drift.

A subtitle segment with no cues is not a broken segment. Every period in the playlist needs a segment, and a period with nothing to say is a valid WebVTT file containing only the header. Packagers that skip these leave holes in the timeline that some players will not recover from.

X-TIMESTAMP-MAP: the Part Everyone Gets Wrong

A WebVTT segment is a section of a WebVTT file, and the cue times inside it are written against the track's own clock rather than the media timeline. Something has to relate the two. That something is a metadata header placed immediately after the WEBVTT line.

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

00:00:02.000 --> 00:00:05.000
The mapping is what keeps this line on the right shot.

The header says: the cue time in LOCAL corresponds to the media timestamp in MPEGTS. Everything else follows from that single anchor point. It must sit among the non-blank lines directly after the header line, and that block must be followed by a blank line before the first cue.

Worked example converting local cue times to media time through the timestamp map
Segment Cue time in the file Map Media time
seg0.vtt00:00:02.000LOCAL:0, MPEGTS:90000012.000 s
seg1.vtt00:00:07.500LOCAL:0, MPEGTS:90000017.500 s
seg2.vtt00:00:13.200LOCAL:0, MPEGTS:90000023.200 s

The arithmetic is media time = LOCAL + (MPEGTS ÷ 90000). Here 900000 divided by 90000 is ten seconds, so every cue plays ten seconds later than it reads. That ten-second head start is a common packager default, which is why a ten-second error is the single most recognisable signature of a mishandled map.

Chart comparing three subtitle cues on a thirty second media timeline, with the unmapped versions landing at 2, 7.5 and 13.2 seconds and the mapped versions at 12, 17.5 and 23.2 seconds, a constant ten second difference.

When the header is absent, the client must assume that a cue time of zero maps to a media timestamp of zero. That assumption is correct for a packager that started its timeline at zero, and wrong for every packager that did not — which is how a subtitle track ends up uniformly early with no error reported anywhere.

One further detail catches people out: the cue timestamp named in LOCAL is allowed to fall outside the time range the segment covers. It is an anchor, not a claim about the segment's contents, and tools that validate it as though it were will reject perfectly legal files.

The 90 kHz Clock and the 33-Bit Wrap

The MPEGTS value is measured in 90 kHz ticks, and it must be, even when the media segments alongside it use a different timescale.

The choice is not arbitrary. 90,000 divides evenly by every frame rate in professional use, which means a frame boundary is always a whole number of ticks:

Ticks per frame at each common frame rate on a 90 kHz clock
Frame rate Ticks per frame Whole number
24 fps3,750Yes
25 fps3,600Yes
30 fps3,000Yes
50 fps1,800Yes
60 fps1,500Yes
29.97 fps3,003Yes, because 29.97 is 30 × 1000/1001

That last row is the interesting one. The fractional broadcast rates land on whole ticks too, which is why a 90 kHz clock survives contact with NTSC-derived material. What it does not do is remove the need to know which rate you authored against in the first place — see our guide to drop frame and non-drop frame timecode for the errors that produces upstream.

MPEG-2 presentation timestamps are 33 bits, so the counter wraps at 233 ticks. That is 8,589,934,592 ÷ 90,000, or roughly 26 hours and 31 minutes. When it wraps, the segment written at that moment should carry a new timestamp map relating the current WebVTT time to the new, low-valued timestamp.

For on-demand programming this never happens. For a 24/7 linear channel packaged as HLS it happens a little over once a day, and a packager that does not handle it produces a subtitle track that fails at the same time every day for no visible reason.

What Happens to a Cue That Crosses a Segment Boundary

A six-second segment and a four-second caption will collide eventually. The specification is explicit about the on-demand case: each segment must contain every cue intended to be displayed during that segment's period, and each cue's start and end must indicate its total display time even where part of that range falls outside the segment.

In practice that means the cue is repeated, in full and with its real times, in every segment it overlaps.

How a cue crossing a segment boundary should be handled in on-demand and live streams
Approach On demand Live
Repeat the whole cue in both segmentsCorrectNot always possible
Clip the cue to the segment periodPermitted only where duration is unknownAccepted practice
Split into two cues at the boundaryCauses a visible re-renderCauses a visible re-render
Place the cue in the first segment onlyWrongWrong

The live exception exists because a caption's end time may genuinely not be known when the segment is written. Outside that case, clipping and splitting both cost something: the viewer sees the caption blink or reflow at the boundary, and a viewer who joined mid-cue sees nothing at all.

Text Segments or Fragmented MP4

There are two ways to carry the same subtitle text through HLS, and they behave quite differently.

Comparison of segmented WebVTT text, fMP4 WebVTT, and fMP4 IMSC in HLS
Segmented WebVTT fMP4 WebVTT fMP4 IMSC
Segment file.vtt text.m4s fragment.m4s fragment
Sample entryNot applicablewvttstpp
Needs EXT-X-MAPNoYesYes
Uses X-TIMESTAMP-MAPYesNoNo
Timing sourceThe headerMovie timescaleMovie timescale
Styling rangeWebVTT cue settingsWebVTT cue settingsFull IMSC styling
Shares segments with DASHNoYes, via CMAFYes, via CMAF

X-TIMESTAMP-MAP is a text-segment mechanism only. Subtitles carried in fragmented MP4, whether as wvtt or stpp, take their timing from the movie timescale in the initialisation segment. The header does not appear in those files, and cannot be the cause of a sync fault in them.

The carriage rules for both sample entries are defined in ISO/IEC 14496-30, the same specification CMAF and DASH build on. That is the practical argument for fMP4: one set of subtitle segments can serve an HLS and a DASH manifest at once, instead of packaging the same text twice.

When IMSC Is the Right Answer

An IMSC segment is a fragmented MP4 media segment carrying subtitle media according to MPEG-4 Part 30, and that media must comply with the Text Profile of IMSC1.

Two constraints follow, and both are routinely missed:

  • Each segment must contain every sample intended to display in its period, exactly as with WebVTT segments.
  • Each segment must define every style it applies, rather than referencing a style defined in an earlier segment. A player that starts at segment 47 has never parsed segment 1.

Choose IMSC when the delivery specification asks for it, when the subtitles need positioning or styling beyond what WebVTT cue settings express, or when the same assets have to serve a DASH manifest. Our guide to TTML, DFXP, and IMSC covers the profile differences, the IMSC format reference covers compatibility, and converting SCC to IMSC covers bringing a broadcast caption file into the streaming world. For the text-first case, the WebVTT format reference sets out what the format can and cannot express.

Signalling Accessibility in the Playlist

A subtitle track that is present but unsignalled is, for compliance purposes, often treated as absent. Platform validators and assistive technology read the playlist attributes, not the subtitle text.

Accessibility characteristics used on HLS subtitle renditions
Value Meaning Use for
public.accessibility.transcribes-spoken-dialogThe track transcribes what is saidSame-language captions and SDH
public.accessibility.describes-music-and-soundThe track describes non-speech audioSDH, alongside the value above
public.easy-to-readSimplified for easier readingEasy-read renditions, rarely used
FORCED=YESForced narrative onlyForeign dialogue and on-screen text

A forced rendition should contain only forced-narrative cues — translated signage, a scene in another language — and never the full dialogue track. Players show it automatically, so a full transcript signalled as forced puts captions on screen for viewers who did not ask for them.

The regulatory backdrop differs by market. Our summaries of FCC closed captioning requirements and European Accessibility Act requirements for video cover which obligations attach to streamed delivery, and captions against subtitles and SDH covers what belongs in each track.

Authoring Captions That Segment Cleanly

Packaging faithfully reproduces whatever it was given. Most of what makes a stream behave well is decided before the packager runs.

  1. Author in real time, not timecode. WebVTT stores milliseconds from zero. A file carrying a broadcast hour start will be segmented exactly as written, and nothing will display.
  2. Remove the hour offset before packaging, not after. It is a constant, so it is trivial to subtract, and impossible to spot once the file is in pieces.
  3. Keep cues shorter than a segment where the content allows. Fewer boundary crossings means fewer chances for a packager to make the wrong call.
  4. Respect reading speed limits. Our guide to CPS and WPM limits covers the thresholds most platforms validate against.
  5. Validate the single file first. A malformed cue in the source becomes a malformed cue in one segment, which is far harder to find.

Closed Caption Creator writes WebVTT directly and converts from timecode-based formats with the source frame rate stated explicitly rather than assumed. The wider procedure is in our caption and subtitle QC checklist, format support lists what reads and writes, and automating segmentation and conform covers doing this at volume rather than a title at a time.

Getting a Single VTT Back Out of a Stream

Going the other way — recovering an editable file from a packaged stream — is where the timestamp map does the most damage, because the failure is silent.

  1. Fetch the subtitle media playlist named by the rendition's URI.
  2. Download every segment it lists, in order, including the ones that look empty.
  3. Convert each segment's cues to media time using that segment's own map. Do not assume one map covers the track.
  4. Remove cues repeated across boundaries, matching on time and text rather than on position.
  5. Rebase to zero if the result is destined for a player that measures from the first frame.

Skipping step three is the classic error, and it produces a file that is valid, opens cleanly, and is wrong by a constant from the first cue to the last. Where the target is a broadcast format rather than another sidecar, the caption file converter handles the frame rate side of the conversion, and format-specific conversion guides cover what survives each direction.

What Goes Wrong

Every fault below produces a stream that plays. That is what lets them reach an audience.

HLS subtitle symptoms mapped to their causes
SymptomCause
Whole track a constant few seconds earlyTimestamp map missing or ignored when merging
Whole track exactly ten seconds earlyPackager timeline starts at MPEGTS:900000
Drift that grows across the programmeFrame rate fault upstream, not a packaging fault
No subtitle option in the player menuVariant tags missing the SUBTITLES attribute
Empty caption menu entry that does nothingCLOSED-CAPTIONS not set to NONE with no 608 data present
Captions vanish for a few seconds, then returnMissing or skipped segment where a period had no cues
A caption blinks or reflows mid-displayCue split at a segment boundary instead of repeated
Captions appear for viewers who never enabled themFull dialogue track signalled FORCED=YES
Track fails at the same time each day on a linear channel33-bit timestamp wrap not handled with a new map

If the error is a constant, look at the packaging. If it grows, look at the authoring. A uniform offset points at the timestamp map or an unremoved hour start. Drift that is invisible in the first minute and obvious in the last is a frame rate mismatch, and no amount of playlist work will fix it.

HLS Subtitle Delivery Checklist

Run this before signing off a streamed subtitle deliverable.

  • Every variant tag references the subtitle group, not just the top bitrate.
  • The subtitle playlist has a segment for every period, including periods with no cues.
  • Every text segment carries a timestamp map, or the timeline genuinely starts at zero.
  • Segment durations match the video playlist, or the difference is deliberate and documented.
  • Cues crossing boundaries are repeated in full, with their real start and end times.
  • Accessibility characteristics are set on any track intended to satisfy a captioning obligation.
  • Forced renditions contain only forced narrative, never the complete dialogue.
  • CLOSED-CAPTIONS is consistent across every variant tag in the playlist.
  • fMP4 renditions have an EXT-X-MAP and each segment defines its own styles.
  • Someone watched the last five minutes, not only the opening.

Frequently Asked Questions

What is X-TIMESTAMP-MAP in WebVTT?

It is a metadata header in an HLS WebVTT segment that ties a cue time to a media time, written as X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:900000. The MPEGTS value uses a 90 kHz clock, so 900000 is ten seconds. Without it, a player cannot place segmented cues on the media timeline.

What happens if a WebVTT segment has no X-TIMESTAMP-MAP?

The client must assume that a cue time of zero maps to a media timestamp of zero. That is correct only when the packager started the media timeline at zero. Many packagers start at ten seconds, so the whole subtitle track then plays ten seconds early.

Why does HLS use a 90 kHz timescale for subtitles?

Because it is the MPEG-2 presentation timestamp clock, and it divides evenly by every broadcast frame rate: 3,750 ticks at 24 fps, 3,600 at 25, and 3,000 at 30. The specification requires 90 kHz for the MPEGTS value even when the media segments use a different timescale.

How long should HLS subtitle segments be?

Match the video segment duration, which for Apple device delivery is usually six seconds. Nothing in the specification forces the two to be equal, but aligning them keeps the playlists the same length and makes a missing or empty subtitle segment obvious during QC.

What happens to a subtitle cue that spans two HLS segments?

For on-demand content it should appear in full in every segment it overlaps, carrying its real start and end times even where those fall outside the segment. Live streams may clip the cue to the segment, because the duration is not known when the segment is written.

Can HLS carry IMSC subtitles instead of WebVTT?

Yes. An IMSC segment is a fragmented MP4 carrying subtitle media per MPEG-4 Part 30, complying with the Text Profile of IMSC1. It needs an EXT-X-MAP initialisation segment, and each segment must define every style it uses rather than referencing one defined elsewhere.

What is the difference between SUBTITLES and CLOSED-CAPTIONS in an HLS playlist?

SUBTITLES names a group of separate subtitle renditions delivered as their own segments. CLOSED-CAPTIONS refers to CEA-608 or CEA-708 caption data carried inside the video elementary stream. They are different delivery mechanisms, and a stream can legitimately offer one, both, or neither.

Do I need CLOSED-CAPTIONS=NONE in my playlist?

Only if there are no CEA-608 or CEA-708 captions embedded in the video. It tells players to stop looking, which prevents an empty caption menu entry. If you use it, every variant stream tag in the playlist has to carry it, not just some of them.

How do I mark an HLS subtitle track as SDH?

Add CHARACTERISTICS to the EXT-X-MEDIA tag. A track transcribing dialogue uses public.accessibility.transcribes-spoken-dialog; add public.accessibility.describes-music-and-sound when it also carries sound effects and music cues. Both together are what players and platform validators read as subtitles for the deaf and hard of hearing.

How do I extract subtitles from an HLS stream?

Read the subtitle media playlist, download every segment, and convert each segment's cue times to media time using its own X-TIMESTAMP-MAP before merging. Then remove cues repeated across segment boundaries and rebase the result to zero. Concatenating the segments without that step misplaces every cue.


Streaming specifications change more often than broadcast ones, so confirm the packaging requirements against the platform's current delivery document rather than the one you used last year. Need the same subtitles as WebVTT, IMSC, and a broadcast sidecar? Talk to our team or start a free trial.


Resources

Format Reference

WebVTT Format Reference

View Format

Blog Article

What Is a TTML File? TTML, DFXP & IMSC

Read Now

Blog Article

Drop Frame vs Non-Drop Frame Timecode

Read Now

Solution

Closed Caption File Converter

Learn More
Closed Caption Creator

Try it free for 7 days

Create closed captions, subtitles, transcripts, and audio descriptions in one application. No credit card required.