ksplayer word splitting fix

This commit is contained in:
tapframe 2025-11-25 01:15:41 +05:30
parent 0ab85ec870
commit ecaaaa66ed
3 changed files with 289 additions and 291 deletions

View file

@ -328,7 +328,7 @@ const KSPlayerCore: React.FC = () => {
id: id || 'placeholder', id: id || 'placeholder',
type: type || 'movie' type: type || 'movie'
}); });
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: () => { } };
const { settings } = useSettings(); const { settings } = useSettings();
// Logo animation values // Logo animation values
@ -559,7 +559,7 @@ const KSPlayerCore: React.FC = () => {
useEffect(() => { useEffect(() => {
return () => { return () => {
if (isSpeedBoosted) { if (isSpeedBoosted) {
try { setPlaybackSpeed(originalSpeed); } catch {} try { setPlaybackSpeed(originalSpeed); } catch { }
} }
}; };
}, [isSpeedBoosted, originalSpeed]); }, [isSpeedBoosted, originalSpeed]);
@ -648,7 +648,7 @@ const KSPlayerCore: React.FC = () => {
if (isOpeningAnimationComplete) { if (isOpeningAnimationComplete) {
enableImmersiveMode(); enableImmersiveMode();
} }
return () => {}; return () => { };
}, [isOpeningAnimationComplete]) }, [isOpeningAnimationComplete])
); );
@ -1011,12 +1011,12 @@ const KSPlayerCore: React.FC = () => {
const trackLang = (track.language || '').toLowerCase(); const trackLang = (track.language || '').toLowerCase();
// Prefer stereo, AAC, or standard audio formats, avoid heavy codecs // Prefer stereo, AAC, or standard audio formats, avoid heavy codecs
return !trackName.includes('truehd') && return !trackName.includes('truehd') &&
!trackName.includes('dts') && !trackName.includes('dts') &&
!trackName.includes('dolby') && !trackName.includes('dolby') &&
!trackName.includes('atmos') && !trackName.includes('atmos') &&
!trackName.includes('7.1') && !trackName.includes('7.1') &&
!trackName.includes('5.1') && !trackName.includes('5.1') &&
index !== selectedAudioTrack; // Don't select the same track index !== selectedAudioTrack; // Don't select the same track
}); });
if (fallbackTrack) { if (fallbackTrack) {
@ -1203,10 +1203,10 @@ const KSPlayerCore: React.FC = () => {
// Auto-select English audio track if available, otherwise first track // Auto-select English audio track if available, otherwise first track
if (selectedAudioTrack === null && formattedAudioTracks.length > 0) { if (selectedAudioTrack === null && formattedAudioTracks.length > 0) {
// Look for English track first // Look for English track first
const englishTrack = formattedAudioTracks.find((track: {id: number, name: string, language?: string}) => { const englishTrack = formattedAudioTracks.find((track: { id: number, name: string, language?: string }) => {
const lang = (track.language || '').toLowerCase(); const lang = (track.language || '').toLowerCase();
return lang === 'english' || lang === 'en' || lang === 'eng' || return lang === 'english' || lang === 'en' || lang === 'eng' ||
(track.name && track.name.toLowerCase().includes('english')); (track.name && track.name.toLowerCase().includes('english'));
}); });
const selectedTrack = englishTrack || formattedAudioTracks[0]; const selectedTrack = englishTrack || formattedAudioTracks[0];
@ -1248,7 +1248,7 @@ const KSPlayerCore: React.FC = () => {
const lang = (track.language || '').toLowerCase(); const lang = (track.language || '').toLowerCase();
const name = (track.name || '').toLowerCase(); const name = (track.name || '').toLowerCase();
return lang === 'english' || lang === 'en' || lang === 'eng' || return lang === 'english' || lang === 'en' || lang === 'eng' ||
name.includes('english') || name.includes('en'); name.includes('english') || name.includes('en');
}); });
if (englishTrack) { if (englishTrack) {
@ -1393,9 +1393,9 @@ const KSPlayerCore: React.FC = () => {
const isTablet = (Platform as any).isPad === true || Math.min(dw, dh) >= 768; const isTablet = (Platform as any).isPad === true || Math.min(dw, dh) >= 768;
setTimeout(() => { setTimeout(() => {
if (isTablet) { if (isTablet) {
ScreenOrientation.unlockAsync().catch(() => {}); ScreenOrientation.unlockAsync().catch(() => { });
} else { } else {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {}); ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => { });
} }
}, 50); }, 50);
} }
@ -1538,12 +1538,12 @@ const KSPlayerCore: React.FC = () => {
const trackLang = (track.language || '').toLowerCase(); const trackLang = (track.language || '').toLowerCase();
// Prefer stereo, AAC, or standard audio formats, avoid heavy codecs // Prefer stereo, AAC, or standard audio formats, avoid heavy codecs
return !trackName.includes('truehd') && return !trackName.includes('truehd') &&
!trackName.includes('dts') && !trackName.includes('dts') &&
!trackName.includes('dolby') && !trackName.includes('dolby') &&
!trackName.includes('atmos') && !trackName.includes('atmos') &&
!trackName.includes('7.1') && !trackName.includes('7.1') &&
!trackName.includes('5.1') && !trackName.includes('5.1') &&
index !== selectedAudioTrack; // Don't select the same track index !== selectedAudioTrack; // Don't select the same track
}); });
if (fallbackTrack) { if (fallbackTrack) {
@ -1673,8 +1673,8 @@ const KSPlayerCore: React.FC = () => {
// Check if this is a multi-channel track that might need downmixing // Check if this is a multi-channel track that might need downmixing
const trackName = selectedTrack.name.toLowerCase(); const trackName = selectedTrack.name.toLowerCase();
const isMultiChannel = trackName.includes('5.1') || trackName.includes('7.1') || const isMultiChannel = trackName.includes('5.1') || trackName.includes('7.1') ||
trackName.includes('truehd') || trackName.includes('dts') || trackName.includes('truehd') || trackName.includes('dts') ||
trackName.includes('dolby') || trackName.includes('atmos'); trackName.includes('dolby') || trackName.includes('atmos');
if (isMultiChannel) { if (isMultiChannel) {
logger.log(`[VideoPlayer] Multi-channel audio track detected: ${selectedTrack.name}`); logger.log(`[VideoPlayer] Multi-channel audio track detected: ${selectedTrack.name}`);
@ -1757,9 +1757,9 @@ const KSPlayerCore: React.FC = () => {
try { try {
const merged = { ...(saved || {}), subtitleSize: migrated }; const merged = { ...(saved || {}), subtitleSize: migrated };
await storageService.saveSubtitleSettings(merged); await storageService.saveSubtitleSettings(merged);
} catch {} } catch { }
} }
try { await mmkvStorage.removeItem(SUBTITLE_SIZE_KEY); } catch {} try { await mmkvStorage.removeItem(SUBTITLE_SIZE_KEY); } catch { }
return; return;
} }
// If no saved settings, use responsive default // If no saved settings, use responsive default
@ -2196,33 +2196,28 @@ const KSPlayerCore: React.FC = () => {
// Extract formatted segments from current cue // Extract formatted segments from current cue
if (currentCue?.formattedSegments) { if (currentCue?.formattedSegments) {
// Split by newlines to get per-line segments
const lines = (currentCue.text || '').split(/\r?\n/);
const segmentsPerLine: SubtitleSegment[][] = []; const segmentsPerLine: SubtitleSegment[][] = [];
let segmentIndex = 0; let currentLine: SubtitleSegment[] = [];
for (const line of lines) { currentCue.formattedSegments.forEach(seg => {
const lineSegments: SubtitleSegment[] = []; const parts = seg.text.split(/\r?\n/);
const words = line.split(/(\s+)/); parts.forEach((part, index) => {
if (index > 0) {
for (const word of words) { // New line found
if (word.trim()) { segmentsPerLine.push(currentLine);
if (segmentIndex < currentCue.formattedSegments.length) { currentLine = [];
lineSegments.push(currentCue.formattedSegments[segmentIndex]);
segmentIndex++;
} else {
// Fallback if segment count doesn't match
lineSegments.push({ text: word });
}
} }
} if (part.length > 0) {
currentLine.push({ ...seg, text: part });
}
});
});
if (lineSegments.length > 0) { if (currentLine.length > 0) {
segmentsPerLine.push(lineSegments); segmentsPerLine.push(currentLine);
}
} }
setCurrentFormattedSegments(segmentsPerLine.length > 0 ? segmentsPerLine : []); setCurrentFormattedSegments(segmentsPerLine);
} else { } else {
setCurrentFormattedSegments([]); setCurrentFormattedSegments([]);
} }
@ -2243,14 +2238,14 @@ const KSPlayerCore: React.FC = () => {
if (typeof saved.subtitleOutlineColor === 'string') setSubtitleOutlineColor(saved.subtitleOutlineColor); if (typeof saved.subtitleOutlineColor === 'string') setSubtitleOutlineColor(saved.subtitleOutlineColor);
if (typeof saved.subtitleOutlineWidth === 'number') setSubtitleOutlineWidth(saved.subtitleOutlineWidth); if (typeof saved.subtitleOutlineWidth === 'number') setSubtitleOutlineWidth(saved.subtitleOutlineWidth);
if (typeof saved.subtitleAlign === 'string') setSubtitleAlign(saved.subtitleAlign as 'center' | 'left' | 'right'); if (typeof saved.subtitleAlign === 'string') setSubtitleAlign(saved.subtitleAlign as 'center' | 'left' | 'right');
if (typeof saved.subtitleBottomOffset === 'number') setSubtitleBottomOffset(saved.subtitleBottomOffset); if (typeof saved.subtitleBottomOffset === 'number') setSubtitleBottomOffset(saved.subtitleBottomOffset);
if (typeof saved.subtitleLetterSpacing === 'number') setSubtitleLetterSpacing(saved.subtitleLetterSpacing); if (typeof saved.subtitleLetterSpacing === 'number') setSubtitleLetterSpacing(saved.subtitleLetterSpacing);
if (typeof saved.subtitleLineHeightMultiplier === 'number') setSubtitleLineHeightMultiplier(saved.subtitleLineHeightMultiplier); if (typeof saved.subtitleLineHeightMultiplier === 'number') setSubtitleLineHeightMultiplier(saved.subtitleLineHeightMultiplier);
if (typeof saved.subtitleOffsetSec === 'number') setSubtitleOffsetSec(saved.subtitleOffsetSec); if (typeof saved.subtitleOffsetSec === 'number') setSubtitleOffsetSec(saved.subtitleOffsetSec);
} }
} catch {} finally { } catch { } finally {
// Mark subtitle settings as loaded so we can safely persist subsequent changes // Mark subtitle settings as loaded so we can safely persist subsequent changes
try { setSubtitleSettingsLoaded(true); } catch {} try { setSubtitleSettingsLoaded(true); } catch { }
} }
})(); })();
}, []); }, []);
@ -2283,7 +2278,7 @@ const KSPlayerCore: React.FC = () => {
subtitleOutlineColor, subtitleOutlineColor,
subtitleOutlineWidth, subtitleOutlineWidth,
subtitleAlign, subtitleAlign,
subtitleBottomOffset, subtitleBottomOffset,
subtitleLetterSpacing, subtitleLetterSpacing,
subtitleLineHeightMultiplier, subtitleLineHeightMultiplier,
subtitleOffsetSec, subtitleOffsetSec,
@ -2690,11 +2685,11 @@ const KSPlayerCore: React.FC = () => {
buffered={buffered} buffered={buffered}
formatTime={formatTime} formatTime={formatTime}
playerBackend={playerBackend} playerBackend={playerBackend}
cyclePlaybackSpeed={cyclePlaybackSpeed} cyclePlaybackSpeed={cyclePlaybackSpeed}
currentPlaybackSpeed={playbackSpeed} currentPlaybackSpeed={playbackSpeed}
isAirPlayActive={isAirPlayActive} isAirPlayActive={isAirPlayActive}
allowsAirPlay={allowsAirPlay} allowsAirPlay={allowsAirPlay}
onAirPlayPress={handleAirPlayPress} onAirPlayPress={handleAirPlayPress}
/> />
{showPauseOverlay && ( {showPauseOverlay && (
@ -2725,7 +2720,7 @@ const KSPlayerCore: React.FC = () => {
<LinearGradient <LinearGradient
start={{ x: 0, y: 0.5 }} start={{ x: 0, y: 0.5 }}
end={{ x: 1, 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]} locations={[0, 1]}
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
/> />
@ -2948,38 +2943,38 @@ const KSPlayerCore: React.FC = () => {
marginRight: 8, marginRight: 8,
marginBottom: 8, marginBottom: 8,
}} }}
onPress={() => { onPress={() => {
setSelectedCastMember(castMember); setSelectedCastMember(castMember);
// Animate metadata out, then cast details in // 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
Animated.parallel([ Animated.parallel([
Animated.timing(castDetailsOpacity, { Animated.timing(metadataOpacity, {
toValue: 1, toValue: 0,
duration: 400, duration: 250,
useNativeDriver: true, useNativeDriver: true,
}), }),
Animated.spring(castDetailsScale, { Animated.timing(metadataScale, {
toValue: 1, toValue: 0.95,
tension: 80, duration: 250,
friction: 8,
useNativeDriver: true, 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={{ <Text style={{
color: '#FFFFFF', color: '#FFFFFF',

View file

@ -65,22 +65,25 @@ export const CustomSubtitles: React.FC<CustomSubtitlesProps> = ({
} }
effectiveBottom = Math.max(0, effectiveBottom); effectiveBottom = Math.max(0, effectiveBottom);
// When using crisp outline, prefer SVG text with real stroke instead of blur shadow
const useCrispSvgOutline = outline === true;
const shadowStyle = (textShadow && !useCrispSvgOutline)
? {
textShadowColor: 'rgba(0, 0, 0, 0.9)',
textShadowOffset: { width: 2, height: 2 },
textShadowRadius: 4,
}
: {};
// Prepare content lines // Prepare content lines
const lines = String(currentSubtitle).split(/\r?\n/); const lines = String(currentSubtitle).split(/\r?\n/);
// Detect RTL for each line // Detect RTL for each line
const lineRTLStatus = lines.map(line => detectRTL(line)); const lineRTLStatus = lines.map(line => detectRTL(line));
const hasRTL = lineRTLStatus.some(status => status);
// When using crisp outline, prefer SVG text with real stroke instead of blur shadow
// However, SVG text does not support complex text shaping (required for Arabic/RTL),
// so we must fallback to standard Text component for RTL languages.
const useCrispSvgOutline = outline === true && !hasRTL;
const shadowStyle = (textShadow && !useCrispSvgOutline)
? {
textShadowColor: 'rgba(0, 0, 0, 0.9)',
textShadowOffset: { width: 2, height: 2 },
textShadowRadius: 4,
}
: {};
const displayFontSize = subtitleSize * inverseScale; const displayFontSize = subtitleSize * inverseScale;
const displayLineHeight = subtitleSize * lineHeightMultiplier * inverseScale; const displayLineHeight = subtitleSize * lineHeightMultiplier * inverseScale;

View file

@ -25,7 +25,7 @@ export const safeDebugLog = (message: string, data?: any) => {
}; };
// Add language code to name mapping // Add language code to name mapping
export const languageMap: {[key: string]: string} = { export const languageMap: { [key: string]: string } = {
'en': 'English', 'en': 'English',
'eng': 'English', 'eng': 'English',
'es': 'Spanish', 'es': 'Spanish',
@ -84,7 +84,7 @@ export const formatLanguage = (code?: string): string => {
// If the result is still the uppercased code, it means we couldn't find it in our map. // If the result is still the uppercased code, it means we couldn't find it in our map.
if (languageName === code.toUpperCase()) { if (languageName === code.toUpperCase()) {
return `Unknown (${code})`; return `Unknown (${code})`;
} }
return languageName; return languageName;
@ -104,7 +104,7 @@ export const getTrackDisplayName = (track: { name?: string, id: number, language
// If the track name contains detailed information (like codec, bitrate, etc.), use it as-is // If the track name contains detailed information (like codec, bitrate, etc.), use it as-is
if (track.name && (track.name.includes('DDP') || track.name.includes('DTS') || track.name.includes('AAC') || if (track.name && (track.name.includes('DDP') || track.name.includes('DTS') || track.name.includes('AAC') ||
track.name.includes('Kbps') || track.name.includes('Atmos') || track.name.includes('~'))) { track.name.includes('Kbps') || track.name.includes('Atmos') || track.name.includes('~'))) {
return track.name; return track.name;
} }
@ -189,7 +189,7 @@ export const detectRTL = (text: string): boolean => {
// Arabic Presentation Forms-B: U+FE70U+FEFF // Arabic Presentation Forms-B: U+FE70U+FEFF
// Hebrew: U+0590U+05FF // Hebrew: U+0590U+05FF
// Persian/Urdu use Arabic script (no separate range) // Persian/Urdu use Arabic script (no separate range)
const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/; const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/g;
// Remove whitespace and count characters // Remove whitespace and count characters
const nonWhitespace = text.replace(/\s/g, ''); const nonWhitespace = text.replace(/\s/g, '');