From 4ab102b30ae32dba800a44de2f490091b36486c3 Mon Sep 17 00:00:00 2001 From: kuekhaoyang Date: Mon, 17 Nov 2025 22:31:27 +0800 Subject: [PATCH] feat: Enhance CustomVideoPlayer with volume bar visibility control and keyboard shortcuts for playback and volume adjustments --- components/player/CustomVideoPlayer.tsx | 134 +++++++++++++++++++++--- 1 file changed, 121 insertions(+), 13 deletions(-) diff --git a/components/player/CustomVideoPlayer.tsx b/components/player/CustomVideoPlayer.tsx index fc15ecb..35ff530 100644 --- a/components/player/CustomVideoPlayer.tsx +++ b/components/player/CustomVideoPlayer.tsx @@ -41,11 +41,13 @@ export function CustomVideoPlayer({ const [showSkipBackwardIndicator, setShowSkipBackwardIndicator] = useState(false); const [isSkipForwardAnimatingOut, setIsSkipForwardAnimatingOut] = useState(false); const [isSkipBackwardAnimatingOut, setIsSkipBackwardAnimatingOut] = useState(false); + const [showVolumeBar, setShowVolumeBar] = useState(false); const controlsTimeoutRef = useRef(null); const speedMenuTimeoutRef = useRef(null); const skipForwardTimeoutRef = useRef(null); const skipBackwardTimeoutRef = useRef(null); + const volumeBarTimeoutRef = useRef(null); const isDraggingProgressRef = useRef(false); const isDraggingVolumeRef = useRef(false); @@ -200,6 +202,21 @@ export function CustomVideoPlayer({ } }; + // Show volume bar temporarily + const showVolumeBarTemporarily = () => { + setShowVolumeBar(true); + + // Clear existing timeout + if (volumeBarTimeoutRef.current) { + clearTimeout(volumeBarTimeoutRef.current); + } + + // Hide after 1 second + volumeBarTimeoutRef.current = setTimeout(() => { + setShowVolumeBar(false); + }, 1000); + }; + const handleVolumeChange = (e: React.MouseEvent) => { if (!videoRef.current || !volumeBarRef.current) return; @@ -306,23 +323,23 @@ export function CustomVideoPlayer({ clearTimeout(skipBackwardTimeoutRef.current); } - // Clear existing timeout + // Clear existing timeout to reset the fade out timer if (skipForwardTimeoutRef.current) { clearTimeout(skipForwardTimeoutRef.current); } - // Calculate new skip amount - const newSkipAmount = skipForwardAmount + 10; + // Calculate new skip amount (accumulate if already showing) + const newSkipAmount = showSkipForwardIndicator ? skipForwardAmount + 10 : 10; setSkipForwardAmount(newSkipAmount); setShowSkipForwardIndicator(true); setIsSkipForwardAnimatingOut(false); - // Apply the accumulated skip immediately to video + // Apply the skip to video const targetTime = Math.min(videoRef.current.currentTime + 10, duration); videoRef.current.currentTime = targetTime; setCurrentTime(targetTime); - // Start fade out animation after 300ms + // Start fade out animation after 800ms of inactivity skipForwardTimeoutRef.current = setTimeout(() => { setIsSkipForwardAnimatingOut(true); // Hide indicator after animation completes (200ms) @@ -331,7 +348,7 @@ export function CustomVideoPlayer({ setSkipForwardAmount(0); setIsSkipForwardAnimatingOut(false); }, 200); - }, 300); + }, 800); }; const skipBackward = () => { @@ -345,23 +362,23 @@ export function CustomVideoPlayer({ clearTimeout(skipForwardTimeoutRef.current); } - // Clear existing timeout + // Clear existing timeout to reset the fade out timer if (skipBackwardTimeoutRef.current) { clearTimeout(skipBackwardTimeoutRef.current); } - // Calculate new skip amount - const newSkipAmount = skipBackwardAmount + 10; + // Calculate new skip amount (accumulate if already showing) + const newSkipAmount = showSkipBackwardIndicator ? skipBackwardAmount + 10 : 10; setSkipBackwardAmount(newSkipAmount); setShowSkipBackwardIndicator(true); setIsSkipBackwardAnimatingOut(false); - // Apply the accumulated skip immediately to video + // Apply the skip to video const targetTime = Math.max(videoRef.current.currentTime - 10, 0); videoRef.current.currentTime = targetTime; setCurrentTime(targetTime); - // Start fade out animation after 300ms + // Start fade out animation after 800ms of inactivity skipBackwardTimeoutRef.current = setTimeout(() => { setIsSkipBackwardAnimatingOut(true); // Hide indicator after animation completes (200ms) @@ -370,7 +387,7 @@ export function CustomVideoPlayer({ setSkipBackwardAmount(0); setIsSkipBackwardAnimatingOut(false); }, 200); - }, 300); + }, 800); }; // Cleanup timeout on unmount @@ -382,9 +399,96 @@ export function CustomVideoPlayer({ if (skipBackwardTimeoutRef.current) { clearTimeout(skipBackwardTimeoutRef.current); } + if (volumeBarTimeoutRef.current) { + clearTimeout(volumeBarTimeoutRef.current); + } }; }, []); + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Ignore if user is typing in an input field + const target = e.target as HTMLElement; + if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { + return; + } + + // Prevent default for our shortcuts + const shortcuts = [' ', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'f', 'F', 'm', 'M', 'i', 'I', '<', '>', ',', '.']; + if (shortcuts.includes(e.key)) { + e.preventDefault(); + } + + switch (e.key) { + // Playback control + case ' ': // Spacebar: Play/pause + togglePlay(); + break; + + case 'ArrowLeft': // Left arrow: Rewind 10 seconds + case '<': // <: Rewind 10 seconds + case ',': // Comma key + skipBackward(); + break; + + case 'ArrowRight': // Right arrow: Fast-forward 10 seconds + case '>': // >: Fast-forward 10 seconds + case '.': // Period key + skipForward(); + break; + + // Volume + case 'm': // M: Mute/unmute + case 'M': + toggleMute(); + showVolumeBarTemporarily(); + break; + + case 'ArrowUp': // Up arrow: Increase volume by 5% + if (videoRef.current) { + const newVolume = Math.min(1, volume + 0.05); + setVolume(newVolume); + videoRef.current.volume = newVolume; + setIsMuted(newVolume === 0); + showVolumeBarTemporarily(); + } + break; + + case 'ArrowDown': // Down arrow: Decrease volume by 5% + if (videoRef.current) { + const newVolume = Math.max(0, volume - 0.05); + setVolume(newVolume); + videoRef.current.volume = newVolume; + setIsMuted(newVolume === 0); + showVolumeBarTemporarily(); + } + break; + + // Display + case 'f': // F: Toggle full-screen mode + case 'F': + toggleFullscreen(); + break; + + case 'i': // I: Toggle miniplayer (Picture-in-Picture) + case 'I': + if (isPiPSupported) { + togglePictureInPicture(); + } + break; + } + }; + + // Add event listener + window.addEventListener('keydown', handleKeyDown); + + // Cleanup + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [isPlaying, volume, isMuted, isPiPSupported, togglePlay, toggleMute, toggleFullscreen, togglePictureInPicture, skipForward, skipBackward, showVolumeBarTemporarily]); // Include dependencies + // Playback speed const speeds = [0.5, 0.75, 1, 1.25, 1.5, 2]; @@ -576,7 +680,11 @@ export function CustomVideoPlayer({ {/* Volume Bar */} -
+