From fa03d4455f492d028a036194b68c45113a745d10 Mon Sep 17 00:00:00 2001 From: tapframe Date: Fri, 12 Sep 2025 22:48:26 +0530 Subject: [PATCH] fixed audio selection issue --- KSPlayer | 1 + local-scrapers-repo | 2 +- src/components/player/AndroidVideoPlayer.tsx | 82 ++++- src/components/player/VideoPlayer.tsx | 79 ++++- .../player/modals/AudioTrackModal.tsx | 28 +- src/screens/StreamsScreen.tsx | 295 ++++++++++-------- 6 files changed, 344 insertions(+), 143 deletions(-) create mode 160000 KSPlayer diff --git a/KSPlayer b/KSPlayer new file mode 160000 index 00000000..8fe5feb7 --- /dev/null +++ b/KSPlayer @@ -0,0 +1 @@ +Subproject commit 8fe5feb73ca3ee5092d2ed1dd8fcb692c10e3c11 diff --git a/local-scrapers-repo b/local-scrapers-repo index 5c020cca..0a273ba5 160000 --- a/local-scrapers-repo +++ b/local-scrapers-repo @@ -1 +1 @@ -Subproject commit 5c020cca433f0400e23eb553f3e4de09f65b66d3 +Subproject commit 0a273ba5eb2632979a6678e005314c5b3ffb70eb diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 0dc2da64..2176f0ff 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -757,12 +757,35 @@ const AndroidVideoPlayer: React.FC = () => { // Handle audio tracks if (data.audioTracks && data.audioTracks.length > 0) { - const formattedAudioTracks = data.audioTracks.map((track: any, index: number) => ({ - id: track.index || index, - name: track.title || track.language || `Audio ${index + 1}`, - language: track.language, - })); + const formattedAudioTracks = data.audioTracks.map((track: any, index: number) => { + const trackIndex = track.index !== undefined ? track.index : index; + const trackName = track.title || track.language || `Audio ${index + 1}`; + const trackLanguage = track.language || 'Unknown'; + + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Audio track ${index}: index=${trackIndex}, name="${trackName}", language="${trackLanguage}"`); + } + + return { + id: trackIndex, // Use the actual track index from react-native-video + name: trackName, + language: trackLanguage, + }; + }); setRnVideoAudioTracks(formattedAudioTracks); + + // Auto-select the first audio track if none is selected + if (selectedAudioTrack === null && formattedAudioTracks.length > 0) { + const firstTrack = formattedAudioTracks[0]; + setSelectedAudioTrack(firstTrack.id); + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Auto-selected first audio track: ${firstTrack.name} (ID: ${firstTrack.id})`); + } + } + + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Formatted audio tracks:`, formattedAudioTracks); + } } // Handle text tracks @@ -1205,7 +1228,42 @@ const AndroidVideoPlayer: React.FC = () => { }; const selectAudioTrack = (trackId: number) => { + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Selecting audio track: ${trackId}`); + logger.log(`[AndroidVideoPlayer] Available tracks:`, rnVideoAudioTracks); + } + + // Validate that the track exists + const trackExists = rnVideoAudioTracks.some(track => track.id === trackId); + if (!trackExists) { + logger.error(`[AndroidVideoPlayer] Audio track ${trackId} not found in available tracks`); + return; + } + + // If changing tracks, briefly pause to allow smooth transition + const wasPlaying = !paused; + if (wasPlaying) { + setPaused(true); + } + + // Set the new audio track setSelectedAudioTrack(trackId); + + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Audio track changed to: ${trackId}`); + } + + // Resume playback after a brief delay if it was playing + if (wasPlaying) { + setTimeout(() => { + if (isMounted.current) { + setPaused(false); + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Resumed playback after audio track change`); + } + } + }, 300); + } }; const selectTextTrack = (trackId: number) => { @@ -1737,6 +1795,20 @@ const AndroidVideoPlayer: React.FC = () => { loadSubtitleSize(); }, []); + // Handle audio track changes with proper logging + useEffect(() => { + if (selectedAudioTrack !== null && rnVideoAudioTracks.length > 0) { + const selectedTrack = rnVideoAudioTracks.find(track => track.id === selectedAudioTrack); + if (selectedTrack) { + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Audio track selected: ${selectedTrack.name} (${selectedTrack.language}) - ID: ${selectedAudioTrack}`); + } + } else { + logger.warn(`[AndroidVideoPlayer] Selected audio track ${selectedAudioTrack} not found in available tracks`); + } + } + }, [selectedAudioTrack, rnVideoAudioTracks]); + // Load global subtitle settings useEffect(() => { (async () => { diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx index 1af3bd50..80413cc9 100644 --- a/src/components/player/VideoPlayer.tsx +++ b/src/components/player/VideoPlayer.tsx @@ -751,7 +751,35 @@ const VideoPlayer: React.FC = () => { } if (data.audioTracks && data.audioTracks.length > 0) { - setVlcAudioTracks(data.audioTracks); + const formattedAudioTracks = data.audioTracks.map((track: any, index: number) => { + const trackIndex = track.index !== undefined ? track.index : index; + const trackName = track.title || track.language || `Audio ${index + 1}`; + const trackLanguage = track.language || 'Unknown'; + + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Audio track ${index}: index=${trackIndex}, name="${trackName}", language="${trackLanguage}"`); + } + + return { + id: trackIndex, // Use the actual track index from VLC + name: trackName, + language: trackLanguage, + }; + }); + setVlcAudioTracks(formattedAudioTracks); + + // Auto-select the first audio track if none is selected + if (selectedAudioTrack === null && formattedAudioTracks.length > 0) { + const firstTrack = formattedAudioTracks[0]; + setSelectedAudioTrack(firstTrack.id); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Auto-selected first audio track: ${firstTrack.name} (ID: ${firstTrack.id})`); + } + } + + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Formatted audio tracks:`, formattedAudioTracks); + } } if (data.textTracks && data.textTracks.length > 0) { setVlcTextTracks(data.textTracks); @@ -1024,7 +1052,42 @@ const VideoPlayer: React.FC = () => { }; const selectAudioTrack = (trackId: number) => { + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Selecting audio track: ${trackId}`); + logger.log(`[VideoPlayer] Available tracks:`, vlcAudioTracks); + } + + // Validate that the track exists + const trackExists = vlcAudioTracks.some(track => track.id === trackId); + if (!trackExists) { + logger.error(`[VideoPlayer] Audio track ${trackId} not found in available tracks`); + return; + } + + // If changing tracks, briefly pause to allow smooth transition + const wasPlaying = !paused; + if (wasPlaying) { + setPaused(true); + } + + // Set the new audio track setSelectedAudioTrack(trackId); + + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Audio track changed to: ${trackId}`); + } + + // Resume playback after a brief delay if it was playing + if (wasPlaying) { + setTimeout(() => { + if (isMounted.current) { + setPaused(false); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Resumed playback after audio track change`); + } + } + }, 300); + } }; const selectTextTrack = (trackId: number) => { @@ -1578,6 +1641,20 @@ const VideoPlayer: React.FC = () => { loadSubtitleSize(); }, []); + // Handle audio track changes with proper logging + useEffect(() => { + if (selectedAudioTrack !== null && vlcAudioTracks.length > 0) { + const selectedTrack = vlcAudioTracks.find(track => track.id === selectedAudioTrack); + if (selectedTrack) { + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Audio track selected: ${selectedTrack.name} (${selectedTrack.language}) - ID: ${selectedAudioTrack}`); + } + } else { + logger.warn(`[VideoPlayer] Selected audio track ${selectedAudioTrack} not found in available tracks`); + } + } + }, [selectedAudioTrack, vlcAudioTracks]); + const increaseSubtitleSize = () => { const newSize = Math.min(subtitleSize + 2, 32); saveSubtitleSize(newSize); diff --git a/src/components/player/modals/AudioTrackModal.tsx b/src/components/player/modals/AudioTrackModal.tsx index 4a382813..52024acd 100644 --- a/src/components/player/modals/AudioTrackModal.tsx +++ b/src/components/player/modals/AudioTrackModal.tsx @@ -7,7 +7,8 @@ import Animated, { SlideInRight, SlideOutRight, } from 'react-native-reanimated'; -import { getTrackDisplayName } from '../utils/playerUtils'; +import { getTrackDisplayName, DEBUG_MODE } from '../utils/playerUtils'; +import { logger } from '../../../utils/logger'; interface AudioTrackModalProps { showAudioModal: boolean; @@ -31,6 +32,20 @@ export const AudioTrackModal: React.FC = ({ setShowAudioModal(false); }; + // Debug logging when modal opens + React.useEffect(() => { + if (showAudioModal && DEBUG_MODE) { + logger.log(`[AudioTrackModal] Modal opened with selectedAudioTrack: ${selectedAudioTrack}`); + logger.log(`[AudioTrackModal] Available tracks:`, vlcAudioTracks); + const selectedTrack = vlcAudioTracks.find(track => track.id === selectedAudioTrack); + if (selectedTrack) { + logger.log(`[AudioTrackModal] Selected track found: ${selectedTrack.name} (${selectedTrack.language})`); + } else { + logger.warn(`[AudioTrackModal] Selected track ${selectedAudioTrack} not found in available tracks`); + } + } + }, [showAudioModal, selectedAudioTrack, vlcAudioTracks]); + if (!showAudioModal) return null; return ( @@ -131,7 +146,9 @@ export const AudioTrackModal: React.FC = ({ {vlcAudioTracks.map((track) => { - const isSelected = selectedAudioTrack === track.id; + // If no track is selected, show the first track as selected + const isSelected = selectedAudioTrack === track.id || + (selectedAudioTrack === null && track.id === vlcAudioTracks[0]?.id); return ( = ({ borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)', }} onPress={() => { + if (DEBUG_MODE) { + logger.log(`[AudioTrackModal] Selecting track: ${track.id} (${track.name})`); + } selectAudioTrack(track.id); + // Close modal after selection + setTimeout(() => { + setShowAudioModal(false); + }, 200); }} activeOpacity={0.7} > diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index bea090d2..3390a43d 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -17,6 +17,13 @@ import { Clipboard, Image as RNImage, } from 'react-native'; +import Animated, { + useSharedValue, + useAnimatedStyle, + withTiming, + withDelay, + runOnJS +} from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import * as ScreenOrientation from 'expo-screen-orientation'; @@ -38,22 +45,6 @@ import { localScraperService } from '../services/localScraperService'; import { VideoPlayerService } from '../services/videoPlayerService'; import { useSettings } from '../hooks/useSettings'; import QualityBadge from '../components/metadata/QualityBadge'; -import Animated, { - FadeIn, - FadeOut, - FadeInDown, - SlideInDown, - withSpring, - withTiming, - useAnimatedStyle, - useSharedValue, - interpolate, - Extrapolate, - runOnJS, - cancelAnimation, - SharedValue, - Layout -} from 'react-native-reanimated'; import { logger } from '../utils/logger'; const TMDB_LOGO = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/Tmdb.new.logo.svg/512px-Tmdb.new.logo.svg.png?20200406190906'; @@ -87,6 +78,102 @@ const detectMkvViaHead = async (url: string, headers?: Record) = } }; +// Animated Components +const AnimatedImage = memo(({ + source, + style, + contentFit, + onLoad +}: { + source: { uri: string } | undefined; + style: any; + contentFit: any; + onLoad?: () => void; +}) => { + const opacity = useSharedValue(0); + + const animatedStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + })); + + useEffect(() => { + if (source?.uri) { + opacity.value = withTiming(1, { duration: 300 }); + } + }, [source?.uri]); + + return ( + + + + ); +}); + +const AnimatedText = memo(({ + children, + style, + delay = 0, + numberOfLines +}: { + children: React.ReactNode; + style: any; + delay?: number; + numberOfLines?: number; +}) => { + const opacity = useSharedValue(0); + const translateY = useSharedValue(20); + + const animatedStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + transform: [{ translateY: translateY.value }], + })); + + useEffect(() => { + opacity.value = withDelay(delay, withTiming(1, { duration: 250 })); + translateY.value = withDelay(delay, withTiming(0, { duration: 250 })); + }, []); + + return ( + + {children} + + ); +}); + +const AnimatedView = memo(({ + children, + style, + delay = 0 +}: { + children: React.ReactNode; + style?: any; + delay?: number; +}) => { + const opacity = useSharedValue(0); + const translateY = useSharedValue(20); + + const animatedStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + transform: [{ translateY: translateY.value }], + })); + + useEffect(() => { + opacity.value = withDelay(delay, withTiming(1, { duration: 250 })); + translateY.value = withDelay(delay, withTiming(0, { duration: 250 })); + }, []); + + return ( + + {children} + + ); +}); + // Extracted Components const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, theme, showLogos, scraperLogo }: { stream: Stream; @@ -269,22 +356,20 @@ const ProviderFilter = memo(({ const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]); const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => ( - - onSelect(item.id)} - > - - {item.name} - - - + onSelect(item.id)} + > + + {item.name} + + ), [selectedProvider, onSelect, styles]); return ( @@ -383,10 +468,6 @@ export const StreamsScreen = () => { const [selectedProvider, setSelectedProvider] = React.useState('all'); const [availableProviders, setAvailableProviders] = React.useState>(new Set()); - // Optimize animation values with cleanup - const headerOpacity = useSharedValue(0); - const heroScale = useSharedValue(0.95); - const filterOpacity = useSharedValue(0); // Add state for provider loading status const [loadingProviders, setLoadingProviders] = useState<{[key: string]: boolean}>({}); @@ -571,34 +652,9 @@ export const StreamsScreen = () => { checkProviders(); }, [type, id, episodeId, settings.autoplayBestStream, fromPlayer]); - React.useEffect(() => { - // Trigger entrance animations - headerOpacity.value = withTiming(1, { duration: 400 }); - heroScale.value = withSpring(1, { - damping: 15, - stiffness: 100, - mass: 0.9, - restDisplacementThreshold: 0.01 - }); - filterOpacity.value = withTiming(1, { duration: 500 }); - - return () => { - // Cleanup animations on unmount - cancelAnimation(headerOpacity); - cancelAnimation(heroScale); - cancelAnimation(filterOpacity); - }; - }, []); // Memoize handlers const handleBack = useCallback(() => { - const cleanup = () => { - headerOpacity.value = withTiming(0, { duration: 100 }); - heroScale.value = withTiming(0.95, { duration: 100 }); - filterOpacity.value = withTiming(0, { duration: 100 }); - }; - cleanup(); - if (type === 'series') { // Reset stack to ensure there is always a screen to go back to from Metadata (navigation as any).reset({ @@ -616,7 +672,7 @@ export const StreamsScreen = () => { } else { (navigation as any).navigate('MainTabs'); } - }, [navigation, headerOpacity, heroScale, filterOpacity, type, id]); + }, [navigation, type, id]); const handleProviderChange = useCallback((provider: string) => { setSelectedProvider(provider); @@ -1430,24 +1486,6 @@ export const StreamsScreen = () => { const showInitialLoading = streamsEmpty && (streamsLoadStart === null || loadElapsed < 10000); const showStillFetching = streamsEmpty && loadElapsed >= 10000; - const heroStyle = useAnimatedStyle(() => ({ - transform: [{ scale: heroScale.value }], - opacity: headerOpacity.value - })); - - const filterStyle = useAnimatedStyle(() => ({ - opacity: filterOpacity.value, - transform: [ - { - translateY: interpolate( - filterOpacity.value, - [0, 1], - [20, 0], - Extrapolate.CLAMP - ) - } - ] - })); const renderItem = useCallback(({ item, index, section }: { item: Stream; index: number; section: any }) => { const stream = item; @@ -1509,7 +1547,7 @@ export const StreamsScreen = () => { {Platform.OS !== 'ios' && ( - { {type === 'series' ? 'Back to Episodes' : 'Back to Info'} - + )} {type === 'movie' && metadata && ( - + {metadata.logo ? ( - ) : ( - + {metadata.name} - + )} - + )} {type === 'series' && ( - - - + + - { > {currentEpisode ? ( - - {currentEpisode.episodeString} - + + + {currentEpisode.episodeString} + + {currentEpisode.name} - + {!!currentEpisode.overview && ( - + {currentEpisode.overview} - + )} - + {tmdbService.formatAirDate(currentEpisode.air_date)} {effectiveEpisodeVote > 0 && ( - + {effectiveEpisodeVote.toFixed(1)} - + )} {!!effectiveEpisodeRuntime && ( - + {effectiveEpisodeRuntime >= 60 ? `${Math.floor(effectiveEpisodeRuntime / 60)}h ${effectiveEpisodeRuntime % 60}m` : `${effectiveEpisodeRuntime}m`} - + )} - - + + ) : ( // Placeholder to reserve space and avoid layout shift while loading )} - - - + + + )} - + {Object.keys(streams).length > 0 && ( { theme={currentTheme} /> )} - + {/* Active Scrapers Status */} {activeFetchingScrapers.length > 0 && ( - Fetching from: @@ -1638,13 +1677,12 @@ export const StreamsScreen = () => { ))} - + )} {/* Update the streams/loading state display logic */} { showNoSourcesError ? ( - @@ -1658,46 +1696,46 @@ export const StreamsScreen = () => { > Add Sources - + ) : streamsEmpty ? ( showInitialLoading ? ( - {isAutoplayWaiting ? 'Finding best stream for autoplay...' : 'Finding available streams...'} - + ) : showStillFetching ? ( - Still fetching streams… - + ) : ( // No streams and not loading = no streams available - No streams available - + ) ) : ( // Show streams immediately when available, even if still loading others {/* Show autoplay loading overlay if waiting for autoplay */} {isAutoplayWaiting && !autoplayTriggered && ( - Starting best stream... - + )} StyleSheet.create({ fontSize: 13, fontWeight: '600', }, - transitionOverlay: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: colors.darkBackground, - justifyContent: 'center', - alignItems: 'center', - zIndex: 9999, - }, sectionHeaderContainer: { paddingHorizontal: 12, paddingVertical: 8,