mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
removed vlc lib
This commit is contained in:
parent
f0f71afd67
commit
767fd2ff87
15 changed files with 124 additions and 715 deletions
|
|
@ -51,7 +51,6 @@
|
||||||
"expo-glass-effect": "~0.1.4",
|
"expo-glass-effect": "~0.1.4",
|
||||||
"expo-haptics": "~15.0.7",
|
"expo-haptics": "~15.0.7",
|
||||||
"expo-intent-launcher": "~13.0.7",
|
"expo-intent-launcher": "~13.0.7",
|
||||||
"expo-libvlc-player": "^2.2.3",
|
|
||||||
"expo-linear-gradient": "~15.0.7",
|
"expo-linear-gradient": "~15.0.7",
|
||||||
"expo-localization": "~17.0.7",
|
"expo-localization": "~17.0.7",
|
||||||
"expo-navigation-bar": "~5.0.10",
|
"expo-navigation-bar": "~5.0.10",
|
||||||
|
|
@ -104,4 +103,4 @@
|
||||||
"xcode": "^3.0.1"
|
"xcode": "^3.0.1"
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
|
@ -649,7 +649,6 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
||||||
streamProvider: cachedStream.stream.addonId || cachedStream.stream.addonName || cachedStream.stream.name,
|
streamProvider: cachedStream.stream.addonId || cachedStream.stream.addonName || cachedStream.stream.name,
|
||||||
streamName: cachedStream.stream.name || cachedStream.stream.title || 'Unnamed Stream',
|
streamName: cachedStream.stream.name || cachedStream.stream.title || 'Unnamed Stream',
|
||||||
headers: cachedStream.stream.headers || undefined,
|
headers: cachedStream.stream.headers || undefined,
|
||||||
forceVlc: false,
|
|
||||||
id: currentItem.id,
|
id: currentItem.id,
|
||||||
type: currentItem.type,
|
type: currentItem.type,
|
||||||
episodeId: episodeId,
|
episodeId: episodeId,
|
||||||
|
|
|
||||||
|
|
@ -977,7 +977,6 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
streamProvider: cachedStream.stream.addonId || cachedStream.stream.addonName || cachedStream.stream.name,
|
streamProvider: cachedStream.stream.addonId || cachedStream.stream.addonName || cachedStream.stream.name,
|
||||||
streamName: cachedStream.stream.name || cachedStream.stream.title || 'Unnamed Stream',
|
streamName: cachedStream.stream.name || cachedStream.stream.title || 'Unnamed Stream',
|
||||||
headers: cachedStream.stream.headers || undefined,
|
headers: cachedStream.stream.headers || undefined,
|
||||||
forceVlc: false,
|
|
||||||
id: item.id,
|
id: item.id,
|
||||||
type: item.type,
|
type: item.type,
|
||||||
episodeId: episodeId,
|
episodeId: episodeId,
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,8 @@ import {
|
||||||
useOpeningAnimation
|
useOpeningAnimation
|
||||||
} from './hooks';
|
} from './hooks';
|
||||||
|
|
||||||
// Android-specific hooks (VLC integration, dual player support)
|
// Android-specific hooks
|
||||||
import { usePlayerSetup } from './android/hooks/usePlayerSetup';
|
import { usePlayerSetup } from './android/hooks/usePlayerSetup';
|
||||||
import { useVlcPlayer } from './android/hooks/useVlcPlayer';
|
|
||||||
import { usePlayerTracks } from './android/hooks/usePlayerTracks';
|
import { usePlayerTracks } from './android/hooks/usePlayerTracks';
|
||||||
import { useWatchProgress } from './android/hooks/useWatchProgress';
|
import { useWatchProgress } from './android/hooks/useWatchProgress';
|
||||||
import { usePlayerControls } from './android/hooks/usePlayerControls';
|
import { usePlayerControls } from './android/hooks/usePlayerControls';
|
||||||
|
|
@ -46,7 +45,7 @@ import { MpvPlayerRef } from './android/MpvPlayer';
|
||||||
// Utils
|
// Utils
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import { styles } from './utils/playerStyles';
|
import { styles } from './utils/playerStyles';
|
||||||
import { formatTime, isHlsStream, processUrlForVLC, getHlsHeaders, defaultAndroidHeaders, parseSRT } from './utils/playerUtils';
|
import { formatTime, isHlsStream, getHlsHeaders, defaultAndroidHeaders, parseSRT } from './utils/playerUtils';
|
||||||
import { storageService } from '../../services/storageService';
|
import { storageService } from '../../services/storageService';
|
||||||
import stremioService from '../../services/stremioService';
|
import stremioService from '../../services/stremioService';
|
||||||
import { WyzieSubtitle, SubtitleCue } from './utils/playerTypes';
|
import { WyzieSubtitle, SubtitleCue } from './utils/playerTypes';
|
||||||
|
|
@ -71,28 +70,12 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const modals = usePlayerModals();
|
const modals = usePlayerModals();
|
||||||
const speedControl = useSpeedControl();
|
const speedControl = useSpeedControl();
|
||||||
|
|
||||||
const forceVlc = useMemo(() => {
|
|
||||||
const rp: any = route.params || {};
|
|
||||||
const v = rp.forceVlc !== undefined ? rp.forceVlc : rp.forceVLC;
|
|
||||||
return typeof v === 'string' ? v.toLowerCase() === 'true' : Boolean(v);
|
|
||||||
}, [route.params]);
|
|
||||||
|
|
||||||
const useVLC = (Platform.OS === 'android' && forceVlc);
|
|
||||||
|
|
||||||
const videoRef = useRef<any>(null);
|
const videoRef = useRef<any>(null);
|
||||||
const mpvPlayerRef = useRef<MpvPlayerRef>(null);
|
const mpvPlayerRef = useRef<MpvPlayerRef>(null);
|
||||||
const vlcHook = useVlcPlayer(useVLC, playerState.paused, playerState.currentTime);
|
const tracksHook = usePlayerTracks();
|
||||||
const tracksHook = usePlayerTracks(
|
|
||||||
useVLC,
|
|
||||||
vlcHook.vlcAudioTracks,
|
|
||||||
vlcHook.vlcSubtitleTracks,
|
|
||||||
vlcHook.vlcSelectedAudioTrack,
|
|
||||||
vlcHook.vlcSelectedSubtitleTrack
|
|
||||||
);
|
|
||||||
|
|
||||||
const [currentStreamUrl, setCurrentStreamUrl] = useState<string>(uri);
|
const [currentStreamUrl, setCurrentStreamUrl] = useState<string>(uri);
|
||||||
const [currentVideoType, setCurrentVideoType] = useState<string | undefined>((route.params as any).videoType);
|
const [currentVideoType, setCurrentVideoType] = useState<string | undefined>((route.params as any).videoType);
|
||||||
const processedStreamUrl = useMemo(() => useVLC ? processUrlForVLC(currentStreamUrl) : currentStreamUrl, [currentStreamUrl, useVLC]);
|
|
||||||
|
|
||||||
const [availableStreams, setAvailableStreams] = useState<any>(passedAvailableStreams || {});
|
const [availableStreams, setAvailableStreams] = useState<any>(passedAvailableStreams || {});
|
||||||
const [currentQuality, setCurrentQuality] = useState(quality);
|
const [currentQuality, setCurrentQuality] = useState(quality);
|
||||||
|
|
@ -132,9 +115,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const setupHook = usePlayerSetup(playerState.setScreenDimensions, setVolume, setBrightness, playerState.paused);
|
const setupHook = usePlayerSetup(playerState.setScreenDimensions, setVolume, setBrightness, playerState.paused);
|
||||||
|
|
||||||
const controlsHook = usePlayerControls(
|
const controlsHook = usePlayerControls(
|
||||||
mpvPlayerRef, // Use mpvPlayerRef for MPV player
|
mpvPlayerRef,
|
||||||
vlcHook.vlcPlayerRef,
|
|
||||||
useVLC,
|
|
||||||
playerState.paused,
|
playerState.paused,
|
||||||
playerState.setPaused,
|
playerState.setPaused,
|
||||||
playerState.currentTime,
|
playerState.currentTime,
|
||||||
|
|
@ -265,23 +246,21 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
playerState.setVideoAspectRatio(16 / 9);
|
playerState.setVideoAspectRatio(16 / 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!useVLC) {
|
if (data.audioTracks) {
|
||||||
if (data.audioTracks) {
|
const formatted = data.audioTracks.map((t: any, i: number) => ({
|
||||||
const formatted = data.audioTracks.map((t: any, i: number) => ({
|
id: t.index !== undefined ? t.index : i,
|
||||||
id: t.index !== undefined ? t.index : i,
|
name: t.title || t.name || `Track ${i + 1}`,
|
||||||
name: t.title || t.name || `Track ${i + 1}`,
|
language: t.language
|
||||||
language: t.language
|
}));
|
||||||
}));
|
tracksHook.setRnVideoAudioTracks(formatted);
|
||||||
tracksHook.setRnVideoAudioTracks(formatted);
|
}
|
||||||
}
|
if (data.textTracks) {
|
||||||
if (data.textTracks) {
|
const formatted = data.textTracks.map((t: any, i: number) => ({
|
||||||
const formatted = data.textTracks.map((t: any, i: number) => ({
|
id: t.index !== undefined ? t.index : i,
|
||||||
id: t.index !== undefined ? t.index : i,
|
name: t.title || t.name || `Track ${i + 1}`,
|
||||||
name: t.title || t.name || `Track ${i + 1}`,
|
language: t.language
|
||||||
language: t.language
|
}));
|
||||||
}));
|
tracksHook.setRnVideoTextTracks(formatted);
|
||||||
tracksHook.setRnVideoTextTracks(formatted);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playerState.setIsVideoLoaded(true);
|
playerState.setIsVideoLoaded(true);
|
||||||
|
|
@ -299,7 +278,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
}, [id, type, episodeId, useVLC, playerState.isMounted, watchProgress.initialPosition]);
|
}, [id, type, episodeId, playerState.isMounted, watchProgress.initialPosition]);
|
||||||
|
|
||||||
const handleProgress = useCallback((data: any) => {
|
const handleProgress = useCallback((data: any) => {
|
||||||
if (playerState.isDragging.current || playerState.isSeeking.current || !playerState.isMounted.current || setupHook.isAppBackgrounded.current) return;
|
if (playerState.isDragging.current || playerState.isSeeking.current || !playerState.isMounted.current || setupHook.isAppBackgrounded.current) return;
|
||||||
|
|
@ -385,7 +364,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
streamProvider: newProvider,
|
streamProvider: newProvider,
|
||||||
streamName: newStreamName,
|
streamName: newStreamName,
|
||||||
headers: stream.headers || undefined,
|
headers: stream.headers || undefined,
|
||||||
forceVlc: false,
|
|
||||||
id,
|
id,
|
||||||
type: 'series',
|
type: 'series',
|
||||||
episodeId: ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`,
|
episodeId: ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`,
|
||||||
|
|
@ -508,24 +486,12 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
|
|
||||||
<View style={{ flex: 1, backgroundColor: 'black' }}>
|
<View style={{ flex: 1, backgroundColor: 'black' }}>
|
||||||
<VideoSurface
|
<VideoSurface
|
||||||
useVLC={!!useVLC}
|
processedStreamUrl={currentStreamUrl}
|
||||||
forceVlcRemount={vlcHook.forceVlcRemount}
|
|
||||||
processedStreamUrl={processedStreamUrl}
|
|
||||||
volume={volume}
|
volume={volume}
|
||||||
playbackSpeed={speedControl.playbackSpeed}
|
playbackSpeed={speedControl.playbackSpeed}
|
||||||
zoomScale={1.0}
|
|
||||||
resizeMode={playerState.resizeMode}
|
resizeMode={playerState.resizeMode}
|
||||||
paused={playerState.paused}
|
paused={playerState.paused}
|
||||||
currentStreamUrl={currentStreamUrl}
|
currentStreamUrl={currentStreamUrl}
|
||||||
headers={headers || (isHlsStream(currentStreamUrl) ? getHlsHeaders() : defaultAndroidHeaders)}
|
|
||||||
videoType={currentVideoType}
|
|
||||||
vlcSelectedAudioTrack={vlcHook.vlcSelectedAudioTrack}
|
|
||||||
vlcSelectedSubtitleTrack={vlcHook.vlcSelectedSubtitleTrack}
|
|
||||||
vlcRestoreTime={vlcHook.vlcRestoreTime}
|
|
||||||
vlcKey={vlcHook.vlcKey}
|
|
||||||
selectedAudioTrack={tracksHook.selectedAudioTrack}
|
|
||||||
selectedTextTrack={tracksHook.selectedTextTrack}
|
|
||||||
useCustomSubtitles={false}
|
|
||||||
toggleControls={toggleControls}
|
toggleControls={toggleControls}
|
||||||
onLoad={handleLoad}
|
onLoad={handleLoad}
|
||||||
onProgress={handleProgress}
|
onProgress={handleProgress}
|
||||||
|
|
@ -540,32 +506,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
onError={(err: any) => {
|
onError={(err: any) => {
|
||||||
logger.error('Video Error', err);
|
logger.error('Video Error', err);
|
||||||
|
|
||||||
// Check for decoding errors to switch to VLC
|
|
||||||
const errorString = err?.errorString || err?.error?.errorString;
|
|
||||||
const errorCode = err?.errorCode || err?.error?.errorCode;
|
|
||||||
const causeMessage = err?.error?.cause?.message;
|
|
||||||
|
|
||||||
const isDecodingError =
|
|
||||||
(errorString && errorString.includes('ERROR_CODE_DECODING_FAILED')) ||
|
|
||||||
errorCode === '24003' ||
|
|
||||||
(causeMessage && causeMessage.includes('MediaCodecVideoRenderer error'));
|
|
||||||
|
|
||||||
if (!useVLC && isDecodingError) {
|
|
||||||
const toastId = toast.loading('Decoding error. Switching to VLC Player...');
|
|
||||||
setTimeout(() => toast.dismiss(toastId), 3000);
|
|
||||||
|
|
||||||
// We can just show a normal toast or use the existing modal system if we want,
|
|
||||||
// but checking the file imports, I don't see Toast imported.
|
|
||||||
// Let's implement the navigation replace.
|
|
||||||
|
|
||||||
// Using a simple navigation replace to force VLC
|
|
||||||
(navigation as any).replace('PlayerAndroid', {
|
|
||||||
...route.params,
|
|
||||||
forceVlc: true
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the actual error message
|
// Determine the actual error message
|
||||||
let displayError = 'An unknown error occurred';
|
let displayError = 'An unknown error occurred';
|
||||||
|
|
||||||
|
|
@ -585,7 +525,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
modals.setShowErrorModal(true);
|
modals.setShowErrorModal(true);
|
||||||
}}
|
}}
|
||||||
onBuffer={(buf) => playerState.setIsBuffering(buf.isBuffering)}
|
onBuffer={(buf) => playerState.setIsBuffering(buf.isBuffering)}
|
||||||
onTracksUpdate={vlcHook.handleVlcTracksUpdate}
|
|
||||||
onTracksChanged={(data) => {
|
onTracksChanged={(data) => {
|
||||||
console.log('[AndroidVideoPlayer] onTracksChanged:', data);
|
console.log('[AndroidVideoPlayer] onTracksChanged:', data);
|
||||||
if (data?.audioTracks) {
|
if (data?.audioTracks) {
|
||||||
|
|
@ -605,17 +544,11 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
tracksHook.setRnVideoTextTracks(formatted);
|
tracksHook.setRnVideoTextTracks(formatted);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
vlcPlayerRef={vlcHook.vlcPlayerRef}
|
|
||||||
mpvPlayerRef={mpvPlayerRef}
|
mpvPlayerRef={mpvPlayerRef}
|
||||||
videoRef={videoRef}
|
|
||||||
pinchRef={useRef(null)}
|
pinchRef={useRef(null)}
|
||||||
onPinchGestureEvent={() => { }}
|
onPinchGestureEvent={() => { }}
|
||||||
onPinchHandlerStateChange={() => { }}
|
onPinchHandlerStateChange={() => { }}
|
||||||
vlcLoadedRef={vlcHook.vlcLoadedRef}
|
|
||||||
screenDimensions={playerState.screenDimensions}
|
screenDimensions={playerState.screenDimensions}
|
||||||
customVideoStyles={{}}
|
|
||||||
loadStartAtRef={loadStartAtRef}
|
|
||||||
firstFrameAtRef={firstFrameAtRef}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Custom Subtitles for addon subtitles */}
|
{/* Custom Subtitles for addon subtitles */}
|
||||||
|
|
@ -698,7 +631,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
buffered={playerState.buffered}
|
buffered={playerState.buffered}
|
||||||
formatTime={formatTime}
|
formatTime={formatTime}
|
||||||
playerBackend={useVLC ? 'VLC' : 'ExoPlayer'}
|
playerBackend={'MPV'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SpeedActivatedOverlay
|
<SpeedActivatedOverlay
|
||||||
|
|
@ -728,14 +661,10 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
ksAudioTracks={tracksHook.ksAudioTracks}
|
ksAudioTracks={tracksHook.ksAudioTracks}
|
||||||
selectedAudioTrack={tracksHook.computedSelectedAudioTrack}
|
selectedAudioTrack={tracksHook.computedSelectedAudioTrack}
|
||||||
selectAudioTrack={(trackId) => {
|
selectAudioTrack={(trackId) => {
|
||||||
if (useVLC) {
|
tracksHook.setSelectedAudioTrack(trackId === null ? null : { type: 'index', value: trackId });
|
||||||
vlcHook.selectVlcAudioTrack(trackId);
|
// Actually tell MPV to switch the audio track
|
||||||
} else {
|
if (trackId !== null && mpvPlayerRef.current) {
|
||||||
tracksHook.setSelectedAudioTrack(trackId === null ? null : { type: 'index', value: trackId });
|
mpvPlayerRef.current.setAudioTrack(trackId);
|
||||||
// Actually tell MPV to switch the audio track
|
|
||||||
if (trackId !== null && mpvPlayerRef.current) {
|
|
||||||
mpvPlayerRef.current.setAudioTrack(trackId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -752,20 +681,16 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
ksTextTracks={tracksHook.ksTextTracks}
|
ksTextTracks={tracksHook.ksTextTracks}
|
||||||
selectedTextTrack={tracksHook.computedSelectedTextTrack}
|
selectedTextTrack={tracksHook.computedSelectedTextTrack}
|
||||||
useCustomSubtitles={useCustomSubtitles}
|
useCustomSubtitles={useCustomSubtitles}
|
||||||
isKsPlayerActive={!useVLC}
|
isKsPlayerActive={true}
|
||||||
subtitleSize={subtitleSize}
|
subtitleSize={subtitleSize}
|
||||||
subtitleBackground={subtitleBackground}
|
subtitleBackground={subtitleBackground}
|
||||||
fetchAvailableSubtitles={fetchAvailableSubtitles}
|
fetchAvailableSubtitles={fetchAvailableSubtitles}
|
||||||
loadWyzieSubtitle={loadWyzieSubtitle}
|
loadWyzieSubtitle={loadWyzieSubtitle}
|
||||||
selectTextTrack={(trackId) => {
|
selectTextTrack={(trackId) => {
|
||||||
if (useVLC) {
|
tracksHook.setSelectedTextTrack(trackId);
|
||||||
vlcHook.selectVlcSubtitleTrack(trackId);
|
// Actually tell MPV to switch the subtitle track
|
||||||
} else {
|
if (mpvPlayerRef.current) {
|
||||||
tracksHook.setSelectedTextTrack(trackId);
|
mpvPlayerRef.current.setSubtitleTrack(trackId);
|
||||||
// Actually tell MPV to switch the subtitle track
|
|
||||||
if (mpvPlayerRef.current) {
|
|
||||||
mpvPlayerRef.current.setSubtitleTrack(trackId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Disable custom subtitles when selecting built-in track
|
// Disable custom subtitles when selecting built-in track
|
||||||
setUseCustomSubtitles(false);
|
setUseCustomSubtitles(false);
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,17 @@ const KSPlayerCore: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [imdbId]);
|
}, [imdbId]);
|
||||||
|
|
||||||
|
// Sync custom subtitle text with current playback time
|
||||||
|
useEffect(() => {
|
||||||
|
if (!customSubs.useCustomSubtitles || customSubs.customSubtitles.length === 0) return;
|
||||||
|
|
||||||
|
const adjustedTime = currentTime + (customSubs.subtitleOffsetSec || 0);
|
||||||
|
const cueNow = customSubs.customSubtitles.find(
|
||||||
|
cue => adjustedTime >= cue.start && adjustedTime <= cue.end
|
||||||
|
);
|
||||||
|
customSubs.setCurrentSubtitle(cueNow ? cueNow.text : '');
|
||||||
|
}, [currentTime, customSubs.useCustomSubtitles, customSubs.customSubtitles, customSubs.subtitleOffsetSec]);
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
const onLoad = (data: any) => {
|
const onLoad = (data: any) => {
|
||||||
setDuration(data.duration);
|
setDuration(data.duration);
|
||||||
|
|
@ -416,7 +427,7 @@ const KSPlayerCore: React.FC = () => {
|
||||||
headers: stream.headers || undefined,
|
headers: stream.headers || undefined,
|
||||||
id,
|
id,
|
||||||
type: 'series',
|
type: 'series',
|
||||||
episodeId: ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`,
|
episodeId: ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number} `,
|
||||||
imdbId: imdbId ?? undefined,
|
imdbId: imdbId ?? undefined,
|
||||||
backdrop: backdrop || undefined,
|
backdrop: backdrop || undefined,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,364 +0,0 @@
|
||||||
import React, { useState, useRef, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react';
|
|
||||||
import { View, Dimensions } from 'react-native';
|
|
||||||
import { logger } from '../../utils/logger';
|
|
||||||
|
|
||||||
// Dynamic import to avoid iOS loading Android native module
|
|
||||||
let LibVlcPlayerViewComponent: any = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const mod = require('expo-libvlc-player');
|
|
||||||
LibVlcPlayerViewComponent = mod?.LibVlcPlayerView || null;
|
|
||||||
} catch {
|
|
||||||
LibVlcPlayerViewComponent = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VlcVideoPlayerProps {
|
|
||||||
source: string;
|
|
||||||
volume: number;
|
|
||||||
playbackSpeed: number;
|
|
||||||
zoomScale: number;
|
|
||||||
resizeMode: 'contain' | 'cover' | 'none';
|
|
||||||
onLoad: (data: any) => void;
|
|
||||||
onProgress: (data: any) => void;
|
|
||||||
onSeek: (data: any) => void;
|
|
||||||
onEnd: () => void;
|
|
||||||
onError: (error: any) => void;
|
|
||||||
onTracksUpdate: (tracks: { audio: any[], subtitle: any[] }) => void;
|
|
||||||
selectedAudioTrack?: number | null;
|
|
||||||
selectedSubtitleTrack?: number | null;
|
|
||||||
restoreTime?: number | null;
|
|
||||||
forceRemount?: boolean;
|
|
||||||
key?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VlcTrack {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
language?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VlcPlayerRef {
|
|
||||||
seek: (timeInSeconds: number) => void;
|
|
||||||
pause: () => void;
|
|
||||||
play: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VlcVideoPlayer = forwardRef<VlcPlayerRef, VlcVideoPlayerProps>(({
|
|
||||||
source,
|
|
||||||
volume,
|
|
||||||
playbackSpeed,
|
|
||||||
zoomScale,
|
|
||||||
resizeMode,
|
|
||||||
onLoad,
|
|
||||||
onProgress,
|
|
||||||
onSeek,
|
|
||||||
onEnd,
|
|
||||||
onError,
|
|
||||||
onTracksUpdate,
|
|
||||||
selectedAudioTrack,
|
|
||||||
selectedSubtitleTrack,
|
|
||||||
restoreTime,
|
|
||||||
forceRemount,
|
|
||||||
key,
|
|
||||||
}, ref) => {
|
|
||||||
const vlcRef = useRef<any>(null);
|
|
||||||
const [vlcActive, setVlcActive] = useState(true);
|
|
||||||
const [duration, setDuration] = useState<number>(0);
|
|
||||||
const [videoAspectRatio, setVideoAspectRatio] = useState<number | null>(null);
|
|
||||||
|
|
||||||
// Expose imperative methods to parent component
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
seek: (timeInSeconds: number) => {
|
|
||||||
if (vlcRef.current && typeof vlcRef.current.seek === 'function') {
|
|
||||||
const fraction = Math.min(Math.max(timeInSeconds / (duration || 1), 0), 0.999);
|
|
||||||
vlcRef.current.seek(fraction);
|
|
||||||
logger.log(`[VLC] Seeked to ${timeInSeconds}s (${fraction.toFixed(3)})`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pause: () => {
|
|
||||||
if (vlcRef.current && typeof vlcRef.current.pause === 'function') {
|
|
||||||
vlcRef.current.pause();
|
|
||||||
logger.log('[VLC] Paused');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
play: () => {
|
|
||||||
if (vlcRef.current && typeof vlcRef.current.play === 'function') {
|
|
||||||
vlcRef.current.play();
|
|
||||||
logger.log('[VLC] Played');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}), [duration]);
|
|
||||||
|
|
||||||
// Compute aspect ratio string for VLC
|
|
||||||
const toVlcRatio = useCallback((w: number, h: number): string => {
|
|
||||||
const a = Math.max(1, Math.round(w));
|
|
||||||
const b = Math.max(1, Math.round(h));
|
|
||||||
const gcd = (x: number, y: number): number => (y === 0 ? x : gcd(y, x % y));
|
|
||||||
const g = gcd(a, b);
|
|
||||||
return `${Math.floor(a / g)}:${Math.floor(b / g)}`;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const screenDimensions = Dimensions.get('screen');
|
|
||||||
|
|
||||||
const vlcAspectRatio = useMemo(() => {
|
|
||||||
// For VLC, no forced aspect ratio - let it preserve natural aspect
|
|
||||||
return undefined;
|
|
||||||
}, [resizeMode, screenDimensions.width, screenDimensions.height, toVlcRatio]);
|
|
||||||
|
|
||||||
const clientScale = useMemo(() => {
|
|
||||||
if (!videoAspectRatio || screenDimensions.width <= 0 || screenDimensions.height <= 0) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (resizeMode === 'cover') {
|
|
||||||
const screenAR = screenDimensions.width / screenDimensions.height;
|
|
||||||
return Math.max(screenAR / videoAspectRatio, videoAspectRatio / screenAR);
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}, [resizeMode, videoAspectRatio, screenDimensions.width, screenDimensions.height]);
|
|
||||||
|
|
||||||
// VLC options for better playback
|
|
||||||
const vlcOptions = useMemo(() => {
|
|
||||||
return [
|
|
||||||
'--network-caching=2000',
|
|
||||||
'--clock-jitter=0',
|
|
||||||
'--http-reconnect',
|
|
||||||
'--sout-mux-caching=2000'
|
|
||||||
];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// VLC tracks prop
|
|
||||||
const vlcTracks = useMemo(() => ({
|
|
||||||
audio: selectedAudioTrack,
|
|
||||||
video: 0, // Use first video track
|
|
||||||
subtitle: selectedSubtitleTrack
|
|
||||||
}), [selectedAudioTrack, selectedSubtitleTrack]);
|
|
||||||
|
|
||||||
const handleFirstPlay = useCallback((info: any) => {
|
|
||||||
try {
|
|
||||||
logger.log('[VLC] Video loaded, extracting tracks...');
|
|
||||||
logger.log('[AndroidVideoPlayer][VLC] Video loaded successfully');
|
|
||||||
|
|
||||||
// Process VLC tracks using optimized function
|
|
||||||
if (info?.tracks) {
|
|
||||||
processVlcTracks(info.tracks);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lenSec = (info?.length ?? 0) / 1000;
|
|
||||||
const width = info?.width || 0;
|
|
||||||
const height = info?.height || 0;
|
|
||||||
setDuration(lenSec);
|
|
||||||
onLoad({ duration: lenSec, naturalSize: width && height ? { width, height } : undefined });
|
|
||||||
|
|
||||||
if (width > 0 && height > 0) {
|
|
||||||
setVideoAspectRatio(width / height);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore playback position after remount (workaround for surface detach)
|
|
||||||
if (restoreTime !== undefined && restoreTime !== null && restoreTime > 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (vlcRef.current && typeof vlcRef.current.seek === 'function') {
|
|
||||||
const seekPosition = Math.min(restoreTime / lenSec, 0.999); // Convert to fraction
|
|
||||||
vlcRef.current.seek(seekPosition);
|
|
||||||
logger.log('[VLC] Seeked to restore position');
|
|
||||||
}
|
|
||||||
}, 500); // Small delay to ensure player is ready
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('[VLC] onFirstPlay error:', e);
|
|
||||||
logger.warn('[AndroidVideoPlayer][VLC] onFirstPlay parse error', e);
|
|
||||||
}
|
|
||||||
}, [onLoad, restoreTime]);
|
|
||||||
|
|
||||||
const handlePositionChanged = useCallback((ev: any) => {
|
|
||||||
const pos = typeof ev?.position === 'number' ? ev.position : 0;
|
|
||||||
// We need duration to calculate current time, but it's not available here
|
|
||||||
// The parent component should handle this calculation
|
|
||||||
onProgress({ position: pos });
|
|
||||||
}, [onProgress]);
|
|
||||||
|
|
||||||
const handlePlaying = useCallback(() => {
|
|
||||||
setVlcActive(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handlePaused = useCallback(() => {
|
|
||||||
setVlcActive(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleEndReached = useCallback(() => {
|
|
||||||
onEnd();
|
|
||||||
}, [onEnd]);
|
|
||||||
|
|
||||||
const handleEncounteredError = useCallback((e: any) => {
|
|
||||||
logger.error('[AndroidVideoPlayer][VLC] Encountered error:', e);
|
|
||||||
onError(e);
|
|
||||||
}, [onError]);
|
|
||||||
|
|
||||||
const handleBackground = useCallback(() => {
|
|
||||||
logger.log('[VLC] App went to background');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleESAdded = useCallback((tracks: any) => {
|
|
||||||
try {
|
|
||||||
logger.log('[VLC] ES Added - processing tracks...');
|
|
||||||
processVlcTracks(tracks);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('[VLC] onESAdded error:', e);
|
|
||||||
logger.warn('[AndroidVideoPlayer][VLC] onESAdded parse error', e);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Format VLC tracks to match RN Video format - raw version
|
|
||||||
const formatVlcTracks = useCallback((vlcTracks: Array<{id: number, name: string}>): VlcTrack[] => {
|
|
||||||
if (!Array.isArray(vlcTracks)) return [];
|
|
||||||
return vlcTracks.map(track => {
|
|
||||||
// Just extract basic language info if available, but keep the full name
|
|
||||||
let language = undefined;
|
|
||||||
let displayName = track.name || `Track ${track.id + 1}`;
|
|
||||||
|
|
||||||
// Log the raw track data for debugging
|
|
||||||
if (__DEV__) {
|
|
||||||
logger.log(`[VLC] Raw track data:`, { id: track.id, name: track.name });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only extract language from brackets if present, but keep full name
|
|
||||||
const languageMatch = track.name?.match(/\[([^\]]+)\]/);
|
|
||||||
if (languageMatch && languageMatch[1]) {
|
|
||||||
language = languageMatch[1].trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: track.id,
|
|
||||||
name: displayName, // Show exactly what VLC provides
|
|
||||||
language: language
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Optimized VLC track processing function with reduced JSON operations
|
|
||||||
const processVlcTracks = useCallback((tracks: any) => {
|
|
||||||
if (!tracks) return;
|
|
||||||
|
|
||||||
// Log raw VLC tracks data for debugging
|
|
||||||
if (__DEV__) {
|
|
||||||
logger.log(`[VLC] Raw tracks data:`, tracks);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { audio = [], subtitle = [] } = tracks;
|
|
||||||
|
|
||||||
// Process audio tracks
|
|
||||||
if (Array.isArray(audio) && audio.length > 0) {
|
|
||||||
const formattedAudio = formatVlcTracks(audio);
|
|
||||||
if (__DEV__) {
|
|
||||||
logger.log(`[VLC] Audio tracks updated:`, formattedAudio.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process subtitle tracks
|
|
||||||
if (Array.isArray(subtitle) && subtitle.length > 0) {
|
|
||||||
const formattedSubs = formatVlcTracks(subtitle);
|
|
||||||
if (__DEV__) {
|
|
||||||
logger.log(`[VLC] Subtitle tracks updated:`, formattedSubs.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify parent of track updates
|
|
||||||
onTracksUpdate({ audio, subtitle });
|
|
||||||
}, [formatVlcTracks, onTracksUpdate]);
|
|
||||||
|
|
||||||
// Process URL for VLC compatibility
|
|
||||||
const processUrlForVLC = useCallback((url: string): string => {
|
|
||||||
if (!url || typeof url !== 'string') {
|
|
||||||
logger.warn('[AndroidVideoPlayer][VLC] Invalid URL provided:', url);
|
|
||||||
return url || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if URL is already properly formatted
|
|
||||||
const urlObj = new URL(url);
|
|
||||||
|
|
||||||
// Handle special characters in the pathname that might cause issues
|
|
||||||
const pathname = urlObj.pathname;
|
|
||||||
const search = urlObj.search;
|
|
||||||
const hash = urlObj.hash;
|
|
||||||
|
|
||||||
// Decode and re-encode the pathname to handle double-encoding
|
|
||||||
const decodedPathname = decodeURIComponent(pathname);
|
|
||||||
const encodedPathname = encodeURI(decodedPathname);
|
|
||||||
|
|
||||||
// Reconstruct the URL
|
|
||||||
const processedUrl = `${urlObj.protocol}//${urlObj.host}${encodedPathname}${search}${hash}`;
|
|
||||||
|
|
||||||
logger.log(`[AndroidVideoPlayer][VLC] URL processed: ${url} -> ${processedUrl}`);
|
|
||||||
return processedUrl;
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`[AndroidVideoPlayer][VLC] URL processing failed, using original: ${error}`);
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const processedSource = useMemo(() => processUrlForVLC(source), [source, processUrlForVLC]);
|
|
||||||
|
|
||||||
if (!LibVlcPlayerViewComponent) {
|
|
||||||
return (
|
|
||||||
<View style={{
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: '#000'
|
|
||||||
}}>
|
|
||||||
{/* VLC not available fallback */}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: screenDimensions.width,
|
|
||||||
height: screenDimensions.height,
|
|
||||||
overflow: 'hidden'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LibVlcPlayerViewComponent
|
|
||||||
ref={vlcRef}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: screenDimensions.width,
|
|
||||||
height: screenDimensions.height,
|
|
||||||
transform: [{ scale: clientScale }]
|
|
||||||
}}
|
|
||||||
// Force remount when surfaces are recreated
|
|
||||||
key={key || 'vlc-default'}
|
|
||||||
source={processedSource}
|
|
||||||
aspectRatio={vlcAspectRatio}
|
|
||||||
// Let VLC auto-fit the video to the view to prevent flicker on mode changes
|
|
||||||
scale={0}
|
|
||||||
options={vlcOptions}
|
|
||||||
tracks={vlcTracks}
|
|
||||||
volume={Math.round(Math.max(0, Math.min(1, volume)) * 100)}
|
|
||||||
mute={false}
|
|
||||||
repeat={false}
|
|
||||||
rate={playbackSpeed}
|
|
||||||
autoplay={false}
|
|
||||||
onFirstPlay={handleFirstPlay}
|
|
||||||
onPositionChanged={handlePositionChanged}
|
|
||||||
onPlaying={handlePlaying}
|
|
||||||
onPaused={handlePaused}
|
|
||||||
onEndReached={handleEndReached}
|
|
||||||
onEncounteredError={handleEncounteredError}
|
|
||||||
onBackground={handleBackground}
|
|
||||||
onESAdded={handleESAdded}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
VlcVideoPlayer.displayName = 'VlcVideoPlayer';
|
|
||||||
|
|
||||||
export default VlcVideoPlayer;
|
|
||||||
|
|
@ -30,27 +30,6 @@ interface VideoSurfaceProps {
|
||||||
onPinchGestureEvent: any;
|
onPinchGestureEvent: any;
|
||||||
onPinchHandlerStateChange: any;
|
onPinchHandlerStateChange: any;
|
||||||
screenDimensions: { width: number, height: number };
|
screenDimensions: { width: number, height: number };
|
||||||
|
|
||||||
// Legacy props (kept for compatibility but unused with MPV)
|
|
||||||
useVLC?: boolean;
|
|
||||||
forceVlcRemount?: boolean;
|
|
||||||
headers?: any;
|
|
||||||
videoType?: any;
|
|
||||||
vlcSelectedAudioTrack?: number;
|
|
||||||
vlcSelectedSubtitleTrack?: number;
|
|
||||||
vlcRestoreTime?: number;
|
|
||||||
vlcKey?: string;
|
|
||||||
selectedAudioTrack?: any;
|
|
||||||
selectedTextTrack?: any;
|
|
||||||
useCustomSubtitles?: boolean;
|
|
||||||
onTracksUpdate?: (tracks: any) => void;
|
|
||||||
vlcPlayerRef?: any;
|
|
||||||
videoRef?: any;
|
|
||||||
vlcLoadedRef?: any;
|
|
||||||
customVideoStyles?: any;
|
|
||||||
loadStartAtRef?: any;
|
|
||||||
firstFrameAtRef?: any;
|
|
||||||
zoomScale?: number;
|
|
||||||
onTracksChanged?: (data: { audioTracks: any[]; subtitleTracks: any[] }) => void;
|
onTracksChanged?: (data: { audioTracks: any[]; subtitleTracks: any[] }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ const END_EPSILON = 0.3;
|
||||||
|
|
||||||
export const usePlayerControls = (
|
export const usePlayerControls = (
|
||||||
mpvPlayerRef: any,
|
mpvPlayerRef: any,
|
||||||
vlcPlayerRef: any,
|
|
||||||
useVLC: boolean,
|
|
||||||
paused: boolean,
|
paused: boolean,
|
||||||
setPaused: (paused: boolean) => void,
|
setPaused: (paused: boolean) => void,
|
||||||
currentTime: number,
|
currentTime: number,
|
||||||
|
|
@ -29,40 +27,31 @@ export const usePlayerControls = (
|
||||||
console.log('[usePlayerControls] seekToTime called:', {
|
console.log('[usePlayerControls] seekToTime called:', {
|
||||||
rawSeconds,
|
rawSeconds,
|
||||||
timeInSeconds,
|
timeInSeconds,
|
||||||
useVLC,
|
|
||||||
hasMpvRef: !!mpvPlayerRef?.current,
|
hasMpvRef: !!mpvPlayerRef?.current,
|
||||||
hasVlcRef: !!vlcPlayerRef?.current,
|
|
||||||
duration,
|
duration,
|
||||||
isSeeking: isSeeking.current
|
isSeeking: isSeeking.current
|
||||||
});
|
});
|
||||||
|
|
||||||
if (useVLC) {
|
// MPV Player
|
||||||
if (vlcPlayerRef.current && duration > 0) {
|
if (mpvPlayerRef.current && duration > 0) {
|
||||||
logger.log(`[usePlayerControls][VLC] Seeking to ${timeInSeconds}`);
|
console.log(`[usePlayerControls][MPV] Seeking to ${timeInSeconds}`);
|
||||||
vlcPlayerRef.current.seek(timeInSeconds);
|
|
||||||
}
|
isSeeking.current = true;
|
||||||
|
mpvPlayerRef.current.seek(timeInSeconds);
|
||||||
|
|
||||||
|
// Reset seeking flag after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isMounted.current) {
|
||||||
|
isSeeking.current = false;
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
} else {
|
} else {
|
||||||
// MPV Player
|
console.log('[usePlayerControls][MPV] Cannot seek - ref or duration invalid:', {
|
||||||
if (mpvPlayerRef.current && duration > 0) {
|
hasRef: !!mpvPlayerRef?.current,
|
||||||
console.log(`[usePlayerControls][MPV] Seeking to ${timeInSeconds}`);
|
duration
|
||||||
|
});
|
||||||
isSeeking.current = true;
|
|
||||||
mpvPlayerRef.current.seek(timeInSeconds);
|
|
||||||
|
|
||||||
// Reset seeking flag after a delay
|
|
||||||
setTimeout(() => {
|
|
||||||
if (isMounted.current) {
|
|
||||||
isSeeking.current = false;
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
} else {
|
|
||||||
console.log('[usePlayerControls][MPV] Cannot seek - ref or duration invalid:', {
|
|
||||||
hasRef: !!mpvPlayerRef?.current,
|
|
||||||
duration
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [useVLC, duration, paused, setPaused, mpvPlayerRef, vlcPlayerRef, isSeeking, isMounted]);
|
}, [duration, paused, setPaused, mpvPlayerRef, isSeeking, isMounted]);
|
||||||
|
|
||||||
const skip = useCallback((seconds: number) => {
|
const skip = useCallback((seconds: number) => {
|
||||||
console.log('[usePlayerControls] skip called:', { seconds, currentTime, newTime: currentTime + seconds });
|
console.log('[usePlayerControls] skip called:', { seconds, currentTime, newTime: currentTime + seconds });
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,8 @@ interface Track {
|
||||||
language?: string;
|
language?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePlayerTracks = (
|
export const usePlayerTracks = () => {
|
||||||
useVLC: boolean,
|
// Tracks from native player (MPV/RN-Video)
|
||||||
vlcAudioTracks: Track[],
|
|
||||||
vlcSubtitleTracks: Track[],
|
|
||||||
vlcSelectedAudioTrack: number | undefined,
|
|
||||||
vlcSelectedSubtitleTrack: number | undefined
|
|
||||||
) => {
|
|
||||||
// React Native Video Tracks
|
|
||||||
const [rnVideoAudioTracks, setRnVideoAudioTracks] = useState<Track[]>([]);
|
const [rnVideoAudioTracks, setRnVideoAudioTracks] = useState<Track[]>([]);
|
||||||
const [rnVideoTextTracks, setRnVideoTextTracks] = useState<Track[]>([]);
|
const [rnVideoTextTracks, setRnVideoTextTracks] = useState<Track[]>([]);
|
||||||
|
|
||||||
|
|
@ -22,31 +16,19 @@ export const usePlayerTracks = (
|
||||||
const [selectedAudioTrack, setSelectedAudioTrack] = useState<SelectedTrack | null>({ type: 'system' });
|
const [selectedAudioTrack, setSelectedAudioTrack] = useState<SelectedTrack | null>({ type: 'system' });
|
||||||
const [selectedTextTrack, setSelectedTextTrack] = useState<number>(-1);
|
const [selectedTextTrack, setSelectedTextTrack] = useState<number>(-1);
|
||||||
|
|
||||||
// Unified Tracks
|
// Unified Tracks (now just returns native tracks)
|
||||||
const ksAudioTracks = useMemo(() =>
|
const ksAudioTracks = useMemo(() => rnVideoAudioTracks, [rnVideoAudioTracks]);
|
||||||
useVLC ? vlcAudioTracks : rnVideoAudioTracks,
|
const ksTextTracks = useMemo(() => rnVideoTextTracks, [rnVideoTextTracks]);
|
||||||
[useVLC, vlcAudioTracks, rnVideoAudioTracks]
|
|
||||||
);
|
|
||||||
|
|
||||||
const ksTextTracks = useMemo(() =>
|
|
||||||
useVLC ? vlcSubtitleTracks : rnVideoTextTracks,
|
|
||||||
[useVLC, vlcSubtitleTracks, rnVideoTextTracks]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Unified Selection
|
// Unified Selection
|
||||||
const computedSelectedAudioTrack = useMemo(() =>
|
const computedSelectedAudioTrack = useMemo(() =>
|
||||||
useVLC
|
selectedAudioTrack?.type === 'index' && selectedAudioTrack?.value !== undefined
|
||||||
? (vlcSelectedAudioTrack ?? null)
|
? Number(selectedAudioTrack?.value)
|
||||||
: (selectedAudioTrack?.type === 'index' && selectedAudioTrack?.value !== undefined
|
: null,
|
||||||
? Number(selectedAudioTrack?.value)
|
[selectedAudioTrack]
|
||||||
: null),
|
|
||||||
[useVLC, vlcSelectedAudioTrack, selectedAudioTrack]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const computedSelectedTextTrack = useMemo(() =>
|
const computedSelectedTextTrack = useMemo(() => selectedTextTrack, [selectedTextTrack]);
|
||||||
useVLC ? (vlcSelectedSubtitleTrack ?? -1) : selectedTextTrack,
|
|
||||||
[useVLC, vlcSelectedSubtitleTrack, selectedTextTrack]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rnVideoAudioTracks, setRnVideoAudioTracks,
|
rnVideoAudioTracks, setRnVideoAudioTracks,
|
||||||
|
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
||||||
import { logger } from '../../../../utils/logger';
|
|
||||||
import { VlcPlayerRef } from '../../VlcVideoPlayer';
|
|
||||||
|
|
||||||
interface Track {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
language?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEBUG_MODE = false;
|
|
||||||
|
|
||||||
export const useVlcPlayer = (useVLC: boolean, paused: boolean, currentTime: number) => {
|
|
||||||
const [vlcAudioTracks, setVlcAudioTracks] = useState<Track[]>([]);
|
|
||||||
const [vlcSubtitleTracks, setVlcSubtitleTracks] = useState<Track[]>([]);
|
|
||||||
const [vlcSelectedAudioTrack, setVlcSelectedAudioTrack] = useState<number | undefined>(undefined);
|
|
||||||
const [vlcSelectedSubtitleTrack, setVlcSelectedSubtitleTrack] = useState<number | undefined>(undefined);
|
|
||||||
const [vlcRestoreTime, setVlcRestoreTime] = useState<number | undefined>(undefined);
|
|
||||||
const [forceVlcRemount, setForceVlcRemount] = useState(false);
|
|
||||||
const [vlcKey, setVlcKey] = useState('vlc-initial');
|
|
||||||
|
|
||||||
const vlcPlayerRef = useRef<VlcPlayerRef>(null);
|
|
||||||
const vlcLoadedRef = useRef<boolean>(false);
|
|
||||||
const trackUpdateTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
|
|
||||||
// Handle VLC pause/play interactions
|
|
||||||
useEffect(() => {
|
|
||||||
if (useVLC && vlcLoadedRef.current && vlcPlayerRef.current) {
|
|
||||||
if (paused) {
|
|
||||||
vlcPlayerRef.current.pause();
|
|
||||||
} else {
|
|
||||||
vlcPlayerRef.current.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [useVLC, paused]);
|
|
||||||
|
|
||||||
// Reset forceVlcRemount when VLC becomes inactive
|
|
||||||
useEffect(() => {
|
|
||||||
if (!useVLC && forceVlcRemount) {
|
|
||||||
setForceVlcRemount(false);
|
|
||||||
}
|
|
||||||
}, [useVLC, forceVlcRemount]);
|
|
||||||
|
|
||||||
// Track selection
|
|
||||||
const selectVlcAudioTrack = useCallback((trackId: number | null) => {
|
|
||||||
setVlcSelectedAudioTrack(trackId ?? undefined);
|
|
||||||
logger.log('[AndroidVideoPlayer][VLC] Audio track selected:', trackId);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const selectVlcSubtitleTrack = useCallback((trackId: number | null) => {
|
|
||||||
setVlcSelectedSubtitleTrack(trackId ?? undefined);
|
|
||||||
logger.log('[AndroidVideoPlayer][VLC] Subtitle track selected:', trackId);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Track updates handler
|
|
||||||
const handleVlcTracksUpdate = useCallback((tracks: { audio: any[], subtitle: any[] }) => {
|
|
||||||
if (!tracks) return;
|
|
||||||
|
|
||||||
if (trackUpdateTimeoutRef.current) {
|
|
||||||
clearTimeout(trackUpdateTimeoutRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
trackUpdateTimeoutRef.current = setTimeout(() => {
|
|
||||||
const { audio = [], subtitle = [] } = tracks;
|
|
||||||
let hasUpdates = false;
|
|
||||||
|
|
||||||
// Process Audio
|
|
||||||
if (Array.isArray(audio) && audio.length > 0) {
|
|
||||||
const formattedAudio = audio.map(track => ({
|
|
||||||
id: track.id,
|
|
||||||
name: track.name || `Track ${track.id + 1}`,
|
|
||||||
language: track.language
|
|
||||||
}));
|
|
||||||
|
|
||||||
const audioChanged = formattedAudio.length !== vlcAudioTracks.length ||
|
|
||||||
formattedAudio.some((track, index) => {
|
|
||||||
const existing = vlcAudioTracks[index];
|
|
||||||
return !existing || track.id !== existing.id || track.name !== existing.name;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (audioChanged) {
|
|
||||||
setVlcAudioTracks(formattedAudio);
|
|
||||||
hasUpdates = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process Subtitles
|
|
||||||
if (Array.isArray(subtitle) && subtitle.length > 0) {
|
|
||||||
const formattedSubs = subtitle.map(track => ({
|
|
||||||
id: track.id,
|
|
||||||
name: track.name || `Track ${track.id + 1}`,
|
|
||||||
language: track.language
|
|
||||||
}));
|
|
||||||
|
|
||||||
const subsChanged = formattedSubs.length !== vlcSubtitleTracks.length ||
|
|
||||||
formattedSubs.some((track, index) => {
|
|
||||||
const existing = vlcSubtitleTracks[index];
|
|
||||||
return !existing || track.id !== existing.id || track.name !== existing.name;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (subsChanged) {
|
|
||||||
setVlcSubtitleTracks(formattedSubs);
|
|
||||||
hasUpdates = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trackUpdateTimeoutRef.current = null;
|
|
||||||
}, 100);
|
|
||||||
}, [vlcAudioTracks, vlcSubtitleTracks]);
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (trackUpdateTimeoutRef.current) {
|
|
||||||
clearTimeout(trackUpdateTimeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const remountVlc = useCallback((reason: string) => {
|
|
||||||
if (useVLC) {
|
|
||||||
logger.log(`[VLC] Forcing complete remount: ${reason}`);
|
|
||||||
setVlcRestoreTime(currentTime);
|
|
||||||
setForceVlcRemount(true);
|
|
||||||
vlcLoadedRef.current = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
setForceVlcRemount(false);
|
|
||||||
setVlcKey(`vlc-${reason}-${Date.now()}`);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}, [useVLC, currentTime]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
vlcAudioTracks,
|
|
||||||
vlcSubtitleTracks,
|
|
||||||
vlcSelectedAudioTrack,
|
|
||||||
vlcSelectedSubtitleTrack,
|
|
||||||
selectVlcAudioTrack,
|
|
||||||
selectVlcSubtitleTrack,
|
|
||||||
vlcPlayerRef,
|
|
||||||
vlcLoadedRef,
|
|
||||||
forceVlcRemount,
|
|
||||||
vlcRestoreTime,
|
|
||||||
vlcKey,
|
|
||||||
handleVlcTracksUpdate,
|
|
||||||
remountVlc,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -2,13 +2,14 @@
|
||||||
* Shared Custom Subtitles Hook
|
* Shared Custom Subtitles Hook
|
||||||
* Used by both Android (VLC) and iOS (KSPlayer) players
|
* Used by both Android (VLC) and iOS (KSPlayer) players
|
||||||
*/
|
*/
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
DEFAULT_SUBTITLE_SIZE,
|
DEFAULT_SUBTITLE_SIZE,
|
||||||
SubtitleCue,
|
SubtitleCue,
|
||||||
SubtitleSegment,
|
SubtitleSegment,
|
||||||
WyzieSubtitle
|
WyzieSubtitle
|
||||||
} from '../utils/playerTypes';
|
} from '../utils/playerTypes';
|
||||||
|
import { storageService } from '../../../services/storageService';
|
||||||
|
|
||||||
export const useCustomSubtitles = () => {
|
export const useCustomSubtitles = () => {
|
||||||
// Data State
|
// Data State
|
||||||
|
|
@ -32,11 +33,58 @@ export const useCustomSubtitles = () => {
|
||||||
const [subtitleOutlineColor, setSubtitleOutlineColor] = useState<string>('#000000');
|
const [subtitleOutlineColor, setSubtitleOutlineColor] = useState<string>('#000000');
|
||||||
const [subtitleOutlineWidth, setSubtitleOutlineWidth] = useState<number>(4);
|
const [subtitleOutlineWidth, setSubtitleOutlineWidth] = useState<number>(4);
|
||||||
const [subtitleAlign, setSubtitleAlign] = useState<'center' | 'left' | 'right'>('center');
|
const [subtitleAlign, setSubtitleAlign] = useState<'center' | 'left' | 'right'>('center');
|
||||||
const [subtitleBottomOffset, setSubtitleBottomOffset] = useState<number>(10);
|
const [subtitleBottomOffset, setSubtitleBottomOffset] = useState<number>(20);
|
||||||
const [subtitleLetterSpacing, setSubtitleLetterSpacing] = useState<number>(0);
|
const [subtitleLetterSpacing, setSubtitleLetterSpacing] = useState<number>(0);
|
||||||
const [subtitleLineHeightMultiplier, setSubtitleLineHeightMultiplier] = useState<number>(1.2);
|
const [subtitleLineHeightMultiplier, setSubtitleLineHeightMultiplier] = useState<number>(1.2);
|
||||||
const [subtitleOffsetSec, setSubtitleOffsetSec] = useState<number>(0);
|
const [subtitleOffsetSec, setSubtitleOffsetSec] = useState<number>(0);
|
||||||
|
|
||||||
|
// Load subtitle settings on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const loadSettings = async () => {
|
||||||
|
const settings = await storageService.getSubtitleSettings();
|
||||||
|
if (settings) {
|
||||||
|
if (settings.subtitleSize !== undefined) setSubtitleSize(settings.subtitleSize);
|
||||||
|
if (settings.subtitleBackground !== undefined) setSubtitleBackground(settings.subtitleBackground);
|
||||||
|
if (settings.subtitleTextColor !== undefined) setSubtitleTextColor(settings.subtitleTextColor);
|
||||||
|
if (settings.subtitleBgOpacity !== undefined) setSubtitleBgOpacity(settings.subtitleBgOpacity);
|
||||||
|
if (settings.subtitleTextShadow !== undefined) setSubtitleTextShadow(settings.subtitleTextShadow);
|
||||||
|
if (settings.subtitleOutline !== undefined) setSubtitleOutline(settings.subtitleOutline);
|
||||||
|
if (settings.subtitleOutlineColor !== undefined) setSubtitleOutlineColor(settings.subtitleOutlineColor);
|
||||||
|
if (settings.subtitleOutlineWidth !== undefined) setSubtitleOutlineWidth(settings.subtitleOutlineWidth);
|
||||||
|
if (settings.subtitleAlign !== undefined) setSubtitleAlign(settings.subtitleAlign);
|
||||||
|
if (settings.subtitleBottomOffset !== undefined) setSubtitleBottomOffset(settings.subtitleBottomOffset);
|
||||||
|
if (settings.subtitleLetterSpacing !== undefined) setSubtitleLetterSpacing(settings.subtitleLetterSpacing);
|
||||||
|
if (settings.subtitleLineHeightMultiplier !== undefined) setSubtitleLineHeightMultiplier(settings.subtitleLineHeightMultiplier);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadSettings();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Save subtitle settings when they change
|
||||||
|
useEffect(() => {
|
||||||
|
const saveSettings = async () => {
|
||||||
|
await storageService.saveSubtitleSettings({
|
||||||
|
subtitleSize,
|
||||||
|
subtitleBackground,
|
||||||
|
subtitleTextColor,
|
||||||
|
subtitleBgOpacity,
|
||||||
|
subtitleTextShadow,
|
||||||
|
subtitleOutline,
|
||||||
|
subtitleOutlineColor,
|
||||||
|
subtitleOutlineWidth,
|
||||||
|
subtitleAlign,
|
||||||
|
subtitleBottomOffset,
|
||||||
|
subtitleLetterSpacing,
|
||||||
|
subtitleLineHeightMultiplier,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
saveSettings();
|
||||||
|
}, [
|
||||||
|
subtitleSize, subtitleBackground, subtitleTextColor, subtitleBgOpacity,
|
||||||
|
subtitleTextShadow, subtitleOutline, subtitleOutlineColor, subtitleOutlineWidth,
|
||||||
|
subtitleAlign, subtitleBottomOffset, subtitleLetterSpacing, subtitleLineHeightMultiplier
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customSubtitles, setCustomSubtitles,
|
customSubtitles, setCustomSubtitles,
|
||||||
currentSubtitle, setCurrentSubtitle,
|
currentSubtitle, setCurrentSubtitle,
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,6 @@ export type RootStackParamList = {
|
||||||
streamProvider?: string;
|
streamProvider?: string;
|
||||||
streamName?: string;
|
streamName?: string;
|
||||||
headers?: { [key: string]: string };
|
headers?: { [key: string]: string };
|
||||||
forceVlc?: boolean;
|
|
||||||
id?: string;
|
id?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
episodeId?: string;
|
episodeId?: string;
|
||||||
|
|
@ -146,7 +145,6 @@ export type RootStackParamList = {
|
||||||
streamProvider?: string;
|
streamProvider?: string;
|
||||||
streamName?: string;
|
streamName?: string;
|
||||||
headers?: { [key: string]: string };
|
headers?: { [key: string]: string };
|
||||||
forceVlc?: boolean;
|
|
||||||
id?: string;
|
id?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
episodeId?: string;
|
episodeId?: string;
|
||||||
|
|
|
||||||
|
|
@ -525,7 +525,6 @@ const DownloadsScreen: React.FC = () => {
|
||||||
streamProvider: 'Downloads',
|
streamProvider: 'Downloads',
|
||||||
streamName: item.providerName || 'Offline',
|
streamName: item.providerName || 'Offline',
|
||||||
headers: undefined,
|
headers: undefined,
|
||||||
forceVlc: Platform.OS === 'android' ? isMkv : false,
|
|
||||||
id: item.contentId, // Use contentId (base ID) instead of compound id for progress tracking
|
id: item.contentId, // Use contentId (base ID) instead of compound id for progress tracking
|
||||||
type: item.type,
|
type: item.type,
|
||||||
episodeId: episodeId, // Pass episodeId for series progress tracking
|
episodeId: episodeId, // Pass episodeId for series progress tracking
|
||||||
|
|
|
||||||
|
|
@ -821,7 +821,7 @@ export const StreamsScreen = () => {
|
||||||
fetchIMDbRatings();
|
fetchIMDbRatings();
|
||||||
}, [type, id, currentEpisode?.season_number, currentEpisode?.episode_number]);
|
}, [type, id, currentEpisode?.season_number, currentEpisode?.episode_number]);
|
||||||
|
|
||||||
const navigateToPlayer = useCallback(async (stream: Stream, options?: { forceVlc?: boolean; headers?: Record<string, string> }) => {
|
const navigateToPlayer = useCallback(async (stream: Stream, options?: { headers?: Record<string, string> }) => {
|
||||||
// Filter headers for Vidrock - only send essential headers
|
// Filter headers for Vidrock - only send essential headers
|
||||||
// Filter headers for Vidrock - only send essential headers
|
// Filter headers for Vidrock - only send essential headers
|
||||||
// Filter headers for Vidrock - only send essential headers
|
// Filter headers for Vidrock - only send essential headers
|
||||||
|
|
@ -859,9 +859,6 @@ export const StreamsScreen = () => {
|
||||||
const streamName = stream.name || stream.title || 'Unnamed Stream';
|
const streamName = stream.name || stream.title || 'Unnamed Stream';
|
||||||
const streamProvider = stream.addonId || stream.addonName || stream.name;
|
const streamProvider = stream.addonId || stream.addonName || stream.name;
|
||||||
|
|
||||||
// Do NOT pre-force VLC. Let ExoPlayer try first; fallback occurs on decoder error in the player.
|
|
||||||
let forceVlc = !!options?.forceVlc;
|
|
||||||
|
|
||||||
// Save stream to cache for future use
|
// Save stream to cache for future use
|
||||||
try {
|
try {
|
||||||
const episodeId = (type === 'series' || type === 'other') && selectedEpisode ? selectedEpisode : undefined;
|
const episodeId = (type === 'series' || type === 'other') && selectedEpisode ? selectedEpisode : undefined;
|
||||||
|
|
@ -922,8 +919,6 @@ export const StreamsScreen = () => {
|
||||||
streamName: streamName,
|
streamName: streamName,
|
||||||
// Use filtered headers for Vidrock compatibility
|
// Use filtered headers for Vidrock compatibility
|
||||||
headers: finalHeaders,
|
headers: finalHeaders,
|
||||||
// Android will use this to choose VLC path; iOS ignores
|
|
||||||
forceVlc,
|
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
episodeId: (type === 'series' || type === 'other') && selectedEpisode ? selectedEpisode : undefined,
|
episodeId: (type === 'series' || type === 'other') && selectedEpisode ? selectedEpisode : undefined,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import { isMkvStream } from './mkvDetection';
|
||||||
export interface PlayerSelectionOptions {
|
export interface PlayerSelectionOptions {
|
||||||
uri: string;
|
uri: string;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
forceVlc?: boolean;
|
|
||||||
platform?: typeof Platform.OS;
|
platform?: typeof Platform.OS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,10 +18,9 @@ export interface PlayerSelectionOptions {
|
||||||
export const shouldUseKSPlayer = ({
|
export const shouldUseKSPlayer = ({
|
||||||
uri,
|
uri,
|
||||||
headers,
|
headers,
|
||||||
forceVlc = false,
|
|
||||||
platform = Platform.OS
|
platform = Platform.OS
|
||||||
}: PlayerSelectionOptions): boolean => {
|
}: PlayerSelectionOptions): boolean => {
|
||||||
// Android always uses AndroidVideoPlayer (react-native-video)
|
// Android always uses AndroidVideoPlayer (MPV)
|
||||||
if (platform === 'android') {
|
if (platform === 'android') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue