Skip to main content

Subtitles and Text Tracks

React Native Video supports text tracks for subtitles, captions, and multiple languages. This guide covers how to add and manage text tracks in your video player.

Text Track Support

React Native Video supports multiple subtitle formats:
  • WebVTT (.vtt) - Recommended, widely supported
  • SubRip (.srt) - Common format
  • SubStation Alpha (.ssa)
  • Advanced SubStation Alpha (.ass)

Adding External Subtitles

Provide subtitle files via the externalSubtitles array:
import { VideoView, useVideoPlayer } from 'react-native-video';

export function PlayerWithSubtitles() {
  const player = useVideoPlayer({
    uri: 'https://example.com/video.mp4',
    externalSubtitles: [
      {
        uri: 'https://example.com/subtitles_en.vtt',
        label: 'English',
        type: 'vtt',
        language: 'en',
      },
      {
        uri: 'https://example.com/subtitles_es.vtt',
        label: 'Español',
        type: 'vtt',
        language: 'es',
      },
    ],
  });

  return <VideoView player={player} style={{ width: '100%', height: 300 }} />;
}

External Subtitle Properties

PropertyTypeRequiredDescription
uristringYesURL or local path to the subtitle file
labelstringYesDisplay label for the track
type'vtt' | 'srt' | 'ssa' | 'ass' | 'auto'NoSubtitle format (auto-detected from URI if omitted)
languagestringNoISO 639-1 or ISO 639-2 language code

Auto-Detecting Subtitle Type

If the URI ends with the file extension, you can omit the type:
const player = useVideoPlayer({
  uri: 'https://example.com/video.mp4',
  externalSubtitles: [
    {
      uri: 'https://example.com/subtitles_en.vtt', // .vtt extension
      label: 'English',
      // type: 'vtt' - auto-detected
    },
    {
      uri: 'https://example.com/subtitles_fr.srt', // .srt extension
      label: 'Français',
      // type: 'srt' - auto-detected
    },
  ],
});
If the URI doesn’t end with the file extension, you must explicitly specify the type.

Local Subtitle Files

Use local subtitle files bundled with your app:
const player = useVideoPlayer({
  uri: require('./assets/video.mp4'),
  externalSubtitles: [
    {
      uri: 'file:///path/to/subtitles_en.vtt',
      label: 'English',
      type: 'vtt',
      language: 'en',
    },
  ],
});

Managing Text Tracks

Get Available Tracks

Retrieve all available text tracks:
import { VideoView, useVideoPlayer } from 'react-native-video';
import { useState, useEffect } from 'react';

export function TrackSelector() {
  const player = useVideoPlayer({
    uri: 'https://example.com/video.mp4',
    externalSubtitles: [
      { uri: 'https://example.com/en.vtt', label: 'English', type: 'vtt', language: 'en' },
      { uri: 'https://example.com/es.vtt', label: 'Español', type: 'vtt', language: 'es' },
      { uri: 'https://example.com/fr.vtt', label: 'Français', type: 'vtt', language: 'fr' },
    ],
  });

  const [tracks, setTracks] = useState([]);

  useEffect(() => {
    // Wait for video to load
    const subscription = player.addEventListener('onLoad', () => {
      const availableTracks = player.getAvailableTextTracks();
      setTracks(availableTracks);
      console.log('Available tracks:', availableTracks);
    });

    return () => subscription?.remove();
  }, []);

  return <VideoView player={player} style={{ width: '100%', height: 300 }} />;
}

TextTrack Interface

The TextTrack object contains:
interface TextTrack {
  id: string;         // Unique identifier
  label: string;      // Display label
  language?: string;  // Language code (e.g., "en", "es")
  selected: boolean;  // Whether this track is currently selected
}

Select a Text Track

Programmatically select a subtitle track:
import { VideoView, useVideoPlayer } from 'react-native-video';
import { View, Button } from 'react-native';

export function TrackSwitcher() {
  const player = useVideoPlayer({
    uri: 'https://example.com/video.mp4',
    externalSubtitles: [
      { uri: 'https://example.com/en.vtt', label: 'English', type: 'vtt', language: 'en' },
      { uri: 'https://example.com/es.vtt', label: 'Español', type: 'vtt', language: 'es' },
    ],
  });

  const selectEnglish = () => {
    const tracks = player.getAvailableTextTracks();
    const englishTrack = tracks.find(track => track.language === 'en');
    if (englishTrack) {
      player.selectTextTrack(englishTrack);
    }
  };

  const selectSpanish = () => {
    const tracks = player.getAvailableTextTracks();
    const spanishTrack = tracks.find(track => track.language === 'es');
    if (spanishTrack) {
      player.selectTextTrack(spanishTrack);
    }
  };

  const disableSubtitles = () => {
    player.selectTextTrack(null);
  };

  return (
    <View>
      <VideoView player={player} style={{ width: '100%', height: 300 }} />
      <View style={{ flexDirection: 'row', gap: 10, padding: 10 }}>
        <Button title="English" onPress={selectEnglish} />
        <Button title="Español" onPress={selectSpanish} />
        <Button title="Off" onPress={disableSubtitles} />
      </View>
    </View>
  );
}

Get Selected Track

Access the currently selected track:
const selectedTrack = player.selectedTrack;
if (selectedTrack) {
  console.log('Current subtitle:', selectedTrack.label);
} else {
  console.log('No subtitle selected');
}

Track Change Events

Listen to track selection changes:
import { VideoView, useVideoPlayer } from 'react-native-video';
import { useState } from 'react';
import { View, Text } from 'react-native';

export function TrackMonitor() {
  const [currentTrack, setCurrentTrack] = useState<string>('None');

  const player = useVideoPlayer(
    {
      uri: 'https://example.com/video.mp4',
      externalSubtitles: [
        { uri: 'https://example.com/en.vtt', label: 'English', type: 'vtt', language: 'en' },
        { uri: 'https://example.com/es.vtt', label: 'Español', type: 'vtt', language: 'es' },
      ],
    },
    (player) => {
      player.addEventListener('onTrackChange', (track) => {
        if (track) {
          setCurrentTrack(track.label);
          console.log('Track changed to:', track.label);
        } else {
          setCurrentTrack('None');
          console.log('Subtitles disabled');
        }
      });
    }
  );

  return (
    <View>
      <VideoView player={player} style={{ width: '100%', height: 300 }} />
      <Text style={{ padding: 10 }}>Current subtitle: {currentTrack}</Text>
    </View>
  );
}

Subtitle Text Events

Receive the actual subtitle text being displayed:
const player = useVideoPlayer(
  {
    uri: 'https://example.com/video.mp4',
    externalSubtitles: [
      { uri: 'https://example.com/en.vtt', label: 'English', type: 'vtt', language: 'en' },
    ],
  },
  (player) => {
    player.addEventListener('onTextTrackDataChanged', (texts) => {
      // texts is an array of currently displayed subtitle strings
      console.log('Current subtitles:', texts);
    });
  }
);

Complete Subtitle Selector Example

import { VideoView, useVideoPlayer, TextTrack } from 'react-native-video';
import { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';

export function SubtitleSelector() {
  const [selectedTrackId, setSelectedTrackId] = useState<string | null>(null);
  const [currentSubtitle, setCurrentSubtitle] = useState<string>('');

  const player = useVideoPlayer(
    {
      uri: 'https://www.w3schools.com/html/mov_bbb.mp4',
      externalSubtitles: [
        {
          uri: 'https://example.com/subtitles_en.vtt',
          label: 'English',
          type: 'vtt',
          language: 'en',
        },
        {
          uri: 'https://example.com/subtitles_es.vtt',
          label: 'Español',
          type: 'vtt',
          language: 'es',
        },
        {
          uri: 'https://example.com/subtitles_fr.vtt',
          label: 'Français',
          type: 'vtt',
          language: 'fr',
        },
      ],
    },
    (player) => {
      player.addEventListener('onTrackChange', (track) => {
        setSelectedTrackId(track?.id || null);
      });

      player.addEventListener('onTextTrackDataChanged', (texts) => {
        setCurrentSubtitle(texts.join(' '));
      });
    }
  );

  const selectTrack = (track: TextTrack | null) => {
    player.selectTextTrack(track);
  };

  const availableTracks = player.getAvailableTextTracks();

  return (
    <View style={styles.container}>
      <VideoView player={player} style={styles.video} controls />

      <View style={styles.trackSelector}>
        <Text style={styles.title}>Subtitles</Text>
        
        <TouchableOpacity
          style={[
            styles.trackButton,
            selectedTrackId === null && styles.trackButtonActive,
          ]}
          onPress={() => selectTrack(null)}
        >
          <Text>Off</Text>
        </TouchableOpacity>

        {availableTracks.map((track) => (
          <TouchableOpacity
            key={track.id}
            style={[
              styles.trackButton,
              track.selected && styles.trackButtonActive,
            ]}
            onPress={() => selectTrack(track)}
          >
            <Text>{track.label}</Text>
          </TouchableOpacity>
        ))}
      </View>

      {currentSubtitle && (
        <View style={styles.subtitlePreview}>
          <Text style={styles.subtitleText}>{currentSubtitle}</Text>
        </View>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#000',
  },
  video: {
    width: '100%',
    height: 300,
  },
  trackSelector: {
    padding: 20,
    backgroundColor: '#fff',
  },
  title: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 10,
  },
  trackButton: {
    padding: 12,
    marginBottom: 8,
    backgroundColor: '#f0f0f0',
    borderRadius: 8,
    borderWidth: 2,
    borderColor: 'transparent',
  },
  trackButtonActive: {
    borderColor: '#007AFF',
    backgroundColor: '#E3F2FF',
  },
  subtitlePreview: {
    padding: 10,
    backgroundColor: '#f9f9f9',
  },
  subtitleText: {
    fontSize: 14,
    fontStyle: 'italic',
  },
});

Platform-Specific Notes

iOS

  • Only WebVTT (.vtt) subtitles are supported for HLS streams and MP4 files
  • The label property can be overridden by the player
  • Embedded subtitles in HLS streams are automatically detected
On iOS, external subtitle labels may be overridden by the system player. Use language codes for consistent identification.

Android

  • Supports WebVTT, SRT, SSA, and ASS formats
  • Subtitles work with both DASH and HLS streams
  • Text styling is preserved from subtitle files

HLS Embedded Subtitles

For HLS streams with embedded subtitles, tracks are automatically detected:
const player = useVideoPlayer('https://example.com/stream.m3u8');

// Access embedded tracks after video loads
player.addEventListener('onLoad', () => {
  const tracks = player.getAvailableTextTracks();
  console.log('Embedded tracks:', tracks);
});

Best Practices

1. Use WebVTT Format

WebVTT is the most widely supported format across platforms:
WEBVTT

00:00:00.000 --> 00:00:03.000
Welcome to the video

00:00:03.000 --> 00:00:06.000
This is a subtitle

2. Provide Language Codes

Always include language codes for proper identification:
externalSubtitles: [
  { uri: '...', label: 'English', language: 'en' },
  { uri: '...', label: 'Español', language: 'es' },
  { uri: '...', label: '中文', language: 'zh' },
]

3. Handle Track Loading

Wait for video to load before accessing tracks:
player.addEventListener('onLoad', () => {
  const tracks = player.getAvailableTextTracks();
  // Now safe to select tracks
});

4. Remember User Preference

Store and restore the user’s subtitle preference:
import AsyncStorage from '@react-native-async-storage/async-storage';

const saveSubtitlePreference = async (language: string) => {
  await AsyncStorage.setItem('preferred_subtitle', language);
};

const restoreSubtitlePreference = async () => {
  const language = await AsyncStorage.getItem('preferred_subtitle');
  if (language) {
    const tracks = player.getAvailableTextTracks();
    const track = tracks.find(t => t.language === language);
    if (track) {
      player.selectTextTrack(track);
    }
  }
};

Troubleshooting

Subtitles Not Showing

  • Verify the subtitle file format is correct
  • Check that the URI is accessible
  • Ensure the video has loaded (onLoad event fired)
  • Confirm a track is selected: player.selectedTrack

Wrong Encoding

Subtitle files should use UTF-8 encoding. Convert files if needed:
iconv -f ISO-8859-1 -t UTF-8 subtitles.srt > subtitles_utf8.srt

Timing Issues

Ensure subtitle timestamps match video timing:
WEBVTT

00:00:01.000 --> 00:00:04.000
First subtitle

00:00:04.000 --> 00:00:08.000
Second subtitle

iOS Label Override

If iOS overrides your labels, rely on language codes instead:
const englishTrack = tracks.find(t => t.language === 'en');
// More reliable than finding by label

Next Steps

Build docs developers (and LLMs) love