fixes to videoplayer

This commit is contained in:
tapframe 2025-07-17 14:18:40 +05:30
parent 42daa4decc
commit 19b6e6b3d5
3 changed files with 198 additions and 113 deletions

View file

@ -17,11 +17,11 @@ import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
import { useMetadata } from '../../hooks/useMetadata'; import { useMetadata } from '../../hooks/useMetadata';
import { useSettings } from '../../hooks/useSettings'; import { useSettings } from '../../hooks/useSettings';
import { import {
DEFAULT_SUBTITLE_SIZE, DEFAULT_SUBTITLE_SIZE,
AudioTrack, AudioTrack,
TextTrack, TextTrack,
ResizeModeType, ResizeModeType,
WyzieSubtitle, WyzieSubtitle,
SubtitleCue, SubtitleCue,
RESUME_PREF_KEY, RESUME_PREF_KEY,
@ -45,7 +45,7 @@ const VideoPlayer: React.FC = () => {
const navigation = useNavigation(); const navigation = useNavigation();
const route = useRoute<RouteProp<RootStackParamList, 'Player'>>(); const route = useRoute<RouteProp<RootStackParamList, 'Player'>>();
const { const {
uri, uri,
title = 'Episode Name', title = 'Episode Name',
@ -90,6 +90,14 @@ const VideoPlayer: React.FC = () => {
const screenData = Dimensions.get('screen'); const screenData = Dimensions.get('screen');
const [screenDimensions, setScreenDimensions] = useState(screenData); const [screenDimensions, setScreenDimensions] = useState(screenData);
// iPad-specific fullscreen handling
const isIPad = Platform.OS === 'ios' && (screenData.width > 1000 || screenData.height > 1000);
const shouldUseFullscreen = isIPad;
// Use window dimensions for iPad instead of screen dimensions
const windowData = Dimensions.get('window');
const effectiveDimensions = shouldUseFullscreen ? windowData : screenData;
const [paused, setPaused] = useState(false); const [paused, setPaused] = useState(false);
const [currentTime, setCurrentTime] = useState(0); const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0); const [duration, setDuration] = useState(0);
@ -116,8 +124,8 @@ const VideoPlayer: React.FC = () => {
const openingScaleAnim = useRef(new Animated.Value(0.8)).current; const openingScaleAnim = useRef(new Animated.Value(0.8)).current;
const backgroundFadeAnim = useRef(new Animated.Value(1)).current; const backgroundFadeAnim = useRef(new Animated.Value(1)).current;
const [isBuffering, setIsBuffering] = useState(false); const [isBuffering, setIsBuffering] = useState(false);
const [vlcAudioTracks, setVlcAudioTracks] = useState<Array<{id: number, name: string, language?: string}>>([]); const [vlcAudioTracks, setVlcAudioTracks] = useState<Array<{ id: number, name: string, language?: string }>>([]);
const [vlcTextTracks, setVlcTextTracks] = useState<Array<{id: number, name: string, language?: string}>>([]); const [vlcTextTracks, setVlcTextTracks] = useState<Array<{ id: number, name: string, language?: string }>>([]);
const [isPlayerReady, setIsPlayerReady] = useState(false); const [isPlayerReady, setIsPlayerReady] = useState(false);
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);
@ -140,6 +148,7 @@ const VideoPlayer: React.FC = () => {
const [customSubtitles, setCustomSubtitles] = useState<SubtitleCue[]>([]); const [customSubtitles, setCustomSubtitles] = useState<SubtitleCue[]>([]);
const [currentSubtitle, setCurrentSubtitle] = useState<string>(''); const [currentSubtitle, setCurrentSubtitle] = useState<string>('');
const [subtitleSize, setSubtitleSize] = useState<number>(DEFAULT_SUBTITLE_SIZE); const [subtitleSize, setSubtitleSize] = useState<number>(DEFAULT_SUBTITLE_SIZE);
const [subtitleBackground, setSubtitleBackground] = useState<boolean>(true);
const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false); const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false);
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false); const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false);
const [availableSubtitles, setAvailableSubtitles] = useState<WyzieSubtitle[]>([]); const [availableSubtitles, setAvailableSubtitles] = useState<WyzieSubtitle[]>([]);
@ -156,24 +165,24 @@ const VideoPlayer: React.FC = () => {
const isMounted = useRef(true); const isMounted = useRef(true);
const controlsTimeout = useRef<NodeJS.Timeout | null>(null); const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false); const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
// Get metadata to access logo (only if we have a valid id) // Get metadata to access logo (only if we have a valid id)
const shouldLoadMetadata = Boolean(id && type); const shouldLoadMetadata = Boolean(id && type);
const metadataResult = useMetadata({ const metadataResult = useMetadata({
id: id || 'placeholder', id: id || 'placeholder',
type: type || 'movie' type: type || 'movie'
}); });
const { metadata, loading: metadataLoading } = shouldLoadMetadata ? metadataResult : { metadata: null, loading: false }; const { metadata, loading: metadataLoading } = shouldLoadMetadata ? metadataResult : { metadata: null, loading: false };
const { settings } = useSettings(); const { settings } = useSettings();
// Logo animation values // Logo animation values
const logoScaleAnim = useRef(new Animated.Value(0.8)).current; const logoScaleAnim = useRef(new Animated.Value(0.8)).current;
const logoOpacityAnim = useRef(new Animated.Value(0)).current; const logoOpacityAnim = useRef(new Animated.Value(0)).current;
const pulseAnim = useRef(new Animated.Value(1)).current; const pulseAnim = useRef(new Animated.Value(1)).current;
// Check if we have a logo to show // Check if we have a logo to show
const hasLogo = metadata && metadata.logo && !metadataLoading; const hasLogo = metadata && metadata.logo && !metadataLoading;
// Small offset (in seconds) used to avoid seeking to the *exact* end of the // Small offset (in seconds) used to avoid seeking to the *exact* end of the
// file which triggers the `onEnd` callback and causes playback to restart. // file which triggers the `onEnd` callback and causes playback to restart.
const END_EPSILON = 0.3; const END_EPSILON = 0.3;
@ -224,25 +233,47 @@ const VideoPlayer: React.FC = () => {
}; };
useEffect(() => { useEffect(() => {
if (videoAspectRatio && screenDimensions.width > 0 && screenDimensions.height > 0) { if (videoAspectRatio && effectiveDimensions.width > 0 && effectiveDimensions.height > 0) {
const styles = calculateVideoStyles( const styles = calculateVideoStyles(
videoAspectRatio * 1000, videoAspectRatio * 1000,
1000, 1000,
screenDimensions.width, effectiveDimensions.width,
screenDimensions.height effectiveDimensions.height
); );
setCustomVideoStyles(styles); setCustomVideoStyles(styles);
if (DEBUG_MODE) { if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Screen dimensions changed, recalculated styles:`, styles); logger.log(`[VideoPlayer] Screen dimensions changed, recalculated styles:`, styles);
} }
} }
}, [screenDimensions, videoAspectRatio]); }, [effectiveDimensions, videoAspectRatio]);
// Force landscape orientation immediately when component mounts
useEffect(() => {
const lockOrientation = async () => {
try {
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
logger.log('[VideoPlayer] Locked to landscape orientation');
} catch (error) {
logger.warn('[VideoPlayer] Failed to lock orientation:', error);
}
};
// Lock orientation immediately
lockOrientation();
return () => {
// Unlock orientation when component unmounts
ScreenOrientation.unlockAsync().catch(() => {
// Ignore unlock errors
});
};
}, []);
useEffect(() => { useEffect(() => {
const subscription = Dimensions.addEventListener('change', ({ screen }) => { const subscription = Dimensions.addEventListener('change', ({ screen }) => {
setScreenDimensions(screen); setScreenDimensions(screen);
}); });
const initializePlayer = () => { const initializePlayer = async () => {
StatusBar.setHidden(true, 'none'); StatusBar.setHidden(true, 'none');
enableImmersiveMode(); enableImmersiveMode();
startOpeningAnimation(); startOpeningAnimation();
@ -250,10 +281,6 @@ const VideoPlayer: React.FC = () => {
initializePlayer(); initializePlayer();
return () => { return () => {
subscription?.remove(); subscription?.remove();
const unlockOrientation = async () => {
await ScreenOrientation.unlockAsync();
};
unlockOrientation();
disableImmersiveMode(); disableImmersiveMode();
}; };
}, []); }, []);
@ -273,7 +300,7 @@ const VideoPlayer: React.FC = () => {
useNativeDriver: true, useNativeDriver: true,
}), }),
]).start(); ]).start();
// Continuous pulse animation for the logo // Continuous pulse animation for the logo
const createPulseAnimation = () => { const createPulseAnimation = () => {
return Animated.sequence([ return Animated.sequence([
@ -289,7 +316,7 @@ const VideoPlayer: React.FC = () => {
}), }),
]); ]);
}; };
const loopPulse = () => { const loopPulse = () => {
createPulseAnimation().start(() => { createPulseAnimation().start(() => {
if (!isOpeningAnimationComplete) { if (!isOpeningAnimationComplete) {
@ -297,7 +324,7 @@ const VideoPlayer: React.FC = () => {
} }
}); });
}; };
// Start pulsing after a short delay // Start pulsing after a short delay
setTimeout(() => { setTimeout(() => {
if (!isOpeningAnimationComplete) { if (!isOpeningAnimationComplete) {
@ -340,11 +367,11 @@ const VideoPlayer: React.FC = () => {
logger.log(`[VideoPlayer] Loading watch progress for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`); logger.log(`[VideoPlayer] Loading watch progress for ${type}:${id}${episodeId ? `:${episodeId}` : ''}`);
const savedProgress = await storageService.getWatchProgress(id, type, episodeId); const savedProgress = await storageService.getWatchProgress(id, type, episodeId);
logger.log(`[VideoPlayer] Saved progress:`, savedProgress); logger.log(`[VideoPlayer] Saved progress:`, savedProgress);
if (savedProgress) { if (savedProgress) {
const progressPercent = (savedProgress.currentTime / savedProgress.duration) * 100; const progressPercent = (savedProgress.currentTime / savedProgress.duration) * 100;
logger.log(`[VideoPlayer] Progress: ${progressPercent.toFixed(1)}% (${savedProgress.currentTime}/${savedProgress.duration})`); logger.log(`[VideoPlayer] Progress: ${progressPercent.toFixed(1)}% (${savedProgress.currentTime}/${savedProgress.duration})`);
if (progressPercent < 85) { if (progressPercent < 85) {
setResumePosition(savedProgress.currentTime); setResumePosition(savedProgress.currentTime);
setSavedDuration(savedProgress.duration); setSavedDuration(savedProgress.duration);
@ -376,7 +403,7 @@ const VideoPlayer: React.FC = () => {
}; };
try { try {
await storageService.setWatchProgress(id, type, progress, episodeId); await storageService.setWatchProgress(id, type, progress, episodeId);
// Sync to Trakt if authenticated // Sync to Trakt if authenticated
await traktAutosync.handleProgressUpdate(currentTime, duration); await traktAutosync.handleProgressUpdate(currentTime, duration);
} catch (error) { } catch (error) {
@ -384,23 +411,23 @@ const VideoPlayer: React.FC = () => {
} }
} }
}; };
useEffect(() => { useEffect(() => {
if (id && type && !paused && duration > 0) { if (id && type && !paused && duration > 0) {
if (progressSaveInterval) { if (progressSaveInterval) {
clearInterval(progressSaveInterval); clearInterval(progressSaveInterval);
} }
// Use the user's configured sync frequency with increased minimum to reduce heating // Use the user's configured sync frequency with increased minimum to reduce heating
// Minimum interval increased from 5s to 30s to reduce CPU usage // Minimum interval increased from 5s to 30s to reduce CPU usage
const syncInterval = Math.max(30000, traktSettings.syncFrequency); const syncInterval = Math.max(30000, traktSettings.syncFrequency);
const interval = setInterval(() => { const interval = setInterval(() => {
saveWatchProgress(); saveWatchProgress();
}, syncInterval); }, syncInterval);
logger.log(`[VideoPlayer] Watch progress save interval set to ${syncInterval}ms`); logger.log(`[VideoPlayer] Watch progress save interval set to ${syncInterval}ms`);
setProgressSaveInterval(interval); setProgressSaveInterval(interval);
return () => { return () => {
clearInterval(interval); clearInterval(interval);
@ -418,11 +445,11 @@ const VideoPlayer: React.FC = () => {
} }
}; };
}, [id, type, currentTime, duration]); }, [id, type, currentTime, duration]);
const onPlaying = () => { const onPlaying = () => {
if (isMounted.current && !isSeeking.current) { if (isMounted.current && !isSeeking.current) {
setPaused(false); setPaused(false);
// Note: handlePlaybackStart is already called in onLoad // Note: handlePlaybackStart is already called in onLoad
// We don't need to call it again here to avoid duplicate calls // We don't need to call it again here to avoid duplicate calls
} }
@ -431,7 +458,7 @@ const VideoPlayer: React.FC = () => {
const onPaused = () => { const onPaused = () => {
if (isMounted.current) { if (isMounted.current) {
setPaused(true); setPaused(true);
// Send a forced pause update to Trakt immediately when user pauses // Send a forced pause update to Trakt immediately when user pauses
if (duration > 0) { if (duration > 0) {
traktAutosync.handleProgressUpdate(currentTime, duration, true); traktAutosync.handleProgressUpdate(currentTime, duration, true);
@ -447,9 +474,9 @@ const VideoPlayer: React.FC = () => {
if (DEBUG_MODE) { if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`); logger.log(`[VideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`);
} }
isSeeking.current = true; isSeeking.current = true;
// For Android, use direct seeking on VLC player ref instead of seek prop // For Android, use direct seeking on VLC player ref instead of seek prop
if (Platform.OS === 'android' && vlcRef.current.seek) { if (Platform.OS === 'android' && vlcRef.current.seek) {
// Calculate position as fraction // Calculate position as fraction
@ -461,7 +488,7 @@ const VideoPlayer: React.FC = () => {
isSeeking.current = false; isSeeking.current = false;
if (DEBUG_MODE) { if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Android seek completed to ${timeInSeconds.toFixed(2)}s`); logger.log(`[VideoPlayer] Android seek completed to ${timeInSeconds.toFixed(2)}s`);
} }
} }
}, 500); }, 500);
} else { } else {
@ -498,17 +525,17 @@ const VideoPlayer: React.FC = () => {
processProgressTouch(locationX); processProgressTouch(locationX);
} }
}; };
const handleProgressBarDragStart = () => { const handleProgressBarDragStart = () => {
setIsDragging(true); setIsDragging(true);
}; };
const handleProgressBarDragMove = (event: any) => { const handleProgressBarDragMove = (event: any) => {
if (!isDragging || !duration || duration <= 0) return; if (!isDragging || !duration || duration <= 0) return;
const { locationX } = event.nativeEvent; const { locationX } = event.nativeEvent;
processProgressTouch(locationX, true); processProgressTouch(locationX, true);
}; };
const handleProgressBarDragEnd = () => { const handleProgressBarDragEnd = () => {
setIsDragging(false); setIsDragging(false);
if (pendingSeekValue.current !== null) { if (pendingSeekValue.current !== null) {
@ -516,7 +543,7 @@ const VideoPlayer: React.FC = () => {
pendingSeekValue.current = null; pendingSeekValue.current = null;
} }
}; };
const processProgressTouch = (locationX: number, isDragging = false) => { const processProgressTouch = (locationX: number, isDragging = false) => {
progressBarRef.current?.measure((x, y, width, height, pageX, pageY) => { progressBarRef.current?.measure((x, y, width, height, pageX, pageY) => {
const percentage = Math.max(0, Math.min(locationX / width, 0.999)); const percentage = Math.max(0, Math.min(locationX / width, 0.999));
@ -533,9 +560,9 @@ const VideoPlayer: React.FC = () => {
const handleProgress = (event: any) => { const handleProgress = (event: any) => {
if (isDragging || isSeeking.current) 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 // 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));
@ -558,12 +585,12 @@ const VideoPlayer: React.FC = () => {
const videoDuration = data.duration / 1000; const videoDuration = data.duration / 1000;
if (data.duration > 0) { if (data.duration > 0) {
setDuration(videoDuration); setDuration(videoDuration);
// Store the actual duration for future reference and update existing progress // Store the actual duration for future reference and update existing progress
if (id && type) { if (id && type) {
storageService.setContentDuration(id, type, videoDuration, episodeId); storageService.setContentDuration(id, type, videoDuration, episodeId);
storageService.updateProgressDuration(id, type, videoDuration, episodeId); storageService.updateProgressDuration(id, type, videoDuration, episodeId);
// Update the saved duration for resume overlay if it was using an estimate // Update the saved duration for resume overlay if it was using an estimate
if (savedDuration && Math.abs(savedDuration - videoDuration) > 60) { if (savedDuration && Math.abs(savedDuration - videoDuration) > 60) {
setSavedDuration(videoDuration); setSavedDuration(videoDuration);
@ -581,12 +608,12 @@ const VideoPlayer: React.FC = () => {
setIsVideoLoaded(true); setIsVideoLoaded(true);
setIsPlayerReady(true); setIsPlayerReady(true);
// Start Trakt watching session when video loads with proper duration // Start Trakt watching session when video loads with proper duration
if (videoDuration > 0) { if (videoDuration > 0) {
traktAutosync.handlePlaybackStart(currentTime, videoDuration); traktAutosync.handlePlaybackStart(currentTime, videoDuration);
} }
if (initialPosition && !isInitialSeekComplete) { if (initialPosition && !isInitialSeekComplete) {
logger.log(`[VideoPlayer] Seeking to initial position: ${initialPosition}s (duration: ${videoDuration}s)`); logger.log(`[VideoPlayer] Seeking to initial position: ${initialPosition}s (duration: ${videoDuration}s)`);
setTimeout(() => { setTimeout(() => {
@ -654,23 +681,23 @@ const VideoPlayer: React.FC = () => {
const handleClose = async () => { const handleClose = async () => {
logger.log('[VideoPlayer] Close button pressed - syncing to Trakt before closing'); logger.log('[VideoPlayer] Close button pressed - syncing to Trakt before closing');
// Set syncing state to prevent multiple close attempts // Set syncing state to prevent multiple close attempts
setIsSyncingBeforeClose(true); setIsSyncingBeforeClose(true);
// Make sure we have the most accurate current time // Make sure we have the most accurate current time
const actualCurrentTime = currentTime; const actualCurrentTime = currentTime;
const progressPercent = duration > 0 ? (actualCurrentTime / duration) * 100 : 0; const progressPercent = duration > 0 ? (actualCurrentTime / duration) * 100 : 0;
logger.log(`[VideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`); logger.log(`[VideoPlayer] Current progress: ${actualCurrentTime}/${duration} (${progressPercent.toFixed(1)}%)`);
try { try {
// Force one last progress update (scrobble/pause) with the exact time // Force one last progress update (scrobble/pause) with the exact time
await traktAutosync.handleProgressUpdate(actualCurrentTime, duration, true); await traktAutosync.handleProgressUpdate(actualCurrentTime, duration, true);
// Sync progress to Trakt before closing // Sync progress to Trakt before closing
await traktAutosync.handlePlaybackEnd(actualCurrentTime, duration, 'unmount'); await traktAutosync.handlePlaybackEnd(actualCurrentTime, duration, 'unmount');
// Start exit animation // Start exit animation
Animated.parallel([ Animated.parallel([
Animated.timing(fadeAnim, { Animated.timing(fadeAnim, {
@ -684,23 +711,54 @@ const VideoPlayer: React.FC = () => {
useNativeDriver: true, useNativeDriver: true,
}), }),
]).start(); ]).start();
// Longer delay to ensure Trakt sync completes // Cleanup and navigate back
setTimeout(() => { const cleanup = async () => {
ScreenOrientation.unlockAsync().then(() => { try {
disableImmersiveMode(); // Unlock orientation first
navigation.goBack(); await ScreenOrientation.unlockAsync();
}).catch(() => { logger.log('[VideoPlayer] Orientation unlocked');
// Fallback: navigate even if orientation unlock fails } catch (orientationError) {
disableImmersiveMode(); logger.warn('[VideoPlayer] Failed to unlock orientation:', orientationError);
navigation.goBack(); }
});
}, 500); // Increased from 100ms to 500ms // Disable immersive mode
disableImmersiveMode();
// Navigate back with proper handling for fullscreen modal
try {
if (navigation.canGoBack()) {
navigation.goBack();
} else {
// Fallback: navigate to main tabs if can't go back
navigation.navigate('MainTabs');
}
logger.log('[VideoPlayer] Navigation completed');
} catch (navError) {
logger.error('[VideoPlayer] Navigation error:', navError);
// Last resort: try to navigate to home
navigation.navigate('MainTabs');
}
};
// Delay to ensure Trakt sync completes and animations finish
setTimeout(cleanup, 500);
} catch (error) { } catch (error) {
logger.error('[VideoPlayer] Error syncing to Trakt before closing:', error); logger.error('[VideoPlayer] Error syncing to Trakt before closing:', error);
// Navigate anyway even if sync fails // Navigate anyway even if sync fails
disableImmersiveMode(); disableImmersiveMode();
navigation.goBack(); try {
await ScreenOrientation.unlockAsync();
} catch (orientationError) {
// Ignore orientation unlock errors
}
if (navigation.canGoBack()) {
navigation.goBack();
} else {
navigation.navigate('MainTabs');
}
} }
}; };
@ -721,7 +779,7 @@ const VideoPlayer: React.FC = () => {
clearTimeout(controlsTimeout.current); clearTimeout(controlsTimeout.current);
controlsTimeout.current = null; controlsTimeout.current = null;
} }
setShowControls(prevShowControls => { setShowControls(prevShowControls => {
const newShowControls = !prevShowControls; const newShowControls = !prevShowControls;
Animated.timing(fadeAnim, { Animated.timing(fadeAnim, {
@ -753,14 +811,14 @@ const VideoPlayer: React.FC = () => {
// Force one last progress update (scrobble/pause) with the exact final time // Force one last progress update (scrobble/pause) with the exact final time
logger.log('[VideoPlayer] Video ended naturally, sending final progress update with 100%'); logger.log('[VideoPlayer] Video ended naturally, sending final progress update with 100%');
await traktAutosync.handleProgressUpdate(finalTime, duration, true); await traktAutosync.handleProgressUpdate(finalTime, duration, true);
// Small delay to ensure the progress update is processed // Small delay to ensure the progress update is processed
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise(resolve => setTimeout(resolve, 300));
// Now send the stop call // Now send the stop call
logger.log('[VideoPlayer] Sending final stop call after natural end'); logger.log('[VideoPlayer] Sending final stop call after natural end');
await traktAutosync.handlePlaybackEnd(finalTime, duration, 'ended'); await traktAutosync.handlePlaybackEnd(finalTime, duration, 'ended');
logger.log('[VideoPlayer] Completed video end sync to Trakt'); logger.log('[VideoPlayer] Completed video end sync to Trakt');
} catch (error) { } catch (error) {
logger.error('[VideoPlayer] Error syncing to Trakt on video end:', error); logger.error('[VideoPlayer] Error syncing to Trakt on video end:', error);
@ -780,7 +838,7 @@ const VideoPlayer: React.FC = () => {
setSelectedTextTrack(trackId); setSelectedTextTrack(trackId);
} }
}; };
const loadSubtitleSize = async () => { const loadSubtitleSize = async () => {
try { try {
const savedSize = await AsyncStorage.getItem(SUBTITLE_SIZE_KEY); const savedSize = await AsyncStorage.getItem(SUBTITLE_SIZE_KEY);
@ -825,8 +883,8 @@ const VideoPlayer: React.FC = () => {
uniqueSubtitles.sort((a, b) => a.display.localeCompare(b.display)); uniqueSubtitles.sort((a, b) => a.display.localeCompare(b.display));
setAvailableSubtitles(uniqueSubtitles); setAvailableSubtitles(uniqueSubtitles);
if (autoSelectEnglish) { if (autoSelectEnglish) {
const englishSubtitle = uniqueSubtitles.find(sub => const englishSubtitle = uniqueSubtitles.find(sub =>
sub.language.toLowerCase() === 'eng' || sub.language.toLowerCase() === 'eng' ||
sub.language.toLowerCase() === 'en' || sub.language.toLowerCase() === 'en' ||
sub.display.toLowerCase().includes('english') sub.display.toLowerCase().includes('english')
); );
@ -861,10 +919,10 @@ const VideoPlayer: React.FC = () => {
setIsLoadingSubtitles(false); setIsLoadingSubtitles(false);
} }
}; };
const togglePlayback = () => { const togglePlayback = () => {
if (vlcRef.current) { if (vlcRef.current) {
setPaused(!paused); setPaused(!paused);
} }
}; };
@ -877,7 +935,7 @@ const VideoPlayer: React.FC = () => {
} }
}; };
}, []); }, []);
const safeSetState = (setter: any) => { const safeSetState = (setter: any) => {
if (isMounted.current) { if (isMounted.current) {
setter(); setter();
@ -891,7 +949,7 @@ const VideoPlayer: React.FC = () => {
} }
return; return;
} }
const currentCue = customSubtitles.find(cue => const currentCue = customSubtitles.find(cue =>
currentTime >= cue.start && currentTime <= cue.end currentTime >= cue.start && currentTime <= cue.end
); );
const newSubtitle = currentCue ? currentCue.text : ''; const newSubtitle = currentCue ? currentCue.text : '';
@ -912,26 +970,30 @@ const VideoPlayer: React.FC = () => {
saveSubtitleSize(newSize); saveSubtitleSize(newSize);
}; };
const toggleSubtitleBackground = () => {
setSubtitleBackground(prev => !prev);
};
useEffect(() => { useEffect(() => {
if (pendingSeek && isPlayerReady && isVideoLoaded && duration > 0) { if (pendingSeek && isPlayerReady && isVideoLoaded && duration > 0) {
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) {
const delayTime = Platform.OS === 'android' ? 1500 : 1000; const delayTime = Platform.OS === 'android' ? 1500 : 1000;
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`);
seekToTime(pendingSeek.position); seekToTime(pendingSeek.position);
if (pendingSeek.shouldPlay) { if (pendingSeek.shouldPlay) {
setTimeout(() => { setTimeout(() => {
logger.log('[VideoPlayer] Resuming playback after source change seek'); logger.log('[VideoPlayer] Resuming playback after source change seek');
setPaused(false); setPaused(false);
}, 850); // Delay should be slightly more than seekToTime's internal timeout }, 850); // Delay should be slightly more than seekToTime's internal timeout
} }
setTimeout(() => { setTimeout(() => {
setPendingSeek(null); setPendingSeek(null);
setIsChangingSource(false); setIsChangingSource(false);
@ -946,7 +1008,7 @@ const VideoPlayer: React.FC = () => {
setPaused(false); setPaused(false);
}, 500); }, 500);
} }
setTimeout(() => { setTimeout(() => {
setPendingSeek(null); setPendingSeek(null);
setIsChangingSource(false); setIsChangingSource(false);
@ -963,15 +1025,15 @@ const VideoPlayer: React.FC = () => {
setIsChangingSource(true); setIsChangingSource(true);
setShowSourcesModal(false); setShowSourcesModal(false);
try { try {
// Save current state // Save current state
const savedPosition = currentTime; const savedPosition = currentTime;
const wasPlaying = !paused; const wasPlaying = !paused;
logger.log(`[VideoPlayer] Changing source from ${currentStreamUrl} to ${newStream.url}`); logger.log(`[VideoPlayer] Changing source from ${currentStreamUrl} to ${newStream.url}`);
logger.log(`[VideoPlayer] Saved position: ${savedPosition}, was playing: ${wasPlaying}`); logger.log(`[VideoPlayer] Saved position: ${savedPosition}, was playing: ${wasPlaying}`);
// Extract quality and provider information from the new stream // Extract quality and provider information from the new stream
let newQuality = newStream.quality; let newQuality = newStream.quality;
if (!newQuality && newStream.title) { if (!newQuality && newStream.title) {
@ -979,38 +1041,38 @@ const VideoPlayer: React.FC = () => {
const qualityMatch = newStream.title.match(/(\d+)p/); const qualityMatch = newStream.title.match(/(\d+)p/);
newQuality = qualityMatch ? qualityMatch[0] : undefined; // Use [0] to get full match like "1080p" newQuality = qualityMatch ? qualityMatch[0] : undefined; // Use [0] to get full match like "1080p"
} }
// For provider, try multiple fields // For provider, try multiple fields
const newProvider = newStream.addonName || newStream.name || newStream.addon || 'Unknown'; const newProvider = newStream.addonName || newStream.name || newStream.addon || 'Unknown';
// For stream name, prioritize the stream name over title // For stream name, prioritize the stream name over title
const newStreamName = newStream.name || newStream.title || 'Unknown Stream'; const newStreamName = newStream.name || newStream.title || 'Unknown Stream';
logger.log(`[VideoPlayer] Stream object:`, newStream); logger.log(`[VideoPlayer] Stream object:`, newStream);
logger.log(`[VideoPlayer] Extracted - Quality: ${newQuality}, Provider: ${newProvider}, Stream Name: ${newStreamName}`); logger.log(`[VideoPlayer] Extracted - Quality: ${newQuality}, Provider: ${newProvider}, Stream Name: ${newStreamName}`);
logger.log(`[VideoPlayer] Available fields - quality: ${newStream.quality}, title: ${newStream.title}, addonName: ${newStream.addonName}, name: ${newStream.name}, addon: ${newStream.addon}`); logger.log(`[VideoPlayer] Available fields - quality: ${newStream.quality}, title: ${newStream.title}, addonName: ${newStream.addonName}, name: ${newStream.name}, addon: ${newStream.addon}`);
// Stop current playback // Stop current playback
if (vlcRef.current) { if (vlcRef.current) {
vlcRef.current.pause && vlcRef.current.pause(); vlcRef.current.pause && vlcRef.current.pause();
} }
setPaused(true); setPaused(true);
// Set pending seek state // Set pending seek state
setPendingSeek({ position: savedPosition, shouldPlay: wasPlaying }); setPendingSeek({ position: savedPosition, shouldPlay: wasPlaying });
// Update the stream URL and details immediately // Update the stream URL and details immediately
setCurrentStreamUrl(newStream.url); setCurrentStreamUrl(newStream.url);
setCurrentQuality(newQuality); setCurrentQuality(newQuality);
setCurrentStreamProvider(newProvider); setCurrentStreamProvider(newProvider);
setCurrentStreamName(newStreamName); setCurrentStreamName(newStreamName);
// Reset player state for new source // Reset player state for new source
setCurrentTime(0); setCurrentTime(0);
setDuration(0); setDuration(0);
setIsPlayerReady(false); setIsPlayerReady(false);
setIsVideoLoaded(false); setIsVideoLoaded(false);
} catch (error) { } catch (error) {
logger.error('[VideoPlayer] Error changing source:', error); logger.error('[VideoPlayer] Error changing source:', error);
setPendingSeek(null); setPendingSeek(null);
@ -1019,14 +1081,22 @@ const VideoPlayer: React.FC = () => {
}; };
return ( return (
<View style={[styles.container, { <View style={[
width: screenDimensions.width, styles.container,
height: screenDimensions.height, shouldUseFullscreen ? {
position: 'absolute', // iPad fullscreen: use flex layout instead of absolute positioning
top: 0, flex: 1,
left: 0, width: '100%',
}]}> height: '100%',
<Animated.View } : {
// iPhone: use absolute positioning with screen dimensions
width: screenDimensions.width,
height: screenDimensions.height,
position: 'absolute',
top: 0,
left: 0,
}]}>
<Animated.View
style={[ style={[
styles.openingOverlay, styles.openingOverlay,
{ {
@ -1056,15 +1126,15 @@ const VideoPlayer: React.FC = () => {
locations={[0, 0.3, 0.7, 1]} locations={[0, 0.3, 0.7, 1]}
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
/> />
<TouchableOpacity <TouchableOpacity
style={styles.loadingCloseButton} style={styles.loadingCloseButton}
onPress={handleClose} onPress={handleClose}
activeOpacity={0.7} activeOpacity={0.7}
> >
<MaterialIcons name="close" size={24} color="#ffffff" /> <MaterialIcons name="close" size={24} color="#ffffff" />
</TouchableOpacity> </TouchableOpacity>
<View style={styles.openingContent}> <View style={styles.openingContent}>
{hasLogo ? ( {hasLogo ? (
<Animated.View style={{ <Animated.View style={{
@ -1093,7 +1163,7 @@ const VideoPlayer: React.FC = () => {
{/* Source Change Loading Overlay */} {/* Source Change Loading Overlay */}
{isChangingSource && ( {isChangingSource && (
<Animated.View <Animated.View
style={[ style={[
styles.sourceChangeOverlay, styles.sourceChangeOverlay,
{ {
@ -1112,7 +1182,7 @@ const VideoPlayer: React.FC = () => {
</Animated.View> </Animated.View>
)} )}
<Animated.View <Animated.View
style={[ style={[
styles.videoPlayerContainer, styles.videoPlayerContainer,
{ {
@ -1205,11 +1275,12 @@ const VideoPlayer: React.FC = () => {
buffered={buffered} buffered={buffered}
formatTime={formatTime} formatTime={formatTime}
/> />
<CustomSubtitles <CustomSubtitles
useCustomSubtitles={useCustomSubtitles} useCustomSubtitles={useCustomSubtitles}
currentSubtitle={currentSubtitle} currentSubtitle={currentSubtitle}
subtitleSize={subtitleSize} subtitleSize={subtitleSize}
subtitleBackground={subtitleBackground}
zoomScale={zoomScale} zoomScale={zoomScale}
/> />
@ -1223,7 +1294,7 @@ const VideoPlayer: React.FC = () => {
handleResume={handleResume} handleResume={handleResume}
handleStartFromBeginning={handleStartFromBeginning} handleStartFromBeginning={handleStartFromBeginning}
/> />
</TouchableOpacity> </TouchableOpacity>
</Animated.View> </Animated.View>
<AudioTrackModal <AudioTrackModal
@ -1246,13 +1317,15 @@ const VideoPlayer: React.FC = () => {
selectedTextTrack={selectedTextTrack} selectedTextTrack={selectedTextTrack}
useCustomSubtitles={useCustomSubtitles} useCustomSubtitles={useCustomSubtitles}
subtitleSize={subtitleSize} subtitleSize={subtitleSize}
subtitleBackground={subtitleBackground}
fetchAvailableSubtitles={fetchAvailableSubtitles} fetchAvailableSubtitles={fetchAvailableSubtitles}
loadWyzieSubtitle={loadWyzieSubtitle} loadWyzieSubtitle={loadWyzieSubtitle}
selectTextTrack={selectTextTrack} selectTextTrack={selectTextTrack}
increaseSubtitleSize={increaseSubtitleSize} increaseSubtitleSize={increaseSubtitleSize}
decreaseSubtitleSize={decreaseSubtitleSize} decreaseSubtitleSize={decreaseSubtitleSize}
toggleSubtitleBackground={toggleSubtitleBackground}
/> />
<SourcesModal <SourcesModal
showSourcesModal={showSourcesModal} showSourcesModal={showSourcesModal}
setShowSourcesModal={setShowSourcesModal} setShowSourcesModal={setShowSourcesModal}
@ -1261,7 +1334,7 @@ const VideoPlayer: React.FC = () => {
onSelectStream={handleSelectStream} onSelectStream={handleSelectStream}
isChangingSource={isChangingSource} isChangingSource={isChangingSource}
/> />
</View> </View>
); );
}; };

View file

@ -3,6 +3,7 @@ import { StyleSheet } from 'react-native';
export const styles = StyleSheet.create({ export const styles = StyleSheet.create({
container: { container: {
backgroundColor: '#000', backgroundColor: '#000',
flex: 1,
position: 'absolute', position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,

View file

@ -770,9 +770,20 @@ const AppNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootStack
options={{ options={{
animation: 'slide_from_right', animation: 'slide_from_right',
animationDuration: Platform.OS === 'android' ? 200 : 300, animationDuration: Platform.OS === 'android' ? 200 : 300,
// Force fullscreen presentation on iPad
presentation: Platform.OS === 'ios' ? 'fullScreenModal' : 'card',
// Disable gestures during video playback
gestureEnabled: false,
// Ensure proper orientation handling
orientation: 'landscape',
contentStyle: { contentStyle: {
backgroundColor: '#000000', // Pure black for video player backgroundColor: '#000000', // Pure black for video player
}, },
// iPad-specific fullscreen options
...(Platform.OS === 'ios' && {
statusBarHidden: true,
statusBarAnimation: 'none',
}),
}} }}
/> />
<Stack.Screen <Stack.Screen