UNPKG

@shopify/react-native-skia

Version:

High-performance React Native Graphics using Skia

160 lines (146 loc) 4.36 kB
import type { SharedValue, FrameInfo } from "react-native-reanimated"; import { useEffect, useMemo } from "react"; import type { SkImage, Video } from "../../skia/types"; import { Platform } from "../../Platform"; import Rea from "./ReanimatedProxy"; import { useVideoLoading } from "./useVideoLoading"; type MaybeAnimated<T> = SharedValue<T> | T; interface PlaybackOptions { looping: MaybeAnimated<boolean>; paused: MaybeAnimated<boolean>; seek: MaybeAnimated<number | null>; volume: MaybeAnimated<number>; } const copyFrameOnAndroid = (currentFrame: SharedValue<SkImage | null>) => { "worklet"; // on android we need to copy the texture before it's invalidated if (Platform.OS === "android") { const tex = currentFrame.value; if (tex) { currentFrame.value = tex.makeNonTextureImage(); tex.dispose(); } } }; const setFrame = (video: Video, currentFrame: SharedValue<SkImage | null>) => { "worklet"; const img = video.nextImage(); if (img) { if (currentFrame.value) { currentFrame.value.dispose(); } currentFrame.value = img; copyFrameOnAndroid(currentFrame); } }; const defaultOptions = { looping: true, paused: false, seek: null, currentTime: 0, volume: 0, }; const useOption = <T>(value: MaybeAnimated<T>) => { "worklet"; // TODO: only create defaultValue is needed (via makeMutable) const defaultValue = Rea.useSharedValue( Rea.isSharedValue(value) ? value.value : value ); return Rea.isSharedValue(value) ? (value as SharedValue<T>) : (defaultValue as SharedValue<T>); }; const disposeVideo = (video: Video | null) => { "worklet"; video?.dispose(); }; export const useVideo = ( source: string | null, userOptions?: Partial<PlaybackOptions> ) => { const video = useVideoLoading(source); const isPaused = useOption(userOptions?.paused ?? defaultOptions.paused); const looping = useOption(userOptions?.looping ?? defaultOptions.looping); const seek = useOption(userOptions?.seek ?? defaultOptions.seek); const volume = useOption(userOptions?.volume ?? defaultOptions.volume); 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( () => (Platform.OS === "web" ? -1 : 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) => { if (paused) { video?.pause(); } else { lastTimestamp.value = -1; video?.play(); } } ); Rea.useAnimatedReaction( () => seek.value, (value) => { if (value !== null) { video?.seek(value); currentTime.value = value; seek.value = null; } } ); Rea.useAnimatedReaction( () => volume.value, (value) => { video?.setVolume(value); } ); Rea.useFrameCallback((frameInfo: FrameInfo) => { "worklet"; if (!video) { return; } if (isPaused.value) { return; } const currentTimestamp = frameInfo.timestamp; if (lastTimestamp.value === -1) { lastTimestamp.value = currentTimestamp; } const delta = currentTimestamp - lastTimestamp.value; const isOver = currentTime.value + delta > duration; if (isOver && looping.value) { seek.value = 0; currentTime.value = seek.value; lastTimestamp.value = currentTimestamp; } // On Web the framerate is uknown. // This could be optimized by using requestVideoFrameCallback (Chrome only) if ((delta >= currentFrameDuration && !isOver) || Platform.OS === "web") { setFrame(video, currentFrame); currentTime.value += delta; lastTimestamp.value = currentTimestamp; } }); useEffect(() => { return () => { // TODO: should video simply be a shared value instead? Rea.runOnUI(disposeVideo)(video); }; }, [video]); return { currentFrame, currentTime, duration, framerate, rotation, size, }; };