Offline Playback
Offline playback allows users to download videos for later viewing without an internet connection. This guide covers the capabilities and recommendations for implementing offline video playback.
Overview
React Native Video focuses on playback of video content. For comprehensive offline downloading capabilities, including DRM-protected content, The Widlarz Group (maintainers of React Native Video) offers the Offline Video SDK.
Playing Downloaded Videos
If you already have videos downloaded to the device, you can play them using local file URIs:
iOS Local Files
import { VideoView, useVideoPlayer } from 'react-native-video';
export function OfflinePlayer() {
// Use file:// URI for files in the Documents directory
const player = useVideoPlayer('file:///var/mobile/Containers/Data/Application/.../Documents/video.mp4');
return <VideoView player={player} style={{ width: '100%', height: 300 }} />;
}
Android Local Files
import { VideoView, useVideoPlayer } from 'react-native-video';
export function OfflinePlayer() {
// Use file:// URI for files in internal storage
const player = useVideoPlayer('file:///data/user/0/com.yourapp/files/video.mp4');
return <VideoView player={player} style={{ width: '100%', height: 300 }} />;
}
Bundled Assets
For videos bundled with your app:
const player = useVideoPlayer(require('./assets/video.mp4'));
File System Access
To manage downloaded video files, use React Native’s file system libraries:
Using react-native-fs
import RNFS from 'react-native-fs';
import { VideoView, useVideoPlayer } from 'react-native-video';
import { useState, useEffect } from 'react';
export function LocalVideoPlayer({ filename }: { filename: string }) {
const [videoPath, setVideoPath] = useState<string | null>(null);
useEffect(() => {
const path = `${RNFS.DocumentDirectoryPath}/${filename}`;
// Check if file exists
RNFS.exists(path).then(exists => {
if (exists) {
setVideoPath(`file://${path}`);
}
});
}, [filename]);
const player = useVideoPlayer(videoPath || '');
if (!videoPath) {
return <Text>Video not found</Text>;
}
return <VideoView player={player} style={{ width: '100%', height: 300 }} />;
}
Install react-native-fs: npm install react-native-fs
Offline Video SDK
For production-ready offline video capabilities, consider the Offline Video SDK by The Widlarz Group:
Key Features
- HLS/DASH Downloading: Download adaptive streaming content
- DRM Support: Download and play DRM-protected content offline
- Widevine (Android)
- FairPlay (iOS)
- Persistent license management
- Progress Tracking: Monitor download progress
- Resume Downloads: Pause and resume downloads
- Storage Management: Manage downloaded content
- Automatic Cleanup: Handle expired licenses and content
Why Use Offline Video SDK?
- DRM Complexity: Handling offline DRM requires persistent licenses, which involves complex certificate and license management
- HLS/DASH Downloading: Adaptive streams need special handling to download all segments
- Storage Optimization: Efficient storage management and cleanup
- Production Ready: Thoroughly tested in production apps
- Support: Professional support from the React Native Video team
Learn More
Visit the Offline Video SDK website for:
- Documentation and guides
- Pricing and licensing
- Demo applications
- Integration examples
Simple Download Implementation
For basic video downloads (non-DRM, non-adaptive), you can implement a simple downloader:
import RNFS from 'react-native-fs';
import { useState } from 'react';
import { View, Button, Text, ProgressBarAndroid } from 'react-native';
import { VideoView, useVideoPlayer } from 'react-native-video';
export function VideoDownloader({ videoUrl }: { videoUrl: string }) {
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState(0);
const [localPath, setLocalPath] = useState<string | null>(null);
const downloadVideo = async () => {
setDownloading(true);
const filename = videoUrl.split('/').pop() || 'video.mp4';
const downloadDest = `${RNFS.DocumentDirectoryPath}/${filename}`;
try {
const { promise } = RNFS.downloadFile({
fromUrl: videoUrl,
toFile: downloadDest,
progress: (res) => {
const progressPercent = (res.bytesWritten / res.contentLength) * 100;
setProgress(progressPercent);
},
});
await promise;
setLocalPath(`file://${downloadDest}`);
setDownloading(false);
} catch (error) {
console.error('Download failed:', error);
setDownloading(false);
}
};
const player = useVideoPlayer(localPath || videoUrl);
return (
<View>
{!localPath && (
<Button
title={downloading ? `Downloading ${progress.toFixed(0)}%` : 'Download'}
onPress={downloadVideo}
disabled={downloading}
/>
)}
{localPath && <Text>Playing from local storage</Text>}
<VideoView player={player} style={{ width: '100%', height: 300 }} />
</View>
);
}
This simple implementation is not suitable for:
- DRM-protected content
- HLS/DASH adaptive streams
- Production applications requiring reliability
- Content requiring license management
Storage Considerations
Check Available Space
import RNFS from 'react-native-fs';
async function checkStorage() {
const freeSpace = await RNFS.getFSInfo();
console.log('Free space:', freeSpace.freeSpace, 'bytes');
// Check if enough space (e.g., 1GB minimum)
const minRequiredSpace = 1024 * 1024 * 1024; // 1GB
if (freeSpace.freeSpace < minRequiredSpace) {
alert('Not enough storage space');
return false;
}
return true;
}
Delete Downloaded Videos
import RNFS from 'react-native-fs';
async function deleteVideo(filename: string) {
const path = `${RNFS.DocumentDirectoryPath}/${filename}`;
try {
const exists = await RNFS.exists(path);
if (exists) {
await RNFS.unlink(path);
console.log('Video deleted successfully');
}
} catch (error) {
console.error('Failed to delete video:', error);
}
}
List Downloaded Videos
import RNFS from 'react-native-fs';
async function listDownloadedVideos() {
const files = await RNFS.readDir(RNFS.DocumentDirectoryPath);
const videoFiles = files.filter(file =>
file.name.endsWith('.mp4') ||
file.name.endsWith('.m4v') ||
file.name.endsWith('.mov')
);
return videoFiles.map(file => ({
name: file.name,
path: file.path,
size: file.size,
}));
}
iOS
No special permissions are needed for reading/writing to the app’s Documents directory.
Android
For Android 10 (API 29) and above, use scoped storage. No permissions are needed for app-specific storage.
For external storage access (Android 9 and below), add to AndroidManifest.xml:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Complete Offline Player Example
import { VideoView, useVideoPlayer } from 'react-native-video';
import { useState, useEffect } from 'react';
import { View, Button, Text, StyleSheet, Alert } from 'react-native';
import RNFS from 'react-native-fs';
interface OfflineVideoPlayerProps {
videoUrl: string;
videoId: string;
}
export function OfflineVideoPlayer({ videoUrl, videoId }: OfflineVideoPlayerProps) {
const [isDownloaded, setIsDownloaded] = useState(false);
const [localPath, setLocalPath] = useState<string | null>(null);
const filename = `${videoId}.mp4`;
const filePath = `${RNFS.DocumentDirectoryPath}/${filename}`;
useEffect(() => {
checkIfDownloaded();
}, []);
const checkIfDownloaded = async () => {
const exists = await RNFS.exists(filePath);
if (exists) {
setIsDownloaded(true);
setLocalPath(`file://${filePath}`);
}
};
const downloadVideo = async () => {
try {
const { promise } = RNFS.downloadFile({
fromUrl: videoUrl,
toFile: filePath,
});
await promise;
setIsDownloaded(true);
setLocalPath(`file://${filePath}`);
Alert.alert('Success', 'Video downloaded successfully');
} catch (error) {
Alert.alert('Error', 'Failed to download video');
}
};
const deleteVideo = async () => {
try {
await RNFS.unlink(filePath);
setIsDownloaded(false);
setLocalPath(null);
Alert.alert('Success', 'Video deleted successfully');
} catch (error) {
Alert.alert('Error', 'Failed to delete video');
}
};
const player = useVideoPlayer(localPath || videoUrl);
return (
<View style={styles.container}>
<VideoView player={player} style={styles.video} controls />
<View style={styles.controls}>
<Text style={styles.status}>
{isDownloaded ? '📥 Downloaded (Playing offline)' : '🌐 Streaming online'}
</Text>
{!isDownloaded ? (
<Button title="Download for Offline" onPress={downloadVideo} />
) : (
<Button title="Delete Downloaded Video" onPress={deleteVideo} color="red" />
)}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
video: {
width: '100%',
height: 300,
},
controls: {
padding: 20,
},
status: {
fontSize: 16,
marginBottom: 10,
textAlign: 'center',
},
});
Limitations of Basic Implementation
The simple file-based approach has significant limitations:
- No DRM Support: Cannot download or play DRM-protected content offline
- No Adaptive Streaming: Cannot download HLS/DASH multi-bitrate content
- No License Management: No support for persistent DRM licenses
- Limited Reliability: Network interruptions can corrupt downloads
- No Resume: Cannot resume interrupted downloads
- Storage Issues: No automatic cleanup of expired content
Recommendations
For Simple Use Cases
If you only need to play locally bundled videos or simple MP4 files:
- Use the basic file system approach shown above
- Store videos in the app’s Documents directory
- Use
react-native-fs for file management
For Production Applications
If you need:
- DRM-protected content offline
- HLS/DASH adaptive streaming downloads
- Reliable download management
- Persistent license handling
- Professional support
Use the Offline Video SDK - a production-ready solution by the React Native Video team.
Next Steps