diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index a96938f..1404911 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -271,6 +271,8 @@ const AndroidVideoPlayer: React.FC = () => { const [currentQuality, setCurrentQuality] = useState(quality); const [currentStreamProvider, setCurrentStreamProvider] = useState(streamProvider); const [currentStreamName, setCurrentStreamName] = useState(streamName); + const [lastAudioTrackCheck, setLastAudioTrackCheck] = useState(0); + const [audioTrackFallbackAttempts, setAudioTrackFallbackAttempts] = useState(0); const isMounted = useRef(true); const controlsTimeout = useRef(null); const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false); @@ -961,6 +963,54 @@ const AndroidVideoPlayer: React.FC = () => { const bufferedTime = data.playableDuration || currentTimeInSeconds; safeSetState(() => setBuffered(bufferedTime)); } + + // Periodic check for disabled audio track (every 3 seconds, max 3 attempts) + const now = Date.now(); + if (now - lastAudioTrackCheck > 3000 && !paused && duration > 0 && audioTrackFallbackAttempts < 3) { + setLastAudioTrackCheck(now); + + // Check if audio track is disabled (-1) and we have available tracks + if (selectedAudioTrack === -1 && rnVideoAudioTracks.length > 1) { + logger.warn('[AndroidVideoPlayer] Detected disabled audio track, attempting fallback'); + + // Find a fallback audio track (prefer stereo/standard formats) + const fallbackTrack = rnVideoAudioTracks.find((track, index) => { + const trackName = (track.name || '').toLowerCase(); + const trackLang = (track.language || '').toLowerCase(); + // Prefer stereo, AAC, or standard audio formats, avoid heavy codecs + return !trackName.includes('truehd') && + !trackName.includes('dts') && + !trackName.includes('dolby') && + !trackName.includes('atmos') && + !trackName.includes('7.1') && + !trackName.includes('5.1') && + index !== selectedAudioTrack; // Don't select the same track + }); + + if (fallbackTrack) { + const fallbackIndex = rnVideoAudioTracks.indexOf(fallbackTrack); + logger.warn(`[AndroidVideoPlayer] Switching to fallback audio track: ${fallbackTrack.name || 'Unknown'} (index: ${fallbackIndex})`); + + // Increment fallback attempts counter + setAudioTrackFallbackAttempts(prev => prev + 1); + + // Switch to fallback audio track + setSelectedAudioTrack(fallbackIndex); + + // Brief pause to allow track switching + setPaused(true); + setTimeout(() => { + if (isMounted.current) { + setPaused(false); + } + }, 500); + } else { + logger.warn('[AndroidVideoPlayer] No suitable fallback audio track found'); + // Increment attempts even if no fallback found to prevent infinite checking + setAudioTrackFallbackAttempts(prev => prev + 1); + } + } + } }; const onLoad = (data: any) => { @@ -1003,13 +1053,132 @@ const AndroidVideoPlayer: React.FC = () => { // Handle audio tracks if (data.audioTracks && data.audioTracks.length > 0) { + // Enhanced debug logging to see all available fields + if (DEBUG_MODE) { + logger.log(`[AndroidVideoPlayer] Raw audio tracks data:`, data.audioTracks); + data.audioTracks.forEach((track: any, idx: number) => { + logger.log(`[AndroidVideoPlayer] Track ${idx} raw data:`, { + index: track.index, + title: track.title, + language: track.language, + type: track.type, + channels: track.channels, + bitrate: track.bitrate, + codec: track.codec, + sampleRate: track.sampleRate, + name: track.name, + label: track.label, + allKeys: Object.keys(track), + fullTrackObject: track + }); + }); + } + const formattedAudioTracks = data.audioTracks.map((track: any, index: number) => { const trackIndex = track.index !== undefined ? track.index : index; - const trackName = track.title || track.language || `Audio ${index + 1}`; - const trackLanguage = track.language || 'Unknown'; + + // Build comprehensive track name from available fields + let trackName = ''; + const parts = []; + + // Add language if available (try multiple possible fields) + let language = track.language || track.lang || track.languageCode; + + // If no language field, try to extract from track name (e.g., "[Russian]", "[English]") + if ((!language || language === 'Unknown' || language === 'und' || language === '') && track.name) { + const languageMatch = track.name.match(/\[([^\]]+)\]/); + if (languageMatch && languageMatch[1]) { + language = languageMatch[1].trim(); + } + } + + if (language && language !== 'Unknown' && language !== 'und' && language !== '') { + parts.push(language.toUpperCase()); + } + + // Add codec information if available (try multiple possible fields) + const codec = track.type || track.codec || track.format; + if (codec && codec !== 'Unknown') { + parts.push(codec.toUpperCase()); + } + + // Add channel information if available + const channels = track.channels || track.channelCount; + if (channels && channels > 0) { + if (channels === 1) { + parts.push('MONO'); + } else if (channels === 2) { + parts.push('STEREO'); + } else if (channels === 6) { + parts.push('5.1CH'); + } else if (channels === 8) { + parts.push('7.1CH'); + } else { + parts.push(`${channels}CH`); + } + } + + // Add bitrate if available + const bitrate = track.bitrate || track.bitRate; + if (bitrate && bitrate > 0) { + parts.push(`${Math.round(bitrate / 1000)}kbps`); + } + + // Add sample rate if available + const sampleRate = track.sampleRate || track.sample_rate; + if (sampleRate && sampleRate > 0) { + parts.push(`${Math.round(sampleRate / 1000)}kHz`); + } + + // Add title if available and not generic + let title = track.title || track.name || track.label; + if (title && !title.match(/^(Audio|Track)\s*\d*$/i) && title !== 'Unknown') { + // Clean up title by removing language brackets and trailing punctuation + title = title.replace(/\s*\[[^\]]+\]\s*[-–—]*\s*$/, '').trim(); + if (title && title !== 'Unknown') { + parts.push(title); + } + } + + // Combine parts or fallback to generic name + if (parts.length > 0) { + trackName = parts.join(' • '); + } else { + // For simple track names like "Track 1", "Audio 1", etc., use them as-is + const simpleName = track.name || track.title || track.label; + if (simpleName && simpleName.match(/^(Track|Audio)\s*\d*$/i)) { + trackName = simpleName; + } else { + // Try to extract any meaningful info from the track object + const meaningfulFields: string[] = []; + Object.keys(track).forEach(key => { + const value = track[key]; + if (value && typeof value === 'string' && value !== 'Unknown' && value !== 'und' && value.length > 1) { + meaningfulFields.push(`${key}: ${value}`); + } + }); + + if (meaningfulFields.length > 0) { + trackName = `Audio ${index + 1} (${meaningfulFields.slice(0, 2).join(', ')})`; + } else { + trackName = `Audio ${index + 1}`; + } + } + } + + const trackLanguage = language || 'Unknown'; if (DEBUG_MODE) { - logger.log(`[AndroidVideoPlayer] Audio track ${index}: index=${trackIndex}, name="${trackName}", language="${trackLanguage}"`); + logger.log(`[AndroidVideoPlayer] Processed track ${index}:`, { + index: trackIndex, + name: trackName, + language: trackLanguage, + parts: parts, + meaningfulFields: Object.keys(track).filter(key => { + const value = track[key]; + return value && typeof value === 'string' && value !== 'Unknown' && value !== 'und' && value.length > 1; + }) + }); } return { @@ -1020,12 +1189,24 @@ const AndroidVideoPlayer: React.FC = () => { }); setRnVideoAudioTracks(formattedAudioTracks); - // Auto-select the first audio track if none is selected + // Auto-select English audio track if available, otherwise first track if (selectedAudioTrack === null && formattedAudioTracks.length > 0) { - const firstTrack = formattedAudioTracks[0]; - setSelectedAudioTrack(firstTrack.id); + // Look for English track first + const englishTrack = formattedAudioTracks.find((track: {id: number, name: string, language?: string}) => { + const lang = (track.language || '').toLowerCase(); + return lang === 'english' || lang === 'en' || lang === 'eng' || + (track.name && track.name.toLowerCase().includes('english')); + }); + + const selectedTrack = englishTrack || formattedAudioTracks[0]; + setSelectedAudioTrack(selectedTrack.id); + if (DEBUG_MODE) { - logger.log(`[AndroidVideoPlayer] Auto-selected first audio track: ${firstTrack.name} (ID: ${firstTrack.id})`); + if (englishTrack) { + logger.log(`[AndroidVideoPlayer] Auto-selected English audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`); + } else { + logger.log(`[AndroidVideoPlayer] No English track found, auto-selected first audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`); + } } } @@ -1047,6 +1228,10 @@ const AndroidVideoPlayer: React.FC = () => { setIsVideoLoaded(true); setIsPlayerReady(true); + // Reset audio track fallback attempts when new video loads + setAudioTrackFallbackAttempts(0); + setLastAudioTrackCheck(0); + // Start Trakt watching session when video loads with proper duration if (videoDuration > 0) { traktAutosync.handlePlaybackStart(currentTime, videoDuration); @@ -1245,27 +1430,33 @@ const AndroidVideoPlayer: React.FC = () => { return; } - // Check for Dolby Digital Plus audio codec errors (ExoPlayer) - const isDolbyCodecError = error?.error?.errorCode === '24001' || - error?.errorCode === '24001' || - (error?.error?.errorString && - error.error.errorString.includes('ERROR_CODE_DECODER_INIT_FAILED')) || - (error?.error?.errorException && - error.error.errorException.includes('audio/eac3')) || - (error?.error?.errorException && - error.error.errorException.includes('Dolby Digital Plus')); + // Check for audio codec errors (TrueHD, DTS, Dolby, etc.) + const isAudioCodecError = + error?.error?.errorCode === '24001' || + error?.errorCode === '24001' || + (error?.error?.errorString && + error.error.errorString.includes('ERROR_CODE_DECODER_INIT_FAILED')) || + (error?.error?.errorException && + error.error.errorException.includes('audio/eac3')) || + (error?.error?.errorException && + error.error.errorException.includes('Dolby Digital Plus')) || + (error?.message && /(trhd|truehd|true\s?hd|dts|dolby|atmos|e-ac3|ac3)/i.test(error.message)) || + (error?.error?.message && /(trhd|truehd|true\s?hd|dts|dolby|atmos|e-ac3|ac3)/i.test(error.error.message)) || + (error?.title && /codec not supported/i.test(error.title)); - // Handle Dolby Digital Plus codec errors with audio track fallback - if (isDolbyCodecError && rnVideoAudioTracks.length > 1) { - logger.warn('[AndroidVideoPlayer] Dolby Digital Plus codec error detected, attempting audio track fallback'); + // Handle audio codec errors with automatic fallback + if (isAudioCodecError && rnVideoAudioTracks.length > 1) { + logger.warn('[AndroidVideoPlayer] Audio codec error detected, attempting audio track fallback'); - // Find a non-Dolby audio track (usually index 0 is stereo/standard) + // Find a fallback audio track (prefer stereo/standard formats) const fallbackTrack = rnVideoAudioTracks.find((track, index) => { const trackName = (track.name || '').toLowerCase(); const trackLang = (track.language || '').toLowerCase(); - // Prefer stereo, AAC, or standard audio formats - return !trackName.includes('dolby') && + // Prefer stereo, AAC, or standard audio formats, avoid heavy codecs + return !trackName.includes('truehd') && !trackName.includes('dts') && + !trackName.includes('dolby') && + !trackName.includes('atmos') && !trackName.includes('7.1') && !trackName.includes('5.1') && index !== selectedAudioTrack; // Don't select the same track @@ -1415,8 +1606,8 @@ const AndroidVideoPlayer: React.FC = () => { // Format error details for user display let errorMessage = 'An unknown error occurred'; if (error) { - if (isDolbyCodecError) { - errorMessage = 'Audio codec compatibility issue detected. The video contains Dolby Digital Plus audio which is not supported on this device. Please try selecting a different audio track or use an alternative video source.'; + if (isAudioCodecError) { + errorMessage = 'Audio codec compatibility issue detected. The video contains unsupported audio codec (TrueHD/DTS/Dolby). Please try selecting a different audio track or use an alternative video source.'; } else if (isHeavyCodecDecoderError) { errorMessage = 'Audio codec issue (DTS/TrueHD/Atmos). Switching to a stereo/standard audio track may help.'; } else if (isServerConfigError) { diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx index 985f19c..33097bc 100644 --- a/src/components/player/VideoPlayer.tsx +++ b/src/components/player/VideoPlayer.tsx @@ -84,17 +84,6 @@ const VideoPlayer: React.FC = () => { // Use VideoPlayer (VLC) for: // - MKV files on iOS (unless forceVlc is set) const shouldUseAndroidPlayer = Platform.OS === 'android' || isXprimeStream || (Platform.OS === 'ios' && !isMkvFile && !forceVlc); - if (__DEV__) { - logger.log('[VideoPlayer] Player selection:', { - platform: Platform.OS, - isXprimeStream, - isMkvFile, - forceVlc: !!forceVlc, - selected: shouldUseAndroidPlayer ? 'AndroidVideoPlayer' : 'VLCPlayer', - streamProvider, - uri - }); - } if (shouldUseAndroidPlayer) { return ; } @@ -230,7 +219,6 @@ const VideoPlayer: React.FC = () => { try { // Always decode URLs for VLC as it has trouble with encoded characters const decoded = decodeURIComponent(url); - logger.log('[VideoPlayer] Decoded URL for VLC:', { original: url, decoded }); return decoded; } catch (e) { logger.warn('[VideoPlayer] URL decoding failed, using original:', e); @@ -247,6 +235,8 @@ const VideoPlayer: React.FC = () => { const [currentQuality, setCurrentQuality] = useState(quality); const [currentStreamProvider, setCurrentStreamProvider] = useState(streamProvider); const [currentStreamName, setCurrentStreamName] = useState(streamName); + const [lastAudioTrackCheck, setLastAudioTrackCheck] = useState(0); + const [audioTrackFallbackAttempts, setAudioTrackFallbackAttempts] = useState(0); const isMounted = useRef(true); const controlsTimeout = useRef(null); const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false); @@ -920,6 +910,54 @@ const VideoPlayer: React.FC = () => { const bufferedTime = event.bufferTime / 1000 || currentTimeInSeconds; safeSetState(() => setBuffered(bufferedTime)); } + + // Periodic check for disabled audio track (every 3 seconds, max 3 attempts) + const now = Date.now(); + if (now - lastAudioTrackCheck > 3000 && !paused && duration > 0 && audioTrackFallbackAttempts < 3) { + setLastAudioTrackCheck(now); + + // Check if audio track is disabled (-1) and we have available tracks + if (selectedAudioTrack === -1 && vlcAudioTracks.length > 1) { + logger.warn('[VideoPlayer] Detected disabled audio track, attempting fallback'); + + // Find a fallback audio track (prefer stereo/standard formats) + const fallbackTrack = vlcAudioTracks.find((track, index) => { + const trackName = (track.name || '').toLowerCase(); + const trackLang = (track.language || '').toLowerCase(); + // Prefer stereo, AAC, or standard audio formats, avoid heavy codecs + return !trackName.includes('truehd') && + !trackName.includes('dts') && + !trackName.includes('dolby') && + !trackName.includes('atmos') && + !trackName.includes('7.1') && + !trackName.includes('5.1') && + index !== selectedAudioTrack; // Don't select the same track + }); + + if (fallbackTrack) { + const fallbackIndex = vlcAudioTracks.indexOf(fallbackTrack); + logger.warn(`[VideoPlayer] Switching to fallback audio track: ${fallbackTrack.name || 'Unknown'} (index: ${fallbackIndex})`); + + // Increment fallback attempts counter + setAudioTrackFallbackAttempts(prev => prev + 1); + + // Switch to fallback audio track + setSelectedAudioTrack(fallbackIndex); + + // Brief pause to allow track switching + setPaused(true); + setTimeout(() => { + if (isMounted.current) { + setPaused(false); + } + }, 500); + } else { + logger.warn('[VideoPlayer] No suitable fallback audio track found'); + // Increment attempts even if no fallback found to prevent infinite checking + setAudioTrackFallbackAttempts(prev => prev + 1); + } + } + } }; const onLoad = (data: any) => { @@ -960,13 +998,132 @@ const VideoPlayer: React.FC = () => { } if (data.audioTracks && data.audioTracks.length > 0) { + // Enhanced debug logging to see all available fields + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Raw audio tracks data:`, data.audioTracks); + data.audioTracks.forEach((track: any, idx: number) => { + logger.log(`[VideoPlayer] Track ${idx} raw data:`, { + index: track.index, + title: track.title, + language: track.language, + type: track.type, + channels: track.channels, + bitrate: track.bitrate, + codec: track.codec, + sampleRate: track.sampleRate, + name: track.name, + label: track.label, + allKeys: Object.keys(track), + fullTrackObject: track + }); + }); + } + const formattedAudioTracks = data.audioTracks.map((track: any, index: number) => { const trackIndex = track.index !== undefined ? track.index : index; - const trackName = track.title || track.language || `Audio ${index + 1}`; - const trackLanguage = track.language || 'Unknown'; + + // Build comprehensive track name from available fields + let trackName = ''; + const parts = []; + + // Add language if available (try multiple possible fields) + let language = track.language || track.lang || track.languageCode; + + // If no language field, try to extract from track name (e.g., "[Russian]", "[English]") + if ((!language || language === 'Unknown' || language === 'und' || language === '') && track.name) { + const languageMatch = track.name.match(/\[([^\]]+)\]/); + if (languageMatch && languageMatch[1]) { + language = languageMatch[1].trim(); + } + } + + if (language && language !== 'Unknown' && language !== 'und' && language !== '') { + parts.push(language.toUpperCase()); + } + + // Add codec information if available (try multiple possible fields) + const codec = track.type || track.codec || track.format; + if (codec && codec !== 'Unknown') { + parts.push(codec.toUpperCase()); + } + + // Add channel information if available + const channels = track.channels || track.channelCount; + if (channels && channels > 0) { + if (channels === 1) { + parts.push('MONO'); + } else if (channels === 2) { + parts.push('STEREO'); + } else if (channels === 6) { + parts.push('5.1CH'); + } else if (channels === 8) { + parts.push('7.1CH'); + } else { + parts.push(`${channels}CH`); + } + } + + // Add bitrate if available + const bitrate = track.bitrate || track.bitRate; + if (bitrate && bitrate > 0) { + parts.push(`${Math.round(bitrate / 1000)}kbps`); + } + + // Add sample rate if available + const sampleRate = track.sampleRate || track.sample_rate; + if (sampleRate && sampleRate > 0) { + parts.push(`${Math.round(sampleRate / 1000)}kHz`); + } + + // Add title if available and not generic + let title = track.title || track.name || track.label; + if (title && !title.match(/^(Audio|Track)\s*\d*$/i) && title !== 'Unknown') { + // Clean up title by removing language brackets and trailing punctuation + title = title.replace(/\s*\[[^\]]+\]\s*[-–—]*\s*$/, '').trim(); + if (title && title !== 'Unknown') { + parts.push(title); + } + } + + // Combine parts or fallback to generic name + if (parts.length > 0) { + trackName = parts.join(' • '); + } else { + // For simple track names like "Track 1", "Audio 1", etc., use them as-is + const simpleName = track.name || track.title || track.label; + if (simpleName && simpleName.match(/^(Track|Audio)\s*\d*$/i)) { + trackName = simpleName; + } else { + // Try to extract any meaningful info from the track object + const meaningfulFields: string[] = []; + Object.keys(track).forEach(key => { + const value = track[key]; + if (value && typeof value === 'string' && value !== 'Unknown' && value !== 'und' && value.length > 1) { + meaningfulFields.push(`${key}: ${value}`); + } + }); + + if (meaningfulFields.length > 0) { + trackName = `Audio ${index + 1} (${meaningfulFields.slice(0, 2).join(', ')})`; + } else { + trackName = `Audio ${index + 1}`; + } + } + } + + const trackLanguage = language || 'Unknown'; if (DEBUG_MODE) { - logger.log(`[VideoPlayer] Audio track ${index}: index=${trackIndex}, name="${trackName}", language="${trackLanguage}"`); + logger.log(`[VideoPlayer] Processed track ${index}:`, { + index: trackIndex, + name: trackName, + language: trackLanguage, + parts: parts, + meaningfulFields: Object.keys(track).filter(key => { + const value = track[key]; + return value && typeof value === 'string' && value !== 'Unknown' && value !== 'und' && value.length > 1; + }) + }); } return { @@ -977,12 +1134,24 @@ const VideoPlayer: React.FC = () => { }); setVlcAudioTracks(formattedAudioTracks); - // Auto-select the first audio track if none is selected + // Auto-select English audio track if available, otherwise first track if (selectedAudioTrack === null && formattedAudioTracks.length > 0) { - const firstTrack = formattedAudioTracks[0]; - setSelectedAudioTrack(firstTrack.id); + // Look for English track first + const englishTrack = formattedAudioTracks.find((track: {id: number, name: string, language?: string}) => { + const lang = (track.language || '').toLowerCase(); + return lang === 'english' || lang === 'en' || lang === 'eng' || + (track.name && track.name.toLowerCase().includes('english')); + }); + + const selectedTrack = englishTrack || formattedAudioTracks[0]; + setSelectedAudioTrack(selectedTrack.id); + if (DEBUG_MODE) { - logger.log(`[VideoPlayer] Auto-selected first audio track: ${firstTrack.name} (ID: ${firstTrack.id})`); + if (englishTrack) { + logger.log(`[VideoPlayer] Auto-selected English audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`); + } else { + logger.log(`[VideoPlayer] No English track found, auto-selected first audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`); + } } } @@ -996,6 +1165,10 @@ const VideoPlayer: React.FC = () => { setIsVideoLoaded(true); setIsPlayerReady(true); + + // Reset audio track fallback attempts when new video loads + setAudioTrackFallbackAttempts(0); + setLastAudioTrackCheck(0); // Start Trakt watching session when video loads with proper duration if (videoDuration > 0) { @@ -1196,36 +1369,104 @@ const VideoPlayer: React.FC = () => { }; const handleError = (error: any) => { - logger.error('[VideoPlayer] Playback Error:', error); - - // Format error details for user display - let errorMessage = 'An unknown error occurred'; - if (error) { - if (typeof error === 'string') { - errorMessage = error; - } else if (error.message) { - errorMessage = error.message; - } else if (error.error && error.error.message) { - errorMessage = error.error.message; - } else if (error.code) { - errorMessage = `Error Code: ${error.code}`; - } else { - errorMessage = JSON.stringify(error, null, 2); + try { + logger.error('[VideoPlayer] Playback Error:', error); + + // Check for audio codec errors (TrueHD, DTS, Dolby, etc.) + const isAudioCodecError = + (error?.message && /(trhd|truehd|true\s?hd|dts|dolby|atmos|e-ac3|ac3)/i.test(error.message)) || + (error?.error?.message && /(trhd|truehd|true\s?hd|dts|dolby|atmos|e-ac3|ac3)/i.test(error.error.message)) || + (error?.title && /codec not supported/i.test(error.title)); + + // Handle audio codec errors with automatic fallback + if (isAudioCodecError && vlcAudioTracks.length > 1) { + logger.warn('[VideoPlayer] Audio codec error detected, attempting audio track fallback'); + + // Find a fallback audio track (prefer stereo/standard formats) + const fallbackTrack = vlcAudioTracks.find((track, index) => { + const trackName = (track.name || '').toLowerCase(); + const trackLang = (track.language || '').toLowerCase(); + // Prefer stereo, AAC, or standard audio formats, avoid heavy codecs + return !trackName.includes('truehd') && + !trackName.includes('dts') && + !trackName.includes('dolby') && + !trackName.includes('atmos') && + !trackName.includes('7.1') && + !trackName.includes('5.1') && + index !== selectedAudioTrack; // Don't select the same track + }); + + if (fallbackTrack) { + const fallbackIndex = vlcAudioTracks.indexOf(fallbackTrack); + logger.warn(`[VideoPlayer] Switching to fallback audio track: ${fallbackTrack.name || 'Unknown'} (index: ${fallbackIndex})`); + + // Clear any existing error state + if (errorTimeoutRef.current) { + clearTimeout(errorTimeoutRef.current); + errorTimeoutRef.current = null; + } + setShowErrorModal(false); + + // Switch to fallback audio track + setSelectedAudioTrack(fallbackIndex); + + // Brief pause to allow track switching + setPaused(true); + setTimeout(() => { + if (isMounted.current) { + setPaused(false); + } + }, 500); + + return; // Don't show error UI, attempt recovery + } + } + + // Format error details for user display + let errorMessage = 'An unknown error occurred'; + if (error) { + if (isAudioCodecError) { + errorMessage = 'Audio codec compatibility issue detected. The video contains unsupported audio codec (TrueHD/DTS/Dolby). Please try selecting a different audio track or use an alternative video source.'; + } else if (typeof error === 'string') { + errorMessage = error; + } else if (error.message) { + errorMessage = error.message; + } else if (error.error && error.error.message) { + errorMessage = error.error.message; + } else if (error.code) { + errorMessage = `Error Code: ${error.code}`; + } else { + errorMessage = JSON.stringify(error, null, 2); + } + } + + setErrorDetails(errorMessage); + setShowErrorModal(true); + + // Clear any existing timeout + if (errorTimeoutRef.current) { + clearTimeout(errorTimeoutRef.current); + } + + // Auto-exit after 5 seconds if user doesn't dismiss + errorTimeoutRef.current = setTimeout(() => { + handleErrorExit(); + }, 5000); + } catch (handlerError) { + // Fallback error handling to prevent crashes during error processing + logger.error('[VideoPlayer] Error in error handler:', handlerError); + if (isMounted.current) { + // Minimal safe error handling + setErrorDetails('A critical error occurred'); + setShowErrorModal(true); + // Force exit after 3 seconds if error handler itself fails + setTimeout(() => { + if (isMounted.current) { + handleClose(); + } + }, 3000); } } - - setErrorDetails(errorMessage); - setShowErrorModal(true); - - // Clear any existing timeout - if (errorTimeoutRef.current) { - clearTimeout(errorTimeoutRef.current); - } - - // Auto-exit after 5 seconds if user doesn't dismiss - errorTimeoutRef.current = setTimeout(() => { - handleErrorExit(); - }, 5000); }; const handleErrorExit = () => { diff --git a/src/components/player/utils/playerUtils.ts b/src/components/player/utils/playerUtils.ts index 354c2c4..d7c79a5 100644 --- a/src/components/player/utils/playerUtils.ts +++ b/src/components/player/utils/playerUtils.ts @@ -5,7 +5,7 @@ import { SubtitleCue } from './playerTypes'; // Debug flag - set back to false to disable verbose logging // WARNING: Setting this to true currently causes infinite render loops // Use selective logging instead if debugging is needed -export const DEBUG_MODE = false; +export const DEBUG_MODE = true; // Safer debug function that won't cause render loops // Call this with any debugging info you need instead of using inline DEBUG_MODE checks @@ -93,6 +93,14 @@ export const formatLanguage = (code?: string): string => { export const getTrackDisplayName = (track: { name?: string, id: number, language?: string }): string => { if (!track) return 'Unknown Track'; + // If no name, use track number + if (!track.name) return `Track ${track.id}`; + + // If the name is already well-formatted (contains • separators), use it as-is + if (track.name.includes('•')) { + return track.name; + } + // If we have a language field, use that for better display if (track.language && track.language !== 'Unknown') { const formattedLanguage = formatLanguage(track.language); @@ -101,9 +109,6 @@ export const getTrackDisplayName = (track: { name?: string, id: number, language } } - // If no name, use track number - if (!track.name) return `Track ${track.id}`; - // Try to extract language from name like "Some Info - [English]" const languageMatch = track.name.match(/\[(.*?)\]/); if (languageMatch && languageMatch[1]) { diff --git a/src/screens/AISettingsScreen.tsx b/src/screens/AISettingsScreen.tsx index 53df969..e6c555c 100644 --- a/src/screens/AISettingsScreen.tsx +++ b/src/screens/AISettingsScreen.tsx @@ -114,32 +114,34 @@ const AISettingsScreen: React.FC = () => { Linking.openURL('https://openrouter.ai/keys'); }; - const headerBaseHeight = Platform.OS === 'android' ? 80 : 60; - const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top; - const headerHeight = headerBaseHeight + topSpacing; - return ( - + {/* Header */} - - - navigation.goBack()} - style={styles.backButton} - > - - - - AI Assistant + + navigation.goBack()} + style={styles.backButton} + > + + + Settings + + + + {/* Empty for now, but ready for future actions */} + + + AI Assistant + { - + ); }; @@ -351,33 +353,45 @@ const styles = StyleSheet.create({ flex: 1, }, header: { - paddingHorizontal: Math.max(16, width * 0.05), - justifyContent: 'flex-end', - paddingBottom: 8, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8, }, - headerContent: { + backButton: { + flexDirection: 'row', + alignItems: 'center', + padding: 8, + }, + backText: { + fontSize: 17, + marginLeft: 8, + }, + headerActions: { flexDirection: 'row', alignItems: 'center', }, - backButton: { - marginRight: 16, + headerButton: { padding: 8, + marginLeft: 8, }, headerTitle: { - fontSize: Math.min(28, width * 0.07), - fontWeight: '800', - letterSpacing: 0.3, + fontSize: 34, + fontWeight: 'bold', + paddingHorizontal: 20, + marginBottom: 24, }, scrollView: { flex: 1, }, scrollContent: { - padding: Math.max(16, width * 0.05), paddingBottom: 40, }, infoCard: { borderRadius: 16, padding: 20, + marginHorizontal: 16, marginBottom: 20, }, infoHeader: { @@ -410,6 +424,7 @@ const styles = StyleSheet.create({ card: { borderRadius: 16, padding: 20, + marginHorizontal: 16, marginBottom: 20, }, cardTitle: { diff --git a/src/screens/HomeScreenSettings.tsx b/src/screens/HomeScreenSettings.tsx index d2270b7..9e4ad19 100644 --- a/src/screens/HomeScreenSettings.tsx +++ b/src/screens/HomeScreenSettings.tsx @@ -264,11 +264,19 @@ const HomeScreenSettings: React.FC = () => { size={24} color={isDarkMode ? colors.highEmphasis : colors.textDark} /> + + Settings + - - Home Screen Settings - + + + {/* Empty for now, but ready for future actions */} + + + + Home Screen Settings + {/* Saved indicator */} StyleSheet.create({ header: { flexDirection: 'row', alignItems: 'center', + justifyContent: 'space-between', paddingHorizontal: 16, - paddingVertical: 16, - paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 16 : 16, + paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8, backgroundColor: colors.darkBackground, }, backButton: { - padding: 4, + flexDirection: 'row', + alignItems: 'center', + padding: 8, + }, + backText: { + fontSize: 17, + marginLeft: 8, + color: colors.white, + }, + headerActions: { + flexDirection: 'row', + alignItems: 'center', + }, + headerButton: { + padding: 8, + marginLeft: 8, }, headerTitle: { - fontSize: 22, - fontWeight: '600', - marginLeft: 16, + fontSize: 34, + fontWeight: 'bold', + paddingHorizontal: 16, + marginBottom: 24, color: colors.white, }, headerRight: { @@ -685,10 +701,16 @@ const LogoSourceSettings = () => { hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > + Settings - Logo Source + + + {/* Empty for now, but ready for future actions */} + + Logo Source + { - + navigation.goBack()} > + + Settings + - Notification Settings - + + + {/* Empty for now, but ready for future actions */} + + + Notification Settings + + { size={24} color={currentTheme.colors.text} /> + + Settings + - - Video Player - + + + {/* Empty for now, but ready for future actions */} + + + + Video Player + StyleSheet.create({ backgroundColor: colors.background, }, header: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 16, - paddingTop: Platform.OS === 'ios' ? 44 : ANDROID_STATUSBAR_HEIGHT + 16, - paddingBottom: 16, - }, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8, + }, backButton: { flexDirection: 'row', alignItems: 'center', + padding: 8, + }, + headerActions: { + flexDirection: 'row', + alignItems: 'center', + }, + headerButton: { + padding: 8, + marginLeft: 8, }, backText: { fontSize: 17, @@ -472,16 +481,6 @@ const createStyles = (colors: any) => StyleSheet.create({ fontSize: 14, fontWeight: '500', }, - helpButton: { - position: 'absolute', - top: Platform.OS === 'ios' ? 44 : ANDROID_STATUSBAR_HEIGHT + 16, - right: 16, - backgroundColor: 'transparent', - borderRadius: 20, - padding: 8, - borderWidth: 1, - borderColor: colors.elevation3, - }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', @@ -1163,11 +1162,8 @@ const PluginsScreen: React.FC = () => { return ( - - + + {/* Header */} @@ -1179,13 +1175,15 @@ const PluginsScreen: React.FC = () => { Settings - {/* Help Button */} - setShowHelpModal(true)} - > - - + + {/* Help Button */} + setShowHelpModal(true)} + > + + + Plugins @@ -1769,7 +1767,7 @@ const PluginsScreen: React.FC = () => { - + ); }; diff --git a/src/screens/ThemeScreen.tsx b/src/screens/ThemeScreen.tsx index 53daeb7..6188449 100644 --- a/src/screens/ThemeScreen.tsx +++ b/src/screens/ThemeScreen.tsx @@ -12,6 +12,7 @@ import { Dimensions, StatusBar, FlatList, + SafeAreaView, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; @@ -438,42 +439,51 @@ const ThemeScreen: React.FC = () => { }; return ( - + - + ); } return ( - + + navigation.goBack()} > + + Settings + - App Themes + + + {/* Empty for now, but ready for future actions */} + + + App Themes + + {/* Category filter */} { /> - + ); }; @@ -554,18 +564,32 @@ const styles = StyleSheet.create({ header: { flexDirection: 'row', alignItems: 'center', - paddingHorizontal: 12, - paddingVertical: 8, + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight || 0 + 8 : 8, }, backButton: { - padding: 6, - borderRadius: 20, - backgroundColor: 'rgba(255, 255, 255, 0.1)', + flexDirection: 'row', + alignItems: 'center', + padding: 8, + }, + backText: { + fontSize: 17, + marginLeft: 8, + }, + headerActions: { + flexDirection: 'row', + alignItems: 'center', + }, + headerButton: { + padding: 8, + marginLeft: 8, }, headerTitle: { - fontSize: 18, + fontSize: 34, fontWeight: 'bold', - marginLeft: 12, + paddingHorizontal: 16, + marginBottom: 24, }, content: { flex: 1, diff --git a/src/screens/TraktSettingsScreen.tsx b/src/screens/TraktSettingsScreen.tsx index 34d678f..8696592 100644 --- a/src/screens/TraktSettingsScreen.tsx +++ b/src/screens/TraktSettingsScreen.tsx @@ -197,14 +197,19 @@ const TraktSettingsScreen: React.FC = () => { size={24} color={isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark} /> + + Settings + - - Trakt Settings - + + + {/* Empty for now, but ready for future actions */} + + + + Trakt Settings + { } }; - const headerBaseHeight = Platform.OS === 'android' ? 80 : 60; - const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top; - const headerHeight = headerBaseHeight + topSpacing; return ( - - - - - navigation.goBack()} - activeOpacity={0.7} - > - - - - App Updates + + + + navigation.goBack()} + activeOpacity={0.7} + > + + + Settings - + + + + {/* Empty for now, but ready for future actions */} + + + + App Updates + { )} - - + ); }; @@ -557,27 +560,34 @@ const styles = StyleSheet.create({ flex: 1, }, header: { - paddingHorizontal: Math.max(12, width * 0.04), flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingBottom: 8, - backgroundColor: 'transparent', - zIndex: 2, + paddingHorizontal: 16, + paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8, }, backButton: { + flexDirection: 'row', + alignItems: 'center', padding: 8, - marginLeft: -8, + }, + backText: { + fontSize: 17, + marginLeft: 8, + }, + headerActions: { + flexDirection: 'row', + alignItems: 'center', + }, + headerButton: { + padding: 8, + marginLeft: 8, }, headerTitle: { - fontSize: Math.min(24, width * 0.06), - fontWeight: '800', - letterSpacing: 0.3, - flex: 1, - textAlign: 'center', - }, - headerSpacer: { - width: 40, // Same width as back button to center the title + fontSize: 34, + fontWeight: 'bold', + paddingHorizontal: 16, + marginBottom: 24, }, contentContainer: { flex: 1, diff --git a/src/services/SyncService.ts b/src/services/SyncService.ts index 4d4b6dc..d86d5b8 100644 --- a/src/services/SyncService.ts +++ b/src/services/SyncService.ts @@ -292,7 +292,7 @@ class SyncService { migrations.push(moveKey('app_settings', `@user:${userId}:app_settings`)); } else if (k === '@user:local:app_settings') { migrations.push(moveKey(k, `@user:${userId}:app_settings`)); - } else if (k === '@user:local:stremio-addons') { + } else if (k === '@user:local:stremio-addons' || k === 'stremio-addons') { migrations.push(moveKey(k, `@user:${userId}:stremio-addons`)); } else if (k === '@user:local:stremio-addon-order') { migrations.push(moveKey(k, `@user:${userId}:stremio-addon-order`)); diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts index 9df97d5..8eec283 100644 --- a/src/services/stremioService.ts +++ b/src/services/stremioService.ts @@ -200,7 +200,10 @@ class StremioService { try { const scope = (await AsyncStorage.getItem('@user:current')) || 'local'; - const storedAddons = await AsyncStorage.getItem(`@user:${scope}:${this.STORAGE_KEY}`); + // Prefer scoped storage, but fall back to legacy keys to preserve older installs + let storedAddons = await AsyncStorage.getItem(`@user:${scope}:${this.STORAGE_KEY}`); + if (!storedAddons) storedAddons = await AsyncStorage.getItem(this.STORAGE_KEY); + if (!storedAddons) storedAddons = await AsyncStorage.getItem(`@user:local:${this.STORAGE_KEY}`); if (storedAddons) { const parsed = JSON.parse(storedAddons); @@ -375,7 +378,11 @@ class StremioService { try { const addonsArray = Array.from(this.installedAddons.values()); const scope = (await AsyncStorage.getItem('@user:current')) || 'local'; - await AsyncStorage.setItem(`@user:${scope}:${this.STORAGE_KEY}`, JSON.stringify(addonsArray)); + // Write to both scoped and legacy keys for compatibility + await Promise.all([ + AsyncStorage.setItem(`@user:${scope}:${this.STORAGE_KEY}`, JSON.stringify(addonsArray)), + AsyncStorage.setItem(this.STORAGE_KEY, JSON.stringify(addonsArray)), + ]); } catch (error) { // Continue even if save fails }