mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-30 22:39:21 +00:00
improved source modal behaviour
This commit is contained in:
parent
f24d889ee7
commit
9452b01e9c
4 changed files with 74 additions and 244 deletions
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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 }}>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue