Advanced playback in React Native
Add posters, thumbnails, timeline hovers, captions, audio tracks, playback speed, and Picture-in-Picture to a React Native video player built on expo-video and FastPix.
The quickstart gets a FastPix video playing with native controls. This guide adds the features real apps need: poster images, timeline hover previews, captions and audio tracks, playback speed, volume, fullscreen, and Picture-in-Picture.
Two systems do the work. FastPix image URLs give you posters and timeline sprites straight from a playback ID, with no extra API calls. expo-video exposes tracks, speed, volume, and Picture-in-Picture on the player and view you already have.
Every section extends the quickstart’s VideoPlayer component.
Each code block is a complete file. The bold filename above it tells you where the code goes. Create or replace that file with the code shown. The individual controls later on (speed, captions, and audio) are complete snippets you drop into the enhanced player from the first section, and each one says exactly where it goes.
Before you begin
Make sure you have the following:
- You’ve completed the quickstart, so you have a working
VideoPlayercomponent withexpo-videoinstalled. - A FastPix playback ID. The quickstart’s sample ID carries multiple audio and subtitle tracks, which makes it good for testing track switching.
The enhanced player also renders the poster with expo-image, which the quickstart doesn’t install. Add it now:
Poster images and thumbnails
FastPix serves a still frame for any playback ID from the image host, with no upload or extra request needed:
Query parameters control the frame and size:
The image is also available as .png or .webp. First, replace your fastpix.ts helper with this version, which adds getThumbnailUrl alongside the getStreamUrl from the quickstart:
src/constants/fastpix.ts
Now replace the quickstart’s video-player.tsx with this enhanced player. It’s the base for everything below: it shows a FastPix thumbnail as a poster until playback starts, sets volume and mute, and renders loading and error states. expo-video doesn’t draw a poster itself, so you overlay an Image and hide it on the first playingChange. useEvent and useEventListener come from the expo package.
src/components/video-player.tsx
volume and muted are independent: muting doesn’t change the volume value, so unmuting restores the previous level. For timeUpdate events (a { currentTime } payload), set player.timeUpdateEventInterval to a value in seconds in the setup callback. It’s 0 (off) by default.
Timeline hover previews (spritesheets)
FastPix generates a spritesheet, a grid of frames sampled across the video, plus metadata mapping each timecode to a tile position. It’s what powers the small preview image that follows your cursor along a scrub bar.
FastPix sizes the grid to the video: 50 tiles under 15 minutes, 100 tiles above.
The native controls include their own scrubber, so you only need this if you build a custom scrub bar. The approach: fetch the .json metadata (each entry gives an x, y, width, height, and time range), and as the user drags, render the spritesheet in a small clipped View offset to the tile whose time range contains the drag position.
NOTE:
A custom scrubber replaces the native controls (nativeControls={false}) and is a sizeable component in its own right. If you just want a poster or a paused-state image, the thumbnail URL above is simpler.
Volume and mute
player.muted and player.volume (0.0 to 1.0) are already set in the enhanced player’s setup callback above. Change them any time in response to UI, for example player.muted = true from a mute button. Muting doesn’t change the volume value, so unmuting restores the previous level.
Playback speed
playbackRate accepts values from 0 to 16 (1.0 is normal). Here’s a simple cycle button:
src/components/video-player.tsx
By default the player preserves audio pitch as speed changes (player.preservesPitch).
Subtitles and captions
The player exposes the tracks in the HLS manifest through availableSubtitleTracks, and you set the active one with subtitleTrack (or null to turn captions off). Each track has a label and language:
src/components/video-player.tsx
NOTE:
Always assign a track object taken fromavailableSubtitleTracks. Don’t construct one by hand. The list is empty until the media has loaded, so read it after playback starts (or from astatusChangelistener) rather than during the first render.
Audio tracks
Multi-language audio works the same way, through availableAudioTracks and audioTrack:
src/components/video-player.tsx
NOTE:
The sample asset in the quickstart carries several audio tracks (including Tamil, Hindi, and Telugu) and subtitles, so it’s a good one to test track switching against.
React to player events
The enhanced player already reacts to statusChange (driving the loading spinner and error text) through useEvent from the expo package. Two other events are worth knowing:
playingChange: payload{ isPlaying }. The enhanced player uses it to hide the poster.timeUpdate: payload{ currentTime }. Useful for a custom progress readout. It only fires if you setplayer.timeUpdateEventIntervalto a value in seconds (it’s0, off, by default).
Use useEvent(player, name, initial) when a value should re-render the UI, and useEventListener(player, name, handler) for side effects that shouldn’t.
Fullscreen and Picture-in-Picture
The enhanced player passes nativeControls and allowsPictureInPicture, so the native controls already show fullscreen and Picture-in-Picture buttons. To trigger either one from your own button, keep a ref to the VideoView. This complete variant adds a ref and two buttons:
src/components/video-player.tsx
NOTE:
Picture-in-Picture requires native configuration and does not work in Expo Go. Build a development build withnpx expo run:iosornpx expo run:android. On iOS it also needs the background modes enabled through the expo-video config plugin, and it is not supported on the iOS Simulator, so test Picture-in-Picture on a real device.startPictureInPicture()returns a promise that rejects when Picture-in-Picture is unavailable, so always.catch()it (as above) to avoid an unhandled rejection.
Play a segment of the video
To start or stop playback at specific timestamps, add start and end (in seconds) to the stream URL, with no code change beyond the URL:
You can also cap the delivered quality with max_resolution (for example, max_resolution=720p) to save bandwidth. See play your videos for the full set of playback URL parameters.
Frequently asked questions
Why doesn't Picture-in-Picture work in Expo Go or the iOS Simulator?
Picture-in-Picture needs native configuration, so it doesn’t run in Expo Go. Build a development build with npx expo run:ios or npx expo run:android, and enable background modes through the expo-video config plugin on iOS. Even in a development build, Picture-in-Picture isn’t supported on the iOS Simulator, so test it on a real device.
Why are `availableSubtitleTracks` or `availableAudioTracks` empty?
Both lists populate only after the media loads, so reading them during the first render returns an empty array. Read them after playback starts, or from a statusChange listener. If a list stays empty on a device where the tracks do exist, check your expo-video version, because some releases have known gaps in reporting these tracks on iOS.
Can I show a thumbnail for a private (signed) playback ID?
Yes. Add a token query parameter to the thumbnail URL with a JWT signed for that playback ID. Public playback needs no token. See Generate JWTs for secure media for how to sign one.
What’s next
- Play a video in React Native, the quickstart this guide builds on.
- expo-video reference for the complete player and view API.
- Create thumbnails and Create timeline hovers for the full image URL options.