mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-08 11:10:55 +00:00
potential fix
This commit is contained in:
parent
a562e45ffd
commit
e8d7eab2e4
2 changed files with 37 additions and 27 deletions
|
|
@ -2,7 +2,7 @@ import React, { useEffect, useRef } from 'react';
|
||||||
import { NavigationContainer, DefaultTheme as NavigationDefaultTheme, DarkTheme as NavigationDarkTheme, Theme, NavigationProp } from '@react-navigation/native';
|
import { NavigationContainer, DefaultTheme as NavigationDefaultTheme, DarkTheme as NavigationDarkTheme, Theme, NavigationProp } from '@react-navigation/native';
|
||||||
import { createNativeStackNavigator, NativeStackNavigationOptions, NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import { createNativeStackNavigator, NativeStackNavigationOptions, NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
import { createBottomTabNavigator, BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
import { createBottomTabNavigator, BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
||||||
import { useColorScheme, Platform, Animated, StatusBar, TouchableOpacity, View, Text, AppState, Easing, Dimensions } from 'react-native';
|
import { useColorScheme, Platform, Animated, StatusBar, TouchableOpacity, View, Text, AppState, Easing, Dimensions, useWindowDimensions } from 'react-native';
|
||||||
import { PaperProvider, MD3DarkTheme, MD3LightTheme, adaptNavigationTheme } from 'react-native-paper';
|
import { PaperProvider, MD3DarkTheme, MD3LightTheme, adaptNavigationTheme } from 'react-native-paper';
|
||||||
import type { MD3Theme } from 'react-native-paper';
|
import type { MD3Theme } from 'react-native-paper';
|
||||||
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
||||||
|
|
@ -358,7 +358,9 @@ const TabIcon = React.memo(({ focused, color, iconName }: {
|
||||||
|
|
||||||
// Update the TabScreenWrapper component with fixed layout dimensions
|
// Update the TabScreenWrapper component with fixed layout dimensions
|
||||||
const TabScreenWrapper: React.FC<{children: React.ReactNode}> = ({ children }) => {
|
const TabScreenWrapper: React.FC<{children: React.ReactNode}> = ({ children }) => {
|
||||||
const isTablet = Dimensions.get('window').width >= 768;
|
const { width, height } = useWindowDimensions();
|
||||||
|
const smallestDimension = Math.min(width, height);
|
||||||
|
const isTablet = (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
// Force consistent status bar settings
|
// Force consistent status bar settings
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -421,7 +423,9 @@ const WrappedScreen: React.FC<{Screen: React.ComponentType<any>}> = ({ Screen })
|
||||||
const MainTabs = () => {
|
const MainTabs = () => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const { isHomeLoading } = useLoading();
|
const { isHomeLoading } = useLoading();
|
||||||
const isTablet = Dimensions.get('window').width >= 768;
|
const { width, height } = useWindowDimensions();
|
||||||
|
const smallestDimension = Math.min(width, height);
|
||||||
|
const isTablet = (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const isIosTablet = Platform.OS === 'ios' && isTablet;
|
const isIosTablet = Platform.OS === 'ios' && isTablet;
|
||||||
const [hidden, setHidden] = React.useState(HeaderVisibility.isHidden());
|
const [hidden, setHidden] = React.useState(HeaderVisibility.isHidden());
|
||||||
|
|
@ -927,7 +931,7 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
||||||
component={VideoPlayer as any}
|
component={VideoPlayer as any}
|
||||||
options={{
|
options={{
|
||||||
animation: Platform.OS === 'android' ? 'none' : 'default',
|
animation: Platform.OS === 'android' ? 'none' : 'default',
|
||||||
animationDuration: Platform.OS === 'android' ? 0 : 300,
|
animationDuration: Platform.OS === 'android' ? 0 : 0,
|
||||||
// Force fullscreen presentation on iPad
|
// Force fullscreen presentation on iPad
|
||||||
presentation: Platform.OS === 'ios' ? 'fullScreenModal' : 'card',
|
presentation: Platform.OS === 'ios' ? 'fullScreenModal' : 'card',
|
||||||
// Disable gestures during video playback
|
// Disable gestures during video playback
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,27 @@ const { width, height } = Dimensions.get('window');
|
||||||
const scraperLogoCache = new Map<string, string>();
|
const scraperLogoCache = new Map<string, string>();
|
||||||
let scraperLogoCachePromise: Promise<void> | null = null;
|
let scraperLogoCachePromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
// Short-budget HEAD detection to avoid long delays before navigation
|
||||||
|
const MKV_HEAD_TIMEOUT_MS = 600;
|
||||||
|
|
||||||
|
const detectMkvViaHead = async (url: string, headers?: Record<string, string>) => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), MKV_HEAD_TIMEOUT_MS);
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'HEAD',
|
||||||
|
headers,
|
||||||
|
signal: controller.signal as any,
|
||||||
|
} as any);
|
||||||
|
const contentType = res.headers.get('content-type') || '';
|
||||||
|
return /matroska|x-matroska/i.test(contentType);
|
||||||
|
} catch (_e) {
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Extracted Components
|
// Extracted Components
|
||||||
const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, theme, showLogos }: {
|
const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, theme, showLogos }: {
|
||||||
stream: Stream;
|
stream: Stream;
|
||||||
|
|
@ -973,43 +994,28 @@ export const StreamsScreen = () => {
|
||||||
logger.warn('[StreamsScreen] MKV pre-check failed:', err);
|
logger.warn('[StreamsScreen] MKV pre-check failed:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// On iOS, for installed addons where URL may not include .mkv, send a HEAD request
|
// iOS: very short MKV detection race; never block longer than MKV_HEAD_TIMEOUT_MS
|
||||||
// to detect MKV via Content-Type before opening the player
|
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
const lowerUrl = (stream.url || '').toLowerCase();
|
const lowerUrl = (stream.url || '').toLowerCase();
|
||||||
const isMkvByPath = lowerUrl.includes('.mkv') || /[?&]ext=mkv\b/.test(lowerUrl) || /format=mkv\b/.test(lowerUrl) || /container=mkv\b/.test(lowerUrl);
|
const isMkvByPath = lowerUrl.includes('.mkv') || /[?&]ext=mkv\b/i.test(lowerUrl) || /format=mkv\b/i.test(lowerUrl) || /container=mkv\b/i.test(lowerUrl);
|
||||||
const isHttp = lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://');
|
const isHttp = lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://');
|
||||||
if (!isMkvByPath && isHttp) {
|
if (!isMkvByPath && isHttp) {
|
||||||
try {
|
try {
|
||||||
const mkvDetected = await (async () => {
|
const mkvDetected = await Promise.race<boolean>([
|
||||||
const controller = new AbortController();
|
detectMkvViaHead(stream.url, (stream.headers as any) || undefined),
|
||||||
const timeout = setTimeout(() => controller.abort(), 4000);
|
new Promise<boolean>(res => setTimeout(() => res(false), MKV_HEAD_TIMEOUT_MS)),
|
||||||
try {
|
]);
|
||||||
const res = await fetch(stream.url, {
|
|
||||||
method: 'HEAD',
|
|
||||||
// Pass along any known headers to improve odds of correct response
|
|
||||||
headers: (stream.headers as any) || undefined,
|
|
||||||
signal: controller.signal as any,
|
|
||||||
} as any);
|
|
||||||
const contentType = res.headers.get('content-type') || '';
|
|
||||||
return typeof contentType === 'string' && /matroska|x-matroska/i.test(contentType);
|
|
||||||
} catch (_e) {
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
if (mkvDetected) {
|
if (mkvDetected) {
|
||||||
const mergedHeaders = {
|
const mergedHeaders = {
|
||||||
...(stream.headers || {}),
|
...(stream.headers || {}),
|
||||||
'Content-Type': 'video/x-matroska',
|
'Content-Type': 'video/x-matroska',
|
||||||
} as Record<string, string>;
|
} as Record<string, string>;
|
||||||
logger.log('[StreamsScreen] HEAD detected MKV via Content-Type, forcing in-app VLC on iOS');
|
logger.log('[StreamsScreen] HEAD detected MKV via Content-Type quickly, forcing in-app VLC on iOS');
|
||||||
navigateToPlayer(stream, { forceVlc: true, headers: mergedHeaders });
|
navigateToPlayer(stream, { forceVlc: true, headers: mergedHeaders });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('[StreamsScreen] HEAD MKV detection failed:', e);
|
logger.warn('[StreamsScreen] Short MKV detection failed:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue