mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
VideoPlayer code cleanup
This commit is contained in:
parent
d0719fabec
commit
c767de12aa
4 changed files with 87 additions and 89 deletions
|
|
@ -75,9 +75,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
backdrop
|
backdrop
|
||||||
} = route.params;
|
} = route.params;
|
||||||
|
|
||||||
// Check if the stream is from Xprime (by provider name or URL pattern)
|
|
||||||
const isXprimeStream = streamProvider === 'xprime' || streamProvider === 'Xprime' ||
|
|
||||||
(uri && /flutch.*\.workers\.dev|fsl\.fastcloud\.casa|xprime/i.test(uri));
|
|
||||||
|
|
||||||
// Check if the stream is HLS (m3u8 playlist)
|
// Check if the stream is HLS (m3u8 playlist)
|
||||||
const isHlsStream = (url: string) => {
|
const isHlsStream = (url: string) => {
|
||||||
|
|
@ -99,37 +96,11 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
} as any;
|
} as any;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Xprime-specific headers for better compatibility (from local-scrapers-repo)
|
|
||||||
const getXprimeHeaders = () => {
|
|
||||||
if (!isXprimeStream) return {};
|
|
||||||
const xprimeHeaders = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
||||||
'Accept': 'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5',
|
|
||||||
'Accept-Language': 'en-US,en;q=0.9',
|
|
||||||
'Accept-Encoding': 'identity',
|
|
||||||
'Origin': 'https://xprime.tv',
|
|
||||||
'Referer': 'https://xprime.tv/',
|
|
||||||
'Sec-Fetch-Dest': 'video',
|
|
||||||
'Sec-Fetch-Mode': 'no-cors',
|
|
||||||
'Sec-Fetch-Site': 'cross-site',
|
|
||||||
'DNT': '1'
|
|
||||||
} as any;
|
|
||||||
logger.log('[AndroidVideoPlayer] Applying Xprime headers for stream:', uri);
|
|
||||||
return xprimeHeaders;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get appropriate headers based on stream type
|
// Get appropriate headers based on stream type
|
||||||
const getStreamHeaders = () => {
|
const getStreamHeaders = () => {
|
||||||
// For Xprime streams, be more flexible - only use HLS headers if it actually looks like HLS
|
// Use HLS headers for HLS streams, default headers for everything else
|
||||||
if (isXprimeStream) {
|
if (isHlsStream(currentStreamUrl)) {
|
||||||
if (isHlsStream(currentStreamUrl)) {
|
|
||||||
logger.log('[AndroidVideoPlayer] Xprime HLS stream detected, applying HLS headers');
|
|
||||||
return getXprimeHeaders();
|
|
||||||
} else {
|
|
||||||
logger.log('[AndroidVideoPlayer] Xprime MP4 stream detected, using default headers');
|
|
||||||
return Platform.OS === 'android' ? defaultAndroidHeaders() : defaultIosHeaders();
|
|
||||||
}
|
|
||||||
} else if (isHlsStream(currentStreamUrl)) {
|
|
||||||
logger.log('[AndroidVideoPlayer] Detected HLS stream, applying HLS headers');
|
logger.log('[AndroidVideoPlayer] Detected HLS stream, applying HLS headers');
|
||||||
return getHlsHeaders();
|
return getHlsHeaders();
|
||||||
}
|
}
|
||||||
|
|
@ -1535,12 +1506,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect Xprime provider to enable a one-shot silent retry (warms upstream/cache)
|
|
||||||
const providerName = ((currentStreamProvider || streamProvider || '') as string).toLowerCase();
|
|
||||||
const isXprimeProvider = providerName.includes('xprime');
|
|
||||||
|
|
||||||
// One-shot, silent retry without showing error UI
|
// One-shot, silent retry without showing error UI
|
||||||
if (isXprimeProvider && retryAttemptRef.current < 1) {
|
if (retryAttemptRef.current < 1) {
|
||||||
retryAttemptRef.current = 1;
|
retryAttemptRef.current = 1;
|
||||||
// Cache-bust to force a fresh fetch and warm upstream
|
// Cache-bust to force a fresh fetch and warm upstream
|
||||||
const addRetryParam = (url: string) => {
|
const addRetryParam = (url: string) => {
|
||||||
|
|
@ -1548,7 +1515,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
return `${url}${sep}rn_retry_ts=${Date.now()}`;
|
return `${url}${sep}rn_retry_ts=${Date.now()}`;
|
||||||
};
|
};
|
||||||
const bustedUrl = addRetryParam(currentStreamUrl);
|
const bustedUrl = addRetryParam(currentStreamUrl);
|
||||||
logger.warn('[AndroidVideoPlayer] Silent retry for Xprime with cache-busted URL');
|
logger.warn('[AndroidVideoPlayer] Silent retry with cache-busted URL');
|
||||||
// Ensure no modal is visible
|
// Ensure no modal is visible
|
||||||
if (errorTimeoutRef.current) {
|
if (errorTimeoutRef.current) {
|
||||||
clearTimeout(errorTimeoutRef.current);
|
clearTimeout(errorTimeoutRef.current);
|
||||||
|
|
|
||||||
|
|
@ -46,57 +46,23 @@ import * as Brightness from 'expo-brightness';
|
||||||
const VideoPlayer: React.FC = () => {
|
const VideoPlayer: React.FC = () => {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const route = useRoute<RouteProp<RootStackParamList, 'Player'>>();
|
const route = useRoute<RouteProp<RootStackParamList, 'Player'>>();
|
||||||
const { streamProvider, uri, headers, forceVlc } = route.params as any;
|
const { uri, headers, forceVlc, streamProvider } = route.params as any;
|
||||||
|
|
||||||
// Check if the stream is from Xprime (by provider name or URL pattern)
|
|
||||||
const isXprimeStream = streamProvider === 'xprime' || streamProvider === 'Xprime' ||
|
|
||||||
(uri && /flutch.*\.workers\.dev|fsl\.fastcloud\.casa|xprime/i.test(uri));
|
|
||||||
|
|
||||||
safeDebugLog("Stream detection", {
|
|
||||||
uri,
|
|
||||||
streamProvider,
|
|
||||||
isXprimeStream,
|
|
||||||
platform: Platform.OS
|
|
||||||
});
|
|
||||||
|
|
||||||
// Xprime-specific headers for better compatibility (from local-scrapers-repo)
|
|
||||||
const getXprimeHeaders = () => {
|
|
||||||
if (!isXprimeStream) return {};
|
|
||||||
const xprimeHeaders = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
||||||
'Accept': 'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5',
|
|
||||||
'Accept-Language': 'en-US,en;q=0.9',
|
|
||||||
'Accept-Encoding': 'identity',
|
|
||||||
'Origin': 'https://xprime.tv',
|
|
||||||
'Referer': 'https://xprime.tv/',
|
|
||||||
'Sec-Fetch-Dest': 'video',
|
|
||||||
'Sec-Fetch-Mode': 'no-cors',
|
|
||||||
'Sec-Fetch-Site': 'cross-site',
|
|
||||||
'DNT': '1'
|
|
||||||
} as any;
|
|
||||||
logger.log('[VideoPlayer] Applying Xprime headers for stream:', uri);
|
|
||||||
return xprimeHeaders;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Detect if stream is MKV format
|
// Detect if stream is MKV format
|
||||||
const isMkvFile = isMkvStream(uri, headers);
|
const isMkvFile = isMkvStream(uri, headers);
|
||||||
|
|
||||||
// Use AndroidVideoPlayer for:
|
// Use AndroidVideoPlayer for:
|
||||||
// - Android devices
|
// - Android devices
|
||||||
// - Xprime streams (unless it's MKV on iOS - allow VLC for better compatibility)
|
|
||||||
// - Non-MKV files on iOS (unless forceVlc is set)
|
// - Non-MKV files on iOS (unless forceVlc is set)
|
||||||
// Use VideoPlayer (VLC) for:
|
// Use VideoPlayer (VLC) for:
|
||||||
// - MKV files on iOS (unless forceVlc is set, even for Xprime)
|
// - MKV files on iOS (unless forceVlc is set)
|
||||||
const shouldUseAndroidPlayer = Platform.OS === 'android' ||
|
const shouldUseAndroidPlayer = Platform.OS === 'android' ||
|
||||||
(isXprimeStream && !(Platform.OS === 'ios' && isMkvFile)) ||
|
|
||||||
(Platform.OS === 'ios' && !isMkvFile && !forceVlc);
|
(Platform.OS === 'ios' && !isMkvFile && !forceVlc);
|
||||||
|
|
||||||
safeDebugLog("Player selection logic", {
|
safeDebugLog("Player selection logic", {
|
||||||
platform: Platform.OS,
|
platform: Platform.OS,
|
||||||
isXprimeStream,
|
|
||||||
isMkvFile,
|
isMkvFile,
|
||||||
forceVlc,
|
forceVlc,
|
||||||
xprimeException: isXprimeStream && Platform.OS === 'ios' && isMkvFile,
|
|
||||||
shouldUseAndroidPlayer
|
shouldUseAndroidPlayer
|
||||||
});
|
});
|
||||||
if (shouldUseAndroidPlayer) {
|
if (shouldUseAndroidPlayer) {
|
||||||
|
|
@ -1176,6 +1142,38 @@ const VideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
if (data.textTracks && data.textTracks.length > 0) {
|
if (data.textTracks && data.textTracks.length > 0) {
|
||||||
setVlcTextTracks(data.textTracks);
|
setVlcTextTracks(data.textTracks);
|
||||||
|
|
||||||
|
// Auto-select English subtitle track if available
|
||||||
|
if (selectedTextTrack === -1 && !useCustomSubtitles && data.textTracks.length > 0) {
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
logger.log(`[VideoPlayer] Available subtitle tracks:`, data.textTracks.map((track: any) => ({
|
||||||
|
id: track.id,
|
||||||
|
index: track.index,
|
||||||
|
name: track.name,
|
||||||
|
language: track.language
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for English track first
|
||||||
|
const englishTrack = data.textTracks.find((track: any) => {
|
||||||
|
const lang = (track.language || '').toLowerCase();
|
||||||
|
const name = (track.name || '').toLowerCase();
|
||||||
|
return lang === 'english' || lang === 'en' || lang === 'eng' ||
|
||||||
|
name.includes('english') || name.includes('en');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (englishTrack) {
|
||||||
|
// Try different ID fields that VLC might use
|
||||||
|
const trackId = englishTrack.id !== undefined ? englishTrack.id :
|
||||||
|
englishTrack.index !== undefined ? englishTrack.index : 0;
|
||||||
|
setSelectedTextTrack(trackId);
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
logger.log(`[VideoPlayer] Auto-selected English subtitle track: ${englishTrack.name || 'Unknown'} (ID: ${trackId})`);
|
||||||
|
}
|
||||||
|
} else if (DEBUG_MODE) {
|
||||||
|
logger.log(`[VideoPlayer] No English subtitle track found, keeping subtitles disabled`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsVideoLoaded(true);
|
setIsVideoLoaded(true);
|
||||||
|
|
@ -2557,13 +2555,12 @@ const VideoPlayer: React.FC = () => {
|
||||||
ref={vlcRef}
|
ref={vlcRef}
|
||||||
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
|
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
|
||||||
source={(() => {
|
source={(() => {
|
||||||
// Use Xprime headers if detected, otherwise use headers from route params
|
// Use route headers for VLC streams
|
||||||
const finalHeaders = getXprimeHeaders() || headers;
|
const sourceWithHeaders = headers && Object.keys(headers).length > 0 ? {
|
||||||
const sourceWithHeaders = finalHeaders ? {
|
|
||||||
uri: currentStreamUrl,
|
uri: currentStreamUrl,
|
||||||
headers: finalHeaders
|
headers: headers
|
||||||
} : { uri: currentStreamUrl };
|
} : { uri: currentStreamUrl };
|
||||||
|
|
||||||
return sourceWithHeaders;
|
return sourceWithHeaders;
|
||||||
})()}
|
})()}
|
||||||
paused={paused}
|
paused={paused}
|
||||||
|
|
@ -2576,7 +2573,7 @@ const VideoPlayer: React.FC = () => {
|
||||||
onPaused={onPaused}
|
onPaused={onPaused}
|
||||||
resizeMode={resizeMode as any}
|
resizeMode={resizeMode as any}
|
||||||
audioTrack={selectedAudioTrack ?? undefined}
|
audioTrack={selectedAudioTrack ?? undefined}
|
||||||
textTrack={useCustomSubtitles ? -1 : (selectedTextTrack ?? undefined)}
|
textTrack={useCustomSubtitles ? -1 : (selectedTextTrack >= 0 ? selectedTextTrack : -1)}
|
||||||
autoAspectRatio
|
autoAspectRatio
|
||||||
volume={volume / 100}
|
volume={volume / 100}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,10 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
<View style={{ gap: 8 }}>
|
<View style={{ gap: 8 }}>
|
||||||
{vlcTextTracks.map((track) => {
|
{vlcTextTracks.map((track) => {
|
||||||
const isSelected = selectedTextTrack === track.id && !useCustomSubtitles;
|
const isSelected = selectedTextTrack === track.id && !useCustomSubtitles;
|
||||||
|
// Debug logging for subtitle selection
|
||||||
|
if (__DEV__ && vlcTextTracks.length > 0) {
|
||||||
|
console.log('[SubtitleModals] Track:', track.id, track.name, 'Selected:', selectedTextTrack, 'isSelected:', isSelected, 'useCustom:', useCustomSubtitles);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={track.id}
|
key={track.id}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { Meta, stremioService } from '../services/stremioService';
|
import { Meta, stremioService } from '../services/stremioService';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
|
import { BlurView } from 'expo-blur';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames';
|
import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames';
|
||||||
|
|
@ -193,12 +194,25 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
top: 10,
|
top: 10,
|
||||||
right: 10,
|
right: 10,
|
||||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||||
borderRadius: 0,
|
borderRadius: 10,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
paddingVertical: 6,
|
paddingVertical: 6,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
|
badgeBlur: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 10,
|
||||||
|
right: 10,
|
||||||
|
borderRadius: 10,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
badgeContent: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 6,
|
||||||
|
},
|
||||||
badgeText: {
|
badgeText: {
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
|
|
@ -584,15 +598,31 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{type === 'movie' && nowPlayingMovies.has(item.id) && (
|
{type === 'movie' && nowPlayingMovies.has(item.id) && (
|
||||||
<View style={styles.badgeContainer}>
|
Platform.OS === 'ios' ? (
|
||||||
<MaterialIcons
|
<View style={styles.badgeBlur}>
|
||||||
name="theaters"
|
<BlurView intensity={40} tint={isDarkMode ? 'dark' : 'light'} style={{ borderRadius: 10 }}>
|
||||||
size={12}
|
<View style={styles.badgeContent}>
|
||||||
color={colors.white}
|
<MaterialIcons
|
||||||
style={{ marginRight: 4 }}
|
name="theaters"
|
||||||
/>
|
size={12}
|
||||||
<Text style={styles.badgeText}>In Theaters</Text>
|
color={colors.white}
|
||||||
</View>
|
style={{ marginRight: 4 }}
|
||||||
|
/>
|
||||||
|
<Text style={styles.badgeText}>In Theaters</Text>
|
||||||
|
</View>
|
||||||
|
</BlurView>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={styles.badgeContainer}>
|
||||||
|
<MaterialIcons
|
||||||
|
name="theaters"
|
||||||
|
size={12}
|
||||||
|
color={colors.white}
|
||||||
|
style={{ marginRight: 4 }}
|
||||||
|
/>
|
||||||
|
<Text style={styles.badgeText}>In Theaters</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue