civil-and-structural-engineering
Building a Custom Video Player with React Native Video Library
Table of Contents
Building a Custom Video Player with React Native Video
Video playback is a core feature in many mobile applications, from social media to e-learning platforms. While default video players work, they often lack the branding, accessibility, and interactivity that a custom solution provides. The react-native-video library gives developers full control over video rendering, controls, and behavior. This article walks through creating a production-ready custom video player using React Native and the react-native-video library, covering everything from installation to advanced features like Picture-in-Picture and background playback.
Prerequisites
Before starting, ensure you have a working React Native environment (CLI or Expo, though the library works best with bare React Native). You should be comfortable with ES6, React hooks, and basic state management. For Expo managed workflows, consider the expo-av module instead, but this guide focuses on the react-native-video library for fully native control. React Native version 0.75 or later is preferred; older versions may require manual linking.
Installation and Setup
Install the library using your package manager of choice:
npm install react-native-video
or
yarn add react-native-video
For React Native 0.59 and below, run react-native link react-native-video. For newer versions, autolinking should handle the native dependencies. After installation, rebuild your app:
npx react-native run-android
or
npx react-native run-ios
If you encounter build errors, verify your native configuration. For iOS, ensure use_frameworks! is not conflicting. For Android, check build.gradle for the correct compileSdkVersion (31 or higher is recommended). Consult the official react-native-video repository for troubleshooting common setup issues.
Basic Video Player Component
Import the Video component and render it with a remote or local source. The simplest implementation looks like this:
import Video from 'react-native-video';
function SimplePlayer() {
return (
<Video
source={{ uri: 'https://www.w3schools.com/html/mov_bbb.mp4' }}
style={{ width: '100%', height: 250 }}
controls={true}
resizeMode="contain"
/>
);
}
The controls prop enables the default OS player controls. However, for a custom player, we set controls={false} and build our own UI overlay. The resizeMode prop (contain, cover, stretch) controls how the video fills the container. Use contain to preserve aspect ratio with letterboxing, or cover to fill the container without distortion (some edges may be clipped).
Building Custom Controls
To create a bespoke user experience, disable the default controls and manage playback state yourself. Use a combination of paused, seek, volume, and fullscreen props, along with callbacks like onProgress, onLoad, and onError. Here is a structured approach:
State Management
Define the essential states:
const [paused, setPaused] = useState(true);
const [muted, setMuted] = useState(false);
const [volume, setVolume] = useState(1.0);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [fullscreen, setFullscreen] = useState(false);
const [isLoading, setIsLoading] = useState(true);
Play/Pause Button
Create a touchable overlay that toggles pause state. Include a loading indicator before the video starts:
<TouchableOpacity onPress={() => setPaused(!paused)} style={styles.overlay}>
{isLoading ? (
<ActivityIndicator size="large" color="#fff" />
) : (
<Icon name={paused ? 'play-circle' : 'pause-circle'} size={64} color="#fff" />
)}
</TouchableOpacity>
Progress Bar
Use the onProgress callback to update the current time. Render a Slider (or custom bar) that allows seeking:
<Slider
style={{ width: '100%', height: 40 }}
minimumValue={0}
maximumValue={duration}
value={currentTime}
onValueChange={(value) => {
videoRef.current.seek(value);
setCurrentTime(value);
}}
minimumTrackTintColor="#FFFFFF"
maximumTrackTintColor="#000000"
thumbTintColor="#FFFFFF"
/>
<Text style={styles.time}>{formatTime(currentTime)} / {formatTime(duration)}</Text>
The seek method requires a reference to the Video component:
const videoRef = useRef(null);
<Video
ref={videoRef}
...
/>
Volume Control
Add a mute toggle and a volume slider. Remember that some devices might not allow programmatic volume control outside the OS ringer – mute is reliable, volume may be limited. On iOS, the volume prop sets the player's volume but does not affect the system volume. Use muted for a simple on/off:
<TouchableOpacity onPress={() => setMuted(!muted)}>
<Icon name={muted ? 'volume-off' : 'volume-up'} size={24} color="#fff" />
</TouchableOpacity>
Fullscreen Toggle
React Native Video does not automatically handle fullscreen – you must manage the layout yourself. Use the onFullscreenPlayerDidPresent and onFullscreenPlayerDidDismiss callbacks if you want to use the native fullscreen API. Alternatively, toggle a state that changes the player's dimensions to cover the entire screen and hide the navigation bar. Example using a modal or absolute positioning:
const toggleFullscreen = () => {
setFullscreen(!fullscreen);
// Optionally use react-native-orientation-locker to lock rotation
};
Handling Video Events
Robust players handle loading, buffering, errors, and end-of-playback gracefully.
Load and Duration
const onLoad = (data) => {
setDuration(data.duration);
setIsLoading(false);
};
Progress Updates
const onProgress = (data) => {
setCurrentTime(data.currentTime);
};
Error Handling
const onError = (error) => {
console.error('Video playback error:', error);
// Show a user-friendly error message
};
End of Video
const onEnd = () => {
setPaused(true);
videoRef.current.seek(0);
// Optionally show replay button
};
Styling the Player
Position controls as an overlay on top of the video. Use absolute positioning and zIndex. A common pattern:
const styles = StyleSheet.create({
container: {
width: '100%',
aspectRatio: 16 / 9,
backgroundColor: '#000',
},
video: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
},
controls: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
padding: 16,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.5)',
},
overlay: {
...StyleSheet.absoluteFillObject,
justifyContent: 'center',
alignItems: 'center',
},
});
For a YouTube-like player, show controls on tap and hide them after a few seconds of inactivity. Use a timeout to toggle a controlsVisible state.
Advanced Features
Once the basics are solid, extend the player with these capabilities:
Picture-in-Picture (PiP)
On iOS and Android (Android 8.0+), PiP lets users watch video while using other apps. For iOS, set the pictureInPicture prop to true on the Video component. On Android, you need to enable the android:supportsPictureInPicture manifest entry and use the onPictureInPictureStatusChanged callback. Note that PiP may require additional configuration for the native layer. See PiP documentation.
Background Playback
To continue audio when the app is in the background, configure the native audio session. On iOS, set the audio session category to playback. For Android, use a foreground service. This involves modifying native files (AppDelegate.m and AndroidManifest.xml). The official guide provides step-by-step instructions.
Multiple Video Sources / Playlists
Allow users to switch between videos by updating the source prop. Reset the player state when the source changes:
useEffect(() => {
setPaused(false);
setCurrentTime(0);
setIsLoading(true);
videoRef.current?.seek(0);
}, [currentVideoUri]);
HLS and DASH Streaming
react-native-video supports HLS streams natively on both platforms. For DASH, additional configuration on Android may be needed. Pass an HLS URL directly as the source URI and the player will handle adaptive bitrate streaming automatically.
React Native Gesture Handler Integration
For advanced gestures (double-tap to skip, swipe to adjust volume), integrate react-native-gesture-handler and react-native-reanimated. This can significantly improve UX but adds complexity. Example: double-tap left side of player to rewind 10 seconds, right side to forward 10 seconds.
Performance Optimization
- Use memoization: Use
React.memofor the Video component and controls to prevent unnecessary re-renders. - Throttle progress updates: The
onProgresscallback fires frequently (every 250ms by default). Throttle your UI updates (e.g., skip every other update) to save CPU cycles. - Prevent re-mounting: Keep the Video component mounted when hiding the player – unmounting and remounting forces re-initialization.
- Cache video buffers: Use libraries like
react-native-video-cacheto reduce bandwidth usage and improve start times for frequently watched content. - FlatList and ScrollView: If you have multiple videos in a scrollable list, use
setNativePropsto pause offscreen videos or use a single instance with source swapping.
Accessibility
Ensure your custom controls are accessible:- Add
accessible={true}andaccessibilityLabelto buttons (e.g., "Play video", "Seek slider"). - Use
accessibilityRolewith appropriate roles (button,adjustablefor sliders). - Provide closed captions if available – add a captions button that toggles a text track via the
selectedTextTrackprop. - Test with screen readers (VoiceOver on iOS, TalkBack on Android) to verify that controls are announced correctly.
Testing Your Video Player
Testing video playback in React Native can be challenging because most testing frameworks run in a Node.js environment without native modules. Strategies include:
- Unit tests: Test state logic and helper functions (e.g., time formatting) with Jest.
- Component tests: Use React Native Testing Library, but mock
react-native-videoto simulateonLoadandonProgressevents. - E2E tests: Detox or Appium can interact with native video players. However, verifying playback (e.g., that a video actually plays) may require custom assertions.
Deployment Considerations
- Bundle size: react-native-video adds approximately 5–10 MB to your native bundle (depending on supported formats). Consider using
excludein metro.config.js if you only need specific codecs. - Network resilience: Implement retry logic and user feedback for streaming failures. Use
onBuffercallback to show buffering status. - Analytics: Track events like play, pause, seek, and completion using your analytics SDK.
- Custom headers: For secured streams, pass a
headersprop with authentication tokens.
Complete Custom Player Example
Below is a condensed but complete example integrating the concepts above. For brevity, styling and imports are omitted.
const CustomVideoPlayer = ({ sourceUri }) => {
const videoRef = useRef(null);
const [paused, setPaused] = useState(true);
const [muted, setMuted] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const onLoad = (data) => {
setDuration(data.duration);
setIsLoading(false);
};
const onProgress = (data) => {
setCurrentTime(data.currentTime);
};
const onEnd = () => {
setPaused(true);
videoRef.current.seek(0);
};
const onError = (e) => {
console.log(e);
};
const togglePlay = () => setPaused(!paused);
const handleSeek = (value) => {
videoRef.current.seek(value);
setCurrentTime(value);
};
return (
<View style={styles.container}>
<Video
ref={videoRef}
source={{ uri: sourceUri }}
style={styles.video}
paused={paused}
muted={muted}
resizeMode="contain"
onLoad={onLoad}
onProgress={onProgress}
onEnd={onEnd}
onError={onError}
onBuffer={({ isBuffering }) => setIsLoading(isBuffering)}
/>
{paused && !isLoading && (
<TouchableOpacity style={styles.overlay} onPress={togglePlay}>
<Icon name="play-circle" size={64} color="#fff" />
</TouchableOpacity>
)}
{isLoading && (
<View style={styles.overlay}>
<ActivityIndicator size="large" color="#fff" />
</View>
)}
<View style={styles.controls}>
<TouchableOpacity onPress={togglePlay}>
<Icon name={paused ? 'play-arrow' : 'pause'} size={24} color="#fff" />
</TouchableOpacity>
<Slider
style={{ flex: 1, marginHorizontal: 10 }}
minimumValue={0}
maximumValue={duration}
value={currentTime}
onValueChange={handleSeek}
minimumTrackTintColor="#FFF"
maximumTrackTintColor="#000"
thumbTintColor="#FFF"
/>
<Text style={{ color: '#fff', marginRight: 10 }}>
{Math.floor(currentTime / 60)}:{Math.floor(currentTime % 60).toString().padStart(2, '0')}
</Text>
<TouchableOpacity onPress={() => setMuted(!muted)}>
<Icon name={muted ? 'volume-off' : 'volume-up'} size={24} color="#fff" />
</TouchableOpacity>
</View>
</View>
);
};
Common Pitfalls and Solutions
- Video not playing on Android: Ensure your video URL uses HTTPS. Some devices require hardware acceleration – check
android:hardwareAccelerated="true"in AndroidManifest. - iOS audio output conflicts: Set the audio session category to
playbackinAppDelegate.m. - Player loses state when app goes to background: Persist
currentTimein AsyncStorage and restore on resume. - Memory leaks on unmount: Call
videoRef.current?.dismissFullscreenPlayer()and clean up event listeners in auseEffectcleanup function.
Conclusion
Building a custom video player with react-native-video gives you full control over the viewing experience. Start with a simple component, then layer on controls, accessibility, performance optimizations, and advanced features like PiP and background playback. The library is actively maintained and integrates well with the React Native ecosystem. For further inspiration, explore open-source players such as react-native-video-player or the sample app in the library’s repository. With careful architecture and testing, your custom video player will be a robust part of any mobile application.