Files
HausApp/.agents/skills/upgrading-expo/references/expo-av-to-video.md
René Schober 4e34270786 initial commit
2026-03-13 06:23:06 +01:00

4.8 KiB

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/useEventListener from expo instead of callback props
  • Video preloading: Create a player without mounting a VideoView to preload for faster transitions
  • Built-in caching: Set useCaching: true in 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.currentTime in the useVideoPlayer setup 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/