Skip to content

Commit 7284ec6

Browse files
authored
fix(📼): add seek playback option (#2448)
1 parent 83e793f commit 7284ec6

File tree

2 files changed

+82
-4
lines changed

2 files changed

+82
-4
lines changed

Diff for: ‎docs/docs/video.md

+67-4
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ sidebar_label: Video
55
slug: /video
66
---
77

8-
React Native Skia provides a way to load video frames as images, enabling rich multimedia experiences within your applications.
9-
A video frame can be used anywhere a Skia image is accepted: `Image`, `ImageShader`, and `Atlas`.
8+
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`.
109

1110
## Requirements
1211

@@ -35,7 +34,7 @@ interface VideoExampleProps {
3534
localVideoFile: string;
3635
}
3736

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

7675
## Using expo-asset
7776

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

8079
```tsx twoslash
8180
import { useVideo } from "@shopify/react-native-skia";
@@ -93,4 +92,68 @@ export const useVideoFromAsset = (
9392
}
9493
return useVideo(assets ? assets[0].localUri : null, options);
9594
};
95+
```
96+
97+
## Playback Options
98+
99+
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.
100+
101+
There is also the `currentTime` option, which is a Reanimated value that contains the current playback time of the video.
102+
103+
`looping` indicates whether the video should be looped or not.
104+
105+
`playbackSpeed` indicates the playback speed of the video (default is 1).
106+
107+
In the example below, every time we tap on the video, we set the video to 2 seconds.
108+
109+
```tsx twoslash
110+
import React from "react";
111+
import {
112+
Canvas,
113+
Fill,
114+
Image,
115+
useVideo
116+
} from "@shopify/react-native-skia";
117+
import { Pressable, useWindowDimensions } from "react-native";
118+
import { useSharedValue } from "react-native-reanimated";
119+
120+
interface VideoExampleProps {
121+
localVideoFile: string;
122+
}
123+
124+
export const VideoExample = ({ localVideoFile }: VideoExampleProps) => {
125+
const seek = useSharedValue<null | number>(null);
126+
// Set this value to true to pause the video
127+
const paused = useSharedValue(false);
128+
// Contains the current playback time of the video
129+
const currentTime = useSharedValue(0);
130+
const { width, height } = useWindowDimensions();
131+
const video = useVideo(
132+
require(localVideoFile),
133+
{
134+
seek,
135+
paused,
136+
currentTime,
137+
looping: true,
138+
playbackSpeed: 1
139+
}
140+
);
141+
return (
142+
<Pressable
143+
style={{ flex: 1 }}
144+
onPress={() => (seek.value = 2000)}
145+
>
146+
<Canvas style={{ flex: 1 }}>
147+
<Image
148+
image={video}
149+
x={0}
150+
y={0}
151+
width={width}
152+
height={height}
153+
fit="cover"
154+
/>
155+
</Canvas>
156+
</Pressable>
157+
);
158+
};
96159
```

Diff for: ‎package/src/external/reanimated/useVideo.ts

+15
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@ export interface PlaybackOptions {
1818
playbackSpeed: Animated<number>;
1919
looping: Animated<boolean>;
2020
paused: Animated<boolean>;
21+
seek: Animated<number | null>;
22+
currentTime: Animated<number>;
2123
}
2224

2325
const defaultOptions = {
2426
playbackSpeed: 1,
2527
looping: true,
2628
paused: false,
29+
seek: null,
30+
currentTime: 0,
2731
};
2832

2933
const useOption = <T>(value: Animated<T>) => {
@@ -42,6 +46,10 @@ export const useVideo = (
4246
const video = useMemo(() => (source ? Skia.Video(source) : null), [source]);
4347
const isPaused = useOption(userOptions?.paused ?? defaultOptions.paused);
4448
const looping = useOption(userOptions?.looping ?? defaultOptions.looping);
49+
const seek = useOption(userOptions?.seek ?? defaultOptions.seek);
50+
const currentTime = useOption(
51+
userOptions?.currentTime ?? defaultOptions.currentTime
52+
);
4553
const playbackSpeed = useOption(
4654
userOptions?.playbackSpeed ?? defaultOptions.playbackSpeed
4755
);
@@ -64,6 +72,12 @@ export const useVideo = (
6472
if (!video) {
6573
return;
6674
}
75+
if (seek.value !== null) {
76+
video.seek(seek.value);
77+
seek.value = null;
78+
lastTimestamp.value = -1;
79+
startTimestamp.value = -1;
80+
}
6781
if (isPaused.value && lastTimestamp.value !== -1) {
6882
return;
6983
}
@@ -76,6 +90,7 @@ export const useVideo = (
7690

7791
// Calculate the current time in the video
7892
const currentTimestamp = timestamp - startTimestamp.value;
93+
currentTime.value = currentTimestamp;
7994

8095
// Handle looping
8196
if (currentTimestamp > duration && looping.value) {

0 commit comments

Comments
 (0)