mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
added subtitle addon support
This commit is contained in:
parent
583db67853
commit
7d9f8fba86
4 changed files with 300 additions and 243 deletions
|
|
@ -15,14 +15,13 @@ import { useTraktAutosync } from '../../hooks/useTraktAutosync';
|
|||
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
||||
import { useMetadata } from '../../hooks/useMetadata';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { testVideoStreamUrl } from '../../utils/httpInterceptor';
|
||||
import { stremioService, Subtitle } from '../../services/stremioService';
|
||||
|
||||
import {
|
||||
DEFAULT_SUBTITLE_SIZE,
|
||||
AudioTrack,
|
||||
TextTrack,
|
||||
ResizeModeType,
|
||||
WyzieSubtitle,
|
||||
SubtitleCue,
|
||||
RESUME_PREF_KEY,
|
||||
RESUME_PREF,
|
||||
|
|
@ -36,6 +35,8 @@ import ResumeOverlay from './modals/ResumeOverlay';
|
|||
import PlayerControls from './controls/PlayerControls';
|
||||
import CustomSubtitles from './subtitles/CustomSubtitles';
|
||||
import { SourcesModal } from './modals/SourcesModal';
|
||||
import { stremioService } from '../../services/stremioService';
|
||||
import axios from 'axios';
|
||||
|
||||
// Map VLC resize modes to react-native-video resize modes
|
||||
const getVideoResizeMode = (resizeMode: ResizeModeType) => {
|
||||
|
|
@ -145,11 +146,12 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
const pinchRef = useRef<PinchGestureHandler>(null);
|
||||
const [customSubtitles, setCustomSubtitles] = useState<SubtitleCue[]>([]);
|
||||
const [currentSubtitle, setCurrentSubtitle] = useState<string>('');
|
||||
const [customSubtitleVersion, setCustomSubtitleVersion] = useState<number>(0);
|
||||
const [subtitleSize, setSubtitleSize] = useState<number>(DEFAULT_SUBTITLE_SIZE);
|
||||
const [subtitleBackground, setSubtitleBackground] = useState<boolean>(true);
|
||||
const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false);
|
||||
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false);
|
||||
const [availableSubtitles, setAvailableSubtitles] = useState<Subtitle[]>([]);
|
||||
const [availableSubtitles, setAvailableSubtitles] = useState<WyzieSubtitle[]>([]);
|
||||
const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState<boolean>(false);
|
||||
const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState<boolean>(false);
|
||||
const [showSourcesModal, setShowSourcesModal] = useState<boolean>(false);
|
||||
|
|
@ -403,8 +405,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
saveWatchProgress();
|
||||
}, syncInterval);
|
||||
|
||||
// Removed excessive logging for watch progress save interval
|
||||
|
||||
setProgressSaveInterval(interval);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
|
|
@ -498,8 +498,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
|
||||
const onLoad = (data: any) => {
|
||||
try {
|
||||
// HTTP response logging removed
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
logger.log('[AndroidVideoPlayer] Video loaded:', data);
|
||||
}
|
||||
|
|
@ -622,7 +620,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
NativeModules.StatusBarManager.setHidden(true);
|
||||
}
|
||||
} catch (error) {
|
||||
// Immersive mode error - silently handled
|
||||
console.log('Immersive mode error:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -715,8 +713,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
|
||||
const handleError = (error: any) => {
|
||||
try {
|
||||
// HTTP error response logging removed
|
||||
|
||||
logger.error('AndroidVideoPlayer error: ', error);
|
||||
|
||||
// Early return if component is unmounted to prevent iOS crashes
|
||||
|
|
@ -891,100 +887,183 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
}
|
||||
setIsLoadingSubtitleList(true);
|
||||
try {
|
||||
// Determine content type and ID format for Stremio
|
||||
let contentType = 'movie';
|
||||
let contentId = targetImdbId;
|
||||
let videoId: string | undefined;
|
||||
|
||||
if (season && episode) {
|
||||
contentType = 'series';
|
||||
videoId = `series:${targetImdbId}:${season}:${episode}`;
|
||||
}
|
||||
|
||||
logger.log(`[AndroidVideoPlayer] Fetching subtitles for ${contentType}: ${contentId}${videoId ? ` (${videoId})` : ''}`);
|
||||
|
||||
const subtitles = await stremioService.getSubtitles(contentType, contentId, videoId);
|
||||
|
||||
// Remove duplicates based on language
|
||||
const uniqueSubtitles = subtitles.reduce((acc: Subtitle[], current: Subtitle) => {
|
||||
const exists = acc.find((item: Subtitle) => item.lang === current.lang);
|
||||
// Fetch from installed OpenSubtitles v3 addon via Stremio only
|
||||
const stremioType = type === 'series' ? 'series' : 'movie';
|
||||
const stremioVideoId = stremioType === 'series' && season && episode
|
||||
? `series:${targetImdbId}:${season}:${episode}`
|
||||
: undefined;
|
||||
const stremioResults = await stremioService.getSubtitles(stremioType, targetImdbId, stremioVideoId);
|
||||
const stremioSubs: WyzieSubtitle[] = (stremioResults || []).map(sub => ({
|
||||
id: sub.id || `${sub.lang}-${sub.url}`,
|
||||
url: sub.url,
|
||||
flagUrl: '',
|
||||
format: 'srt',
|
||||
encoding: 'utf-8',
|
||||
media: 'opensubtitles',
|
||||
display: sub.lang || 'Unknown',
|
||||
language: (sub.lang || '').toLowerCase(),
|
||||
isHearingImpaired: false,
|
||||
source: sub.addonName || 'OpenSubtitles v3',
|
||||
}));
|
||||
|
||||
// De-duplicate by language
|
||||
const uniqueSubtitles = stremioSubs.reduce((acc, current) => {
|
||||
const exists = acc.find(item => item.language === current.language);
|
||||
if (!exists) {
|
||||
acc.push(current);
|
||||
}
|
||||
return acc;
|
||||
}, [] as Subtitle[]);
|
||||
|
||||
// Sort by language
|
||||
uniqueSubtitles.sort((a: Subtitle, b: Subtitle) => a.lang.localeCompare(b.lang));
|
||||
}, [] as WyzieSubtitle[]);
|
||||
uniqueSubtitles.sort((a, b) => a.display.localeCompare(b.display));
|
||||
setAvailableSubtitles(uniqueSubtitles);
|
||||
|
||||
if (autoSelectEnglish) {
|
||||
const englishSubtitle = uniqueSubtitles.find((sub: Subtitle) =>
|
||||
sub.lang.toLowerCase() === 'eng' ||
|
||||
sub.lang.toLowerCase() === 'en' ||
|
||||
sub.lang.toLowerCase().includes('english')
|
||||
const englishSubtitle = uniqueSubtitles.find(sub =>
|
||||
sub.language.toLowerCase() === 'eng' ||
|
||||
sub.language.toLowerCase() === 'en' ||
|
||||
sub.display.toLowerCase().includes('english')
|
||||
);
|
||||
if (englishSubtitle) {
|
||||
loadStremioSubtitle(englishSubtitle);
|
||||
loadWyzieSubtitle(englishSubtitle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!autoSelectEnglish && uniqueSubtitles.length > 0) {
|
||||
if (!autoSelectEnglish) {
|
||||
setShowSubtitleLanguageModal(true);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[AndroidVideoPlayer] Error fetching subtitles from Stremio addons:', error);
|
||||
logger.error('[AndroidVideoPlayer] Error fetching subtitles from OpenSubtitles addon:', error);
|
||||
} finally {
|
||||
setIsLoadingSubtitleList(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadStremioSubtitle = async (subtitle: Subtitle) => {
|
||||
console.log('[AndroidVideoPlayer] Starting subtitle load, setting isLoadingSubtitles to true');
|
||||
const loadWyzieSubtitle = async (subtitle: WyzieSubtitle) => {
|
||||
logger.log(`[AndroidVideoPlayer] Subtitle click received: id=${subtitle.id}, lang=${subtitle.language}, url=${subtitle.url}`);
|
||||
setShowSubtitleLanguageModal(false);
|
||||
logger.log('[AndroidVideoPlayer] setShowSubtitleLanguageModal(false)');
|
||||
setIsLoadingSubtitles(true);
|
||||
logger.log('[AndroidVideoPlayer] isLoadingSubtitles -> true');
|
||||
try {
|
||||
const response = await fetch(subtitle.url);
|
||||
const srtContent = await response.text();
|
||||
logger.log('[AndroidVideoPlayer] Fetching subtitle SRT start');
|
||||
let srtContent = '';
|
||||
try {
|
||||
const axiosResp = await axios.get(subtitle.url, {
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Accept': 'text/plain, */*',
|
||||
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Nuvio/1.0'
|
||||
},
|
||||
responseType: 'text',
|
||||
transitional: {
|
||||
clarifyTimeoutError: true
|
||||
}
|
||||
});
|
||||
srtContent = typeof axiosResp.data === 'string' ? axiosResp.data : String(axiosResp.data || '');
|
||||
} catch (axiosErr: any) {
|
||||
logger.warn('[AndroidVideoPlayer] Axios subtitle fetch failed, falling back to fetch()', {
|
||||
message: axiosErr?.message,
|
||||
code: axiosErr?.code
|
||||
});
|
||||
// Fallback with explicit timeout using AbortController
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
try {
|
||||
const resp = await fetch(subtitle.url, { signal: controller.signal });
|
||||
srtContent = await resp.text();
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
logger.log(`[AndroidVideoPlayer] Fetching subtitle SRT done, size=${srtContent.length}`);
|
||||
const parsedCues = parseSRT(srtContent);
|
||||
logger.log(`[AndroidVideoPlayer] Parsed cues count=${parsedCues.length}`);
|
||||
|
||||
// Force a microtask delay before updating subtitle state
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
setCustomSubtitles(parsedCues);
|
||||
setUseCustomSubtitles(true);
|
||||
setSelectedTextTrack(-1);
|
||||
logger.log(`[AndroidVideoPlayer] Loaded subtitle: ${subtitle.lang} from ${subtitle.addonName}`);
|
||||
console.log('[AndroidVideoPlayer] Subtitle loaded successfully, about to seek');
|
||||
|
||||
// Force a state update by triggering a seek
|
||||
if (videoRef.current && duration > 0 && !isSeeking.current) {
|
||||
const currentPos = currentTime;
|
||||
console.log(`[AndroidVideoPlayer] Forcing a micro-seek to refresh subtitle state from ${currentPos}s`);
|
||||
seekToTime(currentPos);
|
||||
// iOS AVPlayer workaround: clear subtitle state first, then apply
|
||||
if (Platform.OS === 'ios') {
|
||||
logger.log('[AndroidVideoPlayer] iOS detected; clearing subtitle state before apply');
|
||||
// Immediately stop spinner so UI doesn't get stuck
|
||||
setIsLoadingSubtitles(false);
|
||||
logger.log('[AndroidVideoPlayer] isLoadingSubtitles -> false (early stop for iOS)');
|
||||
// Step 1: Clear any existing subtitle state
|
||||
setUseCustomSubtitles(false);
|
||||
logger.log('[AndroidVideoPlayer] useCustomSubtitles -> false');
|
||||
setCustomSubtitles([]);
|
||||
logger.log('[AndroidVideoPlayer] customSubtitles -> []');
|
||||
setSelectedTextTrack(-1);
|
||||
logger.log('[AndroidVideoPlayer] selectedTextTrack -> -1');
|
||||
|
||||
// Wait for seek to complete before clearing loading state
|
||||
setTimeout(() => {
|
||||
console.log('[AndroidVideoPlayer] Clearing isLoadingSubtitles after seek timeout');
|
||||
setIsLoadingSubtitles(false);
|
||||
}, 600); // Wait longer than seekToTime's internal timeout (500ms)
|
||||
// Step 2: Apply immediately (no scheduling), then do a small micro-nudge
|
||||
logger.log('[AndroidVideoPlayer] Applying parsed cues immediately (iOS)');
|
||||
setCustomSubtitles(parsedCues);
|
||||
logger.log('[AndroidVideoPlayer] customSubtitles <- parsedCues');
|
||||
setUseCustomSubtitles(true);
|
||||
logger.log('[AndroidVideoPlayer] useCustomSubtitles -> true');
|
||||
setSelectedTextTrack(-1);
|
||||
logger.log('[AndroidVideoPlayer] selectedTextTrack -> -1 (disable native while using custom)');
|
||||
setCustomSubtitleVersion(v => v + 1);
|
||||
logger.log('[AndroidVideoPlayer] customSubtitleVersion incremented');
|
||||
|
||||
// Immediately set current subtitle based on currentTime to avoid waiting for next onProgress
|
||||
try {
|
||||
const cueNow = parsedCues.find(cue => currentTime >= cue.start && currentTime <= cue.end);
|
||||
const textNow = cueNow ? cueNow.text : '';
|
||||
setCurrentSubtitle(textNow);
|
||||
logger.log('[AndroidVideoPlayer] currentSubtitle set immediately after apply (iOS)');
|
||||
} catch (e) {
|
||||
logger.error('[AndroidVideoPlayer] Error setting immediate subtitle', e);
|
||||
}
|
||||
|
||||
// Micro-seek nudge to force AVPlayer to refresh rendering
|
||||
try {
|
||||
if (videoRef.current && duration > 0) {
|
||||
const wasPaused = paused;
|
||||
const original = currentTime;
|
||||
const forward = Math.min(original + 0.05, Math.max(duration - 0.1, 0));
|
||||
logger.log('[AndroidVideoPlayer] Performing micro-seek nudge (iOS)', { original, forward });
|
||||
if (wasPaused) {
|
||||
setPaused(false);
|
||||
}
|
||||
// Give state a moment to apply before seeking
|
||||
setTimeout(() => {
|
||||
try {
|
||||
videoRef.current?.seek(forward);
|
||||
setTimeout(() => {
|
||||
videoRef.current?.seek(original);
|
||||
if (wasPaused) {
|
||||
setPaused(true);
|
||||
}
|
||||
logger.log('[AndroidVideoPlayer] Micro-seek nudge complete (iOS)');
|
||||
}, 150);
|
||||
} catch (e) {
|
||||
logger.warn('[AndroidVideoPlayer] Inner micro-seek failed (iOS)', e);
|
||||
if (wasPaused) setPaused(true);
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
} catch(e) {
|
||||
logger.warn('[AndroidVideoPlayer] Outer micro-seek failed (iOS)', e);
|
||||
}
|
||||
} else {
|
||||
console.warn(`[AndroidVideoPlayer] Cannot seek to refresh subtitles: videoRef=${!!videoRef.current}, duration=${duration}, seeking=${isSeeking.current}`);
|
||||
// Clear loading state immediately if we can't seek
|
||||
setTimeout(() => {
|
||||
setIsLoadingSubtitles(false);
|
||||
console.log('[AndroidVideoPlayer] isLoadingSubtitles set to false (no seek)');
|
||||
}, 100);
|
||||
// Android works immediately
|
||||
setCustomSubtitles(parsedCues);
|
||||
logger.log('[AndroidVideoPlayer] (Android) customSubtitles <- parsedCues');
|
||||
setUseCustomSubtitles(true);
|
||||
logger.log('[AndroidVideoPlayer] (Android) useCustomSubtitles -> true');
|
||||
setSelectedTextTrack(-1);
|
||||
logger.log('[AndroidVideoPlayer] (Android) selectedTextTrack -> -1');
|
||||
setIsLoadingSubtitles(false);
|
||||
logger.log('[AndroidVideoPlayer] (Android) isLoadingSubtitles -> false');
|
||||
try {
|
||||
const cueNow = parsedCues.find(cue => currentTime >= cue.start && currentTime <= cue.end);
|
||||
const textNow = cueNow ? cueNow.text : '';
|
||||
setCurrentSubtitle(textNow);
|
||||
logger.log('[AndroidVideoPlayer] currentSubtitle set immediately after apply (Android)');
|
||||
} catch {}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[AndroidVideoPlayer] Error loading Stremio subtitle:', error);
|
||||
console.log('[AndroidVideoPlayer] Subtitle loading failed:', error);
|
||||
// Clear loading state on error
|
||||
setTimeout(() => {
|
||||
setIsLoadingSubtitles(false);
|
||||
console.log('[AndroidVideoPlayer] isLoadingSubtitles set to false (error)');
|
||||
}, 100);
|
||||
logger.error('[AndroidVideoPlayer] Error loading Wyzie subtitle:', error);
|
||||
setIsLoadingSubtitles(false);
|
||||
logger.log('[AndroidVideoPlayer] isLoadingSubtitles -> false (error path)');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -996,7 +1075,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
// Send a forced pause update to Trakt immediately when user pauses
|
||||
if (newPausedState && duration > 0) {
|
||||
traktAutosync.handleProgressUpdate(currentTime, duration, true);
|
||||
logger.log('[AndroidVideoPlayer] Sent forced pause update to Trakt');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1031,10 +1109,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
currentTime >= cue.start && currentTime <= cue.end
|
||||
);
|
||||
const newSubtitle = currentCue ? currentCue.text : '';
|
||||
if (newSubtitle !== currentSubtitle) {
|
||||
setCurrentSubtitle(newSubtitle);
|
||||
}
|
||||
}, [currentTime, customSubtitles, useCustomSubtitles, currentSubtitle]);
|
||||
setCurrentSubtitle(newSubtitle);
|
||||
}, [currentTime, customSubtitles, useCustomSubtitles]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSubtitleSize();
|
||||
|
|
@ -1097,23 +1173,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
}
|
||||
}, [pendingSeek, isPlayerReady, isVideoLoaded, duration]);
|
||||
|
||||
// HTTP stream testing with logging
|
||||
useEffect(() => {
|
||||
if (currentStreamUrl && currentStreamUrl.trim() !== '') {
|
||||
const testStream = async () => {
|
||||
try {
|
||||
// Stream testing without verbose logging
|
||||
await testVideoStreamUrl(currentStreamUrl, headers || {});
|
||||
} catch (error) {
|
||||
// Stream test failed silently
|
||||
}
|
||||
};
|
||||
|
||||
// Test the stream URL when it changes
|
||||
testStream();
|
||||
}
|
||||
}, [currentStreamUrl, headers]);
|
||||
|
||||
const handleSelectStream = async (newStream: any) => {
|
||||
if (newStream.url === currentStreamUrl) {
|
||||
setShowSourcesModal(false);
|
||||
|
|
@ -1315,16 +1374,25 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
headers: headers
|
||||
} : { uri: currentStreamUrl };
|
||||
|
||||
// HTTP request logging removed
|
||||
// HTTP request logging removed; source prepared
|
||||
|
||||
return sourceWithHeaders;
|
||||
})()}
|
||||
paused={paused}
|
||||
onProgress={handleProgress}
|
||||
onLoad={onLoad}
|
||||
onLoad={(e) => {
|
||||
logger.log('[AndroidVideoPlayer] onLoad fired', { duration: e?.duration });
|
||||
onLoad(e);
|
||||
}}
|
||||
onEnd={onEnd}
|
||||
onError={handleError}
|
||||
onBuffer={onBuffer}
|
||||
onError={(err) => {
|
||||
logger.error('[AndroidVideoPlayer] onError', err);
|
||||
handleError(err);
|
||||
}}
|
||||
onBuffer={(buf) => {
|
||||
logger.log('[AndroidVideoPlayer] onBuffer', buf);
|
||||
onBuffer(buf);
|
||||
}}
|
||||
resizeMode={getVideoResizeMode(resizeMode)}
|
||||
selectedAudioTrack={selectedAudioTrack !== null ? { type: SelectedTrackType.INDEX, value: selectedAudioTrack } : undefined}
|
||||
selectedTextTrack={useCustomSubtitles ? { type: SelectedTrackType.DISABLED } : (selectedTextTrack >= 0 ? { type: SelectedTrackType.INDEX, value: selectedTextTrack } : undefined)}
|
||||
|
|
@ -1376,6 +1444,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
/>
|
||||
|
||||
<CustomSubtitles
|
||||
key={customSubtitleVersion}
|
||||
useCustomSubtitles={useCustomSubtitles}
|
||||
currentSubtitle={currentSubtitle}
|
||||
subtitleSize={subtitleSize}
|
||||
|
|
@ -1418,7 +1487,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
subtitleSize={subtitleSize}
|
||||
subtitleBackground={subtitleBackground}
|
||||
fetchAvailableSubtitles={fetchAvailableSubtitles}
|
||||
loadStremioSubtitle={loadStremioSubtitle}
|
||||
loadWyzieSubtitle={loadWyzieSubtitle}
|
||||
selectTextTrack={selectTextTrack}
|
||||
increaseSubtitleSize={increaseSubtitleSize}
|
||||
decreaseSubtitleSize={decreaseSubtitleSize}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ import { useTraktAutosync } from '../../hooks/useTraktAutosync';
|
|||
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
||||
import { useMetadata } from '../../hooks/useMetadata';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { stremioService, Subtitle } from '../../services/stremioService';
|
||||
|
||||
import {
|
||||
DEFAULT_SUBTITLE_SIZE,
|
||||
AudioTrack,
|
||||
TextTrack,
|
||||
ResizeModeType,
|
||||
WyzieSubtitle,
|
||||
SubtitleCue,
|
||||
RESUME_PREF_KEY,
|
||||
RESUME_PREF,
|
||||
|
|
@ -37,6 +37,7 @@ import ResumeOverlay from './modals/ResumeOverlay';
|
|||
import PlayerControls from './controls/PlayerControls';
|
||||
import CustomSubtitles from './subtitles/CustomSubtitles';
|
||||
import { SourcesModal } from './modals/SourcesModal';
|
||||
import { stremioService } from '../../services/stremioService';
|
||||
|
||||
const VideoPlayer: React.FC = () => {
|
||||
const route = useRoute<RouteProp<RootStackParamList, 'Player'>>();
|
||||
|
|
@ -51,8 +52,8 @@ const VideoPlayer: React.FC = () => {
|
|||
// Use AndroidVideoPlayer for:
|
||||
// - Android devices
|
||||
// - Xprime streams on any platform
|
||||
// - MKV files on iOS are now handled by VideoPlayer.tsx (VLCPlayer)
|
||||
if (Platform.OS === 'android' || isXprimeStream) {
|
||||
// - Non-MKV files on iOS
|
||||
if (Platform.OS === 'android' || isXprimeStream || (Platform.OS === 'ios' && !isMkvFile)) {
|
||||
return <AndroidVideoPlayer />;
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +161,7 @@ const VideoPlayer: React.FC = () => {
|
|||
const [subtitleBackground, setSubtitleBackground] = useState<boolean>(true);
|
||||
const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false);
|
||||
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false);
|
||||
const [availableSubtitles, setAvailableSubtitles] = useState<Subtitle[]>([]);
|
||||
const [availableSubtitles, setAvailableSubtitles] = useState<WyzieSubtitle[]>([]);
|
||||
const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState<boolean>(false);
|
||||
const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState<boolean>(false);
|
||||
const [showSourcesModal, setShowSourcesModal] = useState<boolean>(false);
|
||||
|
|
@ -434,8 +435,6 @@ const VideoPlayer: React.FC = () => {
|
|||
saveWatchProgress();
|
||||
}, syncInterval);
|
||||
|
||||
// Removed excessive logging for watch progress save interval
|
||||
|
||||
setProgressSaveInterval(interval);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
|
|
@ -470,7 +469,6 @@ const VideoPlayer: React.FC = () => {
|
|||
// Send a forced pause update to Trakt immediately when user pauses
|
||||
if (duration > 0) {
|
||||
traktAutosync.handleProgressUpdate(currentTime, duration, true);
|
||||
logger.log('[VideoPlayer] Sent forced pause update to Trakt');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -567,7 +565,6 @@ const VideoPlayer: React.FC = () => {
|
|||
|
||||
const onLoad = (data: any) => {
|
||||
try {
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
logger.log('[VideoPlayer] Video loaded:', data);
|
||||
}
|
||||
|
|
@ -797,8 +794,6 @@ const VideoPlayer: React.FC = () => {
|
|||
};
|
||||
|
||||
const handleError = (error: any) => {
|
||||
// HTTP error response logging removed
|
||||
|
||||
logger.error('[VideoPlayer] Playback Error:', error);
|
||||
|
||||
// Format error details for user display
|
||||
|
|
@ -907,56 +902,57 @@ const VideoPlayer: React.FC = () => {
|
|||
}
|
||||
setIsLoadingSubtitleList(true);
|
||||
try {
|
||||
// Determine content type and ID format for Stremio
|
||||
let contentType = 'movie';
|
||||
let contentId = targetImdbId;
|
||||
let videoId: string | undefined;
|
||||
|
||||
if (season && episode) {
|
||||
contentType = 'series';
|
||||
videoId = `series:${targetImdbId}:${season}:${episode}`;
|
||||
}
|
||||
|
||||
logger.log(`[VideoPlayer] Fetching subtitles for ${contentType}: ${contentId}${videoId ? ` (${videoId})` : ''}`);
|
||||
|
||||
const subtitles = await stremioService.getSubtitles(contentType, contentId, videoId);
|
||||
|
||||
// Remove duplicates based on language
|
||||
const uniqueSubtitles = subtitles.reduce((acc: Subtitle[], current: Subtitle) => {
|
||||
const exists = acc.find((item: Subtitle) => item.lang === current.lang);
|
||||
// Fetch from installed OpenSubtitles v3 addon via Stremio only
|
||||
const stremioType = type === 'series' ? 'series' : 'movie';
|
||||
const stremioVideoId = stremioType === 'series' && season && episode
|
||||
? `series:${targetImdbId}:${season}:${episode}`
|
||||
: undefined;
|
||||
const stremioResults = await stremioService.getSubtitles(stremioType, targetImdbId, stremioVideoId);
|
||||
const stremioSubs: WyzieSubtitle[] = (stremioResults || []).map(sub => ({
|
||||
id: sub.id || `${sub.lang}-${sub.url}`,
|
||||
url: sub.url,
|
||||
flagUrl: '',
|
||||
format: 'srt',
|
||||
encoding: 'utf-8',
|
||||
media: 'opensubtitles',
|
||||
display: sub.lang || 'Unknown',
|
||||
language: (sub.lang || '').toLowerCase(),
|
||||
isHearingImpaired: false,
|
||||
source: sub.addonName || 'OpenSubtitles v3',
|
||||
}));
|
||||
|
||||
// De-duplicate by language
|
||||
const uniqueSubtitles = stremioSubs.reduce((acc, current) => {
|
||||
const exists = acc.find(item => item.language === current.language);
|
||||
if (!exists) {
|
||||
acc.push(current);
|
||||
}
|
||||
return acc;
|
||||
}, [] as Subtitle[]);
|
||||
|
||||
// Sort by language
|
||||
uniqueSubtitles.sort((a: Subtitle, b: Subtitle) => a.lang.localeCompare(b.lang));
|
||||
}, [] as WyzieSubtitle[]);
|
||||
uniqueSubtitles.sort((a, b) => a.display.localeCompare(b.display));
|
||||
setAvailableSubtitles(uniqueSubtitles);
|
||||
|
||||
if (autoSelectEnglish) {
|
||||
const englishSubtitle = uniqueSubtitles.find((sub: Subtitle) =>
|
||||
sub.lang.toLowerCase() === 'eng' ||
|
||||
sub.lang.toLowerCase() === 'en' ||
|
||||
sub.lang.toLowerCase().includes('english')
|
||||
const englishSubtitle = uniqueSubtitles.find(sub =>
|
||||
sub.language.toLowerCase() === 'eng' ||
|
||||
sub.language.toLowerCase() === 'en' ||
|
||||
sub.display.toLowerCase().includes('english')
|
||||
);
|
||||
if (englishSubtitle) {
|
||||
loadStremioSubtitle(englishSubtitle);
|
||||
loadWyzieSubtitle(englishSubtitle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!autoSelectEnglish && uniqueSubtitles.length > 0) {
|
||||
if (!autoSelectEnglish) {
|
||||
setShowSubtitleLanguageModal(true);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[VideoPlayer] Error fetching subtitles from Stremio addons:', error);
|
||||
logger.error('[VideoPlayer] Error fetching subtitles from OpenSubtitles addon:', error);
|
||||
} finally {
|
||||
setIsLoadingSubtitleList(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadStremioSubtitle = async (subtitle: Subtitle) => {
|
||||
const loadWyzieSubtitle = async (subtitle: WyzieSubtitle) => {
|
||||
setShowSubtitleLanguageModal(false);
|
||||
setIsLoadingSubtitles(true);
|
||||
try {
|
||||
|
|
@ -966,14 +962,10 @@ const VideoPlayer: React.FC = () => {
|
|||
setCustomSubtitles(parsedCues);
|
||||
setUseCustomSubtitles(true);
|
||||
setSelectedTextTrack(-1);
|
||||
logger.log(`[VideoPlayer] Loaded subtitle: ${subtitle.lang} from ${subtitle.addonName}`);
|
||||
} catch (error) {
|
||||
logger.error('[VideoPlayer] Error loading Stremio subtitle:', error);
|
||||
logger.error('[VideoPlayer] Error loading Wyzie subtitle:', error);
|
||||
} finally {
|
||||
// Add a small delay to ensure state updates are processed
|
||||
setTimeout(() => {
|
||||
setIsLoadingSubtitles(false);
|
||||
}, 100);
|
||||
setIsLoadingSubtitles(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1013,10 +1005,8 @@ const VideoPlayer: React.FC = () => {
|
|||
currentTime >= cue.start && currentTime <= cue.end
|
||||
);
|
||||
const newSubtitle = currentCue ? currentCue.text : '';
|
||||
if (newSubtitle !== currentSubtitle) {
|
||||
setCurrentSubtitle(newSubtitle);
|
||||
}
|
||||
}, [currentTime, customSubtitles, useCustomSubtitles, currentSubtitle]);
|
||||
setCurrentSubtitle(newSubtitle);
|
||||
}, [currentTime, customSubtitles, useCustomSubtitles]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSubtitleSize();
|
||||
|
|
@ -1285,17 +1275,15 @@ const VideoPlayer: React.FC = () => {
|
|||
<VLCPlayer
|
||||
ref={vlcRef}
|
||||
style={[styles.video, customVideoStyles, { transform: [{ scale: zoomScale }] }]}
|
||||
source={(() => {
|
||||
// FORCEFULLY use headers from route params if available - no filtering or modification
|
||||
const sourceWithHeaders = headers ? {
|
||||
uri: currentStreamUrl,
|
||||
headers: headers
|
||||
} : { uri: currentStreamUrl };
|
||||
|
||||
// HTTP request logging removed
|
||||
|
||||
return sourceWithHeaders;
|
||||
})()}
|
||||
source={(() => {
|
||||
// FORCEFULLY use headers from route params if available - no filtering or modification
|
||||
const sourceWithHeaders = headers ? {
|
||||
uri: currentStreamUrl,
|
||||
headers: headers
|
||||
} : { uri: currentStreamUrl };
|
||||
|
||||
return sourceWithHeaders;
|
||||
})()}
|
||||
paused={paused}
|
||||
onProgress={handleProgress}
|
||||
onLoad={onLoad}
|
||||
|
|
@ -1388,7 +1376,7 @@ const VideoPlayer: React.FC = () => {
|
|||
subtitleSize={subtitleSize}
|
||||
subtitleBackground={subtitleBackground}
|
||||
fetchAvailableSubtitles={fetchAvailableSubtitles}
|
||||
loadStremioSubtitle={loadStremioSubtitle}
|
||||
loadWyzieSubtitle={loadWyzieSubtitle}
|
||||
selectTextTrack={selectTextTrack}
|
||||
increaseSubtitleSize={increaseSubtitleSize}
|
||||
decreaseSubtitleSize={decreaseSubtitleSize}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ import Animated, {
|
|||
SlideOutRight,
|
||||
} from 'react-native-reanimated';
|
||||
import { styles } from '../utils/playerStyles';
|
||||
import { SubtitleCue } from '../utils/playerTypes';
|
||||
import { Subtitle } from '../../../services/stremioService';
|
||||
import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes';
|
||||
import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils';
|
||||
|
||||
interface SubtitleModalsProps {
|
||||
|
|
@ -20,14 +19,14 @@ interface SubtitleModalsProps {
|
|||
isLoadingSubtitleList: boolean;
|
||||
isLoadingSubtitles: boolean;
|
||||
customSubtitles: SubtitleCue[];
|
||||
availableSubtitles: Subtitle[];
|
||||
availableSubtitles: WyzieSubtitle[];
|
||||
vlcTextTracks: Array<{id: number, name: string, language?: string}>;
|
||||
selectedTextTrack: number;
|
||||
useCustomSubtitles: boolean;
|
||||
subtitleSize: number;
|
||||
subtitleBackground: boolean;
|
||||
fetchAvailableSubtitles: () => void;
|
||||
loadStremioSubtitle: (subtitle: Subtitle) => void;
|
||||
loadWyzieSubtitle: (subtitle: WyzieSubtitle) => void;
|
||||
selectTextTrack: (trackId: number) => void;
|
||||
increaseSubtitleSize: () => void;
|
||||
decreaseSubtitleSize: () => void;
|
||||
|
|
@ -52,7 +51,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
subtitleSize,
|
||||
subtitleBackground,
|
||||
fetchAvailableSubtitles,
|
||||
loadStremioSubtitle,
|
||||
loadWyzieSubtitle,
|
||||
selectTextTrack,
|
||||
increaseSubtitleSize,
|
||||
decreaseSubtitleSize,
|
||||
|
|
@ -60,7 +59,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
}) => {
|
||||
// Track which specific online subtitle is currently loaded
|
||||
const [selectedOnlineSubtitleId, setSelectedOnlineSubtitleId] = React.useState<string | null>(null);
|
||||
// Track which subtitle is currently being loaded
|
||||
// Track which online subtitle is currently loading to show spinner per-item
|
||||
const [loadingSubtitleId, setLoadingSubtitleId] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
@ -76,30 +75,25 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
}
|
||||
}, [useCustomSubtitles]);
|
||||
|
||||
// Clear loading state when subtitles have finished loading
|
||||
React.useEffect(() => {
|
||||
if (!isLoadingSubtitles) {
|
||||
setLoadingSubtitleId(null);
|
||||
}
|
||||
}, [isLoadingSubtitles]);
|
||||
|
||||
// Only OpenSubtitles are provided now; render as a single list
|
||||
|
||||
const handleClose = () => {
|
||||
setShowSubtitleModal(false);
|
||||
};
|
||||
|
||||
const handleLoadStremioSubtitle = (subtitle: Subtitle) => {
|
||||
console.log('[SubtitleModals] Starting to load subtitle:', subtitle.id);
|
||||
setLoadingSubtitleId(subtitle.id);
|
||||
const handleLoadWyzieSubtitle = (subtitle: WyzieSubtitle) => {
|
||||
setSelectedOnlineSubtitleId(subtitle.id);
|
||||
loadStremioSubtitle(subtitle);
|
||||
setLoadingSubtitleId(subtitle.id);
|
||||
loadWyzieSubtitle(subtitle);
|
||||
};
|
||||
|
||||
// Clear loading state when subtitle loading is complete
|
||||
React.useEffect(() => {
|
||||
console.log('[SubtitleModals] isLoadingSubtitles changed:', isLoadingSubtitles);
|
||||
if (!isLoadingSubtitles) {
|
||||
console.log('[SubtitleModals] Clearing loadingSubtitleId');
|
||||
// Force clear loading state with a small delay to ensure proper re-render
|
||||
setTimeout(() => {
|
||||
setLoadingSubtitleId(null);
|
||||
console.log('[SubtitleModals] loadingSubtitleId cleared');
|
||||
}, 50);
|
||||
}
|
||||
}, [isLoadingSubtitles]);
|
||||
|
||||
// Main subtitle menu
|
||||
const renderSubtitleMenu = () => {
|
||||
if (!showSubtitleModal) return null;
|
||||
|
|
@ -398,57 +392,8 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{availableSubtitles.length > 0 ? (
|
||||
<View style={{ gap: 8 }}>
|
||||
{availableSubtitles.map((sub) => {
|
||||
const isSelected = useCustomSubtitles && selectedOnlineSubtitleId === sub.id;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={sub.id}
|
||||
style={{
|
||||
backgroundColor: isSelected ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)',
|
||||
}}
|
||||
onPress={() => {
|
||||
handleLoadStremioSubtitle(sub);
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
disabled={loadingSubtitleId === sub.id}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{
|
||||
color: '#FFFFFF',
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
marginBottom: 4,
|
||||
}}>
|
||||
{formatLanguage(sub.lang)}
|
||||
</Text>
|
||||
<Text style={{
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
fontSize: 13,
|
||||
}}>
|
||||
{sub.addonName || 'Stremio Addon'}
|
||||
</Text>
|
||||
</View>
|
||||
{loadingSubtitleId === sub.id ? (
|
||||
<ActivityIndicator size="small" color="#22C55E" />
|
||||
) : isSelected ? (
|
||||
<MaterialIcons name="check" size={20} color="#22C55E" />
|
||||
) : (
|
||||
<MaterialIcons name="download" size={20} color="rgba(255,255,255,0.4)" />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
) : !isLoadingSubtitleList ? (
|
||||
|
||||
{(availableSubtitles.length === 0) && !isLoadingSubtitleList ? (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
|
|
@ -472,7 +417,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
Tap to search online
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
) : isLoadingSubtitleList ? (
|
||||
<View style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: 16,
|
||||
|
|
@ -488,6 +433,47 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
Searching...
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={{ gap: 8 }}>
|
||||
{availableSubtitles.map((sub) => {
|
||||
const isSelected = useCustomSubtitles && selectedOnlineSubtitleId === sub.id;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={sub.id}
|
||||
style={{
|
||||
backgroundColor: isSelected ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)',
|
||||
}}
|
||||
onPress={() => {
|
||||
handleLoadWyzieSubtitle(sub);
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
disabled={isLoadingSubtitles}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{ color: '#FFFFFF', fontSize: 15, fontWeight: '500', marginBottom: 4 }}>
|
||||
{sub.display}
|
||||
</Text>
|
||||
<Text style={{ color: 'rgba(255, 255, 255, 0.6)', fontSize: 13 }}>
|
||||
{formatLanguage(sub.language)}
|
||||
</Text>
|
||||
</View>
|
||||
{(isLoadingSubtitles && loadingSubtitleId === sub.id) ? (
|
||||
<ActivityIndicator size="small" color="#22C55E" />
|
||||
) : isSelected ? (
|
||||
<MaterialIcons name="check" size={20} color="#22C55E" />
|
||||
) : (
|
||||
<MaterialIcons name="download" size={20} color="rgba(255,255,255,0.4)" />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
|
|
@ -557,4 +543,4 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default SubtitleModals;
|
||||
export default SubtitleModals;
|
||||
|
|
@ -71,4 +71,18 @@ export interface SubtitleCue {
|
|||
start: number;
|
||||
end: number;
|
||||
text: string;
|
||||
}
|
||||
}
|
||||
|
||||
// Add interface for Wyzie subtitle API response
|
||||
export interface WyzieSubtitle {
|
||||
id: string;
|
||||
url: string;
|
||||
flagUrl: string;
|
||||
format: string;
|
||||
encoding: string;
|
||||
media: string;
|
||||
display: string;
|
||||
language: string;
|
||||
isHearingImpaired: boolean;
|
||||
source: string;
|
||||
}
|
||||
Loading…
Reference in a new issue