mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +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 CustomSubtitles from './subtitles/CustomSubtitles';
|
||||
import { SourcesModal } from './modals/SourcesModal';
|
||||
import VlcVideoPlayer, { VlcPlayerRef } from './VlcVideoPlayer';
|
||||
import { stremioService } from '../../services/stremioService';
|
||||
import { shouldUseKSPlayer } from '../../utils/playerSelection';
|
||||
import axios from 'axios';
|
||||
|
|
@ -96,17 +97,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
logger.log(`[AndroidVideoPlayer] Player selection: ${playerType} (${reason})`);
|
||||
}, [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)
|
||||
|
|
@ -222,14 +212,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const [rnVideoAudioTracks, setRnVideoAudioTracks] = 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
|
||||
const trackUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
|
|
@ -239,39 +221,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
// Debounce gesture operations to prevent rapid-fire events
|
||||
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
|
||||
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
|
||||
if (DEBUG_MODE) {
|
||||
logger.log(`[VLC] ${source} - Raw tracks data:`, tracks);
|
||||
// VLC track state - will be managed by VlcVideoPlayer component
|
||||
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
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}, [useVLC, paused]);
|
||||
|
||||
// Memoized computed props for child components
|
||||
const ksAudioTracks = useMemo(() =>
|
||||
useVLC ? vlcAudioTracksForModal : rnVideoAudioTracks,
|
||||
[useVLC, vlcAudioTracksForModal, rnVideoAudioTracks]
|
||||
useVLC ? vlcAudioTracks : rnVideoAudioTracks,
|
||||
[useVLC, vlcAudioTracks, rnVideoAudioTracks]
|
||||
);
|
||||
|
||||
const computedSelectedAudioTrack = useMemo(() =>
|
||||
|
|
@ -389,8 +296,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
);
|
||||
|
||||
const ksTextTracks = useMemo(() =>
|
||||
useVLC ? vlcSubtitleTracksForModal : rnVideoTextTracks,
|
||||
[useVLC, vlcSubtitleTracksForModal, rnVideoTextTracks]
|
||||
useVLC ? vlcSubtitleTracks : rnVideoTextTracks,
|
||||
[useVLC, vlcSubtitleTracks, rnVideoTextTracks]
|
||||
);
|
||||
|
||||
const computedSelectedTextTrack = useMemo(() =>
|
||||
|
|
@ -535,46 +442,77 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const vlcFallbackAttemptedRef = useRef(false);
|
||||
|
||||
// VLC refs/state
|
||||
const vlcRef = useRef<any>(null);
|
||||
const [vlcActive, setVlcActive] = useState(true); // Start as active
|
||||
// VLC key for forcing remounts
|
||||
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
|
||||
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)}`;
|
||||
}, []);
|
||||
// Handler for VLC track updates
|
||||
const handleVlcTracksUpdate = useCallback((tracks: { audio: any[], subtitle: any[] }) => {
|
||||
if (!tracks) return;
|
||||
|
||||
const vlcAspectRatio = useMemo(() => {
|
||||
if (!useVLC) return undefined as string | undefined;
|
||||
// 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);
|
||||
}
|
||||
// Clear any pending updates
|
||||
if (trackUpdateTimeoutRef.current) {
|
||||
clearTimeout(trackUpdateTimeoutRef.current);
|
||||
}
|
||||
// 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
|
||||
const vlcOptions = useMemo(() => {
|
||||
if (!useVLC) return [] as string[];
|
||||
// Basic options for network streaming
|
||||
return [
|
||||
'--network-caching=2000',
|
||||
'--clock-jitter=0',
|
||||
'--http-reconnect',
|
||||
'--sout-mux-caching=2000'
|
||||
];
|
||||
}, [useVLC]);
|
||||
// Debounce track updates to prevent excessive processing
|
||||
trackUpdateTimeoutRef.current = setTimeout(() => {
|
||||
const { audio = [], subtitle = [] } = tracks;
|
||||
let hasUpdates = false;
|
||||
|
||||
// Process audio tracks
|
||||
if (Array.isArray(audio) && audio.length > 0) {
|
||||
const formattedAudio = audio.map(track => ({
|
||||
id: track.id,
|
||||
name: track.name || `Track ${track.id + 1}`,
|
||||
language: track.language
|
||||
}));
|
||||
|
||||
// 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
|
||||
|
|
@ -934,6 +872,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
logger.log('[VLC] Forcing complete remount due to focus gain');
|
||||
setVlcRestoreTime(currentTime); // Save current time for restoration
|
||||
setForceVlcRemount(true);
|
||||
vlcLoadedRef.current = false; // Reset loaded state
|
||||
// Re-enable after a brief moment
|
||||
setTimeout(() => {
|
||||
setForceVlcRemount(false);
|
||||
|
|
@ -954,6 +893,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
logger.log('[VLC] Forcing complete remount due to app foreground');
|
||||
setVlcRestoreTime(currentTime); // Save current time for restoration
|
||||
setForceVlcRemount(true);
|
||||
vlcLoadedRef.current = false; // Reset loaded state
|
||||
// Re-enable after a brief moment
|
||||
setTimeout(() => {
|
||||
setForceVlcRemount(false);
|
||||
|
|
@ -1166,42 +1106,49 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const seekToTime = (rawSeconds: number) => {
|
||||
// Clamp to just before the end of the media.
|
||||
const timeInSeconds = Math.max(0, Math.min(rawSeconds, duration > 0 ? duration - END_EPSILON : rawSeconds));
|
||||
if (useVLC && duration > 0) {
|
||||
try {
|
||||
const fraction = Math.min(Math.max(timeInSeconds / duration, 0), 0.999);
|
||||
if (vlcRef.current && typeof vlcRef.current.seek === 'function') {
|
||||
vlcRef.current.seek(fraction);
|
||||
return;
|
||||
|
||||
if (useVLC) {
|
||||
// Use VLC imperative method
|
||||
if (vlcPlayerRef.current && duration > 0) {
|
||||
if (DEBUG_MODE) {
|
||||
if (__DEV__) logger.log(`[AndroidVideoPlayer][VLC] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
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;
|
||||
}
|
||||
vlcPlayerRef.current.seek(timeInSeconds);
|
||||
} else {
|
||||
if (DEBUG_MODE) {
|
||||
logger.error(`[AndroidVideoPlayer][VLC] Seek failed: vlcRef=${!!vlcPlayerRef.current}, duration=${duration}`);
|
||||
}
|
||||
}, 1200);
|
||||
}
|
||||
} else {
|
||||
if (DEBUG_MODE) {
|
||||
logger.error(`[AndroidVideoPlayer] Seek failed: videoRef=${!!videoRef.current}, duration=${duration}, seeking=${isSeeking.current}`);
|
||||
// Use react-native-video method
|
||||
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');
|
||||
|
||||
// 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;
|
||||
logger.warn('[AndroidVideoPlayer] Codec error detected, silently switching to VLC');
|
||||
// Clear any existing timeout
|
||||
|
|
@ -2472,23 +2419,12 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
|
||||
const togglePlayback = useCallback(() => {
|
||||
const newPausedState = !paused;
|
||||
if (useVLC && vlcRef.current) {
|
||||
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);
|
||||
}
|
||||
setPaused(newPausedState);
|
||||
|
||||
if (duration > 0) {
|
||||
traktAutosync.handleProgressUpdate(currentTime, duration, true);
|
||||
}
|
||||
}, [paused, useVLC, currentTime, duration, traktAutosync]);
|
||||
}, [paused, currentTime, duration, traktAutosync]);
|
||||
|
||||
// Handle next episode button press
|
||||
const handlePlayNextEpisode = useCallback(async () => {
|
||||
|
|
@ -3036,6 +2972,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
setDuration(0);
|
||||
setIsPlayerReady(false);
|
||||
setIsVideoLoaded(false);
|
||||
vlcLoadedRef.current = false;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[AndroidVideoPlayer] Error changing source:', error);
|
||||
|
|
@ -3308,94 +3245,41 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
delayLongPress={300}
|
||||
>
|
||||
{useVLC && !forceVlcRemount ? (
|
||||
<>
|
||||
{LibVlcPlayerViewComponent ? (
|
||||
<LibVlcPlayerViewComponent
|
||||
ref={vlcRef}
|
||||
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
|
||||
// Force remount when surfaces are recreated
|
||||
key={vlcKey}
|
||||
<VlcVideoPlayer
|
||||
ref={vlcPlayerRef}
|
||||
source={processedStreamUrl}
|
||||
aspectRatio={vlcAspectRatio}
|
||||
options={vlcOptions}
|
||||
tracks={vlcTracks}
|
||||
volume={Math.round(Math.max(0, Math.min(1, volume)) * 100)}
|
||||
mute={false}
|
||||
repeat={false}
|
||||
rate={1}
|
||||
autoplay={!paused}
|
||||
onFirstPlay={(info: any) => {
|
||||
try {
|
||||
if (DEBUG_MODE) {
|
||||
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);
|
||||
volume={volume}
|
||||
zoomScale={zoomScale}
|
||||
resizeMode={resizeMode}
|
||||
onLoad={(data) => {
|
||||
vlcLoadedRef.current = true;
|
||||
onLoad(data);
|
||||
// Start playback if not paused
|
||||
if (!paused && vlcPlayerRef.current) {
|
||||
setTimeout(() => {
|
||||
if (vlcPlayerRef.current) {
|
||||
vlcPlayerRef.current.play();
|
||||
}
|
||||
setTimeout(() => {
|
||||
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);
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
onPositionChanged={(ev: any) => {
|
||||
const pos = typeof ev?.position === 'number' ? ev.position : 0;
|
||||
onProgress={(data) => {
|
||||
const pos = typeof data?.position === 'number' ? data.position : 0;
|
||||
if (duration > 0) {
|
||||
const current = pos * duration;
|
||||
handleProgress({ currentTime: current, playableDuration: current });
|
||||
}
|
||||
}}
|
||||
onPlaying={() => setPaused(false)}
|
||||
onPaused={() => setPaused(true)}
|
||||
onEndReached={onEnd}
|
||||
onEncounteredError={(e: any) => {
|
||||
logger.error('[AndroidVideoPlayer][VLC] Encountered error:', e);
|
||||
handleError(e);
|
||||
}}
|
||||
onBackground={() => {
|
||||
logger.log('[VLC] App went to background');
|
||||
}}
|
||||
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}
|
||||
</>
|
||||
onSeek={onSeek}
|
||||
onEnd={onEnd}
|
||||
onError={handleError}
|
||||
onTracksUpdate={handleVlcTracksUpdate}
|
||||
selectedAudioTrack={vlcSelectedAudioTrack}
|
||||
selectedSubtitleTrack={vlcSelectedSubtitleTrack}
|
||||
restoreTime={vlcRestoreTime}
|
||||
forceRemount={forceVlcRemount}
|
||||
key={vlcKey}
|
||||
/>
|
||||
) : (
|
||||
<Video
|
||||
ref={videoRef}
|
||||
|
|
@ -4096,7 +3980,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
<AudioTrackModal
|
||||
showAudioModal={showAudioModal}
|
||||
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)}
|
||||
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