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", "@types/react-native-video": "^5.0.20",
"axios": "^1.10.0", "axios": "^1.10.0",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"cheerio": "^1.1.0",
"cors": "^2.8.5",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"expo": "~52.0.43", "expo": "~52.0.43",
@ -44,8 +46,10 @@
"expo-status-bar": "~2.0.1", "expo-status-bar": "~2.0.1",
"expo-system-ui": "^4.0.9", "expo-system-ui": "^4.0.9",
"expo-web-browser": "~14.0.2", "expo-web-browser": "~14.0.2",
"express": "^5.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"puppeteer": "^24.10.1",
"react": "18.3.1", "react": "18.3.1",
"react-native": "0.76.9", "react-native": "0.76.9",
"react-native-awesome-slider": "^2.9.0", "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 { logger } from '../../utils/logger';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import AndroidVideoPlayer from './AndroidVideoPlayer';
import { import {
DEFAULT_SUBTITLE_SIZE, DEFAULT_SUBTITLE_SIZE,
@ -32,6 +33,11 @@ import CustomSubtitles from './subtitles/CustomSubtitles';
import SourcesModal from './modals/SourcesModal'; import SourcesModal from './modals/SourcesModal';
const VideoPlayer: React.FC = () => { const VideoPlayer: React.FC = () => {
// If on Android, use the AndroidVideoPlayer component
if (Platform.OS === 'android') {
return <AndroidVideoPlayer />;
}
const navigation = useNavigation(); const navigation = useNavigation();
const route = useRoute<RouteProp<RootStackParamList, 'Player'>>(); const route = useRoute<RouteProp<RootStackParamList, 'Player'>>();
@ -70,6 +76,7 @@ const VideoPlayer: React.FC = () => {
const [selectedTextTrack, setSelectedTextTrack] = useState<number>(-1); const [selectedTextTrack, setSelectedTextTrack] = useState<number>(-1);
const [resizeMode, setResizeMode] = useState<ResizeModeType>('stretch'); const [resizeMode, setResizeMode] = useState<ResizeModeType>('stretch');
const [buffered, setBuffered] = useState(0); const [buffered, setBuffered] = useState(0);
const [seekPosition, setSeekPosition] = useState<number | null>(null);
const vlcRef = useRef<any>(null); const vlcRef = useRef<any>(null);
const [showAudioModal, setShowAudioModal] = useState(false); const [showAudioModal, setShowAudioModal] = useState(false);
const [showSubtitleModal, setShowSubtitleModal] = useState(false); const [showSubtitleModal, setShowSubtitleModal] = useState(false);
@ -92,6 +99,7 @@ const VideoPlayer: React.FC = () => {
const progressAnim = useRef(new Animated.Value(0)).current; const progressAnim = useRef(new Animated.Value(0)).current;
const progressBarRef = useRef<View>(null); const progressBarRef = useRef<View>(null);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const isSeeking = useRef(false);
const seekDebounceTimer = useRef<NodeJS.Timeout | null>(null); const seekDebounceTimer = useRef<NodeJS.Timeout | null>(null);
const pendingSeekValue = useRef<number | null>(null); const pendingSeekValue = useRef<number | null>(null);
const lastSeekTime = useRef<number>(0); const lastSeekTime = useRef<number>(0);
@ -131,7 +139,6 @@ const VideoPlayer: React.FC = () => {
left: 0, left: 0,
width: screenWidth, width: screenWidth,
height: screenHeight, height: screenHeight,
backgroundColor: '#000',
}; };
}; };
@ -294,66 +301,62 @@ const VideoPlayer: React.FC = () => {
}; };
}, [id, type, currentTime, duration]); }, [id, type, currentTime, duration]);
const onPlaying = () => {
if (isMounted.current && !isSeeking.current) {
setPaused(false);
}
};
const onPaused = () => {
if (isMounted.current) {
setPaused(true);
}
};
const seekToTime = (timeInSeconds: number) => { const seekToTime = (timeInSeconds: number) => {
if (!isPlayerReady || duration <= 0 || !vlcRef.current) return; if (vlcRef.current && duration > 0 && !isSeeking.current) {
const normalizedPosition = Math.max(0, Math.min(timeInSeconds / duration, 1)); if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s`);
try { }
if (Platform.OS === 'android') {
// On Android, we need to handle seeking differently to prevent black screens isSeeking.current = true;
setIsBuffering(true);
// For Android, use direct seeking on VLC player ref instead of seek prop
// Set a small timeout to prevent overwhelming the player if (Platform.OS === 'android' && vlcRef.current.seek) {
const now = Date.now(); // Calculate position as fraction
if (now - lastSeekTime.current < 300) { const position = timeInSeconds / duration;
// Throttle seeks that are too close together vlcRef.current.seek(position);
if (seekDebounceTimer.current) {
clearTimeout(seekDebounceTimer.current); // Clear seek state after Android seek
} setTimeout(() => {
if (isMounted.current) {
seekDebounceTimer.current = setTimeout(() => { isSeeking.current = false;
if (vlcRef.current) { }
// Set position instead of using seek on Android }, 300);
vlcRef.current.setPosition(normalizedPosition); } else {
lastSeekTime.current = Date.now(); // iOS fallback - use seek prop
const position = timeInSeconds / duration;
// Give the player some time to recover setSeekPosition(position);
setTimeout(() => {
setIsBuffering(false); setTimeout(() => {
}, 500); if (isMounted.current) {
} setSeekPosition(null);
}, 300); isSeeking.current = false;
return; }
} }, 500);
}
// Directly set position } else {
vlcRef.current.setPosition(normalizedPosition); if (DEBUG_MODE) {
lastSeekTime.current = now; logger.error('[VideoPlayer] Seek failed: Player not ready, duration is zero, or already seeking.');
// 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');
}
} }
} catch (error) {
logger.error('[VideoPlayer] Error during seek operation:', error);
setIsBuffering(false);
} }
}; };
const handleProgressBarTouch = (event: any) => { const handleProgressBarTouch = (event: any) => {
if (!duration || duration <= 0) return; if (duration > 0) {
const { locationX } = event.nativeEvent; const { locationX } = event.nativeEvent;
processProgressTouch(locationX); processProgressTouch(locationX);
}
}; };
const handleProgressBarDragStart = () => { const handleProgressBarDragStart = () => {
@ -369,18 +372,8 @@ const VideoPlayer: React.FC = () => {
const handleProgressBarDragEnd = () => { const handleProgressBarDragEnd = () => {
setIsDragging(false); setIsDragging(false);
if (pendingSeekValue.current !== null) { if (pendingSeekValue.current !== null) {
// For Android, add a small delay to ensure UI updates before the seek happens seekToTime(pendingSeekValue.current);
if (Platform.OS === 'android') { pendingSeekValue.current = null;
setTimeout(() => {
if (pendingSeekValue.current !== null) {
seekToTime(pendingSeekValue.current);
pendingSeekValue.current = null;
}
}, 150);
} else {
seekToTime(pendingSeekValue.current);
pendingSeekValue.current = null;
}
} }
}; };
@ -399,8 +392,11 @@ const VideoPlayer: React.FC = () => {
}; };
const handleProgress = (event: any) => { const handleProgress = (event: any) => {
if (isDragging) return; if (isDragging || isSeeking.current) return;
const currentTimeInSeconds = event.currentTime / 1000; const currentTimeInSeconds = event.currentTime / 1000;
// Only update if there's a significant change to avoid unnecessary updates
if (Math.abs(currentTimeInSeconds - currentTime) > 0.5) { if (Math.abs(currentTimeInSeconds - currentTime) > 0.5) {
safeSetState(() => setCurrentTime(currentTimeInSeconds)); safeSetState(() => setCurrentTime(currentTimeInSeconds));
const progressPercent = duration > 0 ? currentTimeInSeconds / duration : 0; const progressPercent = duration > 0 ? currentTimeInSeconds / duration : 0;
@ -415,67 +411,34 @@ const VideoPlayer: React.FC = () => {
}; };
const onLoad = (data: any) => { const onLoad = (data: any) => {
setDuration(data.duration / 1000); if (DEBUG_MODE) {
if (data.videoSize && data.videoSize.width && data.videoSize.height) { logger.log('[VideoPlayer] Video loaded:', data);
const aspectRatio = data.videoSize.width / data.videoSize.height; }
setVideoAspectRatio(aspectRatio); if (isMounted.current) {
const is16x9 = Math.abs(aspectRatio - (16/9)) < 0.1; if (data.duration > 0) {
setIs16by9Content(is16x9); setDuration(data.duration / 1000);
if (is16x9) {
setZoomScale(1.1);
setLastZoomScale(1.1);
} else {
setZoomScale(1);
setLastZoomScale(1);
} }
const styles = calculateVideoStyles( setVideoAspectRatio(data.videoSize.width / data.videoSize.height);
data.videoSize.width,
data.videoSize.height, if (data.audioTracks && data.audioTracks.length > 0) {
screenDimensions.width, setVlcAudioTracks(data.audioTracks);
screenDimensions.height }
); if (data.textTracks && data.textTracks.length > 0) {
setCustomVideoStyles(styles); setVlcTextTracks(data.textTracks);
} else { }
setIs16by9Content(true);
setZoomScale(1.1); setIsVideoLoaded(true);
setLastZoomScale(1.1); setIsPlayerReady(true);
const defaultStyles = { if (initialPosition && !isInitialSeekComplete) {
position: 'absolute', setTimeout(() => {
top: 0, if (vlcRef.current && duration > 0 && isMounted.current) {
left: 0, seekToTime(initialPosition);
width: screenDimensions.width, setIsInitialSeekComplete(true);
height: screenDimensions.height, }
}; }, 1000);
setCustomVideoStyles(defaultStyles); }
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) => { 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`); 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) { if (pendingSeek.position > 0 && vlcRef.current) {
// Longer delay for Android to ensure player is stable const delayTime = Platform.OS === 'android' ? 1500 : 1000;
const delayTime = Platform.OS === 'android' ? 2500 : 1500;
setTimeout(() => { setTimeout(() => {
if (vlcRef.current && duration > 0 && pendingSeek) { if (vlcRef.current && duration > 0 && pendingSeek) {
logger.log(`[VideoPlayer] Executing seek to ${pendingSeek.position}s`); logger.log(`[VideoPlayer] Executing seek to ${pendingSeek.position}s`);
if (Platform.OS === 'android') { seekToTime(pendingSeek.position);
// On Android, wait longer and set isBuffering to improve visual feedback
setIsBuffering(true); if (pendingSeek.shouldPlay) {
// 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
setTimeout(() => { setTimeout(() => {
setIsBuffering(false); logger.log('[VideoPlayer] Resuming playback after source change seek');
setPaused(false);
// Resume playback after a delay if needed }, 850); // Delay should be slightly more than seekToTime's internal timeout
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);
} }
setTimeout(() => {
setPendingSeek(null);
setIsChangingSource(false);
}, 900);
} }
}, delayTime); }, delayTime);
} else { } else {
@ -856,9 +783,6 @@ const VideoPlayer: React.FC = () => {
setTimeout(() => { setTimeout(() => {
logger.log('[VideoPlayer] No seek needed, just resuming playback'); logger.log('[VideoPlayer] No seek needed, just resuming playback');
setPaused(false); setPaused(false);
if (vlcRef.current && typeof vlcRef.current.play === 'function') {
vlcRef.current.play();
}
}, 500); }, 500);
} }
@ -1018,7 +942,6 @@ const VideoPlayer: React.FC = () => {
left: 0, left: 0,
width: screenDimensions.width, width: screenDimensions.width,
height: screenDimensions.height, height: screenDimensions.height,
backgroundColor: '#000',
}}> }}>
<TouchableOpacity <TouchableOpacity
style={{ flex: 1 }} style={{ flex: 1 }}
@ -1029,52 +952,21 @@ const VideoPlayer: React.FC = () => {
> >
<VLCPlayer <VLCPlayer
ref={vlcRef} ref={vlcRef}
style={{ style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
position: 'absolute', source={{ uri: currentStreamUrl }}
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',
],
}}
paused={paused} paused={paused}
autoplay={true}
autoAspectRatio={false}
resizeMode={'stretch' as any}
audioTrack={selectedAudioTrack || undefined}
textTrack={selectedTextTrack === -1 ? undefined : selectedTextTrack}
onLoad={onLoad}
onProgress={handleProgress} onProgress={handleProgress}
onLoad={onLoad}
onEnd={onEnd} onEnd={onEnd}
onError={handleError} onError={handleError}
onBuffering={onBuffering} 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> </TouchableOpacity>
</View> </View>