From b04ad2344cf9f778ddd6fb59f729b883fad3592e Mon Sep 17 00:00:00 2001 From: Yiannis Christodoulou Date: Sun, 28 Sep 2025 18:02:56 +0300 Subject: [PATCH] fix: Make seekbar more touch-friendly on Android --- frontend-tools/video-js/src/VideoJS.css | 92 ++++++------- .../components/video-player/VideoJSPlayer.jsx | 125 +++++++++++++++++- 2 files changed, 164 insertions(+), 53 deletions(-) diff --git a/frontend-tools/video-js/src/VideoJS.css b/frontend-tools/video-js/src/VideoJS.css index b0b1e166..c69a415a 100644 --- a/frontend-tools/video-js/src/VideoJS.css +++ b/frontend-tools/video-js/src/VideoJS.css @@ -23,6 +23,37 @@ html { visibility: hidden !important; } + /* Simple fix: Move seekbar up by 10px on touch devices */ + .video-js .vjs-progress-control { + bottom: 56px !important; /* Move up 10px from original 46px */ + } + + /* Make seekbar more touch-friendly on Android */ + .video-js .vjs-progress-holder { + touch-action: pan-x !important; + -webkit-touch-callout: none !important; + -webkit-user-select: none !important; + user-select: none !important; + } + + .video-js .vjs-seek-bar { + touch-action: pan-x !important; + -webkit-touch-callout: none !important; + -webkit-user-select: none !important; + user-select: none !important; + } + + /* Prevent big play button from interfering with seekbar on touch devices */ + .video-js .vjs-big-play-button { + pointer-events: auto !important; + z-index: 1 !important; /* Lower than seekbar */ + } + + .video-js .vjs-progress-control { + z-index: 10 !important; /* Higher than big play button */ + pointer-events: auto !important; + } + /* Exception: Allow intentional touch-activated tooltips */ .video-js .vjs-autoplay-toggle.touch-active::after { opacity: 1 !important; @@ -744,6 +775,7 @@ html { .vjs-slider-horizontal { top: -5px; } + .video-js .vjs-spacer-control { flex: 1 !important; min-width: 1px !important; @@ -1364,56 +1396,6 @@ button.vjs-button > .vjs-icon-placeholder:before { width: auto; opacity: 1; } - - /* Disable all tooltips on touch devices */ - .video-js .vjs-control:hover::after, - .video-js .vjs-control:focus::after, - .video-js .vjs-control:active::after { - display: none !important; - opacity: 0 !important; - visibility: hidden !important; - } - - .video-js .vjs-play-control:hover::after, - .video-js .vjs-mute-control:hover::after, - .video-js .vjs-volume-panel:hover::after, - .video-js .vjs-fullscreen-control:hover::after, - .video-js .vjs-picture-in-picture-control:hover::after, - .video-js .vjs-settings-control:hover::after, - .video-js .vjs-chapters-control:hover::after, - .video-js .vjs-autoplay-toggle:hover::after, - .video-js .vjs-next-video-control:hover::after, - .video-js .vjs-remaining-time:hover::after { - display: none !important; - opacity: 0 !important; - visibility: hidden !important; - } - - /* Disable Video.js native control text tooltips on touch devices */ - .video-js button.vjs-button:hover span.vjs-control-text { - opacity: 0 !important; - visibility: hidden !important; - } - - /* Disable chapter marker tooltips on touch devices */ - .vjs-chapter-marker:hover .vjs-chapter-marker-tooltip { - opacity: 0 !important; - visibility: hidden !important; - } - - /* Disable chapter floating tooltips on touch devices */ - .vjs-chapter-floating-tooltip, - .vjs-sprite-preview-tooltip { - display: none !important; - opacity: 0 !important; - visibility: hidden !important; - } - - /* Exception: Allow touch-activated autoplay tooltip on touch devices */ - .video-js .vjs-autoplay-toggle.touch-active::after { - opacity: 1 !important; - visibility: visible !important; - } } @media (min-width: 1200px) { @@ -1535,6 +1517,11 @@ button.vjs-button > .vjs-icon-placeholder:before { } @media (max-width: 767px) { + /* Move seekbar up by 10px on mobile to prevent accidental button touches */ + .video-js .vjs-progress-control { + bottom: 56px !important; /* Move up 10px from original 46px */ + } + .vjs-related-vdeo-item:nth-child(n + 5) { display: none; } @@ -1773,6 +1760,11 @@ button.vjs-button > .vjs-icon-placeholder:before { } @media (max-width: 480px) { + /* Move seekbar up by 10px on small mobile to prevent accidental button touches */ + .video-js .vjs-progress-control { + bottom: 56px !important; /* Move up 10px from original 46px */ + } + .video-container { padding: 0 10px; } diff --git a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx b/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx index a63101bb..a11abaab 100644 --- a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx +++ b/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx @@ -1047,7 +1047,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) { }, // other - useRoundedCorners: false, + useRoundedCorners: true, isPlayList: true, previewSprite: { url: 'https://deic.mediacms.io/media/original/thumbnails/user/thorkild/2ca18fadeef8475eae513c12cc0830d3.19990812hd_1920_1080_30fps.mp4sprites.jpg', @@ -1998,14 +1998,27 @@ function VideoJSPlayer({ videoId = 'default-video' }) { touchStartTime = Date.now(); const touch = e.touches[0]; touchStartPos = { x: touch.clientX, y: touch.clientY }; + + // Check if touch is in seekbar area + const progressControl = playerRef.current + .getChild('controlBar') + ?.getChild('progressControl'); + if (progressControl && progressControl.el()) { + const progressRect = progressControl.el().getBoundingClientRect(); + const isInSeekbarArea = + touch.clientY >= progressRect.top && touch.clientY <= progressRect.bottom; + if (isInSeekbarArea) { + playerRef.current.seekbarTouching = true; + } + } }; const handleTouchEnd = (e) => { const touchEndTime = Date.now(); const touchDuration = touchEndTime - touchStartTime; - // Only handle if it's a quick tap - if (touchDuration < 500) { + // Only handle if it's a quick tap and we're not touching the seekbar + if (touchDuration < 500 && !playerRef.current.seekbarTouching) { const touch = e.changedTouches[0]; const touchEndPos = { x: touch.clientX, y: touch.clientY }; const distance = Math.sqrt( @@ -2025,6 +2038,13 @@ function VideoJSPlayer({ videoId = 'default-video' }) { } } } + + // Always clear seekbar touching flag at the end + setTimeout(() => { + if (playerRef.current) { + playerRef.current.seekbarTouching = false; + } + }, 50); }; videoEl.addEventListener('touchstart', handleTouchStart, { passive: true }); @@ -2506,6 +2526,100 @@ function VideoJSPlayer({ videoId = 'default-video' }) { // Store components reference for potential cleanup + // BEGIN: Fix Android seekbar touch functionality + if (isTouchDevice) { + setTimeout(() => { + const progressControl = playerRef.current + .getChild('controlBar') + ?.getChild('progressControl'); + const seekBar = progressControl?.getChild('seekBar'); + + if (seekBar && seekBar.el()) { + const seekBarEl = seekBar.el(); + const progressHolder = seekBarEl.querySelector('.vjs-progress-holder'); + + if (progressHolder) { + let isDragging = false; + + const handleTouchStart = (e) => { + isDragging = true; + e.preventDefault(); + e.stopPropagation(); // Prevent event from reaching video element + playerRef.current.userActive(true); + + // Mark that we're interacting with seekbar to prevent play/pause + playerRef.current.seekbarTouching = true; + + // Temporarily disable big play button + const bigPlayButton = playerRef.current.getChild('bigPlayButton'); + if (bigPlayButton && bigPlayButton.el()) { + bigPlayButton.el().style.pointerEvents = 'none'; + bigPlayButton.el().style.touchAction = 'none'; + } + }; + + const handleTouchMove = (e) => { + if (!isDragging) return; + e.preventDefault(); + e.stopPropagation(); // Prevent event from reaching video element + + const touch = e.touches[0]; + const rect = progressHolder.getBoundingClientRect(); + const percentage = Math.max( + 0, + Math.min(1, (touch.clientX - rect.left) / rect.width) + ); + const duration = playerRef.current.duration(); + + if (duration && !isNaN(duration)) { + const newTime = percentage * duration; + playerRef.current.currentTime(newTime); + } + }; + + const handleTouchEnd = (e) => { + isDragging = false; + e.preventDefault(); + e.stopPropagation(); // Prevent event from reaching video element + + // Re-enable big play button + const bigPlayButton = playerRef.current.getChild('bigPlayButton'); + if (bigPlayButton && bigPlayButton.el()) { + setTimeout(() => { + bigPlayButton.el().style.pointerEvents = ''; + bigPlayButton.el().style.touchAction = ''; + }, 200); + } + + // Clear the seekbar touching flag after a longer delay to prevent conflicts + setTimeout(() => { + if (playerRef.current) { + playerRef.current.seekbarTouching = false; + } + }, 300); + }; + + // Add touch event listeners specifically for Android + progressHolder.addEventListener('touchstart', handleTouchStart, { + passive: false, + }); + progressHolder.addEventListener('touchmove', handleTouchMove, { + passive: false, + }); + progressHolder.addEventListener('touchend', handleTouchEnd, { passive: false }); + + // Store cleanup function + customComponents.current.cleanupSeekbarTouch = () => { + progressHolder.removeEventListener('touchstart', handleTouchStart); + progressHolder.removeEventListener('touchmove', handleTouchMove); + progressHolder.removeEventListener('touchend', handleTouchEnd); + }; + } + } + }, 500); + } + // END: Fix Android seekbar touch functionality + // BEGIN: Add comprehensive keyboard event handling const handleAllKeyboardEvents = (event) => { // Only handle if no input elements are focused @@ -2805,6 +2919,11 @@ function VideoJSPlayer({ videoId = 'default-video' }) { customComponents.current.cleanupArrowKeyHandler(); } + // Clean up seekbar touch handlers if they exist + if (customComponents.current && customComponents.current.cleanupSeekbarTouch) { + customComponents.current.cleanupSeekbarTouch(); + } + if (playerRef.current && !playerRef.current.isDisposed()) { playerRef.current.dispose(); playerRef.current = null;