diff --git a/package.json b/package.json index e4f9750..54fbb23 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "expo-glass-effect": "~0.1.4", "expo-haptics": "~15.0.7", "expo-intent-launcher": "~13.0.7", - "expo-libvlc-player": "^2.2.3", "expo-linear-gradient": "~15.0.7", "expo-localization": "~17.0.7", "expo-navigation-bar": "~5.0.10", @@ -104,4 +103,4 @@ "xcode": "^3.0.1" }, "private": true -} +} \ No newline at end of file diff --git a/src/components/home/AppleTVHero.tsx b/src/components/home/AppleTVHero.tsx index 8fca9f4..29a53c4 100644 --- a/src/components/home/AppleTVHero.tsx +++ b/src/components/home/AppleTVHero.tsx @@ -649,7 +649,6 @@ const AppleTVHero: React.FC = ({ streamProvider: cachedStream.stream.addonId || cachedStream.stream.addonName || cachedStream.stream.name, streamName: cachedStream.stream.name || cachedStream.stream.title || 'Unnamed Stream', headers: cachedStream.stream.headers || undefined, - forceVlc: false, id: currentItem.id, type: currentItem.type, episodeId: episodeId, diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index 7cbfa49..96d132c 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -977,7 +977,6 @@ const ContinueWatchingSection = React.forwardRef((props, re streamProvider: cachedStream.stream.addonId || cachedStream.stream.addonName || cachedStream.stream.name, streamName: cachedStream.stream.name || cachedStream.stream.title || 'Unnamed Stream', headers: cachedStream.stream.headers || undefined, - forceVlc: false, id: item.id, type: item.type, episodeId: episodeId, diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 9a0c7d4..9eea59a 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -13,9 +13,8 @@ import { useOpeningAnimation } from './hooks'; -// Android-specific hooks (VLC integration, dual player support) +// Android-specific hooks import { usePlayerSetup } from './android/hooks/usePlayerSetup'; -import { useVlcPlayer } from './android/hooks/useVlcPlayer'; import { usePlayerTracks } from './android/hooks/usePlayerTracks'; import { useWatchProgress } from './android/hooks/useWatchProgress'; import { usePlayerControls } from './android/hooks/usePlayerControls'; @@ -46,7 +45,7 @@ import { MpvPlayerRef } from './android/MpvPlayer'; // Utils import { logger } from '../../utils/logger'; import { styles } from './utils/playerStyles'; -import { formatTime, isHlsStream, processUrlForVLC, getHlsHeaders, defaultAndroidHeaders, parseSRT } from './utils/playerUtils'; +import { formatTime, isHlsStream, getHlsHeaders, defaultAndroidHeaders, parseSRT } from './utils/playerUtils'; import { storageService } from '../../services/storageService'; import stremioService from '../../services/stremioService'; import { WyzieSubtitle, SubtitleCue } from './utils/playerTypes'; @@ -71,28 +70,12 @@ const AndroidVideoPlayer: React.FC = () => { const modals = usePlayerModals(); const speedControl = useSpeedControl(); - const forceVlc = useMemo(() => { - const rp: any = route.params || {}; - const v = rp.forceVlc !== undefined ? rp.forceVlc : rp.forceVLC; - return typeof v === 'string' ? v.toLowerCase() === 'true' : Boolean(v); - }, [route.params]); - - const useVLC = (Platform.OS === 'android' && forceVlc); - const videoRef = useRef(null); const mpvPlayerRef = useRef(null); - const vlcHook = useVlcPlayer(useVLC, playerState.paused, playerState.currentTime); - const tracksHook = usePlayerTracks( - useVLC, - vlcHook.vlcAudioTracks, - vlcHook.vlcSubtitleTracks, - vlcHook.vlcSelectedAudioTrack, - vlcHook.vlcSelectedSubtitleTrack - ); + const tracksHook = usePlayerTracks(); const [currentStreamUrl, setCurrentStreamUrl] = useState(uri); const [currentVideoType, setCurrentVideoType] = useState((route.params as any).videoType); - const processedStreamUrl = useMemo(() => useVLC ? processUrlForVLC(currentStreamUrl) : currentStreamUrl, [currentStreamUrl, useVLC]); const [availableStreams, setAvailableStreams] = useState(passedAvailableStreams || {}); const [currentQuality, setCurrentQuality] = useState(quality); @@ -132,9 +115,7 @@ const AndroidVideoPlayer: React.FC = () => { const setupHook = usePlayerSetup(playerState.setScreenDimensions, setVolume, setBrightness, playerState.paused); const controlsHook = usePlayerControls( - mpvPlayerRef, // Use mpvPlayerRef for MPV player - vlcHook.vlcPlayerRef, - useVLC, + mpvPlayerRef, playerState.paused, playerState.setPaused, playerState.currentTime, @@ -265,23 +246,21 @@ const AndroidVideoPlayer: React.FC = () => { playerState.setVideoAspectRatio(16 / 9); } - if (!useVLC) { - if (data.audioTracks) { - const formatted = data.audioTracks.map((t: any, i: number) => ({ - id: t.index !== undefined ? t.index : i, - name: t.title || t.name || `Track ${i + 1}`, - language: t.language - })); - tracksHook.setRnVideoAudioTracks(formatted); - } - if (data.textTracks) { - const formatted = data.textTracks.map((t: any, i: number) => ({ - id: t.index !== undefined ? t.index : i, - name: t.title || t.name || `Track ${i + 1}`, - language: t.language - })); - tracksHook.setRnVideoTextTracks(formatted); - } + if (data.audioTracks) { + const formatted = data.audioTracks.map((t: any, i: number) => ({ + id: t.index !== undefined ? t.index : i, + name: t.title || t.name || `Track ${i + 1}`, + language: t.language + })); + tracksHook.setRnVideoAudioTracks(formatted); + } + if (data.textTracks) { + const formatted = data.textTracks.map((t: any, i: number) => ({ + id: t.index !== undefined ? t.index : i, + name: t.title || t.name || `Track ${i + 1}`, + language: t.language + })); + tracksHook.setRnVideoTextTracks(formatted); } playerState.setIsVideoLoaded(true); @@ -299,7 +278,7 @@ const AndroidVideoPlayer: React.FC = () => { } }, 200); } - }, [id, type, episodeId, useVLC, playerState.isMounted, watchProgress.initialPosition]); + }, [id, type, episodeId, playerState.isMounted, watchProgress.initialPosition]); const handleProgress = useCallback((data: any) => { if (playerState.isDragging.current || playerState.isSeeking.current || !playerState.isMounted.current || setupHook.isAppBackgrounded.current) return; @@ -385,7 +364,6 @@ const AndroidVideoPlayer: React.FC = () => { streamProvider: newProvider, streamName: newStreamName, headers: stream.headers || undefined, - forceVlc: false, id, type: 'series', episodeId: ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`, @@ -508,24 +486,12 @@ const AndroidVideoPlayer: React.FC = () => { { onError={(err: any) => { logger.error('Video Error', err); - // Check for decoding errors to switch to VLC - const errorString = err?.errorString || err?.error?.errorString; - const errorCode = err?.errorCode || err?.error?.errorCode; - const causeMessage = err?.error?.cause?.message; - - const isDecodingError = - (errorString && errorString.includes('ERROR_CODE_DECODING_FAILED')) || - errorCode === '24003' || - (causeMessage && causeMessage.includes('MediaCodecVideoRenderer error')); - - if (!useVLC && isDecodingError) { - const toastId = toast.loading('Decoding error. Switching to VLC Player...'); - setTimeout(() => toast.dismiss(toastId), 3000); - - // We can just show a normal toast or use the existing modal system if we want, - // but checking the file imports, I don't see Toast imported. - // Let's implement the navigation replace. - - // Using a simple navigation replace to force VLC - (navigation as any).replace('PlayerAndroid', { - ...route.params, - forceVlc: true - }); - return; - } - // Determine the actual error message let displayError = 'An unknown error occurred'; @@ -585,7 +525,6 @@ const AndroidVideoPlayer: React.FC = () => { modals.setShowErrorModal(true); }} onBuffer={(buf) => playerState.setIsBuffering(buf.isBuffering)} - onTracksUpdate={vlcHook.handleVlcTracksUpdate} onTracksChanged={(data) => { console.log('[AndroidVideoPlayer] onTracksChanged:', data); if (data?.audioTracks) { @@ -605,17 +544,11 @@ const AndroidVideoPlayer: React.FC = () => { tracksHook.setRnVideoTextTracks(formatted); } }} - vlcPlayerRef={vlcHook.vlcPlayerRef} mpvPlayerRef={mpvPlayerRef} - videoRef={videoRef} pinchRef={useRef(null)} onPinchGestureEvent={() => { }} onPinchHandlerStateChange={() => { }} - vlcLoadedRef={vlcHook.vlcLoadedRef} screenDimensions={playerState.screenDimensions} - customVideoStyles={{}} - loadStartAtRef={loadStartAtRef} - firstFrameAtRef={firstFrameAtRef} /> {/* Custom Subtitles for addon subtitles */} @@ -698,7 +631,7 @@ const AndroidVideoPlayer: React.FC = () => { }} buffered={playerState.buffered} formatTime={formatTime} - playerBackend={useVLC ? 'VLC' : 'ExoPlayer'} + playerBackend={'MPV'} /> { ksAudioTracks={tracksHook.ksAudioTracks} selectedAudioTrack={tracksHook.computedSelectedAudioTrack} selectAudioTrack={(trackId) => { - if (useVLC) { - vlcHook.selectVlcAudioTrack(trackId); - } else { - tracksHook.setSelectedAudioTrack(trackId === null ? null : { type: 'index', value: trackId }); - // Actually tell MPV to switch the audio track - if (trackId !== null && mpvPlayerRef.current) { - mpvPlayerRef.current.setAudioTrack(trackId); - } + tracksHook.setSelectedAudioTrack(trackId === null ? null : { type: 'index', value: trackId }); + // Actually tell MPV to switch the audio track + if (trackId !== null && mpvPlayerRef.current) { + mpvPlayerRef.current.setAudioTrack(trackId); } }} /> @@ -752,20 +681,16 @@ const AndroidVideoPlayer: React.FC = () => { ksTextTracks={tracksHook.ksTextTracks} selectedTextTrack={tracksHook.computedSelectedTextTrack} useCustomSubtitles={useCustomSubtitles} - isKsPlayerActive={!useVLC} + isKsPlayerActive={true} subtitleSize={subtitleSize} subtitleBackground={subtitleBackground} fetchAvailableSubtitles={fetchAvailableSubtitles} loadWyzieSubtitle={loadWyzieSubtitle} selectTextTrack={(trackId) => { - if (useVLC) { - vlcHook.selectVlcSubtitleTrack(trackId); - } else { - tracksHook.setSelectedTextTrack(trackId); - // Actually tell MPV to switch the subtitle track - if (mpvPlayerRef.current) { - mpvPlayerRef.current.setSubtitleTrack(trackId); - } + tracksHook.setSelectedTextTrack(trackId); + // Actually tell MPV to switch the subtitle track + if (mpvPlayerRef.current) { + mpvPlayerRef.current.setSubtitleTrack(trackId); } // Disable custom subtitles when selecting built-in track setUseCustomSubtitles(false); diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index 0b21996..9e2fe75 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -302,6 +302,17 @@ const KSPlayerCore: React.FC = () => { } }, [imdbId]); + // Sync custom subtitle text with current playback time + useEffect(() => { + if (!customSubs.useCustomSubtitles || customSubs.customSubtitles.length === 0) return; + + const adjustedTime = currentTime + (customSubs.subtitleOffsetSec || 0); + const cueNow = customSubs.customSubtitles.find( + cue => adjustedTime >= cue.start && adjustedTime <= cue.end + ); + customSubs.setCurrentSubtitle(cueNow ? cueNow.text : ''); + }, [currentTime, customSubs.useCustomSubtitles, customSubs.customSubtitles, customSubs.subtitleOffsetSec]); + // Handlers const onLoad = (data: any) => { setDuration(data.duration); @@ -416,7 +427,7 @@ const KSPlayerCore: React.FC = () => { headers: stream.headers || undefined, id, type: 'series', - episodeId: ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`, + episodeId: ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number} `, imdbId: imdbId ?? undefined, backdrop: backdrop || undefined, }); diff --git a/src/components/player/VlcVideoPlayer.tsx b/src/components/player/VlcVideoPlayer.tsx deleted file mode 100644 index 75a7dba..0000000 --- a/src/components/player/VlcVideoPlayer.tsx +++ /dev/null @@ -1,364 +0,0 @@ -import React, { useState, useRef, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'; -import { View, Dimensions } from 'react-native'; -import { logger } from '../../utils/logger'; - -// Dynamic import to avoid iOS loading Android native module -let LibVlcPlayerViewComponent: any = null; - -try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const mod = require('expo-libvlc-player'); - LibVlcPlayerViewComponent = mod?.LibVlcPlayerView || null; -} catch { - LibVlcPlayerViewComponent = null; -} - -interface VlcVideoPlayerProps { - source: string; - volume: number; - playbackSpeed: number; - zoomScale: number; - resizeMode: 'contain' | 'cover' | 'none'; - onLoad: (data: any) => void; - onProgress: (data: any) => void; - onSeek: (data: any) => void; - onEnd: () => void; - onError: (error: any) => void; - onTracksUpdate: (tracks: { audio: any[], subtitle: any[] }) => void; - selectedAudioTrack?: number | null; - selectedSubtitleTrack?: number | null; - restoreTime?: number | null; - forceRemount?: boolean; - key?: string; -} - -interface VlcTrack { - id: number; - name: string; - language?: string; -} - -export interface VlcPlayerRef { - seek: (timeInSeconds: number) => void; - pause: () => void; - play: () => void; -} - -const VlcVideoPlayer = forwardRef(({ - source, - volume, - playbackSpeed, - zoomScale, - resizeMode, - onLoad, - onProgress, - onSeek, - onEnd, - onError, - onTracksUpdate, - selectedAudioTrack, - selectedSubtitleTrack, - restoreTime, - forceRemount, - key, -}, ref) => { - const vlcRef = useRef(null); - const [vlcActive, setVlcActive] = useState(true); - const [duration, setDuration] = useState(0); - const [videoAspectRatio, setVideoAspectRatio] = useState(null); - - // Expose imperative methods to parent component - useImperativeHandle(ref, () => ({ - seek: (timeInSeconds: number) => { - if (vlcRef.current && typeof vlcRef.current.seek === 'function') { - const fraction = Math.min(Math.max(timeInSeconds / (duration || 1), 0), 0.999); - vlcRef.current.seek(fraction); - logger.log(`[VLC] Seeked to ${timeInSeconds}s (${fraction.toFixed(3)})`); - } - }, - pause: () => { - if (vlcRef.current && typeof vlcRef.current.pause === 'function') { - vlcRef.current.pause(); - logger.log('[VLC] Paused'); - } - }, - play: () => { - if (vlcRef.current && typeof vlcRef.current.play === 'function') { - vlcRef.current.play(); - logger.log('[VLC] Played'); - } - } - }), [duration]); - - // Compute aspect ratio string for VLC - const toVlcRatio = useCallback((w: number, h: number): string => { - const a = Math.max(1, Math.round(w)); - const b = Math.max(1, Math.round(h)); - const gcd = (x: number, y: number): number => (y === 0 ? x : gcd(y, x % y)); - const g = gcd(a, b); - return `${Math.floor(a / g)}:${Math.floor(b / g)}`; - }, []); - - const screenDimensions = Dimensions.get('screen'); - - const vlcAspectRatio = useMemo(() => { - // For VLC, no forced aspect ratio - let it preserve natural aspect - return undefined; - }, [resizeMode, screenDimensions.width, screenDimensions.height, toVlcRatio]); - - const clientScale = useMemo(() => { - if (!videoAspectRatio || screenDimensions.width <= 0 || screenDimensions.height <= 0) { - return 1; - } - if (resizeMode === 'cover') { - const screenAR = screenDimensions.width / screenDimensions.height; - return Math.max(screenAR / videoAspectRatio, videoAspectRatio / screenAR); - } - return 1; - }, [resizeMode, videoAspectRatio, screenDimensions.width, screenDimensions.height]); - - // VLC options for better playback - const vlcOptions = useMemo(() => { - return [ - '--network-caching=2000', - '--clock-jitter=0', - '--http-reconnect', - '--sout-mux-caching=2000' - ]; - }, []); - - // VLC tracks prop - const vlcTracks = useMemo(() => ({ - audio: selectedAudioTrack, - video: 0, // Use first video track - subtitle: selectedSubtitleTrack - }), [selectedAudioTrack, selectedSubtitleTrack]); - - const handleFirstPlay = useCallback((info: any) => { - try { - logger.log('[VLC] Video loaded, extracting tracks...'); - logger.log('[AndroidVideoPlayer][VLC] Video loaded successfully'); - - // Process VLC tracks using optimized function - if (info?.tracks) { - processVlcTracks(info.tracks); - } - - const lenSec = (info?.length ?? 0) / 1000; - const width = info?.width || 0; - const height = info?.height || 0; - setDuration(lenSec); - onLoad({ duration: lenSec, naturalSize: width && height ? { width, height } : undefined }); - - if (width > 0 && height > 0) { - setVideoAspectRatio(width / height); - } - - // Restore playback position after remount (workaround for surface detach) - if (restoreTime !== undefined && restoreTime !== null && restoreTime > 0) { - setTimeout(() => { - if (vlcRef.current && typeof vlcRef.current.seek === 'function') { - const seekPosition = Math.min(restoreTime / lenSec, 0.999); // Convert to fraction - vlcRef.current.seek(seekPosition); - logger.log('[VLC] Seeked to restore position'); - } - }, 500); // Small delay to ensure player is ready - } - } catch (e) { - logger.error('[VLC] onFirstPlay error:', e); - logger.warn('[AndroidVideoPlayer][VLC] onFirstPlay parse error', e); - } - }, [onLoad, restoreTime]); - - const handlePositionChanged = useCallback((ev: any) => { - const pos = typeof ev?.position === 'number' ? ev.position : 0; - // We need duration to calculate current time, but it's not available here - // The parent component should handle this calculation - onProgress({ position: pos }); - }, [onProgress]); - - const handlePlaying = useCallback(() => { - setVlcActive(true); - }, []); - - const handlePaused = useCallback(() => { - setVlcActive(false); - }, []); - - const handleEndReached = useCallback(() => { - onEnd(); - }, [onEnd]); - - const handleEncounteredError = useCallback((e: any) => { - logger.error('[AndroidVideoPlayer][VLC] Encountered error:', e); - onError(e); - }, [onError]); - - const handleBackground = useCallback(() => { - logger.log('[VLC] App went to background'); - }, []); - - const handleESAdded = useCallback((tracks: any) => { - try { - logger.log('[VLC] ES Added - processing tracks...'); - processVlcTracks(tracks); - } catch (e) { - logger.error('[VLC] onESAdded error:', e); - logger.warn('[AndroidVideoPlayer][VLC] onESAdded parse error', e); - } - }, []); - - // Format VLC tracks to match RN Video format - raw version - const formatVlcTracks = useCallback((vlcTracks: Array<{id: number, name: string}>): VlcTrack[] => { - if (!Array.isArray(vlcTracks)) return []; - return vlcTracks.map(track => { - // Just extract basic language info if available, but keep the full name - let language = undefined; - let displayName = track.name || `Track ${track.id + 1}`; - - // Log the raw track data for debugging - if (__DEV__) { - logger.log(`[VLC] Raw track data:`, { id: track.id, name: track.name }); - } - - // Only extract language from brackets if present, but keep full name - const languageMatch = track.name?.match(/\[([^\]]+)\]/); - if (languageMatch && languageMatch[1]) { - language = languageMatch[1].trim(); - } - - return { - id: track.id, - name: displayName, // Show exactly what VLC provides - language: language - }; - }); - }, []); - - // Optimized VLC track processing function with reduced JSON operations - const processVlcTracks = useCallback((tracks: any) => { - if (!tracks) return; - - // Log raw VLC tracks data for debugging - if (__DEV__) { - logger.log(`[VLC] Raw tracks data:`, tracks); - } - - const { audio = [], subtitle = [] } = tracks; - - // Process audio tracks - if (Array.isArray(audio) && audio.length > 0) { - const formattedAudio = formatVlcTracks(audio); - if (__DEV__) { - logger.log(`[VLC] Audio tracks updated:`, formattedAudio.length); - } - } - - // Process subtitle tracks - if (Array.isArray(subtitle) && subtitle.length > 0) { - const formattedSubs = formatVlcTracks(subtitle); - if (__DEV__) { - logger.log(`[VLC] Subtitle tracks updated:`, formattedSubs.length); - } - } - - // Notify parent of track updates - onTracksUpdate({ audio, subtitle }); - }, [formatVlcTracks, onTracksUpdate]); - - // Process URL for VLC compatibility - const processUrlForVLC = useCallback((url: string): string => { - if (!url || typeof url !== 'string') { - logger.warn('[AndroidVideoPlayer][VLC] Invalid URL provided:', url); - return url || ''; - } - - try { - // Check if URL is already properly formatted - const urlObj = new URL(url); - - // Handle special characters in the pathname that might cause issues - const pathname = urlObj.pathname; - const search = urlObj.search; - const hash = urlObj.hash; - - // Decode and re-encode the pathname to handle double-encoding - const decodedPathname = decodeURIComponent(pathname); - const encodedPathname = encodeURI(decodedPathname); - - // Reconstruct the URL - const processedUrl = `${urlObj.protocol}//${urlObj.host}${encodedPathname}${search}${hash}`; - - logger.log(`[AndroidVideoPlayer][VLC] URL processed: ${url} -> ${processedUrl}`); - return processedUrl; - } catch (error) { - logger.warn(`[AndroidVideoPlayer][VLC] URL processing failed, using original: ${error}`); - return url; - } - }, []); - - const processedSource = useMemo(() => processUrlForVLC(source), [source, processUrlForVLC]); - - if (!LibVlcPlayerViewComponent) { - return ( - - {/* VLC not available fallback */} - - ); - } - - return ( - - - - ); -}); - -VlcVideoPlayer.displayName = 'VlcVideoPlayer'; - -export default VlcVideoPlayer; diff --git a/src/components/player/android/components/VideoSurface.tsx b/src/components/player/android/components/VideoSurface.tsx index 0899e2e..2e05f7f 100644 --- a/src/components/player/android/components/VideoSurface.tsx +++ b/src/components/player/android/components/VideoSurface.tsx @@ -30,27 +30,6 @@ interface VideoSurfaceProps { onPinchGestureEvent: any; onPinchHandlerStateChange: any; screenDimensions: { width: number, height: number }; - - // Legacy props (kept for compatibility but unused with MPV) - useVLC?: boolean; - forceVlcRemount?: boolean; - headers?: any; - videoType?: any; - vlcSelectedAudioTrack?: number; - vlcSelectedSubtitleTrack?: number; - vlcRestoreTime?: number; - vlcKey?: string; - selectedAudioTrack?: any; - selectedTextTrack?: any; - useCustomSubtitles?: boolean; - onTracksUpdate?: (tracks: any) => void; - vlcPlayerRef?: any; - videoRef?: any; - vlcLoadedRef?: any; - customVideoStyles?: any; - loadStartAtRef?: any; - firstFrameAtRef?: any; - zoomScale?: number; onTracksChanged?: (data: { audioTracks: any[]; subtitleTracks: any[] }) => void; } diff --git a/src/components/player/android/hooks/usePlayerControls.ts b/src/components/player/android/hooks/usePlayerControls.ts index 83bc421..3f38af2 100644 --- a/src/components/player/android/hooks/usePlayerControls.ts +++ b/src/components/player/android/hooks/usePlayerControls.ts @@ -7,8 +7,6 @@ const END_EPSILON = 0.3; export const usePlayerControls = ( mpvPlayerRef: any, - vlcPlayerRef: any, - useVLC: boolean, paused: boolean, setPaused: (paused: boolean) => void, currentTime: number, @@ -29,40 +27,31 @@ export const usePlayerControls = ( console.log('[usePlayerControls] seekToTime called:', { rawSeconds, timeInSeconds, - useVLC, hasMpvRef: !!mpvPlayerRef?.current, - hasVlcRef: !!vlcPlayerRef?.current, duration, isSeeking: isSeeking.current }); - if (useVLC) { - if (vlcPlayerRef.current && duration > 0) { - logger.log(`[usePlayerControls][VLC] Seeking to ${timeInSeconds}`); - vlcPlayerRef.current.seek(timeInSeconds); - } + // MPV Player + if (mpvPlayerRef.current && duration > 0) { + console.log(`[usePlayerControls][MPV] Seeking to ${timeInSeconds}`); + + isSeeking.current = true; + mpvPlayerRef.current.seek(timeInSeconds); + + // Reset seeking flag after a delay + setTimeout(() => { + if (isMounted.current) { + isSeeking.current = false; + } + }, 500); } else { - // MPV Player - if (mpvPlayerRef.current && duration > 0) { - console.log(`[usePlayerControls][MPV] Seeking to ${timeInSeconds}`); - - isSeeking.current = true; - mpvPlayerRef.current.seek(timeInSeconds); - - // Reset seeking flag after a delay - setTimeout(() => { - if (isMounted.current) { - isSeeking.current = false; - } - }, 500); - } else { - console.log('[usePlayerControls][MPV] Cannot seek - ref or duration invalid:', { - hasRef: !!mpvPlayerRef?.current, - duration - }); - } + console.log('[usePlayerControls][MPV] Cannot seek - ref or duration invalid:', { + hasRef: !!mpvPlayerRef?.current, + duration + }); } - }, [useVLC, duration, paused, setPaused, mpvPlayerRef, vlcPlayerRef, isSeeking, isMounted]); + }, [duration, paused, setPaused, mpvPlayerRef, isSeeking, isMounted]); const skip = useCallback((seconds: number) => { console.log('[usePlayerControls] skip called:', { seconds, currentTime, newTime: currentTime + seconds }); diff --git a/src/components/player/android/hooks/usePlayerTracks.ts b/src/components/player/android/hooks/usePlayerTracks.ts index 53711b2..9fb0479 100644 --- a/src/components/player/android/hooks/usePlayerTracks.ts +++ b/src/components/player/android/hooks/usePlayerTracks.ts @@ -7,14 +7,8 @@ interface Track { language?: string; } -export const usePlayerTracks = ( - useVLC: boolean, - vlcAudioTracks: Track[], - vlcSubtitleTracks: Track[], - vlcSelectedAudioTrack: number | undefined, - vlcSelectedSubtitleTrack: number | undefined -) => { - // React Native Video Tracks +export const usePlayerTracks = () => { + // Tracks from native player (MPV/RN-Video) const [rnVideoAudioTracks, setRnVideoAudioTracks] = useState([]); const [rnVideoTextTracks, setRnVideoTextTracks] = useState([]); @@ -22,31 +16,19 @@ export const usePlayerTracks = ( const [selectedAudioTrack, setSelectedAudioTrack] = useState({ type: 'system' }); const [selectedTextTrack, setSelectedTextTrack] = useState(-1); - // Unified Tracks - const ksAudioTracks = useMemo(() => - useVLC ? vlcAudioTracks : rnVideoAudioTracks, - [useVLC, vlcAudioTracks, rnVideoAudioTracks] - ); - - const ksTextTracks = useMemo(() => - useVLC ? vlcSubtitleTracks : rnVideoTextTracks, - [useVLC, vlcSubtitleTracks, rnVideoTextTracks] - ); + // Unified Tracks (now just returns native tracks) + const ksAudioTracks = useMemo(() => rnVideoAudioTracks, [rnVideoAudioTracks]); + const ksTextTracks = useMemo(() => rnVideoTextTracks, [rnVideoTextTracks]); // Unified Selection const computedSelectedAudioTrack = useMemo(() => - useVLC - ? (vlcSelectedAudioTrack ?? null) - : (selectedAudioTrack?.type === 'index' && selectedAudioTrack?.value !== undefined - ? Number(selectedAudioTrack?.value) - : null), - [useVLC, vlcSelectedAudioTrack, selectedAudioTrack] + selectedAudioTrack?.type === 'index' && selectedAudioTrack?.value !== undefined + ? Number(selectedAudioTrack?.value) + : null, + [selectedAudioTrack] ); - const computedSelectedTextTrack = useMemo(() => - useVLC ? (vlcSelectedSubtitleTrack ?? -1) : selectedTextTrack, - [useVLC, vlcSelectedSubtitleTrack, selectedTextTrack] - ); + const computedSelectedTextTrack = useMemo(() => selectedTextTrack, [selectedTextTrack]); return { rnVideoAudioTracks, setRnVideoAudioTracks, diff --git a/src/components/player/android/hooks/useVlcPlayer.ts b/src/components/player/android/hooks/useVlcPlayer.ts deleted file mode 100644 index c354744..0000000 --- a/src/components/player/android/hooks/useVlcPlayer.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { useState, useRef, useEffect, useCallback, useMemo } from 'react'; -import { logger } from '../../../../utils/logger'; -import { VlcPlayerRef } from '../../VlcVideoPlayer'; - -interface Track { - id: number; - name: string; - language?: string; -} - -const DEBUG_MODE = false; - -export const useVlcPlayer = (useVLC: boolean, paused: boolean, currentTime: number) => { - const [vlcAudioTracks, setVlcAudioTracks] = useState([]); - const [vlcSubtitleTracks, setVlcSubtitleTracks] = useState([]); - const [vlcSelectedAudioTrack, setVlcSelectedAudioTrack] = useState(undefined); - const [vlcSelectedSubtitleTrack, setVlcSelectedSubtitleTrack] = useState(undefined); - const [vlcRestoreTime, setVlcRestoreTime] = useState(undefined); - const [forceVlcRemount, setForceVlcRemount] = useState(false); - const [vlcKey, setVlcKey] = useState('vlc-initial'); - - const vlcPlayerRef = useRef(null); - const vlcLoadedRef = useRef(false); - const trackUpdateTimeoutRef = useRef(null); - - // Handle VLC pause/play interactions - useEffect(() => { - if (useVLC && vlcLoadedRef.current && vlcPlayerRef.current) { - if (paused) { - vlcPlayerRef.current.pause(); - } else { - vlcPlayerRef.current.play(); - } - } - }, [useVLC, paused]); - - // Reset forceVlcRemount when VLC becomes inactive - useEffect(() => { - if (!useVLC && forceVlcRemount) { - setForceVlcRemount(false); - } - }, [useVLC, forceVlcRemount]); - - // Track selection - const selectVlcAudioTrack = useCallback((trackId: number | null) => { - setVlcSelectedAudioTrack(trackId ?? undefined); - logger.log('[AndroidVideoPlayer][VLC] Audio track selected:', trackId); - }, []); - - const selectVlcSubtitleTrack = useCallback((trackId: number | null) => { - setVlcSelectedSubtitleTrack(trackId ?? undefined); - logger.log('[AndroidVideoPlayer][VLC] Subtitle track selected:', trackId); - }, []); - - // Track updates handler - const handleVlcTracksUpdate = useCallback((tracks: { audio: any[], subtitle: any[] }) => { - if (!tracks) return; - - if (trackUpdateTimeoutRef.current) { - clearTimeout(trackUpdateTimeoutRef.current); - } - - trackUpdateTimeoutRef.current = setTimeout(() => { - const { audio = [], subtitle = [] } = tracks; - let hasUpdates = false; - - // Process Audio - if (Array.isArray(audio) && audio.length > 0) { - const formattedAudio = audio.map(track => ({ - id: track.id, - name: track.name || `Track ${track.id + 1}`, - language: track.language - })); - - const audioChanged = formattedAudio.length !== vlcAudioTracks.length || - formattedAudio.some((track, index) => { - const existing = vlcAudioTracks[index]; - return !existing || track.id !== existing.id || track.name !== existing.name; - }); - - if (audioChanged) { - setVlcAudioTracks(formattedAudio); - hasUpdates = true; - } - } - - // Process Subtitles - if (Array.isArray(subtitle) && subtitle.length > 0) { - const formattedSubs = subtitle.map(track => ({ - id: track.id, - name: track.name || `Track ${track.id + 1}`, - language: track.language - })); - - const subsChanged = formattedSubs.length !== vlcSubtitleTracks.length || - formattedSubs.some((track, index) => { - const existing = vlcSubtitleTracks[index]; - return !existing || track.id !== existing.id || track.name !== existing.name; - }); - - if (subsChanged) { - setVlcSubtitleTracks(formattedSubs); - hasUpdates = true; - } - } - - trackUpdateTimeoutRef.current = null; - }, 100); - }, [vlcAudioTracks, vlcSubtitleTracks]); - - // Cleanup - useEffect(() => { - return () => { - if (trackUpdateTimeoutRef.current) { - clearTimeout(trackUpdateTimeoutRef.current); - } - }; - }, []); - - const remountVlc = useCallback((reason: string) => { - if (useVLC) { - logger.log(`[VLC] Forcing complete remount: ${reason}`); - setVlcRestoreTime(currentTime); - setForceVlcRemount(true); - vlcLoadedRef.current = false; - setTimeout(() => { - setForceVlcRemount(false); - setVlcKey(`vlc-${reason}-${Date.now()}`); - }, 100); - } - }, [useVLC, currentTime]); - - return { - vlcAudioTracks, - vlcSubtitleTracks, - vlcSelectedAudioTrack, - vlcSelectedSubtitleTrack, - selectVlcAudioTrack, - selectVlcSubtitleTrack, - vlcPlayerRef, - vlcLoadedRef, - forceVlcRemount, - vlcRestoreTime, - vlcKey, - handleVlcTracksUpdate, - remountVlc, - }; -}; diff --git a/src/components/player/hooks/useCustomSubtitles.ts b/src/components/player/hooks/useCustomSubtitles.ts index 5def5c9..43e7bf6 100644 --- a/src/components/player/hooks/useCustomSubtitles.ts +++ b/src/components/player/hooks/useCustomSubtitles.ts @@ -2,13 +2,14 @@ * Shared Custom Subtitles Hook * Used by both Android (VLC) and iOS (KSPlayer) players */ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { DEFAULT_SUBTITLE_SIZE, SubtitleCue, SubtitleSegment, WyzieSubtitle } from '../utils/playerTypes'; +import { storageService } from '../../../services/storageService'; export const useCustomSubtitles = () => { // Data State @@ -32,11 +33,58 @@ export const useCustomSubtitles = () => { const [subtitleOutlineColor, setSubtitleOutlineColor] = useState('#000000'); const [subtitleOutlineWidth, setSubtitleOutlineWidth] = useState(4); const [subtitleAlign, setSubtitleAlign] = useState<'center' | 'left' | 'right'>('center'); - const [subtitleBottomOffset, setSubtitleBottomOffset] = useState(10); + const [subtitleBottomOffset, setSubtitleBottomOffset] = useState(20); const [subtitleLetterSpacing, setSubtitleLetterSpacing] = useState(0); const [subtitleLineHeightMultiplier, setSubtitleLineHeightMultiplier] = useState(1.2); const [subtitleOffsetSec, setSubtitleOffsetSec] = useState(0); + // Load subtitle settings on mount + useEffect(() => { + const loadSettings = async () => { + const settings = await storageService.getSubtitleSettings(); + if (settings) { + if (settings.subtitleSize !== undefined) setSubtitleSize(settings.subtitleSize); + if (settings.subtitleBackground !== undefined) setSubtitleBackground(settings.subtitleBackground); + if (settings.subtitleTextColor !== undefined) setSubtitleTextColor(settings.subtitleTextColor); + if (settings.subtitleBgOpacity !== undefined) setSubtitleBgOpacity(settings.subtitleBgOpacity); + if (settings.subtitleTextShadow !== undefined) setSubtitleTextShadow(settings.subtitleTextShadow); + if (settings.subtitleOutline !== undefined) setSubtitleOutline(settings.subtitleOutline); + if (settings.subtitleOutlineColor !== undefined) setSubtitleOutlineColor(settings.subtitleOutlineColor); + if (settings.subtitleOutlineWidth !== undefined) setSubtitleOutlineWidth(settings.subtitleOutlineWidth); + if (settings.subtitleAlign !== undefined) setSubtitleAlign(settings.subtitleAlign); + if (settings.subtitleBottomOffset !== undefined) setSubtitleBottomOffset(settings.subtitleBottomOffset); + if (settings.subtitleLetterSpacing !== undefined) setSubtitleLetterSpacing(settings.subtitleLetterSpacing); + if (settings.subtitleLineHeightMultiplier !== undefined) setSubtitleLineHeightMultiplier(settings.subtitleLineHeightMultiplier); + } + }; + loadSettings(); + }, []); + + // Save subtitle settings when they change + useEffect(() => { + const saveSettings = async () => { + await storageService.saveSubtitleSettings({ + subtitleSize, + subtitleBackground, + subtitleTextColor, + subtitleBgOpacity, + subtitleTextShadow, + subtitleOutline, + subtitleOutlineColor, + subtitleOutlineWidth, + subtitleAlign, + subtitleBottomOffset, + subtitleLetterSpacing, + subtitleLineHeightMultiplier, + }); + }; + saveSettings(); + }, [ + subtitleSize, subtitleBackground, subtitleTextColor, subtitleBgOpacity, + subtitleTextShadow, subtitleOutline, subtitleOutlineColor, subtitleOutlineWidth, + subtitleAlign, subtitleBottomOffset, subtitleLetterSpacing, subtitleLineHeightMultiplier + ]); + return { customSubtitles, setCustomSubtitles, currentSubtitle, setCurrentSubtitle, diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index ef1e395..f3e43e2 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -125,7 +125,6 @@ export type RootStackParamList = { streamProvider?: string; streamName?: string; headers?: { [key: string]: string }; - forceVlc?: boolean; id?: string; type?: string; episodeId?: string; @@ -146,7 +145,6 @@ export type RootStackParamList = { streamProvider?: string; streamName?: string; headers?: { [key: string]: string }; - forceVlc?: boolean; id?: string; type?: string; episodeId?: string; diff --git a/src/screens/DownloadsScreen.tsx b/src/screens/DownloadsScreen.tsx index 96710d3..2b20637 100644 --- a/src/screens/DownloadsScreen.tsx +++ b/src/screens/DownloadsScreen.tsx @@ -525,7 +525,6 @@ const DownloadsScreen: React.FC = () => { streamProvider: 'Downloads', streamName: item.providerName || 'Offline', headers: undefined, - forceVlc: Platform.OS === 'android' ? isMkv : false, id: item.contentId, // Use contentId (base ID) instead of compound id for progress tracking type: item.type, episodeId: episodeId, // Pass episodeId for series progress tracking diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index e27b4b3..6191ca1 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -821,7 +821,7 @@ export const StreamsScreen = () => { fetchIMDbRatings(); }, [type, id, currentEpisode?.season_number, currentEpisode?.episode_number]); - const navigateToPlayer = useCallback(async (stream: Stream, options?: { forceVlc?: boolean; headers?: Record }) => { + const navigateToPlayer = useCallback(async (stream: Stream, options?: { headers?: Record }) => { // Filter headers for Vidrock - only send essential headers // Filter headers for Vidrock - only send essential headers // Filter headers for Vidrock - only send essential headers @@ -859,9 +859,6 @@ export const StreamsScreen = () => { const streamName = stream.name || stream.title || 'Unnamed Stream'; const streamProvider = stream.addonId || stream.addonName || stream.name; - // Do NOT pre-force VLC. Let ExoPlayer try first; fallback occurs on decoder error in the player. - let forceVlc = !!options?.forceVlc; - // Save stream to cache for future use try { const episodeId = (type === 'series' || type === 'other') && selectedEpisode ? selectedEpisode : undefined; @@ -922,8 +919,6 @@ export const StreamsScreen = () => { streamName: streamName, // Use filtered headers for Vidrock compatibility headers: finalHeaders, - // Android will use this to choose VLC path; iOS ignores - forceVlc, id, type, episodeId: (type === 'series' || type === 'other') && selectedEpisode ? selectedEpisode : undefined, diff --git a/src/utils/playerSelection.ts b/src/utils/playerSelection.ts index 7f60ef5..81dbcda 100644 --- a/src/utils/playerSelection.ts +++ b/src/utils/playerSelection.ts @@ -9,7 +9,6 @@ import { isMkvStream } from './mkvDetection'; export interface PlayerSelectionOptions { uri: string; headers?: Record; - forceVlc?: boolean; platform?: typeof Platform.OS; } @@ -19,10 +18,9 @@ export interface PlayerSelectionOptions { export const shouldUseKSPlayer = ({ uri, headers, - forceVlc = false, platform = Platform.OS }: PlayerSelectionOptions): boolean => { - // Android always uses AndroidVideoPlayer (react-native-video) + // Android always uses AndroidVideoPlayer (MPV) if (platform === 'android') { return false; }