Add new dependencies and enhance VideoPlayer functionality

This update introduces new dependencies including cheerio, cors, express, and puppeteer to support additional features. The VideoPlayer component has been enhanced to improve seeking behavior on Android, with a new AndroidVideoPlayer component for better performance. Additionally, state management for seeking has been refined, ensuring smoother playback and user experience across platforms.
This commit is contained in:
tapframe 2025-06-18 10:27:02 +05:30
parent 9e03619db7
commit d62874d20d
4 changed files with 1302 additions and 220 deletions

View file

@ -0,0 +1,119 @@
import React, { useEffect, useRef, useState } from 'react';
import { Platform } from 'react-native';
import Video, { VideoRef, SelectedTrack, BufferingStrategyType } from 'react-native-video';
interface VideoPlayerProps {
src: string;
paused: boolean;
volume: number;
currentTime: number;
selectedAudioTrack?: SelectedTrack;
selectedTextTrack?: SelectedTrack;
onProgress?: (data: { currentTime: number; playableDuration: number }) => void;
onLoad?: (data: { duration: number }) => void;
onError?: (error: any) => void;
onBuffer?: (data: { isBuffering: boolean }) => void;
onSeek?: (data: { currentTime: number; seekTime: number }) => void;
onEnd?: () => void;
}
export const AndroidVideoPlayer: React.FC<VideoPlayerProps> = ({
src,
paused,
volume,
currentTime,
selectedAudioTrack,
selectedTextTrack,
onProgress,
onLoad,
onError,
onBuffer,
onSeek,
onEnd,
}) => {
const videoRef = useRef<VideoRef>(null);
const [isLoaded, setIsLoaded] = useState(false);
const [isSeeking, setIsSeeking] = useState(false);
const [lastSeekTime, setLastSeekTime] = useState<number>(0);
// Only render on Android
if (Platform.OS !== 'android') {
return null;
}
useEffect(() => {
if (isLoaded && !isSeeking && Math.abs(currentTime - lastSeekTime) > 1) {
setIsSeeking(true);
videoRef.current?.seek(currentTime);
setLastSeekTime(currentTime);
}
}, [currentTime, isLoaded, isSeeking, lastSeekTime]);
const handleLoad = (data: any) => {
setIsLoaded(true);
onLoad?.(data);
};
const handleProgress = (data: any) => {
if (!isSeeking) {
onProgress?.(data);
}
};
const handleSeek = (data: any) => {
setIsSeeking(false);
onSeek?.(data);
};
const handleBuffer = (data: any) => {
onBuffer?.(data);
};
const handleError = (error: any) => {
console.error('Video playback error:', error);
onError?.(error);
};
const handleEnd = () => {
onEnd?.();
};
return (
<Video
ref={videoRef}
source={{ uri: src }}
style={{ flex: 1 }}
paused={paused}
volume={volume}
selectedAudioTrack={selectedAudioTrack}
selectedTextTrack={selectedTextTrack}
onLoad={handleLoad}
onProgress={handleProgress}
onSeek={handleSeek}
onBuffer={handleBuffer}
onError={handleError}
onEnd={handleEnd}
resizeMode="contain"
controls={false}
playInBackground={false}
playWhenInactive={false}
progressUpdateInterval={250}
allowsExternalPlayback={false}
bufferingStrategy={BufferingStrategyType.DEFAULT}
ignoreSilentSwitch="ignore"
mixWithOthers="inherit"
rate={1.0}
repeat={false}
reportBandwidth={true}
textTracks={[]}
useTextureView={false}
disableFocus={false}
minLoadRetryCount={3}
automaticallyWaitsToMinimizeStalling={true}
hideShutterView={false}
shutterColor="#000000"
/>
);
};
export default AndroidVideoPlayer;

View file

