diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 5af6b374e..958b7731d 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; +import React, { useState, useEffect, useCallback, useRef, useMemo, useLayoutEffect } from 'react'; import { View, Text, @@ -16,7 +16,8 @@ import { Platform, Image, Modal, - Pressable + Pressable, + Alert } from 'react-native'; import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; @@ -60,6 +61,7 @@ import { SkeletonFeatured } from '../components/home/SkeletonLoaders'; import homeStyles, { sharedStyles } from '../styles/homeStyles'; import { useTheme } from '../contexts/ThemeContext'; import type { Theme } from '../contexts/ThemeContext'; +import * as ScreenOrientation from 'expo-screen-orientation'; // Define interfaces for our data interface Category { @@ -517,18 +519,37 @@ const HomeScreen = () => { navigation.navigate('Metadata', { id, type }); }, [navigation]); - const handlePlayStream = useCallback((stream: Stream) => { + const handlePlayStream = useCallback(async (stream: Stream) => { if (!featuredContent) return; - navigation.navigate('Player', { - uri: stream.url, - title: featuredContent.name, - year: featuredContent.year, - quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, - streamProvider: stream.name, - id: featuredContent.id, - type: featuredContent.type - }); + try { + // Lock orientation to landscape before navigation to prevent glitches + await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); + + // Small delay to ensure orientation is set before navigation + await new Promise(resolve => setTimeout(resolve, 100)); + + navigation.navigate('Player', { + uri: stream.url, + title: featuredContent.name, + year: featuredContent.year, + quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, + streamProvider: stream.name, + id: featuredContent.id, + type: featuredContent.type + }); + } catch (error) { + // Fallback: navigate anyway + navigation.navigate('Player', { + uri: stream.url, + title: featuredContent.name, + year: featuredContent.year, + quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, + streamProvider: stream.name, + id: featuredContent.id, + type: featuredContent.type + }); + } }, [featuredContent, navigation]); const refreshContinueWatching = useCallback(async () => { diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index ac5ef769e..5ac6a52ec 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, memo, useState, useEffect } from 'react'; +import React, { useCallback, useMemo, memo, useState, useEffect, useRef, useLayoutEffect } from 'react'; import { View, Text, @@ -13,8 +13,9 @@ import { StatusBar, Alert, Dimensions, - Linking + Linking, } from 'react-native'; +import * as ScreenOrientation from 'expo-screen-orientation'; import { useRoute, useNavigation } from '@react-navigation/native'; import { RouteProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; @@ -241,10 +242,22 @@ export const StreamsScreen = () => { const { currentTheme } = useTheme(); const { colors } = currentTheme; + // Add ref to prevent excessive updates + const isMounted = useRef(true); + const loadStartTimeRef = useRef(0); + const hasDoneInitialLoadRef = useRef(false); + // Add timing logs const [loadStartTime, setLoadStartTime] = useState(0); const [providerLoadTimes, setProviderLoadTimes] = useState<{[key: string]: number}>({}); + // Prevent excessive re-renders by using this guard + const guardedSetState = useCallback((setter: () => void) => { + if (isMounted.current) { + setter(); + } + }, []); + const { metadata, episodes, @@ -285,54 +298,64 @@ export const StreamsScreen = () => { } }>({}); - // Monitor streams loading start and completion + // Monitor streams loading start and completion - FIXED to prevent loops useEffect(() => { + // Skip processing if component is unmounting + if (!isMounted.current) return; + const now = Date.now(); // Define all providers you expect to load. This could be dynamic. const expectedProviders = ['stremio', 'hdrezka']; + // Prevent infinite rerendering by using refs if (loadingStreams || loadingEpisodeStreams) { // --- Stream Loading has STARTED or is IN PROGRESS --- - logger.log("⏱️ Stream loading started or in progress..."); - - // Set load start time only if this is the beginning of a new loading cycle - if (loadStartTime === 0) { + // Only log once when loading starts + if (loadStartTimeRef.current === 0) { + logger.log("⏱️ Stream loading started or in progress..."); + // Update ref directly to avoid render cycle + loadStartTimeRef.current = now; + // Also update state for components that need it setLoadStartTime(now); } - setProviderLoadTimes({}); // Reset individual provider load times tracker + // Only update these once per loading cycle + if (!hasDoneInitialLoadRef.current) { + hasDoneInitialLoadRef.current = true; + + // Use the guarded setState to prevent issues after unmount + guardedSetState(() => setProviderLoadTimes({})); - // Update provider status to loading for all expected providers - setProviderStatus(prevStatus => { - const newStatus = { ...prevStatus }; - expectedProviders.forEach(providerId => { - // If not already marked as loading, or if it's a fresh cycle, set to loading - if (!newStatus[providerId] || !newStatus[providerId].loading || loadStartTime === 0) { - newStatus[providerId] = { - loading: true, - success: false, - error: false, - message: 'Loading...', - timeStarted: (newStatus[providerId]?.loading && newStatus[providerId]?.timeStarted) ? newStatus[providerId].timeStarted : now, - timeCompleted: 0, - }; - } - }); - return newStatus; - }); + // Update provider status to loading for all expected providers + guardedSetState(() => setProviderStatus(prevStatus => { + const newStatus = { ...prevStatus }; + expectedProviders.forEach(providerId => { + // If not already marked as loading, or if it's a fresh cycle, set to loading + if (!newStatus[providerId] || !newStatus[providerId].loading) { + newStatus[providerId] = { + loading: true, + success: false, + error: false, + message: 'Loading...', + timeStarted: (newStatus[providerId]?.loading && newStatus[providerId]?.timeStarted) ? newStatus[providerId].timeStarted : now, + timeCompleted: 0, + }; + } + }); + return newStatus; + })); - // Update simple loading flag for all expected providers - setLoadingProviders(prevLoading => { - const newLoading = { ...prevLoading }; - expectedProviders.forEach(providerId => { - newLoading[providerId] = true; - }); - return newLoading; - }); - - } else if (loadStartTime > 0) { + // Update simple loading flag for all expected providers + guardedSetState(() => setLoadingProviders(prevLoading => { + const newLoading = { ...prevLoading }; + expectedProviders.forEach(providerId => { + newLoading[providerId] = true; + }); + return newLoading; + })); + } + } else if (loadStartTimeRef.current > 0) { // --- Stream Loading has FINISHED --- - // (loadStartTime > 0 implies a loading cycle was active and has now completed) logger.log("🏁 Stream loading finished. Processing results."); const currentStreamsData = type === 'series' ? episodeStreams : groupedStreams; @@ -344,56 +367,53 @@ export const StreamsScreen = () => { logger.log(`📊 Providers with streams: ${providersWithStreams.join(', ')}`); - // Update simple loading flag: all expected providers are no longer loading - setLoadingProviders(prevLoading => { - const newLoading = { ...prevLoading }; - expectedProviders.forEach(providerId => { - newLoading[providerId] = false; - }); - return newLoading; - }); + // Reset refs for next load cycle + loadStartTimeRef.current = 0; + hasDoneInitialLoadRef.current = false; + + // Update states only if component is still mounted + if (isMounted.current) { + // Update simple loading flag: all expected providers are no longer loading + guardedSetState(() => setLoadingProviders(prevLoading => { + const newLoading = { ...prevLoading }; + expectedProviders.forEach(providerId => { + newLoading[providerId] = false; + }); + return newLoading; + })); - // Update detailed provider status based on results - setProviderStatus(prevStatus => { - const newStatus = { ...prevStatus }; - expectedProviders.forEach(providerId => { - if (newStatus[providerId]) { // Ensure the provider entry exists - const providerHasStreams = currentStreamsData[providerId] && - currentStreamsData[providerId].streams && - currentStreamsData[providerId].streams.length > 0; - - newStatus[providerId] = { - ...newStatus[providerId], // Preserve timeStarted - loading: false, - success: providerHasStreams, - // Mark error if it was loading and now no streams, and wasn't already successful - error: !providerHasStreams && newStatus[providerId].loading && !newStatus[providerId].success, - message: providerHasStreams ? 'Loaded successfully' : (newStatus[providerId].error ? 'Error or no streams' : 'No streams found'), - timeCompleted: now, - }; - } else { - // Fallback if somehow not initialized (should be caught by loading phase) - newStatus[providerId] = { - loading: false, - success: false, - error: true, - message: 'Provider status error (not initialized)', - timeStarted: 0, - timeCompleted: now, - }; - } - }); - return newStatus; - }); + // Update detailed provider status based on results + guardedSetState(() => setProviderStatus(prevStatus => { + const newStatus = { ...prevStatus }; + expectedProviders.forEach(providerId => { + if (newStatus[providerId]) { // Ensure the provider entry exists + const providerHasStreams = currentStreamsData[providerId] && + currentStreamsData[providerId].streams && + currentStreamsData[providerId].streams.length > 0; + + newStatus[providerId] = { + ...newStatus[providerId], // Preserve timeStarted + loading: false, + success: providerHasStreams, + // Mark error if it was loading and now no streams, and wasn't already successful + error: !providerHasStreams && newStatus[providerId].loading && !newStatus[providerId].success, + message: providerHasStreams ? 'Loaded successfully' : (newStatus[providerId].error ? 'Error or no streams' : 'No streams found'), + timeCompleted: now, + }; + } + }); + return newStatus; + })); - // Update the set of available providers based on what actually loaded streams - const providersWithStreamsSet = new Set(providersWithStreams); - setAvailableProviders(providersWithStreamsSet); + // Update the set of available providers based on what actually loaded streams + const providersWithStreamsSet = new Set(providersWithStreams); + guardedSetState(() => setAvailableProviders(providersWithStreamsSet)); - // Reset loadStartTime to signify the end of this loading cycle - setLoadStartTime(0); + // Reset loadStartTime to signify the end of this loading cycle + guardedSetState(() => setLoadStartTime(0)); + } } - }, [loadingStreams, loadingEpisodeStreams, groupedStreams, episodeStreams, type /* loadStartTime is intentionally omitted from deps here */]); + }, [loadingStreams, loadingEpisodeStreams, groupedStreams, episodeStreams, type, guardedSetState]); // Add useEffect to update availableProviders whenever streams change useEffect(() => { @@ -487,20 +507,44 @@ export const StreamsScreen = () => { ); }, [selectedEpisode, groupedEpisodes, id]); - const navigateToPlayer = useCallback((stream: Stream) => { - navigation.navigate('Player', { - uri: stream.url, - title: metadata?.name || '', - episodeTitle: type === 'series' ? currentEpisode?.name : undefined, - season: type === 'series' ? currentEpisode?.season_number : undefined, - episode: type === 'series' ? currentEpisode?.episode_number : undefined, - quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, - year: metadata?.year, - streamProvider: stream.name, - id, - type, - episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined - }); + const navigateToPlayer = useCallback(async (stream: Stream) => { + try { + // Lock orientation to landscape before navigation to prevent glitches + await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); + + // Small delay to ensure orientation is set before navigation + await new Promise(resolve => setTimeout(resolve, 100)); + + navigation.navigate('Player', { + uri: stream.url, + title: metadata?.name || '', + episodeTitle: type === 'series' ? currentEpisode?.name : undefined, + season: type === 'series' ? currentEpisode?.season_number : undefined, + episode: type === 'series' ? currentEpisode?.episode_number : undefined, + quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, + year: metadata?.year, + streamProvider: stream.name, + id, + type, + episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined + }); + } catch (error) { + logger.error('[StreamsScreen] Error locking orientation before navigation:', error); + // Fallback: navigate anyway + navigation.navigate('Player', { + uri: stream.url, + title: metadata?.name || '', + episodeTitle: type === 'series' ? currentEpisode?.name : undefined, + season: type === 'series' ? currentEpisode?.season_number : undefined, + episode: type === 'series' ? currentEpisode?.episode_number : undefined, + quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, + year: metadata?.year, + streamProvider: stream.name, + id, + type, + episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined + }); + } }, [metadata, type, currentEpisode, navigation, id, selectedEpisode]); // Update handleStreamPress @@ -834,6 +878,13 @@ export const StreamsScreen = () => { ), [styles.streamGroupTitle]); + // Cleanup on unmount + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + return ( { + // This function only runs once per call site, avoiding render loops + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + if (DEBUG_MODE) { + if (data) { + logger.log(`[VideoPlayer] ${message}`, data); + } else { + logger.log(`[VideoPlayer] ${message}`); + } + } + }, []); // Empty dependency array means this only runs once per mount +}; + // Constants for resume preferences - add after type definitions const RESUME_PREF_KEY = '@video_resume_preference'; const RESUME_PREF = { @@ -129,7 +149,16 @@ const languageMap: {[key: string]: string} = { const formatLanguage = (code?: string): string => { if (!code) return 'Unknown'; const normalized = code.toLowerCase(); - return languageMap[normalized] || code.toUpperCase(); + const languageName = languageMap[normalized] || code.toUpperCase(); + + // Debug logs removed to prevent render loops + + // If the result is still the uppercased code, it means we couldn't find it in our map. + if (languageName === code.toUpperCase()) { + return `Unknown (${code})`; + } + + return languageName; }; // Add VLC specific interface for their event structure @@ -144,6 +173,20 @@ interface VlcMediaEvent { selectedTextTrack?: number; } +// Helper function to extract a display name from the track's name property +const getTrackDisplayName = (track: { name?: string, id: number }): string => { + if (!track || !track.name) return `Track ${track.id}`; + + // Try to extract language from name like "Some Info - [English]" + const languageMatch = track.name.match(/\[(.*?)\]/); + if (languageMatch && languageMatch[1]) { + return languageMatch[1]; + } + + // If no language in brackets, or if the name is simple, use the full name + return track.name; +}; + const VideoPlayer: React.FC = () => { const navigation = useNavigation(); const route = useRoute>(); @@ -163,19 +206,10 @@ const VideoPlayer: React.FC = () => { episodeId } = route.params; - // Log received props for debugging - logger.log("[VideoPlayer] Received props:", { - uri, - title, - season, - episode, - episodeTitle, - quality, - year, - streamProvider, - id, - type, - episodeId + // Use safer debug logging for props + safeDebugLog("Component mounted with props", { + uri, title, season, episode, episodeTitle, quality, year, + streamProvider, id, type, episodeId }); const [paused, setPaused] = useState(false); @@ -186,7 +220,7 @@ const VideoPlayer: React.FC = () => { const [audioTracks, setAudioTracks] = useState([]); const [selectedAudioTrack, setSelectedAudioTrack] = useState(null); const [textTracks, setTextTracks] = useState([]); - const [selectedTextTrack, setSelectedTextTrack] = useState({ type: 'disabled' }); + const [selectedTextTrack, setSelectedTextTrack] = useState(-1); // Use -1 for "disabled" const [resizeMode, setResizeMode] = useState('contain'); // State for resize mode const [buffered, setBuffered] = useState(0); // Add buffered state const vlcRef = useRef(null); @@ -212,6 +246,12 @@ const VideoPlayer: React.FC = () => { // Add animated value for controls opacity const fadeAnim = useRef(new Animated.Value(1)).current; + // Add opening animation states and values + const [isOpeningAnimationComplete, setIsOpeningAnimationComplete] = useState(false); + const openingFadeAnim = useRef(new Animated.Value(0)).current; + const openingScaleAnim = useRef(new Animated.Value(0.8)).current; + const backgroundFadeAnim = useRef(new Animated.Value(1)).current; + // Add VLC specific state and refs const [isBuffering, setIsBuffering] = useState(false); @@ -219,17 +259,35 @@ const VideoPlayer: React.FC = () => { const [vlcAudioTracks, setVlcAudioTracks] = useState>([]); const [vlcTextTracks, setVlcTextTracks] = useState>([]); + // Add a new state to track if the player is ready for seeking + const [isPlayerReady, setIsPlayerReady] = useState(false); + + // Animated value for smooth progress bar + const progressAnim = useRef(new Animated.Value(0)).current; + + // Add ref for progress bar container to measure its width + const progressBarRef = useRef(null); + + // Add state for progress bar touch tracking + const [isDragging, setIsDragging] = useState(false); + + // Add a ref for debouncing seek operations + const seekDebounceTimer = useRef(null); + const pendingSeekValue = useRef(null); + const lastSeekTime = useRef(0); + // Lock screen to landscape when component mounts useEffect(() => { - const lockToLandscape = async () => { - await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); + // Since orientation is now locked before navigation, we can start immediately + const initializePlayer = () => { + // Enable immersive mode + enableImmersiveMode(); + + // Start the opening animation immediately + startOpeningAnimation(); }; - // Lock to landscape - lockToLandscape(); - - // Enable immersive mode when component mounts - enableImmersiveMode(); + initializePlayer(); // Restore screen orientation and disable immersive mode when component unmounts return () => { @@ -241,24 +299,68 @@ const VideoPlayer: React.FC = () => { }; }, []); + // Opening animation sequence + const startOpeningAnimation = () => { + // Much shorter delay since rotation is already handled + setTimeout(() => { + // Start the main animation sequence + Animated.parallel([ + // Fade in the video player + Animated.timing(openingFadeAnim, { + toValue: 1, + duration: 600, // Reduced back to original duration + useNativeDriver: true, + }), + // Scale up from 80% to 100% + Animated.timing(openingScaleAnim, { + toValue: 1, + duration: 700, // Reduced back to original duration + useNativeDriver: true, + }), + // Fade out the black background overlay + Animated.timing(backgroundFadeAnim, { + toValue: 0, + duration: 800, // Reduced back to original duration + useNativeDriver: true, + }), + ]).start(() => { + // Animation is complete + setIsOpeningAnimationComplete(true); + + // Hide the background overlay completely after animation + setTimeout(() => { + backgroundFadeAnim.setValue(0); + }, 100); + }); + }, 150); // Much shorter delay since no rotation is needed + }; + // Load saved watch progress on mount useEffect(() => { const loadWatchProgress = async () => { if (id && type) { try { - logger.log(`[VideoPlayer] Checking for saved progress with id=${id}, type=${type}, episodeId=${episodeId || 'none'}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Checking for saved progress with id=${id}, type=${type}, episodeId=${episodeId || 'none'}`); + } const savedProgress = await storageService.getWatchProgress(id, type, episodeId); if (savedProgress) { - logger.log(`[VideoPlayer] Found saved progress:`, savedProgress); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Found saved progress:`, savedProgress); + } if (savedProgress.currentTime > 0) { // Only auto-resume if less than 95% watched (not effectively complete) const progressPercent = (savedProgress.currentTime / savedProgress.duration) * 100; - logger.log(`[VideoPlayer] Progress percent: ${progressPercent.toFixed(2)}%`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Progress percent: ${progressPercent.toFixed(2)}%`); + } if (progressPercent < 95) { - logger.log(`[VideoPlayer] Setting initial position to ${savedProgress.currentTime}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Setting initial position to ${savedProgress.currentTime}`); + } // Set resume position setResumePosition(savedProgress.currentTime); @@ -266,25 +368,29 @@ const VideoPlayer: React.FC = () => { const pref = await AsyncStorage.getItem(RESUME_PREF_KEY); if (pref === RESUME_PREF.ALWAYS_RESUME) { setInitialPosition(savedProgress.currentTime); - logger.log(`[VideoPlayer] Auto-resuming based on saved preference`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Auto-resuming based on saved preference`); + } } else if (pref === RESUME_PREF.ALWAYS_START_OVER) { setInitialPosition(0); - logger.log(`[VideoPlayer] Auto-starting from beginning based on saved preference`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Auto-starting from beginning based on saved preference`); + } } else { // Only show resume overlay if no preference or ALWAYS_ASK setShowResumeOverlay(true); } - } else { + } else if (DEBUG_MODE) { logger.log(`[VideoPlayer] Progress >= 95%, starting from beginning`); } } - } else { + } else if (DEBUG_MODE) { logger.log(`[VideoPlayer] No saved progress found`); } } catch (error) { logger.error('[VideoPlayer] Error loading watch progress:', error); } - } else { + } else if (DEBUG_MODE) { logger.log(`[VideoPlayer] Missing id or type, can't load progress. id=${id}, type=${type}`); } }; @@ -335,11 +441,10 @@ const VideoPlayer: React.FC = () => { try { await storageService.setWatchProgress(id, type, progress, episodeId); - logger.log(`[VideoPlayer] Saved progress: ${currentTime.toFixed(1)}/${duration.toFixed(1)} (${((currentTime/duration)*100).toFixed(1)}%)`); } catch (error) { logger.error('[VideoPlayer] Error saving watch progress:', error); } - } else { + } else if (DEBUG_MODE) { logger.log(`[VideoPlayer] Cannot save progress: id=${id}, type=${type}, currentTime=${currentTime}, duration=${duration}`); } }; @@ -360,94 +465,160 @@ const VideoPlayer: React.FC = () => { } }; - const onSliderValueChange = (value: number) => { - if (vlcRef.current) { - const newTime = Math.floor(value); - vlcRef.current.seek(newTime); - setCurrentTime(newTime); - progress.value = newTime; + // Replace the reset seek value effect + // useEffect(() => { + // if (seekValue !== undefined) { + // const timer = setTimeout(() => { + // if (isMounted.current) { + // setSeekValue(undefined); + // } + // }, 1000); // Longer timeout to ensure VLC processes the seek properly + + // return () => clearTimeout(timer); + // } + // }, [seekValue]); + + // Simplify the seekToTime function to use VLC's direct methods + const seekToTime = (timeInSeconds: number) => { + if (!isPlayerReady || duration <= 0 || !vlcRef.current) return; + + // Calculate normalized position (0-1) for VLC + const normalizedPosition = Math.max(0, Math.min(timeInSeconds / duration, 1)); + + try { + // Use VLC's direct setPosition method + if (typeof vlcRef.current.setPosition === 'function') { + vlcRef.current.setPosition(normalizedPosition); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Called setPosition with ${normalizedPosition} for time: ${timeInSeconds}s`); + } + } else if (typeof vlcRef.current.seek === 'function') { + // Fallback to seek method if available + vlcRef.current.seek(normalizedPosition); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Called seek with ${normalizedPosition} for time: ${timeInSeconds}s`); + } + } else { + logger.error('[VideoPlayer] No seek method available on VLC player'); + } + + // Update UI immediately for responsiveness + const progressPercent = timeInSeconds / duration; + progressAnim.setValue(progressPercent); + + } catch (error) { + logger.error('[VideoPlayer] Error during seek operation:', error); } }; - const togglePlayback = () => { - if (vlcRef.current) { - if (paused) { - vlcRef.current.resume(); - } else { - vlcRef.current.pause(); + // Simplify handleProgress to always update state + const handleProgress = (event: any) => { + const currentTimeInSeconds = event.currentTime / 1000; // VLC gives time in milliseconds + + // Always update state - let VLC manage the timing + if (Math.abs(currentTimeInSeconds - currentTime) > 0.5) { + safeSetState(() => setCurrentTime(currentTimeInSeconds)); + progress.value = currentTimeInSeconds; + + // Animate the progress bar smoothly + const progressPercent = duration > 0 ? currentTimeInSeconds / duration : 0; + Animated.timing(progressAnim, { + toValue: progressPercent, + duration: 250, + useNativeDriver: false, + }).start(); + + // Update buffered position + const bufferedTime = event.bufferTime / 1000 || currentTimeInSeconds; + safeSetState(() => setBuffered(bufferedTime)); + } + }; + + // Enhanced onLoad handler to mark player as ready + const onLoad = (data: any) => { + setDuration(data.duration / 1000); // VLC returns duration in milliseconds + max.value = data.duration / 1000; + + // Mark player as ready for seeking + setIsPlayerReady(true); + + // Get audio and subtitle tracks from onLoad data + const audioTracksFromLoad = data.audioTracks || []; + const textTracksFromLoad = data.textTracks || []; + setVlcAudioTracks(audioTracksFromLoad); + setVlcTextTracks(textTracksFromLoad); + + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Video loaded with duration: ${data.duration / 1000}`); + const methods = Object.keys(vlcRef.current || {}).filter( + key => typeof vlcRef.current[key] === 'function' + ); + logger.log('[VideoPlayer] Available VLC methods:', methods); + logger.log('[VideoPlayer] Available audio tracks:', audioTracksFromLoad); + logger.log('[VideoPlayer] Available subtitle tracks:', textTracksFromLoad); + } + + // Set default selected tracks + if (audioTracksFromLoad.length > 1) { // More than just "Disable" + const firstEnabledAudio = audioTracksFromLoad.find((t: any) => t.id !== -1); + if(firstEnabledAudio) { + setSelectedAudioTrack(firstEnabledAudio.id); + } + } else if (audioTracksFromLoad.length > 0) { + setSelectedAudioTrack(audioTracksFromLoad[0].id); + } + // Subtitles default to disabled (-1) + + // If we have an initial position to seek to, do it now + if (initialPosition !== null && !isInitialSeekComplete) { + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Will seek to saved position: ${initialPosition}`); } - setPaused(!paused); + + // Seek with a short delay to ensure video is ready + setTimeout(() => { + if (vlcRef.current && duration > 0 && isMounted.current) { + seekToTime(initialPosition); + setIsInitialSeekComplete(true); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Initial seek completed to position: ${initialPosition}s`); + } + } + }, 1000); } }; const skip = (seconds: number) => { if (vlcRef.current) { const newTime = Math.max(0, Math.min(currentTime + seconds, duration)); - vlcRef.current.seek(newTime); - setCurrentTime(newTime); - progress.value = newTime; - } - }; - - const onProgress = (data: { currentTime: number }) => { - setCurrentTime(data.currentTime); - progress.value = data.currentTime; - }; - - const onLoad = (data: any) => { - setDuration(data.duration / 1000); // VLC returns duration in milliseconds - max.value = data.duration / 1000; - - logger.log(`[VideoPlayer] Video loaded with duration: ${data.duration / 1000}`); - - // If we have an initial position to seek to, do it now - if (initialPosition !== null && !isInitialSeekComplete && vlcRef.current) { - logger.log(`[VideoPlayer] Will seek to saved position: ${initialPosition}`); - - // Seek immediately with a small delay - setTimeout(() => { - if (vlcRef.current) { - try { - vlcRef.current.seek(initialPosition); - setCurrentTime(initialPosition); - progress.value = initialPosition; - setIsInitialSeekComplete(true); - logger.log(`[VideoPlayer] Successfully seeked to saved position: ${initialPosition}`); - } catch (error) { - logger.error('[VideoPlayer] Error seeking to saved position:', error); - } - } else { - logger.error('[VideoPlayer] vlcRef is no longer valid when attempting to seek'); - } - }, 1000); // Increase delay to ensure video is fully loaded - } else { - if (initialPosition === null) { - logger.log(`[VideoPlayer] No initial position to seek to`); - } else if (isInitialSeekComplete) { - logger.log(`[VideoPlayer] Initial seek already completed`); - } else { - logger.log(`[VideoPlayer] vlcRef not available for seeking`); - } + seekToTime(newTime); + // Let seekToTime handle all state updates } }; const onAudioTracks = (data: { audioTracks: AudioTrack[] }) => { const tracks = data.audioTracks || []; setAudioTracks(tracks); - logger.log(`[VideoPlayer] Available audio tracks:`, tracks); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Available audio tracks:`, tracks); + } }; const onTextTracks = (e: Readonly<{ textTracks: TextTrack[] }>) => { const tracks = e.textTracks || []; setTextTracks(tracks); - logger.log(`[VideoPlayer] Available subtitle tracks:`, tracks); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Available subtitle tracks:`, tracks); + } }; // Toggle through aspect ratio modes const cycleAspectRatio = () => { const currentIndex = resizeModes.indexOf(resizeMode); const nextIndex = (currentIndex + 1) % resizeModes.length; - logger.log(`[VideoPlayer] Changing aspect ratio from ${resizeMode} to ${resizeModes[nextIndex]}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Changing aspect ratio from ${resizeMode} to ${resizeModes[nextIndex]}`); + } setResizeMode(resizeModes[nextIndex]); }; @@ -495,14 +666,14 @@ const VideoPlayer: React.FC = () => { // Add debug logs for modal visibility useEffect(() => { - if (showAudioModal) { + if (showAudioModal && DEBUG_MODE) { logger.log("[VideoPlayer] Audio modal should be visible now"); logger.log("[VideoPlayer] Available audio tracks:", audioTracks); } }, [showAudioModal, audioTracks]); useEffect(() => { - if (showSubtitleModal) { + if (showSubtitleModal && DEBUG_MODE) { logger.log("[VideoPlayer] Subtitle modal should be visible now"); logger.log("[VideoPlayer] Available text tracks:", textTracks); } @@ -511,13 +682,15 @@ const VideoPlayer: React.FC = () => { // Attempt to seek once vlcRef is available useEffect(() => { if (initialPosition !== null && !isInitialSeekComplete && vlcRef.current) { - logger.log(`[VideoPlayer] vlcRef is now available, attempting to seek to: ${initialPosition}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] vlcRef is now available, attempting to seek to: ${initialPosition}`); + } try { - vlcRef.current.seek(initialPosition); - setCurrentTime(initialPosition); - progress.value = initialPosition; + seekToTime(initialPosition); setIsInitialSeekComplete(true); - logger.log(`[VideoPlayer] Successfully seeked to position: ${initialPosition}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Successfully seeked to position: ${initialPosition}`); + } } catch (error) { logger.error('[VideoPlayer] Error seeking to position on ref available:', error); } @@ -531,17 +704,23 @@ const VideoPlayer: React.FC = () => { const pref = await AsyncStorage.getItem(RESUME_PREF_KEY); if (pref) { setResumePreference(pref); - logger.log(`[VideoPlayer] Loaded resume preference: ${pref}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Loaded resume preference: ${pref}`); + } // If user has a preference, apply it automatically if (pref === RESUME_PREF.ALWAYS_RESUME && resumePosition !== null) { setShowResumeOverlay(false); setInitialPosition(resumePosition); - logger.log(`[VideoPlayer] Auto-resuming based on saved preference`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Auto-resuming based on saved preference`); + } } else if (pref === RESUME_PREF.ALWAYS_START_OVER) { setShowResumeOverlay(false); setInitialPosition(0); - logger.log(`[VideoPlayer] Auto-starting from beginning based on saved preference`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Auto-starting from beginning based on saved preference`); + } } } } catch (error) { @@ -557,7 +736,9 @@ const VideoPlayer: React.FC = () => { try { await AsyncStorage.removeItem(RESUME_PREF_KEY); setResumePreference(null); - logger.log(`[VideoPlayer] Reset resume preference`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Reset resume preference`); + } } catch (error) { logger.error('[VideoPlayer] Error resetting resume preference:', error); } @@ -566,13 +747,17 @@ const VideoPlayer: React.FC = () => { // Handle resume from overlay - modified for VLC const handleResume = async () => { if (resumePosition !== null && vlcRef.current) { - logger.log(`[VideoPlayer] Resuming from ${resumePosition}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Resuming from ${resumePosition}`); + } // Save preference if remember choice is checked if (rememberChoice) { try { await AsyncStorage.setItem(RESUME_PREF_KEY, RESUME_PREF.ALWAYS_RESUME); - logger.log(`[VideoPlayer] Saved resume preference: ${RESUME_PREF.ALWAYS_RESUME}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Saved resume preference: ${RESUME_PREF.ALWAYS_RESUME}`); + } } catch (error) { logger.error('[VideoPlayer] Error saving resume preference:', error); } @@ -586,7 +771,7 @@ const VideoPlayer: React.FC = () => { // Seek to position with VLC setTimeout(() => { if (vlcRef.current) { - vlcRef.current.seek(resumePosition); + seekToTime(resumePosition); } }, 500); } @@ -594,13 +779,17 @@ const VideoPlayer: React.FC = () => { // Handle start from beginning - modified for VLC const handleStartFromBeginning = async () => { - logger.log(`[VideoPlayer] Starting from beginning`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Starting from beginning`); + } // Save preference if remember choice is checked if (rememberChoice) { try { await AsyncStorage.setItem(RESUME_PREF_KEY, RESUME_PREF.ALWAYS_START_OVER); - logger.log(`[VideoPlayer] Saved resume preference: ${RESUME_PREF.ALWAYS_START_OVER}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Saved resume preference: ${RESUME_PREF.ALWAYS_START_OVER}`); + } } catch (error) { logger.error('[VideoPlayer] Error saving resume preference:', error); } @@ -612,7 +801,7 @@ const VideoPlayer: React.FC = () => { setInitialPosition(0); // Make sure we seek to beginning if (vlcRef.current) { - vlcRef.current.seek(0); + seekToTime(0); setCurrentTime(0); progress.value = 0; } @@ -631,29 +820,6 @@ const VideoPlayer: React.FC = () => { setShowControls(!showControls); }; - // Handle VLC progress updates - const handleProgress = (event: any) => { - const currentTimeInSeconds = event.currentTime / 1000; // VLC gives time in milliseconds - setCurrentTime(currentTimeInSeconds); - progress.value = currentTimeInSeconds; - - // Update buffered position - const bufferedTime = event.bufferTime / 1000 || currentTimeInSeconds; - setBuffered(bufferedTime); - - // Calculate buffer ahead (cannot be negative) - const bufferAhead = Math.max(0, bufferedTime - currentTimeInSeconds); - const bufferPercentage = ((bufferedTime / (duration || 1)) * 100); - - // Add detailed buffer logging - logger.log(`[VideoPlayer] Buffer Status: - Current Time: ${currentTimeInSeconds.toFixed(2)}s - Buffered: ${bufferedTime.toFixed(2)}s - Buffered Ahead: ${bufferAhead.toFixed(2)}s - Buffer Percentage: ${bufferPercentage.toFixed(1)}% - `); - }; - // Handle VLC errors const handleError = (error: any) => { logger.error('[VideoPlayer] Playback Error:', error); @@ -663,7 +829,9 @@ const VideoPlayer: React.FC = () => { // Handle VLC buffering const onBuffering = (event: any) => { setIsBuffering(event.isBuffering); - logger.log(`[VideoPlayer] Buffering: ${event.isBuffering}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Buffering: ${event.isBuffering}`); + } }; // Handle VLC playback ended @@ -671,111 +839,14 @@ const VideoPlayer: React.FC = () => { // Your existing playback ended logic here }; - // Function to get audio tracks from VLC - const getAudioTracks = () => { - if (vlcRef.current) { - vlcRef.current.getAudioTracks().then((tracks: any) => { - setVlcAudioTracks(tracks || []); - logger.log("[VideoPlayer] Available VLC audio tracks:", tracks); - }).catch((error: any) => { - logger.error("[VideoPlayer] Failed to get audio tracks:", error); - }); - } - }; - // Function to select audio track in VLC const selectAudioTrack = (trackId: number) => { - if (vlcRef.current) { - vlcRef.current.setAudioTrack(trackId); - setSelectedAudioTrack(trackId); - } - }; - - // Function to get subtitle tracks from VLC - const getTextTracks = () => { - if (vlcRef.current) { - vlcRef.current.getTextTracks().then((tracks: any) => { - setVlcTextTracks(tracks || []); - logger.log("[VideoPlayer] Available VLC subtitle tracks:", tracks); - }).catch((error: any) => { - logger.error("[VideoPlayer] Failed to get subtitle tracks:", error); - }); - } + setSelectedAudioTrack(trackId); }; // Function to select subtitle track in VLC const selectTextTrack = (trackId: number) => { - if (vlcRef.current) { - vlcRef.current.setTextTrack(trackId); - // Update your state accordingly - setSelectedTextTrack({ type: 'index', value: trackId }); - } - }; - - // Add this useEffect to get audio and subtitle tracks after player is loaded - useEffect(() => { - if (duration > 0 && vlcRef.current) { - // Wait a bit for VLC to fully initialize and recognize tracks - setTimeout(() => { - getAudioTracks(); - getTextTracks(); - }, 2000); - } - }, [duration]); - - // Update audio modal to use VLC audio tracks - const renderAudioModal = () => { - if (!showAudioModal) return null; - - return ( - - - - Audio - setShowAudioModal(false)} - > - - - - - - - {vlcAudioTracks.length > 0 ? vlcAudioTracks.map(track => ( - { - selectAudioTrack(track.id); - setShowAudioModal(false); - }} - > - - - {formatLanguage(track.language) || track.name || `Track ${track.id}`} - - {(track.name && track.language) && ( - {track.name} - )} - - {selectedAudioTrack === track.id && ( - - - - )} - - )) : ( - - - No audio tracks available - - )} - - - - - ); + setSelectedTextTrack(trackId); }; // Update subtitle modal to use VLC subtitle tracks @@ -808,8 +879,7 @@ const VideoPlayer: React.FC = () => { Off - {(selectedTextTrack?.type === 'disabled' || - (selectedTextTrack?.type === 'index' && selectedTextTrack.value === -1)) && ( + {selectedTextTrack === -1 && ( @@ -828,14 +898,13 @@ const VideoPlayer: React.FC = () => { > - {formatLanguage(track.language) || track.name || `Subtitle ${track.id}`} + {getTrackDisplayName(track)} {(track.name && track.language) && ( {track.name} )} - {selectedTextTrack?.type === 'index' && - selectedTextTrack?.value === track.id && ( + {selectedTextTrack === track.id && ( @@ -881,7 +950,11 @@ const VideoPlayer: React.FC = () => { // VLC specific method to set playback speed const changePlaybackSpeed = (speed: number) => { if (vlcRef.current) { - vlcRef.current.setRate(speed); + if (typeof vlcRef.current.setRate === 'function') { + vlcRef.current.setRate(speed); + } else if (typeof vlcRef.current.setPlaybackRate === 'function') { + vlcRef.current.setPlaybackRate(speed); + } setPlaybackSpeed(speed); } }; @@ -890,237 +963,422 @@ const VideoPlayer: React.FC = () => { const setVolume = (volumeLevel: number) => { if (vlcRef.current) { // VLC volume is typically between 0-200 - vlcRef.current.setVolume(volumeLevel * 200); + if (typeof vlcRef.current.setVolume === 'function') { + vlcRef.current.setVolume(volumeLevel * 200); + } } }; + // Added back the togglePlayback function + const togglePlayback = () => { + if (vlcRef.current) { + if (paused) { + // Check if resume function exists + if (typeof vlcRef.current.resume === 'function') { + vlcRef.current.resume(); + } else if (typeof vlcRef.current.play === 'function') { + vlcRef.current.play(); + } else { + // Fallback - use setPaused method or property if available + vlcRef.current.setPaused && vlcRef.current.setPaused(false); + } + } else { + // Check if pause function exists + if (typeof vlcRef.current.pause === 'function') { + vlcRef.current.pause(); + } else { + // Fallback - use setPaused method or property if available + vlcRef.current.setPaused && vlcRef.current.setPaused(true); + } + } + setPaused(!paused); + } + }; + + // Re-add the renderAudioModal function + const renderAudioModal = () => { + if (!showAudioModal) return null; + + return ( + + + + Audio + setShowAudioModal(false)} + > + + + + + + + {vlcAudioTracks.length > 0 ? vlcAudioTracks.map(track => ( + { + selectAudioTrack(track.id); + setShowAudioModal(false); + }} + > + + + {getTrackDisplayName(track)} + + {(track.name && track.language) && ( + {track.name} + )} + + {selectedAudioTrack === track.id && ( + + + + )} + + )) : ( + + + No audio tracks available + + )} + + + + + ); + }; + + // Use a ref to track if we're mounted to prevent state updates after unmount + // This helps prevent potential memory leaks and strange behaviors with navigation + const isMounted = useRef(true); + + // Clean up when component unmounts + useEffect(() => { + return () => { + isMounted.current = false; + if (seekDebounceTimer.current) { + clearTimeout(seekDebounceTimer.current); + } + }; + }, []); + + // Wrap all setState calls with this check + const safeSetState = (setter: any) => { + if (isMounted.current) { + setter(); + } + }; + + // Enhanced progress bar touch handling with drag support + const handleProgressBarTouch = (event: any) => { + if (!duration || duration <= 0) return; + + const { locationX } = event.nativeEvent; + processProgressTouch(locationX); + }; + + const handleProgressBarDragStart = () => { + setIsDragging(true); + }; + + const handleProgressBarDragMove = (event: any) => { + if (!isDragging || !duration || duration <= 0) return; + + const { locationX } = event.nativeEvent; + processProgressTouch(locationX); + }; + + const handleProgressBarDragEnd = () => { + setIsDragging(false); + }; + + // Helper function to process touch position and seek + const processProgressTouch = (locationX: number) => { + progressBarRef.current?.measure((x, y, width, height, pageX, pageY) => { + // Calculate percentage of touch position relative to progress bar width + const percentage = Math.max(0, Math.min(locationX / width, 1)); + // Calculate time to seek to + const seekTime = percentage * duration; + + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Seeking to: ${seekTime}s (${percentage * 100}%)`); + } + + // Seek to the calculated time + seekToTime(seekTime); + }); + }; + return ( - - + + + Loading video... + + - {/* Slider Container with buffer indicator */} - - - {/* Buffered Progress */} - - - + + - - {formatTime(currentTime)} - {formatTime(duration)} - - - {/* Controls Overlay - Using Animated.View */} - - {/* Top Gradient & Header */} - - - {/* Title Section - Enhanced with metadata */} - - {title} - {/* Show season and episode for series */} - {season && episode && ( - - S{season}E{episode} {episodeTitle && `• ${episodeTitle}`} - - )} - {/* Show year, quality, and provider */} - - {year && {year}} - {quality && {quality}} - {streamProvider && via {streamProvider}} - - - - - - - - - {/* Center Controls (Play/Pause, Skip) */} - - skip(-10)} style={styles.skipButton}> - - 10 - - - - - skip(10)} style={styles.skipButton}> - - 10 - - - - {/* Bottom Gradient */} - - - {/* Bottom Buttons Row */} - - {/* Speed Button */} - - - Speed ({playbackSpeed}x) - - - {/* Aspect Ratio Button - Added */} - - - - Aspect ({resizeMode}) - - - - {/* Audio Button - Updated language display */} - setShowAudioModal(true)} - disabled={audioTracks.length <= 1} - > - - - {audioTracks.length > 0 && selectedAudioTrack !== null - ? `Audio: ${formatLanguage(audioTracks.find(t => t.index === selectedAudioTrack)?.language)}` - : 'Audio: Default'} - - - - {/* Subtitle Button - Updated language display */} - setShowSubtitleModal(true)} - disabled={textTracks.length === 0} - > - - - {selectedTextTrack?.type === 'disabled' - ? 'Subtitles: Off' - : `Subtitles: ${formatLanguage(textTracks.find(t => t.index === selectedTextTrack?.value)?.language)}`} - - - - - - - - {/* Resume Overlay */} - {showResumeOverlay && resumePosition !== null && ( - - + - - - - - - Continue Watching - - {title} - {season && episode && ` • S${season}E${episode}`} - - - - 0 ? (resumePosition / duration) * 100 : 0}%` } - ]} - /> - - - {formatTime(resumePosition)} {duration > 0 ? `/ ${formatTime(duration)}` : ''} - - - - - - {/* Remember choice checkbox */} - setRememberChoice(!rememberChoice)} - activeOpacity={0.7} + - - - {rememberChoice && } - - Remember my choice + + {/* Buffered Progress */} + + {/* Animated Progress */} + - - {resumePreference && ( - - Reset - - )} + + + {formatTime(currentTime)} + {formatTime(duration)} + + - - - - Start Over - - - - Resume + {/* Controls Overlay - Using Animated.View */} + + {/* Top Gradient & Header */} + + + {/* Title Section - Enhanced with metadata */} + + {title} + {/* Show season and episode for series */} + {season && episode && ( + + S{season}E{episode} {episodeTitle && `• ${episodeTitle}`} + + )} + {/* Show year, quality, and provider */} + + {year && {year}} + {quality && {quality}} + {streamProvider && via {streamProvider}} + + + + - - )} - + + {/* Center Controls (Play/Pause, Skip) */} + + skip(-10)} style={styles.skipButton}> + + 10 + + + + + skip(10)} style={styles.skipButton}> + + 10 + + + + {/* Bottom Gradient */} + + + {/* Bottom Buttons Row */} + + {/* Speed Button */} + + + Speed ({playbackSpeed}x) + + + {/* Aspect Ratio Button - Added */} + + + + Aspect ({resizeMode}) + + + + {/* Audio Button - Updated to use vlcAudioTracks */} + setShowAudioModal(true)} + disabled={vlcAudioTracks.length <= 1} + > + + + {`Audio: ${getTrackDisplayName(vlcAudioTracks.find(t => t.id === selectedAudioTrack) || {id: -1, name: 'Default'})}`} + + + + {/* Subtitle Button - Updated to use vlcTextTracks */} + setShowSubtitleModal(true)} + disabled={vlcTextTracks.length === 0} + > + + + {(selectedTextTrack === -1) + ? 'Subtitles' + : `Subtitles: ${getTrackDisplayName(vlcTextTracks.find(t => t.id === selectedTextTrack) || {id: -1, name: 'On'})}`} + + + + + + + + {/* Resume Overlay */} + {showResumeOverlay && resumePosition !== null && ( + + + + + + + + Continue Watching + + {title} + {season && episode && ` • S${season}E${episode}`} + + + + 0 ? (resumePosition / duration) * 100 : 0}%` } + ]} + /> + + + {formatTime(resumePosition)} {duration > 0 ? `/ ${formatTime(duration)}` : ''} + + + + + + {/* Remember choice checkbox */} + setRememberChoice(!rememberChoice)} + activeOpacity={0.7} + > + + + {rememberChoice && } + + Remember my choice + + + {resumePreference && ( + + Reset + + )} + + + + + + Start Over + + + + Resume + + + + + )} + + {/* Use the new modal rendering functions */} {renderAudioModal()} @@ -1242,16 +1500,18 @@ const styles = StyleSheet.create({ paddingHorizontal: 20, zIndex: 1000, }, - sliderBackground: { - position: 'absolute', - left: 0, - right: 0, - height: 3, + progressTouchArea: { + height: 30, // Increase touch target height for easier interaction + justifyContent: 'center', + width: '100%', + }, + progressBarContainer: { + height: 4, backgroundColor: 'rgba(255, 255, 255, 0.2)', - borderRadius: 1.5, + borderRadius: 2, overflow: 'hidden', - marginHorizontal: 20, - top: 13.5, // Center with the slider thumb + marginHorizontal: 4, + position: 'relative', }, bufferProgress: { position: 'absolute', @@ -1260,17 +1520,20 @@ const styles = StyleSheet.create({ bottom: 0, backgroundColor: 'rgba(255, 255, 255, 0.4)', }, - slider: { - width: '100%', - height: 30, - zIndex: 1, + progressBarFill: { + position: 'absolute', + left: 0, + top: 0, + bottom: 0, + backgroundColor: '#E50914', + height: '100%', }, timeDisplay: { flexDirection: 'row', justifyContent: 'space-between', width: '100%', paddingHorizontal: 4, - marginTop: -4, // Reduced space between slider and time + marginTop: 4, // Increased space between progress bar and time marginBottom: 8, // Added space between time and buttons }, duration: { @@ -1565,6 +1828,33 @@ const styles = StyleSheet.create({ fontSize: 12, fontWeight: 'bold', }, + openingOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0,0,0,0.85)', + justifyContent: 'center', + alignItems: 'center', + zIndex: 2000, + }, + openingContent: { + padding: 20, + backgroundColor: 'rgba(0,0,0,0.85)', + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + }, + openingText: { + color: 'white', + fontSize: 18, + fontWeight: 'bold', + marginTop: 20, + }, + videoPlayerContainer: { + flex: 1, + }, }); export default VideoPlayer; \ No newline at end of file