improved source modal behaviour

This commit is contained in:
tapframe 2025-10-26 20:10:17 +05:30
parent f24d889ee7
commit 9452b01e9c
4 changed files with 74 additions and 244 deletions

View file

@ -479,8 +479,6 @@ const AndroidVideoPlayer: React.FC = () => {
}, [currentStreamUrl, useVLC, processUrlForVLC]);
// Track a single silent retry per source to avoid loops
const retryAttemptRef = useRef<number>(0);
const [isChangingSource, setIsChangingSource] = useState<boolean>(false);
const [pendingSeek, setPendingSeek] = useState<{ position: number; shouldPlay: boolean } | null>(null);
const [currentQuality, setCurrentQuality] = useState<string | undefined>(quality);
const [currentStreamProvider, setCurrentStreamProvider] = useState<string | undefined>(streamProvider);
const [currentStreamName, setCurrentStreamName] = useState<string | undefined>(streamName);
@ -2941,110 +2939,49 @@ const AndroidVideoPlayer: React.FC = () => {
setSubtitleBackground(!subtitleBackground);
};
useEffect(() => {
if (pendingSeek && isPlayerReady && isVideoLoaded && duration > 0) {
logger.log(`[AndroidVideoPlayer] Player ready after source change, seeking to position: ${pendingSeek.position}s out of ${duration}s total`);
if (pendingSeek.position > 0 && videoRef.current) {
const delayTime = 800; // Shorter delay for react-native-video
setTimeout(() => {
if (videoRef.current && duration > 0 && pendingSeek) {
logger.log(`[AndroidVideoPlayer] Executing seek to ${pendingSeek.position}s`);
seekToTime(pendingSeek.position);
if (pendingSeek.shouldPlay) {
setTimeout(() => {
logger.log('[AndroidVideoPlayer] Resuming playback after source change seek');
setPaused(false);
}, 300);
}
setTimeout(() => {
setPendingSeek(null);
setIsChangingSource(false);
}, 400);
}
}, delayTime);
} else {
// No seeking needed, just resume playback if it was playing
if (pendingSeek.shouldPlay) {
setTimeout(() => {
logger.log('[AndroidVideoPlayer] No seek needed, just resuming playback');
setPaused(false);
}, 300);
}
setTimeout(() => {
setPendingSeek(null);
setIsChangingSource(false);
}, 400);
}
}
}, [pendingSeek, isPlayerReady, isVideoLoaded, duration]);
const handleSelectStream = async (newStream: any) => {
if (newStream.url === currentStreamUrl) {
setShowSourcesModal(false);
return;
}
// Note: iOS now always uses KSPlayer, so this AndroidVideoPlayer should never be used on iOS
// This logic is kept for safety in case routing changes
setIsChangingSource(true);
setShowSourcesModal(false);
try {
// Save current state
const savedPosition = currentTime;
const wasPlaying = !paused;
logger.log(`[AndroidVideoPlayer] Changing source from ${currentStreamUrl} to ${newStream.url}`);
logger.log(`[AndroidVideoPlayer] 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;
}
// 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(`[AndroidVideoPlayer] Stream object:`, newStream);
logger.log(`[AndroidVideoPlayer] Extracted - Quality: ${newQuality}, Provider: ${newProvider}, Stream Name: ${newStreamName}`);
// Stop current playback
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);
vlcLoadedRef.current = false;
} catch (error) {
logger.error('[AndroidVideoPlayer] Error changing source:', error);
setPendingSeek(null);
setIsChangingSource(false);
// Extract quality and provider information
let newQuality = newStream.quality;
if (!newQuality && newStream.title) {
const qualityMatch = newStream.title.match(/(\d+)p/);
newQuality = qualityMatch ? qualityMatch[0] : undefined;
}
const newProvider = newStream.addonName || newStream.name || newStream.addon || 'Unknown';
const newStreamName = newStream.name || newStream.title || 'Unknown Stream';
// Pause current playback
setPaused(true);
// Navigate with replace to reload player with new source
setTimeout(() => {
(navigation as any).replace('PlayerAndroid', {
uri: newStream.url,
title: title,
episodeTitle: episodeTitle,
season: season,
episode: episode,
quality: newQuality,
year: year,
streamProvider: newProvider,
streamName: newStreamName,
headers: newStream.headers || undefined,
forceVlc: false,
id,
type,
episodeId,
imdbId: imdbId ?? undefined,
backdrop: backdrop || undefined,
availableStreams: availableStreams,
});
}, 100);
};
useEffect(() => {
@ -3156,27 +3093,6 @@ const AndroidVideoPlayer: React.FC = () => {
</View>
</Animated.View>
{/* Source Change Loading Overlay */}
{isChangingSource && (
<Animated.View
style={[
styles.sourceChangeOverlay,
{
width: screenDimensions.width,
height: screenDimensions.height,
opacity: fadeAnim,
}
]}
pointerEvents="auto"
>
<View style={styles.sourceChangeContent}>
<ActivityIndicator size="large" color="#E50914" />
<Text style={styles.sourceChangeText}>Changing source...</Text>
<Text style={styles.sourceChangeSubtext}>Please wait while we load the new stream</Text>
</View>
</Animated.View>
)}
<Animated.View
style={[
styles.videoPlayerContainer,
@ -4153,7 +4069,6 @@ const AndroidVideoPlayer: React.FC = () => {
availableStreams={availableStreams}
currentStreamUrl={currentStreamUrl}
onSelectStream={handleSelectStream}
isChangingSource={isChangingSource}
/>
{/* Error Modal */}

View file

@ -218,11 +218,9 @@ const KSPlayerCore: React.FC = () => {
setPlaybackSpeed(speedOptions[nextIdx]);
}, [playbackSpeed, speedOptions]);
const [currentStreamUrl, setCurrentStreamUrl] = useState<string>(uri);
const [isChangingSource, setIsChangingSource] = useState<boolean>(false);
const [showErrorModal, setShowErrorModal] = useState(false);
const [errorDetails, setErrorDetails] = useState<string>('');
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [pendingSeek, setPendingSeek] = useState<{ position: number; shouldPlay: boolean } | null>(null);
const [currentQuality, setCurrentQuality] = useState<string | undefined>(quality);
const [currentStreamProvider, setCurrentStreamProvider] = useState<string | undefined>(streamProvider);
const [currentStreamName, setCurrentStreamName] = useState<string | undefined>(streamName);
@ -2305,49 +2303,6 @@ const KSPlayerCore: React.FC = () => {
setSubtitleBackground(prev => !prev);
};
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) {
const delayTime = Platform.OS === 'android' ? 1500 : 1000;
setTimeout(() => {
if (duration > 0 && pendingSeek) {
logger.log(`[VideoPlayer] Executing seek to ${pendingSeek.position}s`);
seekToTime(pendingSeek.position);
if (pendingSeek.shouldPlay) {
setTimeout(() => {
logger.log('[VideoPlayer] Resuming playback after source change seek');
setPaused(false);
}, 850); // Delay should be slightly more than seekToTime's internal timeout
}
setTimeout(() => {
setPendingSeek(null);
setIsChangingSource(false);
}, 900);
}
}, delayTime);
} 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);
}, 500);
}
setTimeout(() => {
setPendingSeek(null);
setIsChangingSource(false);
}, 600);
}
}
}, [pendingSeek, isPlayerReady, isVideoLoaded, duration]);
// AirPlay handler
const handleAirPlayPress = async () => {
if (!ksPlayerRef.current) return;
@ -2375,61 +2330,42 @@ const KSPlayerCore: React.FC = () => {
return;
}
// On iOS: All streams use KSPlayer, no need to switch players
// Stream switching is handled internally by KSPlayerCore
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
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);
// Extract quality and provider information
let newQuality = newStream.quality;
if (!newQuality && newStream.title) {
const qualityMatch = newStream.title.match(/(\d+)p/);
newQuality = qualityMatch ? qualityMatch[0] : undefined;
}
const newProvider = newStream.addonName || newStream.name || newStream.addon || 'Unknown';
const newStreamName = newStream.name || newStream.title || 'Unknown Stream';
// Pause current playback
setPaused(true);
// Navigate with replace to reload player with new source
setTimeout(() => {
navigation.replace('PlayerIOS', {
uri: newStream.url,
title: title,
episodeTitle: episodeTitle,
season: season,
episode: episode,
quality: newQuality,
year: year,
streamProvider: newProvider,
streamName: newStreamName,
headers: newStream.headers || undefined,
id,
type,
episodeId,
imdbId: imdbId ?? undefined,
backdrop: backdrop || undefined,
availableStreams: availableStreams,
});
}, 100);
};
useEffect(() => {
@ -2551,27 +2487,6 @@ const KSPlayerCore: React.FC = () => {
</Animated.View>
)}
{/* Source Change Loading Overlay */}
{isChangingSource && (
<Animated.View
style={[
styles.sourceChangeOverlay,
{
width: shouldUseFullscreen ? '100%' : screenDimensions.width,
height: shouldUseFullscreen ? '100%' : screenDimensions.height,
opacity: fadeAnim,
}
]}
pointerEvents="auto"
>
<View style={styles.sourceChangeContent}>
<ActivityIndicator size="large" color="#E50914" />
<Text style={styles.sourceChangeText}>Changing source...</Text>
<Text style={styles.sourceChangeSubtext}>Please wait while we load the new stream</Text>
</View>
</Animated.View>
)}
<Animated.View
style={[
styles.videoPlayerContainer,
@ -3454,7 +3369,6 @@ const KSPlayerCore: React.FC = () => {
availableStreams={availableStreams}
currentStreamUrl={currentStreamUrl}
onSelectStream={handleSelectStream}
isChangingSource={isChangingSource}
/>
{/* Error Modal */}

View file

@ -15,7 +15,7 @@ interface SourcesModalProps {
availableStreams: { [providerId: string]: { streams: Stream[]; addonName: string } };
currentStreamUrl: string;
onSelectStream: (stream: Stream) => void;
isChangingSource: boolean;
isChangingSource?: boolean;
}
const { width } = Dimensions.get('window');
@ -70,7 +70,7 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
availableStreams,
currentStreamUrl,
onSelectStream,
isChangingSource,
isChangingSource = false,
}) => {
const handleClose = () => {
setShowSourcesModal(false);
@ -81,7 +81,7 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
const sortedProviders = Object.entries(availableStreams);
const handleStreamSelect = (stream: Stream) => {
if (stream.url !== currentStreamUrl && !isChangingSource) {
if (stream.url !== currentStreamUrl && (!isChangingSource || isChangingSource === false)) {
onSelectStream(stream);
}
};
@ -228,11 +228,11 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
padding: 16,
borderWidth: 1,
borderColor: isSelected ? 'rgba(59, 130, 246, 0.3)' : 'rgba(255, 255, 255, 0.1)',
opacity: isChangingSource && !isSelected ? 0.6 : 1,
opacity: (isChangingSource && !isSelected) ? 0.6 : 1,
}}
onPress={() => handleStreamSelect(stream)}
activeOpacity={0.7}
disabled={isChangingSource}
disabled={isChangingSource === true}
>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flex: 1 }}>

View file

@ -862,6 +862,7 @@ const MainTabs = () => {
// Prefer native lazy/freeze when available; still pass for parity
lazy: true,
freezeOnBlur: true,
tabBarStyle: { display: 'none' },
}}
>
<IOSTab.Screen
@ -921,7 +922,7 @@ const MainTabs = () => {
/>
<Tab.Navigator
tabBar={renderTabBar}
tabBar={() => null}
screenOptions={({ route, navigation, theme }) => ({
transitionSpec: {
animation: 'timing',