@azzapp/react-native-skia-video
Version:
video support for react-native-skia
189 lines (175 loc) • 4.92 kB
text/typescript
import {
useSharedValue,
useFrameCallback,
type SharedValue,
} from 'react-native-reanimated';
import { useCallback, useEffect, useMemo, useState } from 'react';
import useEventListener from './utils/useEventListener';
import type {
BufferingRange,
VideoDimensions,
VideoFrame,
VideoPlayer,
} from './types';
import RNSkiaVideoModule from './RNSkiaVideoModule';
type UseVideoPlayerOptions = {
/**
* The URI of the video to play.
* if null, the video player won't be created.
*/
uri: string | null;
/**
* If provided, the resolution to scale the video to.
* If not provided, the original resolution of the video will be used.
* Downscaling the video can improve performance.
* Changing the resolution after the video player will lead to re-creating the video player.
*/
resolution?: { width: number; height: number } | null;
/**
* Whether the video should start playing automatically.
*/
autoPlay?: boolean;
/**
* Weather the video should loop.
*/
isLooping?: boolean;
/**
* The volume of the video.
*/
volume?: number;
/**
* Callback that is called when the video is ready to play.
* @param dimensions The dimensions of the video.
*/
onReadyToPlay?: (dimensions: VideoDimensions) => void;
/**
* Callback that is called when the video starts buffering.
*/
onBufferingStart?: () => void;
/**
* Callback that is called when the video stops buffering.
*/
onBufferingEnd?: () => void;
/**
* Callback that is called when the buffered ranges are updated.
* @param loadedRanges The buffered ranges.
*/
onBufferingUpdate?: (loadedRanges: BufferingRange[]) => void;
/**
* Callback that is called when the video playback completes.
*/
onComplete?: () => void;
/**
* Callback that is called when an error occurs.
* @param error the error that occurred.
* @param retry a function that can be called to retry the operation.
*/
onError?: (error: any, retry: () => void) => void;
/**
* Callback that is called when the playing status changes.
* @param playing Whether the video is playing.
*/
onPlayingStatusChange?: (playing: boolean) => void;
/**
* Callback that is called when a seek operation completes.
* @returns
*/
onSeekComplete?: () => void;
};
type VideoPlayerController = Pick<
VideoPlayer,
'currentTime' | 'duration' | 'play' | 'pause' | 'seekTo' | 'isPlaying'
>;
type UseVideoPlayerReturnType = {
/**
* The current frame of the playing video.
*/
currentFrame: SharedValue<VideoFrame | null>;
/**
* The video player controller.
*/
player: VideoPlayerController | null;
};
/**
* Hook that creates a video player and manages its state.
* @param options The options for the video player.
* @returns
*/
export const useVideoPlayer = ({
uri,
resolution,
autoPlay = false,
isLooping = false,
volume = 1,
onReadyToPlay,
onBufferingStart,
onBufferingEnd,
onBufferingUpdate,
onComplete,
onError,
onPlayingStatusChange,
onSeekComplete,
}: UseVideoPlayerOptions): UseVideoPlayerReturnType => {
const [isErrored, setIsErrored] = useState(false);
const player = useMemo(() => {
if (uri && !isErrored) {
return RNSkiaVideoModule.createVideoPlayer(uri, resolution);
}
return null;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isErrored, resolution?.width, resolution?.height, uri]);
const currentFrame = useSharedValue<null | VideoFrame>(null);
useEffect(
() => () => {
currentFrame.value = null;
player?.dispose();
},
[player, currentFrame]
);
const retry = useCallback(() => {
setIsErrored(false);
}, []);
const errorHandler = useCallback(
(error: any) => {
onError?.(error, retry);
setIsErrored(true);
},
[onError, retry]
);
useEffect(() => {
if (player) {
player.isLooping = isLooping;
}
}, [player, isLooping]);
useEffect(() => {
if (player) {
player.volume = volume;
}
}, [player, volume]);
useEventListener(player, 'ready', onReadyToPlay);
useEventListener(player, 'bufferingStart', onBufferingStart);
useEventListener(player, 'bufferingEnd', onBufferingEnd);
useEventListener(player, 'bufferingUpdate', onBufferingUpdate);
useEventListener(player, 'complete', onComplete);
useEventListener(player, 'error', errorHandler);
useEventListener(player, 'playingStatusChange', onPlayingStatusChange);
useEventListener(player, 'seekComplete', onSeekComplete);
useEffect(() => {
if (autoPlay) {
player?.play();
}
}, [player, autoPlay]);
useFrameCallback(() => {
if (!player || (!player.isPlaying && currentFrame.value)) {
return;
}
const nextFrame = player.decodeNextFrame();
if (nextFrame) {
currentFrame.value = nextFrame;
}
}, true);
return {
currentFrame,
player,
};
};