mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-26 11:02:53 +00:00
TEST
This commit is contained in:
parent
f44c0df168
commit
2dd2b7fc0b
5 changed files with 404 additions and 213 deletions
|
|
@ -189,7 +189,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const [duration, setDuration] = useState(0);
|
const [duration, setDuration] = useState(0);
|
||||||
const [showControls, setShowControls] = useState(true);
|
const [showControls, setShowControls] = useState(true);
|
||||||
const [audioTracks, setAudioTracks] = useState<AudioTrack[]>([]);
|
const [audioTracks, setAudioTracks] = useState<AudioTrack[]>([]);
|
||||||
const [selectedAudioTrack, setSelectedAudioTrack] = useState<number | null>(null);
|
const [selectedAudioTrack, setSelectedAudioTrack] = useState<SelectedTrack | null>({ type: SelectedTrackType.SYSTEM });
|
||||||
const [textTracks, setTextTracks] = useState<TextTrack[]>([]);
|
const [textTracks, setTextTracks] = useState<TextTrack[]>([]);
|
||||||
const [selectedTextTrack, setSelectedTextTrack] = useState<number>(-1);
|
const [selectedTextTrack, setSelectedTextTrack] = useState<number>(-1);
|
||||||
const [resizeMode, setResizeMode] = useState<ResizeModeType>('contain');
|
const [resizeMode, setResizeMode] = useState<ResizeModeType>('contain');
|
||||||
|
|
@ -280,6 +280,13 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const [errorDetails, setErrorDetails] = useState<string>('');
|
const [errorDetails, setErrorDetails] = useState<string>('');
|
||||||
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const errorTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// Toast state for codec unsupported notifications
|
||||||
|
const [showCodecToast, setShowCodecToast] = useState(false);
|
||||||
|
const [codecToastMessage, setCodecToastMessage] = useState<string>('');
|
||||||
|
const codecToastOpacity = useRef(new Animated.Value(0)).current;
|
||||||
|
const codecToastTranslateY = useRef(new Animated.Value(-20)).current;
|
||||||
|
const codecToastTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
// Volume and brightness controls
|
// Volume and brightness controls
|
||||||
const [volume, setVolume] = useState(1.0);
|
const [volume, setVolume] = useState(1.0);
|
||||||
const [brightness, setBrightness] = useState(1.0);
|
const [brightness, setBrightness] = useState(1.0);
|
||||||
|
|
@ -794,8 +801,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
clearInterval(progressSaveInterval);
|
clearInterval(progressSaveInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
// HEATING FIX: Increase sync interval to 15 seconds to reduce CPU load
|
// Sync interval for progress updates
|
||||||
const syncInterval = 15000; // 15 seconds to prevent heating
|
const syncInterval = 5000; // 5 seconds for responsive progress tracking
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
saveWatchProgress();
|
saveWatchProgress();
|
||||||
|
|
@ -969,33 +976,13 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
if (now - lastAudioTrackCheck > 3000 && !paused && duration > 0 && audioTrackFallbackAttempts < 3) {
|
if (now - lastAudioTrackCheck > 3000 && !paused && duration > 0 && audioTrackFallbackAttempts < 3) {
|
||||||
setLastAudioTrackCheck(now);
|
setLastAudioTrackCheck(now);
|
||||||
|
|
||||||
// Check if audio track is disabled (-1) and we have available tracks
|
// Check if audio track is disabled and we have available tracks
|
||||||
if (selectedAudioTrack === -1 && rnVideoAudioTracks.length > 1) {
|
if (selectedAudioTrack?.type === SelectedTrackType.DISABLED && rnVideoAudioTracks.length > 1) {
|
||||||
logger.warn('[AndroidVideoPlayer] Detected disabled audio track, attempting fallback');
|
logger.warn('[AndroidVideoPlayer] Detected disabled audio track, attempting fallback');
|
||||||
|
|
||||||
// Find a fallback audio track (prioritize AAC/stereo over heavy codecs)
|
// Find any available audio track
|
||||||
const fallbackTrack = rnVideoAudioTracks.find((track) => {
|
const fallbackTrack = rnVideoAudioTracks.find((track) => {
|
||||||
const trackName = (track.name || '').toLowerCase();
|
return track.id !== (selectedAudioTrack?.type === SelectedTrackType.INDEX ? selectedAudioTrack.value : null);
|
||||||
// Prefer AAC, stereo, or standard audio formats, avoid heavy codecs
|
|
||||||
const isCompatible = !trackName.includes('truehd') &&
|
|
||||||
!trackName.includes('dts') &&
|
|
||||||
!trackName.includes('atmos') &&
|
|
||||||
track.id !== selectedAudioTrack; // Don't select the same track
|
|
||||||
|
|
||||||
// Prioritize AAC and stereo tracks
|
|
||||||
const isPreferred = trackName.includes('aac') ||
|
|
||||||
trackName.includes('stereo') ||
|
|
||||||
trackName.includes('2.0') ||
|
|
||||||
trackName.includes('2ch');
|
|
||||||
|
|
||||||
return isCompatible && isPreferred;
|
|
||||||
}) || rnVideoAudioTracks.find((track) => {
|
|
||||||
// Fallback: any compatible track (even if not preferred)
|
|
||||||
const trackName = (track.name || '').toLowerCase();
|
|
||||||
return !trackName.includes('truehd') &&
|
|
||||||
!trackName.includes('dts') &&
|
|
||||||
!trackName.includes('atmos') &&
|
|
||||||
track.id !== selectedAudioTrack;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (fallbackTrack) {
|
if (fallbackTrack) {
|
||||||
|
|
@ -1004,8 +991,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
// Increment fallback attempts counter
|
// Increment fallback attempts counter
|
||||||
setAudioTrackFallbackAttempts(prev => prev + 1);
|
setAudioTrackFallbackAttempts(prev => prev + 1);
|
||||||
|
|
||||||
// Switch to fallback audio track
|
// Switch to manual track selection
|
||||||
setSelectedAudioTrack(fallbackTrack.id);
|
setSelectedAudioTrack({ type: SelectedTrackType.INDEX, value: fallbackTrack.id });
|
||||||
|
|
||||||
// Brief pause to allow track switching
|
// Brief pause to allow track switching
|
||||||
setPaused(true);
|
setPaused(true);
|
||||||
|
|
@ -1199,41 +1186,13 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
});
|
});
|
||||||
setRnVideoAudioTracks(formattedAudioTracks);
|
setRnVideoAudioTracks(formattedAudioTracks);
|
||||||
|
|
||||||
// Auto-select compatible audio track (prioritize AAC/stereo over heavy codecs)
|
// Use system auto-selection
|
||||||
if (selectedAudioTrack === null && formattedAudioTracks.length > 0) {
|
if (selectedAudioTrack?.type === SelectedTrackType.SYSTEM && formattedAudioTracks.length > 0) {
|
||||||
// First, try to find a compatible English track
|
|
||||||
const compatibleEnglishTrack = formattedAudioTracks.find((track: {id: number, name: string, language?: string}) => {
|
|
||||||
const lang = (track.language || '').toLowerCase();
|
|
||||||
const trackName = (track.name || '').toLowerCase();
|
|
||||||
const isEnglish = lang === 'english' || lang === 'en' || lang === 'eng' ||
|
|
||||||
(track.name && track.name.toLowerCase().includes('english'));
|
|
||||||
const isCompatible = !trackName.includes('truehd') &&
|
|
||||||
!trackName.includes('dts') &&
|
|
||||||
!trackName.includes('atmos');
|
|
||||||
return isEnglish && isCompatible;
|
|
||||||
});
|
|
||||||
|
|
||||||
// If no compatible English track, find any compatible track
|
|
||||||
const compatibleTrack = compatibleEnglishTrack || formattedAudioTracks.find((track: {id: number, name: string, language?: string}) => {
|
|
||||||
const trackName = (track.name || '').toLowerCase();
|
|
||||||
return !trackName.includes('truehd') &&
|
|
||||||
!trackName.includes('dts') &&
|
|
||||||
!trackName.includes('atmos');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fallback to any track if no compatible ones found
|
|
||||||
const selectedTrack = compatibleTrack || formattedAudioTracks[0];
|
|
||||||
setSelectedAudioTrack(selectedTrack.id);
|
|
||||||
|
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
if (compatibleEnglishTrack) {
|
logger.log(`[AndroidVideoPlayer] Using system auto-selection for ${formattedAudioTracks.length} audio tracks`);
|
||||||
logger.log(`[AndroidVideoPlayer] Auto-selected compatible English audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`);
|
logger.log(`[AndroidVideoPlayer] Available tracks:`, formattedAudioTracks.map((t: any) => `${t.name} (${t.language})`));
|
||||||
} else if (compatibleTrack) {
|
|
||||||
logger.log(`[AndroidVideoPlayer] Auto-selected compatible audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`);
|
|
||||||
} else {
|
|
||||||
logger.log(`[AndroidVideoPlayer] No compatible tracks found, auto-selected first audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Keep using system selection
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
|
|
@ -1474,68 +1433,92 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
if (isAudioCodecError && rnVideoAudioTracks.length > 0) {
|
if (isAudioCodecError && rnVideoAudioTracks.length > 0) {
|
||||||
logger.warn('[AndroidVideoPlayer] Audio codec error detected, attempting audio track fallback');
|
logger.warn('[AndroidVideoPlayer] Audio codec error detected, attempting audio track fallback');
|
||||||
|
|
||||||
// Find a fallback audio track (prioritize AAC/stereo over heavy codecs)
|
// Show toast notification about codec issue
|
||||||
const fallbackTrack = rnVideoAudioTracks.find((track) => {
|
showCodecUnsupportedToast('Audio codec not supported on this device. Switching to compatible track...');
|
||||||
const trackName = (track.name || '').toLowerCase();
|
|
||||||
// Prefer AAC, stereo, or standard audio formats, avoid heavy codecs
|
|
||||||
const isCompatible = !trackName.includes('truehd') &&
|
|
||||||
!trackName.includes('dts') &&
|
|
||||||
!trackName.includes('atmos') &&
|
|
||||||
track.id !== selectedAudioTrack; // Don't select the same track
|
|
||||||
|
|
||||||
// Prioritize AAC and stereo tracks
|
|
||||||
const isPreferred = trackName.includes('aac') ||
|
|
||||||
trackName.includes('stereo') ||
|
|
||||||
trackName.includes('2.0') ||
|
|
||||||
trackName.includes('2ch');
|
|
||||||
|
|
||||||
return isCompatible && isPreferred;
|
|
||||||
}) || rnVideoAudioTracks.find((track) => {
|
|
||||||
// Fallback: any compatible track (even if not preferred)
|
|
||||||
const trackName = (track.name || '').toLowerCase();
|
|
||||||
return !trackName.includes('truehd') &&
|
|
||||||
!trackName.includes('dts') &&
|
|
||||||
!trackName.includes('atmos') &&
|
|
||||||
track.id !== selectedAudioTrack;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fallbackTrack) {
|
// If using system selection failed, try manual selection
|
||||||
logger.warn(`[AndroidVideoPlayer] Switching to fallback audio track: ${fallbackTrack.name || 'Unknown'} (id: ${fallbackTrack.id})`);
|
if (selectedAudioTrack?.type === SelectedTrackType.SYSTEM) {
|
||||||
|
logger.log('[AndroidVideoPlayer] System audio selection failed, falling back to manual selection');
|
||||||
|
|
||||||
// Clear any existing error state
|
// Find any available audio track
|
||||||
if (errorTimeoutRef.current) {
|
const fallbackTrack = rnVideoAudioTracks[0];
|
||||||
clearTimeout(errorTimeoutRef.current);
|
|
||||||
errorTimeoutRef.current = null;
|
if (fallbackTrack) {
|
||||||
|
logger.warn(`[AndroidVideoPlayer] Switching to fallback audio track: ${fallbackTrack.name || 'Unknown'} (id: ${fallbackTrack.id})`);
|
||||||
|
|
||||||
|
// Clear any existing error state
|
||||||
|
if (errorTimeoutRef.current) {
|
||||||
|
clearTimeout(errorTimeoutRef.current);
|
||||||
|
errorTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
safeSetState(() => setShowErrorModal(false));
|
||||||
|
|
||||||
|
// Switch to manual track selection
|
||||||
|
setSelectedAudioTrack({ type: SelectedTrackType.INDEX, value: fallbackTrack.id });
|
||||||
|
|
||||||
|
// Brief pause to allow track switching
|
||||||
|
setPaused(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isMounted.current) return;
|
||||||
|
setPaused(false);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return; // Don't show error UI, attempt recovery
|
||||||
}
|
}
|
||||||
safeSetState(() => setShowErrorModal(false));
|
|
||||||
|
|
||||||
// Switch to fallback audio track
|
|
||||||
setSelectedAudioTrack(fallbackTrack.id);
|
|
||||||
|
|
||||||
// Brief pause to allow track switching
|
|
||||||
setPaused(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!isMounted.current) return;
|
|
||||||
setPaused(false);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return; // Don't show error UI, attempt recovery
|
|
||||||
} else {
|
|
||||||
// As a last resort, disable audio entirely to keep video playing
|
|
||||||
logger.warn('[AndroidVideoPlayer] No compatible audio track found. Disabling audio to keep playback.');
|
|
||||||
if (errorTimeoutRef.current) {
|
|
||||||
clearTimeout(errorTimeoutRef.current);
|
|
||||||
errorTimeoutRef.current = null;
|
|
||||||
}
|
|
||||||
safeSetState(() => setShowErrorModal(false));
|
|
||||||
setSelectedAudioTrack(-1); // handled as DISABLED by selectedTextTrack prop
|
|
||||||
setPaused(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!isMounted.current) return;
|
|
||||||
setPaused(false);
|
|
||||||
}, 300);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For manual selections that failed, try a different manual track
|
||||||
|
const currentTrackId = selectedAudioTrack?.type === SelectedTrackType.INDEX ? selectedAudioTrack.value : null;
|
||||||
|
if (currentTrackId !== null) {
|
||||||
|
const fallbackTrack = rnVideoAudioTracks.find((track) => {
|
||||||
|
return track.id !== currentTrackId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fallbackTrack) {
|
||||||
|
logger.warn(`[AndroidVideoPlayer] Switching from failed track ${currentTrackId} to: ${fallbackTrack.name || 'Unknown'} (id: ${fallbackTrack.id})`);
|
||||||
|
|
||||||
|
// Show toast notification about track switching
|
||||||
|
showCodecUnsupportedToast('Audio codec not supported. Trying alternative track...');
|
||||||
|
|
||||||
|
// Clear any existing error state
|
||||||
|
if (errorTimeoutRef.current) {
|
||||||
|
clearTimeout(errorTimeoutRef.current);
|
||||||
|
errorTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
safeSetState(() => setShowErrorModal(false));
|
||||||
|
|
||||||
|
// Switch to different manual track
|
||||||
|
setSelectedAudioTrack({ type: SelectedTrackType.INDEX, value: fallbackTrack.id });
|
||||||
|
|
||||||
|
// Brief pause to allow track switching
|
||||||
|
setPaused(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isMounted.current) return;
|
||||||
|
setPaused(false);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return; // Don't show error UI, attempt recovery
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// As a last resort, disable audio entirely to keep video playing
|
||||||
|
logger.warn('[AndroidVideoPlayer] No compatible audio track found. Disabling audio to keep playback.');
|
||||||
|
|
||||||
|
// Show toast notification about audio being disabled
|
||||||
|
showCodecUnsupportedToast('No compatible audio tracks found. Audio disabled to continue playback.');
|
||||||
|
|
||||||
|
if (errorTimeoutRef.current) {
|
||||||
|
clearTimeout(errorTimeoutRef.current);
|
||||||
|
errorTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
safeSetState(() => setShowErrorModal(false));
|
||||||
|
setSelectedAudioTrack({ type: SelectedTrackType.DISABLED });
|
||||||
|
setPaused(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!isMounted.current) return;
|
||||||
|
setPaused(false);
|
||||||
|
}, 300);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect Xprime provider to enable a one-shot silent retry (warms upstream/cache)
|
// Detect Xprime provider to enable a one-shot silent retry (warms upstream/cache)
|
||||||
|
|
@ -1649,7 +1632,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
(error?.error?.localizedDescription &&
|
(error?.error?.localizedDescription &&
|
||||||
error.error.localizedDescription.includes('server is not correctly configured'));
|
error.error.localizedDescription.includes('server is not correctly configured'));
|
||||||
|
|
||||||
// Expand audio decoder error detection to include DTS/TrueHD/Atmos families
|
// Audio decoder error detection
|
||||||
const isHeavyCodecDecoderError =
|
const isHeavyCodecDecoderError =
|
||||||
(error?.error?.errorString && /(dts|true\s?hd|truehd|atmos)/i.test(String(error.error.errorString))) ||
|
(error?.error?.errorString && /(dts|true\s?hd|truehd|atmos)/i.test(String(error.error.errorString))) ||
|
||||||
(error?.error?.errorException && /(dts|true\s?hd|truehd|atmos)/i.test(String(error.error.errorException)));
|
(error?.error?.errorException && /(dts|true\s?hd|truehd|atmos)/i.test(String(error.error.errorException)));
|
||||||
|
|
@ -1658,9 +1641,9 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
let errorMessage = 'An unknown error occurred';
|
let errorMessage = 'An unknown error occurred';
|
||||||
if (error) {
|
if (error) {
|
||||||
if (isAudioCodecError) {
|
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.';
|
errorMessage = 'Audio compatibility issue detected. Please try selecting a different audio track or use an alternative video source.';
|
||||||
} else if (isHeavyCodecDecoderError) {
|
} else if (isHeavyCodecDecoderError) {
|
||||||
errorMessage = 'Audio codec issue (DTS/TrueHD/Atmos). Switching to a stereo/standard audio track may help.';
|
errorMessage = 'Audio decoder issue detected. Please try selecting a different audio track.';
|
||||||
} else if (isServerConfigError) {
|
} else if (isServerConfigError) {
|
||||||
errorMessage = 'Stream server configuration issue. This may be a temporary problem with the video source.';
|
errorMessage = 'Stream server configuration issue. This may be a temporary problem with the video source.';
|
||||||
} else if (typeof error === 'string') {
|
} else if (typeof error === 'string') {
|
||||||
|
|
@ -1682,7 +1665,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For audio codec errors we already attempted recovery; avoid showing the modal
|
// For audio errors we already attempted recovery; avoid showing the modal
|
||||||
if (!isAudioCodecError) {
|
if (!isAudioCodecError) {
|
||||||
// Use safeSetState to prevent crashes on iOS when component is unmounted
|
// Use safeSetState to prevent crashes on iOS when component is unmounted
|
||||||
safeSetState(() => {
|
safeSetState(() => {
|
||||||
|
|
@ -1783,17 +1766,32 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAudioTrack = (trackId: number) => {
|
const selectAudioTrack = (trackSelection: SelectedTrack) => {
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
logger.log(`[AndroidVideoPlayer] Selecting audio track: ${trackId}`);
|
logger.log(`[AndroidVideoPlayer] Selecting audio track:`, trackSelection);
|
||||||
logger.log(`[AndroidVideoPlayer] Available tracks:`, rnVideoAudioTracks);
|
logger.log(`[AndroidVideoPlayer] Available tracks:`, rnVideoAudioTracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that the track exists
|
// Validate track selection
|
||||||
const trackExists = rnVideoAudioTracks.some(track => track.id === trackId);
|
if (trackSelection.type === SelectedTrackType.INDEX) {
|
||||||
if (!trackExists) {
|
const trackExists = rnVideoAudioTracks.some(track => track.id === trackSelection.value);
|
||||||
logger.error(`[AndroidVideoPlayer] Audio track ${trackId} not found in available tracks`);
|
if (!trackExists) {
|
||||||
return;
|
logger.error(`[AndroidVideoPlayer] Audio track ${trackSelection.value} not found in available tracks`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the selected track might have codec compatibility issues
|
||||||
|
const selectedTrack = rnVideoAudioTracks.find(track => track.id === trackSelection.value);
|
||||||
|
if (selectedTrack) {
|
||||||
|
const trackName = (selectedTrack.name || '').toLowerCase();
|
||||||
|
const hasHeavyCodec = trackName.includes('truehd') || trackName.includes('dts') || trackName.includes('atmos') ||
|
||||||
|
trackName.includes('eac3') || trackName.includes('dolby') || trackName.includes('hdma');
|
||||||
|
|
||||||
|
if (hasHeavyCodec) {
|
||||||
|
// Show toast warning about potential codec issues
|
||||||
|
showCodecUnsupportedToast(`Audio codec may not be supported on this device. Try selecting a different track if playback fails.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If changing tracks, briefly pause to allow smooth transition
|
// If changing tracks, briefly pause to allow smooth transition
|
||||||
|
|
@ -1803,10 +1801,10 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the new audio track
|
// Set the new audio track
|
||||||
setSelectedAudioTrack(trackId);
|
setSelectedAudioTrack(trackSelection);
|
||||||
|
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
logger.log(`[AndroidVideoPlayer] Audio track changed to: ${trackId}`);
|
logger.log(`[AndroidVideoPlayer] Audio track changed to:`, trackSelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resume playback after a brief delay if it was playing
|
// Resume playback after a brief delay if it was playing
|
||||||
|
|
@ -2347,6 +2345,9 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
if (brightnessOverlayTimeout.current) {
|
if (brightnessOverlayTimeout.current) {
|
||||||
clearTimeout(brightnessOverlayTimeout.current);
|
clearTimeout(brightnessOverlayTimeout.current);
|
||||||
}
|
}
|
||||||
|
if (codecToastTimeout.current) {
|
||||||
|
clearTimeout(codecToastTimeout.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -2356,6 +2357,55 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Function to show codec unsupported toast
|
||||||
|
const showCodecUnsupportedToast = (message: string) => {
|
||||||
|
if (!isMounted.current) return;
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (codecToastTimeout.current) {
|
||||||
|
clearTimeout(codecToastTimeout.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCodecToastMessage(message);
|
||||||
|
setShowCodecToast(true);
|
||||||
|
|
||||||
|
// Animate toast in
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.timing(codecToastOpacity, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 300,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(codecToastTranslateY, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 300,
|
||||||
|
useNativeDriver: true,
|
||||||
|
})
|
||||||
|
]).start();
|
||||||
|
|
||||||
|
// Auto-hide after 3 seconds
|
||||||
|
codecToastTimeout.current = setTimeout(() => {
|
||||||
|
if (isMounted.current) {
|
||||||
|
Animated.parallel([
|
||||||
|
Animated.timing(codecToastOpacity, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 250,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(codecToastTranslateY, {
|
||||||
|
toValue: -20,
|
||||||
|
duration: 250,
|
||||||
|
useNativeDriver: true,
|
||||||
|
})
|
||||||
|
]).start(() => {
|
||||||
|
if (isMounted.current) {
|
||||||
|
setShowCodecToast(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!useCustomSubtitles || customSubtitles.length === 0) {
|
if (!useCustomSubtitles || customSubtitles.length === 0) {
|
||||||
if (currentSubtitle !== '') {
|
if (currentSubtitle !== '') {
|
||||||
|
|
@ -2378,13 +2428,23 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
// Handle audio track changes with proper logging
|
// Handle audio track changes with proper logging
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedAudioTrack !== null && rnVideoAudioTracks.length > 0) {
|
if (selectedAudioTrack !== null && rnVideoAudioTracks.length > 0) {
|
||||||
const selectedTrack = rnVideoAudioTracks.find(track => track.id === selectedAudioTrack);
|
if (selectedAudioTrack.type === SelectedTrackType.INDEX && selectedAudioTrack.value !== undefined) {
|
||||||
if (selectedTrack) {
|
const selectedTrack = rnVideoAudioTracks.find(track => track.id === selectedAudioTrack.value);
|
||||||
if (DEBUG_MODE) {
|
if (selectedTrack) {
|
||||||
logger.log(`[AndroidVideoPlayer] Audio track selected: ${selectedTrack.name} (${selectedTrack.language}) - ID: ${selectedAudioTrack}`);
|
if (DEBUG_MODE) {
|
||||||
|
logger.log(`[AndroidVideoPlayer] Audio track selected: ${selectedTrack.name} (${selectedTrack.language}) - ID: ${selectedAudioTrack.value}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`[AndroidVideoPlayer] Selected audio track ${selectedAudioTrack.value} not found in available tracks`);
|
||||||
|
}
|
||||||
|
} else if (selectedAudioTrack.type === SelectedTrackType.SYSTEM) {
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
logger.log(`[AndroidVideoPlayer] Using system audio selection`);
|
||||||
|
}
|
||||||
|
} else if (selectedAudioTrack.type === SelectedTrackType.DISABLED) {
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
logger.log(`[AndroidVideoPlayer] Audio disabled`);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
logger.warn(`[AndroidVideoPlayer] Selected audio track ${selectedAudioTrack} not found in available tracks`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [selectedAudioTrack, rnVideoAudioTracks]);
|
}, [selectedAudioTrack, rnVideoAudioTracks]);
|
||||||
|
|
@ -2872,7 +2932,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
onBuffer(buf);
|
onBuffer(buf);
|
||||||
}}
|
}}
|
||||||
resizeMode={getVideoResizeMode(resizeMode)}
|
resizeMode={getVideoResizeMode(resizeMode)}
|
||||||
selectedAudioTrack={selectedAudioTrack !== null ? { type: SelectedTrackType.INDEX, value: selectedAudioTrack } : undefined}
|
selectedAudioTrack={selectedAudioTrack || undefined}
|
||||||
selectedTextTrack={useCustomSubtitles ? { type: SelectedTrackType.DISABLED } : (selectedTextTrack >= 0 ? { type: SelectedTrackType.INDEX, value: selectedTextTrack } : undefined)}
|
selectedTextTrack={useCustomSubtitles ? { type: SelectedTrackType.DISABLED } : (selectedTextTrack >= 0 ? { type: SelectedTrackType.INDEX, value: selectedTextTrack } : undefined)}
|
||||||
rate={1.0}
|
rate={1.0}
|
||||||
volume={volume}
|
volume={volume}
|
||||||
|
|
@ -2882,23 +2942,14 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
playWhenInactive={false}
|
playWhenInactive={false}
|
||||||
ignoreSilentSwitch="ignore"
|
ignoreSilentSwitch="ignore"
|
||||||
mixWithOthers="inherit"
|
mixWithOthers="inherit"
|
||||||
progressUpdateInterval={1000}
|
progressUpdateInterval={250}
|
||||||
// Remove artificial bit rate cap to allow high-bitrate streams (e.g., Blu-ray remux) to play
|
// Remove artificial bit rate cap to allow high-bitrate streams (e.g., Blu-ray remux) to play
|
||||||
// maxBitRate intentionally omitted
|
// maxBitRate intentionally omitted
|
||||||
disableFocus={true}
|
disableFocus={true}
|
||||||
// iOS AVPlayer startup tuning
|
// iOS AVPlayer optimization
|
||||||
automaticallyWaitsToMinimizeStalling={true as any}
|
|
||||||
preferredForwardBufferDuration={1 as any}
|
|
||||||
allowsExternalPlayback={false as any}
|
allowsExternalPlayback={false as any}
|
||||||
preventsDisplaySleepDuringVideoPlayback={true as any}
|
preventsDisplaySleepDuringVideoPlayback={true as any}
|
||||||
// ExoPlayer HLS optimization
|
// ExoPlayer HLS optimization - let the player use optimal defaults
|
||||||
bufferConfig={{
|
|
||||||
// Larger buffers for high-bitrate remuxes to reduce rebuffering/crashes
|
|
||||||
minBufferMs: 60000,
|
|
||||||
maxBufferMs: 180000,
|
|
||||||
bufferForPlaybackMs: 2500,
|
|
||||||
bufferForPlaybackAfterRebufferMs: 8000,
|
|
||||||
} as any}
|
|
||||||
// Use SurfaceView on Android to lower memory pressure with 4K/high-bitrate content
|
// Use SurfaceView on Android to lower memory pressure with 4K/high-bitrate content
|
||||||
useTextureView={Platform.OS === 'android' ? false : (undefined as any)}
|
useTextureView={Platform.OS === 'android' ? false : (undefined as any)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -3524,6 +3575,48 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* Codec Unsupported Toast */}
|
||||||
|
{showCodecToast && (
|
||||||
|
<Animated.View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 60 + insets.top,
|
||||||
|
left: 20 + insets.left,
|
||||||
|
right: 20 + insets.right,
|
||||||
|
opacity: codecToastOpacity,
|
||||||
|
transform: [{ translateY: codecToastTranslateY }],
|
||||||
|
zIndex: 2000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{
|
||||||
|
backgroundColor: 'rgba(255, 87, 34, 0.95)',
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 10,
|
||||||
|
borderLeftWidth: 4,
|
||||||
|
borderLeftColor: '#FF5722',
|
||||||
|
}}>
|
||||||
|
<MaterialIcons name="warning" size={20} color="#FFFFFF" style={{ marginRight: 12 }} />
|
||||||
|
<Text style={{
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
flex: 1,
|
||||||
|
lineHeight: 18,
|
||||||
|
}}>
|
||||||
|
{codecToastMessage}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
|
||||||
<AudioTrackModal
|
<AudioTrackModal
|
||||||
showAudioModal={showAudioModal}
|
showAudioModal={showAudioModal}
|
||||||
setShowAudioModal={setShowAudioModal}
|
setShowAudioModal={setShowAudioModal}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { View, Text, TouchableOpacity, Animated, StyleSheet, Platform } from 're
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import Slider from '@react-native-community/slider';
|
import Slider from '@react-native-community/slider';
|
||||||
|
import { SelectedTrack, SelectedTrackType } from 'react-native-video';
|
||||||
import { styles } from '../utils/playerStyles';
|
import { styles } from '../utils/playerStyles';
|
||||||
import { getTrackDisplayName } from '../utils/playerUtils';
|
import { getTrackDisplayName } from '../utils/playerUtils';
|
||||||
import { useTheme } from '../../../contexts/ThemeContext';
|
import { useTheme } from '../../../contexts/ThemeContext';
|
||||||
|
|
@ -24,7 +25,7 @@ interface PlayerControlsProps {
|
||||||
zoomScale: number;
|
zoomScale: number;
|
||||||
currentResizeMode?: string;
|
currentResizeMode?: string;
|
||||||
vlcAudioTracks: Array<{id: number, name: string, language?: string}>;
|
vlcAudioTracks: Array<{id: number, name: string, language?: string}>;
|
||||||
selectedAudioTrack: number | null;
|
selectedAudioTrack: SelectedTrack | null;
|
||||||
availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } };
|
availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } };
|
||||||
togglePlayback: () => void;
|
togglePlayback: () => void;
|
||||||
skip: (seconds: number) => void;
|
skip: (seconds: number) => void;
|
||||||
|
|
@ -177,8 +178,13 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||||
disabled={vlcAudioTracks.length <= 1}
|
disabled={vlcAudioTracks.length <= 1}
|
||||||
>
|
>
|
||||||
<Ionicons name="volume-high" size={20} color={vlcAudioTracks.length <= 1 ? 'grey' : 'white'} />
|
<Ionicons name="volume-high" size={20} color={vlcAudioTracks.length <= 1 ? 'grey' : 'white'} />
|
||||||
<Text style={[styles.bottomButtonText, vlcAudioTracks.length <= 1 && {color: 'grey'}]}>
|
<Text style={[styles.bottomButtonText, vlcAudioTracks.length <= 1 && {color: 'grey'}]} numberOfLines={1}>
|
||||||
{`Audio: ${getTrackDisplayName(vlcAudioTracks.find(t => t.id === selectedAudioTrack) || {id: -1, name: 'Default'})}`}
|
{(() => {
|
||||||
|
const trackName = getTrackDisplayName(vlcAudioTracks.find(t => t.id === (selectedAudioTrack?.type === SelectedTrackType.INDEX ? selectedAudioTrack.value : null)) || {id: -1, name: 'Default'});
|
||||||
|
// Truncate long audio track names to prevent UI cramping
|
||||||
|
const maxLength = 12; // Limit to 12 characters
|
||||||
|
return trackName.length > maxLength ? `${trackName.substring(0, maxLength)}...` : trackName;
|
||||||
|
})()}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,14 @@ import Animated, {
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { getTrackDisplayName, DEBUG_MODE } from '../utils/playerUtils';
|
import { getTrackDisplayName, DEBUG_MODE } from '../utils/playerUtils';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
|
import { SelectedTrack, SelectedTrackType } from 'react-native-video';
|
||||||
|
|
||||||
interface AudioTrackModalProps {
|
interface AudioTrackModalProps {
|
||||||
showAudioModal: boolean;
|
showAudioModal: boolean;
|
||||||
setShowAudioModal: (show: boolean) => void;
|
setShowAudioModal: (show: boolean) => void;
|
||||||
vlcAudioTracks: Array<{id: number, name: string, language?: string}>;
|
vlcAudioTracks: Array<{id: number, name: string, language?: string}>;
|
||||||
selectedAudioTrack: number | null;
|
selectedAudioTrack: SelectedTrack | null;
|
||||||
selectAudioTrack: (trackId: number) => void;
|
selectAudioTrack: (trackSelection: SelectedTrack) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
@ -35,13 +36,17 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
||||||
// Debug logging when modal opens
|
// Debug logging when modal opens
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (showAudioModal && DEBUG_MODE) {
|
if (showAudioModal && DEBUG_MODE) {
|
||||||
logger.log(`[AudioTrackModal] Modal opened with selectedAudioTrack: ${selectedAudioTrack}`);
|
logger.log(`[AudioTrackModal] Modal opened with selectedAudioTrack:`, selectedAudioTrack);
|
||||||
logger.log(`[AudioTrackModal] Available tracks:`, vlcAudioTracks);
|
logger.log(`[AudioTrackModal] Available tracks:`, vlcAudioTracks);
|
||||||
const selectedTrack = vlcAudioTracks.find(track => track.id === selectedAudioTrack);
|
if (selectedAudioTrack?.type === 'index' && selectedAudioTrack.value !== undefined) {
|
||||||
if (selectedTrack) {
|
const selectedTrack = vlcAudioTracks.find(track => track.id === selectedAudioTrack.value);
|
||||||
logger.log(`[AudioTrackModal] Selected track found: ${selectedTrack.name} (${selectedTrack.language})`);
|
if (selectedTrack) {
|
||||||
} else {
|
logger.log(`[AudioTrackModal] Selected track found: ${selectedTrack.name} (${selectedTrack.language})`);
|
||||||
logger.warn(`[AudioTrackModal] Selected track ${selectedAudioTrack} not found in available tracks`);
|
} else {
|
||||||
|
logger.warn(`[AudioTrackModal] Selected track ${selectedAudioTrack.value} not found in available tracks`);
|
||||||
|
}
|
||||||
|
} else if (selectedAudioTrack?.type === 'system') {
|
||||||
|
logger.log(`[AudioTrackModal] Using system auto-selection`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [showAudioModal, selectedAudioTrack, vlcAudioTracks]);
|
}, [showAudioModal, selectedAudioTrack, vlcAudioTracks]);
|
||||||
|
|
@ -146,15 +151,16 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
||||||
|
|
||||||
<View style={{ gap: 8 }}>
|
<View style={{ gap: 8 }}>
|
||||||
{vlcAudioTracks.map((track) => {
|
{vlcAudioTracks.map((track) => {
|
||||||
// If no track is selected, show the first track as selected
|
// Determine if track is selected
|
||||||
const isSelected = selectedAudioTrack === track.id ||
|
let isSelected = false;
|
||||||
(selectedAudioTrack === null && track.id === vlcAudioTracks[0]?.id);
|
if (selectedAudioTrack?.type === 'index' && selectedAudioTrack.value === track.id) {
|
||||||
|
isSelected = true;
|
||||||
|
} else if (selectedAudioTrack?.type === 'system' && track.id === vlcAudioTracks[0]?.id) {
|
||||||
|
// Show first track as selected when using system selection
|
||||||
|
isSelected = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if track uses unsupported codec
|
// All tracks are now available for selection
|
||||||
const trackName = (track.name || '').toLowerCase();
|
|
||||||
const isUnsupported = trackName.includes('truehd') ||
|
|
||||||
trackName.includes('dts') ||
|
|
||||||
trackName.includes('atmos');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|
@ -165,72 +171,43 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
||||||
padding: 16,
|
padding: 16,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)',
|
borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)',
|
||||||
opacity: isUnsupported ? 0.5 : 1,
|
|
||||||
}}
|
}}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (isUnsupported) {
|
|
||||||
if (DEBUG_MODE) {
|
|
||||||
logger.log(`[AudioTrackModal] Attempted to select unsupported track: ${track.id} (${track.name})`);
|
|
||||||
}
|
|
||||||
return; // Don't allow selection of unsupported tracks
|
|
||||||
}
|
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
logger.log(`[AudioTrackModal] Selecting track: ${track.id} (${track.name})`);
|
logger.log(`[AudioTrackModal] Selecting track: ${track.id} (${track.name})`);
|
||||||
}
|
}
|
||||||
selectAudioTrack(track.id);
|
selectAudioTrack({ type: SelectedTrackType.INDEX, value: track.id });
|
||||||
// Close modal after selection
|
// Close modal after selection
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setShowAudioModal(false);
|
setShowAudioModal(false);
|
||||||
}, 200);
|
}, 200);
|
||||||
}}
|
}}
|
||||||
activeOpacity={isUnsupported ? 1 : 0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4 }}>
|
||||||
<Text style={{
|
<Text style={{
|
||||||
color: isUnsupported ? 'rgba(255, 255, 255, 0.4)' : '#FFFFFF',
|
color: '#FFFFFF',
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}}>
|
}}>
|
||||||
{getTrackDisplayName(track)}
|
{getTrackDisplayName(track)}
|
||||||
</Text>
|
</Text>
|
||||||
{isUnsupported && (
|
|
||||||
<View style={{
|
|
||||||
backgroundColor: 'rgba(255, 107, 107, 0.2)',
|
|
||||||
borderRadius: 8,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 2,
|
|
||||||
marginLeft: 8,
|
|
||||||
}}>
|
|
||||||
<Text style={{
|
|
||||||
color: '#FF6B6B',
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: '600',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
}}>
|
|
||||||
Unsupported
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
{track.language && (
|
{track.language && (
|
||||||
<Text style={{
|
<Text style={{
|
||||||
color: isUnsupported ? 'rgba(255, 255, 255, 0.3)' : 'rgba(255, 255, 255, 0.6)',
|
color: 'rgba(255, 255, 255, 0.6)',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
}}>
|
}}>
|
||||||
{track.language.toUpperCase()}
|
{track.language.toUpperCase()}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
{isSelected && !isUnsupported && (
|
{isSelected && (
|
||||||
<MaterialIcons name="check" size={20} color="#22C55E" />
|
<MaterialIcons name="check" size={20} color="#22C55E" />
|
||||||
)}
|
)}
|
||||||
{isSelected && isUnsupported && (
|
|
||||||
<MaterialIcons name="warning" size={20} color="#FF6B6B" />
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,7 @@ class SyncService {
|
||||||
try {
|
try {
|
||||||
const u = await accountService.getCurrentUser();
|
const u = await accountService.getCurrentUser();
|
||||||
if (!u) return;
|
if (!u) return;
|
||||||
// Compare excluding preinstalled
|
// Compare excluding preinstalled addons
|
||||||
const exclude = new Set(['com.linvo.cinemeta', 'org.stremio.opensubtitlesv3']);
|
const exclude = new Set(['com.linvo.cinemeta', 'org.stremio.opensubtitlesv3']);
|
||||||
const localIds = new Set(
|
const localIds = new Set(
|
||||||
(await stremioService.getInstalledAddonsAsync())
|
(await stremioService.getInstalledAddonsAsync())
|
||||||
|
|
@ -651,7 +651,12 @@ class SyncService {
|
||||||
|
|
||||||
// Always include preinstalled regardless of server
|
// Always include preinstalled regardless of server
|
||||||
try { map.set('com.linvo.cinemeta', await stremioService.getManifest('https://v3-cinemeta.strem.io/manifest.json')); } catch {}
|
try { map.set('com.linvo.cinemeta', await stremioService.getManifest('https://v3-cinemeta.strem.io/manifest.json')); } catch {}
|
||||||
try { map.set('org.stremio.opensubtitlesv3', await stremioService.getManifest('https://opensubtitles-v3.strem.io/manifest.json')); } catch {}
|
|
||||||
|
// Only include OpenSubtitles if user hasn't explicitly removed it
|
||||||
|
const hasUserRemovedOpenSubtitles = await stremioService.hasUserRemovedAddon('org.stremio.opensubtitlesv3');
|
||||||
|
if (!hasUserRemovedOpenSubtitles) {
|
||||||
|
try { map.set('org.stremio.opensubtitlesv3', await stremioService.getManifest('https://opensubtitles-v3.strem.io/manifest.json')); } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
(stremioService as any).installedAddons = map;
|
(stremioService as any).installedAddons = map;
|
||||||
let order = (addons as any[]).map(a => a.addon_id);
|
let order = (addons as any[]).map(a => a.addon_id);
|
||||||
|
|
@ -661,7 +666,11 @@ class SyncService {
|
||||||
else if (idx > 0) { arr.splice(idx, 1); arr.unshift(id); }
|
else if (idx > 0) { arr.splice(idx, 1); arr.unshift(id); }
|
||||||
};
|
};
|
||||||
ensureFront(order, 'com.linvo.cinemeta');
|
ensureFront(order, 'com.linvo.cinemeta');
|
||||||
ensureFront(order, 'org.stremio.opensubtitlesv3');
|
|
||||||
|
// Only ensure OpenSubtitles is in order if user hasn't removed it
|
||||||
|
if (!hasUserRemovedOpenSubtitles) {
|
||||||
|
ensureFront(order, 'org.stremio.opensubtitlesv3');
|
||||||
|
}
|
||||||
// Prefer local order if it exists; otherwise use remote
|
// Prefer local order if it exists; otherwise use remote
|
||||||
try {
|
try {
|
||||||
const userScope = `@user:${userId}:stremio-addon-order`;
|
const userScope = `@user:${userId}:stremio-addon-order`;
|
||||||
|
|
@ -861,9 +870,21 @@ class SyncService {
|
||||||
.eq('user_id', userId);
|
.eq('user_id', userId);
|
||||||
if (!rErr && remote) {
|
if (!rErr && remote) {
|
||||||
const localIds = new Set(addons.map((a: any) => a.id));
|
const localIds = new Set(addons.map((a: any) => a.id));
|
||||||
const toDelete = (remote as any[])
|
const toDeletePromises = (remote as any[])
|
||||||
.map(r => r.addon_id as string)
|
.map(r => r.addon_id as string)
|
||||||
.filter(id => !localIds.has(id) && id !== 'com.linvo.cinemeta' && id !== 'org.stremio.opensubtitlesv3');
|
.map(async id => {
|
||||||
|
if (localIds.has(id)) return null; // Don't delete if still installed locally
|
||||||
|
if (id === 'com.linvo.cinemeta') return null; // Never delete Cinemeta
|
||||||
|
if (id === 'org.stremio.opensubtitlesv3') {
|
||||||
|
// Don't delete OpenSubtitles if user has explicitly removed it
|
||||||
|
const userRemoved = await stremioService.hasUserRemovedAddon(id);
|
||||||
|
return userRemoved ? null : id;
|
||||||
|
}
|
||||||
|
return id; // Delete other addons that are no longer installed locally
|
||||||
|
});
|
||||||
|
|
||||||
|
const toDeleteResults = await Promise.all(toDeletePromises);
|
||||||
|
const toDelete = toDeleteResults.filter(id => id !== null);
|
||||||
logger.log(`[Sync] push installed_addons deletions=${toDelete.length}`);
|
logger.log(`[Sync] push installed_addons deletions=${toDelete.length}`);
|
||||||
if (toDelete.length > 0) {
|
if (toDelete.length > 0) {
|
||||||
const del = await supabase
|
const del = await supabase
|
||||||
|
|
|
||||||
|
|
@ -267,9 +267,11 @@ class StremioService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure OpenSubtitles v3 is always installed as a pre-installed addon
|
// Install OpenSubtitles v3 by default unless user has explicitly removed it
|
||||||
const opensubsId = 'org.stremio.opensubtitlesv3';
|
const opensubsId = 'org.stremio.opensubtitlesv3';
|
||||||
if (!this.installedAddons.has(opensubsId)) {
|
const hasUserRemovedOpenSubtitles = await this.hasUserRemovedAddon(opensubsId);
|
||||||
|
|
||||||
|
if (!this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitles) {
|
||||||
try {
|
try {
|
||||||
const opensubsManifest = await this.getManifest('https://opensubtitles-v3.strem.io/manifest.json');
|
const opensubsManifest = await this.getManifest('https://opensubtitles-v3.strem.io/manifest.json');
|
||||||
this.installedAddons.set(opensubsId, opensubsManifest);
|
this.installedAddons.set(opensubsId, opensubsManifest);
|
||||||
|
|
@ -312,7 +314,10 @@ class StremioService {
|
||||||
if (!this.addonOrder.includes(cinemetaId) && this.installedAddons.has(cinemetaId)) {
|
if (!this.addonOrder.includes(cinemetaId) && this.installedAddons.has(cinemetaId)) {
|
||||||
this.addonOrder.push(cinemetaId);
|
this.addonOrder.push(cinemetaId);
|
||||||
}
|
}
|
||||||
if (!this.addonOrder.includes(opensubsId) && this.installedAddons.has(opensubsId)) {
|
|
||||||
|
// Only add OpenSubtitles to order if user hasn't removed it
|
||||||
|
const hasUserRemovedOpenSubtitlesOrder = await this.hasUserRemovedAddon(opensubsId);
|
||||||
|
if (!this.addonOrder.includes(opensubsId) && this.installedAddons.has(opensubsId) && !hasUserRemovedOpenSubtitlesOrder) {
|
||||||
this.addonOrder.push(opensubsId);
|
this.addonOrder.push(opensubsId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -435,6 +440,13 @@ class StremioService {
|
||||||
if (manifest && manifest.id) {
|
if (manifest && manifest.id) {
|
||||||
this.installedAddons.set(manifest.id, manifest);
|
this.installedAddons.set(manifest.id, manifest);
|
||||||
|
|
||||||
|
// If this is OpenSubtitles being reinstalled, remove it from the user removed list
|
||||||
|
if (manifest.id === 'org.stremio.opensubtitlesv3') {
|
||||||
|
await this.unmarkAddonAsRemovedByUser(manifest.id);
|
||||||
|
// Also clean up any storage references
|
||||||
|
await this.cleanupRemovedAddonFromStorage(manifest.id);
|
||||||
|
}
|
||||||
|
|
||||||
// Add to order if not already present (new addons go to the end)
|
// Add to order if not already present (new addons go to the end)
|
||||||
if (!this.addonOrder.includes(manifest.id)) {
|
if (!this.addonOrder.includes(manifest.id)) {
|
||||||
this.addonOrder.push(manifest.id);
|
this.addonOrder.push(manifest.id);
|
||||||
|
|
@ -460,6 +472,14 @@ class StremioService {
|
||||||
this.installedAddons.delete(id);
|
this.installedAddons.delete(id);
|
||||||
// Remove from order
|
// Remove from order
|
||||||
this.addonOrder = this.addonOrder.filter(addonId => addonId !== id);
|
this.addonOrder = this.addonOrder.filter(addonId => addonId !== id);
|
||||||
|
|
||||||
|
// Track if user explicitly removed OpenSubtitles
|
||||||
|
if (id === 'org.stremio.opensubtitlesv3') {
|
||||||
|
this.markAddonAsRemovedByUser(id);
|
||||||
|
// Also remove from any stored addon order to prevent it from being added back
|
||||||
|
this.cleanupRemovedAddonFromStorage(id);
|
||||||
|
}
|
||||||
|
|
||||||
this.saveInstalledAddons();
|
this.saveInstalledAddons();
|
||||||
this.saveAddonOrder();
|
this.saveAddonOrder();
|
||||||
try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {}
|
try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {}
|
||||||
|
|
@ -479,7 +499,10 @@ class StremioService {
|
||||||
if (!result.find(a => a.id === cinId) && this.installedAddons.has(cinId)) {
|
if (!result.find(a => a.id === cinId) && this.installedAddons.has(cinId)) {
|
||||||
result.unshift(this.installedAddons.get(cinId)!);
|
result.unshift(this.installedAddons.get(cinId)!);
|
||||||
}
|
}
|
||||||
|
// Only include OpenSubtitles if user hasn't explicitly removed it
|
||||||
if (!result.find(a => a.id === osId) && this.installedAddons.has(osId)) {
|
if (!result.find(a => a.id === osId) && this.installedAddons.has(osId)) {
|
||||||
|
// Check if user has removed OpenSubtitles (async check, but we'll handle it synchronously for now)
|
||||||
|
// For now, we'll rely on the fact that if user removed it, it shouldn't be in installedAddons
|
||||||
// Put OpenSubtitles right after Cinemeta if possible, else at start
|
// Put OpenSubtitles right after Cinemeta if possible, else at start
|
||||||
const cinIdx = result.findIndex(a => a.id === cinId);
|
const cinIdx = result.findIndex(a => a.id === cinId);
|
||||||
const osManifest = this.installedAddons.get(osId)!;
|
const osManifest = this.installedAddons.get(osId)!;
|
||||||
|
|
@ -502,6 +525,77 @@ class StremioService {
|
||||||
return id === 'com.linvo.cinemeta';
|
return id === 'com.linvo.cinemeta';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if user has explicitly removed an addon
|
||||||
|
async hasUserRemovedAddon(addonId: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const removedAddons = await AsyncStorage.getItem('user_removed_addons');
|
||||||
|
if (!removedAddons) return false;
|
||||||
|
const removedList = JSON.parse(removedAddons);
|
||||||
|
return Array.isArray(removedList) && removedList.includes(addonId);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark an addon as removed by user
|
||||||
|
private async markAddonAsRemovedByUser(addonId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const removedAddons = await AsyncStorage.getItem('user_removed_addons');
|
||||||
|
let removedList = removedAddons ? JSON.parse(removedAddons) : [];
|
||||||
|
if (!Array.isArray(removedList)) removedList = [];
|
||||||
|
|
||||||
|
if (!removedList.includes(addonId)) {
|
||||||
|
removedList.push(addonId);
|
||||||
|
await AsyncStorage.setItem('user_removed_addons', JSON.stringify(removedList));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Silently fail - this is not critical functionality
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove an addon from the user removed list (allows reinstallation)
|
||||||
|
async unmarkAddonAsRemovedByUser(addonId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const removedAddons = await AsyncStorage.getItem('user_removed_addons');
|
||||||
|
if (!removedAddons) return;
|
||||||
|
|
||||||
|
let removedList = JSON.parse(removedAddons);
|
||||||
|
if (!Array.isArray(removedList)) return;
|
||||||
|
|
||||||
|
const updatedList = removedList.filter(id => id !== addonId);
|
||||||
|
await AsyncStorage.setItem('user_removed_addons', JSON.stringify(updatedList));
|
||||||
|
} catch (error) {
|
||||||
|
// Silently fail - this is not critical functionality
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up removed addon from all storage locations
|
||||||
|
private async cleanupRemovedAddonFromStorage(addonId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
|
||||||
|
|
||||||
|
// Remove from all possible addon order storage keys
|
||||||
|
const keys = [
|
||||||
|
`@user:${scope}:${this.ADDON_ORDER_KEY}`,
|
||||||
|
this.ADDON_ORDER_KEY,
|
||||||
|
`@user:local:${this.ADDON_ORDER_KEY}`
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const storedOrder = await AsyncStorage.getItem(key);
|
||||||
|
if (storedOrder) {
|
||||||
|
const order = JSON.parse(storedOrder);
|
||||||
|
if (Array.isArray(order)) {
|
||||||
|
const updatedOrder = order.filter(id => id !== addonId);
|
||||||
|
await AsyncStorage.setItem(key, JSON.stringify(updatedOrder));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Silently fail - this is not critical functionality
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private formatId(id: string): string {
|
private formatId(id: string): string {
|
||||||
return id.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();
|
return id.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue