Skip to content

Commit

Permalink
fix(📼): add seek playback option (#2448)
Browse files Browse the repository at this point in the history
  • Loading branch information
wcandillon authored May 28, 2024
1 parent 83e793f commit 7284ec6
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 4 deletions.
71 changes: 67 additions & 4 deletions docs/docs/video.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ sidebar_label: Video
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`.
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

Expand Down Expand Up @@ -35,7 +34,7 @@ interface VideoExampleProps {
localVideoFile: string;
}

// The URL needs to be a local path, we usually use expo-asset for that.
// The URL needs to be a local path; we usually use expo-asset for that.
export const VideoExample = ({ localVideoFile }: VideoExampleProps) => {
const paused = useSharedValue(false);
const { width, height } = useWindowDimensions();
Expand Down Expand Up @@ -75,7 +74,7 @@ export const VideoExample = ({ localVideoFile }: VideoExampleProps) => {

## Using expo-asset

Below is an example where we use [expo-asset](https://docs.expo.dev/versions/latest/sdk/asset/) to load the video.
Below is an example of how to use [expo-asset](https://docs.expo.dev/versions/latest/sdk/asset/) to load the video.

```tsx twoslash
import { useVideo } from "@shopify/react-native-skia";
Expand All @@ -93,4 +92,68 @@ export const useVideoFromAsset = (
}
return useVideo(assets ? assets[0].localUri : null, options);
};
```

## 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.

There is also the `currentTime` option, which is a Reanimated value that contains the current playback time of the video.

`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.

```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;
}

export const VideoExample = ({ localVideoFile }: VideoExampleProps) => {
const seek = useSharedValue<null | number>(null);
// Set this value to true to pause the video
const paused = useSharedValue(false);
// Contains the current playback time of the video
const currentTime = useSharedValue(0);
const { width, height } = useWindowDimensions();
const video = useVideo(
require(localVideoFile),
{
seek,
paused,
currentTime,
looping: true,
playbackSpeed: 1
}
);
return (
<Pressable
style={{ flex: 1 }}
onPress={() => (seek.value = 2000)}
>
<Canvas style={{ flex: 1 }}>
<Image
image={video}
x={0}
y={0}
width={width}
height={height}
fit="cover"
/>
</Canvas>
</Pressable>
);
};
```
15 changes: 15 additions & 0 deletions package/src/external/reanimated/useVideo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ export interface PlaybackOptions {
playbackSpeed: Animated<number>;
looping: Animated<boolean>;
paused: Animated<boolean>;
seek: Animated<number | null>;
currentTime: Animated<number>;
}

const defaultOptions = {
playbackSpeed: 1,
looping: true,
paused: false,
seek: null,
currentTime: 0,
};

const useOption = <T>(value: Animated<T>) => {
Expand All @@ -42,6 +46,10 @@ export const useVideo = (
const video = useMemo(() => (source ? Skia.Video(source) : null), [source]);
const isPaused = useOption(userOptions?.paused ?? defaultOptions.paused);
const looping = useOption(userOptions?.looping ?? defaultOptions.looping);
const seek = useOption(userOptions?.seek ?? defaultOptions.seek);
const currentTime = useOption(
userOptions?.currentTime ?? defaultOptions.currentTime
);
const playbackSpeed = useOption(
userOptions?.playbackSpeed ?? defaultOptions.playbackSpeed
);
Expand All @@ -64,6 +72,12 @@ export const useVideo = (
if (!video) {
return;
}
if (seek.value !== null) {
video.seek(seek.value);
seek.value = null;
lastTimestamp.value = -1;
startTimestamp.value = -1;
}
if (isPaused.value && lastTimestamp.value !== -1) {
return;
}
Expand All @@ -76,6 +90,7 @@ export const useVideo = (

// Calculate the current time in the video
const currentTimestamp = timestamp - startTimestamp.value;
currentTime.value = currentTimestamp;

// Handle looping
if (currentTimestamp > duration && looping.value) {
Expand Down

0 comments on commit 7284ec6

Please sign in to comment.