Fixed an issue with Container Parsing on AndroidVideoPlayer

This commit is contained in:
tapframe 2025-09-11 13:41:26 +05:30
parent dce3e5c200
commit bc4bf5a963
3 changed files with 60 additions and 4 deletions

View file

@ -74,6 +74,18 @@ const AndroidVideoPlayer: React.FC = () => {
backdrop
} = route.params;
// Optional hint not yet in typed navigator params
const videoType = (route.params as any).videoType as string | undefined;
const defaultAndroidHeaders = () => {
if (Platform.OS !== 'android') return {} as any;
return {
'User-Agent': 'ExoPlayerLib/2.19.1 (Linux;Android) Nuvio/1.0',
'Accept': '*/*',
'Connection': 'keep-alive',
} as any;
};
// Initialize Trakt autosync
const traktAutosync = useTraktAutosync({
id: id || '',
@ -177,6 +189,7 @@ const AndroidVideoPlayer: React.FC = () => {
const [showSourcesModal, setShowSourcesModal] = useState<boolean>(false);
const [availableStreams, setAvailableStreams] = useState<{ [providerId: string]: { streams: any[]; addonName: string } }>(passedAvailableStreams || {});
const [currentStreamUrl, setCurrentStreamUrl] = useState<string>(uri);
const [currentVideoType, setCurrentVideoType] = useState<string | undefined>(videoType);
// Track a single silent retry per source to avoid loops
const retryAttemptRef = useRef<number>(0);
const [isChangingSource, setIsChangingSource] = useState<boolean>(false);
@ -1020,6 +1033,29 @@ const AndroidVideoPlayer: React.FC = () => {
return; // Do not proceed to show error UI
}
// If format unrecognized, try flipping between HLS and MP4 once
const isUnrecognized = !!(error?.error?.errorString && String(error.error.errorString).includes('UnrecognizedInputFormatException'));
if (isUnrecognized && retryAttemptRef.current < 1) {
retryAttemptRef.current = 1;
const nextType = currentVideoType === 'm3u8' ? 'mp4' : 'm3u8';
logger.warn(`[AndroidVideoPlayer] Format not recognized. Retrying with type='${nextType}'`);
if (errorTimeoutRef.current) {
clearTimeout(errorTimeoutRef.current);
errorTimeoutRef.current = null;
}
safeSetState(() => setShowErrorModal(false));
setPaused(true);
setTimeout(() => {
if (!isMounted.current) return;
setCurrentVideoType(nextType);
// Force re-mount of source by tweaking URL param
const sep = currentStreamUrl.includes('?') ? '&' : '?';
setCurrentStreamUrl(`${currentStreamUrl}${sep}rn_type_retry=${Date.now()}`);
setPaused(false);
}, 120);
return;
}
// Check for specific AVFoundation server configuration errors (iOS)
const isServerConfigError = error?.error?.code === -11850 ||
error?.code === -11850 ||
@ -2031,7 +2067,7 @@ const AndroidVideoPlayer: React.FC = () => {
<Video
ref={videoRef}
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
source={{ uri: currentStreamUrl, headers: headers || {} }}
source={{ uri: currentStreamUrl, headers: headers || defaultAndroidHeaders(), type: (currentVideoType as any) }}
paused={paused}
onProgress={handleProgress}
onLoad={(e) => {

View file

@ -121,10 +121,9 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
S{season}E{episode} {episodeTitle && `${episodeTitle}`}
</Text>
)}
{/* Show year, quality, and provider */}
{/* Show year and provider (quality chip removed) */}
<View style={styles.metadataRow}>
{year && <Text style={styles.metadataText}>{year}</Text>}
{quality && <View style={styles.qualityBadge}><Text style={styles.qualityText}>{quality}</Text></View>}
{streamName && <Text style={styles.providerText}>via {streamName}</Text>}
</View>
</View>

View file

@ -911,6 +911,25 @@ export const StreamsScreen = () => {
// Show a quick full-screen black overlay to mask rotation flicker
// by setting a transient state that renders a covering View (implementation already supported by dark backgrounds)
// Infer video type for player (helps Android ExoPlayer choose correct extractor)
const inferVideoTypeFromUrl = (u?: string): string | undefined => {
if (!u) return undefined;
const lower = u.toLowerCase();
if (/(\.|ext=)(m3u8)(\b|$)/i.test(lower)) return 'm3u8';
if (/(\.|ext=)(mpd)(\b|$)/i.test(lower)) return 'mpd';
if (/(\.|ext=)(mp4)(\b|$)/i.test(lower)) return 'mp4';
return undefined;
};
let videoType = inferVideoTypeFromUrl(stream.url);
// Heuristic: certain providers (e.g., Xprime) serve HLS without .m3u8 extension
try {
const providerId = stream.addonId || (stream as any).addon || '';
if (!videoType && /xprime/i.test(providerId)) {
videoType = 'm3u8';
}
} catch {}
navigation.navigate('Player', {
uri: stream.url,
title: metadata?.name || '',
@ -931,7 +950,9 @@ export const StreamsScreen = () => {
imdbId: imdbId || undefined,
availableStreams: streamsToPass,
backdrop: bannerImage || undefined,
});
// Hint for Android ExoPlayer/react-native-video
videoType: videoType,
} as any);
}, [metadata, type, currentEpisode, navigation, id, selectedEpisode, imdbId, episodeStreams, groupedStreams, bannerImage]);