From d5f71ecb62f1374a42ad206e07e126e562c330da Mon Sep 17 00:00:00 2001 From: tapframe Date: Tue, 10 Jun 2025 02:43:48 +0530 Subject: [PATCH] Implement source selection feature in VideoPlayer and enhance stream handling This update introduces a new SourcesModal in the VideoPlayer component, allowing users to select from available streams. The VideoPlayer now manages stream changes more effectively, including handling pending seeks and updating the current stream URL. Additionally, the PlayerControls component has been updated to include a button for changing the source. Styling improvements have been made for better visual feedback during source changes, enhancing the overall user experience. --- src/components/player/VideoPlayer.tsx | 169 ++++++++++++- .../player/controls/PlayerControls.tsx | 17 ++ src/components/player/modals/SourcesModal.tsx | 160 ++++++++++++ src/components/player/utils/playerStyles.ts | 238 +++++++++++++++++- src/hooks/useSettings.ts | 2 - src/navigation/AppNavigator.tsx | 1 + src/screens/MetadataScreen.tsx | 129 +--------- src/screens/SettingsScreen.tsx | 11 - src/screens/StreamsScreen.tsx | 9 +- 9 files changed, 581 insertions(+), 155 deletions(-) create mode 100644 src/components/player/modals/SourcesModal.tsx diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx index 3ea80d77..1ad89c09 100644 --- a/src/components/player/VideoPlayer.tsx +++ b/src/components/player/VideoPlayer.tsx @@ -29,6 +29,7 @@ import AudioTrackModal from './modals/AudioTrackModal'; import ResumeOverlay from './modals/ResumeOverlay'; import PlayerControls from './controls/PlayerControls'; import CustomSubtitles from './subtitles/CustomSubtitles'; +import SourcesModal from './modals/SourcesModal'; const VideoPlayer: React.FC = () => { const navigation = useNavigation(); @@ -46,7 +47,8 @@ const VideoPlayer: React.FC = () => { id, type, episodeId, - imdbId + imdbId, + availableStreams: passedAvailableStreams } = route.params; safeDebugLog("Component mounted with props", { @@ -112,7 +114,15 @@ const VideoPlayer: React.FC = () => { const [availableSubtitles, setAvailableSubtitles] = useState([]); const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState(false); const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState(false); - const isMounted = useRef(true); + const [showSourcesModal, setShowSourcesModal] = useState(false); + const [availableStreams, setAvailableStreams] = useState<{ [providerId: string]: { streams: any[]; addonName: string } }>(passedAvailableStreams || {}); + const [currentStreamUrl, setCurrentStreamUrl] = useState(uri); + const [isChangingSource, setIsChangingSource] = useState(false); + const [pendingSeek, setPendingSeek] = useState<{ position: number; shouldPlay: boolean } | null>(null); + const [currentQuality, setCurrentQuality] = useState(quality); + const [currentStreamProvider, setCurrentStreamProvider] = useState(streamProvider); + const [currentStreamName, setCurrentStreamName] = useState(undefined); + const isMounted = useRef(true); const calculateVideoStyles = (videoWidth: number, videoHeight: number, screenWidth: number, screenHeight: number) => { return { @@ -728,6 +738,123 @@ const VideoPlayer: React.FC = () => { saveSubtitleSize(newSize); }; + useEffect(() => { + 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`); + + if (pendingSeek.position > 0 && vlcRef.current) { + // Wait longer for the player to be fully ready and stable + setTimeout(() => { + if (vlcRef.current && duration > 0 && pendingSeek) { + logger.log(`[VideoPlayer] Executing seek to ${pendingSeek.position}s`); + + // Use our existing seekToTime function which handles VLC methods properly + seekToTime(pendingSeek.position); + + // Also update the current time state to reflect the seek + setCurrentTime(pendingSeek.position); + + // Resume playback if it was playing before the source change + if (pendingSeek.shouldPlay) { + setTimeout(() => { + logger.log('[VideoPlayer] Resuming playback after seek'); + setPaused(false); + if (vlcRef.current && typeof vlcRef.current.play === 'function') { + vlcRef.current.play(); + } + }, 700); // Wait longer for seek to complete properly + } + + // Clean up after a reasonable delay + setTimeout(() => { + setPendingSeek(null); + setIsChangingSource(false); + }, 800); + } + }, 1500); // Increased delay to ensure player is fully stable + } else { + // No seeking needed, just resume playback if it was playing + if (pendingSeek.shouldPlay) { + setTimeout(() => { + logger.log('[VideoPlayer] No seek needed, just resuming playback'); + setPaused(false); + if (vlcRef.current && typeof vlcRef.current.play === 'function') { + vlcRef.current.play(); + } + }, 500); + } + + setTimeout(() => { + setPendingSeek(null); + setIsChangingSource(false); + }, 600); + } + } + }, [pendingSeek, isPlayerReady, isVideoLoaded, duration]); + + const handleSelectStream = async (newStream: any) => { + if (newStream.url === currentStreamUrl) { + setShowSourcesModal(false); + return; + } + + setIsChangingSource(true); + setShowSourcesModal(false); + + try { + // Save current state + const savedPosition = currentTime; + const wasPlaying = !paused; + + logger.log(`[VideoPlayer] Changing source from ${currentStreamUrl} to ${newStream.url}`); + logger.log(`[VideoPlayer] Saved position: ${savedPosition}, was playing: ${wasPlaying}`); + + // Extract quality and provider information from the new stream + let newQuality = newStream.quality; + if (!newQuality && newStream.title) { + // Try to extract quality from title (e.g., "1080p", "720p") + const qualityMatch = newStream.title.match(/(\d+)p/); + newQuality = qualityMatch ? qualityMatch[0] : undefined; // Use [0] to get full match like "1080p" + } + + // For provider, try multiple fields + const newProvider = newStream.addonName || newStream.name || newStream.addon || 'Unknown'; + + // For stream name, prioritize the stream name over title + const newStreamName = newStream.name || newStream.title || 'Unknown Stream'; + + logger.log(`[VideoPlayer] Stream object:`, newStream); + 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}`); + + // Stop current playback + if (vlcRef.current) { + vlcRef.current.pause && vlcRef.current.pause(); + } + setPaused(true); + + // Set pending seek state + setPendingSeek({ position: savedPosition, shouldPlay: wasPlaying }); + + // Update the stream URL and details immediately + setCurrentStreamUrl(newStream.url); + setCurrentQuality(newQuality); + setCurrentStreamProvider(newProvider); + setCurrentStreamName(newStreamName); + + // Reset player state for new source + setCurrentTime(0); + setDuration(0); + setIsPlayerReady(false); + setIsVideoLoaded(false); + + } catch (error) { + logger.error('[VideoPlayer] Error changing source:', error); + setPendingSeek(null); + setIsChangingSource(false); + } + }; + return ( { + {/* Source Change Loading Overlay */} + {isChangingSource && ( + + + + Changing source... + Please wait while we load the new stream + + + )} + { ], }} source={{ - uri: uri, + uri: currentStreamUrl, initOptions: [ '--rtsp-tcp', '--network-caching=150', @@ -849,21 +997,23 @@ const VideoPlayer: React.FC = () => { episodeTitle={episodeTitle} season={season} episode={episode} - quality={quality} + quality={currentQuality || quality} year={year} - streamProvider={streamProvider} + streamProvider={currentStreamProvider || streamProvider} currentTime={currentTime} duration={duration} playbackSpeed={playbackSpeed} zoomScale={zoomScale} vlcAudioTracks={vlcAudioTracks} selectedAudioTrack={selectedAudioTrack} + availableStreams={availableStreams} togglePlayback={togglePlayback} skip={skip} handleClose={handleClose} cycleAspectRatio={cycleAspectRatio} setShowAudioModal={setShowAudioModal} setShowSubtitleModal={setShowSubtitleModal} + setShowSourcesModal={setShowSourcesModal} progressBarRef={progressBarRef} progressAnim={progressAnim} handleProgressBarTouch={handleProgressBarTouch} @@ -923,6 +1073,15 @@ const VideoPlayer: React.FC = () => { increaseSubtitleSize={increaseSubtitleSize} decreaseSubtitleSize={decreaseSubtitleSize} /> + + ); }; diff --git a/src/components/player/controls/PlayerControls.tsx b/src/components/player/controls/PlayerControls.tsx index cc73fb52..47fe0227 100644 --- a/src/components/player/controls/PlayerControls.tsx +++ b/src/components/player/controls/PlayerControls.tsx @@ -22,12 +22,14 @@ interface PlayerControlsProps { zoomScale: number; vlcAudioTracks: Array<{id: number, name: string, language?: string}>; selectedAudioTrack: number | null; + availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } }; togglePlayback: () => void; skip: (seconds: number) => void; handleClose: () => void; cycleAspectRatio: () => void; setShowAudioModal: (show: boolean) => void; setShowSubtitleModal: (show: boolean) => void; + setShowSourcesModal?: (show: boolean) => void; progressBarRef: React.RefObject; progressAnim: Animated.Value; handleProgressBarTouch: (event: any) => void; @@ -55,12 +57,14 @@ export const PlayerControls: React.FC = ({ zoomScale, vlcAudioTracks, selectedAudioTrack, + availableStreams, togglePlayback, skip, handleClose, cycleAspectRatio, setShowAudioModal, setShowSubtitleModal, + setShowSourcesModal, progressBarRef, progressAnim, handleProgressBarTouch, @@ -206,6 +210,19 @@ export const PlayerControls: React.FC = ({ Subtitles + + {/* Change Source Button */} + {setShowSourcesModal && ( + setShowSourcesModal(true)} + > + + + Change Source + + + )} diff --git a/src/components/player/modals/SourcesModal.tsx b/src/components/player/modals/SourcesModal.tsx new file mode 100644 index 00000000..ad5bc996 --- /dev/null +++ b/src/components/player/modals/SourcesModal.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator } from 'react-native'; +import { Ionicons, MaterialIcons } from '@expo/vector-icons'; +import { styles } from '../utils/playerStyles'; +import { Stream } from '../../../types/streams'; +import QualityBadge from '../../metadata/QualityBadge'; + +interface SourcesModalProps { + showSourcesModal: boolean; + setShowSourcesModal: (show: boolean) => void; + availableStreams: { [providerId: string]: { streams: Stream[]; addonName: string } }; + currentStreamUrl: string; + onSelectStream: (stream: Stream) => void; + isChangingSource: boolean; +} + +const SourcesModal: React.FC = ({ + showSourcesModal, + setShowSourcesModal, + availableStreams, + currentStreamUrl, + onSelectStream, + isChangingSource, +}) => { + if (!showSourcesModal) return null; + + const sortedProviders = Object.entries(availableStreams).sort(([a], [b]) => { + // Put HDRezka first + if (a === 'hdrezka') return -1; + if (b === 'hdrezka') return 1; + return 0; + }); + + const handleStreamSelect = (stream: Stream) => { + if (stream.url !== currentStreamUrl && !isChangingSource) { + onSelectStream(stream); + } + }; + + const getQualityFromTitle = (title?: string): string | null => { + if (!title) return null; + const match = title.match(/(\d+)p/); + return match ? match[1] : null; + }; + + const isStreamSelected = (stream: Stream): boolean => { + return stream.url === currentStreamUrl; + }; + + return ( + + + + Choose Source + setShowSourcesModal(false)} + > + + + + + + {sortedProviders.map(([providerId, { streams, addonName }]) => ( + + {addonName} + + {streams.map((stream, index) => { + const quality = getQualityFromTitle(stream.title); + const isSelected = isStreamSelected(stream); + const isHDR = stream.title?.toLowerCase().includes('hdr'); + const isDolby = stream.title?.toLowerCase().includes('dolby') || stream.title?.includes('DV'); + const size = stream.title?.match(/💾\s*([\d.]+\s*[GM]B)/)?.[1]; + const isDebrid = stream.behaviorHints?.cached; + const isHDRezka = providerId === 'hdrezka'; + + return ( + handleStreamSelect(stream)} + disabled={isChangingSource || isSelected} + activeOpacity={0.7} + > + + + + {isHDRezka ? `HDRezka ${stream.title}` : (stream.name || stream.title || 'Unnamed Stream')} + + + {isSelected && ( + + + Current + + )} + + {isChangingSource && isSelected && ( + + )} + + + {!isHDRezka && stream.title && stream.title !== stream.name && ( + {stream.title} + )} + + + {quality && quality >= "720" && ( + + )} + + {isDolby && ( + + )} + + {size && ( + + {size} + + )} + + {isDebrid && ( + + DEBRID + + )} + + {isHDRezka && ( + + HDREZKA + + )} + + + + + {isSelected ? ( + + ) : ( + + )} + + + ); + })} + + ))} + + + + ); +}; + +export default SourcesModal; \ No newline at end of file diff --git a/src/components/player/utils/playerStyles.ts b/src/components/player/utils/playerStyles.ts index e2834e2e..84cea742 100644 --- a/src/components/player/utils/playerStyles.ts +++ b/src/components/player/utils/playerStyles.ts @@ -76,15 +76,16 @@ export const styles = StyleSheet.create({ marginRight: 8, }, qualityBadge: { - backgroundColor: '#E50914', - paddingHorizontal: 6, + backgroundColor: 'rgba(229, 9, 20, 0.2)', + paddingHorizontal: 8, paddingVertical: 2, borderRadius: 4, marginRight: 8, + marginBottom: 4, }, qualityText: { - color: 'white', - fontSize: 10, + color: '#E50914', + fontSize: 11, fontWeight: 'bold', }, providerText: { @@ -189,9 +190,10 @@ export const styles = StyleSheet.create({ }, modalOverlay: { flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.9)', justifyContent: 'center', alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.7)', + padding: 20, }, modalContent: { width: '80%', @@ -207,16 +209,15 @@ export const styles = StyleSheet.create({ shadowRadius: 5, }, modalHeader: { - padding: 16, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - borderBottomWidth: 1, - borderBottomColor: '#333', + marginBottom: 20, + paddingHorizontal: 4, }, modalTitle: { color: 'white', - fontSize: 18, + fontSize: 20, fontWeight: 'bold', }, trackList: { @@ -764,4 +765,223 @@ export const styles = StyleSheet.create({ alignItems: 'center', zIndex: 9999, }, + // Sources Modal Styles + sourcesModal: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.9)', + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + sourcesContainer: { + backgroundColor: 'rgba(20, 20, 20, 0.98)', + borderRadius: 12, + width: '100%', + maxWidth: 500, + maxHeight: '80%', + paddingVertical: 20, + paddingHorizontal: 16, + }, + sourcesHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 20, + paddingHorizontal: 4, + }, + sourcesTitle: { + color: 'white', + fontSize: 20, + fontWeight: 'bold', + }, + modalCloseButton: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: 'rgba(255, 255, 255, 0.1)', + justifyContent: 'center', + alignItems: 'center', + }, + sourcesScrollView: { + maxHeight: 400, + }, + sourceItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 16, + paddingHorizontal: 12, + borderRadius: 8, + backgroundColor: 'rgba(255, 255, 255, 0.05)', + marginBottom: 8, + }, + currentSourceItem: { + backgroundColor: 'rgba(229, 9, 20, 0.2)', + borderWidth: 1, + borderColor: 'rgba(229, 9, 20, 0.5)', + }, + sourceInfo: { + flex: 1, + marginLeft: 12, + }, + sourceTitle: { + color: 'white', + fontSize: 16, + fontWeight: '600', + marginBottom: 4, + }, + sourceDetails: { + flexDirection: 'row', + alignItems: 'center', + flexWrap: 'wrap', + }, + sourceDetailText: { + color: '#888', + fontSize: 12, + marginRight: 8, + marginBottom: 4, + }, + currentStreamBadge: { + backgroundColor: 'rgba(0, 255, 0, 0.2)', + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 4, + marginRight: 8, + marginBottom: 4, + }, + currentStreamText: { + color: '#00FF00', + fontSize: 11, + fontWeight: 'bold', + }, + switchingSourceOverlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0, 0, 0, 0.8)', + justifyContent: 'center', + alignItems: 'center', + zIndex: 9999, + }, + switchingContent: { + alignItems: 'center', + backgroundColor: 'rgba(20, 20, 20, 0.9)', + padding: 30, + borderRadius: 12, + minWidth: 200, + }, + switchingText: { + color: 'white', + fontSize: 16, + fontWeight: '600', + marginTop: 12, + textAlign: 'center', + }, + // Additional SourcesModal styles + sourceProviderSection: { + marginBottom: 20, + }, + sourceProviderTitle: { + color: 'rgba(255, 255, 255, 0.8)', + fontSize: 14, + fontWeight: '600', + marginBottom: 12, + paddingHorizontal: 4, + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + sourceStreamItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + paddingHorizontal: 12, + borderRadius: 8, + backgroundColor: 'rgba(255, 255, 255, 0.05)', + marginBottom: 8, + }, + sourceStreamItemSelected: { + backgroundColor: 'rgba(229, 9, 20, 0.2)', + borderWidth: 1, + borderColor: 'rgba(229, 9, 20, 0.5)', + }, + sourceStreamDetails: { + flex: 1, + }, + sourceStreamTitleRow: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 4, + }, + sourceStreamTitle: { + color: 'white', + fontSize: 16, + fontWeight: '600', + flex: 1, + }, + sourceStreamTitleSelected: { + color: '#E50914', + }, + sourceStreamSubtitle: { + color: 'rgba(255, 255, 255, 0.7)', + fontSize: 14, + marginBottom: 6, + }, + sourceStreamMeta: { + flexDirection: 'row', + alignItems: 'center', + flexWrap: 'wrap', + }, + sourceChip: { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 4, + marginRight: 6, + marginBottom: 4, + }, + sourceChipText: { + color: 'rgba(255, 255, 255, 0.8)', + fontSize: 11, + fontWeight: 'bold', + }, + debridChip: { + backgroundColor: 'rgba(0, 255, 0, 0.2)', + }, + hdrezkaChip: { + backgroundColor: 'rgba(255, 165, 0, 0.2)', + }, + sourceStreamAction: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: 'rgba(255, 255, 255, 0.1)', + justifyContent: 'center', + alignItems: 'center', + }, + // Source Change Loading Overlay + sourceChangeOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.9)', + justifyContent: 'center', + alignItems: 'center', + zIndex: 5000, + }, + sourceChangeContent: { + alignItems: 'center', + padding: 30, + }, + sourceChangeText: { + color: '#E50914', + fontSize: 18, + fontWeight: 'bold', + marginTop: 15, + textAlign: 'center', + }, + sourceChangeSubtext: { + color: 'rgba(255, 255, 255, 0.8)', + fontSize: 14, + marginTop: 8, + textAlign: 'center', + }, }); \ No newline at end of file diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index e25b1d6c..aa63dce1 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -35,7 +35,6 @@ export interface AppSettings { logoSourcePreference: 'metahub' | 'tmdb'; // Preferred source for title logos tmdbLanguagePreference: string; // Preferred language for TMDB logos (ISO 639-1 code) enableInternalProviders: boolean; // Toggle for internal providers like HDRezka - autoPlayFirstStream: boolean; // Auto-play first stream without showing streams selection } export const DEFAULT_SETTINGS: AppSettings = { @@ -53,7 +52,6 @@ export const DEFAULT_SETTINGS: AppSettings = { logoSourcePreference: 'metahub', // Default to Metahub as first source tmdbLanguagePreference: 'en', // Default to English enableInternalProviders: true, // Enable internal providers by default - autoPlayFirstStream: false, // Default to false to maintain existing behavior }; const SETTINGS_STORAGE_KEY = 'app_settings'; diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index c5498d0d..dab0094f 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -78,6 +78,7 @@ export type RootStackParamList = { type?: string; episodeId?: string; imdbId?: string; + availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } }; }; Catalog: { id: string; type: string; addonId?: string; name?: string; genreFilter?: string }; Credits: { mediaId: string; mediaType: string }; diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index e5d9252b..29a690ba 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -12,7 +12,6 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' import { useRoute, useNavigation } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import * as Haptics from 'expo-haptics'; -import * as ScreenOrientation from 'expo-screen-orientation'; import { useTheme } from '../contexts/ThemeContext'; import { useMetadata } from '../hooks/useMetadata'; import { CastSection } from '../components/metadata/CastSection'; @@ -21,7 +20,6 @@ import { MovieContent } from '../components/metadata/MovieContent'; import { MoreLikeThisSection } from '../components/metadata/MoreLikeThisSection'; import { RatingsSection } from '../components/metadata/RatingsSection'; import { RouteParams, Episode } from '../types/metadata'; -import { Stream, GroupedStreams } from '../types/streams'; import Animated, { useAnimatedStyle, interpolate, @@ -77,10 +75,6 @@ const MetadataScreen: React.FC = () => { loadingRecommendations, setMetadata, imdbId, - loadStreams, - loadEpisodeStreams, - groupedStreams, - episodeStreams, } = useMetadata({ id, type }); // Optimized hooks with memoization @@ -113,127 +107,8 @@ const MetadataScreen: React.FC = () => { handleSeasonChange(seasonNumber); }, [handleSeasonChange]); - // Helper function to get the first available stream from grouped streams - const getFirstAvailableStream = useCallback((streams: GroupedStreams): Stream | null => { - const providers = Object.values(streams); - for (const provider of providers) { - if (provider.streams && provider.streams.length > 0) { - // Try to find a cached stream first - const cachedStream = provider.streams.find(stream => - stream.behaviorHints?.cached === true - ); - if (cachedStream) { - return cachedStream; - } - - // Otherwise return the first stream - return provider.streams[0]; - } - } - return null; - }, []); - - const handleShowStreams = useCallback(async () => { + const handleShowStreams = useCallback(() => { const { watchProgress } = watchProgressData; - - // Check if auto-play is enabled - if (settings.autoPlayFirstStream) { - try { - console.log('Auto-play enabled, attempting to load streams...'); - - // Determine the target episode for series - let targetEpisodeId: string | undefined; - if (type === 'series') { - targetEpisodeId = watchProgress?.episodeId || episodeId || (episodes.length > 0 ? - (episodes[0].stremioId || `${id}:${episodes[0].season_number}:${episodes[0].episode_number}`) : undefined); - } - - // Load streams without locking orientation yet - let streamsLoaded = false; - if (type === 'series' && targetEpisodeId) { - console.log('Loading episode streams for:', targetEpisodeId); - await loadEpisodeStreams(targetEpisodeId); - streamsLoaded = true; - } else if (type === 'movie') { - console.log('Loading movie streams...'); - await loadStreams(); - streamsLoaded = true; - } - - if (streamsLoaded) { - // Wait a bit longer for streams to be processed and state to update - console.log('Waiting for streams to be processed...'); - await new Promise(resolve => setTimeout(resolve, 3000)); - - // Check if we have any streams available - const availableStreams = type === 'series' ? episodeStreams : groupedStreams; - console.log('Available streams:', Object.keys(availableStreams)); - - const firstStream = getFirstAvailableStream(availableStreams); - - if (firstStream) { - console.log('Found stream, navigating to player:', firstStream); - - // Now lock orientation to landscape before navigation - await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); - await new Promise(resolve => setTimeout(resolve, 200)); - - if (type === 'series' && targetEpisodeId) { - // Get episode details for navigation - const targetEpisode = episodes.find(ep => - ep.stremioId === targetEpisodeId || - `${id}:${ep.season_number}:${ep.episode_number}` === targetEpisodeId - ); - - // Navigate directly to player with the first stream - navigation.navigate('Player', { - uri: firstStream.url, - title: metadata?.name || 'Unknown', - season: targetEpisode?.season_number, - episode: targetEpisode?.episode_number, - episodeTitle: targetEpisode?.name, - quality: firstStream.title?.match(/(\d+)p/)?.[1] || 'Unknown', - year: metadata?.year, - streamProvider: firstStream.name || 'Unknown', - id, - type, - episodeId: targetEpisodeId, - imdbId: imdbId || id, - }); - return; - } else if (type === 'movie') { - // Navigate directly to player with the first stream - navigation.navigate('Player', { - uri: firstStream.url, - title: metadata?.name || 'Unknown', - quality: firstStream.title?.match(/(\d+)p/)?.[1] || 'Unknown', - year: metadata?.year, - streamProvider: firstStream.name || 'Unknown', - id, - type, - imdbId: imdbId || id, - }); - return; - } - } else { - console.log('No streams found after waiting, disabling auto-play for this session'); - // Don't fall back to streams screen, just show an alert - alert('No streams available for auto-play. Please try selecting streams manually.'); - return; - } - } - - console.log('Auto-play failed, falling back to manual selection'); - } catch (error) { - console.error('Auto-play failed with error:', error); - // Don't fall back on error, just show alert - alert('Auto-play failed. Please try selecting streams manually.'); - return; - } - } - - // Normal behavior: navigate to streams screen (only if auto-play is disabled or not attempted) - console.log('Navigating to streams screen (normal flow)'); if (type === 'series') { const targetEpisodeId = watchProgress?.episodeId || episodeId || (episodes.length > 0 ? (episodes[0].stremioId || `${id}:${episodes[0].season_number}:${episodes[0].episode_number}`) : undefined); @@ -244,7 +119,7 @@ const MetadataScreen: React.FC = () => { } } navigation.navigate('Streams', { id, type, episodeId }); - }, [settings.autoPlayFirstStream, navigation, id, type, episodes, episodeId, watchProgressData, metadata, loadEpisodeStreams, loadStreams, episodeStreams, groupedStreams, imdbId, getFirstAvailableStream]); + }, [navigation, id, type, episodes, episodeId, watchProgressData.watchProgress]); const handleEpisodeSelect = useCallback((episode: Episode) => { const episodeId = episode.stremioId || `${id}:${episode.season_number}:${episode.episode_number}`; diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index d4bd7c96..9dbe1026 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -462,17 +462,6 @@ const SettingsScreen: React.FC = () => { icon="play-arrow" renderControl={ChevronRight} onPress={() => navigation.navigate('PlayerSettings')} - /> - ( - updateSetting('autoPlayFirstStream', value)} - /> - )} isLast={true} /> diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 5ed54079..ea726d4f 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -519,6 +519,9 @@ export const StreamsScreen = () => { // Small delay to ensure orientation is set before navigation await new Promise(resolve => setTimeout(resolve, 100)); + // Prepare available streams for the change source feature + const streamsToPass = type === 'series' ? episodeStreams : groupedStreams; + navigation.navigate('Player', { uri: stream.url, title: metadata?.name || '', @@ -532,10 +535,13 @@ export const StreamsScreen = () => { type, episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined, imdbId: imdbId || undefined, + availableStreams: streamsToPass, }); } catch (error) { logger.error('[StreamsScreen] Error locking orientation before navigation:', error); // Fallback: navigate anyway + const streamsToPass = type === 'series' ? episodeStreams : groupedStreams; + navigation.navigate('Player', { uri: stream.url, title: metadata?.name || '', @@ -549,9 +555,10 @@ export const StreamsScreen = () => { type, episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined, imdbId: imdbId || undefined, + availableStreams: streamsToPass, }); } - }, [metadata, type, currentEpisode, navigation, id, selectedEpisode, imdbId]); + }, [metadata, type, currentEpisode, navigation, id, selectedEpisode, imdbId, episodeStreams, groupedStreams]); // Update handleStreamPress const handleStreamPress = useCallback(async (stream: Stream) => {