Closed captions and subtitles
Compare sideloaded subtitles with captions and subtitles included in a stream.
For sideloaded text tracks, set format on the text track description (for example, 'srt', 'webvtt', and other supported formats).
Tip: open your browser's developer console and use the player and THEOplayer variables to interact with this demo.
Sources
Subtitle style
Text tracks
| Label | Lang | Kind | Format | Mode |
|---|---|---|---|---|
| No text tracks reported yet. | ||||
WebVTT, TTML, and CEA-608 tracks can be included in a manifest. You can also add a subtitle file when setting the source. The demo filters out metadata tracks and reports each text track's label, language, kind, format, and mode.
Style subtitles
You can set player.textTrackStyle directly.
See the TextTrackStyle API for supported properties.
player.textTrackStyle = {
backgroundColor: 'rgba(0, 0, 0, 0.85)',
edgeStyle: 'dropshadow',
fontColor: '#ffff00',
fontSize: '150%',
};
See how to insert subtitles for more details.
Code
demo.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Closed captions and subtitles</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet" />
<style>
html,
body {
margin: 0;
padding: 0;
}
theoplayer-default-ui:not(:defined) {
display: inline-block;
box-sizing: border-box;
}
theoplayer-default-ui {
width: 100%;
aspect-ratio: 16 / 9;
font-family: 'Noto Sans', sans-serif;
background: #000;
}
</style>
<script src="https://cdn.theoplayer.com/dash/theoplayer/THEOplayer.js"></script>
<script nomodule src="https://unpkg.com/@theoplayer/web-ui@2/polyfills"></script>
<script async src="https://unpkg.com/@theoplayer/web-ui@2"></script>
</head>
<body>
<!--
libraryLocation: For demonstration purposes, we use the THEOplayer CDN.
For production use, host the library yourself and update this URL and the script above.
licenseUrl: Point this to your license file, or replace it with a license property.
-->
<theoplayer-default-ui
configuration='{"libraryLocation":"https://cdn.theoplayer.com/dash/theoplayer/","licenseUrl":"../../../../theoplayer-license.txt","mutedAutoplay":"all"}'
source='{"sources":{"src":"https://cdn.theoplayer.com/video/elephants-dream/playlist.m3u8","type":"application/x-mpegurl"},"poster":"https://cdn.theoplayer.com/video/elephants-dream/playlist.png","textTracks":[{"src":"https://cdn.theoplayer.com/video/elephants-dream/captions.nl.srt","srclang":"nl","label":"Dutch (SRT)","kind":"subtitles","format":"srt","default":true}],"metadata":{"title":"Elephants Dream"}}'
></theoplayer-default-ui>
<script src="../utils.js"></script>
<script>
const sourceConfigurations = {
srt: {
sources: {
src: 'https://cdn.theoplayer.com/video/elephants-dream/playlist.m3u8',
type: 'application/x-mpegurl',
},
poster: 'https://cdn.theoplayer.com/video/elephants-dream/playlist.png',
textTracks: [
{
src: 'https://cdn.theoplayer.com/video/elephants-dream/captions.nl.srt',
srclang: 'nl',
label: 'Dutch (SRT)',
kind: 'subtitles',
format: 'srt',
default: true,
},
],
metadata: { title: 'Elephants Dream' },
},
webvtt: {
sources: {
src: 'https://cdn.theoplayer.com/video/elephants-dream/playlist.m3u8',
type: 'application/x-mpegurl',
},
poster: 'https://cdn.theoplayer.com/video/elephants-dream/playlist.png',
textTracks: [
{
src: 'https://cdn.theoplayer.com/video/elephants-dream/captions.en.vtt',
srclang: 'en',
label: 'English (WebVTT)',
kind: 'subtitles',
format: 'webvtt',
default: true,
},
],
metadata: { title: 'Elephants Dream' },
},
cea608: {
sources: {
src: 'https://cdn.theoplayer.com/video/adultswim2/clip.m3u8',
type: 'application/x-mpegurl',
},
poster: 'https://cdn.theoplayer.com/video/adultswim2/poster.jpg',
metadata: { title: 'Adult Swim (CEA-608)' },
},
ttml: {
sources: {
src: 'https://livesim.dashif.org/dash/vod/testpic_2s/multi_subs.mpd',
type: 'application/dash+xml',
},
metadata: { title: 'DASH TTML subtitles' },
},
};
const resetStyle = {
backgroundColor: undefined,
edgeStyle: undefined,
fontColor: undefined,
fontFamily: undefined,
fontSize: undefined,
marginBottom: undefined,
marginLeft: undefined,
marginRight: undefined,
marginTop: undefined,
windowColor: undefined,
};
const stylePresets = {
default: {},
yellow: {
backgroundColor: 'rgba(0, 0, 0, 0.85)',
edgeStyle: 'none',
fontColor: '#ffff00',
windowColor: 'rgba(0, 0, 0, 0.85)',
},
large: {
backgroundColor: 'rgba(0, 0, 0, 0.65)',
edgeStyle: 'dropshadow',
fontColor: '#ffffff',
fontSize: '150%',
windowColor: 'rgba(0, 0, 0, 0.65)',
},
};
onPlayerReady((player) => {
let currentSource = 'srt';
let autoSelectedTrack;
function reportTracks() {
const tracks = Array.from(player.textTracks)
.filter((track) => track.kind !== 'metadata')
.map((track) => ({
label: track.label || track.kind,
language: track.language || '',
kind: track.kind,
format: track.type || track.format || '—',
mode: track.mode,
}));
sendToParent('text-tracks', { tracks });
}
function selectSubtitleTrack() {
if (currentSource !== 'cea608' && currentSource !== 'ttml') return;
const tracks = Array.from(player.textTracks).filter((track) => track.kind === 'subtitles' || track.kind === 'captions');
const showingTrack = tracks.find((track) => track.mode === 'showing');
const englishTrack = currentSource === 'ttml' ? tracks.find((track) => (track.language || '').toLowerCase().startsWith('en')) : undefined;
if (showingTrack) {
if (englishTrack && showingTrack === autoSelectedTrack && showingTrack !== englishTrack) {
showingTrack.mode = 'disabled';
englishTrack.mode = 'showing';
autoSelectedTrack = englishTrack;
reportTracks();
}
return;
}
const track = englishTrack || tracks[0];
if (track) {
track.mode = 'showing';
autoSelectedTrack = track;
reportTracks();
}
}
player.textTracks.addEventListener(['addtrack', 'removetrack', 'change'], reportTracks);
player.textTracks.addEventListener('addtrack', selectSubtitleTrack);
player.addEventListener('sourcechange', reportTracks);
onParentMessage('text-source', (message) => {
if (typeof message.name !== 'string' || !sourceConfigurations[message.name]) return;
if (message.name === 'cea608' || message.name === 'ttml') {
Array.from(player.textTracks)
.filter((track) => track.kind === 'subtitles' || track.kind === 'captions')
.forEach((track) => {
track.mode = 'disabled';
});
}
currentSource = message.name;
autoSelectedTrack = undefined;
player.autoplay = true;
player.source = sourceConfigurations[message.name];
reportTracks();
});
onParentMessage('text-style', (message) => {
if (typeof message.name !== 'string' || !stylePresets[message.name]) return;
Object.assign(player.textTrackStyle, resetStyle, stylePresets[message.name]);
});
reportTracks();
});
</script>
</body>
</html>