mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-20 16:22:04 +00:00
some VLCPLayer fixes
This commit is contained in:
parent
07eab50848
commit
004ee178a4
2 changed files with 509 additions and 283 deletions
|
|
@ -36,6 +36,7 @@ import { AudioTrackModal } from './modals/AudioTrackModal';
|
||||||
import PlayerControls from './controls/PlayerControls';
|
import PlayerControls from './controls/PlayerControls';
|
||||||
import CustomSubtitles from './subtitles/CustomSubtitles';
|
import CustomSubtitles from './subtitles/CustomSubtitles';
|
||||||
import { SourcesModal } from './modals/SourcesModal';
|
import { SourcesModal } from './modals/SourcesModal';
|
||||||
|
import VlcVideoPlayer, { VlcPlayerRef } from './VlcVideoPlayer';
|
||||||
import { stremioService } from '../../services/stremioService';
|
import { stremioService } from '../../services/stremioService';
|
||||||
import { shouldUseKSPlayer } from '../../utils/playerSelection';
|
import { shouldUseKSPlayer } from '../../utils/playerSelection';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
@ -96,17 +97,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
logger.log(`[AndroidVideoPlayer] Player selection: ${playerType} (${reason})`);
|
logger.log(`[AndroidVideoPlayer] Player selection: ${playerType} (${reason})`);
|
||||||
}, [useVLC, forceVlc]);
|
}, [useVLC, forceVlc]);
|
||||||
|
|
||||||
// Resolve VLC view dynamically to avoid iOS loading the Android native module
|
|
||||||
const LibVlcPlayerViewComponent: any = useMemo(() => {
|
|
||||||
if (Platform.OS !== 'android') return null;
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const mod = require('expo-libvlc-player');
|
|
||||||
return mod?.LibVlcPlayerView || null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
// Check if the stream is HLS (m3u8 playlist)
|
// Check if the stream is HLS (m3u8 playlist)
|
||||||
|
|
@ -222,14 +212,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const [rnVideoAudioTracks, setRnVideoAudioTracks] = useState<Array<{id: number, name: string, language?: string}>>([]);
|
const [rnVideoAudioTracks, setRnVideoAudioTracks] = useState<Array<{id: number, name: string, language?: string}>>([]);
|
||||||
const [rnVideoTextTracks, setRnVideoTextTracks] = useState<Array<{id: number, name: string, language?: string}>>([]);
|
const [rnVideoTextTracks, setRnVideoTextTracks] = useState<Array<{id: number, name: string, language?: string}>>([]);
|
||||||
|
|
||||||
// VLC tracks state
|
|
||||||
const [vlcAudioTracks, setVlcAudioTracks] = useState<Array<{id: number, name: string, language?: string}>>([]);
|
|
||||||
const [vlcSubtitleTracks, setVlcSubtitleTracks] = useState<Array<{id: number, name: string, language?: string}>>([]);
|
|
||||||
const [vlcSelectedAudioTrack, setVlcSelectedAudioTrack] = useState<number | undefined>(undefined);
|
|
||||||
const [vlcSelectedSubtitleTrack, setVlcSelectedSubtitleTrack] = useState<number | undefined>(undefined);
|
|
||||||
const [vlcRestoreTime, setVlcRestoreTime] = useState<number | undefined>(undefined); // Time to restore after remount
|
|
||||||
const [forceVlcRemount, setForceVlcRemount] = useState(false); // Force complete unmount/remount
|
|
||||||
|
|
||||||
// Debounce track updates to prevent excessive processing
|
// Debounce track updates to prevent excessive processing
|
||||||
const trackUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const trackUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
|
@ -239,39 +221,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
// Debounce gesture operations to prevent rapid-fire events
|
// Debounce gesture operations to prevent rapid-fire events
|
||||||
const gestureDebounceRef = useRef<NodeJS.Timeout | null>(null);
|
const gestureDebounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
// Memoize VLC tracks prop to prevent unnecessary re-renders
|
|
||||||
const vlcTracks = useMemo(() => ({
|
|
||||||
audio: vlcSelectedAudioTrack,
|
|
||||||
video: 0, // Use first video track
|
|
||||||
subtitle: vlcSelectedSubtitleTrack
|
|
||||||
}), [vlcSelectedAudioTrack, vlcSelectedSubtitleTrack]);
|
|
||||||
|
|
||||||
// Format VLC tracks to match RN Video format - raw version
|
|
||||||
const formatVlcTracks = useCallback((vlcTracks: Array<{id: number, name: string}>) => {
|
|
||||||
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 (DEBUG_MODE) {
|
|
||||||
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
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Process URL for VLC compatibility
|
// Process URL for VLC compatibility
|
||||||
const processUrlForVLC = useCallback((url: string): string => {
|
const processUrlForVLC = useCallback((url: string): string => {
|
||||||
|
|
@ -304,79 +254,36 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Optimized VLC track processing function with reduced JSON operations
|
|
||||||
const processVlcTracks = useCallback((tracks: any, source: string) => {
|
|
||||||
if (!tracks) return;
|
|
||||||
|
|
||||||
// Log raw VLC tracks data for debugging
|
// VLC track state - will be managed by VlcVideoPlayer component
|
||||||
if (DEBUG_MODE) {
|
const [vlcAudioTracks, setVlcAudioTracks] = useState<Array<{id: number, name: string, language?: string}>>([]);
|
||||||
logger.log(`[VLC] ${source} - Raw tracks data:`, tracks);
|
const [vlcSubtitleTracks, setVlcSubtitleTracks] = useState<Array<{id: number, name: string, language?: string}>>([]);
|
||||||
|
const [vlcSelectedAudioTrack, setVlcSelectedAudioTrack] = useState<number | undefined>(undefined);
|
||||||
|
const [vlcSelectedSubtitleTrack, setVlcSelectedSubtitleTrack] = useState<number | undefined>(undefined);
|
||||||
|
const [vlcRestoreTime, setVlcRestoreTime] = useState<number | undefined>(undefined); // Time to restore after remount
|
||||||
|
const [forceVlcRemount, setForceVlcRemount] = useState(false); // Force complete unmount/remount
|
||||||
|
|
||||||
|
// VLC player ref for imperative methods
|
||||||
|
const vlcPlayerRef = useRef<VlcPlayerRef>(null);
|
||||||
|
|
||||||
|
// Track if VLC has loaded and needs initial play command
|
||||||
|
const vlcLoadedRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
// Handle VLC pause/play state changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (useVLC && vlcLoadedRef.current && vlcPlayerRef.current) {
|
||||||
|
if (paused) {
|
||||||
|
vlcPlayerRef.current.pause();
|
||||||
|
} else {
|
||||||
|
vlcPlayerRef.current.play();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}, [useVLC, paused]);
|
||||||
// Clear any pending updates
|
|
||||||
if (trackUpdateTimeoutRef.current) {
|
|
||||||
clearTimeout(trackUpdateTimeoutRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce track updates to prevent excessive processing
|
|
||||||
trackUpdateTimeoutRef.current = setTimeout(() => {
|
|
||||||
const { audio = [], subtitle = [] } = tracks;
|
|
||||||
let hasUpdates = false;
|
|
||||||
|
|
||||||
// Process audio tracks with optimized comparison
|
|
||||||
if (Array.isArray(audio) && audio.length > 0) {
|
|
||||||
const formattedAudio = formatVlcTracks(audio);
|
|
||||||
// Use length and first/last item comparison instead of full JSON.stringify
|
|
||||||
const audioChanged = formattedAudio.length !== vlcAudioTracks.length ||
|
|
||||||
(formattedAudio.length > 0 && vlcAudioTracks.length > 0 &&
|
|
||||||
(formattedAudio[0]?.id !== vlcAudioTracks[0]?.id ||
|
|
||||||
formattedAudio[formattedAudio.length - 1]?.id !== vlcAudioTracks[vlcAudioTracks.length - 1]?.id));
|
|
||||||
|
|
||||||
if (audioChanged) {
|
|
||||||
setVlcAudioTracks(formattedAudio);
|
|
||||||
hasUpdates = true;
|
|
||||||
// Only log in debug mode or when tracks actually change
|
|
||||||
if (DEBUG_MODE) {
|
|
||||||
logger.log(`[VLC] ${source} - Audio tracks updated:`, formattedAudio.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process subtitle tracks with optimized comparison
|
|
||||||
if (Array.isArray(subtitle) && subtitle.length > 0) {
|
|
||||||
const formattedSubs = formatVlcTracks(subtitle);
|
|
||||||
// Use length and first/last item comparison instead of full JSON.stringify
|
|
||||||
const subsChanged = formattedSubs.length !== vlcSubtitleTracks.length ||
|
|
||||||
(formattedSubs.length > 0 && vlcSubtitleTracks.length > 0 &&
|
|
||||||
(formattedSubs[0]?.id !== vlcSubtitleTracks[0]?.id ||
|
|
||||||
formattedSubs[formattedSubs.length - 1]?.id !== vlcSubtitleTracks[vlcSubtitleTracks.length - 1]?.id));
|
|
||||||
|
|
||||||
if (subsChanged) {
|
|
||||||
setVlcSubtitleTracks(formattedSubs);
|
|
||||||
hasUpdates = true;
|
|
||||||
if (DEBUG_MODE) {
|
|
||||||
logger.log(`[VLC] ${source} - Subtitle tracks updated:`, formattedSubs.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log summary only if tracks were actually updated
|
|
||||||
if (hasUpdates && DEBUG_MODE) {
|
|
||||||
logger.log(`[AndroidVideoPlayer][VLC] ${source} - Track processing complete. Audio: ${vlcAudioTracks.length}, Subs: ${vlcSubtitleTracks.length}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
trackUpdateTimeoutRef.current = null;
|
|
||||||
}, 100); // 100ms debounce
|
|
||||||
}, [formatVlcTracks, vlcAudioTracks, vlcSubtitleTracks]);
|
|
||||||
|
|
||||||
// Use VLC tracks directly (they only update when tracks change)
|
|
||||||
const vlcAudioTracksForModal = vlcAudioTracks;
|
|
||||||
const vlcSubtitleTracksForModal = vlcSubtitleTracks;
|
|
||||||
|
|
||||||
// Memoized computed props for child components
|
// Memoized computed props for child components
|
||||||
const ksAudioTracks = useMemo(() =>
|
const ksAudioTracks = useMemo(() =>
|
||||||
useVLC ? vlcAudioTracksForModal : rnVideoAudioTracks,
|
useVLC ? vlcAudioTracks : rnVideoAudioTracks,
|
||||||
[useVLC, vlcAudioTracksForModal, rnVideoAudioTracks]
|
[useVLC, vlcAudioTracks, rnVideoAudioTracks]
|
||||||
);
|
);
|
||||||
|
|
||||||
const computedSelectedAudioTrack = useMemo(() =>
|
const computedSelectedAudioTrack = useMemo(() =>
|
||||||
|
|
@ -389,8 +296,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const ksTextTracks = useMemo(() =>
|
const ksTextTracks = useMemo(() =>
|
||||||
useVLC ? vlcSubtitleTracksForModal : rnVideoTextTracks,
|
useVLC ? vlcSubtitleTracks : rnVideoTextTracks,
|
||||||
[useVLC, vlcSubtitleTracksForModal, rnVideoTextTracks]
|
[useVLC, vlcSubtitleTracks, rnVideoTextTracks]
|
||||||
);
|
);
|
||||||
|
|
||||||
const computedSelectedTextTrack = useMemo(() =>
|
const computedSelectedTextTrack = useMemo(() =>
|
||||||
|
|
@ -535,46 +442,77 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const vlcFallbackAttemptedRef = useRef(false);
|
const vlcFallbackAttemptedRef = useRef(false);
|
||||||
|
|
||||||
// VLC refs/state
|
// VLC key for forcing remounts
|
||||||
const vlcRef = useRef<any>(null);
|
|
||||||
const [vlcActive, setVlcActive] = useState(true); // Start as active
|
|
||||||
const [vlcKey, setVlcKey] = useState('vlc-initial'); // Force remount key
|
const [vlcKey, setVlcKey] = useState('vlc-initial'); // Force remount key
|
||||||
|
|
||||||
// Compute aspect ratio string for VLC (e.g., "16:9") based on current screen and resizeMode
|
// Handler for VLC track updates
|
||||||
const toVlcRatio = useCallback((w: number, h: number): string => {
|
const handleVlcTracksUpdate = useCallback((tracks: { audio: any[], subtitle: any[] }) => {
|
||||||
const a = Math.max(1, Math.round(w));
|
if (!tracks) return;
|
||||||
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 vlcAspectRatio = useMemo(() => {
|
// Clear any pending updates
|
||||||
if (!useVLC) return undefined as string | undefined;
|
if (trackUpdateTimeoutRef.current) {
|
||||||
// For VLC, we handle aspect ratio through custom zoom for cover mode
|
clearTimeout(trackUpdateTimeoutRef.current);
|
||||||
// Only force aspect for fill mode (stretch to fit)
|
|
||||||
if (resizeMode === 'fill') {
|
|
||||||
const sw = screenDimensions.width || 0;
|
|
||||||
const sh = screenDimensions.height || 0;
|
|
||||||
if (sw > 0 && sh > 0) {
|
|
||||||
return toVlcRatio(sw, sh);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// For cover/contain/none: let VLC preserve natural aspect, we handle zoom separately
|
|
||||||
return undefined;
|
|
||||||
}, [useVLC, resizeMode, screenDimensions.width, screenDimensions.height, toVlcRatio]);
|
|
||||||
|
|
||||||
// VLC options for better playback
|
// Debounce track updates to prevent excessive processing
|
||||||
const vlcOptions = useMemo(() => {
|
trackUpdateTimeoutRef.current = setTimeout(() => {
|
||||||
if (!useVLC) return [] as string[];
|
const { audio = [], subtitle = [] } = tracks;
|
||||||
// Basic options for network streaming
|
let hasUpdates = false;
|
||||||
return [
|
|
||||||
'--network-caching=2000',
|
// Process audio tracks
|
||||||
'--clock-jitter=0',
|
if (Array.isArray(audio) && audio.length > 0) {
|
||||||
'--http-reconnect',
|
const formattedAudio = audio.map(track => ({
|
||||||
'--sout-mux-caching=2000'
|
id: track.id,
|
||||||
];
|
name: track.name || `Track ${track.id + 1}`,
|
||||||
}, [useVLC]);
|
language: track.language
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Simple comparison - check if tracks changed
|
||||||
|
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;
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
logger.log(`[VLC] Audio tracks updated:`, formattedAudio.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process subtitle tracks
|
||||||
|
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;
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
logger.log(`[VLC] Subtitle tracks updated:`, formattedSubs.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasUpdates && DEBUG_MODE) {
|
||||||
|
logger.log(`[AndroidVideoPlayer][VLC] Track processing complete. Audio: ${vlcAudioTracks.length}, Subs: ${vlcSubtitleTracks.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackUpdateTimeoutRef.current = null;
|
||||||
|
}, 100); // 100ms debounce
|
||||||
|
}, [vlcAudioTracks, vlcSubtitleTracks]);
|
||||||
|
|
||||||
|
|
||||||
// Volume and brightness controls
|
// Volume and brightness controls
|
||||||
|
|
@ -934,6 +872,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
logger.log('[VLC] Forcing complete remount due to focus gain');
|
logger.log('[VLC] Forcing complete remount due to focus gain');
|
||||||
setVlcRestoreTime(currentTime); // Save current time for restoration
|
setVlcRestoreTime(currentTime); // Save current time for restoration
|
||||||
setForceVlcRemount(true);
|
setForceVlcRemount(true);
|
||||||
|
vlcLoadedRef.current = false; // Reset loaded state
|
||||||
// Re-enable after a brief moment
|
// Re-enable after a brief moment
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setForceVlcRemount(false);
|
setForceVlcRemount(false);
|
||||||
|
|
@ -954,6 +893,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
logger.log('[VLC] Forcing complete remount due to app foreground');
|
logger.log('[VLC] Forcing complete remount due to app foreground');
|
||||||
setVlcRestoreTime(currentTime); // Save current time for restoration
|
setVlcRestoreTime(currentTime); // Save current time for restoration
|
||||||
setForceVlcRemount(true);
|
setForceVlcRemount(true);
|
||||||
|
vlcLoadedRef.current = false; // Reset loaded state
|
||||||
// Re-enable after a brief moment
|
// Re-enable after a brief moment
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setForceVlcRemount(false);
|
setForceVlcRemount(false);
|
||||||
|
|
@ -1166,42 +1106,49 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const seekToTime = (rawSeconds: number) => {
|
const seekToTime = (rawSeconds: number) => {
|
||||||
// Clamp to just before the end of the media.
|
// Clamp to just before the end of the media.
|
||||||
const timeInSeconds = Math.max(0, Math.min(rawSeconds, duration > 0 ? duration - END_EPSILON : rawSeconds));
|
const timeInSeconds = Math.max(0, Math.min(rawSeconds, duration > 0 ? duration - END_EPSILON : rawSeconds));
|
||||||
if (useVLC && duration > 0) {
|
|
||||||
try {
|
if (useVLC) {
|
||||||
const fraction = Math.min(Math.max(timeInSeconds / duration, 0), 0.999);
|
// Use VLC imperative method
|
||||||
if (vlcRef.current && typeof vlcRef.current.seek === 'function') {
|
if (vlcPlayerRef.current && duration > 0) {
|
||||||
vlcRef.current.seek(fraction);
|
if (DEBUG_MODE) {
|
||||||
return;
|
if (__DEV__) logger.log(`[AndroidVideoPlayer][VLC] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`);
|
||||||
}
|
}
|
||||||
} catch {}
|
vlcPlayerRef.current.seek(timeInSeconds);
|
||||||
}
|
} else {
|
||||||
if (videoRef.current && duration > 0 && !isSeeking.current) {
|
if (DEBUG_MODE) {
|
||||||
if (DEBUG_MODE) {
|
logger.error(`[AndroidVideoPlayer][VLC] Seek failed: vlcRef=${!!vlcPlayerRef.current}, duration=${duration}`);
|
||||||
if (__DEV__) logger.log(`[AndroidVideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`);
|
|
||||||
}
|
|
||||||
|
|
||||||
isSeeking.current = true;
|
|
||||||
setSeekTime(timeInSeconds);
|
|
||||||
if (Platform.OS === 'ios') {
|
|
||||||
iosWasPausedDuringSeekRef.current = paused;
|
|
||||||
if (!paused) setPaused(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear seek state handled in onSeek; keep a fallback timeout
|
|
||||||
setTimeout(() => {
|
|
||||||
if (isMounted.current && isSeeking.current) {
|
|
||||||
setSeekTime(null);
|
|
||||||
isSeeking.current = false;
|
|
||||||
if (DEBUG_MODE) logger.log('[AndroidVideoPlayer] Seek fallback timeout cleared seeking state');
|
|
||||||
if (Platform.OS === 'ios' && iosWasPausedDuringSeekRef.current === false) {
|
|
||||||
setPaused(false);
|
|
||||||
iosWasPausedDuringSeekRef.current = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, 1200);
|
}
|
||||||
} else {
|
} else {
|
||||||
if (DEBUG_MODE) {
|
// Use react-native-video method
|
||||||
logger.error(`[AndroidVideoPlayer] Seek failed: videoRef=${!!videoRef.current}, duration=${duration}, seeking=${isSeeking.current}`);
|
if (videoRef.current && duration > 0 && !isSeeking.current) {
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
if (__DEV__) logger.log(`[AndroidVideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSeeking.current = true;
|
||||||
|
setSeekTime(timeInSeconds);
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
iosWasPausedDuringSeekRef.current = paused;
|
||||||
|
if (!paused) setPaused(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear seek state handled in onSeek; keep a fallback timeout
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isMounted.current && isSeeking.current) {
|
||||||
|
setSeekTime(null);
|
||||||
|
isSeeking.current = false;
|
||||||
|
if (DEBUG_MODE) logger.log('[AndroidVideoPlayer] Seek fallback timeout cleared seeking state');
|
||||||
|
if (Platform.OS === 'ios' && iosWasPausedDuringSeekRef.current === false) {
|
||||||
|
setPaused(false);
|
||||||
|
iosWasPausedDuringSeekRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1200);
|
||||||
|
} else {
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
logger.error(`[AndroidVideoPlayer] Seek failed: videoRef=${!!videoRef.current}, duration=${duration}, seeking=${isSeeking.current}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1853,7 +1800,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
errorString.includes('ERROR_CODE_DECODER_INIT_FAILED');
|
errorString.includes('ERROR_CODE_DECODER_INIT_FAILED');
|
||||||
|
|
||||||
// If it's a codec error and we're not already using VLC, silently switch to VLC
|
// If it's a codec error and we're not already using VLC, silently switch to VLC
|
||||||
if (isCodecError && !useVLC && LibVlcPlayerViewComponent && !vlcFallbackAttemptedRef.current) {
|
if (isCodecError && !useVLC && !vlcFallbackAttemptedRef.current) {
|
||||||
vlcFallbackAttemptedRef.current = true;
|
vlcFallbackAttemptedRef.current = true;
|
||||||
logger.warn('[AndroidVideoPlayer] Codec error detected, silently switching to VLC');
|
logger.warn('[AndroidVideoPlayer] Codec error detected, silently switching to VLC');
|
||||||
// Clear any existing timeout
|
// Clear any existing timeout
|
||||||
|
|
@ -2472,23 +2419,12 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
|
|
||||||
const togglePlayback = useCallback(() => {
|
const togglePlayback = useCallback(() => {
|
||||||
const newPausedState = !paused;
|
const newPausedState = !paused;
|
||||||
if (useVLC && vlcRef.current) {
|
setPaused(newPausedState);
|
||||||
try {
|
|
||||||
if (newPausedState) {
|
|
||||||
if (typeof vlcRef.current.pause === 'function') vlcRef.current.pause();
|
|
||||||
} else {
|
|
||||||
if (typeof vlcRef.current.play === 'function') vlcRef.current.play();
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
setPaused(newPausedState);
|
|
||||||
} else if (videoRef.current) {
|
|
||||||
setPaused(newPausedState);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (duration > 0) {
|
if (duration > 0) {
|
||||||
traktAutosync.handleProgressUpdate(currentTime, duration, true);
|
traktAutosync.handleProgressUpdate(currentTime, duration, true);
|
||||||
}
|
}
|
||||||
}, [paused, useVLC, currentTime, duration, traktAutosync]);
|
}, [paused, currentTime, duration, traktAutosync]);
|
||||||
|
|
||||||
// Handle next episode button press
|
// Handle next episode button press
|
||||||
const handlePlayNextEpisode = useCallback(async () => {
|
const handlePlayNextEpisode = useCallback(async () => {
|
||||||
|
|
@ -3036,6 +2972,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
setDuration(0);
|
setDuration(0);
|
||||||
setIsPlayerReady(false);
|
setIsPlayerReady(false);
|
||||||
setIsVideoLoaded(false);
|
setIsVideoLoaded(false);
|
||||||
|
vlcLoadedRef.current = false;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[AndroidVideoPlayer] Error changing source:', error);
|
logger.error('[AndroidVideoPlayer] Error changing source:', error);
|
||||||
|
|
@ -3308,94 +3245,41 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
delayLongPress={300}
|
delayLongPress={300}
|
||||||
>
|
>
|
||||||
{useVLC && !forceVlcRemount ? (
|
{useVLC && !forceVlcRemount ? (
|
||||||
<>
|
<VlcVideoPlayer
|
||||||
{LibVlcPlayerViewComponent ? (
|
ref={vlcPlayerRef}
|
||||||
<LibVlcPlayerViewComponent
|
|
||||||
ref={vlcRef}
|
|
||||||
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
|
|
||||||
// Force remount when surfaces are recreated
|
|
||||||
key={vlcKey}
|
|
||||||
source={processedStreamUrl}
|
source={processedStreamUrl}
|
||||||
aspectRatio={vlcAspectRatio}
|
volume={volume}
|
||||||
options={vlcOptions}
|
zoomScale={zoomScale}
|
||||||
tracks={vlcTracks}
|
resizeMode={resizeMode}
|
||||||
volume={Math.round(Math.max(0, Math.min(1, volume)) * 100)}
|
onLoad={(data) => {
|
||||||
mute={false}
|
vlcLoadedRef.current = true;
|
||||||
repeat={false}
|
onLoad(data);
|
||||||
rate={1}
|
// Start playback if not paused
|
||||||
autoplay={!paused}
|
if (!paused && vlcPlayerRef.current) {
|
||||||
onFirstPlay={(info: any) => {
|
setTimeout(() => {
|
||||||
try {
|
if (vlcPlayerRef.current) {
|
||||||
if (DEBUG_MODE) {
|
vlcPlayerRef.current.play();
|
||||||
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, 'onFirstPlay');
|
|
||||||
}
|
|
||||||
|
|
||||||
const lenSec = (info?.length ?? 0) / 1000;
|
|
||||||
const width = info?.width || 0;
|
|
||||||
const height = info?.height || 0;
|
|
||||||
onLoad({ duration: lenSec, naturalSize: width && height ? { width, height } : undefined });
|
|
||||||
|
|
||||||
// Restore playback position after remount (workaround for surface detach)
|
|
||||||
if (vlcRestoreTime !== undefined && vlcRestoreTime > 0) {
|
|
||||||
if (DEBUG_MODE) {
|
|
||||||
logger.log('[VLC] Restoring playback position:', vlcRestoreTime);
|
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
}, 100);
|
||||||
if (vlcRef.current && typeof vlcRef.current.seek === 'function') {
|
|
||||||
const seekPosition = Math.min(vlcRestoreTime / lenSec, 0.999); // Convert to fraction
|
|
||||||
vlcRef.current.seek(seekPosition);
|
|
||||||
if (DEBUG_MODE) {
|
|
||||||
logger.log('[VLC] Seeked to restore position');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 500); // Small delay to ensure player is ready
|
|
||||||
setVlcRestoreTime(undefined); // Clear restore time
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('[VLC] onFirstPlay error:', e);
|
|
||||||
logger.warn('[AndroidVideoPlayer][VLC] onFirstPlay parse error', e);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPositionChanged={(ev: any) => {
|
onProgress={(data) => {
|
||||||
const pos = typeof ev?.position === 'number' ? ev.position : 0;
|
const pos = typeof data?.position === 'number' ? data.position : 0;
|
||||||
if (duration > 0) {
|
if (duration > 0) {
|
||||||
const current = pos * duration;
|
const current = pos * duration;
|
||||||
handleProgress({ currentTime: current, playableDuration: current });
|
handleProgress({ currentTime: current, playableDuration: current });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPlaying={() => setPaused(false)}
|
onSeek={onSeek}
|
||||||
onPaused={() => setPaused(true)}
|
onEnd={onEnd}
|
||||||
onEndReached={onEnd}
|
onError={handleError}
|
||||||
onEncounteredError={(e: any) => {
|
onTracksUpdate={handleVlcTracksUpdate}
|
||||||
logger.error('[AndroidVideoPlayer][VLC] Encountered error:', e);
|
selectedAudioTrack={vlcSelectedAudioTrack}
|
||||||
handleError(e);
|
selectedSubtitleTrack={vlcSelectedSubtitleTrack}
|
||||||
}}
|
restoreTime={vlcRestoreTime}
|
||||||
onBackground={() => {
|
forceRemount={forceVlcRemount}
|
||||||
logger.log('[VLC] App went to background');
|
key={vlcKey}
|
||||||
}}
|
/>
|
||||||
onESAdded={(tracks: any) => {
|
|
||||||
try {
|
|
||||||
if (DEBUG_MODE) {
|
|
||||||
logger.log('[VLC] ES Added - processing tracks...');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process VLC tracks using optimized function
|
|
||||||
if (tracks) {
|
|
||||||
processVlcTracks(tracks, 'onESAdded');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('[VLC] onESAdded error:', e);
|
|
||||||
logger.warn('[AndroidVideoPlayer][VLC] onESAdded parse error', e);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>) : null}
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<Video
|
<Video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
|
|
@ -4096,7 +3980,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
<AudioTrackModal
|
<AudioTrackModal
|
||||||
showAudioModal={showAudioModal}
|
showAudioModal={showAudioModal}
|
||||||
setShowAudioModal={setShowAudioModal}
|
setShowAudioModal={setShowAudioModal}
|
||||||
ksAudioTracks={useVLC ? vlcAudioTracksForModal : rnVideoAudioTracks}
|
ksAudioTracks={useVLC ? vlcAudioTracks : rnVideoAudioTracks}
|
||||||
selectedAudioTrack={useVLC ? (vlcSelectedAudioTrack ?? null) : (selectedAudioTrack?.type === SelectedTrackType.INDEX && selectedAudioTrack.value !== undefined ? Number(selectedAudioTrack.value) : null)}
|
selectedAudioTrack={useVLC ? (vlcSelectedAudioTrack ?? null) : (selectedAudioTrack?.type === SelectedTrackType.INDEX && selectedAudioTrack.value !== undefined ? Number(selectedAudioTrack.value) : null)}
|
||||||
selectAudioTrack={selectAudioTrackById}
|
selectAudioTrack={selectAudioTrackById}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
342
src/components/player/VlcVideoPlayer.tsx
Normal file
342
src/components/player/VlcVideoPlayer.tsx
Normal file
|
|
@ -0,0 +1,342 @@
|
||||||
|
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;
|
||||||
|
zoomScale: number;
|
||||||
|
resizeMode: 'contain' | 'cover' | 'fill' | 'stretch' | '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,
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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, we handle aspect ratio through custom zoom for cover mode
|
||||||
|
// Only force aspect for fill mode (stretch to fit)
|
||||||
|
if (resizeMode === 'fill') {
|
||||||
|
const sw = screenDimensions.width || 0;
|
||||||
|
const sh = screenDimensions.height || 0;
|
||||||
|
if (sw > 0 && sh > 0) {
|
||||||
|
return toVlcRatio(sw, sh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For cover/contain/none: let VLC preserve natural aspect, we handle zoom separately
|
||||||
|
return undefined;
|
||||||
|
}, [resizeMode, screenDimensions.width, screenDimensions.height, toVlcRatio]);
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<LibVlcPlayerViewComponent
|
||||||
|
ref={vlcRef}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: screenDimensions.width,
|
||||||
|
height: screenDimensions.height,
|
||||||
|
transform: [{ scale: zoomScale }]
|
||||||
|
}}
|
||||||
|
// Force remount when surfaces are recreated
|
||||||
|
key={key || 'vlc-default'}
|
||||||
|
source={processedSource}
|
||||||
|
aspectRatio={vlcAspectRatio}
|
||||||
|
options={vlcOptions}
|
||||||
|
tracks={vlcTracks}
|
||||||
|
volume={Math.round(Math.max(0, Math.min(1, volume)) * 100)}
|
||||||
|
mute={false}
|
||||||
|
repeat={false}
|
||||||
|
rate={1}
|
||||||
|
autoplay={false}
|
||||||
|
onFirstPlay={handleFirstPlay}
|
||||||
|
onPositionChanged={handlePositionChanged}
|
||||||
|
onPlaying={handlePlaying}
|
||||||
|
onPaused={handlePaused}
|
||||||
|
onEndReached={handleEndReached}
|
||||||
|
onEncounteredError={handleEncounteredError}
|
||||||
|
onBackground={handleBackground}
|
||||||
|
onESAdded={handleESAdded}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
VlcVideoPlayer.displayName = 'VlcVideoPlayer';
|
||||||
|
|
||||||
|
export default VlcVideoPlayer;
|
||||||
Loading…
Reference in a new issue