mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
fixed xprime initial loading issue
This commit is contained in:
parent
675e3c24a4
commit
81373a2bb2
4 changed files with 133 additions and 49 deletions
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue