Skip to content

Commit

Permalink
fix(📚): Improve documentation on useVideo (#2463)
Browse files Browse the repository at this point in the history
  • Loading branch information
wcandillon authored Jun 6, 2024
1 parent 04ddb02 commit df5162e
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 116 deletions.
173 changes: 82 additions & 91 deletions docs/docs/video.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ slug: /video

React Native Skia provides a way to load video frames as images, enabling rich multimedia experiences within your applications. A video frame can be used anywhere a Skia image is accepted: `Image`, `ImageShader`, and `Atlas`.

## Requirements
### Requirements

- **Reanimated** version 3 or higher.
- **Android:** API level 26 or higher.
- **Video URL:** Must be a local path. We recommend using it in combination with [expo-asset](https://docs.expo.dev/versions/latest/sdk/asset/) to download the video.

## Example

Here is an example of how to use the video support in React Native Skia. This example demonstrates how to load and display video frames within a canvas, applying a color matrix for visual effects. Tapping the screen will pause and play the video.

The video can be a remote (`http://...`) or local URL (`file://`), as well as a [video from the bundle](#using-assets).

```tsx twoslash
import React from "react";
import {
Expand All @@ -29,16 +30,11 @@ import {
import { Pressable, useWindowDimensions } from "react-native";
import { useSharedValue } from "react-native-reanimated";

interface VideoExampleProps {
localVideoFile: string;
}

// The URL needs to be a local path; we usually use expo-asset for that.
export const VideoExample = ({ localVideoFile }: VideoExampleProps) => {
export const VideoExample = () => {
const paused = useSharedValue(false);
const { width, height } = useWindowDimensions();
const { currentFrame } = useVideo(
require(localVideoFile),
"https://bit.ly/skia-video",
{
paused,
}
Expand Down Expand Up @@ -71,36 +67,70 @@ export const VideoExample = ({ localVideoFile }: VideoExampleProps) => {
};
```

## Using expo-asset
## Returned Values

The `useVideo` hook returns `currentFrame`, which contains the current video frame, as well as `currentTime`, `rotation`, and `size`.

## Playback Options

The following table describes the playback options available for the `useVideo` hook:

| Option | Description |
|---------------|----------------------------------------------------------------------------------------------|
| `seek` | Allows seeking to a specific point in the video in milliseconds. Default is `null`. |
| `paused` | Indicates whether the video is paused. |
| `looping` | Indicates whether the video should loop. |
| `volume` | A value from 0 to 1 representing the volume level (0 is muted, 1 is the maximum volume). |

Below is an example of how to use [expo-asset](https://docs.expo.dev/versions/latest/sdk/asset/) to load the video.
In the example below, every time we tap on the video, we set the video seek at 2 seconds.

```tsx twoslash
import { useVideo } from "@shopify/react-native-skia";
import { useAssets } from "expo-asset";
import React from "react";
import {
Canvas,
Fill,
Image,
useVideo
} from "@shopify/react-native-skia";
import { Pressable, useWindowDimensions } from "react-native";
import { useSharedValue } from "react-native-reanimated";

// Example usage:
// const video = useVideoFromAsset(require("./BigBuckBunny.mp4"));
export 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);
export const VideoExample = () => {
const seek = useSharedValue<null | number>(null);
// Set this value to true to pause the video
const paused = useSharedValue(false);
const { width, height } = useWindowDimensions();
const {currentFrame, currentTime} = useVideo(
"https://bit.ly/skia-video",
{
seek,
paused,
looping: true
}
);
return (
<Pressable
style={{ flex: 1 }}
onPress={() => (seek.value = 2000)}
>
<Canvas style={{ flex: 1 }}>
<Image
image={currentFrame}
x={0}
y={0}
width={width}
height={height}
fit="cover"
/>
</Canvas>
</Pressable>
);
};
```

## Returned Values

The `useVideo` hook returns `currentFrame` which contains the current video frame, as well as `currentTime`, `rotation`, and `size`.

## Rotated Video

`rotation` can either be `0`, `90`, `180`, or `270`.
We provide a `fitbox` function that can help rotating and scaling the video.
The `rotation` property can be `0`, `90`, `180`, or `270`. We provide a `fitbox` function that can help with rotating and scaling the video.

```tsx twoslash
import React from "react";
Expand All @@ -114,15 +144,10 @@ import {
import { Pressable, useWindowDimensions } from "react-native";
import { useSharedValue } from "react-native-reanimated";

interface VideoExampleProps {
localVideoFile: string;
}

// The URL needs to be a local path; we usually use expo-asset for that.
export const VideoExample = ({ localVideoFile }: VideoExampleProps) => {
export const VideoExample = () => {
const paused = useSharedValue(false);
const { width, height } = useWindowDimensions();
const { currentFrame, rotation, size } = useVideo(require(localVideoFile));
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);
Expand All @@ -142,62 +167,28 @@ export const VideoExample = ({ localVideoFile }: VideoExampleProps) => {
};
```

## Using Assets

## Playback Options

You can seek a video via the `seek` playback option. By default, the seek option is null. If you set a value in milliseconds, it will seek to that point in the video and then set the option value to null again.

`looping` indicates whether the video should be looped or not.

`playbackSpeed` indicates the playback speed of the video (default is 1).

In the example below, every time we tap on the video, we set the video to 2 seconds.
Below is an example where we use [expo-asset](https://docs.expo.dev/versions/latest/sdk/asset/) to load a video file from the bundle.

```tsx twoslash
import React from "react";
import {
Canvas,
Fill,
Image,
useVideo
} from "@shopify/react-native-skia";
import { Pressable, useWindowDimensions } from "react-native";
import { useSharedValue } from "react-native-reanimated";

interface VideoExampleProps {
localVideoFile: string;
}
import { useVideo } from "@shopify/react-native-skia";
import { useAssets } from "expo-asset";

export const VideoExample = ({ localVideoFile }: VideoExampleProps) => {
const seek = useSharedValue<null | number>(null);
// Set this value to true to pause the video
const paused = useSharedValue(false);
const { width, height } = useWindowDimensions();
const {currentFrame, currentTime} = useVideo(
require(localVideoFile),
{
seek,
paused,
looping: true,
playbackSpeed: 1
}
);
return (
<Pressable
style={{ flex: 1 }}
onPress={() => (seek.value = 2000)}
>
<Canvas style={{ flex: 1 }}>
<Image
image={currentFrame}
x={0}
y={0}
width={width}
height={height}
fit="cover"
/>
</Canvas>
</Pressable>
);
// Example usage:
// const video = useVideoFromAsset(require("./BigBuckBunny.mp4"));
export 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);
};
```
```

## Video Encoding

To encode videos from Skia images, you can use ffmpeg or also look into [react-native-skia-video](https://github.com/AzzappApp/react-native-skia-video).
31 changes: 6 additions & 25 deletions package/src/external/reanimated/useVideo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,16 @@ import { Platform } from "../../Platform";

import Rea from "./ReanimatedProxy";

export type Animated<T> = SharedValue<T> | T;
// TODO: Move to useVideo.ts
export interface PlaybackOptions {
playbackSpeed: Animated<number>;
type Animated<T> = SharedValue<T> | T;

interface PlaybackOptions {
looping: Animated<boolean>;
paused: Animated<boolean>;
seek: Animated<number | null>;
volume: Animated<number>;
}

type Materialized<T> = {
[K in keyof T]: T[K] extends Animated<infer U> ? U : T[K];
};

export type MaterializedPlaybackOptions = Materialized<
Omit<PlaybackOptions, "seek">
>;

// TODO: move
export const setFrame = (
video: Video,
currentFrame: SharedValue<SkImage | null>
) => {
const setFrame = (video: Video, currentFrame: SharedValue<SkImage | null>) => {
"worklet";
const img = video.nextImage();
if (img) {
Expand All @@ -45,7 +32,6 @@ export const setFrame = (
};

const defaultOptions = {
playbackSpeed: 1,
looping: true,
paused: false,
seek: null,
Expand Down Expand Up @@ -76,16 +62,15 @@ export const useVideo = (
const looping = useOption(userOptions?.looping ?? defaultOptions.looping);
const seek = useOption(userOptions?.seek ?? defaultOptions.seek);
const volume = useOption(userOptions?.volume ?? defaultOptions.volume);
const playbackSpeed = useOption(
userOptions?.playbackSpeed ?? defaultOptions.playbackSpeed
);
const currentFrame = Rea.useSharedValue<null | SkImage>(null);
const currentTime = Rea.useSharedValue(0);
const lastTimestamp = Rea.useSharedValue(-1);
const duration = useMemo(() => video?.duration() ?? 0, [video]);
const framerate = useMemo(() => video?.framerate() ?? 0, [video]);
const size = useMemo(() => video?.size() ?? { width: 0, height: 0 }, [video]);
const rotation = useMemo(() => video?.rotation() ?? 0, [video]);
const frameDuration = 1000 / framerate;
const currentFrameDuration = Math.floor(frameDuration);
Rea.useAnimatedReaction(
() => isPaused.value,
(paused) => {
Expand Down Expand Up @@ -127,10 +112,6 @@ export const useVideo = (
}
const delta = currentTimestamp - lastTimestamp.value;

const frameDuration = 1000 / framerate;
const currentFrameDuration = Math.floor(
frameDuration / playbackSpeed.value
);
const isOver = currentTime.value + delta > duration;
if (isOver && looping.value) {
seek.value = 0;
Expand Down

0 comments on commit df5162e

Please sign in to comment.