UNPKG

@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
"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