fixed xprime initial loading issue

This commit is contained in:
tapframe 2025-08-09 18:20:58 +05:30
parent 675e3c24a4
commit 81373a2bb2
4 changed files with 133 additions and 49 deletions

View file

@ -175,6 +175,8 @@ const AndroidVideoPlayer: React.FC = () => {
const [showSourcesModal, setShowSourcesModal] = useState<boolean>(false);
const [availableStreams, setAvailableStreams] = useState<{ [providerId: string]: { streams: any[]; addonName: string } }>(passedAvailableStreams || {});
const [currentStreamUrl, setCurrentStreamUrl] = useState<string>(uri);
// 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);
@ -781,11 +783,41 @@ const AndroidVideoPlayer: React.FC = () => {
return;
}
// Check for specific AVFoundation server configuration errors
const isServerConfigError = error?.error?.code === -11850 ||
error?.code === -11850 ||
(error?.error?.localizedDescription &&
error.error.localizedDescription.includes('server is not correctly configured'));
// Detect Xprime provider to enable a one-shot silent retry (warms upstream/cache)
const providerName = ((currentStreamProvider || streamProvider || '') as string).toLowerCase();
const isXprimeProvider = providerName.includes('xprime');
// One-shot, silent retry without showing error UI
if (isXprimeProvider && retryAttemptRef.current < 1) {
retryAttemptRef.current = 1;
// Cache-bust to force a fresh fetch and warm upstream
const addRetryParam = (url: string) => {
const sep = url.includes('?') ? '&' : '?';
return `${url}${sep}rn_retry_ts=${Date.now()}`;
};
const bustedUrl = addRetryParam(currentStreamUrl);
logger.warn('[AndroidVideoPlayer] Silent retry for Xprime with cache-busted URL');
// Ensure no modal is visible
if (errorTimeoutRef.current) {
clearTimeout(errorTimeoutRef.current);
errorTimeoutRef.current = null;
}
safeSetState(() => setShowErrorModal(false));
// Brief pause to let the player reset
setPaused(true);
setTimeout(() => {
if (!isMounted.current) return;
setCurrentStreamUrl(bustedUrl);
setPaused(false);
}, 120);
return; // Do not proceed to show error UI
}
// Check for specific AVFoundation server configuration errors (iOS)
const isServerConfigError = error?.error?.code === -11850 ||
error?.code === -11850 ||
(error?.error?.localizedDescription &&
error.error.localizedDescription.includes('server is not correctly configured'));
// Format error details for user display
let errorMessage = 'An unknown error occurred';
@ -1488,17 +1520,7 @@ const AndroidVideoPlayer: React.FC = () => {
<Video
ref={videoRef}
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
source={(() => {
// FORCEFULLY use headers from route params if available - no filtering or modification
const sourceWithHeaders = headers ? {
uri: currentStreamUrl,
headers: headers
} : { uri: currentStreamUrl };
// HTTP request logging removed; source prepared
return sourceWithHeaders;
})()}
source={{ uri: currentStreamUrl, headers: headers || {} }}
paused={paused}
onProgress={handleProgress}
onLoad={(e) => {

View file

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import { getColors } from 'react-native-image-colors';
import type { ImageColorsResult } from 'react-native-image-colors';
@ -145,12 +145,13 @@ export const preloadDominantColor = async (imageUri: string | null) => {
console.log('[useDominantColor] Preloading color for URI:', imageUri);
try {
// Fast first-pass: prioritize speed to avoid visible delay
const result = await getColors(imageUri, {
fallback: '#1a1a1a',
cache: true,
key: imageUri,
quality: 'high', // Use higher quality for better color extraction
pixelSpacing: 3, // Better sampling (Android only)
quality: 'low', // Faster extraction
pixelSpacing: 5, // Fewer sampled pixels (Android only)
});
const extractedColor = selectBestColor(result);
@ -172,10 +173,18 @@ export const useDominantColor = (imageUri: string | null): DominantColorResult =
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const lastSetColorRef = useRef<string | null>(dominantColor);
const safelySetColor = useCallback((color: string) => {
if (lastSetColorRef.current !== color) {
lastSetColorRef.current = color;
setDominantColor(color);
}
}, []);
const extractColor = useCallback(async (uri: string) => {
if (!uri) {
setDominantColor('#1a1a1a');
safelySetColor('#1a1a1a');
setLoading(false);
return;
}
@ -183,7 +192,7 @@ export const useDominantColor = (imageUri: string | null): DominantColorResult =
// Check cache first
if (colorCache.has(uri)) {
const cachedColor = colorCache.get(uri)!;
setDominantColor(cachedColor);
safelySetColor(cachedColor);
setLoading(false);
return;
}
@ -192,27 +201,48 @@ export const useDominantColor = (imageUri: string | null): DominantColorResult =
setLoading(true);
setError(null);
const result: ImageColorsResult = await getColors(uri, {
// 1) Fast first-pass extraction to update UI immediately
const fastResult: ImageColorsResult = await getColors(uri, {
fallback: '#1a1a1a',
cache: true,
key: uri,
quality: 'high', // Use higher quality for better accuracy
pixelSpacing: 3, // Better pixel sampling (Android only)
quality: 'low', // Fastest available
pixelSpacing: 5,
});
const extractedColor = selectBestColor(result);
const fastColor = selectBestColor(fastResult);
colorCache.set(uri, fastColor); // Cache fast color to avoid flicker
safelySetColor(fastColor);
setLoading(false);
// Cache the extracted color for future use
colorCache.set(uri, extractedColor);
setDominantColor(extractedColor);
// 2) Optional high-quality refine in background
// Only refine if URI is still the same when this completes
Promise.resolve()
.then(async () => {
const hqResult: ImageColorsResult = await getColors(uri, {
fallback: '#1a1a1a',
cache: true,
key: uri,
quality: 'high',
pixelSpacing: 3,
});
const refinedColor = selectBestColor(hqResult);
if (refinedColor && refinedColor !== fastColor) {
colorCache.set(uri, refinedColor);
safelySetColor(refinedColor);
}
})
.catch(() => {
// Ignore refine errors silently
});
} catch (err) {
console.warn('[useDominantColor] Failed to extract color:', err);
setError(err instanceof Error ? err.message : 'Failed to extract color');
const fallbackColor = '#1a1a1a';
colorCache.set(uri, fallbackColor); // Cache fallback to avoid repeated failures
setDominantColor(fallbackColor);
safelySetColor(fallbackColor);
} finally {
setLoading(false);
// loading already set to false after fast pass
}
}, []);
@ -220,18 +250,18 @@ export const useDominantColor = (imageUri: string | null): DominantColorResult =
if (imageUri) {
// If we have a cached color, use it immediately, but still extract in background for accuracy
if (colorCache.has(imageUri)) {
setDominantColor(colorCache.get(imageUri)!);
safelySetColor(colorCache.get(imageUri)!);
setLoading(false);
} else {
// No cache, extract color
extractColor(imageUri);
}
} else {
setDominantColor('#1a1a1a');
safelySetColor('#1a1a1a');
setLoading(false);
setError(null);
}
}, [imageUri, extractColor]);
}, [imageUri, extractColor, safelySetColor]);
return {
dominantColor,

View file

@ -30,7 +30,9 @@ import Animated, {
useSharedValue,
withTiming,
runOnJS,
runOnUI,
Easing,
interpolateColor,
} from 'react-native-reanimated';
import { RouteProp } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
@ -116,30 +118,59 @@ const MetadataScreen: React.FC = () => {
const { dominantColor, loading: colorLoading } = useDominantColor(heroImageUri);
// Create a shared value for animated background color transitions
const backgroundColorShared = useSharedValue(currentTheme.colors.darkBackground);
// Create shared values for smooth color interpolation
const bgFromColor = useSharedValue(currentTheme.colors.darkBackground);
const bgToColor = useSharedValue(currentTheme.colors.darkBackground);
const bgProgress = useSharedValue(1);
// Update the shared value when dominant color changes
const hasAnimatedInitialColorRef = useRef(false);
useEffect(() => {
if (dominantColor && dominantColor !== '#1a1a1a' && dominantColor !== null && dominantColor !== currentTheme.colors.darkBackground) {
// Smoothly transition to the new color
backgroundColorShared.value = withTiming(dominantColor, {
duration: 300, // Faster appearance
easing: Easing.out(Easing.cubic), // Smooth out easing
});
} else {
// Transition back to theme background if needed
backgroundColorShared.value = withTiming(currentTheme.colors.darkBackground, {
duration: 300,
easing: Easing.out(Easing.cubic),
const base = currentTheme.colors.darkBackground;
const target = (dominantColor && dominantColor !== '#1a1a1a' && dominantColor !== null)
? dominantColor
: base;
if (!hasAnimatedInitialColorRef.current) {
// Initial: animate from base to target smoothly
bgFromColor.value = base as any;
bgToColor.value = target as any;
bgProgress.value = 0;
bgProgress.value = withTiming(1, {
duration: 420,
easing: Easing.bezier(0.16, 1, 0.3, 1),
});
hasAnimatedInitialColorRef.current = true;
return;
}
// Subsequent updates: retarget smoothly from the current on-screen color
runOnUI(() => {
'worklet';
const current = interpolateColor(
bgProgress.value,
[0, 1],
[bgFromColor.value as any, bgToColor.value as any]
);
bgFromColor.value = current as any;
bgToColor.value = target as any;
bgProgress.value = 0;
bgProgress.value = withTiming(1, {
duration: 380,
easing: Easing.bezier(0.2, 0, 0, 1),
});
})();
}, [dominantColor, currentTheme.colors.darkBackground]);
// Create an animated style for the background color
const animatedBackgroundStyle = useAnimatedStyle(() => ({
backgroundColor: backgroundColorShared.value,
}));
const animatedBackgroundStyle = useAnimatedStyle(() => {
const color = interpolateColor(
bgProgress.value,
[0, 1],
[bgFromColor.value as any, bgToColor.value as any]
);
return { backgroundColor: color as any };
});
// For compatibility with existing code, maintain the static value as well
const dynamicBackgroundColor = useMemo(() => {

View file

@ -860,6 +860,7 @@ export const StreamsScreen = () => {
year: metadata?.year,
streamProvider: streamProvider,
streamName: streamName,
// Always prefer stream.headers; player will use these for requests
headers: stream.headers || undefined,
id,
type,