Supporting closed captions and subtitles makes your media app accessible to viewers with hearing disabilities, those in noisy environments, and audiences who need language translation. The Vega W3C Media API provides TextTrack APIs for rendering both In-Band captions (embedded in media segments or referenced in manifests) and Out-of-Band captions (hosted in separate text files).
Text tracks come from two sources:
- In-Band — present in the media itself. In ABR streaming, the URL may be in the manifest file, or text tracks can be embedded in media segments.
- Out-of-Band (OOB) — present in a separate text file with its own URL.
Setup
Add @amazon-devices/react-native-w3cmedia to your package.json dependencies. This package provides KeplerCaptionsView (the rendering component), VideoPlayer / AudioPlayer (the HTMLMediaElement implementations), and the VTTCue polyfill used throughout the examples below.
Key components
KeplerCaptionsView
KeplerCaptionsView is a React Native component that renders caption text as an overlay on top of your video. Add it to your render tree alongside KeplerVideoSurfaceView. When the component mounts, it fires an onCaptionViewCreated callback with a handle — pass this handle to your VideoPlayer or AudioPlayer via setCaptionViewHandle so the player knows where to render captions:
const onCaptionViewCreated = (captionsHandle: string): void => {
videoPlayer.setCaptionViewHandle(captionsHandle);
};
<View style={{
backgroundColor: "white",
alignItems: "stretch",
width: deviceWidth,
height: deviceHeight
}}>
<KeplerCaptionsView
onCaptionViewCreated={onCaptionViewCreated}
show={true}
style={{
width: '100%',
height: '100%',
top: 0,
left: 0,
position: 'absolute',
backgroundColor: 'transparent',
flexDirection: 'column',
alignItems: 'center',
zIndex: 2
}}
/>
</View>
Note: The
showproperty must be set totruefor captions to render.
HTMLMediaElement and TextTracks
VideoPlayer and AudioPlayer implement the HTMLMediaElement interface, which includes the addTextTrack method for attaching caption data to the player:
addTextTrack(kind: TextTrackKind, label?: string, language?: string): TextTrack;
The kind parameter determines how the track is used. Only subtitles and captions are supported for rendering:
enum TextTrackKind { "subtitles", "captions", "descriptions", "chapters", "metadata" };
Shaka Player can parse In-Band captions automatically and add them as VTTCues. For Out-of-Band captions or custom parsing, you add cues manually.
Note:
HTMLTrackElement(the<track>HTML element) is not available — use theaddTextTrackAPI instead.
How to render TextTracks
Follow these steps in order to enable caption rendering in your app:
Step 1: Add the accessibility privilege
Your app needs the accessibility privilege to render captions. Add to manifest.toml:
[[wants.privilege]]
id = "com.amazon.devconf.privilege.accessibility"
Step 2: Install the VTTCue polyfill
VTTCue is the standard interface for individual caption cues (a piece of text with a start time, end time, and content). The polyfill must be installed once at app startup, before any captions are added:
import {VTTCue} from '@amazon-devices/react-native-w3cmedia';
// Call once at app startup, before any media playback
function installCaptionPolyfills() {
global.window.VTTCue = global.VTTCue = VTTCue;
if (!window.VTTCue) {
console.log("VTTCue not polyfilled");
}
}
Note: VTTRegion is not supported. The VTTCue polyfill is only needed if you’re manually adding cues (Option A below). If you’re using platform-detected captions (Option B) or Out-of-Band captions (Option C), the platform handles cue creation.
Step 3: Add TextTracks
How you add TextTracks depends on where the captions come from.
Option A: In-Band captions (manual parsing)
If your app parses captions from the manifest or media segments, create a TextTrack and add cues manually:
const textTrack = videoPlayer.addTextTrack('subtitles', 'SampleTextTrack', 'en');
Then add individual cues as you parse them:
const vttCue = new VTTCue(startTime, endTime, payload);
vttCue.lineAlign = lineAlign;
vttCue.positionAlign = positionAlign;
if (size) {
vttCue.size = size;
}
textTrack.addCue(vttCue);
Option B: In-Band captions (platform-detected CEA 608/708)
The Vega platform can detect and decode CEA 608/708 captions embedded in media segments natively. This is the preferred approach for in-band captions — it offloads parsing from JavaScript and handles the complexity for you.
Recommended: If you’re using Shaka Player, disable its in-band caption parsing to avoid conflicts with the native decoder:
shaka.media.ClosedCaptionParser.unregisterParser('video/mp4'); shaka.media.ClosedCaptionParser.unregisterParser('video/mp2t');
When the platform detects captions, it adds TextTracks automatically and notifies your app via the addtrack event. Register a listener to discover available tracks:
videoPlayer.textTracks.addEventListener("addtrack", onTextTrackAdded);
const onTextTrackAdded = (event: Event): void => {
const textTrackList = videoPlayer.textTracks;
if (!textTrackList) return;
for (let i = 0; i < textTrackList.length; i++) {
const textTrack = textTrackList[i];
console.log(`text track: kind=${textTrack.kind}, language=${textTrack.language}, label=${textTrack.label}, mode=${textTrack.mode}`);
}
};
Option C: Out-of-Band captions
For captions hosted in a separate file, provide the URL and MIME type when creating the TextTrack. The platform downloads and parses the file for you:
const textTrack = videoPlayer.addTextTrack("subtitles", 'SampleTextTrack', 'en', contentUri, contentMimeType);
Supported MIME types:
application/ttml+xmltext/vttapplication/x-subtitle-srt(SRT)application/x-subtitleapplication/x-subtitle-samiapplication/x-subtitle-tmplayerapplication/x-subtitle-mpl2application/x-subtitle-dksapplication/x-subtitle-qttextapplication/x-subtitle-lrcapplication/x-subtitle-vtt
Note: The
uriandmimeTypeparameters are Vega-specific extensions to the W3CaddTextTrackAPI. They may be deprecated in the future.
Step 4: Select a TextTrack for display
TextTracks are hidden by default when added. Set the mode to 'showing' to render a track:
textTrack.mode = 'showing';
To hide a track (e.g., when switching languages), set it back to 'hidden':
textTrack.mode = 'hidden';
Using Shaka Player for captions
Shaka Player can handle caption parsing end-to-end for captions referenced in manifests (e.g., DASH <AdaptationSet> with subtitle MIME types, or HLS #EXT-X-MEDIA:TYPE=SUBTITLES). Use this approach for manifest-referenced captions. For CEA 608/708 embedded in video segments, use the platform-native approach described in Option B above.
After loading content, enable text track visibility:
async load(content: any) {
await shakaPlayer.load(content.uri);
shakaPlayer.setTextTrackVisibility(true);
}
To auto-show captions without manual selection:
shakaPlayer.configure({
autoShowText: shaka.config.AutoShowText.ALWAYS,
});
For supported Shaka Player versions and patch downloads, see Play Adaptive Content with Shaka Player.
Full example: Shaka Player with DASH captions
This example initializes a VideoPlayer, attaches the surface and caption views, and loads DASH content with captions through Shaka Player:
import * as React from 'react';
import {useRef, useEffect} from 'react';
import {useWindowDimensions, View, StyleSheet} from 'react-native';
import {
VideoPlayer,
KeplerVideoSurfaceView,
KeplerCaptionsView,
} from '@amazon-devices/react-native-w3cmedia';
import {ShakaPlayer} from './shakaplayer/ShakaPlayer';
const content = {
uri: 'https://dash.akamaized.net/akamai/test/caption_test/ElephantsDream/elephants_dream_480p_heaac5_1_https.mpd',
};
export const App = () => {
const player = useRef<any>(null);
const videoPlayer = useRef<VideoPlayer | null>(null);
const captionViewHandle = useRef<string | null>(null);
const surfaceViewHandle = useRef<string | null>(null);
useEffect(() => {
initializeVideoPlayer();
}, []);
const initializeVideoPlayer = async () => {
videoPlayer.current = new VideoPlayer();
await videoPlayer.current.initialize();
videoPlayer.current!.autoplay = false;
videoPlayer.current.addEventListener('error', () => {
console.log('playback error');
});
// Initialize Shaka Player
player.current = new ShakaPlayer(videoPlayer.current, {
secure: false,
abrEnabled: true,
abrMaxWidth: 3840,
abrMaxHeight: 2160,
});
player.current.load(content, true);
};
const maybePlay = () => {
if (videoPlayer.current && captionViewHandle.current && surfaceViewHandle.current) {
videoPlayer.current.play();
}
};
const onSurfaceViewCreated = (surfaceHandle: string): void => {
videoPlayer.current?.setSurfaceHandle(surfaceHandle);
surfaceViewHandle.current = surfaceHandle;
maybePlay();
};
const onSurfaceViewDestroyed = (surfaceHandle: string): void => {
videoPlayer.current?.clearSurfaceHandle(surfaceHandle);
};
const onCaptionViewCreated = (captionsHandle: string): void => {
captionViewHandle.current = captionsHandle;
videoPlayer.current?.setCaptionViewHandle(captionsHandle);
maybePlay();
};
return (
<View style={styles.container}>
<KeplerVideoSurfaceView
style={styles.surfaceView}
onSurfaceViewCreated={onSurfaceViewCreated}
onSurfaceViewDestroyed={onSurfaceViewDestroyed}
/>
<KeplerCaptionsView
onCaptionViewCreated={onCaptionViewCreated}
show={true}
style={styles.captionView}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'black',
},
surfaceView: {
zIndex: 0,
},
captionView: {
width: '100%',
height: '100%',
top: 0,
left: 0,
position: 'absolute',
backgroundColor: 'transparent',
flexDirection: 'column',
alignItems: 'center',
zIndex: 2,
},
});
Known issues
- Native TextTrack renders by default. When the platform detects CEA 608/708 captions in media segments, it selects a text track by default and renders it. As a workaround, set the
modeof unwanted TextTracks to'hidden'. - VTTRegion is not supported.
- Styling of TextTrackCues is not supported.
Related resources
- W3C Media API Reference
- KeplerCaptionsView API Reference
- Play Adaptive Content with Shaka Player
- Media Player FAQ
Last updated: May 14, 2026