mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +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 [showSourcesModal, setShowSourcesModal] = useState<boolean>(false);
|
||||||
const [availableStreams, setAvailableStreams] = useState<{ [providerId: string]: { streams: any[]; addonName: string } }>(passedAvailableStreams || {});
|
const [availableStreams, setAvailableStreams] = useState<{ [providerId: string]: { streams: any[]; addonName: string } }>(passedAvailableStreams || {});
|
||||||
const [currentStreamUrl, setCurrentStreamUrl] = useState<string>(uri);
|
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 [isChangingSource, setIsChangingSource] = useState<boolean>(false);
|
||||||
const [pendingSeek, setPendingSeek] = useState<{ position: number; shouldPlay: boolean } | null>(null);
|
const [pendingSeek, setPendingSeek] = useState<{ position: number; shouldPlay: boolean } | null>(null);
|
||||||
const [currentQuality, setCurrentQuality] = useState<string | undefined>(quality);
|
const [currentQuality, setCurrentQuality] = useState<string | undefined>(quality);
|
||||||
|
|
@ -781,11 +783,41 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for specific AVFoundation server configuration errors
|
// Detect Xprime provider to enable a one-shot silent retry (warms upstream/cache)
|
||||||
const isServerConfigError = error?.error?.code === -11850 ||
|
const providerName = ((currentStreamProvider || streamProvider || '') as string).toLowerCase();
|
||||||
error?.code === -11850 ||
|
const isXprimeProvider = providerName.includes('xprime');
|
||||||
(error?.error?.localizedDescription &&
|
|
||||||
error.error.localizedDescription.includes('server is not correctly configured'));
|
// 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
|
// Format error details for user display
|
||||||
let errorMessage = 'An unknown error occurred';
|
let errorMessage = 'An unknown error occurred';
|
||||||
|
|
@ -1488,17 +1520,7 @@ 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={(() => {
|
source={{ uri: currentStreamUrl, headers: headers || {} }}
|
||||||
// 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;
|
|
||||||
})()}
|
|
||||||
paused={paused}
|
paused={paused}
|
||||||
onProgress={handleProgress}
|
onProgress={handleProgress}
|
||||||
onLoad={(e) => {
|
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 { getColors } from 'react-native-image-colors';
|
||||||
import type { ImageColorsResult } 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);
|
console.log('[useDominantColor] Preloading color for URI:', imageUri);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Fast first-pass: prioritize speed to avoid visible delay
|
||||||
const result = await getColors(imageUri, {
|
const result = await getColors(imageUri, {
|
||||||
fallback: '#1a1a1a',
|
fallback: '#1a1a1a',
|
||||||
cache: true,
|
cache: true,
|
||||||
key: imageUri,
|
key: imageUri,
|
||||||
quality: 'high', // Use higher quality for better color extraction
|
quality: 'low', // Faster extraction
|
||||||
pixelSpacing: 3, // Better sampling (Android only)
|
pixelSpacing: 5, // Fewer sampled pixels (Android only)
|
||||||
});
|
});
|
||||||
|
|
||||||
const extractedColor = selectBestColor(result);
|
const extractedColor = selectBestColor(result);
|
||||||
|
|
@ -172,10 +173,18 @@ export const useDominantColor = (imageUri: string | null): DominantColorResult =
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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) => {
|
const extractColor = useCallback(async (uri: string) => {
|
||||||
if (!uri) {
|
if (!uri) {
|
||||||
setDominantColor('#1a1a1a');
|
safelySetColor('#1a1a1a');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -183,7 +192,7 @@ export const useDominantColor = (imageUri: string | null): DominantColorResult =
|
||||||
// Check cache first
|
// Check cache first
|
||||||
if (colorCache.has(uri)) {
|
if (colorCache.has(uri)) {
|
||||||
const cachedColor = colorCache.get(uri)!;
|
const cachedColor = colorCache.get(uri)!;
|
||||||
setDominantColor(cachedColor);
|
safelySetColor(cachedColor);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -192,27 +201,48 @@ export const useDominantColor = (imageUri: string | null): DominantColorResult =
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
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',
|
fallback: '#1a1a1a',
|
||||||
cache: true,
|
cache: true,
|
||||||
key: uri,
|
key: uri,
|
||||||
quality: 'high', // Use higher quality for better accuracy
|
quality: 'low', // Fastest available
|
||||||
pixelSpacing: 3, // Better pixel sampling (Android only)
|
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
|
// 2) Optional high-quality refine in background
|
||||||
colorCache.set(uri, extractedColor);
|
// Only refine if URI is still the same when this completes
|
||||||
setDominantColor(extractedColor);
|
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) {
|
} catch (err) {
|
||||||
console.warn('[useDominantColor] Failed to extract color:', err);
|
console.warn('[useDominantColor] Failed to extract color:', err);
|
||||||
setError(err instanceof Error ? err.message : 'Failed to extract color');
|
setError(err instanceof Error ? err.message : 'Failed to extract color');
|
||||||
const fallbackColor = '#1a1a1a';
|
const fallbackColor = '#1a1a1a';
|
||||||
colorCache.set(uri, fallbackColor); // Cache fallback to avoid repeated failures
|
colorCache.set(uri, fallbackColor); // Cache fallback to avoid repeated failures
|
||||||
setDominantColor(fallbackColor);
|
safelySetColor(fallbackColor);
|
||||||
} finally {
|
} 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 (imageUri) {
|
||||||
// If we have a cached color, use it immediately, but still extract in background for accuracy
|
// If we have a cached color, use it immediately, but still extract in background for accuracy
|
||||||
if (colorCache.has(imageUri)) {
|
if (colorCache.has(imageUri)) {
|
||||||
setDominantColor(colorCache.get(imageUri)!);
|
safelySetColor(colorCache.get(imageUri)!);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} else {
|
} else {
|
||||||
// No cache, extract color
|
// No cache, extract color
|
||||||
extractColor(imageUri);
|
extractColor(imageUri);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setDominantColor('#1a1a1a');
|
safelySetColor('#1a1a1a');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
}, [imageUri, extractColor]);
|
}, [imageUri, extractColor, safelySetColor]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dominantColor,
|
dominantColor,
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,9 @@ import Animated, {
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
withTiming,
|
withTiming,
|
||||||
runOnJS,
|
runOnJS,
|
||||||
|
runOnUI,
|
||||||
Easing,
|
Easing,
|
||||||
|
interpolateColor,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { RouteProp } from '@react-navigation/native';
|
import { RouteProp } from '@react-navigation/native';
|
||||||
import { NavigationProp } from '@react-navigation/native';
|
import { NavigationProp } from '@react-navigation/native';
|
||||||
|
|
@ -116,30 +118,59 @@ const MetadataScreen: React.FC = () => {
|
||||||
|
|
||||||
const { dominantColor, loading: colorLoading } = useDominantColor(heroImageUri);
|
const { dominantColor, loading: colorLoading } = useDominantColor(heroImageUri);
|
||||||
|
|
||||||
// Create a shared value for animated background color transitions
|
// Create shared values for smooth color interpolation
|
||||||
const backgroundColorShared = useSharedValue(currentTheme.colors.darkBackground);
|
const bgFromColor = useSharedValue(currentTheme.colors.darkBackground);
|
||||||
|
const bgToColor = useSharedValue(currentTheme.colors.darkBackground);
|
||||||
|
const bgProgress = useSharedValue(1);
|
||||||
|
|
||||||
// Update the shared value when dominant color changes
|
// Update the shared value when dominant color changes
|
||||||
|
const hasAnimatedInitialColorRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dominantColor && dominantColor !== '#1a1a1a' && dominantColor !== null && dominantColor !== currentTheme.colors.darkBackground) {
|
const base = currentTheme.colors.darkBackground;
|
||||||
// Smoothly transition to the new color
|
const target = (dominantColor && dominantColor !== '#1a1a1a' && dominantColor !== null)
|
||||||
backgroundColorShared.value = withTiming(dominantColor, {
|
? dominantColor
|
||||||
duration: 300, // Faster appearance
|
: base;
|
||||||
easing: Easing.out(Easing.cubic), // Smooth out easing
|
|
||||||
});
|
if (!hasAnimatedInitialColorRef.current) {
|
||||||
} else {
|
// Initial: animate from base to target smoothly
|
||||||
// Transition back to theme background if needed
|
bgFromColor.value = base as any;
|
||||||
backgroundColorShared.value = withTiming(currentTheme.colors.darkBackground, {
|
bgToColor.value = target as any;
|
||||||
duration: 300,
|
bgProgress.value = 0;
|
||||||
easing: Easing.out(Easing.cubic),
|
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]);
|
}, [dominantColor, currentTheme.colors.darkBackground]);
|
||||||
|
|
||||||
// Create an animated style for the background color
|
// Create an animated style for the background color
|
||||||
const animatedBackgroundStyle = useAnimatedStyle(() => ({
|
const animatedBackgroundStyle = useAnimatedStyle(() => {
|
||||||
backgroundColor: backgroundColorShared.value,
|
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
|
// For compatibility with existing code, maintain the static value as well
|
||||||
const dynamicBackgroundColor = useMemo(() => {
|
const dynamicBackgroundColor = useMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -860,6 +860,7 @@ export const StreamsScreen = () => {
|
||||||
year: metadata?.year,
|
year: metadata?.year,
|
||||||
streamProvider: streamProvider,
|
streamProvider: streamProvider,
|
||||||
streamName: streamName,
|
streamName: streamName,
|
||||||
|
// Always prefer stream.headers; player will use these for requests
|
||||||
headers: stream.headers || undefined,
|
headers: stream.headers || undefined,
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue