updated langs

This commit is contained in:
tapframe 2026-01-10 02:11:32 +05:30
parent 3ef4f20781
commit 30ebba0722
14 changed files with 354 additions and 62 deletions

View file

@ -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<VideoRef>(null);
const shouldUseMpvOnly = Platform.OS === 'ios' && settings.iosVideoPlayerEngine === 'mpv';
const [useMpvFallback, setUseMpvFallback] = useState<boolean>(shouldUseMpvOnly);
const hasAviOSFailed = useRef(false);
const lastPlaybackTimeRef = useRef<number>(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 */}
<KSPlayerSurface
ksPlayerRef={mpvPlayerRef}
uri={uri}
headers={headers}
paused={paused}
volume={volume}
playbackSpeed={speedControl.playbackSpeed}
resizeMode={resizeMode}
zoomScale={zoomScale}
setZoomScale={setZoomScale}
lastZoomScale={lastZoomScale}
setLastZoomScale={setLastZoomScale}
audioTrack={tracks.selectedAudioTrack ?? undefined}
textTrack={customSubs.useCustomSubtitles ? -1 : tracks.selectedTextTrack}
onAudioTracks={(d) => tracks.setKsAudioTracks(d.audioTracks || [])}
onTextTracks={(d) => tracks.setKsTextTracks(d.textTracks || [])}
onLoad={onLoad}
onProgress={(d) => {
if (!isSliderDragging) {
setCurrentTime(d.currentTime);
{(shouldUseMpvOnly || useMpvFallback) ? (
<KSPlayerSurface
ksPlayerRef={mpvPlayerRef}
uri={uri}
headers={headers}
paused={paused}
volume={volume}
playbackSpeed={speedControl.playbackSpeed}
resizeMode={resizeMode}
zoomScale={zoomScale}
setZoomScale={setZoomScale}
lastZoomScale={lastZoomScale}
setLastZoomScale={setLastZoomScale}
audioTrack={tracks.selectedAudioTrack ?? undefined}
textTrack={customSubs.useCustomSubtitles ? -1 : tracks.selectedTextTrack}
onAudioTracks={(d) => 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}
/>
) : (
<AVPlayerSurface
videoRef={avPlayerRef}
uri={uri}
headers={headers}
paused={paused}
volume={volume}
playbackSpeed={speedControl.playbackSpeed}
resizeMode={resizeMode}
zoomScale={zoomScale}
setZoomScale={setZoomScale}
lastZoomScale={lastZoomScale}
setLastZoomScale={setLastZoomScale}
selectedAudioTrack={
tracks.selectedAudioTrack != null
? ({ type: 'index', value: tracks.selectedAudioTrack } as SelectedTrack)
: undefined
}
// 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);
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 */}
<CustomSubtitles
@ -733,7 +812,17 @@ const KSPlayerCore: React.FC = () => {
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
}
/>
</View>
)}

View file

@ -11,6 +11,7 @@ interface MPVPlayerProps {
rate?: number;
audioTrack?: number;
textTrack?: number;
resizeMode?: 'contain' | 'cover' | 'stretch';
subtitleTextColor?: string;
subtitleBackgroundColor?: string;
subtitleFontSize?: number;

View file

@ -342,8 +342,13 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
)}
</View>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
{/* 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')
) && (
<TouchableOpacity
style={{ padding: 8 }}
onPress={onSwitchToMPV}

View file

@ -0,0 +1,124 @@
import React, { useRef } from 'react';
import { Animated } from 'react-native';
import { PinchGestureHandler, PinchGestureHandlerGestureEvent, State } from 'react-native-gesture-handler';
import Video, { VideoRef, SelectedTrack } from 'react-native-video';
interface AVPlayerSurfaceProps {
videoRef: React.RefObject<VideoRef>;
uri: string;
headers?: Record<string, string>;
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<AVPlayerSurfaceProps> = ({
videoRef,
uri,
headers,
paused,
volume,
playbackSpeed,
resizeMode,
zoomScale,
setZoomScale,
lastZoomScale,
setLastZoomScale,
selectedAudioTrack,
selectedTextTrack,
onLoad,
onProgress,
onEnd,
onError,
onBuffer,
screenWidth,
screenHeight,
customVideoStyles,
}) => {
const pinchRef = useRef<PinchGestureHandler>(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 (
<PinchGestureHandler
ref={pinchRef}
onGestureEvent={onPinchGestureEvent}
onHandlerStateChange={onPinchHandlerStateChange}
>
<Animated.View style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
transform: [{ scale: zoomScale }]
}}>
<Video
ref={videoRef}
source={{ uri, headers: headers || {} }}
style={customVideoStyles.width ? customVideoStyles : { width: screenWidth, height: screenHeight }}
paused={paused}
volume={volume}
rate={playbackSpeed}
resizeMode={resizeMode as any}
selectedAudioTrack={selectedAudioTrack}
selectedTextTrack={selectedTextTrack}
onLoad={handleLoad}
onProgress={handleProgress}
onEnd={onEnd}
onError={onError}
onBuffer={(b: any) => onBuffer(!!b?.isBuffering)}
progressUpdateInterval={250}
// Keep background behavior consistent with the rest of the player logic
playInBackground={false}
playWhenInactive={false}
ignoreSilentSwitch="ignore"
/>
</Animated.View>
</PinchGestureHandler>
);
};
export default AVPlayerSurface;

View file

@ -136,6 +136,7 @@ export const KSPlayerSurface: React.FC<KSPlayerSurfaceProps> = ({
rate={playbackSpeed}
audioTrack={audioTrack}
textTrack={textTrack}
resizeMode={resizeMode}
subtitleTextColor={subtitleTextColor}
subtitleBackgroundColor={subtitleBackgroundColor}
subtitleFontSize={subtitleFontSize}

View file

@ -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

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -333,7 +333,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.textMuted },
]}
>
{t('player.engine_desc')}
{t('player.engine_desc_android')}
</Text>
</View>
</View>
@ -497,6 +497,67 @@ const PlayerSettingsScreen: React.FC = () => {
</>
)}
{/* Video Player Engine for iOS Internal Player */}
{Platform.OS === 'ios' && settings.preferredPlayer === 'internal' && (
<View style={[styles.settingItem, styles.settingItemBorder, { borderTopColor: 'rgba(255,255,255,0.08)', borderTopWidth: 1 }]}>
<View style={styles.settingContent}>
<View style={[
styles.settingIconContainer,
{ backgroundColor: 'rgba(255,255,255,0.1)' }
]}>
<MaterialIcons
name="play-circle-filled"
size={20}
color={currentTheme.colors.primary}
/>
</View>
<View style={styles.settingText}>
<Text
style={[
styles.settingTitle,
{ color: currentTheme.colors.text },
]}
>
{t('player.engine_title')}
</Text>
<Text
style={[
styles.settingDescription,
{ color: currentTheme.colors.textMuted },
]}
>
{t('player.engine_desc_ios')}
</Text>
</View>
</View>
<View style={styles.optionButtonsRow}>
{([
{ id: 'auto', label: t('player.option_auto') },
{ id: 'mpv', label: t('player.option_mpv') },
] as const).map((option) => (
<TouchableOpacity
key={option.id}
onPress={() => updateSetting('iosVideoPlayerEngine', option.id)}
style={[
styles.optionButton,
styles.optionButtonWide,
settings.iosVideoPlayerEngine === option.id && { backgroundColor: currentTheme.colors.primary },
]}
>
<Text
style={[
styles.optionButtonText,
{ color: settings.iosVideoPlayerEngine === option.id ? '#fff' : currentTheme.colors.text },
]}
>
{option.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
)}
{/* External Player for Downloads */}
{((Platform.OS === 'android' && settings.useExternalPlayer) ||
(Platform.OS === 'ios' && settings.preferredPlayer !== 'internal')) && (