@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
248 lines (242 loc) • 7.38 kB
JavaScript
;
import { useEffect, useRef, useImperativeHandle, forwardRef, useCallback, useMemo } from 'react';
import { View } from 'react-native';
import globalStyles from "../VideoPlayer/styles/globalStyles.js";
import Video from 'react-native-video';
import { useAdsPlayerStore } from "./store/adsPlayerStore.js";
import { AdMediaControlsProvider } from "./MediaControls/index.js";
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { runOnJS } from 'react-native-reanimated';
import { toggleAdControls, showAdControls } from "./utils/controls.js";
import { resetAdState } from "./utils/adStateReset.js";
import { useAdTracking } from "./utils/useAdTracking.js";
import { jsx as _jsx } from "react/jsx-runtime";
const AdsPlayer = /*#__PURE__*/forwardRef(({
onAdEnd,
onAdSkip,
onAdError,
onAdTracking,
onClose,
insets
}, ref) => {
const {
currentAd,
adDuration,
adCurrentTime,
isAdPaused,
isAdMuted,
isAdPlaying,
setIsAdPaused,
setAdCurrentTime,
setAdDuration,
setShowSkipButton,
setIsAdBuffering
} = useAdsPlayerStore();
const adVideoRef = useRef(null);
const trackedQuartilesRef = useRef({
firstQuartile: false,
midpoint: false,
thirdQuartile: false
});
const trackedImpressionRef = useRef(null);
const {
trackAdImpression,
trackAdStart,
trackAdComplete,
trackAdQuartile
} = useAdTracking({
onAdTracking
});
const trigger = useCallback(() => {
toggleAdControls();
}, []);
const tapGesture = useMemo(() => Gesture.Tap().onEnd(() => {
runOnJS(trigger)();
}).maxDuration(250), [trigger]);
useImperativeHandle(ref, () => ({
seek: time => {
adVideoRef.current?.seek(time);
},
getCurrentTime: () => adCurrentTime,
getDuration: () => adDuration
}));
// Track impression only once per ad
useEffect(() => {
if (currentAd && isAdPlaying && trackedImpressionRef.current !== currentAd.id) {
try {
trackedImpressionRef.current = currentAd.id;
trackAdImpression(currentAd);
} catch (error) {
console.error('Error in ad impression tracking:', error);
}
}
}, [currentAd, isAdPlaying, trackAdImpression]);
// Separate effect for ensuring ad is not paused
useEffect(() => {
if (currentAd && isAdPlaying) {
setIsAdPaused(false);
showAdControls();
}
}, [currentAd, isAdPlaying, setIsAdPaused]);
useEffect(() => {
if (currentAd?.skippable) {
if (currentAd.skipAfter > 0) {
const timer = setTimeout(() => {
setShowSkipButton(true);
}, currentAd.skipAfter * 1000);
return () => clearTimeout(timer);
}
setShowSkipButton(true);
return undefined;
}
setShowSkipButton(false);
return undefined;
}, [currentAd, setShowSkipButton]);
const handleAdProgress = data => {
setAdCurrentTime(data.currentTime);
setAdDuration(data.seekableDuration || 0);
// Reset buffering state when progress updates (playback has resumed)
const {
isAdBuffering
} = useAdsPlayerStore.getState();
if (isAdBuffering) {
setIsAdBuffering(false);
}
if (currentAd && adDuration > 0) {
const progress = data.currentTime / adDuration;
if (progress >= 0.25 && !trackedQuartilesRef.current.firstQuartile) {
trackedQuartilesRef.current.firstQuartile = true;
trackAdQuartile(currentAd, 'firstQuartile');
}
if (progress >= 0.5 && !trackedQuartilesRef.current.midpoint) {
trackedQuartilesRef.current.midpoint = true;
trackAdQuartile(currentAd, 'midpoint');
}
if (progress >= 0.75 && !trackedQuartilesRef.current.thirdQuartile) {
trackedQuartilesRef.current.thirdQuartile = true;
trackAdQuartile(currentAd, 'thirdQuartile');
}
}
};
const handleAdLoadStart = _ => {
setIsAdBuffering(true);
};
const handleAdLoad = () => {
try {
setIsAdBuffering(false);
if (currentAd) {
setAdDuration(currentAd.duration);
trackAdStart(currentAd);
}
} catch (error) {
console.error('Error in handleAdLoad:', error);
if (currentAd && onAdError) {
onAdError(error instanceof Error ? error : new Error('Ad load failed'), currentAd);
}
}
};
const handleAdEnd = () => {
if (!currentAd) return;
try {
trackAdComplete(currentAd);
resetAdState();
trackedQuartilesRef.current = {
firstQuartile: false,
midpoint: false,
thirdQuartile: false
};
// Reset impression tracking for next ad
trackedImpressionRef.current = null;
onAdEnd(currentAd);
} catch (error) {
console.error('Error in handleAdEnd:', error);
resetAdState();
trackedImpressionRef.current = null;
onAdEnd(currentAd);
}
};
const handleAdError = error => {
if (!currentAd) return;
try {
const errorMessage = error.error?.localizedDescription || error.error?.errorString || 'Ad playback failed';
console.error('Ad playback error:', errorMessage, error);
resetAdState();
// Reset impression tracking on error
trackedImpressionRef.current = null;
if (onAdError) {
onAdError(new Error(errorMessage), currentAd);
} else {
// Fallback: treat error as ad end
onAdEnd(currentAd);
}
} catch (err) {
console.error('Error in handleAdError:', err);
resetAdState();
trackedImpressionRef.current = null;
if (currentAd && onAdEnd) {
onAdEnd(currentAd);
}
}
};
// Memoize video style for performance
const videoStyle = useMemo(() => [globalStyles.absoluteFill, {
transform: [{
scaleX: 1.24
}, {
scaleY: 1.15
}]
}], []);
// Memoize container style
const containerStyle = useMemo(() => [globalStyles.absoluteFill, {
backgroundColor: '#000'
}], []);
if (!currentAd || !isAdPlaying) {
return null;
}
return /*#__PURE__*/_jsx(GestureDetector, {
gesture: tapGesture,
children: /*#__PURE__*/_jsx(View, {
style: containerStyle,
collapsable: false,
children: /*#__PURE__*/_jsx(AdMediaControlsProvider, {
onAdSkip: onAdSkip,
onAdTracking: onAdTracking,
onClose: onClose,
insets: insets,
children: /*#__PURE__*/_jsx(Video, {
ref: adVideoRef,
source: {
uri: currentAd.source
},
paused: isAdPaused,
muted: isAdMuted,
onProgress: handleAdProgress,
onLoad: handleAdLoad,
onEnd: handleAdEnd,
onError: handleAdError,
onBuffer: ({
isBuffering
}) => {
if (isBuffering) {
setIsAdBuffering(true);
} else {
setTimeout(() => setIsAdBuffering(false), 150);
}
},
onLoadStart: handleAdLoadStart,
resizeMode: "contain",
style: videoStyle,
controls: false,
playInBackground: false,
playWhenInactive: false,
pointerEvents: "box-none",
ignoreSilentSwitch: "ignore",
progressUpdateInterval: 250
})
})
})
});
});
AdsPlayer.displayName = 'AdsPlayer';
export default AdsPlayer;
//# sourceMappingURL=AdsPlayer.js.map