android player wordsplitting fix

This commit is contained in:
tapframe 2025-11-25 01:17:23 +05:30
parent ecaaaa66ed
commit 348cbf86d8

View file

@ -116,8 +116,8 @@ const AndroidVideoPlayer: React.FC = () => {
// Check if the stream is HLS (m3u8 playlist)
const isHlsStream = (url: string) => {
return url.includes('.m3u8') || url.includes('m3u8') ||
url.includes('hls') || url.includes('playlist') ||
(currentVideoType && currentVideoType.toLowerCase() === 'm3u8');
url.includes('hls') || url.includes('playlist') ||
(currentVideoType && currentVideoType.toLowerCase() === 'm3u8');
};
// HLS-specific headers for better ExoPlayer compatibility
@ -226,8 +226,8 @@ const AndroidVideoPlayer: React.FC = () => {
const [isBackdropLoaded, setIsBackdropLoaded] = useState(false);
const backdropImageOpacityAnim = useRef(new Animated.Value(0)).current;
const [isBuffering, setIsBuffering] = useState(false);
const [rnVideoAudioTracks, setRnVideoAudioTracks] = useState<Array<{id: number, name: string, language?: string}>>([]);
const [rnVideoTextTracks, setRnVideoTextTracks] = useState<Array<{id: number, name: string, language?: string}>>([]);
const [rnVideoAudioTracks, setRnVideoAudioTracks] = useState<Array<{ id: number, name: string, language?: string }>>([]);
const [rnVideoTextTracks, setRnVideoTextTracks] = useState<Array<{ id: number, name: string, language?: string }>>([]);
// Speed boost state for hold-to-speed-up feature
const [isSpeedBoosted, setIsSpeedBoosted] = useState(false);
@ -323,8 +323,8 @@ const AndroidVideoPlayer: React.FC = () => {
// VLC track state - will be managed by VlcVideoPlayer component
const [vlcAudioTracks, setVlcAudioTracks] = useState<Array<{id: number, name: string, language?: string}>>([]);
const [vlcSubtitleTracks, setVlcSubtitleTracks] = useState<Array<{id: number, name: string, language?: string}>>([]);
const [vlcAudioTracks, setVlcAudioTracks] = useState<Array<{ id: number, name: string, language?: string }>>([]);
const [vlcSubtitleTracks, setVlcSubtitleTracks] = useState<Array<{ id: number, name: string, language?: string }>>([]);
const [vlcSelectedAudioTrack, setVlcSelectedAudioTrack] = useState<number | undefined>(undefined);
const [vlcSelectedSubtitleTrack, setVlcSelectedSubtitleTrack] = useState<number | undefined>(undefined);
const [vlcRestoreTime, setVlcRestoreTime] = useState<number | undefined>(undefined); // Time to restore after remount
@ -357,8 +357,8 @@ const AndroidVideoPlayer: React.FC = () => {
useVLC
? (vlcSelectedAudioTrack ?? null)
: (selectedAudioTrack?.type === SelectedTrackType.INDEX && selectedAudioTrack.value !== undefined
? Number(selectedAudioTrack.value)
: null),
? Number(selectedAudioTrack.value)
: null),
[useVLC, vlcSelectedAudioTrack, selectedAudioTrack]
);
@ -629,7 +629,7 @@ const AndroidVideoPlayer: React.FC = () => {
const shouldLoadMetadata = Boolean(id && type);
const metadataResult = useMetadata({ id: id || 'placeholder', type: (type as any) });
const { settings: appSettings } = useSettings();
const { metadata, loading: metadataLoading, groupedEpisodes: metadataGroupedEpisodes, cast, loadCast } = shouldLoadMetadata ? (metadataResult as any) : { metadata: null, loading: false, groupedEpisodes: {}, cast: [], loadCast: () => {} };
const { metadata, loading: metadataLoading, groupedEpisodes: metadataGroupedEpisodes, cast, loadCast } = shouldLoadMetadata ? (metadataResult as any) : { metadata: null, loading: false, groupedEpisodes: {}, cast: [], loadCast: () => { } };
// Logo animation values
const logoScaleAnim = useRef(new Animated.Value(0.8)).current;
@ -823,7 +823,7 @@ const AndroidVideoPlayer: React.FC = () => {
return () => {
if (isSpeedBoosted) {
// best-effort restoration on unmount
try { setPlaybackSpeed(originalSpeed); } catch {}
try { setPlaybackSpeed(originalSpeed); } catch { }
}
};
}, [isSpeedBoosted, originalSpeed]);
@ -916,7 +916,7 @@ const AndroidVideoPlayer: React.FC = () => {
setVlcKey(`vlc-focus-${Date.now()}`);
}, 100);
}
return () => {};
return () => { };
}, [useVLC])
);
@ -1496,101 +1496,101 @@ const AndroidVideoPlayer: React.FC = () => {
}
}
// Handle text tracks
if (data.textTracks && data.textTracks.length > 0) {
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Raw text tracks data:`, data.textTracks);
data.textTracks.forEach((track: any, idx: number) => {
logger.log(`[AndroidVideoPlayer] Text Track ${idx} raw data:`, {
index: track.index,
title: track.title,
language: track.language,
type: track.type,
name: track.name,
label: track.label,
allKeys: Object.keys(track),
fullTrackObject: track
});
// Handle text tracks
if (data.textTracks && data.textTracks.length > 0) {
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Raw text tracks data:`, data.textTracks);
data.textTracks.forEach((track: any, idx: number) => {
logger.log(`[AndroidVideoPlayer] Text Track ${idx} raw data:`, {
index: track.index,
title: track.title,
language: track.language,
type: track.type,
name: track.name,
label: track.label,
allKeys: Object.keys(track),
fullTrackObject: track
});
}
const formattedTextTracks = data.textTracks.map((track: any, index: number) => {
const trackIndex = track.index !== undefined ? track.index : index;
// 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.title) {
const languageMatch = track.title.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.codec || track.format;
if (codec && codec !== 'Unknown' && codec !== 'und') {
parts.push(codec.toUpperCase());
}
// Add title if available and not generic
let title = track.title || track.name || track.label;
if (title && !title.match(/^(Subtitle|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", "Subtitle 1", etc., use them as-is
const simpleName = track.title || track.name || track.label;
if (simpleName && simpleName.match(/^(Track|Subtitle)\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 = meaningfulFields.join(' • ');
} else {
trackName = `Subtitle ${index + 1}`;
}
}
}
return {
id: trackIndex, // Use the actual track index from react-native-video
name: trackName,
language: language,
};
});
setRnVideoTextTracks(formattedTextTracks);
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Formatted text tracks:`, formattedTextTracks);
}
}
const formattedTextTracks = data.textTracks.map((track: any, index: number) => {
const trackIndex = track.index !== undefined ? track.index : index;
// 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.title) {
const languageMatch = track.title.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.codec || track.format;
if (codec && codec !== 'Unknown' && codec !== 'und') {
parts.push(codec.toUpperCase());
}
// Add title if available and not generic
let title = track.title || track.name || track.label;
if (title && !title.match(/^(Subtitle|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", "Subtitle 1", etc., use them as-is
const simpleName = track.title || track.name || track.label;
if (simpleName && simpleName.match(/^(Track|Subtitle)\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 = meaningfulFields.join(' • ');
} else {
trackName = `Subtitle ${index + 1}`;
}
}
}
return {
id: trackIndex, // Use the actual track index from react-native-video
name: trackName,
language: language,
};
});
setRnVideoTextTracks(formattedTextTracks);
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Formatted text tracks:`, formattedTextTracks);
}
}
setIsVideoLoaded(true);
setIsPlayerReady(true);
@ -1773,10 +1773,10 @@ const AndroidVideoPlayer: React.FC = () => {
const isTablet = Math.min(dw, dh) >= 768 || ((Platform as any).isPad === true);
if (!isTablet) {
setTimeout(() => {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => { });
}, 50);
} else {
ScreenOrientation.unlockAsync().catch(() => {});
ScreenOrientation.unlockAsync().catch(() => { });
}
disableImmersiveMode();
@ -1793,10 +1793,10 @@ const AndroidVideoPlayer: React.FC = () => {
const isTablet = Math.min(dw, dh) >= 768 || ((Platform as any).isPad === true);
if (!isTablet) {
setTimeout(() => {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => { });
}, 50);
} else {
ScreenOrientation.unlockAsync().catch(() => {});
ScreenOrientation.unlockAsync().catch(() => { });
}
disableImmersiveMode();
@ -1875,14 +1875,14 @@ const AndroidVideoPlayer: React.FC = () => {
// Check for codec errors that should trigger VLC fallback
const errorString = JSON.stringify(error || {});
const isCodecError = errorString.includes('MediaCodecVideoRenderer error') ||
errorString.includes('MediaCodecAudioRenderer error') ||
errorString.includes('NO_EXCEEDS_CAPABILITIES') ||
errorString.includes('NO_UNSUPPORTED_TYPE') ||
errorString.includes('Decoder failed') ||
errorString.includes('video/hevc') ||
errorString.includes('audio/eac3') ||
errorString.includes('ERROR_CODE_DECODING_FAILED') ||
errorString.includes('ERROR_CODE_DECODER_INIT_FAILED');
errorString.includes('MediaCodecAudioRenderer error') ||
errorString.includes('NO_EXCEEDS_CAPABILITIES') ||
errorString.includes('NO_UNSUPPORTED_TYPE') ||
errorString.includes('Decoder failed') ||
errorString.includes('video/hevc') ||
errorString.includes('audio/eac3') ||
errorString.includes('ERROR_CODE_DECODING_FAILED') ||
errorString.includes('ERROR_CODE_DECODER_INIT_FAILED');
// If it's a codec error and we're not already using VLC, silently switch to VLC
if (isCodecError && !useVLC && !vlcFallbackAttemptedRef.current) {
@ -1937,7 +1937,7 @@ const AndroidVideoPlayer: React.FC = () => {
// Check if this might be an HLS stream that needs different handling
const mightBeHls = currentStreamUrl.includes('.m3u8') || currentStreamUrl.includes('playlist') ||
currentStreamUrl.includes('hls') || currentStreamUrl.includes('stream');
currentStreamUrl.includes('hls') || currentStreamUrl.includes('stream');
if (mightBeHls) {
logger.warn(`[AndroidVideoPlayer] HLS stream format not recognized. Retrying with explicit HLS type and headers`);
@ -1982,9 +1982,9 @@ const AndroidVideoPlayer: React.FC = () => {
// Handle HLS manifest parsing errors (when content isn't actually M3U8)
const isManifestParseError = error?.error?.errorCode === '23002' ||
error?.errorCode === '23002' ||
(error?.error?.errorString &&
error.error.errorString.includes('ERROR_CODE_PARSING_MANIFEST_MALFORMED'));
error?.errorCode === '23002' ||
(error?.error?.errorString &&
error.error.errorString.includes('ERROR_CODE_PARSING_MANIFEST_MALFORMED'));
if (isManifestParseError && retryAttemptRef.current < 2) {
retryAttemptRef.current = 2;
@ -2010,9 +2010,9 @@ const AndroidVideoPlayer: React.FC = () => {
// Check for specific AVFoundation server configuration errors (iOS)
const isServerConfigError = error?.error?.code === -11850 ||
error?.code === -11850 ||
(error?.error?.localizedDescription &&
error.error.localizedDescription.includes('server is not correctly configured'));
error?.code === -11850 ||
(error?.error?.localizedDescription &&
error.error.localizedDescription.includes('server is not correctly configured'));
// Format error details for user display
let errorMessage = 'An unknown error occurred';
@ -2334,9 +2334,9 @@ const AndroidVideoPlayer: React.FC = () => {
try {
const merged = { ...(saved || {}), subtitleSize: migrated };
await storageService.saveSubtitleSettings(merged);
} catch {}
} catch { }
}
try { await mmkvStorage.removeItem(SUBTITLE_SIZE_KEY); } catch {}
try { await mmkvStorage.removeItem(SUBTITLE_SIZE_KEY); } catch { }
return;
}
// If no saved settings, use responsive default
@ -2518,7 +2518,7 @@ const AndroidVideoPlayer: React.FC = () => {
const textNow = cueNow ? cueNow.text : '';
setCurrentSubtitle(textNow);
logger.log('[AndroidVideoPlayer] currentSubtitle set immediately after apply (Android)');
} catch {}
} catch { }
}
} catch (error) {
logger.error('[AndroidVideoPlayer] Error loading Wyzie subtitle:', error);
@ -2834,33 +2834,28 @@ const AndroidVideoPlayer: React.FC = () => {
// Extract formatted segments from current cue
if (currentCue?.formattedSegments) {
// Split by newlines to get per-line segments
const lines = (currentCue.text || '').split(/\r?\n/);
const segmentsPerLine: SubtitleSegment[][] = [];
let segmentIndex = 0;
let currentLine: SubtitleSegment[] = [];
for (const line of lines) {
const lineSegments: SubtitleSegment[] = [];
const words = line.split(/(\s+)/);
for (const word of words) {
if (word.trim()) {
if (segmentIndex < currentCue.formattedSegments.length) {
lineSegments.push(currentCue.formattedSegments[segmentIndex]);
segmentIndex++;
} else {
// Fallback if segment count doesn't match
lineSegments.push({ text: word });
}
currentCue.formattedSegments.forEach(seg => {
const parts = seg.text.split(/\r?\n/);
parts.forEach((part, index) => {
if (index > 0) {
// New line found
segmentsPerLine.push(currentLine);
currentLine = [];
}
}
if (part.length > 0) {
currentLine.push({ ...seg, text: part });
}
});
});
if (lineSegments.length > 0) {
segmentsPerLine.push(lineSegments);
}
if (currentLine.length > 0) {
segmentsPerLine.push(currentLine);
}
setCurrentFormattedSegments(segmentsPerLine.length > 0 ? segmentsPerLine : []);
setCurrentFormattedSegments(segmentsPerLine);
} else {
setCurrentFormattedSegments([]);
}
@ -2914,8 +2909,8 @@ const AndroidVideoPlayer: React.FC = () => {
if (typeof saved.subtitleLineHeightMultiplier === 'number') setSubtitleLineHeightMultiplier(saved.subtitleLineHeightMultiplier);
if (typeof saved.subtitleOffsetSec === 'number') setSubtitleOffsetSec(saved.subtitleOffsetSec);
}
} catch {} finally {
try { setSubtitleSettingsLoaded(true); } catch {}
} catch { } finally {
try { setSubtitleSettingsLoaded(true); } catch { }
}
})();
}, []);
@ -3283,14 +3278,14 @@ const AndroidVideoPlayer: React.FC = () => {
/>
) : (
<Video
ref={videoRef}
style={[styles.video, customVideoStyles]}
source={{
uri: currentStreamUrl,
headers: headers || getStreamHeaders(),
type: isHlsStream(currentStreamUrl) ? 'm3u8' : (currentVideoType as any)
}}
paused={paused}
ref={videoRef}
style={[styles.video, customVideoStyles]}
source={{
uri: currentStreamUrl,
headers: headers || getStreamHeaders(),
type: isHlsStream(currentStreamUrl) ? 'm3u8' : (currentVideoType as any)
}}
paused={paused}
onLoadStart={() => {
logger.log('[AndroidVideoPlayer][RN Video] onLoadStart');
loadStartAtRef.current = Date.now();
@ -3305,54 +3300,54 @@ const AndroidVideoPlayer: React.FC = () => {
};
logger.log('[AndroidVideoPlayer][RN Video] Stream info:', streamInfo);
}}
onProgress={handleProgress}
onLoad={(e) => {
logger.log('[AndroidVideoPlayer][RN Video] Video loaded successfully');
logger.log('[AndroidVideoPlayer][RN Video] onLoad fired', { duration: e?.duration });
onLoad(e);
}}
onReadyForDisplay={() => {
firstFrameAtRef.current = Date.now();
const startedAt = loadStartAtRef.current;
if (startedAt) {
const deltaMs = firstFrameAtRef.current - startedAt;
logger.log(`[AndroidVideoPlayer] First frame ready after ${deltaMs} ms (${Platform.OS})`);
} else {
logger.log('[AndroidVideoPlayer] First frame ready (no start timestamp)');
}
}}
onSeek={onSeek}
onEnd={onEnd}
onError={(err) => {
logger.error('[AndroidVideoPlayer][RN Video] Encountered error:', err);
handleError(err);
}}
onBuffer={(buf) => {
logger.log('[AndroidVideoPlayer] onBuffer', buf);
onBuffer(buf);
}}
resizeMode={getVideoResizeMode(resizeMode)}
selectedAudioTrack={selectedAudioTrack || undefined}
selectedTextTrack={useCustomSubtitles ? { type: SelectedTrackType.DISABLED } : (selectedTextTrack >= 0 ? { type: SelectedTrackType.INDEX, value: selectedTextTrack } : undefined)}
rate={playbackSpeed}
volume={volume}
muted={false}
repeat={false}
playInBackground={false}
playWhenInactive={false}
ignoreSilentSwitch="ignore"
mixWithOthers="inherit"
progressUpdateInterval={500}
// Remove artificial bit rate cap to allow high-bitrate streams (e.g., Blu-ray remux) to play
// maxBitRate intentionally omitted
disableFocus={true}
// iOS AVPlayer optimization
allowsExternalPlayback={false as any}
preventsDisplaySleepDuringVideoPlayback={true as any}
// ExoPlayer HLS optimization - let the player use optimal defaults
// Use surfaceView on Android for improved compatibility
viewType={Platform.OS === 'android' ? ViewType.SURFACE : undefined}
/>
onProgress={handleProgress}
onLoad={(e) => {
logger.log('[AndroidVideoPlayer][RN Video] Video loaded successfully');
logger.log('[AndroidVideoPlayer][RN Video] onLoad fired', { duration: e?.duration });
onLoad(e);
}}
onReadyForDisplay={() => {
firstFrameAtRef.current = Date.now();
const startedAt = loadStartAtRef.current;
if (startedAt) {
const deltaMs = firstFrameAtRef.current - startedAt;
logger.log(`[AndroidVideoPlayer] First frame ready after ${deltaMs} ms (${Platform.OS})`);
} else {
logger.log('[AndroidVideoPlayer] First frame ready (no start timestamp)');
}
}}
onSeek={onSeek}
onEnd={onEnd}
onError={(err) => {
logger.error('[AndroidVideoPlayer][RN Video] Encountered error:', err);
handleError(err);
}}
onBuffer={(buf) => {
logger.log('[AndroidVideoPlayer] onBuffer', buf);
onBuffer(buf);
}}
resizeMode={getVideoResizeMode(resizeMode)}
selectedAudioTrack={selectedAudioTrack || undefined}
selectedTextTrack={useCustomSubtitles ? { type: SelectedTrackType.DISABLED } : (selectedTextTrack >= 0 ? { type: SelectedTrackType.INDEX, value: selectedTextTrack } : undefined)}
rate={playbackSpeed}
volume={volume}
muted={false}
repeat={false}
playInBackground={false}
playWhenInactive={false}
ignoreSilentSwitch="ignore"
mixWithOthers="inherit"
progressUpdateInterval={500}
// Remove artificial bit rate cap to allow high-bitrate streams (e.g., Blu-ray remux) to play
// maxBitRate intentionally omitted
disableFocus={true}
// iOS AVPlayer optimization
allowsExternalPlayback={false as any}
preventsDisplaySleepDuringVideoPlayback={true as any}
// ExoPlayer HLS optimization - let the player use optimal defaults
// Use surfaceView on Android for improved compatibility
viewType={Platform.OS === 'android' ? ViewType.SURFACE : undefined}
/>
)}
</TouchableOpacity>
</View>
@ -3402,7 +3397,7 @@ const AndroidVideoPlayer: React.FC = () => {
onSlidingComplete={handleSlidingComplete}
buffered={buffered}
formatTime={formatTime}
playerBackend={useVLC ? 'VLC' : 'ExoPlayer'}
playerBackend={useVLC ? 'VLC' : 'ExoPlayer'}
/>
{showPauseOverlay && (
@ -3433,7 +3428,7 @@ const AndroidVideoPlayer: React.FC = () => {
<LinearGradient
start={{ x: 0, y: 0.5 }}
end={{ x: 1, y: 0.5 }}
colors={[ 'rgba(0,0,0,0.85)', 'rgba(0,0,0,0.0)' ]}
colors={['rgba(0,0,0,0.85)', 'rgba(0,0,0,0.0)']}
locations={[0, 1]}
style={StyleSheet.absoluteFill}
/>
@ -3656,38 +3651,38 @@ const AndroidVideoPlayer: React.FC = () => {
marginRight: 8,
marginBottom: 8,
}}
onPress={() => {
setSelectedCastMember(castMember);
// Animate metadata out, then cast details in
Animated.parallel([
Animated.timing(metadataOpacity, {
toValue: 0,
duration: 250,
useNativeDriver: true,
}),
Animated.timing(metadataScale, {
toValue: 0.95,
duration: 250,
useNativeDriver: true,
})
]).start(() => {
setShowCastDetails(true);
// Animate cast details in
onPress={() => {
setSelectedCastMember(castMember);
// Animate metadata out, then cast details in
Animated.parallel([
Animated.timing(castDetailsOpacity, {
toValue: 1,
duration: 400,
Animated.timing(metadataOpacity, {
toValue: 0,
duration: 250,
useNativeDriver: true,
}),
Animated.spring(castDetailsScale, {
toValue: 1,
tension: 80,
friction: 8,
Animated.timing(metadataScale, {
toValue: 0.95,
duration: 250,
useNativeDriver: true,
})
]).start();
});
}}
]).start(() => {
setShowCastDetails(true);
// Animate cast details in
Animated.parallel([
Animated.timing(castDetailsOpacity, {
toValue: 1,
duration: 400,
useNativeDriver: true,
}),
Animated.spring(castDetailsScale, {
toValue: 1,
tension: 80,
friction: 8,
useNativeDriver: true,
})
]).start();
});
}}
>
<Text style={{
color: '#FFFFFF',
@ -3984,12 +3979,12 @@ const AndroidVideoPlayer: React.FC = () => {
<>
<AudioTrackModal
showAudioModal={showAudioModal}
setShowAudioModal={setShowAudioModal}
ksAudioTracks={useVLC ? vlcAudioTracks : rnVideoAudioTracks}
selectedAudioTrack={useVLC ? (vlcSelectedAudioTrack ?? null) : (selectedAudioTrack?.type === SelectedTrackType.INDEX && selectedAudioTrack.value !== undefined ? Number(selectedAudioTrack.value) : null)}
selectAudioTrack={selectAudioTrackById}
/>
showAudioModal={showAudioModal}
setShowAudioModal={setShowAudioModal}
ksAudioTracks={useVLC ? vlcAudioTracks : rnVideoAudioTracks}
selectedAudioTrack={useVLC ? (vlcSelectedAudioTrack ?? null) : (selectedAudioTrack?.type === SelectedTrackType.INDEX && selectedAudioTrack.value !== undefined ? Number(selectedAudioTrack.value) : null)}
selectAudioTrack={selectAudioTrackById}
/>
</>
<SubtitleModals
showSubtitleModal={showSubtitleModal}
@ -4089,91 +4084,91 @@ const AndroidVideoPlayer: React.FC = () => {
supportedOrientations={['landscape', 'portrait']}
statusBarTranslucent={true}
>
<View style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.8)'
}}>
<View style={{
backgroundColor: '#1a1a1a',
borderRadius: 14,
width: '85%',
maxHeight: '70%',
padding: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.25,
shadowRadius: 8,
elevation: 5,
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.8)'
}}>
<View style={{
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16
backgroundColor: '#1a1a1a',
borderRadius: 14,
width: '85%',
maxHeight: '70%',
padding: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.25,
shadowRadius: 8,
elevation: 5,
}}>
<MaterialIcons name="error" size={24} color="#ff4444" style={{ marginRight: 8 }} />
<View style={{
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16
}}>
<MaterialIcons name="error" size={24} color="#ff4444" style={{ marginRight: 8 }} />
<Text style={{
fontSize: 18,
fontWeight: 'bold',
color: '#ffffff',
flex: 1
}}>Playback Error</Text>
<TouchableOpacity onPress={handleErrorExit}>
<MaterialIcons name="close" size={24} color="#ffffff" />
</TouchableOpacity>
</View>
<Text style={{
fontSize: 18,
fontWeight: 'bold',
color: '#ffffff',
flex: 1
}}>Playback Error</Text>
<TouchableOpacity onPress={handleErrorExit}>
<MaterialIcons name="close" size={24} color="#ffffff" />
</TouchableOpacity>
</View>
fontSize: 14,
color: '#cccccc',
marginBottom: 16,
lineHeight: 20
}}>The video player encountered an error and cannot continue playback:</Text>
<Text style={{
fontSize: 14,
color: '#cccccc',
marginBottom: 16,
lineHeight: 20
}}>The video player encountered an error and cannot continue playback:</Text>
<View style={{
backgroundColor: '#2a2a2a',
borderRadius: 8,
padding: 12,
marginBottom: 20,
maxHeight: 200
}}>
<Text style={{
fontSize: 12,
color: '#ff8888',
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace'
}}>{errorDetails}</Text>
</View>
<View style={{
flexDirection: 'row',
justifyContent: 'flex-end'
}}>
<TouchableOpacity
style={{
backgroundColor: '#ff4444',
borderRadius: 8,
paddingVertical: 10,
paddingHorizontal: 20
}}
onPress={handleErrorExit}
>
<Text style={{
color: '#ffffff',
fontWeight: '600',
fontSize: 16
}}>Exit Player</Text>
</TouchableOpacity>
</View>
<View style={{
backgroundColor: '#2a2a2a',
borderRadius: 8,
padding: 12,
marginBottom: 20,
maxHeight: 200
}}>
<Text style={{
fontSize: 12,
color: '#ff8888',
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace'
}}>{errorDetails}</Text>
color: '#888888',
textAlign: 'center',
marginTop: 12
}}>This dialog will auto-close in 5 seconds</Text>
</View>
<View style={{
flexDirection: 'row',
justifyContent: 'flex-end'
}}>
<TouchableOpacity
style={{
backgroundColor: '#ff4444',
borderRadius: 8,
paddingVertical: 10,
paddingHorizontal: 20
}}
onPress={handleErrorExit}
>
<Text style={{
color: '#ffffff',
fontWeight: '600',
fontSize: 16
}}>Exit Player</Text>
</TouchableOpacity>
</View>
<Text style={{
fontSize: 12,
color: '#888888',
textAlign: 'center',
marginTop: 12
}}>This dialog will auto-close in 5 seconds</Text>
</View>
</View>
</Modal>
)}
</View>