removed vlc lib

This commit is contained in:
tapframe 2025-12-23 16:42:41 +05:30
parent f0f71afd67
commit 767fd2ff87
15 changed files with 124 additions and 715 deletions

View file

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

View file

@ -649,7 +649,6 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
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,

View file

@ -977,7 +977,6 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((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,

View file

@ -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<any>(null);
const mpvPlayerRef = useRef<MpvPlayerRef>(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<string>(uri);
const [currentVideoType, setCurrentVideoType] = useState<string | undefined>((route.params as any).videoType);
const processedStreamUrl = useMemo(() => useVLC ? processUrlForVLC(currentStreamUrl) : currentStreamUrl, [currentStreamUrl, useVLC]);
const [availableStreams, setAvailableStreams] = useState<any>(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 = () => {
<View style={{ flex: 1, backgroundColor: 'black' }}>
<VideoSurface
useVLC={!!useVLC}
forceVlcRemount={vlcHook.forceVlcRemount}
processedStreamUrl={processedStreamUrl}
processedStreamUrl={currentStreamUrl}
volume={volume}
playbackSpeed={speedControl.playbackSpeed}
zoomScale={1.0}
resizeMode={playerState.resizeMode}
paused={playerState.paused}
currentStreamUrl={currentStreamUrl}
headers={headers || (isHlsStream(currentStreamUrl) ? getHlsHeaders() : defaultAndroidHeaders)}
videoType={currentVideoType}
vlcSelectedAudioTrack={vlcHook.vlcSelectedAudioTrack}
vlcSelectedSubtitleTrack={vlcHook.vlcSelectedSubtitleTrack}
vlcRestoreTime={vlcHook.vlcRestoreTime}
vlcKey={vlcHook.vlcKey}
selectedAudioTrack={tracksHook.selectedAudioTrack}
selectedTextTrack={tracksHook.selectedTextTrack}
useCustomSubtitles={false}
toggleControls={toggleControls}
onLoad={handleLoad}
onProgress={handleProgress}
@ -540,32 +506,6 @@ 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'}
/>
<SpeedActivatedOverlay
@ -728,14 +661,10 @@ const AndroidVideoPlayer: React.FC = () => {
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);

View file

@ -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,
});

View file

@ -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<VlcPlayerRef, VlcVideoPlayerProps>(({
source,
volume,
playbackSpeed,
zoomScale,
resizeMode,
onLoad,
onProgress,
onSeek,
onEnd,
onError,
onTracksUpdate,
selectedAudioTrack,
selectedSubtitleTrack,
restoreTime,
forceRemount,
key,
}, ref) => {
const vlcRef = useRef<any>(null);
const [vlcActive, setVlcActive] = useState(true);
const [duration, setDuration] = useState<number>(0);
const [videoAspectRatio, setVideoAspectRatio] = useState<number | null>(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 (
<View style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#000'
}}>
{/* VLC not available fallback */}
</View>
);
}
return (
<View
style={{
position: 'absolute',
top: 0,
left: 0,
width: screenDimensions.width,
height: screenDimensions.height,
overflow: 'hidden'
}}
>
<LibVlcPlayerViewComponent
ref={vlcRef}
style={{
position: 'absolute',
top: 0,
left: 0,
width: screenDimensions.width,
height: screenDimensions.height,
transform: [{ scale: clientScale }]
}}
// Force remount when surfaces are recreated
key={key || 'vlc-default'}
source={processedSource}
aspectRatio={vlcAspectRatio}
// Let VLC auto-fit the video to the view to prevent flicker on mode changes
scale={0}
options={vlcOptions}
tracks={vlcTracks}
volume={Math.round(Math.max(0, Math.min(1, volume)) * 100)}
mute={false}
repeat={false}
rate={playbackSpeed}
autoplay={false}
onFirstPlay={handleFirstPlay}
onPositionChanged={handlePositionChanged}
onPlaying={handlePlaying}
onPaused={handlePaused}
onEndReached={handleEndReached}
onEncounteredError={handleEncounteredError}
onBackground={handleBackground}
onESAdded={handleESAdded}
/>
</View>
);
});
VlcVideoPlayer.displayName = 'VlcVideoPlayer';
export default VlcVideoPlayer;

View file

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

View file

@ -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 });

View file

@ -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<Track[]>([]);
const [rnVideoTextTracks, setRnVideoTextTracks] = useState<Track[]>([]);
@ -22,31 +16,19 @@ export const usePlayerTracks = (
const [selectedAudioTrack, setSelectedAudioTrack] = useState<SelectedTrack | null>({ type: 'system' });
const [selectedTextTrack, setSelectedTextTrack] = useState<number>(-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,

View file

@ -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<Track[]>([]);
const [vlcSubtitleTracks, setVlcSubtitleTracks] = useState<Track[]>([]);
const [vlcSelectedAudioTrack, setVlcSelectedAudioTrack] = useState<number | undefined>(undefined);
const [vlcSelectedSubtitleTrack, setVlcSelectedSubtitleTrack] = useState<number | undefined>(undefined);
const [vlcRestoreTime, setVlcRestoreTime] = useState<number | undefined>(undefined);
const [forceVlcRemount, setForceVlcRemount] = useState(false);
const [vlcKey, setVlcKey] = useState('vlc-initial');
const vlcPlayerRef = useRef<VlcPlayerRef>(null);
const vlcLoadedRef = useRef<boolean>(false);
const trackUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(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,
};
};

View file

@ -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<string>('#000000');
const [subtitleOutlineWidth, setSubtitleOutlineWidth] = useState<number>(4);
const [subtitleAlign, setSubtitleAlign] = useState<'center' | 'left' | 'right'>('center');
const [subtitleBottomOffset, setSubtitleBottomOffset] = useState<number>(10);
const [subtitleBottomOffset, setSubtitleBottomOffset] = useState<number>(20);
const [subtitleLetterSpacing, setSubtitleLetterSpacing] = useState<number>(0);
const [subtitleLineHeightMultiplier, setSubtitleLineHeightMultiplier] = useState<number>(1.2);
const [subtitleOffsetSec, setSubtitleOffsetSec] = useState<number>(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,

View file

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

View file

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

View file

@ -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<string, string> }) => {
const navigateToPlayer = useCallback(async (stream: Stream, options?: { headers?: Record<string, string> }) => {
// 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,

View file

@ -9,7 +9,6 @@ import { isMkvStream } from './mkvDetection';
export interface PlayerSelectionOptions {
uri: string;
headers?: Record<string, string>;
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;
}