mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-20 16:22:04 +00:00
fixes to videoplayer
This commit is contained in:
parent
42daa4decc
commit
19b6e6b3d5
3 changed files with 198 additions and 113 deletions
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue