Skip to main content
React Native Skia enables rich multimedia experiences by allowing you to load video frames as images. Video frames can be used anywhere a Skia image is accepted: Image, ImageShader, and Atlas components.

Requirements

  • React Native Reanimated version 3 or higher
  • Android API level 26 or higher
  • Web Fully supported

useVideo Hook

The useVideo hook loads video files and provides the current frame as a shared value.

Basic Example

import { Canvas, Image, useVideo } from "@shopify/react-native-skia";
import { useWindowDimensions } from "react-native";
import { useSharedValue } from "react-native-reanimated";

const VideoExample = () => {
  const { width, height } = useWindowDimensions();
  const { currentFrame } = useVideo("https://bit.ly/skia-video");
  
  return (
    <Canvas style={{ flex: 1 }}>
      <Image
        image={currentFrame}
        x={0}
        y={0}
        width={width}
        height={height}
        fit="cover"
      />
    </Canvas>
  );
};

Returned Values

The useVideo hook returns an object with:
PropertyTypeDescription
currentFrameSharedValue<SkImage | null>Current video frame or null if not loaded
currentTimeSharedValue<number>Current playback position in milliseconds
durationnumberTotal video duration in milliseconds
frameratenumberVideo frame rate (frames per second)
rotation0 | 90 | 180 | 270Video rotation in degrees
size{ width: number, height: number }Video dimensions

Using Returned Values

import { Canvas, Image, useVideo, Text } from "@shopify/react-native-skia";
import { useDerivedValue } from "react-native-reanimated";

const VideoWithInfo = () => {
  const { currentFrame, currentTime, duration, framerate, size } = useVideo(
    "https://bit.ly/skia-video"
  );
  
  const timeText = useDerivedValue(
    () => `${Math.floor(currentTime.value / 1000)}s / ${Math.floor(duration / 1000)}s`
  );
  
  return (
    <Canvas style={{ flex: 1 }}>
      <Image
        image={currentFrame}
        x={0}
        y={0}
        width={size.width}
        height={size.height}
        fit="contain"
      />
      <Text x={10} y={30} text={timeText} />
    </Canvas>
  );
};

Playback Options

Control video playback with options:
OptionTypeDescription
pausedSharedValue<boolean>Controls pause state
loopingSharedValue<boolean>Enables looping
seekSharedValue<number | null>Seek to specific time in milliseconds
volumeSharedValue<number>Volume level (0-1, where 0 is muted)

Pause and Play

import { Canvas, Image, useVideo } from "@shopify/react-native-skia";
import { Pressable } from "react-native";
import { useSharedValue } from "react-native-reanimated";

const PauseableVideo = () => {
  const paused = useSharedValue(false);
  const { currentFrame } = useVideo(
    "https://bit.ly/skia-video",
    { paused }
  );
  
  return (
    <Pressable
      style={{ flex: 1 }}
      onPress={() => {
        paused.value = !paused.value;
      }}
    >
      <Canvas style={{ flex: 1 }}>
        <Image
          image={currentFrame}
          x={0}
          y={0}
          width={400}
          height={300}
          fit="cover"
        />
      </Canvas>
    </Pressable>
  );
};

Looping Video

import { useSharedValue } from "react-native-reanimated";

const LoopingVideo = () => {
  const looping = useSharedValue(true);
  const { currentFrame } = useVideo(
    "https://bit.ly/skia-video",
    { looping }
  );
  
  return (
    <Canvas style={{ flex: 1 }}>
      <Image image={currentFrame} x={0} y={0} width={400} height={300} />
    </Canvas>
  );
};

Seeking

import { Canvas, Image, useVideo } from "@shopify/react-native-skia";
import { Pressable } from "react-native";
import { useSharedValue } from "react-native-reanimated";

const SeekableVideo = () => {
  const seek = useSharedValue<number | null>(null);
  const { currentFrame, currentTime } = useVideo(
    "https://bit.ly/skia-video",
    { seek, looping: true }
  );
  
  return (
    <Pressable
      style={{ flex: 1 }}
      onPress={() => {
        // Seek to 2 seconds
        seek.value = 2000;
      }}
    >
      <Canvas style={{ flex: 1 }}>
        <Image
          image={currentFrame}
          x={0}
          y={0}
          width={400}
          height={300}
          fit="cover"
        />
      </Canvas>
    </Pressable>
  );
};

Volume Control

import { useSharedValue } from "react-native-reanimated";
import { Slider } from "react-native";

const VideoWithVolume = () => {
  const volume = useSharedValue(1.0);
  const { currentFrame } = useVideo(
    "https://bit.ly/skia-video",
    { volume }
  );
  
  return (
    <>
      <Canvas style={{ flex: 1 }}>
        <Image image={currentFrame} x={0} y={0} width={400} height={300} />
      </Canvas>
      <Slider
        value={volume.value}
        onValueChange={(value) => {
          volume.value = value;
        }}
        minimumValue={0}
        maximumValue={1}
      />
    </>
  );
};

Loading Videos

From Network

const { currentFrame } = useVideo("https://example.com/video.mp4");

