<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: lieber</title>
    <description>The latest articles on DEV Community by lieber (@lieber).</description>
    <link>https://dev.to/lieber</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4195646%2Facd1425a-8fde-490c-8023-3123caa7c8b1.png</url>
      <title>DEV Community: lieber</title>
      <link>https://dev.to/lieber</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lieber"/>
    <language>en</language>
    <item>
      <title>SRT to WebVTT for an HTML video track: a minimal working example</title>
      <dc:creator>lieber</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:56:04 +0000</pubDate>
      <link>https://dev.to/lieber/srt-to-webvtt-for-an-html-video-track-a-minimal-working-example-5aap</link>
      <guid>https://dev.to/lieber/srt-to-webvtt-for-an-html-video-track-a-minimal-working-example-5aap</guid>
      <description>&lt;p&gt;An editor gave you &lt;code&gt;captions.srt&lt;/code&gt;, but your web page uses a native HTML &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; player. The missing step is a WebVTT text track. Here is a two-cue example you can inspect without installing a subtitle editor.&lt;/p&gt;

&lt;p&gt;This example handles existing subtitles. It does not create captions from an audio recording.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start with a small SRT
&lt;/h2&gt;

&lt;p&gt;Save this as UTF-8 in &lt;code&gt;captions.srt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1
00:00:01,250 --&amp;gt; 00:00:02,500
Hello, world!

2
00:00:03,000 --&amp;gt; 00:00:05,750
A second cue.
This line stays below it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first cue starts 1.25 seconds into the video and lasts 1.25 seconds. The second lasts 2.75 seconds and contains two lines. Those are useful checks after conversion.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Write the equivalent WebVTT
&lt;/h2&gt;

&lt;p&gt;For this plain-text example, the complete &lt;code&gt;captions.vtt&lt;/code&gt; is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WEBVTT

00:00:01.250 --&amp;gt; 00:00:02.500
Hello, world!

00:00:03.000 --&amp;gt; 00:00:05.750
A second cue.
This line stays below it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;WEBVTT&lt;/code&gt; header and the empty line below it. Milliseconds now use dots. The SRT sequence numbers have been omitted; WebVTT can use optional cue identifiers. An empty line separates the cues, while the newline inside the second cue remains part of its text.&lt;/p&gt;

&lt;p&gt;For these two cues, you can make the changes by hand. Simply renaming an unchanged SRT file to &lt;code&gt;.vtt&lt;/code&gt; does not do them.&lt;/p&gt;

&lt;p&gt;Keep the original file. Real-world subtitles can contain markup, positioning, styles or metadata that need a format-aware conversion and a check in the destination player. This example makes no promise to preserve advanced formatting.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Attach the track to a video
&lt;/h2&gt;

&lt;p&gt;Put a playable MP4 of at least six seconds in the same folder as &lt;code&gt;captions.vtt&lt;/code&gt;. Use a clip you own and name it &lt;code&gt;demo.mp4&lt;/code&gt;. Save the following as &lt;code&gt;index.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width,initial-scale=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;WebVTT track example&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Two subtitle cues&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;video&lt;/span&gt; &lt;span class="na"&gt;controls&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"640"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"max-width:100%;height:auto"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"demo.mp4"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"video/mp4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;track&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt; &lt;span class="na"&gt;kind=&lt;/span&gt;&lt;span class="s"&gt;"subtitles"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"captions.vtt"&lt;/span&gt;
           &lt;span class="na"&gt;srclang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"English"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Your browser does not support HTML video.
  &lt;span class="nt"&gt;&amp;lt;/video&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Play the video. If needed, enable English in its subtitle menu.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The track's &lt;code&gt;src&lt;/code&gt; is relative to this HTML file. &lt;code&gt;srclang&lt;/code&gt; identifies the language; &lt;code&gt;label&lt;/code&gt; is the name offered to the viewer. Choose &lt;code&gt;kind="captions"&lt;/code&gt; when your track also represents relevant sounds and other audio information for accessibility. This short example is a text-track plumbing exercise, not a complete accessible media package.&lt;/p&gt;

&lt;p&gt;If you already have FFmpeg installed, a silent ten-second background is enough to test the timing without using anyone's footage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-f&lt;/span&gt; lavfi &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nv"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;c&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0x244c40:s&lt;span class="o"&gt;=&lt;/span&gt;640x360:r&lt;span class="o"&gt;=&lt;/span&gt;25:d&lt;span class="o"&gt;=&lt;/span&gt;10 &lt;span class="nt"&gt;-an&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart demo.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It contains no speech; the two lines are synthetic test data.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Serve the folder over HTTP
&lt;/h2&gt;

&lt;p&gt;From that folder, with Python 3 installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; http.server &lt;span class="nt"&gt;--bind&lt;/span&gt; 127.0.0.1 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://127.0.0.1:8000/&lt;/code&gt; on the same computer. Use this server for local development only. Opening the HTML through &lt;code&gt;file://&lt;/code&gt; can introduce local-file restrictions that distract from the conversion check.&lt;/p&gt;

&lt;p&gt;In browser developer tools, inspect the &lt;code&gt;captions.vtt&lt;/code&gt; request. It should succeed and be served as &lt;code&gt;text/vtt&lt;/code&gt;. When deploying, configure that MIME type on your host if necessary. Keep the HTML, MP4 and VTT on the same origin for this first test. A track on another origin needs deliberate CORS configuration, including the parent media element's &lt;code&gt;crossorigin&lt;/code&gt; attribute and appropriate server responses; adding the attribute alone is insufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Check the result, not just the extension
&lt;/h2&gt;

&lt;p&gt;Play through the first six seconds. Verify that the first cue begins at 1.25 seconds and ends at 2.50, and the second runs from 3.00 to 5.75. Its two lines should remain separate.&lt;/p&gt;

&lt;p&gt;Then replace the second cue's text with &lt;code&gt;第二行 🙂&lt;/code&gt; and a second line, save as UTF-8, and reload. This catches encoding problems that an English-only fixture can hide. The synthetic MP4 cannot test synchronization with speech: check that separately with your actual video.&lt;/p&gt;

&lt;p&gt;If nothing appears, check the VTT request, header, blank lines, timestamp punctuation and the player's subtitle selection before changing the video itself.&lt;/p&gt;

&lt;p&gt;For an independent file check, FFprobe can inspect the timestamps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffprobe &lt;span class="nt"&gt;-v&lt;/span&gt; error &lt;span class="nt"&gt;-show_packets&lt;/span&gt; &lt;span class="nt"&gt;-show_entries&lt;/span&gt; &lt;span class="nv"&gt;packet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pts_time,duration_time &lt;span class="nt"&gt;-of&lt;/span&gt; json captions.vtt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect two packets with starts &lt;code&gt;1.250000&lt;/code&gt; and &lt;code&gt;3.000000&lt;/code&gt;, and durations &lt;code&gt;1.250000&lt;/code&gt; and &lt;code&gt;2.750000&lt;/code&gt;. This checks parsed timing, not browser rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional conversion tool
&lt;/h2&gt;

&lt;p&gt;Disclosure: I maintain &lt;a href="https://cuefern.com/tools/srt-to-vtt/" rel="noopener noreferrer"&gt;CueFern's SRT-to-VTT tool&lt;/a&gt;. It is an optional browser interface for the conversion step above. The current tools are free, require no account, and process subtitle content in your browser. They accept existing UTF-8 SRT/VTT files up to 5 MB; they do not transcribe video or audio. Check any compatibility warnings and test the result in your target player.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API/Web_Video_Text_Tracks_Format" rel="noopener noreferrer"&gt;MDN: WebVTT format&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/track" rel="noopener noreferrer"&gt;MDN: the track element&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/library/http.server.html" rel="noopener noreferrer"&gt;Python: http.server&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disclosure: This article was drafted with AI assistance. Its subtitle examples were checked against the converter and FFprobe; this does not establish compatibility with every player or editor.&lt;/p&gt;

</description>
      <category>html</category>
    </item>
  </channel>
</rss>
