Add VLC media player support to VideoPlayer component and update dependencies

This update integrates the react-native-vlc-media-player into the VideoPlayer component, replacing the previous video player implementation. It introduces new state management for audio and subtitle tracks specific to VLC, enhances buffering handling, and updates the UI for audio and subtitle selection modals. Additionally, package.json and package-lock.json have been updated to include the new VLC media player dependency and node-fetch. The TypeScript configuration has also been adjusted to support JSX and module interoperability.
This commit is contained in:
tapframe 2025-06-08 13:16:43 +05:30
parent 92704f0998
commit 9465486a47
4 changed files with 434 additions and 195 deletions

104
package-lock.json generated
View file

@ -61,6 +61,7 @@
"react-native-tab-view": "^4.0.10",
"react-native-url-polyfill": "^2.0.0",
"react-native-video": "^6.12.0",
"react-native-vlc-media-player": "^1.0.87",
"react-native-web": "~0.19.13",
"react-native-wheel-color-picker": "^1.3.1",
"subsrt": "^1.1.1"
@ -11294,6 +11295,15 @@
"react-native": "*"
}
},
"node_modules/react-native-slider": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-native-slider/-/react-native-slider-0.11.0.tgz",
"integrity": "sha512-jV9K87eu9uWr0uJIyrSpBLnCKvVlOySC2wynq9TFCdV9oGgjt7Niq8Q1A8R8v+5GHsuBw/s8vEj1AAkkUi+u+w==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.5.6"
}
},
"node_modules/react-native-svg": {
"version": "15.11.2",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.11.2.tgz",
@ -11455,6 +11465,100 @@
"react-native": "*"
}
},
"node_modules/react-native-vlc-media-player": {
"version": "1.0.87",
"resolved": "https://registry.npmjs.org/react-native-vlc-media-player/-/react-native-vlc-media-player-1.0.87.tgz",
"integrity": "sha512-b05fW2WXVEFoatUcEcszi49FyiBF6ca9HZNQgpJYahL79obLHXRUMejh1RMlxC511UKS+TsDIe2pMJfi8NFbaA==",
"license": "MIT",
"dependencies": {
"react-native-slider": "^0.11.0",
"react-native-vector-icons": "^9.2.0"
}
},
"node_modules/react-native-vlc-media-player/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/react-native-vlc-media-player/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/react-native-vlc-media-player/node_modules/react-native-vector-icons": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-9.2.0.tgz",
"integrity": "sha512-wKYLaFuQST/chH3AJRjmOLoLy3JEs1JR6zMNgTaemFpNoXs0ztRnTxcxFD9xhX7cJe1/zoN5BpQYe7kL0m5yyA==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.7.2",
"yargs": "^16.1.1"
},
"bin": {
"fa5-upgrade": "bin/fa5-upgrade.sh",
"generate-icon": "bin/generate-icon.js"
}
},
"node_modules/react-native-vlc-media-player/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/react-native-vlc-media-player/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/react-native-vlc-media-player/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"license": "MIT",
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/react-native-vlc-media-player/node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/react-native-web": {
"version": "0.19.13",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz",

View file

@ -45,6 +45,7 @@
"expo-system-ui": "^4.0.9",
"expo-web-browser": "~14.0.2",
"lodash": "^4.17.21",
"node-fetch": "^2.6.7",
"react": "18.3.1",
"react-native": "0.76.9",
"react-native-awesome-slider": "^2.9.0",
@ -61,10 +62,10 @@
"react-native-tab-view": "^4.0.10",
"react-native-url-polyfill": "^2.0.0",
"react-native-video": "^6.12.0",
"react-native-vlc-media-player": "^1.0.87",
"react-native-web": "~0.19.13",
"react-native-wheel-color-picker": "^1.3.1",
"subsrt": "^1.1.1",
"node-fetch": "^2.6.7"
"subsrt": "^1.1.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",

View file

