import React, { useEffect, useRef, useState, useCallback } from 'react'; import { View, StatusBar, StyleSheet, Animated, Dimensions } from 'react-native'; import { useNavigation, useRoute } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import axios from 'axios'; // Shared Components import LoadingOverlay from './modals/LoadingOverlay'; import UpNextButton from './common/UpNextButton'; import { PlayerControls } from './controls/PlayerControls'; import AudioTrackModal from './modals/AudioTrackModal'; import SpeedModal from './modals/SpeedModal'; import SubtitleModals from './modals/SubtitleModals'; import SourcesModal from './modals/SourcesModal'; import EpisodesModal from './modals/EpisodesModal'; import { EpisodeStreamsModal } from './modals/EpisodeStreamsModal'; import { ErrorModal } from './modals/ErrorModal'; import CustomSubtitles from './subtitles/CustomSubtitles'; import { SpeedActivatedOverlay, PauseOverlay, GestureControls } from './components'; // Platform-specific components import { KSPlayerSurface } from './ios/components/KSPlayerSurface'; // Shared Hooks import { usePlayerState, usePlayerModals, useSpeedControl, useOpeningAnimation, usePlayerTracks, useCustomSubtitles, usePlayerControls, usePlayerSetup } from './hooks'; // Platform-specific hooks import { useKSPlayer } from './ios/hooks/useKSPlayer'; // App-level Hooks import { useTraktAutosync } from '../../hooks/useTraktAutosync'; import { useMetadata } from '../../hooks/useMetadata'; import { usePlayerGestureControls } from '../../hooks/usePlayerGestureControls'; import stremioService from '../../services/stremioService'; import { logger } from '../../utils/logger'; // Utils import { formatTime } from './utils/playerUtils'; import { WyzieSubtitle } from './utils/playerTypes'; import { parseSRT } from './utils/subtitleParser'; // Player route params interface interface PlayerRouteParams { uri: string; title: string; episodeTitle?: string; season?: number; episode?: number; quality?: string; year?: number; streamProvider?: string; streamName?: string; id: string; type: string; episodeId?: string; imdbId?: string; backdrop?: string; availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } }; headers?: Record; initialPosition?: number; } const KSPlayerCore: React.FC = () => { // Navigation & Route const navigation = useNavigation(); const route = useRoute(); const insets = useSafeAreaInsets(); const params = route.params as PlayerRouteParams; // Deconstruct params const { uri, title, episodeTitle, season, episode, id, type, quality, year, episodeId, imdbId, backdrop, availableStreams, headers, streamProvider, streamName, initialPosition: routeInitialPosition } = params; // --- Hooks --- const playerState = usePlayerState(); const { paused, setPaused, currentTime, setCurrentTime, duration, setDuration, buffered, setBuffered, isBuffering, setIsBuffering, isVideoLoaded, setIsVideoLoaded, isPlayerReady, setIsPlayerReady, showControls, setShowControls, resizeMode, setResizeMode, screenDimensions, setScreenDimensions, zoomScale, setZoomScale, lastZoomScale, setLastZoomScale, isAirPlayActive, allowsAirPlay, isSeeking, isMounted, } = playerState; const modals = usePlayerModals(); const speedControl = useSpeedControl(1.0); // Metadata Hook const { metadata, groupedEpisodes, cast } = useMetadata({ id, type: type as 'movie' | 'series' }); // Trakt Autosync const traktAutosync = useTraktAutosync({ type: type as 'movie' | 'series', imdbId: imdbId || (id?.startsWith('tt') ? id : ''), season, episode, title, id, year: year?.toString() || metadata?.year?.toString() || '' }); const openingAnim = useOpeningAnimation(backdrop, metadata); const tracks = usePlayerTracks(); const { ksPlayerRef, seek } = useKSPlayer(); const customSubs = useCustomSubtitles(); const controls = usePlayerControls({ playerRef: ksPlayerRef, paused, setPaused, currentTime, duration, isSeeking, isMounted }); // Gestures const fadeAnim = useRef(new Animated.Value(1)).current; // Controls timeout const controlsTimeout = useRef(null); const hideControls = useCallback(() => { // Allow hiding controls even when paused (per user request) setShowControls(false); Animated.timing(fadeAnim, { toValue: 0, duration: 300, useNativeDriver: true, }).start(); }, [fadeAnim, setShowControls]); // Volume/Brightness State const [volume, setVolumeState] = useState(1.0); const [brightness, setBrightnessState] = useState(0.5); const [isSliderDragging, setIsSliderDragging] = useState(false); // Watch Progress State const [initialPosition, setInitialPosition] = useState(routeInitialPosition || null); // Shared Gesture Hook const gestureControls = usePlayerGestureControls({ volume: volume, setVolume: (v) => setVolumeState(v), brightness: brightness, setBrightness: (b) => setBrightnessState(b), }); // Setup Hook (Listeners, StatusBar, etc) usePlayerSetup({ setScreenDimensions, setVolume: setVolumeState, setBrightness: setBrightnessState, isOpeningAnimationComplete: openingAnim.isOpeningAnimationComplete }); // Refs for Logic const isSyncingBeforeClose = useRef(false); // Toggle controls wrapper const toggleControls = useCallback(() => { if (controlsTimeout.current) { clearTimeout(controlsTimeout.current); controlsTimeout.current = null; } setShowControls(prev => { const next = !prev; Animated.timing(fadeAnim, { toValue: next ? 1 : 0, duration: 300, useNativeDriver: true, }).start(); // Start auto-hide timer if showing controls and not paused if (next && !paused) { controlsTimeout.current = setTimeout(hideControls, 5000); } return next; }); }, [fadeAnim, hideControls, setShowControls, paused]); // Auto-hide controls when playback resumes useEffect(() => { if (showControls && !paused) { // Reset auto-hide timer when playback resumes if (controlsTimeout.current) { clearTimeout(controlsTimeout.current); } controlsTimeout.current = setTimeout(hideControls, 5000); } else if (paused) { // Clear timeout when paused - user controls when to hide if (controlsTimeout.current) { clearTimeout(controlsTimeout.current); controlsTimeout.current = null; } } return () => { if (controlsTimeout.current) { clearTimeout(controlsTimeout.current); } }; }, [paused, showControls, hideControls]); // Subtitle Fetching Logic const fetchAvailableSubtitles = async (imdbIdParam?: string, autoSelectEnglish = true) => { const targetImdbId = imdbIdParam || imdbId; if (!targetImdbId) return; customSubs.setIsLoadingSubtitleList(true); try { const stremioType = type === 'series' ? 'series' : 'movie'; const stremioVideoId = stremioType === 'series' && season && episode ? `series:${targetImdbId}:${season}:${episode}` : undefined; const results = await stremioService.getSubtitles(stremioType, targetImdbId, stremioVideoId); const subs: WyzieSubtitle[] = (results || []).map((sub: any) => ({ id: sub.id || `${sub.lang}-${sub.url}`, url: sub.url, flagUrl: '', format: 'srt', encoding: 'utf-8', media: sub.addonName || sub.addon || '', display: sub.lang || 'Unknown', language: (sub.lang || '').toLowerCase(), isHearingImpaired: false, source: sub.addonName || sub.addon || 'Addon', })); customSubs.setAvailableSubtitles(subs); if (autoSelectEnglish) { const englishSubtitle = subs.find(sub => sub.language.includes('en') || sub.display.toLowerCase().includes('english') ); if (englishSubtitle) { loadWyzieSubtitle(englishSubtitle); return; } } if (!autoSelectEnglish) { modals.setShowSubtitleLanguageModal(true); } } catch (e) { logger.error('[VideoPlayer] Error fetching subtitles', e); } finally { customSubs.setIsLoadingSubtitleList(false); } }; const loadWyzieSubtitle = async (subtitle: WyzieSubtitle) => { modals.setShowSubtitleLanguageModal(false); customSubs.setIsLoadingSubtitles(true); try { let srtContent = ''; try { const resp = await axios.get(subtitle.url, { timeout: 10000 }); srtContent = typeof resp.data === 'string' ? resp.data : String(resp.data); } catch { const resp = await fetch(subtitle.url); srtContent = await resp.text(); } const parsedCues = parseSRT(srtContent); customSubs.setCustomSubtitles(parsedCues); customSubs.setUseCustomSubtitles(true); tracks.selectTextTrack(-1); const adjustedTime = currentTime + (customSubs.subtitleOffsetSec || 0); const cueNow = parsedCues.find(cue => adjustedTime >= cue.start && adjustedTime <= cue.end); customSubs.setCurrentSubtitle(cueNow ? cueNow.text : ''); } catch (e) { logger.error('[VideoPlayer] Error loading wyzie', e); } finally { customSubs.setIsLoadingSubtitles(false); } }; // Auto-fetch subtitles on load useEffect(() => { if (imdbId) { fetchAvailableSubtitles(undefined, true); } }, [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 ); const newText = cueNow ? cueNow.text : ''; // Only update state if the text has changed to avoid unnecessary re-renders if (newText !== customSubs.currentSubtitle) { customSubs.setCurrentSubtitle(newText); } }, [currentTime, customSubs.useCustomSubtitles, customSubs.customSubtitles, customSubs.subtitleOffsetSec, customSubs.currentSubtitle]); // Handlers const onLoad = (data: any) => { setDuration(data.duration); if (data.audioTracks) tracks.setKsAudioTracks(data.audioTracks); if (data.textTracks) tracks.setKsTextTracks(data.textTracks); setIsVideoLoaded(true); setIsPlayerReady(true); openingAnim.completeOpeningAnimation(); // Initial Seek if (initialPosition && initialPosition > 0) { setTimeout(() => { controls.seekToTime(initialPosition); }, 500); } // Start trakt session if (data.duration > 0) { traktAutosync.handlePlaybackStart(currentTime, data.duration); } }; const handleError = (error: any) => { let msg = 'Unknown Error'; try { if (typeof error === 'string') { msg = error; } else if (error?.error?.localizedDescription) { msg = error.error.localizedDescription; } else if (error?.error?.message) { msg = error.error.message; } else if (error?.message) { msg = error.message; } else if (error?.error) { msg = typeof error.error === 'string' ? error.error : JSON.stringify(error.error); } else { msg = JSON.stringify(error); } } catch (e) { msg = 'Error parsing error details'; } modals.setErrorDetails(msg); modals.setShowErrorModal(true); }; const handleClose = async () => { if (isSyncingBeforeClose.current) return; isSyncingBeforeClose.current = true; await traktAutosync.handleProgressUpdate(currentTime, duration, true); await traktAutosync.handlePlaybackEnd(currentTime, duration, 'user_close'); navigation.goBack(); }; // Stream selection handler const handleSelectStream = async (newStream: any) => { if (newStream.url === uri) { modals.setShowSourcesModal(false); return; } modals.setShowSourcesModal(false); setPaused(true); const newQuality = newStream.quality || newStream.title?.match(/(\d+)p/)?.[0]; const newProvider = newStream.addonName || newStream.name || newStream.addon || 'Unknown'; const newStreamName = newStream.name || newStream.title || 'Unknown'; setTimeout(() => { (navigation as any).replace('PlayerIOS', { ...params, uri: newStream.url, quality: newQuality, streamProvider: newProvider, streamName: newStreamName, headers: newStream.headers, availableStreams: availableStreams }); }, 100); }; // Episode selection handler - opens streams modal const handleSelectEpisode = (ep: any) => { modals.setSelectedEpisodeForStreams(ep); modals.setShowEpisodesModal(false); modals.setShowEpisodeStreamsModal(true); }; // Episode stream selection handler - navigates to new episode with selected stream const handleEpisodeStreamSelect = async (stream: any) => { if (!modals.selectedEpisodeForStreams) return; modals.setShowEpisodeStreamsModal(false); setPaused(true); const ep = modals.selectedEpisodeForStreams; const newQuality = stream.quality || (stream.title?.match(/(\d+)p/)?.[0]); const newProvider = stream.addonName || stream.name || stream.addon || 'Unknown'; const newStreamName = stream.name || stream.title || 'Unknown Stream'; setTimeout(() => { (navigation as any).replace('PlayerIOS', { uri: stream.url, title: title, episodeTitle: ep.name, season: ep.season_number, episode: ep.episode_number, quality: newQuality, year: year, streamProvider: newProvider, streamName: newStreamName, headers: stream.headers || undefined, id, type: 'series', episodeId: ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number} `, imdbId: imdbId ?? undefined, backdrop: backdrop || undefined, }); }, 100); }; // Slider handlers const onSliderValueChange = (value: number) => { setCurrentTime(value); }; const onSlidingStart = () => { setIsSliderDragging(true); }; const onSlidingComplete = (value: number) => { setIsSliderDragging(false); controls.seekToTime(value); }; return ( ); }; export default KSPlayerCore;