mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
742 lines
No EOL
26 KiB
TypeScript
742 lines
No EOL
26 KiB
TypeScript
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<string, string>;
|
|
initialPosition?: number;
|
|
}
|
|
|
|
const KSPlayerCore: React.FC = () => {
|
|
// Navigation & Route
|
|
const navigation = useNavigation<any>();
|
|
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<NodeJS.Timeout | null>(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<number | null>(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 (
|
|
<View style={{ flex: 1, backgroundColor: '#000000' }}>
|
|
<StatusBar hidden={true} />
|
|
|
|
{/* Opening Animation Overlay */}
|
|
<LoadingOverlay
|
|
visible={!openingAnim.shouldHideOpeningOverlay}
|
|
backdrop={backdrop}
|
|
hasLogo={!!metadata?.logo}
|
|
logo={metadata?.logo}
|
|
backgroundFadeAnim={openingAnim.backgroundFadeAnim}
|
|
backdropImageOpacityAnim={openingAnim.backdropImageOpacityAnim}
|
|
onClose={handleClose}
|
|
width={screenDimensions.width}
|
|
height={screenDimensions.height}
|
|
/>
|
|
|
|
{/* Video Surface & Pinch Zoom */}
|
|
<KSPlayerSurface
|
|
ksPlayerRef={ksPlayerRef}
|
|
uri={uri}
|
|
headers={headers}
|
|
paused={paused}
|
|
volume={volume}
|
|
playbackSpeed={speedControl.playbackSpeed}
|
|
resizeMode={resizeMode}
|
|
zoomScale={zoomScale}
|
|
setZoomScale={setZoomScale}
|
|
lastZoomScale={lastZoomScale}
|
|
setLastZoomScale={setLastZoomScale}
|
|
audioTrack={tracks.selectedAudioTrack !== null ? tracks.selectedAudioTrack : undefined}
|
|
textTrack={customSubs.useCustomSubtitles ? undefined : (tracks.selectedTextTrack !== -1 ? tracks.selectedTextTrack : undefined)}
|
|
onAudioTracks={(d) => tracks.setKsAudioTracks(d.audioTracks || [])}
|
|
onTextTracks={(d) => tracks.setKsTextTracks(d.textTracks || [])}
|
|
onLoad={onLoad}
|
|
onProgress={(d) => {
|
|
if (!isSliderDragging) {
|
|
setCurrentTime(d.currentTime);
|
|
}
|
|
// 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%' }}
|
|
/>
|
|
|
|
{/* Custom Subtitles Overlay */}
|
|
<CustomSubtitles
|
|
useCustomSubtitles={customSubs.useCustomSubtitles}
|
|
currentSubtitle={customSubs.currentSubtitle}
|
|
subtitleSize={customSubs.subtitleSize}
|
|
subtitleBackground={customSubs.subtitleBackground}
|
|
zoomScale={zoomScale}
|
|
textColor={customSubs.subtitleTextColor}
|
|
backgroundOpacity={customSubs.subtitleBgOpacity}
|
|
textShadow={customSubs.subtitleTextShadow}
|
|
outline={customSubs.subtitleOutline}
|
|
outlineColor={customSubs.subtitleOutlineColor}
|
|
outlineWidth={customSubs.subtitleOutlineWidth}
|
|
align={customSubs.subtitleAlign}
|
|
bottomOffset={customSubs.subtitleBottomOffset}
|
|
letterSpacing={customSubs.subtitleLetterSpacing}
|
|
lineHeightMultiplier={customSubs.subtitleLineHeightMultiplier}
|
|
formattedSegments={customSubs.currentFormattedSegments}
|
|
controlsVisible={showControls}
|
|
controlsFixedOffset={106}
|
|
/>
|
|
|
|
{/* Gesture Controls Overlay (Pan/Tap) */}
|
|
<GestureControls
|
|
screenDimensions={screenDimensions}
|
|
gestureControls={gestureControls}
|
|
onLongPressActivated={speedControl.activateSpeedBoost}
|
|
onLongPressEnd={speedControl.deactivateSpeedBoost}
|
|
onLongPressStateChange={() => { }}
|
|
toggleControls={toggleControls}
|
|
showControls={showControls}
|
|
hideControls={hideControls}
|
|
volume={volume}
|
|
brightness={brightness}
|
|
controlsTimeout={controlsTimeout}
|
|
/>
|
|
|
|
{/* UI Controls */}
|
|
{isVideoLoaded && (
|
|
<View pointerEvents="box-none" style={StyleSheet.absoluteFill}>
|
|
<PlayerControls
|
|
showControls={showControls}
|
|
fadeAnim={fadeAnim}
|
|
paused={paused}
|
|
title={title}
|
|
episodeTitle={episodeTitle}
|
|
season={season}
|
|
episode={episode}
|
|
quality={quality}
|
|
year={year}
|
|
streamProvider={streamProvider}
|
|
streamName={streamName}
|
|
currentTime={currentTime}
|
|
duration={duration}
|
|
zoomScale={zoomScale}
|
|
currentResizeMode={resizeMode}
|
|
ksAudioTracks={tracks.ksAudioTracks}
|
|
selectedAudioTrack={tracks.selectedAudioTrack}
|
|
availableStreams={availableStreams}
|
|
togglePlayback={controls.togglePlayback}
|
|
skip={controls.skip}
|
|
handleClose={handleClose}
|
|
cycleAspectRatio={() => setResizeMode(prev => prev === 'cover' ? 'contain' : 'cover')}
|
|
cyclePlaybackSpeed={() => speedControl.setPlaybackSpeed(speedControl.playbackSpeed >= 2 ? 1 : speedControl.playbackSpeed + 0.25)}
|
|
currentPlaybackSpeed={speedControl.playbackSpeed}
|
|
setShowAudioModal={modals.setShowAudioModal}
|
|
setShowSubtitleModal={modals.setShowSubtitleModal}
|
|
setShowSpeedModal={modals.setShowSpeedModal}
|
|
isSubtitleModalOpen={modals.showSubtitleModal}
|
|
setShowSourcesModal={modals.setShowSourcesModal}
|
|
setShowEpisodesModal={type === 'series' ? modals.setShowEpisodesModal : undefined}
|
|
onSliderValueChange={onSliderValueChange}
|
|
onSlidingStart={onSlidingStart}
|
|
onSlidingComplete={onSlidingComplete}
|
|
buffered={buffered}
|
|
formatTime={formatTime}
|
|
playerBackend="KSAVPlayer"
|
|
isAirPlayActive={isAirPlayActive}
|
|
allowsAirPlay={allowsAirPlay}
|
|
onAirPlayPress={() => ksPlayerRef.current?.showAirPlayPicker()}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{/* Speed Overlay */}
|
|
<SpeedActivatedOverlay
|
|
visible={speedControl.showSpeedActivatedOverlay}
|
|
opacity={speedControl.speedActivatedOverlayOpacity}
|
|
speed={speedControl.holdToSpeedValue}
|
|
/>
|
|
|
|
{/* Pause Overlay */}
|
|
<PauseOverlay
|
|
visible={paused && !showControls}
|
|
onClose={() => setShowControls(true)}
|
|
title={title}
|
|
episodeTitle={episodeTitle}
|
|
season={season}
|
|
episode={episode}
|
|
year={year}
|
|
type={type}
|
|
description={metadata?.description || ''}
|
|
cast={cast || []}
|
|
screenDimensions={screenDimensions}
|
|
/>
|
|
|
|
{/* Up Next Button */}
|
|
<UpNextButton
|
|
type={type}
|
|
nextEpisode={null}
|
|
currentTime={currentTime}
|
|
duration={duration}
|
|
insets={insets}
|
|
isLoading={false}
|
|
nextLoadingProvider={null}
|
|
nextLoadingQuality={null}
|
|
nextLoadingTitle={null}
|
|
onPress={() => { }}
|
|
metadata={metadata ? { poster: metadata.poster, id: metadata.id } : undefined}
|
|
controlsVisible={showControls}
|
|
controlsFixedOffset={126}
|
|
/>
|
|
|
|
{/* Modals */}
|
|
<AudioTrackModal
|
|
showAudioModal={modals.showAudioModal}
|
|
setShowAudioModal={modals.setShowAudioModal}
|
|
ksAudioTracks={tracks.ksAudioTracks}
|
|
selectedAudioTrack={tracks.selectedAudioTrack}
|
|
selectAudioTrack={tracks.selectAudioTrack}
|
|
/>
|
|
|
|
<ErrorModal
|
|
showErrorModal={modals.showErrorModal}
|
|
setShowErrorModal={modals.setShowErrorModal}
|
|
errorDetails={modals.errorDetails}
|
|
onDismiss={handleClose}
|
|
/>
|
|
|
|
<SpeedModal
|
|
showSpeedModal={modals.showSpeedModal}
|
|
setShowSpeedModal={modals.setShowSpeedModal}
|
|
currentSpeed={speedControl.playbackSpeed}
|
|
setPlaybackSpeed={speedControl.setPlaybackSpeed}
|
|
holdToSpeedEnabled={speedControl.holdToSpeedEnabled}
|
|
setHoldToSpeedEnabled={speedControl.setHoldToSpeedEnabled}
|
|
holdToSpeedValue={speedControl.holdToSpeedValue}
|
|
setHoldToSpeedValue={speedControl.setHoldToSpeedValue}
|
|
/>
|
|
|
|
<SubtitleModals
|
|
showSubtitleModal={modals.showSubtitleModal}
|
|
setShowSubtitleModal={modals.setShowSubtitleModal}
|
|
showSubtitleLanguageModal={modals.showSubtitleLanguageModal}
|
|
setShowSubtitleLanguageModal={modals.setShowSubtitleLanguageModal}
|
|
customSubtitles={customSubs.customSubtitles}
|
|
availableSubtitles={customSubs.availableSubtitles}
|
|
fetchAvailableSubtitles={fetchAvailableSubtitles}
|
|
loadWyzieSubtitle={loadWyzieSubtitle}
|
|
subtitleSize={customSubs.subtitleSize}
|
|
increaseSubtitleSize={() => customSubs.setSubtitleSize((s: number) => s + 2)}
|
|
decreaseSubtitleSize={() => customSubs.setSubtitleSize((s: number) => Math.max(10, s - 2))}
|
|
subtitleBackground={customSubs.subtitleBackground}
|
|
toggleSubtitleBackground={() => customSubs.setSubtitleBackground((b: boolean) => !b)}
|
|
subtitleTextColor={customSubs.subtitleTextColor}
|
|
setSubtitleTextColor={customSubs.setSubtitleTextColor}
|
|
subtitleBgOpacity={customSubs.subtitleBgOpacity}
|
|
setSubtitleBgOpacity={customSubs.setSubtitleBgOpacity}
|
|
subtitleTextShadow={customSubs.subtitleTextShadow}
|
|
setSubtitleTextShadow={customSubs.setSubtitleTextShadow}
|
|
subtitleOutline={customSubs.subtitleOutline}
|
|
setSubtitleOutline={customSubs.setSubtitleOutline}
|
|
subtitleOutlineColor={customSubs.subtitleOutlineColor}
|
|
setSubtitleOutlineColor={customSubs.setSubtitleOutlineColor}
|
|
subtitleOutlineWidth={customSubs.subtitleOutlineWidth}
|
|
setSubtitleOutlineWidth={customSubs.setSubtitleOutlineWidth}
|
|
subtitleAlign={customSubs.subtitleAlign}
|
|
setSubtitleAlign={customSubs.setSubtitleAlign}
|
|
subtitleBottomOffset={customSubs.subtitleBottomOffset}
|
|
setSubtitleBottomOffset={customSubs.setSubtitleBottomOffset}
|
|
subtitleLetterSpacing={customSubs.subtitleLetterSpacing}
|
|
setSubtitleLetterSpacing={customSubs.setSubtitleLetterSpacing}
|
|
subtitleLineHeightMultiplier={customSubs.subtitleLineHeightMultiplier}
|
|
setSubtitleLineHeightMultiplier={customSubs.setSubtitleLineHeightMultiplier}
|
|
subtitleOffsetSec={customSubs.subtitleOffsetSec}
|
|
setSubtitleOffsetSec={customSubs.setSubtitleOffsetSec}
|
|
isLoadingSubtitleList={customSubs.isLoadingSubtitleList}
|
|
isLoadingSubtitles={customSubs.isLoadingSubtitles}
|
|
ksTextTracks={tracks.ksTextTracks}
|
|
selectedTextTrack={tracks.selectedTextTrack !== null ? tracks.selectedTextTrack : -1}
|
|
useCustomSubtitles={customSubs.useCustomSubtitles}
|
|
selectTextTrack={tracks.selectTextTrack}
|
|
disableCustomSubtitles={() => {
|
|
customSubs.setUseCustomSubtitles(false);
|
|
tracks.selectTextTrack(-1);
|
|
}}
|
|
/>
|
|
|
|
<SourcesModal
|
|
showSourcesModal={modals.showSourcesModal}
|
|
setShowSourcesModal={modals.setShowSourcesModal}
|
|
availableStreams={availableStreams || {}}
|
|
currentStreamUrl={uri}
|
|
onSelectStream={handleSelectStream}
|
|
/>
|
|
|
|
{type === 'series' && (
|
|
<EpisodesModal
|
|
showEpisodesModal={modals.showEpisodesModal}
|
|
setShowEpisodesModal={modals.setShowEpisodesModal}
|
|
groupedEpisodes={groupedEpisodes}
|
|
currentEpisode={{ season: season || 1, episode: episode || 1 }}
|
|
metadata={{ poster: metadata?.poster, id: id }}
|
|
onSelectEpisode={handleSelectEpisode}
|
|
/>
|
|
)}
|
|
|
|
<EpisodeStreamsModal
|
|
visible={modals.showEpisodeStreamsModal}
|
|
onClose={() => modals.setShowEpisodeStreamsModal(false)}
|
|
episode={modals.selectedEpisodeForStreams}
|
|
onSelectStream={handleEpisodeStreamSelect}
|
|
metadata={{ id: id, name: title }}
|
|
/>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default KSPlayerCore; |