ffmpegsubtitlescaptions

Convert SRT to VTT (and Back) Without Breaking Your Timestamps

·Javid Jamae·10 min read
Convert SRT to VTT (and Back) Without Breaking Your Timestamps

Whisper or AssemblyAI hands you an .srt. Your HTML5 <track> element or your HLS playlist wants WebVTT. You run the obvious one-liner, FFmpeg exits 0, the output looks correct in a text editor, and then the captions render dead center over the speaker's face instead of where you positioned them.

Quick answer: To convert SRT to VTT, run ffmpeg -i subs.srt -c:s webvtt subs.vtt. FFmpeg writes the required WEBVTT header line and swaps SRT's comma millisecond separator (00:00:01,000) for WebVTT's period (00:00:01.000), but it routes every text subtitle through an ASS intermediate, so cue positioning, alignment, and font colors disappear with no warning and no non-zero exit code. If you'd rather not run the conversion, the burn-in, and the sidecar as three separate steps you maintain yourself, FFmpeg Micro handles captions as one API call: see the docs.

What actually differs between SRT and WebVTT

SubRip and WebVTT are close relatives, which is exactly why the conversion feels safe. Both are plain text, both use a cue-per-block layout, both put a timing line above the caption text. The differences are small in count and large in consequence.

SubRip (.srt)WebVTT (.vtt)
File headernone`WEBVTT` on line 1, required
Millisecond separatorcommaperiod
Hours fieldrequiredoptional (`00:05.000` is legal)
Cue identifiernumeric, sequentialoptional, any string
Positioningnothing in the spec`line`, `position`, `size`, `align`, `vertical`, `region`
Inline stylingde facto `<b>`, `<i>`, `<u>`, `<font color>``<b>`, `<i>`, `<u>`, `<c.classname>`, plus `STYLE` blocks and `::cue` CSS
Commentsnone`NOTE` blocks

WebVTT requires the literal string WEBVTT as the first line of the file, and SRT has no header at all. That one line is the difference between a <track> element that loads and one that stays silent while the browser logs nothing useful.

Why `ffmpeg -i subs.srt subs.vtt` loses your cue settings

The common belief is that this conversion is lossless, since both formats carry the same cue model in plain text. Most conversions do look exactly that clean. The loss isn't in the text encoding, though. It's in FFmpeg's subtitle pipeline: FFmpeg converts every text subtitle format through ASS (Advanced SubStation Alpha) as its internal representation, so srt → vtt is really srt → ass → vtt, and each hop drops whatever the next format can't hold.

That explains the specific losses people report. A <font color="#ffcc00"> tag in your SRT survives the first hop, because ASS has color, and then dies at the second, because FFmpeg's WebVTT encoder emits only bold, italic, and underline. Going the other way, a WebVTT cue carrying align:start line:85% position:10% has no ASS equivalent that FFmpeg maps back on the way out, so a vtt → srt → vtt round trip returns you a file where every cue sits at the player default.

Two habits make this less painful. Name the encoder explicitly with -c:s webvtt or -c:s subrip instead of letting FFmpeg guess from the file extension, so a bad request fails loudly. And don't reach for -c:s copy to dodge the transcode, because across formats it stops with Subtitle encoding currently only possible from text to text or bitmap to bitmap.

# SRT to WebVTT
ffmpeg -i subs.srt -c:s webvtt subs.vtt

# WebVTT to SRT
ffmpeg -i subs.vtt -c:s subrip subs.srt

Going from VTT to SRT without producing an invalid file

A VTT to SRT converter has an easier job in one respect and a harder one in another. SRT is the smaller format, so nothing needs inventing, but SRT is also stricter about timestamps: the hours field is mandatory. WebVTT lets you write 00:05.000 --> 00:08.000, and a hand-rolled script that only swaps periods for commas will hand your player 00:05,000 --> 00:08,000, which most SRT parsers reject outright. FFmpeg's subrip encoder pads the hours for you.

What FFmpeg does not carry across: NOTE comment blocks, STYLE blocks and their ::cue rules, REGION definitions, and any string cue identifiers, which get renumbered sequentially. If those matter to you, keep the WebVTT file as the source of truth and treat the SRT as a derived artifact you can regenerate, not the reverse.

SRT to ASS when you need styling that survives burn-in

ASS is the format to convert to when the destination is burned-in pixels rather than a player track, because it's the only one of the three with a real styling model that libass will honor. The conversion is the same shape:

ffmpeg -i subs.srt subs.ass

Read the output before you use it. FFmpeg writes a generic script header, including a PlayResX: 384 / PlayResY: 288 coordinate space and an Arial default style. libass scales that script space to whatever frame it renders into, so the proportions hold on 1080p, but the numbers stop meaning pixels. If you edit Fontsize: 18 expecting 18 pixels on a 1920x1080 frame, you'll get roughly five times that. Either set PlayResX/PlayResY to your actual output resolution before editing sizes, or skip the intermediate file entirely and style at burn time:

ffmpeg -i in.mp4 -vf "subtitles=subs.srt:force_style='FontName=Inter,Fontsize=24,Alignment=2,PrimaryColour=&H00FFFFFF,OutlineColour=&H80000000,BorderStyle=3'" -c:a copy out.mp4

The font you name has to exist inside the machine doing the render, which is its own class of silent failure. We covered that one in FFmpeg fontconfig error isn't fatal, it ships the wrong font.

