mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 16:51:57 +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,13 @@ else
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Try upload with extended timeout and retry logic
|
# Upload with multiple strategies to handle server issues
|
||||||
max_retries=3
|
echo "🔄 Uploading with extended timeout..."
|
||||||
retry_count=0
|
echo "📊 File size: $(du -h ${timestamp}.zip | cut -f1)"
|
||||||
|
|
||||||
while [ $retry_count -lt $max_retries ]; do
|
# Strategy 1: Try with HTTP/2 disabled and longer timeouts
|
||||||
echo "🔄 Upload attempt $((retry_count + 1))/$max_retries..."
|
echo "🔍 Attempt 1: HTTP/1.1 with extended timeout..."
|
||||||
|
response=$(curl --http1.1 --max-time 600 --connect-timeout 60 -X POST $serverHost/api/upload \
|
||||||
response=$(curl --http1.1 --max-time 300 --connect-timeout 30 -X POST $serverHost/api/upload \
|
|
||||||
-F "file=@${timestamp}.zip" \
|
-F "file=@${timestamp}.zip" \
|
||||||
-F "runtimeVersion=$runtimeVersion" \
|
-F "runtimeVersion=$runtimeVersion" \
|
||||||
-F "commitHash=$commitHash" \
|
-F "commitHash=$commitHash" \
|
||||||
|
|
@ -139,12 +138,50 @@ while [ $retry_count -lt $max_retries ]; do
|
||||||
${RELEASE_NOTES:+-F "releaseNotes=$RELEASE_NOTES"} \
|
${RELEASE_NOTES:+-F "releaseNotes=$RELEASE_NOTES"} \
|
||||||
--write-out "HTTP_CODE:%{http_code}" \
|
--write-out "HTTP_CODE:%{http_code}" \
|
||||||
--silent \
|
--silent \
|
||||||
--show-error)
|
--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..."
|
||||||
|
|
||||||
|
# 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" \
|
||||||
|
-F "commitMessage=$commitMessage" \
|
||||||
|
${RELEASE_NOTES:+-F "releaseNotes=$RELEASE_NOTES"} \
|
||||||
|
--write-out "HTTP_CODE:%{http_code}" \
|
||||||
|
--silent \
|
||||||
|
--show-error \
|
||||||
|
--no-buffer \
|
||||||
|
--tcp-nodelay)
|
||||||
|
|
||||||
# Extract HTTP code from response
|
# Extract HTTP code from response
|
||||||
http_code=$(echo "$response" | grep -o "HTTP_CODE:[0-9]*" | cut -d: -f2)
|
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
|
if [ -z "$http_code" ] || ! [[ "$http_code" =~ ^[0-9]+$ ]]; then
|
||||||
echo "❌ Failed to extract HTTP status code from response"
|
echo "❌ Failed to extract HTTP status code from response"
|
||||||
echo "Response: $response"
|
echo "Response: $response"
|
||||||
|
|
@ -156,14 +193,13 @@ while [ $retry_count -lt $max_retries ]; do
|
||||||
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
|
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ Successfully uploaded to $serverHost/api/upload"
|
echo "✅ Successfully uploaded to $serverHost/api/upload"
|
||||||
break
|
# 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
|
else
|
||||||
retry_count=$((retry_count + 1))
|
echo "❌ Error: All upload attempts failed"
|
||||||
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"
|
echo "📊 Final HTTP Status: $http_code"
|
||||||
if [ "$http_code" = "524" ]; then
|
if [ "$http_code" = "524" ]; then
|
||||||
echo "💡 Error 524: Server timeout - try again later or check server capacity"
|
echo "💡 Error 524: Server timeout - try again later or check server capacity"
|
||||||
|
|
@ -171,13 +207,16 @@ while [ $retry_count -lt $max_retries ]; do
|
||||||
echo "💡 Error 413: File too large - consider reducing bundle size"
|
echo "💡 Error 413: File too large - consider reducing bundle size"
|
||||||
elif [ "$http_code" = "500" ]; then
|
elif [ "$http_code" = "500" ]; then
|
||||||
echo "💡 Error 500: Server error - check server logs"
|
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
|
else
|
||||||
echo "💡 Check server status and try again"
|
echo "💡 Check server status and try again"
|
||||||
fi
|
fi
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -363,7 +363,8 @@ const WatchProgressDisplay = memo(({
|
||||||
animatedStyle,
|
animatedStyle,
|
||||||
isWatched,
|
isWatched,
|
||||||
isTrailerPlaying,
|
isTrailerPlaying,
|
||||||
trailerMuted
|
trailerMuted,
|
||||||
|
trailerReady
|
||||||
}: {
|
}: {
|
||||||
watchProgress: {
|
watchProgress: {
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
|
|
@ -379,6 +380,7 @@ const WatchProgressDisplay = memo(({
|
||||||
isWatched: boolean;
|
isWatched: boolean;
|
||||||
isTrailerPlaying: boolean;
|
isTrailerPlaying: boolean;
|
||||||
trailerMuted: boolean;
|
trailerMuted: boolean;
|
||||||
|
trailerReady: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const { isAuthenticated: isTraktAuthenticated, forceSyncTraktProgress } = useTraktContext();
|
const { isAuthenticated: isTraktAuthenticated, forceSyncTraktProgress } = useTraktContext();
|
||||||
|
|
@ -589,8 +591,8 @@ const WatchProgressDisplay = memo(({
|
||||||
|
|
||||||
if (!progressData) return null;
|
if (!progressData) return null;
|
||||||
|
|
||||||
// Hide watch progress when trailer is playing AND unmuted
|
// Hide watch progress when trailer is playing AND unmuted AND trailer is ready
|
||||||
if (isTrailerPlaying && !trailerMuted) return null;
|
if (isTrailerPlaying && !trailerMuted && trailerReady) return null;
|
||||||
|
|
||||||
const isCompleted = progressData.isWatched || progressData.progressPercent >= 85;
|
const isCompleted = progressData.isWatched || progressData.progressPercent >= 85;
|
||||||
|
|
||||||
|
|
@ -1488,6 +1490,7 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
isWatched={isWatched}
|
isWatched={isWatched}
|
||||||
isTrailerPlaying={globalTrailerPlaying}
|
isTrailerPlaying={globalTrailerPlaying}
|
||||||
trailerMuted={trailerMuted}
|
trailerMuted={trailerMuted}
|
||||||
|
trailerReady={trailerReady}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Optimized genre display with lazy loading */}
|
{/* Optimized genre display with lazy loading */}
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,26 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const isXprimeStream = streamProvider === 'xprime' || streamProvider === 'Xprime' ||
|
const isXprimeStream = streamProvider === 'xprime' || streamProvider === 'Xprime' ||
|
||||||
(uri && /flutch.*\.workers\.dev|fsl\.fastcloud\.casa|xprime/i.test(uri));
|
(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)
|
// Xprime-specific headers for better compatibility (from local-scrapers-repo)
|
||||||
const getXprimeHeaders = () => {
|
const getXprimeHeaders = () => {
|
||||||
if (!isXprimeStream) return {};
|
if (!isXprimeStream) return {};
|
||||||
|
|
@ -98,6 +118,24 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
return xprimeHeaders;
|
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
|
// Optional hint not yet in typed navigator params
|
||||||
const videoType = (route.params as any).videoType as string | undefined;
|
const videoType = (route.params as any).videoType as string | undefined;
|
||||||
|
|
||||||
|
|
@ -1263,10 +1301,34 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
return; // Do not proceed to show error UI
|
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'));
|
const isUnrecognized = !!(error?.error?.errorString && String(error.error.errorString).includes('UnrecognizedInputFormatException'));
|
||||||
if (isUnrecognized && retryAttemptRef.current < 1) {
|
if (isUnrecognized && retryAttemptRef.current < 1) {
|
||||||
retryAttemptRef.current = 1;
|
retryAttemptRef.current = 1;
|
||||||
|
|
||||||
|
// 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';
|
const nextType = currentVideoType === 'm3u8' ? 'mp4' : 'm3u8';
|
||||||
logger.warn(`[AndroidVideoPlayer] Format not recognized. Retrying with type='${nextType}'`);
|
logger.warn(`[AndroidVideoPlayer] Format not recognized. Retrying with type='${nextType}'`);
|
||||||
if (errorTimeoutRef.current) {
|
if (errorTimeoutRef.current) {
|
||||||
|
|
@ -1285,6 +1347,34 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}, 120);
|
}, 120);
|
||||||
return;
|
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;
|
||||||
|
}
|
||||||
|
safeSetState(() => setShowErrorModal(false));
|
||||||
|
setPaused(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isMounted.current) return;
|
||||||
|
setCurrentVideoType('mp4');
|
||||||
|
// Force re-mount of source by tweaking URL param
|
||||||
|
const sep = currentStreamUrl.includes('?') ? '&' : '?';
|
||||||
|
setCurrentStreamUrl(`${currentStreamUrl}${sep}manifest_fix_retry=${Date.now()}`);
|
||||||
|
setPaused(false);
|
||||||
|
}, 120);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for specific AVFoundation server configuration errors (iOS)
|
// Check for specific AVFoundation server configuration errors (iOS)
|
||||||
const isServerConfigError = error?.error?.code === -11850 ||
|
const isServerConfigError = error?.error?.code === -11850 ||
|
||||||
|
|
@ -2437,11 +2527,25 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
<Video
|
<Video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
|
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}
|
paused={paused}
|
||||||
onLoadStart={() => {
|
onLoadStart={() => {
|
||||||
loadStartAtRef.current = Date.now();
|
loadStartAtRef.current = Date.now();
|
||||||
logger.log('[AndroidVideoPlayer] onLoadStart');
|
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}
|
onProgress={handleProgress}
|
||||||
onLoad={(e) => {
|
onLoad={(e) => {
|
||||||
|
|
@ -2487,6 +2591,13 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
preferredForwardBufferDuration={1 as any}
|
preferredForwardBufferDuration={1 as any}
|
||||||
allowsExternalPlayback={false as any}
|
allowsExternalPlayback={false as any}
|
||||||
preventsDisplaySleepDuringVideoPlayback={true as any}
|
preventsDisplaySleepDuringVideoPlayback={true as any}
|
||||||
|
// ExoPlayer HLS optimization
|
||||||
|
bufferConfig={{
|
||||||
|
minBufferMs: 15000,
|
||||||
|
maxBufferMs: 50000,
|
||||||
|
bufferForPlaybackMs: 2500,
|
||||||
|
bufferForPlaybackAfterRebufferMs: 5000,
|
||||||
|
} as any}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -929,7 +929,7 @@ export const StreamsScreen = () => {
|
||||||
const streamName = stream.name || stream.title || 'Unnamed Stream';
|
const streamName = stream.name || stream.title || 'Unnamed Stream';
|
||||||
const streamProvider = stream.addonId || stream.addonName || stream.name;
|
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;
|
let forceVlc = !!options?.forceVlc;
|
||||||
try {
|
try {
|
||||||
if (Platform.OS === 'ios' && !forceVlc) {
|
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 isMkvByPath = lowerUri.includes('.mkv') || /[?&]ext=mkv\b/.test(lowerUri) || /format=mkv\b/.test(lowerUri) || /container=mkv\b/.test(lowerUri);
|
||||||
const isMkvFile = Boolean(isMkvByHeader || isMkvByPath);
|
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;
|
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) {
|
} catch (e) {
|
||||||
|
|
@ -1199,7 +1212,22 @@ export const StreamsScreen = () => {
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
|
// Add delay before locking orientation to prevent background glitches
|
||||||
|
const orientationTimer = setTimeout(() => {
|
||||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
|
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 () => {};
|
return () => {};
|
||||||
}, [])
|
}, [])
|
||||||
|
|
@ -1509,43 +1537,6 @@ export const StreamsScreen = () => {
|
||||||
const showStillFetching = streamsEmpty && loadElapsed >= 10000;
|
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 renderSectionHeader = useCallback(({ section }: { section: { title: string; addonId: string; isEmptyDueToQualityFilter?: boolean } }) => {
|
||||||
const isProviderLoading = loadingProviders[section.addonId];
|
const isProviderLoading = loadingProviders[section.addonId];
|
||||||
|
|
@ -1777,37 +1768,84 @@ export const StreamsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SectionList
|
<ScrollView
|
||||||
sections={sections}
|
style={styles.streamsContent}
|
||||||
|
contentContainerStyle={[
|
||||||
|
styles.streamsContainer,
|
||||||
|
{ paddingBottom: insets.bottom + 100 } // Add safe area + extra padding
|
||||||
|
]}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
bounces={true}
|
||||||
|
overScrollMode="never"
|
||||||
|
// 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) => {
|
keyExtractor={(item, index) => {
|
||||||
if (item && item.url) {
|
if (item && item.url) {
|
||||||
return item.url;
|
return item.url;
|
||||||
}
|
}
|
||||||
// For empty sections, use a special key
|
return `empty-${sectionIndex}-${index}`;
|
||||||
return `empty-${index}`;
|
|
||||||
}}
|
}}
|
||||||
renderItem={renderItem}
|
renderItem={({ item, index }) => (
|
||||||
renderSectionHeader={renderSectionHeader}
|
<View>
|
||||||
stickySectionHeadersEnabled={false}
|
<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}
|
initialNumToRender={6}
|
||||||
maxToRenderPerBatch={2}
|
maxToRenderPerBatch={2}
|
||||||
windowSize={3}
|
windowSize={3}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
contentContainerStyle={styles.streamsContainer}
|
|
||||||
style={styles.streamsContent}
|
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
bounces={true}
|
/>
|
||||||
overScrollMode="never"
|
) : (
|
||||||
ListEmptyComponent={null}
|
// Empty section placeholder
|
||||||
ListFooterComponent={
|
<View style={styles.emptySectionContainer}>
|
||||||
(loadingStreams || loadingEpisodeStreams) && hasStremioStreamProviders ? (
|
<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}>
|
<View style={styles.footerLoading}>
|
||||||
<ActivityIndicator size="small" color={colors.primary} />
|
<ActivityIndicator size="small" color={colors.primary} />
|
||||||
<Text style={styles.footerLoadingText}>Loading more sources...</Text>
|
<Text style={styles.footerLoadingText}>Loading more sources...</Text>
|
||||||
</View>
|
</View>
|
||||||
) : null
|
)}
|
||||||
}
|
</ScrollView>
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -1820,6 +1858,15 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: colors.darkBackground,
|
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: {
|
backButtonContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|
@ -1848,6 +1895,13 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
backgroundColor: colors.darkBackground,
|
backgroundColor: colors.darkBackground,
|
||||||
paddingTop: 12,
|
paddingTop: 12,
|
||||||
zIndex: 1,
|
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: {
|
streamsMainContentMovie: {
|
||||||
paddingTop: Platform.OS === 'android' ? 10 : 15,
|
paddingTop: Platform.OS === 'android' ? 10 : 15,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue