Migrating from expo-av to expo-video
Imports
// Before
import { Video, ResizeMode } from 'expo-av';
// After
import { useVideoPlayer, VideoView, VideoSource } from 'expo-video';
import { useEvent, useEventListener } from 'expo';Video Playback
Before (expo-av)
const videoRef = useRef<Video>(null);
const [status, setStatus] = useState({});
<Video
ref={videoRef}
source={{ uri: 'https://example.com/video.mp4' }}
style={{ width: 350, height: 200 }}
resizeMode={ResizeMode.CONTAIN}
isLooping
onPlaybackStatusUpdate={setStatus}
/>
// Control
videoRef.current?.playAsync();
videoRef.current?.pauseAsync();After (expo-video)
const player = useVideoPlayer('https://example.com/video.mp4', player => {
player.loop = true;
});
const { isPlaying } = useEvent(player, 'playingChange', { isPlaying: player.playing });
<VideoView
player={player}
style={{ width: 350, height: 200 }}
contentFit="contain"
/>
// Control
player.play();
player.pause();Status Updates
Before (expo-av)
<Video
onPlaybackStatusUpdate={status => {
if (status.isLoaded) {
console.log(status.positionMillis, status.durationMillis, status.isPlaying);
if (status.didJustFinish) console.log('finished');
}
}}
/>After (expo-video)
// Reactive state
const { isPlaying } = useEvent(player, 'playingChange', { isPlaying: player.playing });
// Side effects
useEventListener(player, 'playToEnd', () => console.log('finished'));
// Direct access
console.log(player.currentTime, player.duration, player.playing);Local Files
Before (expo-av)
<Video source={require('./video.mp4')} />After (expo-video)
const player = useVideoPlayer({ assetId: require('./video.mp4') });Fullscreen and PiP
<VideoView
player={player}
allowsFullscreen
allowsPictureInPicture
onFullscreenEnter={() => {}}
onFullscreenExit={() => {}}
/>For PiP and background playback, add to app.json:
{
"expo": {
"plugins": [
["expo-video", { "supportsBackgroundPlayback": true, "supportsPictureInPicture": true }]
]
}
}API Mapping
| expo-av | expo-video |
|---|---|
<Video> | <VideoView> |
ref={videoRef} | player={useVideoPlayer()} |
source={{ uri }} | Pass to useVideoPlayer(uri) |
resizeMode={ResizeMode.CONTAIN} | contentFit="contain" |
isLooping | player.loop = true |
shouldPlay | player.play() in setup |
isMuted | player.muted = true |
useNativeControls | nativeControls={true} |
onPlaybackStatusUpdate | useEvent / useEventListener |
videoRef.current.playAsync() | player.play() |
videoRef.current.pauseAsync() | player.pause() |
videoRef.current.replayAsync() | player.replay() |
videoRef.current.setPositionAsync(ms) | player.currentTime = seconds |
status.positionMillis | player.currentTime (seconds) |
status.durationMillis | player.duration (seconds) |
status.didJustFinish | useEventListener(player, 'playToEnd') |
Key Differences
- Separate player and view: Player logic decoupled from the view—one player can be used across multiple views
- Time in seconds: Uses seconds, not milliseconds
- Event system: Uses
useEvent/useEventListenerfromexpoinstead of callback props - Video preloading: Create a player without mounting a VideoView to preload for faster transitions
- Built-in caching: Set
useCaching: truein VideoSource for persistent offline caching
Known Issues
- Uninstall expo-av first: On Android, having both expo-av and expo-video installed can cause VideoView to show a black screen. Uninstall expo-av before installing expo-video
- Android: Reusing players: Mounting the same player in multiple VideoViews simultaneously can cause black screens on Android (works on iOS)
- Android: currentTime in setup: Setting
player.currentTimein theuseVideoPlayersetup callback may not work on Android—set it after mount instead - Changing source: Use
player.replace(newSource)to change videos without recreating the player
API Reference
https://docs.expo.dev/versions/latest/sdk/video/