This is also where most pipelines end up maintaining three things: the format conversion, the burn-in render, and the sidecar file the player still needs for accessibility and SEO. FFmpeg Micro's caption jobs do both halves in one request, returning the burned-in video and the caption file together, so there's no font container to keep patched and no second job to sequence. The API docs have the request shape.

Shifting cue timestamps after you trim the video

Trimming the video and forgetting the sidecar is the most common way a correct conversion still ships broken captions. If you cut 90 seconds off the front of a clip, every cue in the SRT is now 90 seconds early, and no format conversion will fix it.

# drop everything before 1:30 and rebase the remaining cues to zero
ffmpeg -ss 00:01:30 -t 00:00:45 -i subs.srt -c copy clip.srt

# push every cue 2.5 seconds later
ffmpeg -itsoffset 2.5 -i subs.srt -c copy delayed.srt

-itsoffset must come before -i to apply to that input, and its behavior varies more across muxers than the timestamp math suggests. Run head -8 delayed.srt and confirm the first cue moved the amount you asked for before you trust it in a batch. The related failure, where cues are individually correct but drift against the media timeline, shows up in thumbnail tracks too: your video thumbnail sprite sheet is fine, the VTT cues drift.

WebVTT for HLS needs more than the header line

Handing an HLS player a single .vtt file that opens fine in Chrome is where segmented delivery bites. In HLS, subtitles are segmented like every other rendition, each WebVTT segment must begin with its own WEBVTT header, and each one needs an X-TIMESTAMP-MAP line so the cue clock lines up with the MPEG-2 Transport Stream clock. RFC 8216 specifies the mapping; the value you'll see most often is X-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000, where 900000 is ten seconds on the 90 kHz MPEG-TS clock, matching the standard initial PTS offset.

Skip that line and the captions load without error and sit ten seconds off. That's the tell: uniform offset, not drift.

Honest boundary. If your target is a native mobile SDK or a broadcast pipeline that expects TTML or IMSC1, neither SRT nor WebVTT gets you there, and FFmpeg's text subtitle path won't either. That's a job for a packaging tool, not a conversion command.

Common pitfalls

Almost every broken caption file traced back to one of these five.

  • The sed comma swap. sed 's/,/./g' converts timestamps and also rewrites your dialogue: "Wait, no, don't" becomes "Wait. no. don't". If you must do it by hand, anchor the pattern: sed -E 's/([0-9]{2}:[0-9]{2}:[0-9]{2}),([0-9]{3})/\1.\2/g'.
  • A byte order mark before WEBVTT. FFmpeg tolerates a UTF-8 BOM, strict player parsers do not, and the file looks perfect in every editor you'd open it in.
  • Non-UTF-8 source files. Legacy SRTs from desktop tools are often CP1252, and FFmpeg will mangle accented characters unless you pass -sub_charenc WINDOWS-1252 on the input.
  • Overlapping cues. WebVTT permits overlapping cue times, SRT players generally don't, and Whisper output with word-level timestamps can produce them. Check for them before converting down to SRT.
  • MP4 embedding. Muxing captions into an MP4 needs -c:s mov_text, not webvtt. Passing the WebVTT encoder to the MP4 muxer fails in most builds.

FAQ

How do I convert SRT to VTT with FFmpeg?

Run ffmpeg -i subs.srt -c:s webvtt subs.vtt. FFmpeg adds the mandatory WEBVTT header and converts comma millisecond separators to periods. Any font colors or non-standard tags in the SRT will not survive, because FFmpeg's WebVTT encoder writes only bold, italic, and underline.

Is there a VTT to SRT converter that keeps positioning?

No converter can keep positioning in an SRT file, because the SubRip format has no positioning syntax to keep it in. WebVTT cue settings like line, position, and align are dropped in every VTT to SRT conversion. Keep the WebVTT file as your source and generate SRT from it when a tool demands SRT.

Why won't my VTT file load in the browser?

A WebVTT file that fails to load in a browser is usually missing the WEBVTT first line, has a byte order mark in front of it, or is being served with the wrong MIME type. Serve it as text/vtt, and remember that a <track> element loading from another origin needs CORS headers or it fails silently.

Do I still need a sidecar file if I'm burning captions into the video?

Burned-in captions and sidecar files solve different problems, so most publishing pipelines produce both. Burned-in text survives autoplay-muted feeds where no caption track renders, and the sidecar gives you searchable text, viewer-toggleable captions, and a base for translation.

Can I convert SRT to VTT without installing FFmpeg?

You can convert SRT to VTT through an API call instead of a local FFmpeg install, which is what pipelines running in n8n, Make, or Zapier usually need, since those environments have no reliable way to ship an FFmpeg binary. The same request that transcodes or burns in the captions can return the sidecar file.

Sidecar conversion is the small part of this. The part that costs you time is the encoder container, the font that isn't installed, and the trim that silently desynced 400 cues. Both halves run as one job on our free tier: sign up free and send your first caption request from the docs.

About Javid Jamae

Founder & CEO at FFmpeg Micro

Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.

Software EngineeringVideo ProcessingFFmpegCloud ArchitectureAPI DesignAutomation

Skip the command line

The Auto Captions blueprint transcribes your video and burns the captions in. You just review the transcript.

Run it (free)