mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 16:51:57 +00:00
android player wordsplitting fix
This commit is contained in:
parent
ecaaaa66ed
commit
348cbf86d8
1 changed files with 525 additions and 530 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue