From 668099b5426ed0ed00624a3aac8ba50e75693e0e Mon Sep 17 00:00:00 2001 From: tapframe Date: Fri, 21 Nov 2025 16:09:54 +0530 Subject: [PATCH] hdr issue fix ios --- ios/KSPlayerView.swift | 42 +++-------------- src/components/home/ContentItem.tsx | 4 +- src/components/metadata/HeroSection.tsx | 60 +++++++++++++------------ src/hooks/useMetadata.ts | 3 +- src/services/catalogService.ts | 12 ++++- 5 files changed, 52 insertions(+), 69 deletions(-) diff --git a/ios/KSPlayerView.swift b/ios/KSPlayerView.swift index 19469e7..cda64d5 100644 --- a/ios/KSPlayerView.swift +++ b/ios/KSPlayerView.swift @@ -385,9 +385,11 @@ class KSPlayerView: UIView { options.asynchronousDecompression = true #endif - // PERFORMANCE OPTIMIZATION: Native HDR processing - // Set destination dynamic range based on device capabilities to eliminate unnecessary color conversions - options.destinationDynamicRange = getOptimalDynamicRange() + // HDR handling: Let KSPlayer automatically detect content's native dynamic range + // Setting destinationDynamicRange to nil allows KSPlayer to use the content's actual HDR/SDR mode + // This prevents forcing HDR tone mapping on SDR content (which causes oversaturation) + // KSPlayer will automatically detect HDR10/Dolby Vision/HLG from the video format description + options.destinationDynamicRange = nil // Configure audio for proper dialogue mixing using FFmpeg's pan filter // This approach uses standard audio engineering practices for multi-channel downmixing @@ -804,40 +806,6 @@ class KSPlayerView: UIView { } // MARK: - Performance Optimization Helpers - - /// Detects device HDR capabilities and returns optimal dynamic range setting - /// This prevents unnecessary color space conversion overhead - private func getOptimalDynamicRange() -> DynamicRange? { - #if canImport(UIKit) - let availableHDRModes = AVPlayer.availableHDRModes - - // If no HDR modes available, use SDR (nil will use content's native range) - if availableHDRModes == AVPlayer.HDRMode(rawValue: 0) { - return .sdr - } - - // Prefer HDR10 if supported (most common HDR format) - if availableHDRModes.contains(.hdr10) { - return .hdr10 - } - - // Fallback to Dolby Vision if available - if availableHDRModes.contains(.dolbyVision) { - return .dolbyVision - } - - // Fallback to HLG if available - if availableHDRModes.contains(.hlg) { - return .hlg - } - - // Default to SDR if no HDR support - return .sdr - #else - // macOS: Check screen capabilities - return .sdr - #endif - } } // MARK: - High Performance KSOptions Subclass diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx index 7aa251a..ef6aeb9 100644 --- a/src/components/home/ContentItem.tsx +++ b/src/components/home/ContentItem.tsx @@ -89,7 +89,9 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe // Subscribe to library updates and update local state if this item's status changes const unsubscribe = catalogService.subscribeToLibraryUpdates((items) => { const found = items.find((libItem) => libItem.id === item.id && libItem.type === item.type); - setInLibrary(!!found); + const newInLibrary = !!found; + // Only update state if the value actually changed to prevent unnecessary re-renders + setInLibrary(prev => prev !== newInLibrary ? newInLibrary : prev); }); return () => unsubscribe(); }, [item.id, item.type]); diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 2a9dd86..e720f74 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -1041,45 +1041,49 @@ const HeroSection: React.FC = memo(({ // Grace delay before showing text fallback to avoid flashing when logo arrives late const [shouldShowTextFallback, setShouldShowTextFallback] = useState(!metadata?.logo); const logoWaitTimerRef = useRef(null); + // Ref to track the last synced logo to break circular dependency with error handling + const lastSyncedLogoRef = useRef(metadata?.logo); // Update stable logo URI when metadata logo changes useEffect(() => { - // Reset text fallback and timers on logo updates - if (logoWaitTimerRef.current) { - try { clearTimeout(logoWaitTimerRef.current); } catch (_e) {} - logoWaitTimerRef.current = null; - } + // Check if metadata logo has actually changed from what we last processed + const currentMetadataLogo = metadata?.logo; + + if (currentMetadataLogo !== lastSyncedLogoRef.current) { + lastSyncedLogoRef.current = currentMetadataLogo; - if (metadata?.logo && metadata.logo !== stableLogoUri) { - setStableLogoUri(metadata.logo); - onStableLogoUriChange?.(metadata.logo); - setLogoHasLoadedSuccessfully(false); // Reset for new logo - logoLoadOpacity.value = 0; // reset fade for new logo - setShouldShowTextFallback(false); - } else if (!metadata?.logo && stableLogoUri) { - // Clear logo if metadata no longer has one - setStableLogoUri(null); - onStableLogoUriChange?.(null); - setLogoHasLoadedSuccessfully(false); - // Start a short grace period before showing text fallback - setShouldShowTextFallback(false); - logoWaitTimerRef.current = setTimeout(() => { - setShouldShowTextFallback(true); - }, 600); - } else if (!metadata?.logo && !stableLogoUri) { - // No logo currently; wait briefly before showing text to avoid flash - setShouldShowTextFallback(false); - logoWaitTimerRef.current = setTimeout(() => { - setShouldShowTextFallback(true); - }, 600); + // Reset text fallback and timers on logo updates + if (logoWaitTimerRef.current) { + try { clearTimeout(logoWaitTimerRef.current); } catch (_e) {} + logoWaitTimerRef.current = null; + } + + if (currentMetadataLogo) { + setStableLogoUri(currentMetadataLogo); + onStableLogoUriChange?.(currentMetadataLogo); + setLogoHasLoadedSuccessfully(false); // Reset for new logo + logoLoadOpacity.value = 0; // reset fade for new logo + setShouldShowTextFallback(false); + } else { + // Clear logo if metadata no longer has one + setStableLogoUri(null); + onStableLogoUriChange?.(null); + setLogoHasLoadedSuccessfully(false); + // Start a short grace period before showing text fallback + setShouldShowTextFallback(false); + logoWaitTimerRef.current = setTimeout(() => { + setShouldShowTextFallback(true); + }, 600); + } } + return () => { if (logoWaitTimerRef.current) { try { clearTimeout(logoWaitTimerRef.current); } catch (_e) {} logoWaitTimerRef.current = null; } }; - }, [metadata?.logo, stableLogoUri]); + }, [metadata?.logo]); // Removed stableLogoUri from dependencies to prevent circular updates on error // Handle logo load success - once loaded successfully, keep it stable const handleLogoLoad = useCallback(() => { diff --git a/src/hooks/useMetadata.ts b/src/hooks/useMetadata.ts index 1279b58..d4f9e48 100644 --- a/src/hooks/useMetadata.ts +++ b/src/hooks/useMetadata.ts @@ -2168,7 +2168,8 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat useEffect(() => { const unsubscribe = catalogService.subscribeToLibraryUpdates((libraryItems) => { const isInLib = libraryItems.some(item => item.id === id); - setInLibrary(isInLib); + // Only update state if the value actually changed to prevent unnecessary re-renders + setInLibrary(prev => prev !== isInLib ? isInLib : prev); }); return () => unsubscribe(); diff --git a/src/services/catalogService.ts b/src/services/catalogService.ts index b6970b2..a174e94 100644 --- a/src/services/catalogService.ts +++ b/src/services/catalogService.ts @@ -928,8 +928,16 @@ class CatalogService { public subscribeToLibraryUpdates(callback: (items: StreamingContent[]) => void): () => void { this.librarySubscribers.push(callback); - // Initial callback with current items - this.getLibraryItems().then(items => callback(items)); + // Defer initial callback to next tick to avoid synchronous state updates during render + // This prevents infinite loops when the callback triggers setState in useEffect + Promise.resolve().then(() => { + this.getLibraryItems().then(items => { + // Only call if still subscribed (callback might have been unsubscribed) + if (this.librarySubscribers.includes(callback)) { + callback(items); + } + }); + }); // Return unsubscribe function return () => {