mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
improved player detection logic ios
This commit is contained in:
parent
a241de97f6
commit
dc181905e9
4 changed files with 312 additions and 105 deletions
|
|
@ -124,14 +124,50 @@ else
|
|||
fi
|
||||
echo ""
|
||||
|
||||
# Try upload with extended timeout and retry logic
|
||||
max_retries=3
|
||||
retry_count=0
|
||||
# Upload with multiple strategies to handle server issues
|
||||
echo "🔄 Uploading with extended timeout..."
|
||||
echo "📊 File size: $(du -h ${timestamp}.zip | cut -f1)"
|
||||
|
||||
while [ $retry_count -lt $max_retries ]; do
|
||||
echo "🔄 Upload attempt $((retry_count + 1))/$max_retries..."
|
||||
# Strategy 1: Try with HTTP/2 disabled and longer timeouts
|
||||
echo "🔍 Attempt 1: HTTP/1.1 with extended timeout..."
|
||||
response=$(curl --http1.1 --max-time 600 --connect-timeout 60 -X POST $serverHost/api/upload \
|
||||
-F "file=@${timestamp}.zip" \
|
||||
-F "runtimeVersion=$runtimeVersion" \
|
||||
-F "commitHash=$commitHash" \
|
||||
-F "commitMessage=$commitMessage" \
|
||||
${RELEASE_NOTES:+-F "releaseNotes=$RELEASE_NOTES"} \
|
||||
--write-out "HTTP_CODE:%{http_code}" \
|
||||
--silent \
|
||||
--show-error \
|
||||
--retry 2 \
|
||||
--retry-delay 5)
|
||||
|
||||
# Extract HTTP code from response
|
||||
http_code=$(echo "$response" | grep -o "HTTP_CODE:[0-9]*" | cut -d: -f2)
|
||||
|
||||
# Check if we got a valid HTTP code
|
||||
if [ -z "$http_code" ] || ! [[ "$http_code" =~ ^[0-9]+$ ]]; then
|
||||
echo "❌ Failed to extract HTTP status code from response"
|
||||
echo "Response: $response"
|
||||
http_code="000"
|
||||
fi
|
||||
|
||||
echo "HTTP Status: $http_code"
|
||||
|
||||
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
|
||||
echo ""
|
||||
echo "✅ Successfully uploaded to $serverHost/api/upload"
|
||||
# Extract the response body (everything before HTTP_CODE)
|
||||
response_body=$(echo "$response" | sed 's/HTTP_CODE:[0-9]*$//')
|
||||
if [ -n "$response_body" ]; then
|
||||
echo "📦 Server response: $response_body"
|
||||
fi
|
||||
else
|
||||
echo "❌ Strategy 1 failed, trying alternative approach..."
|
||||
|
||||
response=$(curl --http1.1 --max-time 300 --connect-timeout 30 -X POST $serverHost/api/upload \
|
||||
# Strategy 2: Try with different curl options
|
||||
echo "🔍 Attempt 2: Alternative curl configuration..."
|
||||
response=$(curl --http1.1 --max-time 900 --connect-timeout 120 -X POST $serverHost/api/upload \
|
||||
-F "file=@${timestamp}.zip" \
|
||||
-F "runtimeVersion=$runtimeVersion" \
|
||||
-F "commitHash=$commitHash" \
|
||||
|
|
@ -139,12 +175,13 @@ while [ $retry_count -lt $max_retries ]; do
|
|||
${RELEASE_NOTES:+-F "releaseNotes=$RELEASE_NOTES"} \
|
||||
--write-out "HTTP_CODE:%{http_code}" \
|
||||
--silent \
|
||||
--show-error)
|
||||
--show-error \
|
||||
--no-buffer \
|
||||
--tcp-nodelay)
|
||||
|
||||
# Extract HTTP code from response
|
||||
http_code=$(echo "$response" | grep -o "HTTP_CODE:[0-9]*" | cut -d: -f2)
|
||||
|
||||
# Check if we got a valid HTTP code
|
||||
if [ -z "$http_code" ] || ! [[ "$http_code" =~ ^[0-9]+$ ]]; then
|
||||
echo "❌ Failed to extract HTTP status code from response"
|
||||
echo "Response: $response"
|
||||
|
|
@ -156,28 +193,30 @@ while [ $retry_count -lt $max_retries ]; do
|
|||
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
|
||||
echo ""
|
||||
echo "✅ Successfully uploaded to $serverHost/api/upload"
|
||||
break
|
||||
else
|
||||
retry_count=$((retry_count + 1))
|
||||
if [ $retry_count -lt $max_retries ]; then
|
||||
echo "⚠️ Upload attempt $retry_count failed, retrying in 5 seconds..."
|
||||
sleep 5
|
||||
else
|
||||
echo "❌ Error: Upload failed after $max_retries attempts"
|
||||
echo "📊 Final HTTP Status: $http_code"
|
||||
if [ "$http_code" = "524" ]; then
|
||||
echo "💡 Error 524: Server timeout - try again later or check server capacity"
|
||||
elif [ "$http_code" = "413" ]; then
|
||||
echo "💡 Error 413: File too large - consider reducing bundle size"
|
||||
elif [ "$http_code" = "500" ]; then
|
||||
echo "💡 Error 500: Server error - check server logs"
|
||||
else
|
||||
echo "💡 Check server status and try again"
|
||||
fi
|
||||
exit 1
|
||||
# Extract the response body (everything before HTTP_CODE)
|
||||
response_body=$(echo "$response" | sed 's/HTTP_CODE:[0-9]*$//')
|
||||
if [ -n "$response_body" ]; then
|
||||
echo "📦 Server response: $response_body"
|
||||
fi
|
||||
else
|
||||
echo "❌ Error: All upload attempts failed"
|
||||
echo "📊 Final HTTP Status: $http_code"
|
||||
if [ "$http_code" = "524" ]; then
|
||||
echo "💡 Error 524: Server timeout - try again later or check server capacity"
|
||||
elif [ "$http_code" = "413" ]; then
|
||||
echo "💡 Error 413: File too large - consider reducing bundle size"
|
||||
elif [ "$http_code" = "500" ]; then
|
||||
echo "💡 Error 500: Server error - check server logs"
|
||||
elif [ "$http_code" = "502" ]; then
|
||||
echo "💡 Error 502: Bad Gateway - server may be overloaded"
|
||||
echo "💡 Try running the script again in a few minutes"
|
||||
echo "💡 Or use manual curl: curl -X POST $serverHost/api/upload -F \"file=@${timestamp}.zip\" -F \"runtimeVersion=$runtimeVersion\" -F \"commitHash=$commitHash\" -F \"commitMessage=$commitMessage\""
|
||||
else
|
||||
echo "💡 Check server status and try again"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
cd ..
|
||||
|
||||
|
|
|
|||
|
|
@ -363,7 +363,8 @@ const WatchProgressDisplay = memo(({
|
|||
animatedStyle,
|
||||
isWatched,
|
||||
isTrailerPlaying,
|
||||
trailerMuted
|
||||
trailerMuted,
|
||||
trailerReady
|
||||
}: {
|
||||
watchProgress: {
|
||||
currentTime: number;
|
||||
|
|
@ -379,6 +380,7 @@ const WatchProgressDisplay = memo(({
|
|||
isWatched: boolean;
|
||||
isTrailerPlaying: boolean;
|
||||
trailerMuted: boolean;
|
||||
trailerReady: boolean;
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
const { isAuthenticated: isTraktAuthenticated, forceSyncTraktProgress } = useTraktContext();
|
||||
|
|
@ -589,8 +591,8 @@ const WatchProgressDisplay = memo(({
|
|||
|
||||
if (!progressData) return null;
|
||||
|
||||
// Hide watch progress when trailer is playing AND unmuted
|
||||
if (isTrailerPlaying && !trailerMuted) return null;
|
||||
// Hide watch progress when trailer is playing AND unmuted AND trailer is ready
|
||||
if (isTrailerPlaying && !trailerMuted && trailerReady) return null;
|
||||
|
||||
const isCompleted = progressData.isWatched || progressData.progressPercent >= 85;
|
||||
|
||||
|
|
@ -1488,6 +1490,7 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
isWatched={isWatched}
|
||||
isTrailerPlaying={globalTrailerPlaying}
|
||||
trailerMuted={trailerMuted}
|
||||
trailerReady={trailerReady}
|
||||
/>
|
||||
|
||||
{/* Optimized genre display with lazy loading */}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,26 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const isXprimeStream = streamProvider === 'xprime' || streamProvider === 'Xprime' ||
|
||||
(uri && /flutch.*\.workers\.dev|fsl\.fastcloud\.casa|xprime/i.test(uri));
|
||||
|
||||
// Check if the stream is HLS (m3u8 playlist)
|
||||
const isHlsStream = (url: string) => {
|
||||
return url.includes('.m3u8') || url.includes('m3u8') ||
|
||||
url.includes('hls') || url.includes('playlist') ||
|
||||
(currentVideoType && currentVideoType.toLowerCase() === 'm3u8');
|
||||
};
|
||||
|
||||
// HLS-specific headers for better ExoPlayer compatibility
|
||||
const getHlsHeaders = () => {
|
||||
return {
|
||||
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
|
||||
'Accept': 'application/vnd.apple.mpegurl, application/x-mpegurl, application/vnd.apple.mpegurl, video/mp2t, video/mp4, */*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Connection': 'keep-alive',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
} as any;
|
||||
};
|
||||
|
||||
// Xprime-specific headers for better compatibility (from local-scrapers-repo)
|
||||
const getXprimeHeaders = () => {
|
||||
if (!isXprimeStream) return {};
|
||||
|
|
@ -98,6 +118,24 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
return xprimeHeaders;
|
||||
};
|
||||
|
||||
// Get appropriate headers based on stream type
|
||||
const getStreamHeaders = () => {
|
||||
// For Xprime streams, be more flexible - only use HLS headers if it actually looks like HLS
|
||||
if (isXprimeStream) {
|
||||
if (isHlsStream(currentStreamUrl)) {
|
||||
logger.log('[AndroidVideoPlayer] Xprime HLS stream detected, applying HLS headers');
|
||||
return getXprimeHeaders();
|
||||
} else {
|
||||
logger.log('[AndroidVideoPlayer] Xprime MP4 stream detected, using default headers');
|
||||
return Platform.OS === 'android' ? defaultAndroidHeaders() : defaultIosHeaders();
|
||||
}
|
||||
} else if (isHlsStream(currentStreamUrl)) {
|
||||
logger.log('[AndroidVideoPlayer] Detected HLS stream, applying HLS headers');
|
||||
return getHlsHeaders();
|
||||
}
|
||||
return Platform.OS === 'android' ? defaultAndroidHeaders() : defaultIosHeaders();
|
||||
};
|
||||
|
||||
// Optional hint not yet in typed navigator params
|
||||
const videoType = (route.params as any).videoType as string | undefined;
|
||||
|
||||
|
|
@ -1263,12 +1301,64 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
return; // Do not proceed to show error UI
|
||||
}
|
||||
|
||||
// If format unrecognized, try flipping between HLS and MP4 once
|
||||
// If format unrecognized, try different approaches for HLS streams
|
||||
const isUnrecognized = !!(error?.error?.errorString && String(error.error.errorString).includes('UnrecognizedInputFormatException'));
|
||||
if (isUnrecognized && retryAttemptRef.current < 1) {
|
||||
retryAttemptRef.current = 1;
|
||||
const nextType = currentVideoType === 'm3u8' ? 'mp4' : 'm3u8';
|
||||
logger.warn(`[AndroidVideoPlayer] Format not recognized. Retrying with type='${nextType}'`);
|
||||
|
||||
// Check if this might be an HLS stream that needs different handling
|
||||
const mightBeHls = currentStreamUrl.includes('.m3u8') || currentStreamUrl.includes('playlist') ||
|
||||
currentStreamUrl.includes('hls') || currentStreamUrl.includes('stream');
|
||||
|
||||
if (mightBeHls) {
|
||||
logger.warn(`[AndroidVideoPlayer] HLS stream format not recognized. Retrying with explicit HLS type and headers`);
|
||||
if (errorTimeoutRef.current) {
|
||||
clearTimeout(errorTimeoutRef.current);
|
||||
errorTimeoutRef.current = null;
|
||||
}
|
||||
safeSetState(() => setShowErrorModal(false));
|
||||
setPaused(true);
|
||||
setTimeout(() => {
|
||||
if (!isMounted.current) return;
|
||||
// Force HLS type and add cache-busting
|
||||
setCurrentVideoType('m3u8');
|
||||
const sep = currentStreamUrl.includes('?') ? '&' : '?';
|
||||
setCurrentStreamUrl(`${currentStreamUrl}${sep}hls_retry=${Date.now()}`);
|
||||
setPaused(false);
|
||||
}, 120);
|
||||
return;
|
||||
} else {
|
||||
// For non-HLS streams, try flipping between HLS and MP4
|
||||
const nextType = currentVideoType === 'm3u8' ? 'mp4' : 'm3u8';
|
||||
logger.warn(`[AndroidVideoPlayer] Format not recognized. Retrying with type='${nextType}'`);
|
||||
if (errorTimeoutRef.current) {
|
||||
clearTimeout(errorTimeoutRef.current);
|
||||
errorTimeoutRef.current = null;
|
||||
}
|
||||
safeSetState(() => setShowErrorModal(false));
|
||||
setPaused(true);
|
||||
setTimeout(() => {
|
||||
if (!isMounted.current) return;
|
||||
setCurrentVideoType(nextType);
|
||||
// Force re-mount of source by tweaking URL param
|
||||
const sep = currentStreamUrl.includes('?') ? '&' : '?';
|
||||
setCurrentStreamUrl(`${currentStreamUrl}${sep}rn_type_retry=${Date.now()}`);
|
||||
setPaused(false);
|
||||
}, 120);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle HLS manifest parsing errors (when content isn't actually M3U8)
|
||||
const isManifestParseError = error?.error?.errorCode === '23002' ||
|
||||
error?.errorCode === '23002' ||
|
||||
(error?.error?.errorString &&
|
||||
error.error.errorString.includes('ERROR_CODE_PARSING_MANIFEST_MALFORMED'));
|
||||
|
||||
if (isManifestParseError && retryAttemptRef.current < 2) {
|
||||
retryAttemptRef.current = 2;
|
||||
logger.warn('[AndroidVideoPlayer] HLS manifest parsing failed, likely not M3U8. Retrying as MP4');
|
||||
|
||||
if (errorTimeoutRef.current) {
|
||||
clearTimeout(errorTimeoutRef.current);
|
||||
errorTimeoutRef.current = null;
|
||||
|
|
@ -1277,10 +1367,10 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
setPaused(true);
|
||||
setTimeout(() => {
|
||||
if (!isMounted.current) return;
|
||||
setCurrentVideoType(nextType);
|
||||
setCurrentVideoType('mp4');
|
||||
// Force re-mount of source by tweaking URL param
|
||||
const sep = currentStreamUrl.includes('?') ? '&' : '?';
|
||||
setCurrentStreamUrl(`${currentStreamUrl}${sep}rn_type_retry=${Date.now()}`);
|
||||
setCurrentStreamUrl(`${currentStreamUrl}${sep}manifest_fix_retry=${Date.now()}`);
|
||||
setPaused(false);
|
||||
}, 120);
|
||||
return;
|
||||
|
|
@ -2437,11 +2527,25 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
<Video
|
||||
ref={videoRef}
|
||||
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
|
||||
source={{ uri: currentStreamUrl, headers: getXprimeHeaders() || headers || (Platform.OS === 'android' ? defaultAndroidHeaders() : defaultIosHeaders()), type: (currentVideoType as any) }}
|
||||
source={{
|
||||
uri: currentStreamUrl,
|
||||
headers: headers || getStreamHeaders(),
|
||||
type: isHlsStream(currentStreamUrl) ? 'm3u8' : (currentVideoType as any)
|
||||
}}
|
||||
paused={paused}
|
||||
onLoadStart={() => {
|
||||
loadStartAtRef.current = Date.now();
|
||||
logger.log('[AndroidVideoPlayer] onLoadStart');
|
||||
|
||||
// Log stream information for debugging
|
||||
const streamInfo = {
|
||||
url: currentStreamUrl,
|
||||
isHls: isHlsStream(currentStreamUrl),
|
||||
videoType: currentVideoType,
|
||||
headers: headers || getStreamHeaders(),
|
||||
provider: currentStreamProvider || streamProvider
|
||||
};
|
||||
logger.log('[AndroidVideoPlayer] Stream info:', streamInfo);
|
||||
}}
|
||||
onProgress={handleProgress}
|
||||
onLoad={(e) => {
|
||||
|
|
@ -2487,6 +2591,13 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
preferredForwardBufferDuration={1 as any}
|
||||
allowsExternalPlayback={false as any}
|
||||
preventsDisplaySleepDuringVideoPlayback={true as any}
|
||||
// ExoPlayer HLS optimization
|
||||
bufferConfig={{
|
||||
minBufferMs: 15000,
|
||||
maxBufferMs: 50000,
|
||||
bufferForPlaybackMs: 2500,
|
||||
bufferForPlaybackAfterRebufferMs: 5000,
|
||||
} as any}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -929,7 +929,7 @@ export const StreamsScreen = () => {
|
|||
const streamName = stream.name || stream.title || 'Unnamed Stream';
|
||||
const streamProvider = stream.addonId || stream.addonName || stream.name;
|
||||
|
||||
// Determine if we should force VLC on iOS based on actual stream format (not provider capability)
|
||||
// Determine if we should force VLC on iOS based on actual stream format and provider capability
|
||||
let forceVlc = !!options?.forceVlc;
|
||||
try {
|
||||
if (Platform.OS === 'ios' && !forceVlc) {
|
||||
|
|
@ -940,9 +940,22 @@ export const StreamsScreen = () => {
|
|||
const isMkvByPath = lowerUri.includes('.mkv') || /[?&]ext=mkv\b/.test(lowerUri) || /format=mkv\b/.test(lowerUri) || /container=mkv\b/.test(lowerUri);
|
||||
const isMkvFile = Boolean(isMkvByHeader || isMkvByPath);
|
||||
|
||||
if (isMkvFile) {
|
||||
// Also check if the provider declares MKV format support
|
||||
let providerSupportsMkv = false;
|
||||
try {
|
||||
const availableScrapers = await localScraperService.getAvailableScrapers();
|
||||
const provider = availableScrapers.find(scraper => scraper.id === streamProvider);
|
||||
if (provider && provider.formats) {
|
||||
providerSupportsMkv = provider.formats.includes('mkv');
|
||||
logger.log(`[StreamsScreen] Provider ${streamProvider} formats:`, provider.formats, 'supports MKV:', providerSupportsMkv);
|
||||
}
|
||||
} catch (providerError) {
|
||||
logger.warn('[StreamsScreen] Failed to check provider formats:', providerError);
|
||||
}
|
||||
|
||||
if (isMkvFile || providerSupportsMkv) {
|
||||
forceVlc = true;
|
||||
logger.log(`[StreamsScreen] Stream is MKV format -> forcing VLC`);
|
||||
logger.log(`[StreamsScreen] Stream is MKV format (detected: ${isMkvFile}, provider supports: ${providerSupportsMkv}) -> forcing VLC`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -1199,7 +1212,22 @@ export const StreamsScreen = () => {
|
|||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (Platform.OS === 'ios') {
|
||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
|
||||
// Add delay before locking orientation to prevent background glitches
|
||||
const orientationTimer = setTimeout(() => {
|
||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
|
||||
}, 200); // Small delay to let the screen render properly
|
||||
|
||||
// iOS-specific: Force a re-render to prevent background glitches
|
||||
// This helps ensure the background is properly rendered when returning from player
|
||||
const renderTimer = setTimeout(() => {
|
||||
// Trigger a small state update to force re-render
|
||||
setStreamsLoadStart(prev => prev);
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
clearTimeout(orientationTimer);
|
||||
clearTimeout(renderTimer);
|
||||
};
|
||||
}
|
||||
return () => {};
|
||||
}, [])
|
||||
|
|
@ -1509,43 +1537,6 @@ export const StreamsScreen = () => {
|
|||
const showStillFetching = streamsEmpty && loadElapsed >= 10000;
|
||||
|
||||
|
||||
const renderItem = useCallback(({ item, index, section }: { item: any; index: number; section: any }) => {
|
||||
// Handle empty sections due to quality filtering
|
||||
if (item.isEmptyPlaceholder && section.isEmptyDueToQualityFilter) {
|
||||
return (
|
||||
<View style={styles.emptySectionContainer}>
|
||||
<View style={styles.emptySectionContent}>
|
||||
<MaterialIcons name="filter-list-off" size={32} color={colors.mediumEmphasis} />
|
||||
<Text style={[styles.emptySectionTitle, { color: colors.mediumEmphasis }]}>
|
||||
No streams available
|
||||
</Text>
|
||||
<Text style={[styles.emptySectionSubtitle, { color: colors.textMuted }]}>
|
||||
All streams were filtered by your quality settings
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const stream = item as Stream;
|
||||
// Don't show loading for individual streams that are already available and displayed
|
||||
const isLoading = false; // If streams are being rendered, they're available and shouldn't be loading
|
||||
|
||||
return (
|
||||
<View>
|
||||
<StreamCard
|
||||
stream={stream}
|
||||
onPress={() => handleStreamPress(stream)}
|
||||
index={index}
|
||||
isLoading={isLoading}
|
||||
statusMessage={undefined}
|
||||
theme={currentTheme}
|
||||
showLogos={settings.showScraperLogos}
|
||||
scraperLogo={(stream.addonId && scraperLogos[stream.addonId]) || (stream as any).addon ? scraperLogoCache.get((stream.addonId || (stream as any).addon) as string) || null : null}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}, [handleStreamPress, currentTheme, settings.showScraperLogos, scraperLogos, colors.mediumEmphasis, colors.textMuted, styles.emptySectionContainer, styles.emptySectionContent, styles.emptySectionTitle, styles.emptySectionSubtitle]);
|
||||
|
||||
const renderSectionHeader = useCallback(({ section }: { section: { title: string; addonId: string; isEmptyDueToQualityFilter?: boolean } }) => {
|
||||
const isProviderLoading = loadingProviders[section.addonId];
|
||||
|
|
@ -1777,37 +1768,84 @@ export const StreamsScreen = () => {
|
|||
</View>
|
||||
)}
|
||||
|
||||
<SectionList
|
||||
sections={sections}
|
||||
keyExtractor={(item, index) => {
|
||||
if (item && item.url) {
|
||||
return item.url;
|
||||
}
|
||||
// For empty sections, use a special key
|
||||
return `empty-${index}`;
|
||||
}}
|
||||
renderItem={renderItem}
|
||||
renderSectionHeader={renderSectionHeader}
|
||||
stickySectionHeadersEnabled={false}
|
||||
initialNumToRender={6}
|
||||
maxToRenderPerBatch={2}
|
||||
windowSize={3}
|
||||
removeClippedSubviews={true}
|
||||
contentContainerStyle={styles.streamsContainer}
|
||||
<ScrollView
|
||||
style={styles.streamsContent}
|
||||
contentContainerStyle={[
|
||||
styles.streamsContainer,
|
||||
{ paddingBottom: insets.bottom + 100 } // Add safe area + extra padding
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
bounces={true}
|
||||
overScrollMode="never"
|
||||
ListEmptyComponent={null}
|
||||
ListFooterComponent={
|
||||
(loadingStreams || loadingEpisodeStreams) && hasStremioStreamProviders ? (
|
||||
<View style={styles.footerLoading}>
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
<Text style={styles.footerLoadingText}>Loading more sources...</Text>
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
// iOS-specific fixes for navigation transition glitches
|
||||
{...(Platform.OS === 'ios' && {
|
||||
// Ensure proper rendering during transitions
|
||||
removeClippedSubviews: false, // Prevent iOS from clipping views during transitions
|
||||
// Force hardware acceleration for smoother transitions
|
||||
scrollEventThrottle: 16,
|
||||
})}
|
||||
>
|
||||
{sections.map((section, sectionIndex) => (
|
||||
<View key={section.addonId || sectionIndex}>
|
||||
{/* Section Header */}
|
||||
{renderSectionHeader({ section })}
|
||||
|
||||
{/* Stream Cards using FlatList */}
|
||||
{section.data && section.data.length > 0 ? (
|
||||
<FlatList
|
||||
data={section.data}
|
||||
keyExtractor={(item, index) => {
|
||||
if (item && item.url) {
|
||||
return item.url;
|
||||
}
|
||||
return `empty-${sectionIndex}-${index}`;
|
||||
}}
|
||||
renderItem={({ item, index }) => (
|
||||
<View>
|
||||
<StreamCard
|
||||
stream={item}
|
||||
onPress={() => handleStreamPress(item)}
|
||||
index={index}
|
||||
isLoading={false}
|
||||
statusMessage={undefined}
|
||||
theme={currentTheme}
|
||||
showLogos={settings.showScraperLogos}
|
||||
scraperLogo={(item.addonId && scraperLogos[item.addonId]) || (item as any).addon ? scraperLogoCache.get((item.addonId || (item as any).addon) as string) || null : null}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
scrollEnabled={false}
|
||||
initialNumToRender={6}
|
||||
maxToRenderPerBatch={2}
|
||||
windowSize={3}
|
||||
removeClippedSubviews={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
) : (
|
||||
// Empty section placeholder
|
||||
<View style={styles.emptySectionContainer}>
|
||||
<View style={styles.emptySectionContent}>
|
||||
<MaterialIcons name="filter-list-off" size={32} color={colors.mediumEmphasis} />
|
||||
<Text style={[styles.emptySectionTitle, { color: colors.mediumEmphasis }]}>
|
||||
No streams available
|
||||
</Text>
|
||||
<Text style={[styles.emptySectionSubtitle, { color: colors.textMuted }]}>
|
||||
All streams were filtered by your quality settings
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* Footer Loading */}
|
||||
{(loadingStreams || loadingEpisodeStreams) && hasStremioStreamProviders && (
|
||||
<View style={styles.footerLoading}>
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
<Text style={styles.footerLoadingText}>Loading more sources...</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
|
@ -1820,6 +1858,15 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.darkBackground,
|
||||
// iOS-specific fixes for navigation transition glitches
|
||||
...(Platform.OS === 'ios' && {
|
||||
// Ensure the background is properly rendered during transitions
|
||||
opacity: 1,
|
||||
// Prevent iOS from trying to optimize the background during transitions
|
||||
shouldRasterizeIOS: false,
|
||||
// Ensure the view is properly composited
|
||||
renderToHardwareTextureAndroid: false,
|
||||
}),
|
||||
},
|
||||
backButtonContainer: {
|
||||
position: 'absolute',
|
||||
|
|
@ -1848,6 +1895,13 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
backgroundColor: colors.darkBackground,
|
||||
paddingTop: 12,
|
||||
zIndex: 1,
|
||||
// iOS-specific fixes for navigation transition glitches
|
||||
...(Platform.OS === 'ios' && {
|
||||
// Ensure proper rendering during transitions
|
||||
opacity: 1,
|
||||
// Prevent iOS optimization that can cause glitches
|
||||
shouldRasterizeIOS: false,
|
||||
}),
|
||||
},
|
||||
streamsMainContentMovie: {
|
||||
paddingTop: Platform.OS === 'android' ? 10 : 15,
|
||||
|
|
|
|||
Loading…
Reference in a new issue