@zezosoft/zezo-ott-react-native-video-player
Version:
Production-ready React Native OTT video player library for Android & iOS. Features: playlists, seasons, auto-next playback, subtitles (SRT/VTT), custom theming, analytics tracking, fullscreen mode, gesture controls, ads player (pre-roll/mid-roll/post-roll
244 lines (235 loc) • 9.38 kB
JavaScript
"use strict";
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AppState, View } from 'react-native';
import Video from 'react-native-video';
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
import { runOnJS, useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';
import { hideControlsStyles, videoRef } from "./utils/index.js";
import { getVideoSource, handlePause } from "./utils/index.js";
import MediaControlsProvider from "./MediaControls/MediaControlsProvider.js";
import { useVideoPlayerStore } from "./store/videoPlayerStore.js";
import { useAdsPlayerStore } from "../AdsPlayer/store/adsPlayerStore.js";
import { createPlayerEvents } from "./utils/index.js";
import globalStyles from "./styles/globalStyles.js";
import { VideoPlayerConfigProvider } from "./context/index.js";
import { useWatchReporter } from "./utils/index.js";
import Toast from "./components/Toast.js";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const VideoPlayerCore = ({
onClose,
seekTime,
event,
autoNext = true,
theme,
onWatchProgress,
insets,
isPausedOverride,
onVideoEnd
}) => {
// Use separate selectors to avoid infinite loop (object selectors create new objects on each call)
const isPaused = useVideoPlayerStore(state => state.isPaused);
const playBackRate = useVideoPlayerStore(state => state.playBackRate);
const resizeMode = useVideoPlayerStore(state => state.resizeMode);
const controlsVisible = useVideoPlayerStore(state => state.controlsVisible);
const selectedAudioTrack = useVideoPlayerStore(state => state.selectedAudioTrack);
const selectedSubtitleTrack = useVideoPlayerStore(state => state.selectedSubtitleTrack);
const selectedVideoTrack = useVideoPlayerStore(state => state.selectedVideoTrack);
const maxBitRate = useVideoPlayerStore(state => state.maxBitRate);
const playList = useVideoPlayerStore(state => state.playList);
const currentTrackIndex = useVideoPlayerStore(state => state.currentTrackIndex);
const activeTrack = useVideoPlayerStore(state => state.activeTrack);
const setResizeMode = useVideoPlayerStore(state => state.setResizeMode);
const setControlsVisible = useVideoPlayerStore(state => state.setControlsVisible);
const setIsPaused = useVideoPlayerStore(state => state.setIsPaused);
const setError = useVideoPlayerStore(state => state.setError);
const playerEventsRef = useRef(createPlayerEvents());
const playerEvents = playerEventsRef.current;
const {
reportProgress
} = useWatchReporter({
onWatchProgress
});
const seekTimeRef = useRef(seekTime);
// Toast state
const [toastMessage, setToastMessage] = useState(null);
const toastOpacity = useSharedValue(0);
// Pinch gesture state
const pinchStartScale = useRef(1);
useEffect(() => {
seekTimeRef.current = seekTime;
}, [seekTime]);
const effectivePaused = useMemo(() => isPausedOverride !== undefined ? isPausedOverride : isPaused, [isPausedOverride, isPaused]);
const videoSource = useMemo(() => getVideoSource({
playList,
currentTrackIndex
}), [playList, currentTrackIndex]);
const selectedAudioTrackMemo = useMemo(() => selectedAudioTrack, [selectedAudioTrack]);
const selectedTextTrackMemo = useMemo(() => selectedSubtitleTrack?.value === 'Off' ? undefined : selectedSubtitleTrack, [selectedSubtitleTrack]);
const selectedVideoTrackMemo = useMemo(() => selectedVideoTrack ? {
type: selectedVideoTrack.type,
value: selectedVideoTrack.value
} : undefined, [selectedVideoTrack]);
// Memoize video style for performance
const videoStyle = useMemo(() => globalStyles.absoluteFill, []);
// Memoize buffer config for performance
const bufferConfig = useMemo(() => ({
minBufferMs: 15000,
maxBufferMs: 50000,
bufferForPlaybackMs: 2500,
bufferForPlaybackAfterRebufferMs: 5000
}), []);
useEffect(() => {
if (seekTimeRef.current && !activeTrack?.isTrailer && videoRef?.current) {
videoRef.current.seek(seekTimeRef.current);
}
}, [seekTime, activeTrack?.isTrailer]);
const handleAppStateChange = useCallback(nextState => {
if (nextState === 'inactive' || nextState === 'background') {
handlePause();
setIsPaused(true);
reportProgress('PROGRESS');
}
}, [setIsPaused, reportProgress]);
useEffect(() => {
const sub = AppState.addEventListener('change', handleAppStateChange);
return () => sub.remove();
}, [handleAppStateChange]);
const handleProgress = useCallback(progressEvent => {
playerEvents.onProgress(progressEvent);
}, [playerEvents]);
const handleVideoEnd = useCallback(() => {
if (onVideoEnd) {
onVideoEnd();
} else {
playerEvents.onEnd({
reportProgress,
onPressEpisode: event?.onPressEpisode ?? (async () => true),
autoNext
});
}
}, [onVideoEnd, playerEvents, reportProgress, event?.onPressEpisode, autoNext]);
const handleError = useCallback(() => {
setError("Video couldn't be loaded");
}, [setError]);
const handleTap = useCallback(() => {
setControlsVisible(!controlsVisible);
}, [controlsVisible, setControlsVisible]);
const showToast = useCallback(message => {
setToastMessage(message);
toastOpacity.value = withTiming(1, {
duration: 200
});
setTimeout(() => {
toastOpacity.value = withTiming(0, {
duration: 200
}, () => {
runOnJS(setToastMessage)(null);
});
}, 2000);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]);
const handlePinchEnd = useCallback(finalScale => {
// Determine pinch direction based on scale change
// If scale increased (pinch out) → cover (zoomed to fill)
// If scale decreased (pinch in) → contain (original)
const scaleChange = finalScale - pinchStartScale.current;
const newMode = scaleChange > 0 ? 'cover' : 'contain';
setResizeMode(newMode);
// Show toast message
const message = newMode === 'cover' ? 'Zoomed to Fill' : 'Original';
showToast(message);
}, [setResizeMode, showToast]);
// Animated style for toast
const toastAnimatedStyle = useAnimatedStyle(() => {
return {
opacity: toastOpacity.value
};
}, []);
const pinchGesture = useMemo(() => Gesture.Pinch().onStart(pinchEvent => {
'worklet';
pinchStartScale.current = pinchEvent.scale;
}).onEnd(pinchEvent => {
'worklet';
runOnJS(handlePinchEnd)(pinchEvent.scale);
}), [handlePinchEnd]);
const tapGesture = useMemo(() => Gesture.Tap().onEnd(() => {
runOnJS(handleTap)();
}).maxDuration(250), [handleTap]);
const composedGestures = useMemo(() => Gesture.Race(pinchGesture, tapGesture), [pinchGesture, tapGesture]);
const handleClose = useCallback(() => {
reportProgress('VIDEO_CLOSE');
// Reset stores after report is sent
setTimeout(() => {
const {
resetStore
} = useVideoPlayerStore.getState();
const {
resetAdsStore
} = useAdsPlayerStore.getState();
resetStore();
resetAdsStore();
}, 0);
onClose?.();
}, [reportProgress, onClose]);
const handlePressEpisode = useCallback(async ({
episode
}) => {
if (event?.onPressEpisode) {
return await event.onPressEpisode({
episode
});
}
return Promise.resolve(true);
}, [event]);
return /*#__PURE__*/_jsx(VideoPlayerConfigProvider, {
theme: theme,
children: /*#__PURE__*/_jsx(GestureHandlerRootView, {
style: globalStyles.flexOne,
children: /*#__PURE__*/_jsx(MediaControlsProvider, {
onClose: handleClose,
onPressEpisode: handlePressEpisode,
reportProgress: reportProgress,
insets: insets,
children: /*#__PURE__*/_jsx(GestureDetector, {
gesture: composedGestures,
children: /*#__PURE__*/_jsxs(View, {
style: globalStyles.flexOneWithBlackBackground,
children: [/*#__PURE__*/_jsx(Video, {
ref: videoRef,
source: videoSource,
paused: effectivePaused,
onProgress: handleProgress,
onLoad: playerEvents.onLoad,
onBuffer: playerEvents.onBuffer,
onEnd: handleVideoEnd,
onLoadStart: playerEvents.onLoadStart,
onError: handleError,
controls: false,
resizeMode: resizeMode || 'contain',
rate: playBackRate || 1,
selectedAudioTrack: selectedAudioTrackMemo,
selectedTextTrack: selectedTextTrackMemo,
selectedVideoTrack: selectedVideoTrackMemo,
maxBitRate: maxBitRate ?? undefined,
style: videoStyle,
controlsStyles: hideControlsStyles,
ignoreSilentSwitch: "ignore",
playInBackground: false,
playWhenInactive: false,
allowsExternalPlayback: true,
bufferConfig: bufferConfig
}), toastMessage && /*#__PURE__*/_jsx(Toast, {
message: toastMessage,
animatedStyle: toastAnimatedStyle,
insets: insets,
position: "top"
})]
})
})
})
})
});
};
export default /*#__PURE__*/React.memo(VideoPlayerCore);
//# sourceMappingURL=VideoPlayerCore.js.map