mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-23 17:52:04 +00:00
Remove SkiaProgressSlider from PlayerControls and refactor progress handling for improved touch interaction. Update AndroidVideoPlayer and VideoPlayer components to eliminate seekToTime prop and adjust subtitle handling.
This commit is contained in:
parent
6433acac77
commit
d66764471f
4 changed files with 53 additions and 293 deletions
|
|
@ -1195,7 +1195,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
handleProgressBarDragEnd={handleProgressBarDragEnd}
|
handleProgressBarDragEnd={handleProgressBarDragEnd}
|
||||||
buffered={buffered}
|
buffered={buffered}
|
||||||
formatTime={formatTime}
|
formatTime={formatTime}
|
||||||
seekToTime={seekToTime}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CustomSubtitles
|
<CustomSubtitles
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,6 @@ const VideoPlayer: React.FC = () => {
|
||||||
const [currentSubtitle, setCurrentSubtitle] = useState<string>('');
|
const [currentSubtitle, setCurrentSubtitle] = useState<string>('');
|
||||||
const [subtitleSize, setSubtitleSize] = useState<number>(DEFAULT_SUBTITLE_SIZE);
|
const [subtitleSize, setSubtitleSize] = useState<number>(DEFAULT_SUBTITLE_SIZE);
|
||||||
const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false);
|
const [useCustomSubtitles, setUseCustomSubtitles] = useState<boolean>(false);
|
||||||
const [subtitleBackground, setSubtitleBackground] = useState<boolean>(true); // Add missing state
|
|
||||||
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false);
|
const [isLoadingSubtitles, setIsLoadingSubtitles] = useState<boolean>(false);
|
||||||
const [availableSubtitles, setAvailableSubtitles] = useState<WyzieSubtitle[]>([]);
|
const [availableSubtitles, setAvailableSubtitles] = useState<WyzieSubtitle[]>([]);
|
||||||
const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState<boolean>(false);
|
const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState<boolean>(false);
|
||||||
|
|
@ -904,21 +903,15 @@ const VideoPlayer: React.FC = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const increaseSubtitleSize = () => {
|
const increaseSubtitleSize = () => {
|
||||||
const newSize = Math.min(subtitleSize + 2, 40);
|
const newSize = Math.min(subtitleSize + 2, 32);
|
||||||
setSubtitleSize(newSize);
|
|
||||||
saveSubtitleSize(newSize);
|
saveSubtitleSize(newSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
const decreaseSubtitleSize = () => {
|
const decreaseSubtitleSize = () => {
|
||||||
const newSize = Math.max(subtitleSize - 2, 12);
|
const newSize = Math.max(subtitleSize - 2, 8);
|
||||||
setSubtitleSize(newSize);
|
|
||||||
saveSubtitleSize(newSize);
|
saveSubtitleSize(newSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleSubtitleBackground = () => {
|
|
||||||
setSubtitleBackground(prev => !prev);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pendingSeek && isPlayerReady && isVideoLoaded && duration > 0) {
|
if (pendingSeek && isPlayerReady && isVideoLoaded && duration > 0) {
|
||||||
logger.log(`[VideoPlayer] Player ready after source change, seeking to position: ${pendingSeek.position}s out of ${duration}s total`);
|
logger.log(`[VideoPlayer] Player ready after source change, seeking to position: ${pendingSeek.position}s out of ${duration}s total`);
|
||||||
|
|
@ -1211,7 +1204,6 @@ const VideoPlayer: React.FC = () => {
|
||||||
handleProgressBarDragEnd={handleProgressBarDragEnd}
|
handleProgressBarDragEnd={handleProgressBarDragEnd}
|
||||||
buffered={buffered}
|
buffered={buffered}
|
||||||
formatTime={formatTime}
|
formatTime={formatTime}
|
||||||
seekToTime={seekToTime}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CustomSubtitles
|
<CustomSubtitles
|
||||||
|
|
@ -1219,7 +1211,6 @@ const VideoPlayer: React.FC = () => {
|
||||||
currentSubtitle={currentSubtitle}
|
currentSubtitle={currentSubtitle}
|
||||||
subtitleSize={subtitleSize}
|
subtitleSize={subtitleSize}
|
||||||
zoomScale={zoomScale}
|
zoomScale={zoomScale}
|
||||||
subtitleBackground={subtitleBackground}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ResumeOverlay
|
<ResumeOverlay
|
||||||
|
|
@ -1255,13 +1246,11 @@ const VideoPlayer: React.FC = () => {
|
||||||
selectedTextTrack={selectedTextTrack}
|
selectedTextTrack={selectedTextTrack}
|
||||||
useCustomSubtitles={useCustomSubtitles}
|
useCustomSubtitles={useCustomSubtitles}
|
||||||
subtitleSize={subtitleSize}
|
subtitleSize={subtitleSize}
|
||||||
subtitleBackground={subtitleBackground}
|
|
||||||
fetchAvailableSubtitles={fetchAvailableSubtitles}
|
fetchAvailableSubtitles={fetchAvailableSubtitles}
|
||||||
loadWyzieSubtitle={loadWyzieSubtitle}
|
loadWyzieSubtitle={loadWyzieSubtitle}
|
||||||
selectTextTrack={selectTextTrack}
|
selectTextTrack={selectTextTrack}
|
||||||
increaseSubtitleSize={increaseSubtitleSize}
|
increaseSubtitleSize={increaseSubtitleSize}
|
||||||
decreaseSubtitleSize={decreaseSubtitleSize}
|
decreaseSubtitleSize={decreaseSubtitleSize}
|
||||||
toggleSubtitleBackground={toggleSubtitleBackground}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SourcesModal
|
<SourcesModal
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { View, Text, TouchableOpacity, Animated, StyleSheet, Dimensions } from 'react-native';
|
import { View, Text, TouchableOpacity, Animated, StyleSheet } from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { styles } from '../utils/playerStyles';
|
import { styles } from '../utils/playerStyles';
|
||||||
import { getTrackDisplayName } from '../utils/playerUtils';
|
import { getTrackDisplayName } from '../utils/playerUtils';
|
||||||
import { SkiaProgressSlider } from './SkiaProgressSlider';
|
|
||||||
|
|
||||||
interface PlayerControlsProps {
|
interface PlayerControlsProps {
|
||||||
showControls: boolean;
|
showControls: boolean;
|
||||||
|
|
@ -40,11 +39,8 @@ interface PlayerControlsProps {
|
||||||
handleProgressBarDragEnd: () => void;
|
handleProgressBarDragEnd: () => void;
|
||||||
buffered: number;
|
buffered: number;
|
||||||
formatTime: (seconds: number) => string;
|
formatTime: (seconds: number) => string;
|
||||||
seekToTime?: (time: number) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { width: screenWidth } = Dimensions.get('window');
|
|
||||||
|
|
||||||
export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||||
showControls,
|
showControls,
|
||||||
fadeAnim,
|
fadeAnim,
|
||||||
|
|
@ -79,61 +75,63 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||||
handleProgressBarDragEnd,
|
handleProgressBarDragEnd,
|
||||||
buffered,
|
buffered,
|
||||||
formatTime,
|
formatTime,
|
||||||
seekToTime,
|
|
||||||
}) => {
|
}) => {
|
||||||
// State for tracking preview time during dragging
|
|
||||||
const [previewTime, setPreviewTime] = useState<number | null>(null);
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
|
||||||
|
|
||||||
// Calculate slider width based on screen width minus padding
|
|
||||||
const sliderWidth = screenWidth - 40; // 20px padding on each side
|
|
||||||
|
|
||||||
const handleSeek = (time: number) => {
|
|
||||||
if (seekToTime) {
|
|
||||||
seekToTime(time);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSeekPreview = (time: number) => {
|
|
||||||
setPreviewTime(time);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSeekStart = () => {
|
|
||||||
setIsDragging(true);
|
|
||||||
handleProgressBarDragStart();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSeekEnd = (time: number) => {
|
|
||||||
setIsDragging(false);
|
|
||||||
setPreviewTime(null);
|
|
||||||
handleProgressBarDragEnd();
|
|
||||||
handleSeek(time);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Determine which time to display (preview time while dragging, otherwise current time)
|
|
||||||
const displayTime = isDragging && previewTime !== null ? previewTime : currentTime;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[StyleSheet.absoluteFill, { opacity: fadeAnim }]}
|
style={[StyleSheet.absoluteFill, { opacity: fadeAnim }]}
|
||||||
pointerEvents={showControls ? 'auto' : 'none'}
|
pointerEvents={showControls ? 'auto' : 'none'}
|
||||||
>
|
>
|
||||||
{/* Progress bar with Skia slider */}
|
{/* Progress bar with enhanced touch handling */}
|
||||||
<View style={styles.sliderContainer}>
|
<View style={styles.sliderContainer}>
|
||||||
<SkiaProgressSlider
|
<View
|
||||||
currentTime={currentTime}
|
style={styles.progressTouchArea}
|
||||||
duration={duration}
|
onTouchStart={handleProgressBarDragStart}
|
||||||
buffered={buffered}
|
onTouchMove={handleProgressBarDragMove}
|
||||||
onSeek={handleSeek}
|
onTouchEnd={handleProgressBarDragEnd}
|
||||||
onSeekStart={handleSeekStart}
|
>
|
||||||
onSeekEnd={handleSeekEnd}
|
<TouchableOpacity
|
||||||
onSeekPreview={handleSeekPreview}
|
activeOpacity={0.8}
|
||||||
width={sliderWidth}
|
onPress={handleProgressBarTouch}
|
||||||
/>
|
style={{width: '100%'}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
ref={progressBarRef}
|
||||||
|
style={styles.progressBarContainer}
|
||||||
|
>
|
||||||
|
{/* Buffered Progress */}
|
||||||
|
<View style={[styles.bufferProgress, {
|
||||||
|
width: `${(buffered / (duration || 1)) * 100}%`
|
||||||
|
}]} />
|
||||||
|
{/* Animated Progress */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.progressBarFill,
|
||||||
|
{
|
||||||
|
width: progressAnim.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: ['0%', '100%']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Progress Thumb - Moved outside the progressBarContainer */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.progressThumb,
|
||||||
|
{
|
||||||
|
left: progressAnim.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: ['0%', '100%']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
<View style={styles.timeDisplay}>
|
<View style={styles.timeDisplay}>
|
||||||
<Text style={[styles.duration, isDragging && { color: '#E50914' }]}>
|
<Text style={styles.duration}>{formatTime(currentTime)}</Text>
|
||||||
{formatTime(displayTime)}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.duration}>{formatTime(duration)}</Text>
|
<Text style={styles.duration}>{formatTime(duration)}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -1,226 +0,0 @@
|
||||||
import React, { useMemo, useEffect } from 'react';
|
|
||||||
import { View, Dimensions } from 'react-native';
|
|
||||||
import {
|
|
||||||
Canvas,
|
|
||||||
RoundedRect,
|
|
||||||
Circle,
|
|
||||||
Group,
|
|
||||||
Shadow,
|
|
||||||
} from '@shopify/react-native-skia';
|
|
||||||
import {
|
|
||||||
Gesture,
|
|
||||||
GestureDetector,
|
|
||||||
GestureHandlerRootView,
|
|
||||||
} from 'react-native-gesture-handler';
|
|
||||||
import Animated, {
|
|
||||||
useSharedValue,
|
|
||||||
useAnimatedStyle,
|
|
||||||
withSpring,
|
|
||||||
runOnJS,
|
|
||||||
interpolate,
|
|
||||||
} from 'react-native-reanimated';
|
|
||||||
|
|
||||||
interface SkiaProgressSliderProps {
|
|
||||||
currentTime: number;
|
|
||||||
duration: number;
|
|
||||||
buffered: number;
|
|
||||||
onSeek: (time: number) => void;
|
|
||||||
onSeekStart?: () => void;
|
|
||||||
onSeekEnd?: (time: number) => void;
|
|
||||||
onSeekPreview?: (time: number) => void; // New callback for preview time
|
|
||||||
width: number;
|
|
||||||
height?: number;
|
|
||||||
thumbSize?: number;
|
|
||||||
progressColor?: string;
|
|
||||||
backgroundColor?: string;
|
|
||||||
bufferedColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SkiaProgressSlider: React.FC<SkiaProgressSliderProps> = ({
|
|
||||||
currentTime,
|
|
||||||
duration,
|
|
||||||
buffered,
|
|
||||||
onSeek,
|
|
||||||
onSeekStart,
|
|
||||||
onSeekEnd,
|
|
||||||
onSeekPreview,
|
|
||||||
width,
|
|
||||||
height = 4,
|
|
||||||
thumbSize = 16,
|
|
||||||
progressColor = '#E50914',
|
|
||||||
backgroundColor = 'rgba(255, 255, 255, 0.2)',
|
|
||||||
bufferedColor = 'rgba(255, 255, 255, 0.4)',
|
|
||||||
}) => {
|
|
||||||
const progress = useSharedValue(0);
|
|
||||||
const isDragging = useSharedValue(false);
|
|
||||||
const thumbScale = useSharedValue(1);
|
|
||||||
const progressWidth = useSharedValue(0);
|
|
||||||
const previewTimeValue = useSharedValue(0);
|
|
||||||
|
|
||||||
// Add padding to account for thumb size
|
|
||||||
const trackPadding = thumbSize / 2;
|
|
||||||
const trackWidth = width - (trackPadding * 2);
|
|
||||||
|
|
||||||
// Update progress when currentTime changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isDragging.value && duration > 0) {
|
|
||||||
const newProgress = (currentTime / duration);
|
|
||||||
progress.value = newProgress;
|
|
||||||
progressWidth.value = newProgress * trackWidth;
|
|
||||||
}
|
|
||||||
}, [currentTime, duration, trackWidth]);
|
|
||||||
|
|
||||||
// Calculate buffered width
|
|
||||||
const bufferedWidth = useMemo(() => {
|
|
||||||
return duration > 0 ? (buffered / duration) * trackWidth : 0;
|
|
||||||
}, [buffered, duration, trackWidth]);
|
|
||||||
|
|
||||||
// Handle seeking with proper coordinate mapping
|
|
||||||
const seekToPosition = (gestureX: number, isPreview: boolean = false) => {
|
|
||||||
'worklet';
|
|
||||||
// Map gesture coordinates to track coordinates
|
|
||||||
const trackX = gestureX - trackPadding;
|
|
||||||
const clampedX = Math.max(0, Math.min(trackX, trackWidth));
|
|
||||||
|
|
||||||
const newProgress = clampedX / trackWidth;
|
|
||||||
progress.value = newProgress;
|
|
||||||
progressWidth.value = clampedX;
|
|
||||||
|
|
||||||
const seekTime = newProgress * duration;
|
|
||||||
previewTimeValue.value = seekTime;
|
|
||||||
|
|
||||||
if (isPreview && onSeekPreview) {
|
|
||||||
runOnJS(onSeekPreview)(seekTime);
|
|
||||||
} else if (!isPreview) {
|
|
||||||
runOnJS(onSeek)(seekTime);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pan gesture for dragging
|
|
||||||
const panGesture = Gesture.Pan()
|
|
||||||
.onBegin((e) => {
|
|
||||||
'worklet';
|
|
||||||
isDragging.value = true;
|
|
||||||
thumbScale.value = withSpring(1.2, { damping: 15, stiffness: 400 });
|
|
||||||
// Process the initial touch position immediately
|
|
||||||
seekToPosition(e.x, true);
|
|
||||||
if (onSeekStart) {
|
|
||||||
runOnJS(onSeekStart)();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onUpdate((e) => {
|
|
||||||
'worklet';
|
|
||||||
seekToPosition(e.x, true); // Use preview mode during dragging
|
|
||||||
})
|
|
||||||
.onFinalize((e) => {
|
|
||||||
'worklet';
|
|
||||||
isDragging.value = false;
|
|
||||||
thumbScale.value = withSpring(1, { damping: 15, stiffness: 400 });
|
|
||||||
|
|
||||||
// Use the exact same preview time for the final seek to ensure consistency
|
|
||||||
if (onSeekEnd) {
|
|
||||||
runOnJS(onSeekEnd)(previewTimeValue.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final seek when drag ends - use the same calculation as preview
|
|
||||||
runOnJS(onSeek)(previewTimeValue.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tap gesture for seeking
|
|
||||||
const tapGesture = Gesture.Tap()
|
|
||||||
.onEnd((e) => {
|
|
||||||
'worklet';
|
|
||||||
seekToPosition(e.x, false); // Direct seek on tap
|
|
||||||
});
|
|
||||||
|
|
||||||
const composedGesture = Gesture.Simultaneous(tapGesture, panGesture);
|
|
||||||
|
|
||||||
// Animated styles for thumb
|
|
||||||
const animatedThumbStyle = useAnimatedStyle(() => {
|
|
||||||
const thumbX = progress.value * trackWidth + trackPadding;
|
|
||||||
|
|
||||||
return {
|
|
||||||
transform: [
|
|
||||||
{ translateX: thumbX - thumbSize / 2 },
|
|
||||||
{ scale: thumbScale.value }
|
|
||||||
],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animated style for progress width
|
|
||||||
const animatedProgressStyle = useAnimatedStyle(() => {
|
|
||||||
return {
|
|
||||||
width: progressWidth.value,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={{ width, height: height + thumbSize + 8 }}>
|
|
||||||
<GestureDetector gesture={composedGesture}>
|
|
||||||
<View>
|
|
||||||
<Canvas style={{ width, height: height + thumbSize + 8 }}>
|
|
||||||
<Group transform={[{ translateY: (thumbSize / 2) + 4 }, { translateX: trackPadding }]}>
|
|
||||||
{/* Background track */}
|
|
||||||
<RoundedRect
|
|
||||||
x={0}
|
|
||||||
y={0}
|
|
||||||
width={trackWidth}
|
|
||||||
height={height}
|
|
||||||
r={height / 2}
|
|
||||||
color={backgroundColor}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Buffered progress */}
|
|
||||||
{bufferedWidth > 0 && (
|
|
||||||
<RoundedRect
|
|
||||||
x={0}
|
|
||||||
y={0}
|
|
||||||
width={bufferedWidth}
|
|
||||||
height={height}
|
|
||||||
r={height / 2}
|
|
||||||
color={bufferedColor}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
</Canvas>
|
|
||||||
|
|
||||||
{/* Current progress - using regular view for animation */}
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
{
|
|
||||||
position: 'absolute',
|
|
||||||
top: (thumbSize / 2) + 4,
|
|
||||||
left: trackPadding,
|
|
||||||
height: height,
|
|
||||||
backgroundColor: progressColor,
|
|
||||||
borderRadius: height / 2,
|
|
||||||
},
|
|
||||||
animatedProgressStyle,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Animated thumb */}
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 4,
|
|
||||||
left: 0,
|
|
||||||
width: thumbSize,
|
|
||||||
height: thumbSize,
|
|
||||||
borderRadius: thumbSize / 2,
|
|
||||||
backgroundColor: progressColor,
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOffset: { width: 0, height: 2 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 3,
|
|
||||||
elevation: 4,
|
|
||||||
},
|
|
||||||
animatedThumbStyle,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</GestureDetector>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Loading…
Reference in a new issue