mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
updated langs
This commit is contained in:
parent
3ef4f20781
commit
30ebba0722
14 changed files with 354 additions and 62 deletions
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ interface MPVPlayerProps {
|
|||
rate?: number;
|
||||
audioTrack?: number;
|
||||
textTrack?: number;
|
||||
resizeMode?: 'contain' | 'cover' | 'stretch';
|
||||
subtitleTextColor?: string;
|
||||
subtitleBackgroundColor?: string;
|
||||
subtitleFontSize?: number;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
124
src/components/player/ios/components/AVPlayerSurface.tsx
Normal file
124
src/components/player/ios/components/AVPlayerSurface.tsx
Normal 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;
|
||||
|
||||
|
|
@ -136,6 +136,7 @@ export const KSPlayerSurface: React.FC<KSPlayerSurfaceProps> = ({
|
|||
rate={playbackSpeed}
|
||||
audioTrack={audioTrack}
|
||||
textTrack={textTrack}
|
||||
resizeMode={resizeMode}
|
||||
subtitleTextColor={subtitleTextColor}
|
||||
subtitleBackgroundColor={subtitleBackgroundColor}
|
||||
subtitleFontSize={subtitleFontSize}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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')) && (
|
||||
|
|
|
|||
Loading…
Reference in a new issue