To display a subtitle file in an HTML video player, supply a WebVTT track, serve it with the correct content type, and connect it to the exact video timeline. A downloadable subtitle URL alone is not enough: the browser must be able to fetch and parse the track, and the viewer must have it enabled.
This guide walks through a small same-origin example and a diagnostic sequence for tracks that never appear. It covers browser delivery, not transcription generation or automatic translation.
Check the subtitle artifact before wiring the player
Tornado's job-status documentation describes an optional subtitle_url. When one is returned, inspect the artifact before treating it as a browser-ready track. Availability does not imply a particular subtitle format, language, completeness or synchronization quality. A missing reference is not evidence that subtitles can be reconstructed from the video without a separate process.
Keep the media revision, subtitle revision and language together in your application. Use the storage and access guide to distinguish durable artifact identity from a temporary delivery URL. A signed URL that works during development may expire before a later viewer opens the player.
For native HTML text tracks, use WebVTT. Renaming captions.srt to captions.vtt does not convert the contents. If the source is SRT, use a converter that supports its encoding and formatting, then inspect the resulting cues. Our SRT timing guide covers input timing checks; this article starts at the browser handoff.
Start with a same-origin WebVTT fixture
Create captions.en.vtt as UTF-8 text. The following is a synthetic two-cue fixture, not a transcript of a real recording:
WEBVTT
00:00:01.000 --> 00:00:03.000
The sample begins here.
00:00:04.000 --> 00:00:06.000
The second caption appears here.
WebVTT uses a WEBVTT header, a blank line before cues and a period before milliseconds. Serve the response as Content-Type: text/vtt. These format requirements are documented in the WebVTT format reference.
Put a video you are authorized to use at /media/demo.mp4 and the fixture at /media/captions.en.vtt, on the same origin as the page. The example paths are placeholders you must supply; they are not Tornado API endpoints.
<video controls preload="metadata" src="/media/demo.mp4">
<track
default
kind="subtitles"
src="/media/captions.en.vtt"
srclang="en"
label="English"
>
</video>
The HTML track reference explains the track attributes and selection behavior. srclang identifies the text language; label is the viewer-facing name. Use kind="captions" when the content includes the speech and relevant non-speech audio needed for captions. Changing the attribute does not add missing sound descriptions to subtitle text.
The default attribute offers an initial selection, subject to user preferences. It is not a substitute for checking the player's subtitle menu. Only one track on a media element should have that attribute.
Separate loading from visibility
Open the page through an HTTP server, then inspect the track request in the browser's Network panel. Avoid debugging solely from a local file: page, where origin behavior differs from a deployed application.
Check the response body as well as the status. A request can return HTTP 200 with a login page or a generic application fallback instead of WebVTT. The response should contain the intended cues, not HTML. Inspect the final response after redirects and confirm the MIME type.
For a controlled diagnostic, select the track in the player or set its mode from the browser console:
const element = document.querySelector("video track");
if (!element) throw new Error("No track element found");
element.track.mode = "showing";
console.log({
readyState: element.readyState,
cueCount: element.track.cues?.length ?? null,
mode: element.track.mode,
});
This diagnostic assumes one player and selects its first track. For multiple players, target the relevant element explicitly. The request may still be in progress when the console statement runs; inspect again after the track's load or error event. The HTMLTrackElement interface defines readyState: 0 is not loaded, 1 is loading, 2 is loaded, and 3 is an error.
Do not leave a forced showing assignment in normal playback code if it overrides the viewer's choice. A loaded track with two parsed cues proves that the browser accepted those cues. It does not prove that the words match the recording or that they are visible at the current playback position.
Diagnose remote hosting separately
The same-origin example avoids cross-origin configuration while you establish a working baseline. If the subtitle file lives on another origin, the video element needs an appropriate crossorigin setting and the remote server must permit the request through CORS. For a public, credential-free setup, crossorigin="anonymous" is a common starting point.
Adding that attribute also changes how the media resource is fetched. Check both the video and subtitle servers; fixing the caption request can expose a missing CORS configuration on the video request. Do not assume a signed URL automatically supplies CORS permission. Keep private files behind your intended access controls, and do not enable broad credentialed access just to silence a console error.
Use the browser's reported error and response headers to distinguish expired access, an unauthorized response, a network failure and CORS rejection. Our expired media URL guide helps with access failures. Redact query strings before sharing diagnostics, as described in logging without signed URLs.
Use the symptom to choose the next check
| Symptom | Next check |
|---|---|
| No track request appears | Confirm the element exists and enable the intended track |
| Request returns an error or HTML | Inspect access, redirects and static-host fallback behavior |
| Browser reports a cross-origin failure | Check the parent media attribute and both servers' CORS responses |
| Track loads but no text appears at time zero | Seek inside a cue; the fixture starts at one second |
| Text appears at the wrong moment | Pair the track with the exact edit and compare its timeline |
| Text is garbled or styling disappears | Inspect encoding and the converter's supported markup |
If the video itself will not play, solve that independently. An MP4 extension does not establish codec compatibility; see the container and codec guide.
For final review, play passages near the start, middle and end of your real asset, including edited boundaries. Check language selection, wrapping, names and numbers. Test the browsers and devices your audience actually uses. Native controls and caption presentation differ between environments.
What this example verifies—and what remains yours
The HTML and WebVTT fixtures were checked locally in Chromium: the track loaded, both cues parsed with the expected text and timestamps, and the diagnostic selected showing mode. This was a synthetic track-loading check. It did not stream a production Tornado job, validate cross-origin hosting, verify spoken-word alignment or establish accessibility compliance.
Your release check should include the real media file, its subtitle revision, the production response headers and a viewer's language-selection flow. Keep these artifacts together so a later video replacement triggers another subtitle review.
Build your first Tornado media workflow to acquire and deliver the source media. When a subtitle artifact is available, add this browser-delivery check before exposing it in your application's player.