Fixes on downloading and series episode lookup.
This commit is contained in:
@@ -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) && (
|
||||
|
||||
Reference in New Issue
Block a user