Added ExoPlayer FFmpeg Extension for better Codec Support
This commit is contained in:
parent
004ee178a4
commit
b8d3d68b65
7 changed files with 34 additions and 34 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -54,4 +54,5 @@ hackintosh-emulator-fix.sh
|
||||||
src/screens/xavio.md
|
src/screens/xavio.md
|
||||||
/nuvio-providers
|
/nuvio-providers
|
||||||
/KSPlayer
|
/KSPlayer
|
||||||
/exobase
|
/exobase
|
||||||
|
ffmpegreadme.md
|
||||||
|
|
|
||||||
|
|
@ -184,4 +184,7 @@ dependencies {
|
||||||
} else {
|
} else {
|
||||||
implementation jscFlavor
|
implementation jscFlavor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include only FFmpeg decoder AAR to avoid duplicates with Maven Media3
|
||||||
|
implementation files("libs/lib-decoder-ffmpeg-release.aar")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
android/app/proguard-rules.pro
vendored
4
android/app/proguard-rules.pro
vendored
|
|
@ -12,3 +12,7 @@
|
||||||
-keep class com.facebook.react.turbomodule.** { *; }
|
-keep class com.facebook.react.turbomodule.** { *; }
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
# Add any project specific keep options here:
|
||||||
|
|
||||||
|
# Media3 / ExoPlayer keep (extensions and reflection)
|
||||||
|
-keep class androidx.media3.** { *; }
|
||||||
|
-dontwarn androidx.media3.**
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true
|
||||||
# Use legacy packaging to compress native libraries in the resulting APK.
|
# Use legacy packaging to compress native libraries in the resulting APK.
|
||||||
expo.useLegacyPackaging=false
|
expo.useLegacyPackaging=false
|
||||||
android.minSdkVersion=26
|
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
|
# Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin
|
||||||
expo.edgeToEdgeEnabled=false
|
expo.edgeToEdgeEnabled=false
|
||||||
|
|
@ -84,16 +84,17 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const v = rp.forceVlc !== undefined ? rp.forceVlc : rp.forceVLC;
|
const v = rp.forceVlc !== undefined ? rp.forceVlc : rp.forceVLC;
|
||||||
return typeof v === 'string' ? v.toLowerCase() === 'true' : Boolean(v);
|
return typeof v === 'string' ? v.toLowerCase() === 'true' : Boolean(v);
|
||||||
}, [route.params]);
|
}, [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 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
|
// Log player selection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const playerType = useVLC ? 'VLC (expo-libvlc-player)' : 'React Native Video';
|
const playerType = useVLC ? 'VLC (expo-libvlc-player)' : 'React Native Video';
|
||||||
const reason = useVLC
|
const reason = useVLC
|
||||||
? (TEMP_FORCE_VLC ? 'TEMP_FORCE_VLC=true' : `forceVlc=${forceVlc} from route params`)
|
? (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})`);
|
logger.log(`[AndroidVideoPlayer] Player selection: ${playerType} (${reason})`);
|
||||||
}, [useVLC, forceVlc]);
|
}, [useVLC, forceVlc]);
|
||||||
|
|
||||||
|
|
@ -3397,6 +3398,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
onSlidingComplete={handleSlidingComplete}
|
onSlidingComplete={handleSlidingComplete}
|
||||||
buffered={buffered}
|
buffered={buffered}
|
||||||
formatTime={formatTime}
|
formatTime={formatTime}
|
||||||
|
playerBackend={useVLC ? 'VLC' : 'ExoPlayer'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showPauseOverlay && (
|
{showPauseOverlay && (
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ interface PlayerControlsProps {
|
||||||
onSlidingComplete: (value: number) => void;
|
onSlidingComplete: (value: number) => void;
|
||||||
buffered: number;
|
buffered: number;
|
||||||
formatTime: (seconds: number) => string;
|
formatTime: (seconds: number) => string;
|
||||||
|
playerBackend?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||||
|
|
@ -74,6 +75,7 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||||
onSlidingComplete,
|
onSlidingComplete,
|
||||||
buffered,
|
buffered,
|
||||||
formatTime,
|
formatTime,
|
||||||
|
playerBackend,
|
||||||
}) => {
|
}) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
return (
|
return (
|
||||||
|
|
@ -128,6 +130,11 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||||
{year && <Text style={styles.metadataText}>{year}</Text>}
|
{year && <Text style={styles.metadataText}>{year}</Text>}
|
||||||
{streamName && <Text style={styles.providerText}>via {streamName}</Text>}
|
{streamName && <Text style={styles.providerText}>via {streamName}</Text>}
|
||||||
</View>
|
</View>
|
||||||
|
{playerBackend && (
|
||||||
|
<View style={styles.metadataRow}>
|
||||||
|
<Text style={[styles.providerText, { fontSize: 11, opacity: 0.9 }]}>{playerBackend}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
|
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
|
||||||
<Ionicons name="close" size={24} color="white" />
|
<Ionicons name="close" size={24} color="white" />
|
||||||
|
|
|
||||||
|
|
@ -666,7 +666,7 @@ export const StreamsScreen = () => {
|
||||||
// Skip processing if component is unmounting
|
// Skip processing if component is unmounting
|
||||||
if (!isMounted.current) return;
|
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 });
|
if (__DEV__) console.log('[StreamsScreen] streams state changed', { providerKeys: Object.keys(currentStreamsData || {}), type });
|
||||||
|
|
||||||
// Update available providers immediately when streams change
|
// Update available providers immediately when streams change
|
||||||
|
|
@ -720,7 +720,7 @@ export const StreamsScreen = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if provider exists in current streams data
|
// 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] &&
|
const hasStreamsForProvider = currentStreamsData[selectedProvider] &&
|
||||||
currentStreamsData[selectedProvider].streams &&
|
currentStreamsData[selectedProvider].streams &&
|
||||||
currentStreamsData[selectedProvider].streams.length > 0;
|
currentStreamsData[selectedProvider].streams.length > 0;
|
||||||
|
|
@ -762,7 +762,7 @@ export const StreamsScreen = () => {
|
||||||
}, 500);
|
}, 500);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
} else {
|
} else {
|
||||||
if ((type === 'series' || type === 'other') && episodeId) {
|
if (metadata?.videos && metadata.videos.length > 1 && episodeId) {
|
||||||
logger.log(`🎬 Loading episode streams for: ${episodeId}`);
|
logger.log(`🎬 Loading episode streams for: ${episodeId}`);
|
||||||
setLoadingProviders({
|
setLoadingProviders({
|
||||||
'stremio': true
|
'stremio': true
|
||||||
|
|
@ -776,7 +776,7 @@ export const StreamsScreen = () => {
|
||||||
setStreamsLoadStart(Date.now());
|
setStreamsLoadStart(Date.now());
|
||||||
if (__DEV__) console.log('[StreamsScreen] calling loadStreams (movie)', id);
|
if (__DEV__) console.log('[StreamsScreen] calling loadStreams (movie)', id);
|
||||||
loadStreams();
|
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
|
// Series with no episodes (e.g., TV/live channels) – fetch streams directly
|
||||||
logger.log(`🎬 Loading series streams (no episodes) for: ${id}`);
|
logger.log(`🎬 Loading series streams (no episodes) for: ${id}`);
|
||||||
setLoadingProviders({
|
setLoadingProviders({
|
||||||
|
|
@ -1007,26 +1007,8 @@ export const StreamsScreen = () => {
|
||||||
const streamName = stream.name || stream.title || 'Unnamed Stream';
|
const streamName = stream.name || stream.title || 'Unnamed Stream';
|
||||||
const streamProvider = stream.addonId || stream.addonName || stream.name;
|
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;
|
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
|
// Show a quick full-screen black overlay to mask rotation flicker
|
||||||
|
|
@ -1309,7 +1291,7 @@ export const StreamsScreen = () => {
|
||||||
!autoplayTriggered &&
|
!autoplayTriggered &&
|
||||||
isAutoplayWaiting
|
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) {
|
if (Object.keys(streams).length > 0) {
|
||||||
const bestStream = getBestStream(streams);
|
const bestStream = getBestStream(streams);
|
||||||
|
|
@ -1340,7 +1322,7 @@ export const StreamsScreen = () => {
|
||||||
|
|
||||||
const filterItems = useMemo(() => {
|
const filterItems = useMemo(() => {
|
||||||
const installedAddons = stremioService.getInstalledAddons();
|
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
|
// Make sure we include all providers with streams, not just those in availableProviders
|
||||||
const allProviders = new Set([
|
const allProviders = new Set([
|
||||||
|
|
@ -1418,7 +1400,7 @@ export const StreamsScreen = () => {
|
||||||
}, [availableProviders, type, episodeStreams, groupedStreams, settings.streamDisplayMode]);
|
}, [availableProviders, type, episodeStreams, groupedStreams, settings.streamDisplayMode]);
|
||||||
|
|
||||||
const sections = useMemo(() => {
|
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();
|
const installedAddons = stremioService.getInstalledAddons();
|
||||||
|
|
||||||
// Filter streams by selected provider
|
// Filter streams by selected provider
|
||||||
|
|
@ -1710,8 +1692,8 @@ export const StreamsScreen = () => {
|
||||||
});
|
});
|
||||||
}, [episodeImage, bannerImage, metadata]);
|
}, [episodeImage, bannerImage, metadata]);
|
||||||
|
|
||||||
const isLoading = (type === 'series' || (type === 'other' && selectedEpisode)) ? loadingEpisodeStreams : loadingStreams;
|
const isLoading = metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? loadingEpisodeStreams : loadingStreams;
|
||||||
const streams = (type === 'series' || (type === 'other' && selectedEpisode)) ? episodeStreams : groupedStreams;
|
const streams = metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? episodeStreams : groupedStreams;
|
||||||
|
|
||||||
// Determine extended loading phases
|
// Determine extended loading phases
|
||||||
const streamsEmpty = Object.keys(streams).length === 0;
|
const streamsEmpty = Object.keys(streams).length === 0;
|
||||||
|
|
@ -1776,7 +1758,7 @@ export const StreamsScreen = () => {
|
||||||
>
|
>
|
||||||
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
|
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
|
||||||
<Text style={styles.backButtonText}>
|
<Text style={styles.backButtonText}>
|
||||||
{(type === 'series' || (type === 'other' && selectedEpisode)) ? 'Back to Episodes' : 'Back to Info'}
|
{metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? 'Back to Episodes' : 'Back to Info'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -1801,7 +1783,7 @@ export const StreamsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(type === 'series' || (type === 'other' && selectedEpisode)) && (
|
{metadata?.videos && metadata.videos.length > 1 && selectedEpisode && (
|
||||||
<View style={[styles.streamsHeroContainer]}>
|
<View style={[styles.streamsHeroContainer]}>
|
||||||
<View style={StyleSheet.absoluteFill}>
|
<View style={StyleSheet.absoluteFill}>
|
||||||
<View
|
<View
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue