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-tab-view": "^4.0.10",
"react-native-url-polyfill": "^2.0.0", "react-native-url-polyfill": "^2.0.0",
"react-native-video": "^6.12.0", "react-native-video": "^6.12.0",
"react-native-vlc-media-player": "^1.0.87",
"react-native-web": "~0.19.13", "react-native-web": "~0.19.13",
"react-native-wheel-color-picker": "^1.3.1", "react-native-wheel-color-picker": "^1.3.1",
"subsrt": "^1.1.1" "subsrt": "^1.1.1"
@ -11294,6 +11295,15 @@
"react-native": "*" "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": { "node_modules/react-native-svg": {
"version": "15.11.2", "version": "15.11.2",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.11.2.tgz", "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.11.2.tgz",
@ -11455,6 +11465,100 @@
"react-native": "*" "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": { "node_modules/react-native-web": {
"version": "0.19.13", "version": "0.19.13",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz", "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-system-ui": "^4.0.9",
"expo-web-browser": "~14.0.2", "expo-web-browser": "~14.0.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"node-fetch": "^2.6.7",
"react": "18.3.1", "react": "18.3.1",
"react-native": "0.76.9", "react-native": "0.76.9",
"react-native-awesome-slider": "^2.9.0", "react-native-awesome-slider": "^2.9.0",
@ -61,10 +62,10 @@
"react-native-tab-view": "^4.0.10", "react-native-tab-view": "^4.0.10",
"react-native-url-polyfill": "^2.0.0", "react-native-url-polyfill": "^2.0.0",
"react-native-video": "^6.12.0", "react-native-video": "^6.12.0",
"react-native-vlc-media-player": "^1.0.87",
"react-native-web": "~0.19.13", "react-native-web": "~0.19.13",
"react-native-wheel-color-picker": "^1.3.1", "react-native-wheel-color-picker": "^1.3.1",
"subsrt": "^1.1.1", "subsrt": "^1.1.1"
"node-fetch": "^2.6.7"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",

View file

@ -1,6 +1,6 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { View, TouchableOpacity, StyleSheet, Text, Dimensions, Modal, Pressable, StatusBar, Platform, ScrollView, Animated } from 'react-native'; 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 { Ionicons } from '@expo/vector-icons';
import { Slider } from 'react-native-awesome-slider'; import { Slider } from 'react-native-awesome-slider';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
@ -69,9 +69,9 @@ interface TextTrack {
type?: string | null; // Adjusting type based on linter error type?: string | null; // Adjusting type based on linter error
} }
// Define the possible resize modes // Define the possible resize modes - adjust to match VLCPlayer's PlayerResizeMode options
type ResizeModeType = 'contain' | 'cover' | 'stretch' | 'none'; type ResizeModeType = 'contain' | 'cover' | 'fill' | 'none';
const resizeModes: ResizeModeType[] = ['contain', 'cover', 'stretch']; const resizeModes: ResizeModeType[] = ['contain', 'cover', 'fill'];
// Add language code to name mapping // Add language code to name mapping
const languageMap: {[key: string]: string} = { const languageMap: {[key: string]: string} = {
@ -132,6 +132,18 @@ const formatLanguage = (code?: string): string => {
return languageMap[normalized] || code.toUpperCase(); 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 VideoPlayer: React.FC = () => {
const navigation = useNavigation(); const navigation = useNavigation();
const route = useRoute<RouteProp<RootStackParamList, 'Player'>>(); const route = useRoute<RouteProp<RootStackParamList, 'Player'>>();
@ -176,7 +188,8 @@ const VideoPlayer: React.FC = () => {
const [textTracks, setTextTracks] = useState<TextTrack[]>([]); const [textTracks, setTextTracks] = useState<TextTrack[]>([]);
const [selectedTextTrack, setSelectedTextTrack] = useState<SelectedTrack | null>({ type: 'disabled' }); const [selectedTextTrack, setSelectedTextTrack] = useState<SelectedTrack | null>({ type: 'disabled' });
const [resizeMode, setResizeMode] = useState<ResizeModeType>('contain'); // State for resize mode 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 progress = useSharedValue(0);
const min = useSharedValue(0); const min = useSharedValue(0);
const max = useSharedValue(duration); const max = useSharedValue(duration);
@ -199,8 +212,12 @@ const VideoPlayer: React.FC = () => {
// Add animated value for controls opacity // Add animated value for controls opacity
const fadeAnim = useRef(new Animated.Value(1)).current; const fadeAnim = useRef(new Animated.Value(1)).current;
// Add buffered state // Add VLC specific state and refs
const [buffered, setBuffered] = useState<number>(0); 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 // Lock screen to landscape when component mounts
useEffect(() => { useEffect(() => {
@ -344,22 +361,29 @@ const VideoPlayer: React.FC = () => {
}; };
const onSliderValueChange = (value: number) => { const onSliderValueChange = (value: number) => {
if (videoRef.current) { if (vlcRef.current) {
const newTime = Math.floor(value); const newTime = Math.floor(value);
videoRef.current.seek(newTime); vlcRef.current.seek(newTime);
setCurrentTime(newTime); setCurrentTime(newTime);
progress.value = newTime; progress.value = newTime;
} }
}; };
const togglePlayback = () => { const togglePlayback = () => {
setPaused(!paused); if (vlcRef.current) {
if (paused) {
vlcRef.current.resume();
} else {
vlcRef.current.pause();
}
setPaused(!paused);
}
}; };
const skip = (seconds: number) => { const skip = (seconds: number) => {
if (videoRef.current) { if (vlcRef.current) {
const newTime = Math.max(0, Math.min(currentTime + seconds, duration)); const newTime = Math.max(0, Math.min(currentTime + seconds, duration));
videoRef.current.seek(newTime); vlcRef.current.seek(newTime);
setCurrentTime(newTime); setCurrentTime(newTime);
progress.value = newTime; progress.value = newTime;
} }
@ -370,21 +394,21 @@ const VideoPlayer: React.FC = () => {
progress.value = data.currentTime; progress.value = data.currentTime;
}; };
const onLoad = (data: { duration: number }) => { const onLoad = (data: any) => {
setDuration(data.duration); setDuration(data.duration / 1000); // VLC returns duration in milliseconds
max.value = data.duration; 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 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}`); logger.log(`[VideoPlayer] Will seek to saved position: ${initialPosition}`);
// Seek immediately with a small delay // Seek immediately with a small delay
setTimeout(() => { setTimeout(() => {
if (videoRef.current) { if (vlcRef.current) {
try { try {
videoRef.current.seek(initialPosition); vlcRef.current.seek(initialPosition);
setCurrentTime(initialPosition); setCurrentTime(initialPosition);
progress.value = initialPosition; progress.value = initialPosition;
setIsInitialSeekComplete(true); setIsInitialSeekComplete(true);
@ -393,7 +417,7 @@ const VideoPlayer: React.FC = () => {
logger.error('[VideoPlayer] Error seeking to saved position:', error); logger.error('[VideoPlayer] Error seeking to saved position:', error);
} }
} else { } 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 }, 1000); // Increase delay to ensure video is fully loaded
} else { } else {
@ -402,7 +426,7 @@ const VideoPlayer: React.FC = () => {
} else if (isInitialSeekComplete) { } else if (isInitialSeekComplete) {
logger.log(`[VideoPlayer] Initial seek already completed`); logger.log(`[VideoPlayer] Initial seek already completed`);
} else { } 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]); }, [showSubtitleModal, textTracks]);
// Attempt to seek once videoRef is available // Attempt to seek once vlcRef is available
useEffect(() => { useEffect(() => {
if (initialPosition !== null && !isInitialSeekComplete && videoRef.current) { if (initialPosition !== null && !isInitialSeekComplete && vlcRef.current) {
logger.log(`[VideoPlayer] videoRef is now available, attempting to seek to: ${initialPosition}`); logger.log(`[VideoPlayer] vlcRef is now available, attempting to seek to: ${initialPosition}`);
try { try {
videoRef.current.seek(initialPosition); vlcRef.current.seek(initialPosition);
setCurrentTime(initialPosition); setCurrentTime(initialPosition);
progress.value = initialPosition; progress.value = initialPosition;
setIsInitialSeekComplete(true); setIsInitialSeekComplete(true);
@ -498,7 +522,7 @@ const VideoPlayer: React.FC = () => {
logger.error('[VideoPlayer] Error seeking to position on ref available:', error); logger.error('[VideoPlayer] Error seeking to position on ref available:', error);
} }
} }
}, [videoRef.current, initialPosition, isInitialSeekComplete]); }, [vlcRef.current, initialPosition, isInitialSeekComplete]);
// Load resume preference on mount // Load resume preference on mount
useEffect(() => { 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 () => { const handleResume = async () => {
if (resumePosition !== null && videoRef.current) { if (resumePosition !== null && vlcRef.current) {
logger.log(`[VideoPlayer] Resuming from ${resumePosition}`); logger.log(`[VideoPlayer] Resuming from ${resumePosition}`);
// Save preference if remember choice is checked // Save preference if remember choice is checked
@ -558,10 +582,17 @@ const VideoPlayer: React.FC = () => {
setInitialPosition(resumePosition); setInitialPosition(resumePosition);
// Hide overlay // Hide overlay
setShowResumeOverlay(false); 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 () => { const handleStartFromBeginning = async () => {
logger.log(`[VideoPlayer] Starting from beginning`); logger.log(`[VideoPlayer] Starting from beginning`);
@ -580,8 +611,8 @@ const VideoPlayer: React.FC = () => {
// Set initial position to 0 // Set initial position to 0
setInitialPosition(0); setInitialPosition(0);
// Make sure we seek to beginning // Make sure we seek to beginning
if (videoRef.current) { if (vlcRef.current) {
videoRef.current.seek(0); vlcRef.current.seek(0);
setCurrentTime(0); setCurrentTime(0);
progress.value = 0; progress.value = 0;
} }
@ -600,45 +631,269 @@ const VideoPlayer: React.FC = () => {
setShowControls(!showControls); setShowControls(!showControls);
}; };
// Add onBuffer handler to Video component // Handle VLC progress updates
const onBuffer = ({ isBuffering }: { isBuffering: boolean }) => { const handleProgress = (event: any) => {
// You can use this to show a loading indicator if needed const currentTimeInSeconds = event.currentTime / 1000; // VLC gives time in milliseconds
logger.log(`[VideoPlayer] Buffering: ${isBuffering}`); setCurrentTime(currentTimeInSeconds);
}; progress.value = currentTimeInSeconds;
// Add onProgress handler to track buffered data // Update buffered position
const onLoadStart = () => { const bufferedTime = event.bufferTime / 1000 || currentTimeInSeconds;
setBuffered(0); setBuffered(bufferedTime);
};
const handleProgress = (data: { currentTime: number, playableDuration: number, seekableDuration?: number }) => {
setCurrentTime(data.currentTime);
progress.value = data.currentTime;
// 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) // Calculate buffer ahead (cannot be negative)
const bufferAhead = Math.max(0, effectivePlayableDuration - data.currentTime); const bufferAhead = Math.max(0, bufferedTime - currentTimeInSeconds);
const bufferPercentage = ((effectivePlayableDuration / (duration || 1)) * 100); const bufferPercentage = ((bufferedTime / (duration || 1)) * 100);
// Add detailed buffer logging // Add detailed buffer logging
logger.log(`[VideoPlayer] Buffer Status: logger.log(`[VideoPlayer] Buffer Status:
Current Time: ${data.currentTime.toFixed(2)}s Current Time: ${currentTimeInSeconds.toFixed(2)}s
Playable Duration: ${effectivePlayableDuration.toFixed(2)}s Buffered: ${bufferedTime.toFixed(2)}s
Buffered Ahead: ${bufferAhead.toFixed(2)}s Buffered Ahead: ${bufferAhead.toFixed(2)}s
Seekable Duration: ${data.seekableDuration?.toFixed(2) || 'N/A'}s
Buffer Percentage: ${bufferPercentage.toFixed(1)}% Buffer Percentage: ${bufferPercentage.toFixed(1)}%
`); `);
}; };
// Add onError handler // Handle VLC errors
const handleError = (error: any) => { const handleError = (error: any) => {
logger.error('[VideoPlayer] Playback Error:', error); logger.error('[VideoPlayer] Playback Error:', error);
// Optionally, you could show an error message to the user here // 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 ( return (
<View style={styles.container}> <View style={styles.container}>
<TouchableOpacity <TouchableOpacity
@ -646,26 +901,23 @@ const VideoPlayer: React.FC = () => {
onPress={toggleControls} onPress={toggleControls}
activeOpacity={1} activeOpacity={1}
> >
<Video <VLCPlayer
ref={videoRef} ref={vlcRef}
source={{ uri }} source={{
uri: uri,
}}
style={styles.video} style={styles.video}
paused={paused || showResumeOverlay} paused={paused || showResumeOverlay}
resizeMode={resizeMode} resizeMode={resizeMode as any} // Type cast to avoid type error
onLoad={onLoad} onLoad={onLoad}
onProgress={handleProgress} onProgress={handleProgress}
rate={playbackSpeed} 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} 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 */} {/* Slider Container with buffer indicator */}
@ -870,129 +1122,9 @@ const VideoPlayer: React.FC = () => {
)} )}
</TouchableOpacity> </TouchableOpacity>
{/* Audio Selection Modal - Updated language display */} {/* Use the new modal rendering functions */}
{showAudioModal && ( {renderAudioModal()}
<View style={styles.fullscreenOverlay}> {renderSubtitleModal()}
<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>
)}
</View> </View>
); );
}; };

View file

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