mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-20 16:22:04 +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
|
// Guards to avoid repeated auto-starts
|
||||||
const startedOnFocusRef = useRef(false);
|
const startedOnFocusRef = useRef(false);
|
||||||
const startedOnReadyRef = 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
|
// Animation values for trailer unmute effects
|
||||||
const actionButtonsOpacity = useSharedValue(1);
|
const actionButtonsOpacity = useSharedValue(1);
|
||||||
|
|
@ -1691,7 +1688,6 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
}, [isFocused, setTrailerPlaying]);
|
}, [isFocused, setTrailerPlaying]);
|
||||||
|
|
||||||
// Ultra-optimized scroll-based pause/resume with cached calculations
|
// Ultra-optimized scroll-based pause/resume with cached calculations
|
||||||
// This worklet only sets flags - actual pause/resume happens asynchronously to avoid blocking scroll
|
|
||||||
useDerivedValue(() => {
|
useDerivedValue(() => {
|
||||||
'worklet';
|
'worklet';
|
||||||
try {
|
try {
|
||||||
|
|
@ -1704,49 +1700,21 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
const isPlaying = isPlayingSV.value === 1;
|
const isPlaying = isPlayingSV.value === 1;
|
||||||
const isPausedByScroll = pausedByScrollSV.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) {
|
if (y > pauseThreshold && isPlaying && !isPausedByScroll) {
|
||||||
pendingPauseResumeSV.value = 1; // Request pause
|
pausedByScrollSV.value = 1;
|
||||||
|
runOnJS(setTrailerPlaying)(false);
|
||||||
|
isPlayingSV.value = 0;
|
||||||
} else if (y < resumeThreshold && isPausedByScroll) {
|
} else if (y < resumeThreshold && isPausedByScroll) {
|
||||||
pendingPauseResumeSV.value = 2; // Request resume
|
pausedByScrollSV.value = 0;
|
||||||
|
runOnJS(setTrailerPlaying)(true);
|
||||||
|
isPlayingSV.value = 1;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silent error handling for performance
|
// 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
|
// Memory management and cleanup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { useTraktAutosync } from '../../hooks/useTraktAutosync';
|
||||||
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
||||||
import { useMetadata } from '../../hooks/useMetadata';
|
import { useMetadata } from '../../hooks/useMetadata';
|
||||||
import { useSettings } from '../../hooks/useSettings';
|
import { useSettings } from '../../hooks/useSettings';
|
||||||
|
import { usePlayerGestureControls } from '../../hooks/usePlayerGestureControls';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_SUBTITLE_SIZE,
|
DEFAULT_SUBTITLE_SIZE,
|
||||||
|
|
@ -278,8 +279,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
// Debounce resize operations to prevent rapid successive clicks
|
// Debounce resize operations to prevent rapid successive clicks
|
||||||
const resizeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
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);
|
clearTimeout(resizeTimeoutRef.current);
|
||||||
resizeTimeoutRef.current = null;
|
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
|
// Store Android system brightness state to restore on exit/unmount
|
||||||
const originalSystemBrightnessRef = useRef<number | null>(null);
|
const originalSystemBrightnessRef = useRef<number | null>(null);
|
||||||
const originalSystemBrightnessModeRef = 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 [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 lastVolumeChange = useRef<number>(0);
|
||||||
const lastBrightnessChange = 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
|
// iOS startup timing diagnostics
|
||||||
const loadStartAtRef = useRef<number | null>(null);
|
const loadStartAtRef = useRef<number | null>(null);
|
||||||
const firstFrameAtRef = useRef<number | null>(null);
|
const firstFrameAtRef = useRef<number | null>(null);
|
||||||
|
|
@ -743,124 +744,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
return;
|
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
|
// Long press gesture handlers for speed boost
|
||||||
const onLongPressActivated = useCallback(() => {
|
const onLongPressActivated = useCallback(() => {
|
||||||
if (!holdToSpeedEnabled) return;
|
if (!holdToSpeedEnabled) return;
|
||||||
|
|
@ -2902,14 +2785,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
clearTimeout(errorTimeoutRef.current);
|
clearTimeout(errorTimeoutRef.current);
|
||||||
errorTimeoutRef.current = null;
|
errorTimeoutRef.current = null;
|
||||||
}
|
}
|
||||||
if (volumeOverlayTimeout.current) {
|
|
||||||
clearTimeout(volumeOverlayTimeout.current);
|
|
||||||
volumeOverlayTimeout.current = null;
|
|
||||||
}
|
|
||||||
if (brightnessOverlayTimeout.current) {
|
|
||||||
clearTimeout(brightnessOverlayTimeout.current);
|
|
||||||
brightnessOverlayTimeout.current = null;
|
|
||||||
}
|
|
||||||
if (controlsTimeout.current) {
|
if (controlsTimeout.current) {
|
||||||
clearTimeout(controlsTimeout.current);
|
clearTimeout(controlsTimeout.current);
|
||||||
controlsTimeout.current = null;
|
controlsTimeout.current = null;
|
||||||
|
|
@ -2918,14 +2793,13 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
clearTimeout(pauseOverlayTimerRef.current);
|
clearTimeout(pauseOverlayTimerRef.current);
|
||||||
pauseOverlayTimerRef.current = null;
|
pauseOverlayTimerRef.current = null;
|
||||||
}
|
}
|
||||||
if (gestureDebounceRef.current) {
|
|
||||||
clearTimeout(gestureDebounceRef.current);
|
|
||||||
gestureDebounceRef.current = null;
|
|
||||||
}
|
|
||||||
if (progressSaveInterval) {
|
if (progressSaveInterval) {
|
||||||
clearInterval(progressSaveInterval);
|
clearInterval(progressSaveInterval);
|
||||||
setProgressSaveInterval(null);
|
setProgressSaveInterval(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup gesture controls
|
||||||
|
gestureControls.cleanup();
|
||||||
// Best-effort restore of Android system brightness state on unmount
|
// Best-effort restore of Android system brightness state on unmount
|
||||||
if (Platform.OS === 'android') {
|
if (Platform.OS === 'android') {
|
||||||
try {
|
try {
|
||||||
|
|
@ -3323,9 +3197,9 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
simultaneousHandlers={[]}
|
simultaneousHandlers={[]}
|
||||||
>
|
>
|
||||||
<PanGestureHandler
|
<PanGestureHandler
|
||||||
onGestureEvent={onBrightnessGestureEvent}
|
onGestureEvent={gestureControls.onBrightnessGestureEvent}
|
||||||
activeOffsetY={[-5, 5]}
|
activeOffsetY={[-10, 10]}
|
||||||
failOffsetX={[-20, 20]}
|
failOffsetX={[-30, 30]}
|
||||||
shouldCancelWhenOutside={false}
|
shouldCancelWhenOutside={false}
|
||||||
simultaneousHandlers={[]}
|
simultaneousHandlers={[]}
|
||||||
maxPointers={1}
|
maxPointers={1}
|
||||||
|
|
@ -3356,9 +3230,9 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
simultaneousHandlers={[]}
|
simultaneousHandlers={[]}
|
||||||
>
|
>
|
||||||
<PanGestureHandler
|
<PanGestureHandler
|
||||||
onGestureEvent={onVolumeGestureEvent}
|
onGestureEvent={gestureControls.onVolumeGestureEvent}
|
||||||
activeOffsetY={[-5, 5]}
|
activeOffsetY={[-10, 10]}
|
||||||
failOffsetX={[-20, 20]}
|
failOffsetX={[-30, 30]}
|
||||||
shouldCancelWhenOutside={false}
|
shouldCancelWhenOutside={false}
|
||||||
simultaneousHandlers={[]}
|
simultaneousHandlers={[]}
|
||||||
maxPointers={1}
|
maxPointers={1}
|
||||||
|
|
@ -3974,13 +3848,13 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Volume Overlay */}
|
{/* Volume Overlay */}
|
||||||
{showVolumeOverlay && (
|
{gestureControls.showVolumeOverlay && (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: screenDimensions.width / 2 - 60,
|
left: screenDimensions.width / 2 - 60,
|
||||||
top: screenDimensions.height / 2 - 60,
|
top: screenDimensions.height / 2 - 60,
|
||||||
opacity: volumeOverlayOpacity,
|
opacity: gestureControls.volumeOverlayOpacity,
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -4071,13 +3945,13 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Brightness Overlay */}
|
{/* Brightness Overlay */}
|
||||||
{showBrightnessOverlay && (
|
{gestureControls.showBrightnessOverlay && (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: screenDimensions.width / 2 - 60,
|
left: screenDimensions.width / 2 - 60,
|
||||||
top: screenDimensions.height / 2 - 60,
|
top: screenDimensions.height / 2 - 60,
|
||||||
opacity: brightnessOverlayOpacity,
|
opacity: gestureControls.brightnessOverlayOpacity,
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { useTraktAutosync } from '../../hooks/useTraktAutosync';
|
||||||
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
||||||
import { useMetadata } from '../../hooks/useMetadata';
|
import { useMetadata } from '../../hooks/useMetadata';
|
||||||
import { useSettings } from '../../hooks/useSettings';
|
import { useSettings } from '../../hooks/useSettings';
|
||||||
|
import { usePlayerGestureControls } from '../../hooks/usePlayerGestureControls';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_SUBTITLE_SIZE,
|
DEFAULT_SUBTITLE_SIZE,
|
||||||
|
|
@ -125,6 +126,7 @@ const KSPlayerCore: React.FC = () => {
|
||||||
const ksPlayerRef = useRef<KSPlayerRef>(null);
|
const ksPlayerRef = useRef<KSPlayerRef>(null);
|
||||||
const [showAudioModal, setShowAudioModal] = useState(false);
|
const [showAudioModal, setShowAudioModal] = useState(false);
|
||||||
const [showSubtitleModal, setShowSubtitleModal] = useState(false);
|
const [showSubtitleModal, setShowSubtitleModal] = useState(false);
|
||||||
|
const [showSpeedModal, setShowSpeedModal] = useState(false);
|
||||||
const [initialPosition, setInitialPosition] = useState<number | null>(null);
|
const [initialPosition, setInitialPosition] = useState<number | null>(null);
|
||||||
const [progressSaveInterval, setProgressSaveInterval] = useState<NodeJS.Timeout | null>(null);
|
const [progressSaveInterval, setProgressSaveInterval] = useState<NodeJS.Timeout | null>(null);
|
||||||
const [isInitialSeekComplete, setIsInitialSeekComplete] = useState(false);
|
const [isInitialSeekComplete, setIsInitialSeekComplete] = useState(false);
|
||||||
|
|
@ -297,15 +299,19 @@ const KSPlayerCore: React.FC = () => {
|
||||||
// Volume and brightness controls
|
// Volume and brightness controls
|
||||||
const [volume, setVolume] = useState(100); // KSPlayer uses 0-100 range
|
const [volume, setVolume] = useState(100); // KSPlayer uses 0-100 range
|
||||||
const [brightness, setBrightness] = useState(1.0);
|
const [brightness, setBrightness] = useState(1.0);
|
||||||
const [showVolumeOverlay, setShowVolumeOverlay] = useState(false);
|
|
||||||
const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false);
|
|
||||||
const [subtitleSettingsLoaded, setSubtitleSettingsLoaded] = useState(false);
|
const [subtitleSettingsLoaded, setSubtitleSettingsLoaded] = useState(false);
|
||||||
const volumeOverlayOpacity = useRef(new Animated.Value(0)).current;
|
|
||||||
const brightnessOverlayOpacity = useRef(new Animated.Value(0)).current;
|
// Use reusable gesture controls hook
|
||||||
const volumeOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
|
const gestureControls = usePlayerGestureControls({
|
||||||
const brightnessOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
|
volume,
|
||||||
const lastVolumeChange = useRef<number>(0);
|
setVolume,
|
||||||
const lastBrightnessChange = useRef<number>(0);
|
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)
|
// Get metadata to access logo (only if we have a valid id)
|
||||||
const shouldLoadMetadata = Boolean(id && type);
|
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(() => {
|
useEffect(() => {
|
||||||
if (videoAspectRatio && effectiveDimensions.width > 0 && effectiveDimensions.height > 0) {
|
if (videoAspectRatio && effectiveDimensions.width > 0 && effectiveDimensions.height > 0) {
|
||||||
const styles = calculateVideoStyles(
|
const styles = calculateVideoStyles(
|
||||||
|
|
@ -2211,12 +2119,10 @@ const KSPlayerCore: React.FC = () => {
|
||||||
if (errorTimeoutRef.current) {
|
if (errorTimeoutRef.current) {
|
||||||
clearTimeout(errorTimeoutRef.current);
|
clearTimeout(errorTimeoutRef.current);
|
||||||
}
|
}
|
||||||
if (volumeOverlayTimeout.current) {
|
|
||||||
clearTimeout(volumeOverlayTimeout.current);
|
// Cleanup gesture controls
|
||||||
}
|
gestureControls.cleanup();
|
||||||
if (brightnessOverlayTimeout.current) {
|
|
||||||
clearTimeout(brightnessOverlayTimeout.current);
|
|
||||||
}
|
|
||||||
if (startupRetryTimerRef.current) {
|
if (startupRetryTimerRef.current) {
|
||||||
clearTimeout(startupRetryTimerRef.current);
|
clearTimeout(startupRetryTimerRef.current);
|
||||||
startupRetryTimerRef.current = null;
|
startupRetryTimerRef.current = null;
|
||||||
|
|
@ -2619,9 +2525,9 @@ const KSPlayerCore: React.FC = () => {
|
||||||
>
|
>
|
||||||
{/* Combined gesture handler for left side - brightness + tap */}
|
{/* Combined gesture handler for left side - brightness + tap */}
|
||||||
<PanGestureHandler
|
<PanGestureHandler
|
||||||
onGestureEvent={onBrightnessGestureEvent}
|
onGestureEvent={gestureControls.onBrightnessGestureEvent}
|
||||||
activeOffsetY={[-5, 5]}
|
activeOffsetY={[-10, 10]}
|
||||||
failOffsetX={[-20, 20]}
|
failOffsetX={[-30, 30]}
|
||||||
shouldCancelWhenOutside={false}
|
shouldCancelWhenOutside={false}
|
||||||
simultaneousHandlers={[]}
|
simultaneousHandlers={[]}
|
||||||
maxPointers={1}
|
maxPointers={1}
|
||||||
|
|
@ -2644,9 +2550,9 @@ const KSPlayerCore: React.FC = () => {
|
||||||
|
|
||||||
{/* Combined gesture handler for right side - volume + tap */}
|
{/* Combined gesture handler for right side - volume + tap */}
|
||||||
<PanGestureHandler
|
<PanGestureHandler
|
||||||
onGestureEvent={onVolumeGestureEvent}
|
onGestureEvent={gestureControls.onVolumeGestureEvent}
|
||||||
activeOffsetY={[-5, 5]}
|
activeOffsetY={[-10, 10]}
|
||||||
failOffsetX={[-20, 20]}
|
failOffsetX={[-30, 30]}
|
||||||
shouldCancelWhenOutside={false}
|
shouldCancelWhenOutside={false}
|
||||||
simultaneousHandlers={[]}
|
simultaneousHandlers={[]}
|
||||||
maxPointers={1}
|
maxPointers={1}
|
||||||
|
|
@ -2776,6 +2682,7 @@ const KSPlayerCore: React.FC = () => {
|
||||||
currentResizeMode={resizeMode}
|
currentResizeMode={resizeMode}
|
||||||
setShowAudioModal={setShowAudioModal}
|
setShowAudioModal={setShowAudioModal}
|
||||||
setShowSubtitleModal={setShowSubtitleModal}
|
setShowSubtitleModal={setShowSubtitleModal}
|
||||||
|
setShowSpeedModal={setShowSpeedModal}
|
||||||
isSubtitleModalOpen={showSubtitleModal}
|
isSubtitleModalOpen={showSubtitleModal}
|
||||||
setShowSourcesModal={setShowSourcesModal}
|
setShowSourcesModal={setShowSourcesModal}
|
||||||
onSliderValueChange={handleSliderValueChange}
|
onSliderValueChange={handleSliderValueChange}
|
||||||
|
|
@ -3169,13 +3076,13 @@ const KSPlayerCore: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Volume Overlay */}
|
{/* Volume Overlay */}
|
||||||
{showVolumeOverlay && (
|
{gestureControls.showVolumeOverlay && (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: getDimensions().width / 2 - 60,
|
left: getDimensions().width / 2 - 60,
|
||||||
top: getDimensions().height / 2 - 60,
|
top: getDimensions().height / 2 - 60,
|
||||||
opacity: volumeOverlayOpacity,
|
opacity: gestureControls.volumeOverlayOpacity,
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -3266,13 +3173,13 @@ const KSPlayerCore: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Brightness Overlay */}
|
{/* Brightness Overlay */}
|
||||||
{showBrightnessOverlay && (
|
{gestureControls.showBrightnessOverlay && (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: getDimensions().width / 2 - 60,
|
left: getDimensions().width / 2 - 60,
|
||||||
top: getDimensions().height / 2 - 60,
|
top: getDimensions().height / 2 - 60,
|
||||||
opacity: brightnessOverlayOpacity,
|
opacity: gestureControls.brightnessOverlayOpacity,
|
||||||
zIndex: 1000,
|
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