Fixes on downloading and series episode lookup.

This commit is contained in:
2026-01-02 13:02:41 +01:00
parent 5cecb0cd48
commit f7d43e10b7
15 changed files with 1274 additions and 208 deletions

View File

@@ -17,6 +17,8 @@ import {
Users,
HardDrive,
Subtitles,
AlertTriangle,
MonitorPlay,
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { clsx } from 'clsx';
@@ -24,6 +26,8 @@ import type { Movie, Subtitle } from '../../types';
import type { StreamSession } from '../../services/streaming/streamingService';
import { searchSubtitles, downloadSubtitle, convertSrtToVtt } from '../../services/subtitles/opensubtitles';
import { useSettingsStore } from '../../stores/settingsStore';
import { isCapacitor } from '../../utils/platform';
import { playWithNativePlayer } from '../../plugins/ExoPlayer';
interface StreamingPlayerProps {
movie: Movie;
@@ -47,6 +51,8 @@ export default function StreamingPlayer({
const containerRef = useRef<HTMLDivElement>(null);
const controlsTimeoutRef = useRef<NodeJS.Timeout>();
const currentSourceRef = useRef<string | null>(null); // Track current source to avoid re-setting
const hasAppliedInitialTimeRef = useRef(false); // Track if initial seek has been applied
const lastPlaybackPositionRef = useRef<number>(0); // Track last playback position for source switches
const navigate = useNavigate();
const [isPlaying, setIsPlaying] = useState(false);
@@ -68,8 +74,80 @@ export default function StreamingPlayer({
const [networkSpeed, setNetworkSpeed] = useState<number>(0);
const [currentQualityLevel, setCurrentQualityLevel] = useState<string>('auto');
const [availableQualities, setAvailableQualities] = useState<string[]>(['auto']);
const [audioWarning, setAudioWarning] = useState<string | null>(null);
const [audioWarningDismissed, setAudioWarningDismissed] = useState(false);
const [useNativePlayer, setUseNativePlayer] = useState(false);
const [isNativePlayerPlaying, setIsNativePlayerPlaying] = useState(false);
const { settings } = useSettingsStore();
// Detect potential audio issues on Android with MKV files and enable native player
useEffect(() => {
// Only check on Capacitor (Android) since desktop has FFmpeg transcoding
if (!isCapacitor()) return;
// Check if this is an MKV file (common for TV shows from EZTV)
const videoName = streamSession?.videoFile?.name?.toLowerCase() || '';
const isMkv = videoName.endsWith('.mkv');
if (isMkv && !hlsUrl) {
// MKV files often have AC3/DTS audio that Android WebView can't play
// Enable native player option
setUseNativePlayer(true);
setAudioWarning(
'This video uses an MKV format. Click "Play with Native Player" for better audio codec support, or try a different torrent with MP4 format.'
);
} else {
setAudioWarning(null);
setUseNativePlayer(false);
}
}, [streamSession?.videoFile?.name, hlsUrl]);
// Handle launching native ExoPlayer for better codec support
const handleNativePlayerLaunch = useCallback(async () => {
if (!streamUrl) return;
setIsNativePlayerPlaying(true);
// Pause webview video
if (videoRef.current) {
videoRef.current.pause();
}
const title = movie.title || 'Video';
const startPos = currentTime;
console.log('[NativePlayer] Launching ExoPlayer with URL:', streamUrl);
const result = await playWithNativePlayer(streamUrl, title, startPos);
setIsNativePlayerPlaying(false);
if (result && result.success) {
console.log('[NativePlayer] Playback ended at:', result.position, 'completed:', result.completed);
// Update current time from native player
if (result.position > 0) {
setCurrentTime(result.position);
if (videoRef.current) {
videoRef.current.currentTime = result.position;
}
// Report time to parent
if (onTimeUpdate) {
onTimeUpdate(result.position, result.duration || duration);
}
}
// If video completed, navigate back
if (result.completed) {
navigate(-1);
}
} else {
console.error('[NativePlayer] Failed to play with native player');
// Show error and fall back to webview
setAudioWarning('Native player failed. Using built-in player which may have audio issues with this format.');
}
}, [streamUrl, movie.title, currentTime, duration, navigate, onTimeUpdate]);
// Format time to MM:SS or HH:MM:SS
const formatTime = (seconds: number): string => {
if (!isFinite(seconds)) return '0:00';
@@ -109,6 +187,13 @@ export default function StreamingPlayer({
const useHls = hlsUrl && Hls.isSupported();
// Save current playback position before potentially switching sources
// This prevents the video from jumping back to initialTime when HLS becomes available
if (video.currentTime > 0 && !video.paused) {
lastPlaybackPositionRef.current = video.currentTime;
console.log('[Source] Saving current position before switch:', video.currentTime);
}
if (useHls) {
// Skip if we already have this HLS source loaded
if (currentSourceRef.current === hlsUrl && hlsRef.current) {
@@ -191,9 +276,19 @@ export default function StreamingPlayer({
setAvailableQualities(sortedQualities);
console.log('[HLS] Available qualities:', sortedQualities);
if (initialTime > 0) {
video.currentTime = initialTime;
// Determine the correct seek position:
// - If we were already playing (source switch), use last playback position
// - If this is first load and initialTime is set, use that
// - Otherwise start from beginning
const seekPosition = lastPlaybackPositionRef.current > 0
? lastPlaybackPositionRef.current
: (!hasAppliedInitialTimeRef.current && initialTime > 0 ? initialTime : 0);
if (seekPosition > 0) {
console.log('[HLS] Seeking to position:', seekPosition);
video.currentTime = seekPosition;
}
hasAppliedInitialTimeRef.current = true;
video.play().catch(console.error);
});
@@ -270,9 +365,21 @@ export default function StreamingPlayer({
const handleCanPlay = () => {
console.log('[Video] Can play, starting playback...');
setIsBuffering(false);
if (initialTime > 0) {
video.currentTime = initialTime;
// Determine the correct seek position:
// - If we were already playing (source switch), use last playback position
// - If this is first load and initialTime is set, use that
// - Otherwise start from beginning
const seekPosition = lastPlaybackPositionRef.current > 0
? lastPlaybackPositionRef.current
: (!hasAppliedInitialTimeRef.current && initialTime > 0 ? initialTime : 0);
if (seekPosition > 0) {
console.log('[Video] Seeking to position:', seekPosition);
video.currentTime = seekPosition;
}
hasAppliedInitialTimeRef.current = true;
video.play().catch((err) => {
console.warn('[Video] Auto-play blocked:', err.message);
// Some browsers block autoplay, that's ok - user can click play
@@ -283,6 +390,20 @@ export default function StreamingPlayer({
const handleLoadedMetadata = () => {
console.log('[Video] Metadata loaded, duration:', video.duration);
setDuration(video.duration);
// Check for audio tracks on Android - if no audio tracks detected, show warning
if (isCapacitor()) {
const audioTracks = (video as any).audioTracks;
// Log audio tracks info for debugging
console.log('[Video] Audio tracks:', audioTracks?.length || 'unknown');
if (audioTracks && audioTracks.length === 0) {
setAudioWarning(
'No audio track detected. This video file may use an audio codec that Android cannot play. Try a different torrent.'
);
}
}
};
const handleLoadedData = () => {
@@ -318,6 +439,8 @@ export default function StreamingPlayer({
// If HLS is available, try switching to it
if (hlsUrl && !hlsRef.current) {
console.log('[Video] Decode error detected, switching to HLS transcoding...');
// Save current position before switching
const currentPosition = video.currentTime > 0 ? video.currentTime : lastPlaybackPositionRef.current;
// Force HLS playback
const hls = new Hls({
enableWorker: true,
@@ -325,8 +448,13 @@ export default function StreamingPlayer({
});
hls.loadSource(hlsUrl);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
if (currentPosition > 0) {
video.currentTime = currentPosition;
}
video.play().catch(console.error);
});
hlsRef.current = hls;
video.play().catch(console.error);
}
break;
case error.MEDIA_ERR_SRC_NOT_SUPPORTED:
@@ -334,14 +462,21 @@ export default function StreamingPlayer({
// If HLS is available, try switching to it
if (hlsUrl && !hlsRef.current) {
console.log('[Video] Format not supported, switching to HLS transcoding...');
// Save current position before switching
const currentPosition = video.currentTime > 0 ? video.currentTime : lastPlaybackPositionRef.current;
const hls = new Hls({
enableWorker: true,
lowLatencyMode: false,
});
hls.loadSource(hlsUrl);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
if (currentPosition > 0) {
video.currentTime = currentPosition;
}
video.play().catch(console.error);
});
hlsRef.current = hls;
video.play().catch(console.error);
}
break;
}
@@ -422,6 +557,10 @@ export default function StreamingPlayer({
const handleTimeUpdate = () => {
setCurrentTime(video.currentTime);
// Keep track of playback position for source switches
if (video.currentTime > 0) {
lastPlaybackPositionRef.current = video.currentTime;
}
onTimeUpdate?.(video.currentTime, video.duration);
};
@@ -668,6 +807,58 @@ export default function StreamingPlayer({
}}
playsInline
/>
{/* Audio Warning Banner for Android MKV files with Native Player option */}
<AnimatePresence>
{audioWarning && !audioWarningDismissed && (
<motion.div
initial={{ opacity: 0, y: -50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -50 }}
className="absolute top-0 left-0 right-0 z-50 bg-yellow-600/95 text-white p-3"
>
<div className="flex items-start gap-3 max-w-4xl mx-auto">
<AlertTriangle size={24} className="flex-shrink-0 mt-0.5" />
<div className="flex-1 text-sm">
<p className="font-medium mb-1">Audio Format Issue</p>
<p className="text-yellow-100">{audioWarning}</p>
{useNativePlayer && (
<button
onClick={handleNativePlayerLaunch}
disabled={isNativePlayerPlaying}
className="mt-2 flex items-center gap-2 px-4 py-2 bg-white/20 hover:bg-white/30 rounded-lg font-medium transition-colors disabled:opacity-50"
>
<MonitorPlay size={18} />
{isNativePlayerPlaying ? 'Opening Native Player...' : 'Play with Native Player (Full Audio Support)'}
</button>
)}
</div>
<button
onClick={() => setAudioWarningDismissed(true)}
className="text-yellow-200 hover:text-white p-1"
>
</button>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Native Player Loading Overlay */}
<AnimatePresence>
{isNativePlayerPlaying && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 flex flex-col items-center justify-center bg-black/80 z-50"
>
<Loader2 size={48} className="animate-spin text-netflix-red mb-4" />
<p className="text-white text-lg">Opening Native Player...</p>
<p className="text-gray-400 text-sm mt-2">ExoPlayer with full codec support</p>
</motion.div>
)}
</AnimatePresence>
{/* Click outside to close menus */}
{(showSettings || showSubtitles) && (