@ -1,6 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import { View, TouchableOpacity, StyleSheet, Text, Dimensions, Modal, Pressable, StatusBar, Platform, ScrollView, Animated } from 'react-native';
import Video from 'react-native-video';
import { VLCPlayer } from 'react-native-vlc-media-player';
import { Ionicons } from '@expo/vector-icons';
import { Slider } from 'react-native-awesome-slider';
import { LinearGradient } from 'expo-linear-gradient';
@ -69,9 +69,9 @@ interface TextTrack {
type?: string | null; // Adjusting type based on linter error
}
// Define the possible resize modes
type ResizeModeType = 'contain' | 'cover' | 'stretch' | 'none';
const resizeModes: ResizeModeType[] = ['contain', 'cover', 'stretch'];
// Define the possible resize modes - adjust to match VLCPlayer's PlayerResizeMode options
type ResizeModeType = 'contain' | 'cover' | 'fill' | 'none';
const resizeModes: ResizeModeType[] = ['contain', 'cover', 'fill'];
// Add language code to name mapping
const languageMap: {[key: string]: string} = {
@ -132,6 +132,18 @@ const formatLanguage = (code?: string): string => {
return languageMap[normalized] || code.toUpperCase();
};
// Add VLC specific interface for their event structure
interface VlcMediaEvent {
currentTime: number;
duration: number;
bufferTime?: number;
isBuffering?: boolean;
audioTracks?: Array<{id: number, name: string, language?: string}>;
textTracks?: Array<{id: number, name: string, language?: string}>;
selectedAudioTrack?: number;
selectedTextTrack?: number;
}
const VideoPlayer: React.FC = () => {
const navigation = useNavigation();
const route = useRoute<RouteProp<RootStackParamList, 'Player'>>();
@ -176,7 +188,8 @@ const VideoPlayer: React.FC = () => {
const [textTracks, setTextTracks] = useState<TextTrack[]>([]);
const [selectedTextTrack, setSelectedTextTrack] = useState<SelectedTrack | null>({ type: 'disabled' });
const [resizeMode, setResizeMode] = useState<ResizeModeType>('contain'); // State for resize mode
const videoRef = useRef<any>(null);
const [buffered, setBuffered] = useState(0); // Add buffered state
const vlcRef = useRef<any>(null);
const progress = useSharedValue(0);
const min = useSharedValue(0);
const max = useSharedValue(duration);
@ -199,8 +212,12 @@ const VideoPlayer: React.FC = () => {
// Add animated value for controls opacity
const fadeAnim = useRef(new Animated.Value(1)).current;
// Add buffered state
const [buffered, setBuffered] = useState<number>(0);
// Add VLC specific state and refs
const [isBuffering, setIsBuffering] = useState(false);
// Modify audio tracks handling for VLC
const [vlcAudioTracks, setVlcAudioTracks] = useState<Array<{id: number, name: string, language?: string}>>([]);
const [vlcTextTracks, setVlcTextTracks] = useState<Array<{id: number, name: string, language?: string}>>([]);
// Lock screen to landscape when component mounts
useEffect(() => {
@ -344,22 +361,29 @@ const VideoPlayer: React.FC = () => {
};
const onSliderValueChange = (value: number) => {
if (videoRef.current) {
if (vlcRef.current) {
const newTime = Math.floor(value);
videoRef.current.seek(newTime);
vlcRef.current.seek(newTime);
setCurrentTime(newTime);
progress.value = newTime;
}
};
const togglePlayback = () => {
setPaused(!paused);
if (vlcRef.current) {
if (paused) {
vlcRef.current.resume();
} else {
vlcRef.current.pause();
}
setPaused(!paused);
}
};
const skip = (seconds: number) => {
if (videoRef.current) {
if (vlcRef.current) {
const newTime = Math.max(0, Math.min(currentTime + seconds, duration));
videoRef.current.seek(newTime);
vlcRef.current.seek(newTime);
setCurrentTime(newTime);
progress.value = newTime;
}
@ -370,21 +394,21 @@ const VideoPlayer: React.FC = () => {
progress.value = data.currentTime;
};
const onLoad = (data: { duration: number }) => {
setDuration(data.duration);
max.value = data.duration;
const onLoad = (data: any) => {
setDuration(data.duration / 1000); // VLC returns duration in milliseconds
max.value = data.duration / 1000;
logger.log(`[VideoPlayer] Video loaded with duration: ${data.duration}`);
logger.log(`[VideoPlayer] Video loaded with duration: ${data.duration / 1000}`);
// If we have an initial position to seek to, do it now
if (initialPosition !== null && !isInitialSeekComplete && videoRef.current) {
if (initialPosition !== null && !isInitialSeekComplete && vlcRef.current) {
logger.log(`[VideoPlayer] Will seek to saved position: ${initialPosition}`);
// Seek immediately with a small delay
setTimeout(() => {
if (videoRef.current) {
if (vlcRef.current) {
try {
videoRef.current.seek(initialPosition);
vlcRef.current.seek(initialPosition);
setCurrentTime(initialPosition);
progress.value = initialPosition;
setIsInitialSeekComplete(true);
@ -393,7 +417,7 @@ const VideoPlayer: React.FC = () => {
logger.error('[VideoPlayer] Error seeking to saved position:', error);
}
} else {
logger.error('[VideoPlayer] videoRef is no longer valid when attempting to seek');
logger.error('[VideoPlayer] vlcRef is no longer valid when attempting to seek');
}
}, 1000); // Increase delay to ensure video is fully loaded
} else {
@ -402,7 +426,7 @@ const VideoPlayer: React.FC = () => {
} else if (isInitialSeekComplete) {
logger.log(`[VideoPlayer] Initial seek already completed`);
} else {
logger.log(`[VideoPlayer] videoRef not available for seeking`);
logger.log(`[VideoPlayer] vlcRef not available for seeking`);
}
}
};
@ -484,12 +508,12 @@ const VideoPlayer: React.FC = () => {
}
}, [showSubtitleModal, textTracks]);
// Attempt to seek once videoRef is available
// Attempt to seek once vlcRef is available
useEffect(() => {
if (initialPosition !== null && !isInitialSeekComplete && videoRef.current) {
logger.log(`[VideoPlayer] videoRef is now available, attempting to seek to: ${initialPosition}`);
if (initialPosition !== null && !isInitialSeekComplete && vlcRef.current) {
logger.log(`[VideoPlayer] vlcRef is now available, attempting to seek to: ${initialPosition}`);
try {
videoRef.current.seek(initialPosition);
vlcRef.current.seek(initialPosition);
setCurrentTime(initialPosition);
progress.value = initialPosition;
setIsInitialSeekComplete(true);
@ -498,7 +522,7 @@ const VideoPlayer: React.FC = () => {
logger.error('[VideoPlayer] Error seeking to position on ref available:', error);
}
}
}, [videoRef.current, initialPosition, isInitialSeekComplete]);
}, [vlcRef.current, initialPosition, isInitialSeekComplete]);
// Load resume preference on mount
useEffect(() => {
@ -539,9 +563,9 @@ const VideoPlayer: React.FC = () => {
}
};
// Handle resume from overlay - modified to save preference
// Handle resume from overlay - modified for VLC
const handleResume = async () => {
if (resumePosition !== null && videoRef.current) {
if (resumePosition !== null && vlcRef.current) {
logger.log(`[VideoPlayer] Resuming from ${resumePosition}`);
// Save preference if remember choice is checked
@ -558,10 +582,17 @@ const VideoPlayer: React.FC = () => {
setInitialPosition(resumePosition);
// Hide overlay
setShowResumeOverlay(false);
// Seek to position with VLC
setTimeout(() => {
if (vlcRef.current) {
vlcRef.current.seek(resumePosition);
}
}, 500);
}
};
// Handle start from beginning - modified to save preference
// Handle start from beginning - modified for VLC
const handleStartFromBeginning = async () => {
logger.log(`[VideoPlayer] Starting from beginning`);
@ -580,8 +611,8 @@ const VideoPlayer: React.FC = () => {
// Set initial position to 0
setInitialPosition(0);
// Make sure we seek to beginning
if (videoRef.current) {
videoRef.current.seek(0);
if (vlcRef.current) {
vlcRef.current.seek(0);
setCurrentTime(0);
progress.value = 0;
}
@ -600,45 +631,269 @@ const VideoPlayer: React.FC = () => {
setShowControls(!showControls);
};
// Add onBuffer handler to Video component
const onBuffer = ({ isBuffering }: { isBuffering: boolean }) => {
// You can use this to show a loading indicator if needed
logger.log(`[VideoPlayer] Buffering: ${isBuffering}`);
};
// Add onProgress handler to track buffered data
const onLoadStart = () => {
setBuffered(0);
};
const handleProgress = (data: { currentTime: number, playableDuration: number, seekableDuration?: number }) => {
setCurrentTime(data.currentTime);
progress.value = data.currentTime;
// Handle VLC progress updates
const handleProgress = (event: any) => {
const currentTimeInSeconds = event.currentTime / 1000; // VLC gives time in milliseconds
setCurrentTime(currentTimeInSeconds);
progress.value = currentTimeInSeconds;
// Update buffered position
const bufferedTime = event.bufferTime / 1000 || currentTimeInSeconds;
setBuffered(bufferedTime);
// Ensure playableDuration is always at least equal to currentTime
const effectivePlayableDuration = Math.max(data.currentTime, data.playableDuration);
setBuffered(effectivePlayableDuration);
// Calculate buffer ahead (cannot be negative)
const bufferAhead = Math.max(0, effectivePlayableDuration - data.currentTime);
const bufferPercentage = ((effectivePlayableDuration / (duration || 1)) * 100);
const bufferAhead = Math.max(0, bufferedTime - currentTimeInSeconds);
const bufferPercentage = ((bufferedTime / (duration || 1)) * 100);
// Add detailed buffer logging
logger.log(`[VideoPlayer] Buffer Status:
Current Time: ${data.currentTime.toFixed(2)}s
Playable Duration: ${effectivePlayableDuration.toFixed(2)}s
Current Time: ${currentTimeInSeconds.toFixed(2)}s
Buffered: ${bufferedTime.toFixed(2)}s
Buffered Ahead: ${bufferAhead.toFixed(2)}s
Seekable Duration: ${data.seekableDuration?.toFixed(2) || 'N/A'}s
Buffer Percentage: ${bufferPercentage.toFixed(1)}%
`);
};
// Add onError handler
// Handle VLC errors
const handleError = (error: any) => {
logger.error('[VideoPlayer] Playback Error:', error);
// Optionally, you could show an error message to the user here
};
// Handle VLC buffering
const onBuffering = (event: any) => {
setIsBuffering(event.isBuffering);
logger.log(`[VideoPlayer] Buffering: ${event.isBuffering}`);
};
// Handle VLC playback ended
const onEnd = () => {
// Your existing playback ended logic here
};
// Function to get audio tracks from VLC
const getAudioTracks = () => {
if (vlcRef.current) {
vlcRef.current.getAudioTracks().then((tracks: any) => {
setVlcAudioTracks(tracks || []);
logger.log("[VideoPlayer] Available VLC audio tracks:", tracks);
}).catch((error: any) => {
logger.error("[VideoPlayer] Failed to get audio tracks:", error);
});
}
};
// Function to select audio track in VLC
const selectAudioTrack = (trackId: number) => {
if (vlcRef.current) {
vlcRef.current.setAudioTrack(trackId);
setSelectedAudioTrack(trackId);
}
};
// Function to get subtitle tracks from VLC
const getTextTracks = () => {
if (vlcRef.current) {
vlcRef.current.getTextTracks().then((tracks: any) => {
setVlcTextTracks(tracks || []);
logger.log("[VideoPlayer] Available VLC subtitle tracks:", tracks);
}).catch((error: any) => {
logger.error("[VideoPlayer] Failed to get subtitle tracks:", error);
});
}
};
// Function to select subtitle track in VLC
const selectTextTrack = (trackId: number) => {
if (vlcRef.current) {
vlcRef.current.setTextTrack(trackId);
// Update your state accordingly
setSelectedTextTrack({ type: 'index', value: trackId });
}
};
// Add this useEffect to get audio and subtitle tracks after player is loaded
useEffect(() => {
if (duration > 0 && vlcRef.current) {
// Wait a bit for VLC to fully initialize and recognize tracks
setTimeout(() => {
getAudioTracks();
getTextTracks();
}, 2000);
}
}, [duration]);
// Update audio modal to use VLC audio tracks
const renderAudioModal = () => {
if (!showAudioModal) return null;
return (
<View style={styles.fullscreenOverlay}>
<View style={styles.enhancedModalContainer}>
<View style={styles.enhancedModalHeader}>
<Text style={styles.enhancedModalTitle}>Audio</Text>
<TouchableOpacity
style={styles.enhancedCloseButton}
onPress={() => setShowAudioModal(false)}
>
<Ionicons name="close" size={24} color="white" />
</TouchableOpacity>
</View>
<ScrollView style={styles.trackListScrollContainer}>
<View style={styles.trackListContainer}>
{vlcAudioTracks.length > 0 ? vlcAudioTracks.map(track => (
<TouchableOpacity
key={track.id}
style={styles.enhancedTrackItem}
onPress={() => {
selectAudioTrack(track.id);
setShowAudioModal(false);
}}
>
<View style={styles.trackInfoContainer}>
<Text style={styles.trackPrimaryText}>
{formatLanguage(track.language) || track.name || `Track ${track.id}`}
</Text>
{(track.name && track.language) && (
<Text style={styles.trackSecondaryText}>{track.name}</Text>
)}
</View>
{selectedAudioTrack === track.id && (
<View style={styles.selectedIndicatorContainer}>
<Ionicons name="checkmark" size={22} color="#E50914" />
</View>
)}
</TouchableOpacity>
)) : (
<View style={styles.emptyStateContainer}>
<Ionicons name="alert-circle-outline" size={40} color="#888" />
<Text style={styles.emptyStateText}>No audio tracks available</Text>
</View>
)}
</View>
</ScrollView>
</View>
</View>
);
};
// Update subtitle modal to use VLC subtitle tracks
const renderSubtitleModal = () => {
if (!showSubtitleModal) return null;
return (
<View style={styles.fullscreenOverlay}>
<View style={styles.enhancedModalContainer}>
<View style={styles.enhancedModalHeader}>
<Text style={styles.enhancedModalTitle}>Subtitles</Text>
<TouchableOpacity
style={styles.enhancedCloseButton}
onPress={() => setShowSubtitleModal(false)}
>
<Ionicons name="close" size={24} color="white" />
</TouchableOpacity>
</View>
<ScrollView style={styles.trackListScrollContainer}>
<View style={styles.trackListContainer}>
{/* Off option with improved design */}
<TouchableOpacity
style={styles.enhancedTrackItem}
onPress={() => {
selectTextTrack(-1); // -1 typically disables subtitles in VLC
setShowSubtitleModal(false);
}}
>
<View style={styles.trackInfoContainer}>
<Text style={styles.trackPrimaryText}>Off</Text>
</View>
{(selectedTextTrack?.type === 'disabled' ||
(selectedTextTrack?.type === 'index' && selectedTextTrack.value === -1)) && (
<View style={styles.selectedIndicatorContainer}>
<Ionicons name="checkmark" size={22} color="#E50914" />
</View>
)}
</TouchableOpacity>
{/* Available subtitle tracks with improved design */}
{vlcTextTracks.length > 0 ? vlcTextTracks.map(track => (
<TouchableOpacity
key={track.id}
style={styles.enhancedTrackItem}
onPress={() => {
selectTextTrack(track.id);
setShowSubtitleModal(false);
}}
>
<View style={styles.trackInfoContainer}>
<Text style={styles.trackPrimaryText}>
{formatLanguage(track.language) || track.name || `Subtitle ${track.id}`}
</Text>
{(track.name && track.language) && (
<Text style={styles.trackSecondaryText}>{track.name}</Text>
)}
</View>
{selectedTextTrack?.type === 'index' &&
selectedTextTrack?.value === track.id && (
<View style={styles.selectedIndicatorContainer}>
<Ionicons name="checkmark" size={22} color="#E50914" />
</View>
)}
</TouchableOpacity>
)) : (
<View style={styles.emptyStateContainer}>
<Ionicons name="alert-circle-outline" size={40} color="#888" />
<Text style={styles.emptyStateText}>No subtitle tracks available</Text>
</View>
)}
</View>
</ScrollView>
</View>
</View>
);
};
// Update the getInfo method for VLC
const getInfo = async () => {
if (vlcRef.current) {
try {
const position = await vlcRef.current.getPosition();
const lengthResult = await vlcRef.current.getLength();
return {
currentTime: position,
duration: lengthResult / 1000 // Convert to seconds
};
} catch (e) {
logger.error('[VideoPlayer] Error getting playback info:', e);
return {
currentTime: currentTime,
duration: duration
};
}
}
return {
currentTime: 0,
duration: 0
};
};
// VLC specific method to set playback speed
const changePlaybackSpeed = (speed: number) => {
if (vlcRef.current) {
vlcRef.current.setRate(speed);
setPlaybackSpeed(speed);
}
};
// VLC specific method for volume control
const setVolume = (volumeLevel: number) => {
if (vlcRef.current) {
// VLC volume is typically between 0-200
vlcRef.current.setVolume(volumeLevel * 200);
}
};
return (
<View style={styles.container}>
<TouchableOpacity
@ -646,26 +901,23 @@ const VideoPlayer: React.FC = () => {
onPress={toggleControls}
activeOpacity={1}
>
<Video
ref={videoRef}
source={{ uri }}
<VLCPlayer
ref={vlcRef}
source={{
uri: uri,
}}
style={styles.video}
paused={paused || showResumeOverlay}
resizeMode={resizeMode}
resizeMode={resizeMode as any} // Type cast to avoid type error
onLoad={onLoad}
onProgress={handleProgress}
rate={playbackSpeed}
progressUpdateInterval={250}
selectedAudioTrack={selectedAudioTrack !== null ?
{ type: 'index', value: selectedAudioTrack } as any :
undefined
}
onAudioTracks={onAudioTracks}
selectedTextTrack={selectedTextTrack as any}
onTextTracks={onTextTracks}
onBuffer={onBuffer}
onLoadStart={onLoadStart}
onError={handleError}
onEnd={onEnd}
// VLC specific props
autoAspectRatio={true}
// autoReloadOnError={true} - Removed, not supported by VLCPlayer
// Note: VLC handles audio tracks differently, we'll need to adjust the UI for this
/>
{/* Slider Container with buffer indicator */}
@ -870,129 +1122,9 @@ const VideoPlayer: React.FC = () => {
)}
</TouchableOpacity>
{/* Audio Selection Modal - Updated language display */}
{showAudioModal && (
<View style={styles.fullscreenOverlay}>
<View style={styles.enhancedModalContainer}>
<View style={styles.enhancedModalHeader}>
<Text style={styles.enhancedModalTitle}>Audio</Text>
<TouchableOpacity
style={styles.enhancedCloseButton}
onPress={() => setShowAudioModal(false)}
>
<Ionicons name="close" size={24} color="white" />
</TouchableOpacity>
</View>
<ScrollView style={styles.trackListScrollContainer}>
<View style={styles.trackListContainer}>
{audioTracks.length > 0 ? audioTracks.map(track => (
<TouchableOpacity
key={track.index}
style={styles.enhancedTrackItem}
onPress={() => {
setSelectedAudioTrack(track.index);
setShowAudioModal(false);
}}
>
<View style={styles.trackInfoContainer}>
<Text style={styles.trackPrimaryText}>
{formatLanguage(track.language) || track.title || `Track ${track.index + 1}`}
</Text>
{(track.title && track.language) && (
<Text style={styles.trackSecondaryText}>{track.title}</Text>
)}
{track.type && <Text style={styles.trackSecondaryText}>{track.type}</Text>}
</View>
{selectedAudioTrack === track.index && (
<View style={styles.selectedIndicatorContainer}>
<Ionicons name="checkmark" size={22} color="#E50914" />
</View>
)}
</TouchableOpacity>
)) : (
<View style={styles.emptyStateContainer}>
<Ionicons name="alert-circle-outline" size={40} color="#888" />
<Text style={styles.emptyStateText}>No audio tracks available</Text>
</View>
)}
</View>
</ScrollView>
</View>
</View>
)}
{/* Subtitle Selection Modal - Updated language display */}
{showSubtitleModal && (
<View style={styles.fullscreenOverlay}>
<View style={styles.enhancedModalContainer}>
<View style={styles.enhancedModalHeader}>
<Text style={styles.enhancedModalTitle}>Subtitles</Text>
<TouchableOpacity
style={styles.enhancedCloseButton}
onPress={() => setShowSubtitleModal(false)}
>
<Ionicons name="close" size={24} color="white" />
</TouchableOpacity>
</View>
<ScrollView style={styles.trackListScrollContainer}>
<View style={styles.trackListContainer}>
{/* Off option with improved design */}
<TouchableOpacity
style={styles.enhancedTrackItem}
onPress={() => {
setSelectedTextTrack({ type: 'disabled' });
setShowSubtitleModal(false);
}}
>
<View style={styles.trackInfoContainer}>
<Text style={styles.trackPrimaryText}>Off</Text>
</View>
{selectedTextTrack?.type === 'disabled' && (
<View style={styles.selectedIndicatorContainer}>
<Ionicons name="checkmark" size={22} color="#E50914" />
</View>
)}
</TouchableOpacity>
{/* Available subtitle tracks with improved design */}
{textTracks.length > 0 ? textTracks.map(track => (
<TouchableOpacity
key={track.index}
style={styles.enhancedTrackItem}
onPress={() => {
setSelectedTextTrack({ type: 'index', value: track.index });
setShowSubtitleModal(false);
}}
>
<View style={styles.trackInfoContainer}>
<Text style={styles.trackPrimaryText}>
{formatLanguage(track.language) || track.title || `Subtitle ${track.index + 1}`}
</Text>
{(track.title && track.language) && (
<Text style={styles.trackSecondaryText}>{track.title}</Text>
)}
{track.type && <Text style={styles.trackSecondaryText}>{track.type}</Text>}
</View>
{selectedTextTrack?.type === 'index' &&
selectedTextTrack?.value === track.index && (
<View style={styles.selectedIndicatorContainer}>
<Ionicons name="checkmark" size={22} color="#E50914" />
</View>
)}
</TouchableOpacity>
)) : (
<View style={styles.emptyStateContainer}>
<Ionicons name="alert-circle-outline" size={40} color="#888" />
<Text style={styles.emptyStateText}>No subtitle tracks available</Text>
</View>
)}
</View>
</ScrollView>
</View>
</View>
)}
{/* Use the new modal rendering functions */}
{renderAudioModal()}
{renderSubtitleModal()}
</View>
);
};

View file

@ -1,6 +1,8 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
"strict": true,
"jsx": "react-jsx",
"esModuleInterop": true
}
}