diff --git a/.gitignore b/.gitignore index c3be47b..bbb0b26 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,5 @@ hackintosh-emulator-fix.sh src/screens/xavio.md /nuvio-providers /KSPlayer -/exobase \ No newline at end of file +/exobase +ffmpegreadme.md diff --git a/android/app/build.gradle b/android/app/build.gradle index 8b423f8..c3f84f8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -184,4 +184,7 @@ dependencies { } else { implementation jscFlavor } + + // Include only FFmpeg decoder AAR to avoid duplicates with Maven Media3 + implementation files("libs/lib-decoder-ffmpeg-release.aar") } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 551eb41..79923cc 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -12,3 +12,7 @@ -keep class com.facebook.react.turbomodule.** { *; } # Add any project specific keep options here: + +# Media3 / ExoPlayer keep (extensions and reflection) +-keep class androidx.media3.** { *; } +-dontwarn androidx.media3.** diff --git a/android/gradle.properties b/android/gradle.properties index 939f1d9..bc2e641 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -55,6 +55,7 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true # Use legacy packaging to compress native libraries in the resulting APK. expo.useLegacyPackaging=false android.minSdkVersion=26 +RNVideo_media3Version=1.8.0 # Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin expo.edgeToEdgeEnabled=false \ No newline at end of file diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 7fa7bd2..7b4287f 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -84,16 +84,17 @@ const AndroidVideoPlayer: React.FC = () => { const v = rp.forceVlc !== undefined ? rp.forceVlc : rp.forceVLC; return typeof v === 'string' ? v.toLowerCase() === 'true' : Boolean(v); }, [route.params]); - // TEMP toggle disabled; rely on route param forceVlc + // TEMP: force React Native Video for testing (disable VLC) + const TEMP_FORCE_RNV = false; const TEMP_FORCE_VLC = false; - const useVLC = Platform.OS === 'android' && (TEMP_FORCE_VLC || forceVlc); + const useVLC = Platform.OS === 'android' && !TEMP_FORCE_RNV && (TEMP_FORCE_VLC || forceVlc); // Log player selection useEffect(() => { const playerType = useVLC ? 'VLC (expo-libvlc-player)' : 'React Native Video'; const reason = useVLC ? (TEMP_FORCE_VLC ? 'TEMP_FORCE_VLC=true' : `forceVlc=${forceVlc} from route params`) - : 'default react-native-video'; + : (TEMP_FORCE_RNV ? 'TEMP_FORCE_RNV=true' : 'default react-native-video'); logger.log(`[AndroidVideoPlayer] Player selection: ${playerType} (${reason})`); }, [useVLC, forceVlc]); @@ -3397,6 +3398,7 @@ const AndroidVideoPlayer: React.FC = () => { onSlidingComplete={handleSlidingComplete} buffered={buffered} formatTime={formatTime} + playerBackend={useVLC ? 'VLC' : 'ExoPlayer'} /> {showPauseOverlay && ( diff --git a/src/components/player/controls/PlayerControls.tsx b/src/components/player/controls/PlayerControls.tsx index 16d6d43..c963cc2 100644 --- a/src/components/player/controls/PlayerControls.tsx +++ b/src/components/player/controls/PlayerControls.tsx @@ -40,6 +40,7 @@ interface PlayerControlsProps { onSlidingComplete: (value: number) => void; buffered: number; formatTime: (seconds: number) => string; + playerBackend?: string; } export const PlayerControls: React.FC = ({ @@ -74,6 +75,7 @@ export const PlayerControls: React.FC = ({ onSlidingComplete, buffered, formatTime, + playerBackend, }) => { const { currentTheme } = useTheme(); return ( @@ -128,6 +130,11 @@ export const PlayerControls: React.FC = ({ {year && {year}} {streamName && via {streamName}} + {playerBackend && ( + + {playerBackend} + + )} diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 4c34317..98c3eff 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -666,7 +666,7 @@ export const StreamsScreen = () => { // Skip processing if component is unmounting if (!isMounted.current) return; - const currentStreamsData = (type === 'series' || (type === 'other' && selectedEpisode)) ? episodeStreams : groupedStreams; + const currentStreamsData = metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? episodeStreams : groupedStreams; if (__DEV__) console.log('[StreamsScreen] streams state changed', { providerKeys: Object.keys(currentStreamsData || {}), type }); // Update available providers immediately when streams change @@ -720,7 +720,7 @@ export const StreamsScreen = () => { } // Check if provider exists in current streams data - const currentStreamsData = (type === 'series' || (type === 'other' && selectedEpisode)) ? episodeStreams : groupedStreams; + const currentStreamsData = metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? episodeStreams : groupedStreams; const hasStreamsForProvider = currentStreamsData[selectedProvider] && currentStreamsData[selectedProvider].streams && currentStreamsData[selectedProvider].streams.length > 0; @@ -762,7 +762,7 @@ export const StreamsScreen = () => { }, 500); return () => clearTimeout(timer); } else { - if ((type === 'series' || type === 'other') && episodeId) { + if (metadata?.videos && metadata.videos.length > 1 && episodeId) { logger.log(`🎬 Loading episode streams for: ${episodeId}`); setLoadingProviders({ 'stremio': true @@ -776,7 +776,7 @@ export const StreamsScreen = () => { setStreamsLoadStart(Date.now()); if (__DEV__) console.log('[StreamsScreen] calling loadStreams (movie)', id); loadStreams(); - } else if ((type === 'series' || type === 'other') && !episodeId) { + } else if (metadata?.videos && metadata.videos.length > 1 && !episodeId) { // Series with no episodes (e.g., TV/live channels) – fetch streams directly logger.log(`🎬 Loading series streams (no episodes) for: ${id}`); setLoadingProviders({ @@ -1007,26 +1007,8 @@ export const StreamsScreen = () => { const streamName = stream.name || stream.title || 'Unnamed Stream'; const streamProvider = stream.addonId || stream.addonName || stream.name; - // Decide if we should force VLC on Android based on scraper format or URL/content-type + // Do NOT pre-force VLC. Let ExoPlayer try first; fallback occurs on decoder error in the player. let forceVlc = !!options?.forceVlc; - try { - const providerId = stream.addonId || (stream as any).addon || ''; - // If provider declares MKV support in manifest, prefer VLC - if (Platform.OS === 'android' && providerId) { - const supportsMkv = await localScraperService.supportsFormat(providerId, 'mkv'); - if (supportsMkv) forceVlc = true; - } - // URL/content-type heuristic - if (!forceVlc) { - const lowerUrl = (stream.url || '').toLowerCase(); - const isMkvByPath = lowerUrl.includes('.mkv') || /[?&]ext=mkv\b/i.test(lowerUrl) || /format=mkv\b/i.test(lowerUrl) || /container=mkv\b/i.test(lowerUrl); - const contentType = (stream.headers && ((stream.headers as any)['Content-Type'] || (stream.headers as any)['content-type'])) || ''; - const isMkvByHeader = typeof contentType === 'string' && /matroska|x-matroska/i.test(contentType); - if (Platform.OS === 'android' && (isMkvByPath || isMkvByHeader)) { - forceVlc = true; - } - } - } catch {} // Show a quick full-screen black overlay to mask rotation flicker @@ -1309,7 +1291,7 @@ export const StreamsScreen = () => { !autoplayTriggered && isAutoplayWaiting ) { - const streams = (type === 'series' || (type === 'other' && selectedEpisode)) ? episodeStreams : groupedStreams; + const streams = metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? episodeStreams : groupedStreams; if (Object.keys(streams).length > 0) { const bestStream = getBestStream(streams); @@ -1340,7 +1322,7 @@ export const StreamsScreen = () => { const filterItems = useMemo(() => { const installedAddons = stremioService.getInstalledAddons(); - const streams = (type === 'series' || (type === 'other' && selectedEpisode)) ? episodeStreams : groupedStreams; + const streams = metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? episodeStreams : groupedStreams; // Make sure we include all providers with streams, not just those in availableProviders const allProviders = new Set([ @@ -1418,7 +1400,7 @@ export const StreamsScreen = () => { }, [availableProviders, type, episodeStreams, groupedStreams, settings.streamDisplayMode]); const sections = useMemo(() => { - const streams = (type === 'series' || (type === 'other' && selectedEpisode)) ? episodeStreams : groupedStreams; + const streams = metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? episodeStreams : groupedStreams; const installedAddons = stremioService.getInstalledAddons(); // Filter streams by selected provider @@ -1710,8 +1692,8 @@ export const StreamsScreen = () => { }); }, [episodeImage, bannerImage, metadata]); - const isLoading = (type === 'series' || (type === 'other' && selectedEpisode)) ? loadingEpisodeStreams : loadingStreams; - const streams = (type === 'series' || (type === 'other' && selectedEpisode)) ? episodeStreams : groupedStreams; + const isLoading = metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? loadingEpisodeStreams : loadingStreams; + const streams = metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? episodeStreams : groupedStreams; // Determine extended loading phases const streamsEmpty = Object.keys(streams).length === 0; @@ -1776,7 +1758,7 @@ export const StreamsScreen = () => { > - {(type === 'series' || (type === 'other' && selectedEpisode)) ? 'Back to Episodes' : 'Back to Info'} + {metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? 'Back to Episodes' : 'Back to Info'} @@ -1801,7 +1783,7 @@ export const StreamsScreen = () => { )} - {(type === 'series' || (type === 'other' && selectedEpisode)) && ( + {metadata?.videos && metadata.videos.length > 1 && selectedEpisode && (