@ -27,6 +27,8 @@
"@types/react-native-video": "^5.0.20",
"axios": "^1.10.0",
"base64-js": "^1.5.1",
"cheerio": "^1.1.0",
"cors": "^2.8.5",
"date-fns": "^4.1.0",
"eventemitter3": "^5.0.1",
"expo": "~52.0.43",
@ -44,8 +46,10 @@
"expo-status-bar": "~2.0.1",
"expo-system-ui": "^4.0.9",
"expo-web-browser": "~14.0.2",
"express": "^5.1.0",
"lodash": "^4.17.21",
"node-fetch": "^2.6.7",
"puppeteer": "^24.10.1",
"react": "18.3.1",
"react-native": "0.76.9",
"react-native-awesome-slider": "^2.9.0",

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,7 @@ import { storageService } from '../../services/storageService';
import { logger } from '../../utils/logger';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { MaterialIcons } from '@expo/vector-icons';
import AndroidVideoPlayer from './AndroidVideoPlayer';
import {
DEFAULT_SUBTITLE_SIZE,
@ -32,6 +33,11 @@ import CustomSubtitles from './subtitles/CustomSubtitles';
import SourcesModal from './modals/SourcesModal';
const VideoPlayer: React.FC = () => {
// If on Android, use the AndroidVideoPlayer component
if (Platform.OS === 'android') {
return <AndroidVideoPlayer />;
}
const navigation = useNavigation();
const route = useRoute<RouteProp<RootStackParamList, 'Player'>>();
@ -70,6 +76,7 @@ const VideoPlayer: React.FC = () => {
const [selectedTextTrack, setSelectedTextTrack] = useState<number>(-1);
const [resizeMode, setResizeMode] = useState<ResizeModeType>('stretch');
const [buffered, setBuffered] = useState(0);
const [seekPosition, setSeekPosition] = useState<number | null>(null);
const vlcRef = useRef<any>(null);
const [showAudioModal, setShowAudioModal] = useState(false);
const [showSubtitleModal, setShowSubtitleModal] = useState(false);
@ -92,6 +99,7 @@ const VideoPlayer: React.FC = () => {
const progressAnim = useRef(new Animated.Value(0)).current;
const progressBarRef = useRef<View>(null);
const [isDragging, setIsDragging] = useState(false);
const isSeeking = useRef(false);
const seekDebounceTimer = useRef<NodeJS.Timeout | null>(null);
const pendingSeekValue = useRef<number | null>(null);
const lastSeekTime = useRef<number>(0);
@ -131,7 +139,6 @@ const VideoPlayer: React.FC = () => {
left: 0,
width: screenWidth,
height: screenHeight,
backgroundColor: '#000',
};
};
@ -294,66 +301,62 @@ const VideoPlayer: React.FC = () => {
};
}, [id, type, currentTime, duration]);
const onPlaying = () => {
if (isMounted.current && !isSeeking.current) {
setPaused(false);
}
};
const onPaused = () => {
if (isMounted.current) {
setPaused(true);
}
};
const seekToTime = (timeInSeconds: number) => {
if (!isPlayerReady || duration <= 0 || !vlcRef.current) return;
const normalizedPosition = Math.max(0, Math.min(timeInSeconds / duration, 1));
try {
if (Platform.OS === 'android') {
// On Android, we need to handle seeking differently to prevent black screens
setIsBuffering(true);
// Set a small timeout to prevent overwhelming the player
const now = Date.now();
if (now - lastSeekTime.current < 300) {
// Throttle seeks that are too close together
if (seekDebounceTimer.current) {
clearTimeout(seekDebounceTimer.current);
}
seekDebounceTimer.current = setTimeout(() => {
if (vlcRef.current) {
// Set position instead of using seek on Android
vlcRef.current.setPosition(normalizedPosition);
lastSeekTime.current = Date.now();
// Give the player some time to recover
setTimeout(() => {
setIsBuffering(false);
}, 500);
}
}, 300);
return;
}
// Directly set position
vlcRef.current.setPosition(normalizedPosition);
lastSeekTime.current = now;
// Reset buffering state after a delay
setTimeout(() => {
setIsBuffering(false);
}, 500);
} else {
// For iOS, keep the original behavior
if (typeof vlcRef.current.setPosition === 'function') {
vlcRef.current.setPosition(normalizedPosition);
} else if (typeof vlcRef.current.seek === 'function') {
vlcRef.current.seek(normalizedPosition);
} else {
logger.error('[VideoPlayer] No seek method available on VLC player');
}
if (vlcRef.current && duration > 0 && !isSeeking.current) {
if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s`);
}
isSeeking.current = true;
// For Android, use direct seeking on VLC player ref instead of seek prop
if (Platform.OS === 'android' && vlcRef.current.seek) {
// Calculate position as fraction
const position = timeInSeconds / duration;
vlcRef.current.seek(position);
// Clear seek state after Android seek
setTimeout(() => {
if (isMounted.current) {
isSeeking.current = false;
}
}, 300);
} else {
// iOS fallback - use seek prop
const position = timeInSeconds / duration;
setSeekPosition(position);
setTimeout(() => {
if (isMounted.current) {
setSeekPosition(null);
isSeeking.current = false;
}
}, 500);
}
} else {
if (DEBUG_MODE) {
logger.error('[VideoPlayer] Seek failed: Player not ready, duration is zero, or already seeking.');
}
} catch (error) {
logger.error('[VideoPlayer] Error during seek operation:', error);
setIsBuffering(false);
}
};
const handleProgressBarTouch = (event: any) => {
if (!duration || duration <= 0) return;
const { locationX } = event.nativeEvent;
processProgressTouch(locationX);
if (duration > 0) {
const { locationX } = event.nativeEvent;
processProgressTouch(locationX);
}
};
const handleProgressBarDragStart = () => {
@ -369,18 +372,8 @@ const VideoPlayer: React.FC = () => {
const handleProgressBarDragEnd = () => {
setIsDragging(false);
if (pendingSeekValue.current !== null) {
// For Android, add a small delay to ensure UI updates before the seek happens
if (Platform.OS === 'android') {
setTimeout(() => {
if (pendingSeekValue.current !== null) {
seekToTime(pendingSeekValue.current);
pendingSeekValue.current = null;
}
}, 150);
} else {
seekToTime(pendingSeekValue.current);
pendingSeekValue.current = null;
}
seekToTime(pendingSeekValue.current);
pendingSeekValue.current = null;
}
};
@ -399,8 +392,11 @@ const VideoPlayer: React.FC = () => {
};
const handleProgress = (event: any) => {
if (isDragging) return;
if (isDragging || isSeeking.current) return;
const currentTimeInSeconds = event.currentTime / 1000;
// Only update if there's a significant change to avoid unnecessary updates
if (Math.abs(currentTimeInSeconds - currentTime) > 0.5) {
safeSetState(() => setCurrentTime(currentTimeInSeconds));
const progressPercent = duration > 0 ? currentTimeInSeconds / duration : 0;
@ -415,67 +411,34 @@ const VideoPlayer: React.FC = () => {
};
const onLoad = (data: any) => {
setDuration(data.duration / 1000);
if (data.videoSize && data.videoSize.width && data.videoSize.height) {
const aspectRatio = data.videoSize.width / data.videoSize.height;
setVideoAspectRatio(aspectRatio);
const is16x9 = Math.abs(aspectRatio - (16/9)) < 0.1;
setIs16by9Content(is16x9);
if (is16x9) {
setZoomScale(1.1);
setLastZoomScale(1.1);
} else {
setZoomScale(1);
setLastZoomScale(1);
if (DEBUG_MODE) {
logger.log('[VideoPlayer] Video loaded:', data);
}
if (isMounted.current) {
if (data.duration > 0) {
setDuration(data.duration / 1000);
}
const styles = calculateVideoStyles(
data.videoSize.width,
data.videoSize.height,
screenDimensions.width,
screenDimensions.height
);
setCustomVideoStyles(styles);
} else {
setIs16by9Content(true);
setZoomScale(1.1);
setLastZoomScale(1.1);
const defaultStyles = {
position: 'absolute',
top: 0,
left: 0,
width: screenDimensions.width,
height: screenDimensions.height,
};
setCustomVideoStyles(defaultStyles);
setVideoAspectRatio(data.videoSize.width / data.videoSize.height);
if (data.audioTracks && data.audioTracks.length > 0) {
setVlcAudioTracks(data.audioTracks);
}
if (data.textTracks && data.textTracks.length > 0) {
setVlcTextTracks(data.textTracks);
}
setIsVideoLoaded(true);
setIsPlayerReady(true);
if (initialPosition && !isInitialSeekComplete) {
setTimeout(() => {
if (vlcRef.current && duration > 0 && isMounted.current) {
seekToTime(initialPosition);
setIsInitialSeekComplete(true);
}
}, 1000);
}
completeOpeningAnimation();
}
setIsPlayerReady(true);
const audioTracksFromLoad = data.audioTracks || [];
const textTracksFromLoad = data.textTracks || [];
setVlcAudioTracks(audioTracksFromLoad);
setVlcTextTracks(textTracksFromLoad);
if (audioTracksFromLoad.length > 1) {
const firstEnabledAudio = audioTracksFromLoad.find((t: any) => t.id !== -1);
if(firstEnabledAudio) {
setSelectedAudioTrack(firstEnabledAudio.id);
}
} else if (audioTracksFromLoad.length > 0) {
setSelectedAudioTrack(audioTracksFromLoad[0].id);
}
if (imdbId && !customSubtitles.length) {
setTimeout(() => {
fetchAvailableSubtitles(imdbId, true);
}, 2000);
}
if (initialPosition !== null && !isInitialSeekComplete) {
setTimeout(() => {
if (vlcRef.current && duration > 0 && isMounted.current) {
seekToTime(initialPosition);
setIsInitialSeekComplete(true);
}
}, 1000);
}
setIsVideoLoaded(true);
completeOpeningAnimation();
};
const skip = (seconds: number) => {
@ -793,61 +756,25 @@ const VideoPlayer: React.FC = () => {
logger.log(`[VideoPlayer] Player ready after source change, seeking to position: ${pendingSeek.position}s out of ${duration}s total`);
if (pendingSeek.position > 0 && vlcRef.current) {
// Longer delay for Android to ensure player is stable
const delayTime = Platform.OS === 'android' ? 2500 : 1500;
const delayTime = Platform.OS === 'android' ? 1500 : 1000;
setTimeout(() => {
if (vlcRef.current && duration > 0 && pendingSeek) {
logger.log(`[VideoPlayer] Executing seek to ${pendingSeek.position}s`);
if (Platform.OS === 'android') {
// On Android, wait longer and set isBuffering to improve visual feedback
setIsBuffering(true);
// For Android, use setPosition directly with normalized value
const normalizedPosition = Math.max(0, Math.min(pendingSeek.position / duration, 1));
vlcRef.current.setPosition(normalizedPosition);
// Update the current time
setCurrentTime(pendingSeek.position);
// Give the player time to recover from the seek
seekToTime(pendingSeek.position);
if (pendingSeek.shouldPlay) {
setTimeout(() => {
setIsBuffering(false);
// Resume playback after a delay if needed
if (pendingSeek.shouldPlay) {
setPaused(false);
}
// Clean up
setPendingSeek(null);
setIsChangingSource(false);
}, 800);
} else {
// iOS - use the normal seekToTime function
seekToTime(pendingSeek.position);
// Also update the current time state
setCurrentTime(pendingSeek.position);
// Resume playback if needed
if (pendingSeek.shouldPlay) {
setTimeout(() => {
logger.log('[VideoPlayer] Resuming playback after seek');
setPaused(false);
if (vlcRef.current && typeof vlcRef.current.play === 'function') {
vlcRef.current.play();
}
}, 700);
}
// Clean up
setTimeout(() => {
setPendingSeek(null);
setIsChangingSource(false);
}, 800);
logger.log('[VideoPlayer] Resuming playback after source change seek');
setPaused(false);
}, 850); // Delay should be slightly more than seekToTime's internal timeout
}
setTimeout(() => {
setPendingSeek(null);
setIsChangingSource(false);
}, 900);
}
}, delayTime);
} else {
@ -856,9 +783,6 @@ const VideoPlayer: React.FC = () => {
setTimeout(() => {
logger.log('[VideoPlayer] No seek needed, just resuming playback');
setPaused(false);
if (vlcRef.current && typeof vlcRef.current.play === 'function') {
vlcRef.current.play();
}
}, 500);
}
@ -1018,7 +942,6 @@ const VideoPlayer: React.FC = () => {
left: 0,
width: screenDimensions.width,
height: screenDimensions.height,
backgroundColor: '#000',
}}>
<TouchableOpacity
style={{ flex: 1 }}
@ -1029,52 +952,21 @@ const VideoPlayer: React.FC = () => {
>
<VLCPlayer
ref={vlcRef}
style={{
position: 'absolute',
top: 0,
left: 0,
width: screenDimensions.width,
height: screenDimensions.height,
transform: [
{ scale: zoomScale },
],
}}
source={{
uri: currentStreamUrl,
initOptions: Platform.OS === 'android' ? [
'--rtsp-tcp',
'--network-caching=1500',
'--rtsp-caching=1500',
'--no-audio-time-stretch',
'--clock-jitter=0',
'--clock-synchro=0',
'--drop-late-frames',
'--skip-frames',
'--aout=opensles',
'--file-caching=1500',
'--sout-mux-caching=1500',
] : [
'--rtsp-tcp',
'--network-caching=150',
'--rtsp-caching=150',
'--no-audio-time-stretch',
'--clock-jitter=0',
'--clock-synchro=0',
'--drop-late-frames',
'--skip-frames',
],
}}
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
source={{ uri: currentStreamUrl }}
paused={paused}
autoplay={true}
autoAspectRatio={false}
resizeMode={'stretch' as any}
audioTrack={selectedAudioTrack || undefined}
textTrack={selectedTextTrack === -1 ? undefined : selectedTextTrack}
onLoad={onLoad}
onProgress={handleProgress}
onLoad={onLoad}
onEnd={onEnd}
onError={handleError}
onBuffering={onBuffering}
onPlaying={onPlaying}
onPaused={onPaused}
resizeMode={resizeMode as any}
audioTrack={selectedAudioTrack ?? undefined}
textTrack={useCustomSubtitles ? -1 : (selectedTextTrack ?? undefined)}
seek={Platform.OS === 'ios' ? (seekPosition ?? undefined) : undefined}
autoAspectRatio
/>
</TouchableOpacity>
</View>