From df5162e29b9e8ea773b84899f23035513df66c67 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 16:53:37 +0200 Subject: [PATCH] =?UTF-8?q?fix(=F0=9F=93=9A):=20Improve=20documentation=20?= =?UTF-8?q?on=20useVideo=20(#2463)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs/video.md | 173 ++++++++++---------- package/src/external/reanimated/useVideo.ts | 31 +--- 2 files changed, 88 insertions(+), 116 deletions(-) diff --git a/docs/docs/video.md b/docs/docs/video.md index 1e692b8cba..670ed29118 100644 --- a/docs/docs/video.md +++ b/docs/docs/video.md @@ -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 { @@ -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, } @@ -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[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); + // 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 ( + (seek.value = 2000)} + > + + + + + ); }; ``` -## 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"; @@ -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); @@ -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); - // 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 ( - (seek.value = 2000)} - > - - - - - ); +// Example usage: +// const video = useVideoFromAsset(require("./BigBuckBunny.mp4")); +export const useVideoFromAsset = ( + mod: number, + options?: Parameters[1] +) => { + const [assets, error] = useAssets([mod]); + if (error) { + throw error; + } + return useVideo(assets ? assets[0].localUri : null, options); }; -``` \ No newline at end of file +``` + +## 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). \ No newline at end of file diff --git a/package/src/external/reanimated/useVideo.ts b/package/src/external/reanimated/useVideo.ts index 4e46a6c3e9..0d9638e840 100644 --- a/package/src/external/reanimated/useVideo.ts +++ b/package/src/external/reanimated/useVideo.ts @@ -7,29 +7,16 @@ import { Platform } from "../../Platform"; import Rea from "./ReanimatedProxy"; -export type Animated = SharedValue | T; -// TODO: Move to useVideo.ts -export interface PlaybackOptions { - playbackSpeed: Animated; +type Animated = SharedValue | T; + +interface PlaybackOptions { looping: Animated; paused: Animated; seek: Animated; volume: Animated; } -type Materialized = { - [K in keyof T]: T[K] extends Animated ? U : T[K]; -}; - -export type MaterializedPlaybackOptions = Materialized< - Omit ->; - -// TODO: move -export const setFrame = ( - video: Video, - currentFrame: SharedValue -) => { +const setFrame = (video: Video, currentFrame: SharedValue) => { "worklet"; const img = video.nextImage(); if (img) { @@ -45,7 +32,6 @@ export const setFrame = ( }; const defaultOptions = { - playbackSpeed: 1, looping: true, paused: false, seek: null, @@ -76,9 +62,6 @@ 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); const currentTime = Rea.useSharedValue(0); const lastTimestamp = Rea.useSharedValue(-1); @@ -86,6 +69,8 @@ export const useVideo = ( 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) => { @@ -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;