mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
improved player volume/brightness gesture sensitivity
This commit is contained in:
parent
ed3aef88ff
commit
94e165f0b0
4 changed files with 233 additions and 313 deletions
|
|
@ -1106,9 +1106,6 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
// Guards to avoid repeated auto-starts
|
||||
const startedOnFocusRef = useRef(false);
|
||||
const startedOnReadyRef = useRef(false);
|
||||
// Debounced pause/resume flag to avoid blocking scroll
|
||||
const pendingPauseResumeSV = useSharedValue(0); // 0 = no action, 1 = pause, 2 = resume
|
||||
const pauseResumeTimerRef = useRef<any>(null);
|
||||
|
||||
// Animation values for trailer unmute effects
|
||||
const actionButtonsOpacity = useSharedValue(1);
|
||||
|
|
@ -1691,7 +1688,6 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
}, [isFocused, setTrailerPlaying]);
|
||||
|
||||
// Ultra-optimized scroll-based pause/resume with cached calculations
|
||||
// This worklet only sets flags - actual pause/resume happens asynchronously to avoid blocking scroll
|
||||
useDerivedValue(() => {
|
||||
'worklet';
|
||||
try {
|
||||
|
|
@ -1704,49 +1700,21 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
const isPlaying = isPlayingSV.value === 1;
|
||||
const isPausedByScroll = pausedByScrollSV.value === 1;
|
||||
|
||||
// Set flags for pause/resume - don't execute immediately to keep scroll smooth
|
||||
// Optimized pause/resume logic with minimal branching
|
||||
if (y > pauseThreshold && isPlaying && !isPausedByScroll) {
|
||||
pendingPauseResumeSV.value = 1; // Request pause
|
||||
pausedByScrollSV.value = 1;
|
||||
runOnJS(setTrailerPlaying)(false);
|
||||
isPlayingSV.value = 0;
|
||||
} else if (y < resumeThreshold && isPausedByScroll) {
|
||||
pendingPauseResumeSV.value = 2; // Request resume
|
||||
pausedByScrollSV.value = 0;
|
||||
runOnJS(setTrailerPlaying)(true);
|
||||
isPlayingSV.value = 1;
|
||||
}
|
||||
} catch (e) {
|
||||
// Silent error handling for performance
|
||||
}
|
||||
});
|
||||
|
||||
// Debounced pause/resume effect - executes asynchronously to keep scroll smooth
|
||||
useEffect(() => {
|
||||
const checkPendingAction = () => {
|
||||
const pendingAction = pendingPauseResumeSV.value;
|
||||
|
||||
if (pendingAction === 1) {
|
||||
// Execute pause
|
||||
pausedByScrollSV.value = 1;
|
||||
setTrailerPlaying(false);
|
||||
isPlayingSV.value = 0;
|
||||
pendingPauseResumeSV.value = 0; // Clear flag
|
||||
} else if (pendingAction === 2) {
|
||||
// Execute resume
|
||||
pausedByScrollSV.value = 0;
|
||||
setTrailerPlaying(true);
|
||||
isPlayingSV.value = 1;
|
||||
pendingPauseResumeSV.value = 0; // Clear flag
|
||||
}
|
||||
};
|
||||
|
||||
// Set up a recurring check with small delay to avoid blocking scroll
|
||||
const intervalId = setInterval(checkPendingAction, 100);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
if (pauseResumeTimerRef.current) {
|
||||
clearTimeout(pauseResumeTimerRef.current);
|
||||
pauseResumeTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [setTrailerPlaying, pendingPauseResumeSV, pausedByScrollSV, isPlayingSV]);
|
||||
|
||||
// Memory management and cleanup
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { useTraktAutosync } from '../../hooks/useTraktAutosync';
|
|||
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
||||
import { useMetadata } from '../../hooks/useMetadata';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { usePlayerGestureControls } from '../../hooks/usePlayerGestureControls';
|
||||
|
||||
import {
|
||||
DEFAULT_SUBTITLE_SIZE,
|
||||
|
|
@ -278,8 +279,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
// Debounce resize operations to prevent rapid successive clicks
|
||||
const resizeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Debounce gesture operations to prevent rapid-fire events
|
||||
const gestureDebounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
|
||||
|
||||
|
|
@ -376,10 +375,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
clearTimeout(resizeTimeoutRef.current);
|
||||
resizeTimeoutRef.current = null;
|
||||
}
|
||||
if (gestureDebounceRef.current) {
|
||||
clearTimeout(gestureDebounceRef.current);
|
||||
gestureDebounceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
@ -576,16 +571,22 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
// Store Android system brightness state to restore on exit/unmount
|
||||
const originalSystemBrightnessRef = useRef<number | null>(null);
|
||||
const originalSystemBrightnessModeRef = useRef<number | null>(null);
|
||||
const [showVolumeOverlay, setShowVolumeOverlay] = useState(false);
|
||||
const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false);
|
||||
const [subtitleSettingsLoaded, setSubtitleSettingsLoaded] = useState(false);
|
||||
const volumeOverlayOpacity = useRef(new Animated.Value(0)).current;
|
||||
const brightnessOverlayOpacity = useRef(new Animated.Value(0)).current;
|
||||
const volumeOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const brightnessOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const lastVolumeChange = useRef<number>(0);
|
||||
const lastBrightnessChange = useRef<number>(0);
|
||||
|
||||
// Use reusable gesture controls hook
|
||||
const gestureControls = usePlayerGestureControls({
|
||||
volume,
|
||||
setVolume,
|
||||
brightness,
|
||||
setBrightness,
|
||||
volumeRange: { min: 0, max: 1 },
|
||||
volumeSensitivity: 0.006,
|
||||
brightnessSensitivity: 0.004,
|
||||
debugMode: DEBUG_MODE,
|
||||
});
|
||||
|
||||
// iOS startup timing diagnostics
|
||||
const loadStartAtRef = useRef<number | null>(null);
|
||||
const firstFrameAtRef = useRef<number | null>(null);
|
||||
|
|
@ -743,124 +744,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
return;
|
||||
};
|
||||
|
||||
// Volume gesture handler (right side of screen) - optimized with debouncing
|
||||
const onVolumeGestureEvent = async (event: PanGestureHandlerGestureEvent) => {
|
||||
const { translationY, state } = event.nativeEvent;
|
||||
const sensitivity = 0.002; // Lower sensitivity for gradual volume control on Android
|
||||
|
||||
if (state === State.ACTIVE) {
|
||||
// Debounce rapid gesture events
|
||||
if (gestureDebounceRef.current) {
|
||||
clearTimeout(gestureDebounceRef.current);
|
||||
}
|
||||
|
||||
gestureDebounceRef.current = setTimeout(() => {
|
||||
const deltaY = -translationY; // Invert for natural feel (up = increase)
|
||||
const volumeChange = deltaY * sensitivity;
|
||||
const newVolume = Math.max(0, Math.min(1, volume + volumeChange));
|
||||
|
||||
if (Math.abs(newVolume - volume) > 0.01) { // Lower threshold for smoother Android volume control
|
||||
setVolume(newVolume);
|
||||
lastVolumeChange.current = Date.now();
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
logger.log(`[AndroidVideoPlayer] Volume set to: ${newVolume}`);
|
||||
}
|
||||
|
||||
// Show overlay with smoother animation
|
||||
if (!showVolumeOverlay) {
|
||||
setShowVolumeOverlay(true);
|
||||
Animated.spring(volumeOverlayOpacity, {
|
||||
toValue: 1,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}
|
||||
|
||||
// Clear existing timeout
|
||||
if (volumeOverlayTimeout.current) {
|
||||
clearTimeout(volumeOverlayTimeout.current);
|
||||
}
|
||||
|
||||
// Hide overlay after 1.5 seconds (reduced from 2 seconds)
|
||||
volumeOverlayTimeout.current = setTimeout(() => {
|
||||
Animated.timing(volumeOverlayOpacity, {
|
||||
toValue: 0,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
setShowVolumeOverlay(false);
|
||||
});
|
||||
}, 1500);
|
||||
}
|
||||
}, 16); // ~60fps debouncing
|
||||
}
|
||||
};
|
||||
|
||||
// Brightness gesture handler (left side of screen) - optimized with debouncing
|
||||
const onBrightnessGestureEvent = async (event: PanGestureHandlerGestureEvent) => {
|
||||
const { translationY, state } = event.nativeEvent;
|
||||
const sensitivity = 0.001; // Lower sensitivity for finer brightness control
|
||||
|
||||
if (state === State.ACTIVE) {
|
||||
// Debounce rapid gesture events
|
||||
if (gestureDebounceRef.current) {
|
||||
clearTimeout(gestureDebounceRef.current);
|
||||
}
|
||||
|
||||
gestureDebounceRef.current = setTimeout(() => {
|
||||
const deltaY = -translationY; // Invert for natural feel (up = increase)
|
||||
const brightnessChange = deltaY * sensitivity;
|
||||
const newBrightness = Math.max(0, Math.min(1, brightness + brightnessChange));
|
||||
|
||||
if (Math.abs(newBrightness - brightness) > 0.001) { // Much lower threshold for more responsive updates
|
||||
setBrightness(newBrightness);
|
||||
lastBrightnessChange.current = Date.now();
|
||||
|
||||
// Set device brightness using Expo Brightness
|
||||
try {
|
||||
Brightness.setBrightnessAsync(newBrightness).catch((error) => {
|
||||
logger.warn('[AndroidVideoPlayer] Error setting device brightness:', error);
|
||||
});
|
||||
if (DEBUG_MODE) {
|
||||
logger.log(`[AndroidVideoPlayer] Device brightness set to: ${newBrightness}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[AndroidVideoPlayer] Error setting device brightness:', error);
|
||||
}
|
||||
|
||||
// Show overlay with smoother animation
|
||||
if (!showBrightnessOverlay) {
|
||||
setShowBrightnessOverlay(true);
|
||||
Animated.spring(brightnessOverlayOpacity, {
|
||||
toValue: 1,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}
|
||||
|
||||
// Clear existing timeout
|
||||
if (brightnessOverlayTimeout.current) {
|
||||
clearTimeout(brightnessOverlayTimeout.current);
|
||||
}
|
||||
|
||||
// Hide overlay after 1.5 seconds (reduced from 2 seconds)
|
||||
brightnessOverlayTimeout.current = setTimeout(() => {
|
||||
Animated.timing(brightnessOverlayOpacity, {
|
||||
toValue: 0,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
setShowBrightnessOverlay(false);
|
||||
});
|
||||
}, 1500);
|
||||
}
|
||||
}, 16); // ~60fps debouncing
|
||||
}
|
||||
};
|
||||
|
||||
// Long press gesture handlers for speed boost
|
||||
const onLongPressActivated = useCallback(() => {
|
||||
if (!holdToSpeedEnabled) return;
|
||||
|
|
@ -2902,14 +2785,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
clearTimeout(errorTimeoutRef.current);
|
||||
errorTimeoutRef.current = null;
|
||||
}
|
||||
if (volumeOverlayTimeout.current) {
|
||||
clearTimeout(volumeOverlayTimeout.current);
|
||||
volumeOverlayTimeout.current = null;
|
||||
}
|
||||
if (brightnessOverlayTimeout.current) {
|
||||
clearTimeout(brightnessOverlayTimeout.current);
|
||||
brightnessOverlayTimeout.current = null;
|
||||
}
|
||||
if (controlsTimeout.current) {
|
||||
clearTimeout(controlsTimeout.current);
|
||||
controlsTimeout.current = null;
|
||||
|
|
@ -2918,14 +2793,13 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
clearTimeout(pauseOverlayTimerRef.current);
|
||||
pauseOverlayTimerRef.current = null;
|
||||
}
|
||||
if (gestureDebounceRef.current) {
|
||||
clearTimeout(gestureDebounceRef.current);
|
||||
gestureDebounceRef.current = null;
|
||||
}
|
||||
if (progressSaveInterval) {
|
||||
clearInterval(progressSaveInterval);
|
||||
setProgressSaveInterval(null);
|
||||
}
|
||||
|
||||
// Cleanup gesture controls
|
||||
gestureControls.cleanup();
|
||||
// Best-effort restore of Android system brightness state on unmount
|
||||
if (Platform.OS === 'android') {
|
||||
try {
|
||||
|
|
@ -3323,9 +3197,9 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
simultaneousHandlers={[]}
|
||||
>
|
||||
<PanGestureHandler
|
||||
onGestureEvent={onBrightnessGestureEvent}
|
||||
activeOffsetY={[-5, 5]}
|
||||
failOffsetX={[-20, 20]}
|
||||
onGestureEvent={gestureControls.onBrightnessGestureEvent}
|
||||
activeOffsetY={[-10, 10]}
|
||||
failOffsetX={[-30, 30]}
|
||||
shouldCancelWhenOutside={false}
|
||||
simultaneousHandlers={[]}
|
||||
maxPointers={1}
|
||||
|
|
@ -3356,9 +3230,9 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
simultaneousHandlers={[]}
|
||||
>
|
||||
<PanGestureHandler
|
||||
onGestureEvent={onVolumeGestureEvent}
|
||||
activeOffsetY={[-5, 5]}
|
||||
failOffsetX={[-20, 20]}
|
||||
onGestureEvent={gestureControls.onVolumeGestureEvent}
|
||||
activeOffsetY={[-10, 10]}
|
||||
failOffsetX={[-30, 30]}
|
||||
shouldCancelWhenOutside={false}
|
||||
simultaneousHandlers={[]}
|
||||
maxPointers={1}
|
||||
|
|
@ -3974,13 +3848,13 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
/>
|
||||
|
||||
{/* Volume Overlay */}
|
||||
{showVolumeOverlay && (
|
||||
{gestureControls.showVolumeOverlay && (
|
||||
<Animated.View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: screenDimensions.width / 2 - 60,
|
||||
top: screenDimensions.height / 2 - 60,
|
||||
opacity: volumeOverlayOpacity,
|
||||
opacity: gestureControls.volumeOverlayOpacity,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
|
|
@ -4071,13 +3945,13 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
)}
|
||||
|
||||
{/* Brightness Overlay */}
|
||||
{showBrightnessOverlay && (
|
||||
{gestureControls.showBrightnessOverlay && (
|
||||
<Animated.View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: screenDimensions.width / 2 - 60,
|
||||
top: screenDimensions.height / 2 - 60,
|
||||
opacity: brightnessOverlayOpacity,
|
||||
opacity: gestureControls.brightnessOverlayOpacity,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { useTraktAutosync } from '../../hooks/useTraktAutosync';
|
|||
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
||||
import { useMetadata } from '../../hooks/useMetadata';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { usePlayerGestureControls } from '../../hooks/usePlayerGestureControls';
|
||||
|
||||
import {
|
||||
DEFAULT_SUBTITLE_SIZE,
|
||||
|
|
@ -125,6 +126,7 @@ const KSPlayerCore: React.FC = () => {
|
|||
const ksPlayerRef = useRef<KSPlayerRef>(null);
|
||||
const [showAudioModal, setShowAudioModal] = useState(false);
|
||||
const [showSubtitleModal, setShowSubtitleModal] = useState(false);
|
||||
const [showSpeedModal, setShowSpeedModal] = useState(false);
|
||||
const [initialPosition, setInitialPosition] = useState<number | null>(null);
|
||||
const [progressSaveInterval, setProgressSaveInterval] = useState<NodeJS.Timeout | null>(null);
|
||||
const [isInitialSeekComplete, setIsInitialSeekComplete] = useState(false);
|
||||
|
|
@ -297,15 +299,19 @@ const KSPlayerCore: React.FC = () => {
|
|||
// Volume and brightness controls
|
||||
const [volume, setVolume] = useState(100); // KSPlayer uses 0-100 range
|
||||
const [brightness, setBrightness] = useState(1.0);
|
||||
const [showVolumeOverlay, setShowVolumeOverlay] = useState(false);
|
||||
const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false);
|
||||
const [subtitleSettingsLoaded, setSubtitleSettingsLoaded] = useState(false);
|
||||
const volumeOverlayOpacity = useRef(new Animated.Value(0)).current;
|
||||
const brightnessOverlayOpacity = useRef(new Animated.Value(0)).current;
|
||||
const volumeOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const brightnessOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const lastVolumeChange = useRef<number>(0);
|
||||
const lastBrightnessChange = useRef<number>(0);
|
||||
|
||||
// Use reusable gesture controls hook
|
||||
const gestureControls = usePlayerGestureControls({
|
||||
volume,
|
||||
setVolume,
|
||||
brightness,
|
||||
setBrightness,
|
||||
volumeRange: { min: 0, max: 100 }, // KSPlayer uses 0-100
|
||||
volumeSensitivity: 0.006,
|
||||
brightnessSensitivity: 0.004,
|
||||
debugMode: DEBUG_MODE,
|
||||
});
|
||||
|
||||
// Get metadata to access logo (only if we have a valid id)
|
||||
const shouldLoadMetadata = Boolean(id && type);
|
||||
|
|
@ -462,104 +468,6 @@ const KSPlayerCore: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// Volume gesture handler (right side of screen)
|
||||
const onVolumeGestureEvent = async (event: PanGestureHandlerGestureEvent) => {
|
||||
const { translationY, state } = event.nativeEvent;
|
||||
const sensitivity = 0.050; // Higher sensitivity for volume (more responsive than brightness)
|
||||
|
||||
if (state === State.ACTIVE) {
|
||||
const deltaY = -translationY; // Invert for natural feel (up = increase)
|
||||
const volumeChange = deltaY * sensitivity;
|
||||
const newVolume = Math.max(0, Math.min(100, volume + volumeChange));
|
||||
|
||||
if (Math.abs(newVolume - volume) > 0.05) { // Even lower threshold for volume responsiveness
|
||||
setVolume(newVolume);
|
||||
lastVolumeChange.current = Date.now();
|
||||
|
||||
// Show overlay with smoother animation
|
||||
if (!showVolumeOverlay) {
|
||||
setShowVolumeOverlay(true);
|
||||
Animated.spring(volumeOverlayOpacity, {
|
||||
toValue: 1,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}
|
||||
|
||||
// Clear existing timeout
|
||||
if (volumeOverlayTimeout.current) {
|
||||
clearTimeout(volumeOverlayTimeout.current);
|
||||
}
|
||||
|
||||
// Hide overlay after 1.5 seconds
|
||||
volumeOverlayTimeout.current = setTimeout(() => {
|
||||
Animated.timing(volumeOverlayOpacity, {
|
||||
toValue: 0,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
setShowVolumeOverlay(false);
|
||||
});
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Brightness gesture handler (left side of screen)
|
||||
const onBrightnessGestureEvent = async (event: PanGestureHandlerGestureEvent) => {
|
||||
const { translationY, state } = event.nativeEvent;
|
||||
const sensitivity = 0.001; // Lower sensitivity for finer brightness control
|
||||
|
||||
if (state === State.ACTIVE) {
|
||||
const deltaY = -translationY; // Invert for natural feel (up = increase)
|
||||
const brightnessChange = deltaY * sensitivity;
|
||||
const newBrightness = Math.max(0, Math.min(1, brightness + brightnessChange));
|
||||
|
||||
if (Math.abs(newBrightness - brightness) > 0.001) { // Much lower threshold for more responsive updates
|
||||
setBrightness(newBrightness);
|
||||
lastBrightnessChange.current = Date.now();
|
||||
|
||||
// Set device brightness using Expo Brightness
|
||||
try {
|
||||
await Brightness.setBrightnessAsync(newBrightness);
|
||||
if (DEBUG_MODE) {
|
||||
logger.log(`[VideoPlayer] Device brightness set to: ${newBrightness}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[VideoPlayer] Error setting device brightness:', error);
|
||||
}
|
||||
|
||||
// Show overlay with smoother animation
|
||||
if (!showBrightnessOverlay) {
|
||||
setShowBrightnessOverlay(true);
|
||||
Animated.spring(brightnessOverlayOpacity, {
|
||||
toValue: 1,
|
||||
tension: 100,
|
||||
friction: 8,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}
|
||||
|
||||
// Clear existing timeout
|
||||
if (brightnessOverlayTimeout.current) {
|
||||
clearTimeout(brightnessOverlayTimeout.current);
|
||||
}
|
||||
|
||||
// Hide overlay after 1.5 seconds (reduced from 2 seconds)
|
||||
brightnessOverlayTimeout.current = setTimeout(() => {
|
||||
Animated.timing(brightnessOverlayOpacity, {
|
||||
toValue: 0,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
setShowBrightnessOverlay(false);
|
||||
});
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (videoAspectRatio && effectiveDimensions.width > 0 && effectiveDimensions.height > 0) {
|
||||
const styles = calculateVideoStyles(
|
||||
|
|
@ -2211,12 +2119,10 @@ const KSPlayerCore: React.FC = () => {
|
|||
if (errorTimeoutRef.current) {
|
||||
clearTimeout(errorTimeoutRef.current);
|
||||
}
|
||||
if (volumeOverlayTimeout.current) {
|
||||
clearTimeout(volumeOverlayTimeout.current);
|
||||
}
|
||||
if (brightnessOverlayTimeout.current) {
|
||||
clearTimeout(brightnessOverlayTimeout.current);
|
||||
}
|
||||
|
||||
// Cleanup gesture controls
|
||||
gestureControls.cleanup();
|
||||
|
||||
if (startupRetryTimerRef.current) {
|
||||
clearTimeout(startupRetryTimerRef.current);
|
||||
startupRetryTimerRef.current = null;
|
||||
|
|
@ -2619,9 +2525,9 @@ const KSPlayerCore: React.FC = () => {
|
|||
>
|
||||
{/* Combined gesture handler for left side - brightness + tap */}
|
||||
<PanGestureHandler
|
||||
onGestureEvent={onBrightnessGestureEvent}
|
||||
activeOffsetY={[-5, 5]}
|
||||
failOffsetX={[-20, 20]}
|
||||
onGestureEvent={gestureControls.onBrightnessGestureEvent}
|
||||
activeOffsetY={[-10, 10]}
|
||||
failOffsetX={[-30, 30]}
|
||||
shouldCancelWhenOutside={false}
|
||||
simultaneousHandlers={[]}
|
||||
maxPointers={1}
|
||||
|
|
@ -2644,9 +2550,9 @@ const KSPlayerCore: React.FC = () => {
|
|||
|
||||
{/* Combined gesture handler for right side - volume + tap */}
|
||||
<PanGestureHandler
|
||||
onGestureEvent={onVolumeGestureEvent}
|
||||
activeOffsetY={[-5, 5]}
|
||||
failOffsetX={[-20, 20]}
|
||||
onGestureEvent={gestureControls.onVolumeGestureEvent}
|
||||
activeOffsetY={[-10, 10]}
|
||||
failOffsetX={[-30, 30]}
|
||||
shouldCancelWhenOutside={false}
|
||||
simultaneousHandlers={[]}
|
||||
maxPointers={1}
|
||||
|
|
@ -2776,6 +2682,7 @@ const KSPlayerCore: React.FC = () => {
|
|||
currentResizeMode={resizeMode}
|
||||
setShowAudioModal={setShowAudioModal}
|
||||
setShowSubtitleModal={setShowSubtitleModal}
|
||||
setShowSpeedModal={setShowSpeedModal}
|
||||
isSubtitleModalOpen={showSubtitleModal}
|
||||
setShowSourcesModal={setShowSourcesModal}
|
||||
onSliderValueChange={handleSliderValueChange}
|
||||
|
|
@ -3169,13 +3076,13 @@ const KSPlayerCore: React.FC = () => {
|
|||
/>
|
||||
|
||||
{/* Volume Overlay */}
|
||||
{showVolumeOverlay && (
|
||||
{gestureControls.showVolumeOverlay && (
|
||||
<Animated.View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: getDimensions().width / 2 - 60,
|
||||
top: getDimensions().height / 2 - 60,
|
||||
opacity: volumeOverlayOpacity,
|
||||
opacity: gestureControls.volumeOverlayOpacity,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
|
|
@ -3266,13 +3173,13 @@ const KSPlayerCore: React.FC = () => {
|
|||
)}
|
||||
|
||||
{/* Brightness Overlay */}
|
||||
{showBrightnessOverlay && (
|
||||
{gestureControls.showBrightnessOverlay && (
|
||||
<Animated.View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: getDimensions().width / 2 - 60,
|
||||
top: getDimensions().height / 2 - 60,
|
||||
opacity: brightnessOverlayOpacity,
|
||||
opacity: gestureControls.brightnessOverlayOpacity,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
171
src/hooks/usePlayerGestureControls.ts
Normal file
171
src/hooks/usePlayerGestureControls.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { useRef, useState } from 'react';
|
||||
import { Animated, Platform } from 'react-native';
|
||||
import { PanGestureHandlerGestureEvent, State } from 'react-native-gesture-handler';
|
||||
import * as Brightness from 'expo-brightness';
|
||||
|
||||
interface GestureControlConfig {
|
||||
volume: number;
|
||||
setVolume: (value: number) => void;
|
||||
brightness: number;
|
||||
setBrightness: (value: number) => void;
|
||||
volumeRange?: { min: number; max: number }; // Default: { min: 0, max: 1 }
|
||||
volumeSensitivity?: number; // Default: 0.006 (iOS), 0.0084 (Android with 1.4x multiplier)
|
||||
brightnessSensitivity?: number; // Default: 0.004 (iOS), 0.0056 (Android with 1.4x multiplier)
|
||||
overlayTimeout?: number; // Default: 1500ms
|
||||
debugMode?: boolean;
|
||||
}
|
||||
|
||||
export const usePlayerGestureControls = (config: GestureControlConfig) => {
|
||||
// State for overlays
|
||||
const [showVolumeOverlay, setShowVolumeOverlay] = useState(false);
|
||||
const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false);
|
||||
|
||||
// Animated values
|
||||
const volumeGestureTranslateY = useRef(new Animated.Value(0)).current;
|
||||
const brightnessGestureTranslateY = useRef(new Animated.Value(0)).current;
|
||||
const volumeOverlayOpacity = useRef(new Animated.Value(0)).current;
|
||||
const brightnessOverlayOpacity = useRef(new Animated.Value(0)).current;
|
||||
|
||||
// Tracking refs
|
||||
const lastVolumeGestureY = useRef(0);
|
||||
const lastBrightnessGestureY = useRef(0);
|
||||
const volumeOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const brightnessOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Extract config with defaults and platform adjustments
|
||||
const volumeRange = config.volumeRange || { min: 0, max: 1 };
|
||||
const baseVolumeSensitivity = config.volumeSensitivity || 0.006;
|
||||
const baseBrightnessSensitivity = config.brightnessSensitivity || 0.004;
|
||||
const overlayTimeout = config.overlayTimeout || 1500;
|
||||
|
||||
// Platform-specific sensitivity adjustments
|
||||
// Android needs higher sensitivity due to different touch handling
|
||||
const platformMultiplier = Platform.OS === 'android' ? 1.6 : 1.0;
|
||||
const volumeSensitivity = baseVolumeSensitivity * platformMultiplier;
|
||||
const brightnessSensitivity = baseBrightnessSensitivity * platformMultiplier;
|
||||
|
||||
// Volume gesture handler
|
||||
const onVolumeGestureEvent = Animated.event(
|
||||
[{ nativeEvent: { translationY: volumeGestureTranslateY } }],
|
||||
{
|
||||
useNativeDriver: false,
|
||||
listener: (event: PanGestureHandlerGestureEvent) => {
|
||||
const { translationY, state } = event.nativeEvent;
|
||||
|
||||
if (state === State.ACTIVE) {
|
||||
// Auto-initialize on first active frame
|
||||
if (Math.abs(translationY) < 5 && Math.abs(lastVolumeGestureY.current - translationY) > 20) {
|
||||
lastVolumeGestureY.current = translationY;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate delta from last position
|
||||
const deltaY = -(translationY - lastVolumeGestureY.current);
|
||||
lastVolumeGestureY.current = translationY;
|
||||
|
||||
// Normalize sensitivity based on volume range
|
||||
const rangeMultiplier = volumeRange.max - volumeRange.min;
|
||||
const volumeChange = deltaY * volumeSensitivity * rangeMultiplier;
|
||||
const newVolume = Math.max(volumeRange.min, Math.min(volumeRange.max, config.volume + volumeChange));
|
||||
|
||||
config.setVolume(newVolume);
|
||||
|
||||
if (config.debugMode) {
|
||||
console.log(`[GestureControls] Volume set to: ${newVolume} (Platform: ${Platform.OS}, Sensitivity: ${volumeSensitivity})`);
|
||||
}
|
||||
|
||||
// Show overlay
|
||||
if (!showVolumeOverlay) {
|
||||
setShowVolumeOverlay(true);
|
||||
volumeOverlayOpacity.setValue(1);
|
||||
}
|
||||
|
||||
// Reset hide timer
|
||||
if (volumeOverlayTimeout.current) {
|
||||
clearTimeout(volumeOverlayTimeout.current);
|
||||
}
|
||||
volumeOverlayTimeout.current = setTimeout(() => {
|
||||
Animated.timing(volumeOverlayOpacity, {
|
||||
toValue: 0,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}).start(() => setShowVolumeOverlay(false));
|
||||
}, overlayTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Brightness gesture handler
|
||||
const onBrightnessGestureEvent = Animated.event(
|
||||
[{ nativeEvent: { translationY: brightnessGestureTranslateY } }],
|
||||
{
|
||||
useNativeDriver: false,
|
||||
listener: (event: PanGestureHandlerGestureEvent) => {
|
||||
const { translationY, state } = event.nativeEvent;
|
||||
|
||||
if (state === State.ACTIVE) {
|
||||
// Auto-initialize
|
||||
if (Math.abs(translationY) < 5 && Math.abs(lastBrightnessGestureY.current - translationY) > 20) {
|
||||
lastBrightnessGestureY.current = translationY;
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaY = -(translationY - lastBrightnessGestureY.current);
|
||||
lastBrightnessGestureY.current = translationY;
|
||||
|
||||
const brightnessChange = deltaY * brightnessSensitivity;
|
||||
const newBrightness = Math.max(0, Math.min(1, config.brightness + brightnessChange));
|
||||
|
||||
config.setBrightness(newBrightness);
|
||||
Brightness.setBrightnessAsync(newBrightness).catch(() => {});
|
||||
|
||||
if (config.debugMode) {
|
||||
console.log(`[GestureControls] Device brightness set to: ${newBrightness} (Platform: ${Platform.OS}, Sensitivity: ${brightnessSensitivity})`);
|
||||
}
|
||||
|
||||
if (!showBrightnessOverlay) {
|
||||
setShowBrightnessOverlay(true);
|
||||
brightnessOverlayOpacity.setValue(1);
|
||||
}
|
||||
|
||||
if (brightnessOverlayTimeout.current) {
|
||||
clearTimeout(brightnessOverlayTimeout.current);
|
||||
}
|
||||
brightnessOverlayTimeout.current = setTimeout(() => {
|
||||
Animated.timing(brightnessOverlayOpacity, {
|
||||
toValue: 0,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}).start(() => setShowBrightnessOverlay(false));
|
||||
}, overlayTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Cleanup function
|
||||
const cleanup = () => {
|
||||
if (volumeOverlayTimeout.current) {
|
||||
clearTimeout(volumeOverlayTimeout.current);
|
||||
}
|
||||
if (brightnessOverlayTimeout.current) {
|
||||
clearTimeout(brightnessOverlayTimeout.current);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// Gesture handlers
|
||||
onVolumeGestureEvent,
|
||||
onBrightnessGestureEvent,
|
||||
|
||||
// Overlay state
|
||||
showVolumeOverlay,
|
||||
showBrightnessOverlay,
|
||||
volumeOverlayOpacity,
|
||||
brightnessOverlayOpacity,
|
||||
|
||||
// Cleanup
|
||||
cleanup,
|
||||
};
|
||||
};
|
||||
Loading…
Reference in a new issue