From 30ebba0722e45c9332a9edb8b06ae3f72d42910b Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 10 Jan 2026 02:11:32 +0530 Subject: [PATCH] updated langs --- src/components/player/KSPlayerCore.tsx | 193 +++++++++++++----- src/components/player/MPVPlayerComponent.tsx | 1 + .../player/controls/PlayerControls.tsx | 9 +- .../player/ios/components/AVPlayerSurface.tsx | 124 +++++++++++ .../player/ios/components/KSPlayerSurface.tsx | 1 + src/hooks/useSettings.ts | 4 + src/i18n/locales/ar.json | 3 +- src/i18n/locales/en.json | 3 +- src/i18n/locales/es.json | 3 +- src/i18n/locales/fr.json | 3 +- src/i18n/locales/it.json | 3 +- src/i18n/locales/pt-BR.json | 3 +- src/i18n/locales/pt-PT.json | 3 +- src/screens/PlayerSettingsScreen.tsx | 63 +++++- 14 files changed, 354 insertions(+), 62 deletions(-) create mode 100644 src/components/player/ios/components/AVPlayerSurface.tsx diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index ae755b7..42cedb8 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; -import { View, StatusBar, StyleSheet, Animated, Dimensions } from 'react-native'; +import { View, StatusBar, StyleSheet, Animated, Dimensions, Platform } from 'react-native'; import { useNavigation, useRoute } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import axios from 'axios'; @@ -24,6 +24,7 @@ import { SpeedActivatedOverlay, PauseOverlay, GestureControls } from './componen // Platform-specific components import { KSPlayerSurface } from './ios/components/KSPlayerSurface'; +import AVPlayerSurface from './ios/components/AVPlayerSurface'; import { usePlayerState, @@ -55,6 +56,7 @@ import { parseSRT } from './utils/subtitleParser'; import { findBestSubtitleTrack, autoSelectAudioTrack, findBestAudioTrack } from './utils/trackSelectionUtils'; import { useSettings } from '../../hooks/useSettings'; import { useTheme } from '../../contexts/ThemeContext'; +import type { VideoRef, SelectedTrack } from 'react-native-video'; // Player route params interface interface PlayerRouteParams { @@ -135,6 +137,13 @@ const KSPlayerCore: React.FC = () => { const { settings } = useSettings(); const { currentTheme } = useTheme(); + // iOS Auto engine switching: AVPlayer (react-native-video) primary, MPV fallback + const avPlayerRef = useRef(null); + const shouldUseMpvOnly = Platform.OS === 'ios' && settings.iosVideoPlayerEngine === 'mpv'; + const [useMpvFallback, setUseMpvFallback] = useState(shouldUseMpvOnly); + const hasAviOSFailed = useRef(false); + const lastPlaybackTimeRef = useRef(0); + // Subtitle sync modal state const [showSyncModal, setShowSyncModal] = useState(false); @@ -170,7 +179,7 @@ const KSPlayerCore: React.FC = () => { }); const controls = usePlayerControls({ - playerRef: mpvPlayerRef, + playerRef: (shouldUseMpvOnly || useMpvFallback) ? mpvPlayerRef : avPlayerRef, paused, setPaused, currentTime, @@ -411,8 +420,8 @@ const KSPlayerCore: React.FC = () => { if (bestAudioTrack !== null) { logger.debug(`[KSPlayerCore] Auto-selecting audio track ${bestAudioTrack} for language: ${settings.preferredAudioLanguage}`); tracks.selectAudioTrack(bestAudioTrack); - if (ksPlayerRef.current) { - ksPlayerRef.current.setAudioTrack(bestAudioTrack); + if (shouldUseMpvOnly || useMpvFallback) { + mpvPlayerRef.current?.setAudioTrack(bestAudioTrack); } } } @@ -448,9 +457,11 @@ const KSPlayerCore: React.FC = () => { const resumeTarget = routeInitialPosition || watchProgress.initialPosition || watchProgress.initialSeekTargetRef?.current; if (resumeTarget && resumeTarget > 0 && !watchProgress.showResumeOverlay && data.duration > 0) { setTimeout(() => { - if (ksPlayerRef.current) { - logger.debug(`[KSPlayerCore] Auto-resuming to ${resumeTarget}`); - ksPlayerRef.current.seek(resumeTarget); + logger.debug(`[KSPlayerCore] Auto-resuming to ${resumeTarget}`); + if (shouldUseMpvOnly || useMpvFallback) { + mpvPlayerRef.current?.seek(resumeTarget); + } else { + avPlayerRef.current?.seek(resumeTarget); } }, 500); } @@ -462,6 +473,13 @@ const KSPlayerCore: React.FC = () => { }; const handleError = (error: any) => { + // Auto mode: if AVPlayer fails, silently switch to MPV for this session. + if (!shouldUseMpvOnly && !useMpvFallback) { + hasAviOSFailed.current = true; + setUseMpvFallback(true); + return; + } + let msg = 'Unknown Error'; try { if (typeof error === 'string') { @@ -512,8 +530,10 @@ const KSPlayerCore: React.FC = () => { const handleSelectAudioTrack = useCallback((trackId: number) => { tracks.selectAudioTrack(trackId); - mpvPlayerRef.current?.setAudioTrack(trackId); - }, [tracks, mpvPlayerRef]); + if (shouldUseMpvOnly || useMpvFallback) { + mpvPlayerRef.current?.setAudioTrack(trackId); + } + }, [tracks, mpvPlayerRef, shouldUseMpvOnly, useMpvFallback]); // Stream selection handler const handleSelectStream = async (newStream: any) => { @@ -612,50 +632,109 @@ const KSPlayerCore: React.FC = () => { /> {/* Video Surface & Pinch Zoom */} - tracks.setKsAudioTracks(d.audioTracks || [])} - onTextTracks={(d) => tracks.setKsTextTracks(d.textTracks || [])} - onLoad={onLoad} - onProgress={(d) => { - if (!isSliderDragging) { - setCurrentTime(d.currentTime); + {(shouldUseMpvOnly || useMpvFallback) ? ( + tracks.setKsAudioTracks(d.audioTracks || [])} + onTextTracks={(d) => tracks.setKsTextTracks(d.textTracks || [])} + onLoad={(d) => { + onLoad(d); + // If we fell back from AVPlayer, continue from last time once MPV is ready. + if (!shouldUseMpvOnly && hasAviOSFailed.current) { + const target = lastPlaybackTimeRef.current || 0; + if (target > 0) { + setTimeout(() => mpvPlayerRef.current?.seek(target), 200); + } + hasAviOSFailed.current = false; + } + }} + onProgress={(d) => { + if (!isSliderDragging) { + setCurrentTime(d.currentTime); + } + lastPlaybackTimeRef.current = d.currentTime || 0; + // Only update buffered if it changed by more than 0.5s to reduce re-renders + const newBuffered = d.buffered || 0; + if (Math.abs(newBuffered - buffered) > 0.5) { + setBuffered(newBuffered); + } + }} + onEnd={async () => { + setCurrentTime(duration); + await traktAutosync.handlePlaybackEnd(duration, duration, 'ended'); + }} + onError={handleError} + onBuffer={setIsBuffering} + onReadyForDisplay={() => setIsPlayerReady(true)} + onPlaybackStalled={() => setIsBuffering(true)} + onPlaybackResume={() => setIsBuffering(false)} + screenWidth={screenDimensions.width} + screenHeight={screenDimensions.height} + customVideoStyles={{ width: '100%', height: '100%' }} + subtitleTextColor={customSubs.subtitleTextColor} + subtitleBackgroundColor={customSubs.subtitleBackground ? `rgba(0,0,0,${customSubs.subtitleBgOpacity})` : 'transparent'} + subtitleFontSize={customSubs.subtitleSize} + subtitleBottomOffset={customSubs.subtitleBottomOffset} + /> + ) : ( + 0.5) { - setBuffered(newBuffered); + selectedTextTrack={ + customSubs.useCustomSubtitles + ? ({ type: 'disabled' } as SelectedTrack) + : (tracks.selectedTextTrack >= 0 + ? ({ type: 'index', value: tracks.selectedTextTrack } as SelectedTrack) + : ({ type: 'disabled' } as SelectedTrack)) } - }} - onEnd={async () => { - setCurrentTime(duration); - await traktAutosync.handlePlaybackEnd(duration, duration, 'ended'); - }} - onError={handleError} - onBuffer={setIsBuffering} - onReadyForDisplay={() => setIsPlayerReady(true)} - onPlaybackStalled={() => setIsBuffering(true)} - onPlaybackResume={() => setIsBuffering(false)} - screenWidth={screenDimensions.width} - screenHeight={screenDimensions.height} - customVideoStyles={{ width: '100%', height: '100%' }} - subtitleTextColor={customSubs.subtitleTextColor} - subtitleBackgroundColor={customSubs.subtitleBackground ? `rgba(0,0,0,${customSubs.subtitleBgOpacity})` : 'transparent'} - subtitleFontSize={customSubs.subtitleSize} - subtitleBottomOffset={customSubs.subtitleBottomOffset} - /> + onLoad={onLoad} + onProgress={(d) => { + if (!isSliderDragging) { + setCurrentTime(d.currentTime); + } + lastPlaybackTimeRef.current = d.currentTime || 0; + const newBuffered = d.buffered || 0; + if (Math.abs(newBuffered - buffered) > 0.5) { + setBuffered(newBuffered); + } + }} + onEnd={async () => { + setCurrentTime(duration); + await traktAutosync.handlePlaybackEnd(duration, duration, 'ended'); + }} + onError={handleError} + onBuffer={setIsBuffering} + screenWidth={screenDimensions.width} + screenHeight={screenDimensions.height} + customVideoStyles={{ width: '100%', height: '100%' }} + /> + )} {/* Custom Subtitles Overlay */} { onSlidingComplete={onSlidingComplete} buffered={buffered} formatTime={formatTime} - playerBackend="MPV" + playerBackend={(shouldUseMpvOnly || useMpvFallback) ? 'MPV' : 'AVPlayer'} + onSwitchToMPV={ + (!shouldUseMpvOnly && !useMpvFallback) + ? () => { + // Manual switch (like Android): go to MPV for this session and resume at current time. + lastPlaybackTimeRef.current = currentTime || lastPlaybackTimeRef.current || 0; + hasAviOSFailed.current = true; // reuse the resume-on-load path + setUseMpvFallback(true); + } + : undefined + } /> )} diff --git a/src/components/player/MPVPlayerComponent.tsx b/src/components/player/MPVPlayerComponent.tsx index 147f4a8..9250bf6 100644 --- a/src/components/player/MPVPlayerComponent.tsx +++ b/src/components/player/MPVPlayerComponent.tsx @@ -11,6 +11,7 @@ interface MPVPlayerProps { rate?: number; audioTrack?: number; textTrack?: number; + resizeMode?: 'contain' | 'cover' | 'stretch'; subtitleTextColor?: string; subtitleBackgroundColor?: string; subtitleFontSize?: number; diff --git a/src/components/player/controls/PlayerControls.tsx b/src/components/player/controls/PlayerControls.tsx index 2de9678..29d884b 100644 --- a/src/components/player/controls/PlayerControls.tsx +++ b/src/components/player/controls/PlayerControls.tsx @@ -342,8 +342,13 @@ export const PlayerControls: React.FC = ({ )} - {/* Switch to MPV Button - Android only, when using ExoPlayer */} - {Platform.OS === 'android' && onSwitchToMPV && useExoPlayer && ( + {/* Switch to MPV Button */} + {( + // Android: only show when ExoPlayer is active + (Platform.OS === 'android' && onSwitchToMPV && useExoPlayer) || + // iOS: show when AVPlayer is active + (Platform.OS === 'ios' && onSwitchToMPV && playerBackend === 'AVPlayer') + ) && ( ; + uri: string; + headers?: Record; + paused: boolean; + volume: number; + playbackSpeed: number; + resizeMode: 'contain' | 'cover' | 'stretch'; + zoomScale: number; + setZoomScale: (scale: number) => void; + lastZoomScale: number; + setLastZoomScale: (scale: number) => void; + + // Tracks (react-native-video style) + selectedAudioTrack?: SelectedTrack; + selectedTextTrack?: SelectedTrack; + + // Events + onLoad: (data: any) => void; + onProgress: (data: any) => void; + onEnd: () => void; + onError: (error: any) => void; + onBuffer: (isBuffering: boolean) => void; + + // Dimensions + screenWidth: number; + screenHeight: number; + customVideoStyles: any; +} + +export const AVPlayerSurface: React.FC = ({ + videoRef, + uri, + headers, + paused, + volume, + playbackSpeed, + resizeMode, + zoomScale, + setZoomScale, + lastZoomScale, + setLastZoomScale, + selectedAudioTrack, + selectedTextTrack, + onLoad, + onProgress, + onEnd, + onError, + onBuffer, + screenWidth, + screenHeight, + customVideoStyles, +}) => { + const pinchRef = useRef(null); + + const onPinchGestureEvent = (event: PinchGestureHandlerGestureEvent) => { + const { scale } = event.nativeEvent; + const newScale = Math.max(1, Math.min(lastZoomScale * scale, 1.1)); + setZoomScale(newScale); + }; + + const onPinchHandlerStateChange = (event: PinchGestureHandlerGestureEvent) => { + if (event.nativeEvent.state === State.END) { + setLastZoomScale(zoomScale); + } + }; + + const handleLoad = (data: any) => { + onLoad(data); + }; + + const handleProgress = (data: any) => { + // Match iOS player expected shape (KSPlayerCore reads currentTime + buffered) + onProgress({ + currentTime: data?.currentTime ?? 0, + buffered: data?.playableDuration ?? 0, + }); + }; + + return ( + + + + + ); +}; + +export default AVPlayerSurface; + diff --git a/src/components/player/ios/components/KSPlayerSurface.tsx b/src/components/player/ios/components/KSPlayerSurface.tsx index 48e78a0..7ce655e 100644 --- a/src/components/player/ios/components/KSPlayerSurface.tsx +++ b/src/components/player/ios/components/KSPlayerSurface.tsx @@ -136,6 +136,7 @@ export const KSPlayerSurface: React.FC = ({ rate={playbackSpeed} audioTrack={audioTrack} textTrack={textTrack} + resizeMode={resizeMode} subtitleTextColor={subtitleTextColor} subtitleBackgroundColor={subtitleBackgroundColor} subtitleFontSize={subtitleFontSize} diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 18e5d9a..3307452 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -107,6 +107,8 @@ export interface AppSettings { videoPlayerEngine: 'auto' | 'mpv'; // Video player engine: auto (ExoPlayer primary, MPV fallback) or mpv (MPV only) decoderMode: 'auto' | 'sw' | 'hw' | 'hw+'; // Decoder mode: auto (auto-copy), sw (software), hw (mediacodec-copy), hw+ (mediacodec) gpuMode: 'gpu' | 'gpu-next'; // GPU rendering mode: gpu (standard) or gpu-next (advanced HDR/color) + // iOS internal player settings + iosVideoPlayerEngine: 'auto' | 'mpv'; // iOS internal engine: auto (AVPlayer primary, MPV fallback) or mpv (MPV only) showDiscover: boolean; // Audio/Subtitle Language Preferences preferredSubtitleLanguage: string; // Preferred language for subtitles (ISO 639-1 code, e.g., 'en', 'es', 'fr') @@ -193,6 +195,8 @@ export const DEFAULT_SETTINGS: AppSettings = { videoPlayerEngine: 'auto', // Default to auto (ExoPlayer primary, MPV fallback) decoderMode: 'auto', // Default to auto (best compatibility and performance) gpuMode: 'gpu', // Default to gpu (gpu-next for advanced HDR) + // iOS internal player settings + iosVideoPlayerEngine: 'auto', // Default to auto (AVPlayer primary, MPV fallback) showDiscover: true, // Show Discover section in SearchScreen // Audio/Subtitle Language Preferences preferredSubtitleLanguage: 'en', // Default to English subtitles diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json index a42c202..6f8bc9c 100644 --- a/src/i18n/locales/ar.json +++ b/src/i18n/locales/ar.json @@ -1095,7 +1095,8 @@ "resume_title": "استكمال دائماً", "resume_desc": "تخطي مطالبة الاستكمال والمتابعة تلقائياً من حيث توقفت (إذا تمت مشاهدة أقل من 85%).", "engine_title": "محرك مشغل الفيديو", - "engine_desc": "التلقائي يستخدم ExoPlayer مع الرجوع لـ MPV. بعض التنسيقات مثل Dolby Vision و HDR قد لا يدعمها MPV، لذا يوصى بـ التلقائي لأفضل توافق.", + "engine_desc_android": "التلقائي يستخدم ExoPlayer مع الرجوع لـ MPV. بعض التنسيقات مثل Dolby Vision و HDR قد لا يدعمها MPV، لذا يوصى بـ التلقائي لأفضل توافق.", + "engine_desc_ios": "التلقائي يستخدم AVPlayer مع الرجوع لـ MPV. إذا فشل AVPlayer ستتحول المشاهدة إلى MPV لهذه الجلسة.", "decoder_title": "وضع فك التشفير", "decoder_desc": "كيف يتم فك تشفير الفيديو. يوصى بـ التلقائي لأفضل توازن.", "gpu_title": "رندرة GPU", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 4bca439..e30e73a 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1098,7 +1098,8 @@ "resume_title": "Always Resume", "resume_desc": "Skip the resume prompt and automatically continue where you left off (if less than 85% watched).", "engine_title": "Video Player Engine", - "engine_desc": "Auto uses ExoPlayer with MPV fallback. Some formats like Dolby Vision and HDR may not be supported by MPV, so Auto is recommended for best compatibility.", + "engine_desc_android": "Auto uses ExoPlayer with MPV fallback. Some formats like Dolby Vision and HDR may not be supported by MPV, so Auto is recommended for best compatibility.", + "engine_desc_ios": "Auto uses AVPlayer with MPV fallback. If AVPlayer fails, playback will switch to MPV for this session.", "decoder_title": "Decoder Mode", "decoder_desc": "How video is decoded. Auto is recommended for best balance.", "gpu_title": "GPU Rendering", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 2325eb3..8f8c4ba 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -1095,7 +1095,8 @@ "resume_title": "Reanudar siempre", "resume_desc": "Saltar el aviso de reanudar y continuar automáticamente donde lo dejaste (si se ha visto menos del 85%).", "engine_title": "Motor del reproductor", - "engine_desc": "Auto usa ExoPlayer con alternativa a MPV. Algunos formatos como Dolby Vision y HDR pueden no ser compatibles con MPV, por lo que se recomienda Auto.", + "engine_desc_android": "Auto usa ExoPlayer con alternativa a MPV. Algunos formatos como Dolby Vision y HDR pueden no ser compatibles con MPV, por lo que se recomienda Auto.", + "engine_desc_ios": "Auto usa AVPlayer con alternativa a MPV. Si AVPlayer falla, la reproducción cambiará a MPV para esta sesión.", "decoder_title": "Modo de decodificador", "decoder_desc": "Cómo se decodifica el video. Auto es la opción recomendada.", "gpu_title": "Renderizado por GPU", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 90177c8..a79933a 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -1095,7 +1095,8 @@ "resume_title": "Toujours reprendre", "resume_desc": "Passer l'invite de reprise et continuer automatiquement là où vous vous étiez arrêté (si moins de 85% vus).", "engine_title": "Moteur du lecteur vidéo", - "engine_desc": "Auto utilise ExoPlayer avec un repli sur MPV. Certains formats comme Dolby Vision et HDR peuvent ne pas être supportés par MPV, donc Auto est recommandé pour une meilleure compatibilité.", + "engine_desc_android": "Auto utilise ExoPlayer avec un repli sur MPV. Certains formats comme Dolby Vision et HDR peuvent ne pas être supportés par MPV, donc Auto est recommandé pour une meilleure compatibilité.", + "engine_desc_ios": "Auto utilise AVPlayer avec un repli sur MPV. Si AVPlayer échoue, la lecture basculera vers MPV pour cette session.", "decoder_title": "Mode décodeur", "decoder_desc": "Comment la vidéo est décodée. Auto est recommandé pour le meilleur équilibre.", "gpu_title": "Rendu GPU", diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 42bc7b0..3fa10a8 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -1095,7 +1095,8 @@ "resume_title": "Riprendi Sempre", "resume_desc": "Salta la richiesta di ripresa e continua automaticamente da dove avevi interrotto (se visto meno dell'85%).", "engine_title": "Motore Video Player", - "engine_desc": "Auto utilizza ExoPlayer con MPV come ripiego. Alcuni formati come Dolby Vision e HDR potrebbero non essere supportati da MPV, quindi Auto è raccomandato.", + "engine_desc_android": "Auto utilizza ExoPlayer con MPV come ripiego. Alcuni formati come Dolby Vision e HDR potrebbero non essere supportati da MPV, quindi Auto è raccomandato.", + "engine_desc_ios": "Auto utilizza AVPlayer con MPV come ripiego. Se AVPlayer fallisce, la riproduzione passerà a MPV per questa sessione.", "decoder_title": "Modalità Decodificatore", "decoder_desc": "Come viene decodificato il video. Auto è raccomandato per il miglior bilanciamento.", "gpu_title": "Rendering GPU", diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json index 2c3f5bd..04f4c05 100644 --- a/src/i18n/locales/pt-BR.json +++ b/src/i18n/locales/pt-BR.json @@ -986,7 +986,8 @@ "resume_title": "Sempre Retomar", "resume_desc": "Pular o aviso de retomar e continuar automaticamente de onde parou (se assistido menos de 85%).", "engine_title": "Motor do Player de Vídeo", - "engine_desc": "Escolha o motor de reprodução de vídeo subjacente (apenas Android)", + "engine_desc_android": "Escolha o motor de reprodução de vídeo subjacente (apenas Android)", + "engine_desc_ios": "Auto usa AVPlayer com reserva para MPV. Se o AVPlayer falhar, a reprodução mudará para MPV nesta sessão.", "option_auto": "Auto", "option_auto_desc_engine": "ExoPlayer + MPV como reserva", "option_mpv": "MPV", diff --git a/src/i18n/locales/pt-PT.json b/src/i18n/locales/pt-PT.json index a40280e..afd7b5c 100644 --- a/src/i18n/locales/pt-PT.json +++ b/src/i18n/locales/pt-PT.json @@ -986,7 +986,8 @@ "resume_title": "Sempre Retomar", "resume_desc": "Saltar o aviso de retomar e continuar automaticamente de onde parou (se assistido menos de 85%).", "engine_title": "Motor do reprodutor", - "engine_desc": "Escolhe o motor de reprodução de vídeo subjacente (apenas Android)", + "engine_desc_android": "Escolhe o motor de reprodução de vídeo subjacente (apenas Android)", + "engine_desc_ios": "Auto usa AVPlayer com reserva para MPV. Se o AVPlayer falhar, a reprodução mudará para MPV nesta sessão.", "option_auto": "Auto", "option_auto_desc_engine": "ExoPlayer + MPV como reserva", "option_mpv": "MPV", diff --git a/src/screens/PlayerSettingsScreen.tsx b/src/screens/PlayerSettingsScreen.tsx index 93f4031..28be9c3 100644 --- a/src/screens/PlayerSettingsScreen.tsx +++ b/src/screens/PlayerSettingsScreen.tsx @@ -333,7 +333,7 @@ const PlayerSettingsScreen: React.FC = () => { { color: currentTheme.colors.textMuted }, ]} > - {t('player.engine_desc')} + {t('player.engine_desc_android')} @@ -497,6 +497,67 @@ const PlayerSettingsScreen: React.FC = () => { )} + {/* Video Player Engine for iOS Internal Player */} + {Platform.OS === 'ios' && settings.preferredPlayer === 'internal' && ( + + + + + + + + {t('player.engine_title')} + + + {t('player.engine_desc_ios')} + + + + + {([ + { id: 'auto', label: t('player.option_auto') }, + { id: 'mpv', label: t('player.option_mpv') }, + ] as const).map((option) => ( + updateSetting('iosVideoPlayerEngine', option.id)} + style={[ + styles.optionButton, + styles.optionButtonWide, + settings.iosVideoPlayerEngine === option.id && { backgroundColor: currentTheme.colors.primary }, + ]} + > + + {option.label} + + + ))} + + + )} + {/* External Player for Downloads */} {((Platform.OS === 'android' && settings.useExternalPlayer) || (Platform.OS === 'ios' && settings.preferredPlayer !== 'internal')) && (