mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
Refactor image handling and prefetching logic across multiple components for improved performance and memory management
This update modifies the image handling in ContentItem, ContinueWatchingSection, and FeaturedContent components to utilize a placeholder image and adjust caching strategies. Additionally, the HomeScreen component has been enhanced to limit concurrent image prefetching, reducing memory pressure and improving overall performance. These changes aim to create a smoother user experience while navigating through content.
This commit is contained in:
parent
4a6f349cdb
commit
9ac99ee0a3
6 changed files with 157 additions and 80 deletions
|
|
@ -27,8 +27,11 @@ const ContentItem = ({ item, onPress, width }: ContentItemProps) => {
|
|||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
||||
style={styles.poster}
|
||||
contentFit="cover"
|
||||
cachePolicy="memory-disk"
|
||||
transition={300}
|
||||
cachePolicy="memory"
|
||||
transition={200}
|
||||
placeholder={{ uri: 'https://via.placeholder.com/300x450' }}
|
||||
placeholderContentFit="cover"
|
||||
recyclingKey={item.id}
|
||||
/>
|
||||
<LinearGradient
|
||||
colors={['transparent', 'rgba(0,0,0,0.85)']}
|
||||
|
|
|
|||
|
|
@ -99,12 +99,14 @@ const ContentItem = React.memo(({ item, onPress }: ContentItemProps) => {
|
|||
>
|
||||
<View style={styles.contentItemContainer}>
|
||||
<ExpoImage
|
||||
source={{ uri: item.poster }}
|
||||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
||||
style={styles.poster}
|
||||
contentFit="cover"
|
||||
transition={300}
|
||||
cachePolicy="memory-disk"
|
||||
recyclingKey={`poster-${item.id}`}
|
||||
cachePolicy="memory"
|
||||
transition={200}
|
||||
placeholder={{ uri: 'https://via.placeholder.com/300x450' }}
|
||||
placeholderContentFit="cover"
|
||||
recyclingKey={item.id}
|
||||
onLoadStart={() => {
|
||||
setImageLoaded(false);
|
||||
setImageError(false);
|
||||
|
|
|
|||
|
|
@ -295,11 +295,14 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
{/* Poster Image */}
|
||||
<View style={styles.posterContainer}>
|
||||
<ExpoImage
|
||||
source={{ uri: item.poster }}
|
||||
style={styles.widePoster}
|
||||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
||||
style={styles.continueWatchingPoster}
|
||||
contentFit="cover"
|
||||
cachePolicy="memory"
|
||||
transition={200}
|
||||
cachePolicy="memory-disk"
|
||||
placeholder={{ uri: 'https://via.placeholder.com/300x450' }}
|
||||
placeholderContentFit="cover"
|
||||
recyclingKey={item.id}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
|
@ -429,7 +432,7 @@ const styles = StyleSheet.create({
|
|||
width: 80,
|
||||
height: '100%',
|
||||
},
|
||||
widePoster: {
|
||||
continueWatchingPoster: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderTopLeftRadius: 12,
|
||||
|
|
|
|||
|
|
@ -99,16 +99,35 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
|
|||
|
||||
// Preload the image
|
||||
const preloadImage = async (url: string): Promise<boolean> => {
|
||||
if (!url) return false;
|
||||
// Skip if already cached to prevent redundant prefetch
|
||||
if (imageCache[url]) return true;
|
||||
|
||||
try {
|
||||
// For Metahub logos, only do validation if enabled
|
||||
// Note: Temporarily disable metahub validation until fixed
|
||||
if (false && url.includes('metahub.space')) {
|
||||
// Basic URL validation
|
||||
if (!url || typeof url !== 'string') return false;
|
||||
|
||||
// Check if URL appears to be a valid image URL
|
||||
const urlLower = url.toLowerCase();
|
||||
const hasImageExtension = /\.(jpg|jpeg|png|webp|svg)(\?.*)?$/i.test(url);
|
||||
const isImageService = urlLower.includes('image') || urlLower.includes('poster') || urlLower.includes('banner') || urlLower.includes('logo');
|
||||
|
||||
if (!hasImageExtension && !isImageService) {
|
||||
try {
|
||||
const isValid = await isValidMetahubLogo(url);
|
||||
if (!isValid) {
|
||||
// For URLs without clear image extensions, do a quick HEAD request
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'HEAD',
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) return false;
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.startsWith('image/')) {
|
||||
return false;
|
||||
}
|
||||
} catch (validationError) {
|
||||
|
|
@ -117,10 +136,22 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
|
|||
}
|
||||
|
||||
// Always attempt to prefetch the image regardless of format validation
|
||||
await ExpoImage.prefetch(url);
|
||||
// Add timeout and retry logic for prefetch
|
||||
const prefetchWithTimeout = () => {
|
||||
return Promise.race([
|
||||
ExpoImage.prefetch(url),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Prefetch timeout')), 5000)
|
||||
)
|
||||
]);
|
||||
};
|
||||
|
||||
await prefetchWithTimeout();
|
||||
imageCache[url] = true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Clear any partial cache entry on error
|
||||
delete imageCache[url];
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
@ -423,8 +454,9 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
|
|||
source={{ uri: logoUrl }}
|
||||
style={styles.featuredLogo as ImageStyle}
|
||||
contentFit="contain"
|
||||
cachePolicy="memory-disk"
|
||||
transition={400}
|
||||
cachePolicy="memory"
|
||||
transition={300}
|
||||
recyclingKey={`logo-${featuredContent.id}`}
|
||||
onError={onLogoLoadError}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
|
|
|||
|
|
@ -63,30 +63,30 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) =
|
|||
'worklet';
|
||||
|
||||
try {
|
||||
// Start with slightly reduced values and animate to full visibility
|
||||
screenOpacity.value = withTiming(1, {
|
||||
duration: 250,
|
||||
easing: easings.fast
|
||||
});
|
||||
|
||||
heroOpacity.value = withTiming(1, {
|
||||
duration: 300,
|
||||
easing: easings.fast
|
||||
});
|
||||
|
||||
heroScale.value = withSpring(1, ultraFastSpring);
|
||||
|
||||
uiElementsOpacity.value = withTiming(1, {
|
||||
duration: 400,
|
||||
easing: easings.natural
|
||||
});
|
||||
|
||||
uiElementsTranslateY.value = withSpring(0, fastSpring);
|
||||
|
||||
contentOpacity.value = withTiming(1, {
|
||||
duration: 350,
|
||||
easing: easings.fast
|
||||
});
|
||||
// Start with slightly reduced values and animate to full visibility
|
||||
screenOpacity.value = withTiming(1, {
|
||||
duration: 250,
|
||||
easing: easings.fast
|
||||
});
|
||||
|
||||
heroOpacity.value = withTiming(1, {
|
||||
duration: 300,
|
||||
easing: easings.fast
|
||||
});
|
||||
|
||||
heroScale.value = withSpring(1, ultraFastSpring);
|
||||
|
||||
uiElementsOpacity.value = withTiming(1, {
|
||||
duration: 400,
|
||||
easing: easings.natural
|
||||
});
|
||||
|
||||
uiElementsTranslateY.value = withSpring(0, fastSpring);
|
||||
|
||||
contentOpacity.value = withTiming(1, {
|
||||
duration: 350,
|
||||
easing: easings.fast
|
||||
});
|
||||
} catch (error) {
|
||||
// Silently handle any animation errors
|
||||
console.warn('Animation error in enterAnimations:', error);
|
||||
|
|
@ -95,7 +95,7 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) =
|
|||
|
||||
// Use runOnUI for better performance with error handling
|
||||
try {
|
||||
runOnUI(enterAnimations)();
|
||||
runOnUI(enterAnimations)();
|
||||
} catch (error) {
|
||||
console.warn('Failed to run enter animations:', error);
|
||||
}
|
||||
|
|
@ -109,17 +109,17 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) =
|
|||
'worklet';
|
||||
|
||||
try {
|
||||
progressOpacity.value = withTiming(hasProgress ? 1 : 0, {
|
||||
duration: hasProgress ? 200 : 150,
|
||||
easing: easings.fast
|
||||
});
|
||||
progressOpacity.value = withTiming(hasProgress ? 1 : 0, {
|
||||
duration: hasProgress ? 200 : 150,
|
||||
easing: easings.fast
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Animation error in updateProgress:', error);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
runOnUI(updateProgress)();
|
||||
runOnUI(updateProgress)();
|
||||
} catch (error) {
|
||||
console.warn('Failed to run progress animation:', error);
|
||||
}
|
||||
|
|
@ -151,19 +151,19 @@ export const useMetadataAnimations = (safeAreaTop: number, watchProgress: any) =
|
|||
'worklet';
|
||||
|
||||
try {
|
||||
const rawScrollY = event.contentOffset.y;
|
||||
scrollY.value = rawScrollY;
|
||||
const rawScrollY = event.contentOffset.y;
|
||||
scrollY.value = rawScrollY;
|
||||
|
||||
// Single calculation for header threshold
|
||||
const threshold = height * 0.4 - safeAreaTop;
|
||||
const progress = rawScrollY > threshold ? 1 : 0;
|
||||
|
||||
// Use single progress value for all header animations
|
||||
if (headerProgress.value !== progress) {
|
||||
headerProgress.value = withTiming(progress, {
|
||||
duration: progress ? 200 : 150,
|
||||
easing: easings.ultraFast
|
||||
});
|
||||
// Single calculation for header threshold
|
||||
const threshold = height * 0.4 - safeAreaTop;
|
||||
const progress = rawScrollY > threshold ? 1 : 0;
|
||||
|
||||
// Use single progress value for all header animations
|
||||
if (headerProgress.value !== progress) {
|
||||
headerProgress.value = withTiming(progress, {
|
||||
duration: progress ? 200 : 150,
|
||||
easing: easings.ultraFast
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Animation error in scroll handler:', error);
|
||||
|
|
|
|||
|
|
@ -325,21 +325,40 @@ const HomeScreen = () => {
|
|||
if (!content.length) return;
|
||||
|
||||
try {
|
||||
const imagePromises = content.map(item => {
|
||||
const imagesToLoad = [
|
||||
item.poster,
|
||||
item.banner,
|
||||
item.logo
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
return Promise.all(
|
||||
imagesToLoad.map(imageUrl =>
|
||||
ExpoImage.prefetch(imageUrl)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all(imagePromises);
|
||||
// Limit concurrent prefetching to prevent memory pressure
|
||||
const MAX_CONCURRENT_PREFETCH = 5;
|
||||
const BATCH_SIZE = 3;
|
||||
|
||||
const allImages = content.slice(0, 10) // Limit total images to prefetch
|
||||
.map(item => [item.poster, item.banner, item.logo])
|
||||
.flat()
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
// Process in small batches to prevent memory pressure
|
||||
for (let i = 0; i < allImages.length; i += BATCH_SIZE) {
|
||||
const batch = allImages.slice(i, i + BATCH_SIZE);
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
batch.map(async (imageUrl) => {
|
||||
try {
|
||||
await ExpoImage.prefetch(imageUrl);
|
||||
// Small delay between prefetches to reduce memory pressure
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
} catch (error) {
|
||||
// Silently handle individual prefetch errors
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Delay between batches to allow GC
|
||||
if (i + BATCH_SIZE < allImages.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
} catch (error) {
|
||||
// Continue with next batch if current batch fails
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently handle preload errors
|
||||
}
|
||||
|
|
@ -353,11 +372,27 @@ const HomeScreen = () => {
|
|||
if (!featuredContent) return;
|
||||
|
||||
try {
|
||||
// Lock orientation to landscape before navigation to prevent glitches
|
||||
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
|
||||
// Clear image cache to reduce memory pressure before orientation change
|
||||
if (typeof (global as any)?.ExpoImage?.clearMemoryCache === 'function') {
|
||||
try {
|
||||
(global as any).ExpoImage.clearMemoryCache();
|
||||
} catch (e) {
|
||||
// Ignore cache clear errors
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay to ensure orientation is set before navigation
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
// Lock orientation to landscape before navigation to prevent glitches
|
||||
try {
|
||||
await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE);
|
||||
|
||||
// Longer delay to ensure orientation is fully set before navigation
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
} catch (orientationError) {
|
||||
// If orientation lock fails, continue anyway but log it
|
||||
logger.warn('[HomeScreen] Orientation lock failed:', orientationError);
|
||||
// Still add a small delay
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
navigation.navigate('Player', {
|
||||
uri: stream.url,
|
||||
|
|
@ -369,6 +404,8 @@ const HomeScreen = () => {
|
|||
type: featuredContent.type
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[HomeScreen] Error in handlePlayStream:', error);
|
||||
|
||||
// Fallback: navigate anyway
|
||||
navigation.navigate('Player', {
|
||||
uri: stream.url,
|
||||
|
|
|
|||
Loading…
Reference in a new issue