From 9ac99ee0a3085a5b59806dfd5a00971fe8c98540 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 21 Jun 2025 18:47:46 +0530 Subject: [PATCH] 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. --- src/components/discover/ContentItem.tsx | 7 +- src/components/home/ContentItem.tsx | 10 ++- .../home/ContinueWatchingSection.tsx | 11 ++- src/components/home/FeaturedContent.tsx | 50 +++++++++-- src/hooks/useMetadataAnimations.ts | 84 +++++++++---------- src/screens/HomeScreen.tsx | 75 ++++++++++++----- 6 files changed, 157 insertions(+), 80 deletions(-) diff --git a/src/components/discover/ContentItem.tsx b/src/components/discover/ContentItem.tsx index de22a611..1f7e85d2 100644 --- a/src/components/discover/ContentItem.tsx +++ b/src/components/discover/ContentItem.tsx @@ -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} /> { > { setImageLoaded(false); setImageError(false); diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index f3aef200..f0cae1b4 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -295,11 +295,14 @@ const ContinueWatchingSection = React.forwardRef((props, re {/* Poster Image */} @@ -429,7 +432,7 @@ const styles = StyleSheet.create({ width: 80, height: '100%', }, - widePoster: { + continueWatchingPoster: { width: '100%', height: '100%', borderTopLeftRadius: 12, diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index 3fa762f5..64ebe7bc 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -99,16 +99,35 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat // Preload the image const preloadImage = async (url: string): Promise => { - 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} /> diff --git a/src/hooks/useMetadataAnimations.ts b/src/hooks/useMetadataAnimations.ts index 2adc886c..671e4008 100644 --- a/src/hooks/useMetadataAnimations.ts +++ b/src/hooks/useMetadataAnimations.ts @@ -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); diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 355e6a62..b79efaed 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -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,