mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
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
|
||||
/nuvio-providers
|
||||
/KSPlayer
|
||||
/exobase
|
||||
/exobase
|
||||
ffmpegreadme.md
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
4
android/app/proguard-rules.pro
vendored
4
android/app/proguard-rules.pro
vendored
|
|
@ -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.**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ interface PlayerControlsProps {
|
|||
onSlidingComplete: (value: number) => void;
|
||||
buffered: number;
|
||||
formatTime: (seconds: number) => string;
|
||||
playerBackend?: string;
|
||||
}
|
||||
|
||||
export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||
|
|
@ -74,6 +75,7 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|||
onSlidingComplete,
|
||||
buffered,
|
||||
formatTime,
|
||||
playerBackend,
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
return (
|
||||
|
|
@ -128,6 +130,11 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|||
{year && <Text style={styles.metadataText}>{year}</Text>}
|
||||
{streamName && <Text style={styles.providerText}>via {streamName}</Text>}
|
||||
</View>
|
||||
{playerBackend && (
|
||||
<View style={styles.metadataRow}>
|
||||
<Text style={[styles.providerText, { fontSize: 11, opacity: 0.9 }]}>{playerBackend}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
|
||||
<Ionicons name="close" size={24} color="white" />
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
>
|
||||
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
|
||||
<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>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
@ -1801,7 +1783,7 @@ export const StreamsScreen = () => {
|
|||
</View>
|
||||
)}
|
||||
|
||||
{(type === 'series' || (type === 'other' && selectedEpisode)) && (
|
||||
{metadata?.videos && metadata.videos.length > 1 && selectedEpisode && (
|
||||
<View style={[styles.streamsHeroContainer]}>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
<View
|
||||
|
|
|
|||
Loading…
Reference in a new issue