diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj index dafb0c3e..7c31ca79 100644 --- a/ios/Nuvio.xcodeproj/project.pbxproj +++ b/ios/Nuvio.xcodeproj/project.pbxproj @@ -428,7 +428,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app; - PRODUCT_NAME = "Nuvio"; + PRODUCT_NAME = Nuvio; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -462,7 +462,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app; - PRODUCT_NAME = "Nuvio"; + PRODUCT_NAME = Nuvio; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/ios/Nuvio/KSPlayerView.swift b/ios/Nuvio/KSPlayerView.swift index d63663e0..7570f297 100644 --- a/ios/Nuvio/KSPlayerView.swift +++ b/ios/Nuvio/KSPlayerView.swift @@ -16,7 +16,6 @@ class KSPlayerView: UIView { private var isPaused = false private var currentVolume: Float = 1.0 weak var viewManager: KSPlayerViewManager? - private var loadTimeoutWorkItem: DispatchWorkItem? // Event blocks for Fabric @objc var onLoad: RCTDirectEventBlock? @@ -149,18 +148,6 @@ class KSPlayerView: UIView { print("KSPlayerView: Setting source: \(uri)") print("KSPlayerView: URL scheme: \(url.scheme ?? "unknown"), host: \(url.host ?? "unknown")") - // Add timeout for source loading - loadTimeoutWorkItem?.cancel() - let work = DispatchWorkItem { [weak self] in - guard let self = self else { return } - let dur = self.playerView.playerLayer?.player.duration ?? 0 - if dur <= 0 { - self.sendEvent("onError", ["error": "Stream timeout: unable to open input after 8 seconds"]) - } - } - loadTimeoutWorkItem = work - DispatchQueue.main.asyncAfter(deadline: .now() + 8, execute: work) - playerView.set(resource: resource) // Set up delegate after setting the resource @@ -427,8 +414,6 @@ extension KSPlayerView: KSPlayerLayerDelegate { func player(layer: KSPlayerLayer, state: KSPlayerState) { switch state { case .readyToPlay: - // Cancel timeout when ready - loadTimeoutWorkItem?.cancel() // Send onLoad event to React Native with track information let p = layer.player let tracks = getAvailableTracks() diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 23c41051..a5191fb2 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -37,7 +37,7 @@ import PlayerControls from './controls/PlayerControls'; import CustomSubtitles from './subtitles/CustomSubtitles'; import { SourcesModal } from './modals/SourcesModal'; import { stremioService } from '../../services/stremioService'; -import { isMkvStream } from '../../utils/mkvDetection'; +import { shouldUseKSPlayer } from '../../utils/playerSelection'; import axios from 'axios'; import * as Brightness from 'expo-brightness'; @@ -2332,39 +2332,8 @@ const AndroidVideoPlayer: React.FC = () => { return; } - // On iOS: if the selected stream is MKV, switch to KSPlayer screen by replacing route - if (Platform.OS === 'ios') { - const targetIsMkv = isMkvStream(newStream.url, newStream.headers || {}); - if (targetIsMkv) { - // Ensure current player stops immediately before switching screens - setPaused(true); - setShowSourcesModal(false); - // Small delay to guarantee audio halt before navigation switch - setTimeout(() => { - (navigation as any).replace('Player', { - uri: newStream.url, - title, - episodeTitle, - season, - episode, - quality: (newStream.title?.match(/(\d+)p/) || [])[1] || newStream.quality, - year, - streamProvider: newStream.addonName || newStream.name || newStream.addon || 'Unknown', - streamName: newStream.name || newStream.title || 'Unknown Stream', - headers: newStream.headers || undefined, - id, - type, - episodeId, - imdbId, - backdrop, - availableStreams, - // Ensure KSPlayer is chosen even if URL/headers do not reveal MKV - forceVlc: true, - }); - }, 50); - return; - } - } + // Note: iOS now always uses KSPlayer, so this AndroidVideoPlayer should never be used on iOS + // This logic is kept for safety in case routing changes setIsChangingSource(true); setShowSourcesModal(false); diff --git a/src/components/player/KSPlayer.tsx b/src/components/player/KSPlayer.tsx index bdbd278b..5ac73c89 100644 --- a/src/components/player/KSPlayer.tsx +++ b/src/components/player/KSPlayer.tsx @@ -32,7 +32,7 @@ import { } from './utils/playerTypes'; import { safeDebugLog, parseSRT, DEBUG_MODE, formatTime } from './utils/playerUtils'; import { styles } from './utils/playerStyles'; -import { isMkvStream } from '../../utils/mkvDetection'; +import { shouldUseKSPlayer } from '../../utils/playerSelection'; import { SubtitleModals } from './modals/SubtitleModals'; import { AudioTrackModal } from './modals/AudioTrackModal'; // Removed ResumeOverlay usage when alwaysResume is enabled @@ -43,32 +43,32 @@ import axios from 'axios'; import { stremioService } from '../../services/stremioService'; import * as Brightness from 'expo-brightness'; -// KSPlayerRouter component handles platform selection without conditional hook calls +// KSPlayerRouter component handles platform selection const KSPlayerRouter: React.FC = () => { const route = useRoute>(); - const { uri, headers } = route.params as any; + const { uri, headers, forceVlc } = route.params as any; - // Detect if stream is MKV format - const isMkvFile = isMkvStream(uri, headers); - - // Honor forceVlc from navigation params for iOS, or fallback to MKV detection - const forceVlc = ((route.params as any)?.forceVlc === true); - - // Use AndroidVideoPlayer for Android devices. On iOS, use KSPlayer when MKV or forced. - const shouldUseAndroidPlayer = Platform.OS === 'android' || (Platform.OS === 'ios' && !(isMkvFile || forceVlc)); + // Use centralized player selection logic + const shouldUseKSPlayerComponent = shouldUseKSPlayer({ + uri, + headers, + forceVlc + }); safeDebugLog("Player selection logic", { platform: Platform.OS, - isMkvFile, + uri, forceVlc, - shouldUseAndroidPlayer + shouldUseKSPlayer: shouldUseKSPlayerComponent }); - if (shouldUseAndroidPlayer) { - return ; + // iOS: Always use KSPlayer (handles all formats with AVPlayer → FFmpeg fallback) + // Android: Always use AndroidVideoPlayer (react-native-video/ExoPlayer) + if (shouldUseKSPlayerComponent) { + return ; } - return ; + return ; }; const KSPlayer: React.FC = () => { @@ -2318,36 +2318,8 @@ const KSPlayerCore: React.FC = () => { return; } - // On iOS: if the selected stream is NOT MKV, switch to AndroidVideoPlayer screen by replacing route - if (Platform.OS === 'ios') { - const targetIsMkv = isMkvStream(newStream.url, newStream.headers || {}); - if (!targetIsMkv) { - // Ensure KSPlayer stops immediately before switching screens - setPaused(true); - setShowSourcesModal(false); - setTimeout(() => { - navigation.replace('Player', { - uri: newStream.url, - title, - episodeTitle, - season, - episode, - quality: (newStream.title?.match(/(\d+)p/) || [])[1] || newStream.quality, - year, - streamProvider: newStream.addonName || newStream.name || newStream.addon || 'Unknown', - streamName: newStream.name || newStream.title || 'Unknown Stream', - headers: newStream.headers || undefined, - id, - type, - episodeId, - imdbId, - backdrop, - availableStreams, - } as any); - }, 50); - return; - } - } + // On iOS: All streams use KSPlayer, no need to switch players + // Stream switching is handled internally by KSPlayerCore setIsChangingSource(true); setShowSourcesModal(false); diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 98b6caa1..95f23815 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -874,41 +874,9 @@ export const StreamsScreen = () => { const streamName = stream.name || stream.title || 'Unnamed Stream'; const streamProvider = stream.addonId || stream.addonName || stream.name; - // Determine if we should force VLC on iOS based on actual stream format and provider capability - let forceVlc = !!options?.forceVlc; - try { - if (Platform.OS === 'ios' && !forceVlc) { - const isMkvFile = isMkvStream(stream.url, stream.headers); - - // Special case: moviebox should always use AndroidVideoPlayer - if (streamProvider === 'moviebox') { - forceVlc = false; - logger.log(`[StreamsScreen] Provider ${streamProvider} -> always using AndroidVideoPlayer`); - } else { - // Also check if the provider declares MKV format support - let providerSupportsMkv = false; - try { - const availableScrapers = await localScraperService.getAvailableScrapers(); - const provider = availableScrapers.find(scraper => scraper.id === streamProvider); - if (provider && provider.formats) { - providerSupportsMkv = provider.formats.includes('mkv'); - logger.log(`[StreamsScreen] Provider ${streamProvider} formats:`, provider.formats, 'supports MKV:', providerSupportsMkv); - } - } catch (providerError) { - logger.warn('[StreamsScreen] Failed to check provider formats:', providerError); - } - - if (isMkvFile || providerSupportsMkv) { - forceVlc = true; - logger.log(`[StreamsScreen] Stream is MKV format (detected: ${isMkvFile}, provider supports: ${providerSupportsMkv}) -> forcing VLC`); - } else { - logger.log(`[StreamsScreen] Stream is NOT MKV format (detected: ${isMkvFile}, provider supports: ${providerSupportsMkv}) -> using AndroidVideoPlayer`); - } - } - } - } catch (e) { - logger.warn('[StreamsScreen] Stream format detection failed:', e); - } + // iOS now always uses KSPlayer, no need for player selection logic + // Keep forceVlc for backward compatibility but it's ignored by player selection + const forceVlc = !!options?.forceVlc; // Show a quick full-screen black overlay to mask rotation flicker @@ -944,7 +912,7 @@ export const StreamsScreen = () => { streamName: streamName, // Always prefer stream.headers; player will use these for requests headers: options?.headers || stream.headers || undefined, - // Force VLC for providers that declare MKV format support on iOS + // iOS now always uses KSPlayer, forceVlc kept for backward compatibility forceVlc, id, type, @@ -974,15 +942,15 @@ export const StreamsScreen = () => { if (Platform.OS === 'ios' && settings.preferredPlayer === 'internal') { // Check if the actual stream is an MKV file const lowerUri = (stream.url || '').toLowerCase(); + // iOS now always uses KSPlayer, no need for format-specific logic + // Keep this for logging purposes only const contentType = (stream.headers && ((stream.headers as any)['Content-Type'] || (stream.headers as any)['content-type'])) || ''; const isMkvByHeader = typeof contentType === 'string' && contentType.includes('matroska'); const isMkvByPath = lowerUri.includes('.mkv') || /[?&]ext=mkv\b/.test(lowerUri) || /format=mkv\b/.test(lowerUri) || /container=mkv\b/.test(lowerUri); const isMkvFile = Boolean(isMkvByHeader || isMkvByPath); - + if (isMkvFile) { - logger.log(`[StreamsScreen] Stream is MKV format -> forcing VLC on iOS (internal preferred)`); - navigateToPlayer(stream, { forceVlc: true }); - return; + logger.log(`[StreamsScreen] Stream is MKV format - will play in KSPlayer on iOS`); } } } catch (err) { @@ -1005,8 +973,8 @@ export const StreamsScreen = () => { ...(stream.headers || {}), 'Content-Type': 'video/x-matroska', } as Record; - logger.log('[StreamsScreen] HEAD detected MKV via Content-Type quickly, forcing in-app VLC on iOS (internal preferred)'); - navigateToPlayer(stream, { forceVlc: true, headers: mergedHeaders }); + logger.log('[StreamsScreen] HEAD detected MKV via Content-Type - will play in KSPlayer on iOS'); + navigateToPlayer(stream, { headers: mergedHeaders }); return; } } catch (e) { diff --git a/src/utils/playerSelection.ts b/src/utils/playerSelection.ts new file mode 100644 index 00000000..7f60ef54 --- /dev/null +++ b/src/utils/playerSelection.ts @@ -0,0 +1,45 @@ +/** + * Centralized player selection logic + * Used by both StreamsScreen and KSPlayer routing + */ + +import { Platform } from 'react-native'; +import { isMkvStream } from './mkvDetection'; + +export interface PlayerSelectionOptions { + uri: string; + headers?: Record; + forceVlc?: boolean; + platform?: typeof Platform.OS; +} + +/** + * Determines which player should be used for a given stream + */ +export const shouldUseKSPlayer = ({ + uri, + headers, + forceVlc = false, + platform = Platform.OS +}: PlayerSelectionOptions): boolean => { + // Android always uses AndroidVideoPlayer (react-native-video) + if (platform === 'android') { + return false; + } + + // iOS: Always use KSPlayer for all formats + // KSPlayer handles automatic fallback (AVPlayer → FFmpeg) + if (platform === 'ios') { + return true; + } + + // Default fallback + return false; +}; + +/** + * Get the appropriate player component name + */ +export const getPlayerComponent = (options: PlayerSelectionOptions): 'AndroidVideoPlayer' | 'KSPlayerCore' => { + return shouldUseKSPlayer(options) ? 'KSPlayerCore' : 'AndroidVideoPlayer'; +};