From Local File

const { currentFrame } = useVideo("file:///path/to/video.mp4");

From Assets with Expo

import { useVideo } from "@shopify/react-native-skia";
import { useAssets } from "expo-asset";

const useVideoFromAsset = (
  mod: number,
  options?: Parameters<typeof useVideo>[1]
) => {
  const [assets, error] = useAssets([mod]);
  
  if (error) {
    throw error;
  }
  
  return useVideo(assets ? assets[0].localUri : null, options);
};

// Usage
const VideoFromAsset = () => {
  const { currentFrame } = useVideoFromAsset(
    require("./assets/BigBuckBunny.mp4")
  );
  
  return (
    <Canvas style={{ flex: 1 }}>
      <Image image={currentFrame} x={0} y={0} width={400} height={300} />
    </Canvas>
  );
};

Handling Rotation

Videos can have rotation metadata. Use the fitbox function to handle rotation:
import { Canvas, Image, useVideo, fitbox, rect } from "@shopify/react-native-skia";
import { useWindowDimensions } from "react-native";

const RotatedVideo = () => {
  const { width, height } = useWindowDimensions();
  const { currentFrame, rotation, size } = useVideo(
    "https://bit.ly/skia-video"
  );
  
  const src = rect(0, 0, size.width, size.height);
  const dst = rect(0, 0, width, height);
  const transform = fitbox("cover", src, dst, rotation);
  
  return (
    <Canvas style={{ flex: 1 }}>
      <Image
        image={currentFrame}
        x={0}
        y={0}
        width={width}
        height={height}
        fit="none"
        transform={transform}
      />
    </Canvas>
  );
};

Using with Shaders

Video frames can be used as shader inputs:
import {
  Canvas,
  Fill,
  ImageShader,
  ColorMatrix,
  useVideo,
} from "@shopify/react-native-skia";
import { useWindowDimensions } from "react-native";

const VideoWithShader = () => {
  const { width, height } = useWindowDimensions();
  const { currentFrame } = useVideo("https://bit.ly/skia-video");
  
  return (
    <Canvas style={{ flex: 1 }}>
      <Fill>
        <ImageShader
          image={currentFrame}
          x={0}
          y={0}
          width={width}
          height={height}
          fit="cover"
        />
        <ColorMatrix
          matrix={[
            0.95, 0, 0, 0, 0.05,
            0.65, 0, 0, 0, 0.15,
            0.15, 0, 0, 0, 0.5,
            0, 0, 0, 1, 0,
          ]}
        />
      </Fill>
    </Canvas>
  );
};

Complete Player Example

import { Canvas, Image, useVideo } from "@shopify/react-native-skia";
import { View, Pressable, Text, StyleSheet } from "react-native";
import { useSharedValue } from "react-native-reanimated";
import { useState } from "react";

const VideoPlayer = () => {
  const [isPlaying, setIsPlaying] = useState(true);
  const paused = useSharedValue(false);
  const looping = useSharedValue(true);
  
  const { currentFrame, currentTime, duration, size } = useVideo(
    "https://bit.ly/skia-video",
    { paused, looping }
  );
  
  const togglePlay = () => {
    paused.value = !paused.value;
    setIsPlaying(!isPlaying);
  };
  
  return (
    <View style={styles.container}>
      <Canvas style={styles.canvas}>
        <Image
          image={currentFrame}
          x={0}
          y={0}
          width={size.width}
          height={size.height}
          fit="contain"
        />
      </Canvas>
      <View style={styles.controls}>
        <Pressable style={styles.button} onPress={togglePlay}>
          <Text>{isPlaying ? "Pause" : "Play"}</Text>
        </Pressable>
        <Text>
          {Math.floor(currentTime.value / 1000)}s / {Math.floor(duration / 1000)}s
        </Text>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  canvas: {
    flex: 1,
  },
  controls: {
    flexDirection: "row",
    justifyContent: "space-between",
    padding: 10,
    backgroundColor: "#000",
  },
  button: {
    padding: 10,
    backgroundColor: "#007AFF",
    borderRadius: 5,
  },
});

Performance Tips

  • Use appropriate video resolution for your use case
  • Consider using lower frame rates for better performance
  • Limit the number of simultaneous videos
  • Pause videos when not visible
  • Use hardware acceleration when available

Video Encoding

To encode videos from Skia images:

Supported Formats

Supported formats vary by platform: iOS:
  • MP4
  • MOV
  • M4V
Android:
  • MP4
  • 3GP
  • WebM
  • MKV
Web:
  • MP4
  • WebM
  • OGG

Troubleshooting

Video Not Playing

  • Check that the video URL is accessible
  • Verify the video format is supported
  • Ensure Reanimated is properly installed
  • Check Android API level (26+)

Poor Performance

  • Reduce video resolution
  • Lower frame rate
  • Use hardware-accelerated formats
  • Limit number of simultaneous videos

Audio Issues

  • Check volume settings
  • Verify audio is present in source file
  • Test with different video files

See Also

Build docs developers (and LLMs) love