From f90e03740e083a8dd3a329ae2f5f02d3d77fcb31 Mon Sep 17 00:00:00 2001 From: Yiannis Christodoulou Date: Mon, 20 Oct 2025 01:10:50 +0300 Subject: [PATCH] Improve caption positioning and sizing for mobile/iOS Enhances subtitle and caption display by adjusting their position above the control bar on iOS using both CSS (::cue) and programmatic cue updates. Increases caption font size responsively for mobile and tablet screens, and ensures native text tracks are used on iOS for better fullscreen support. Dynamically updates cue positioning based on user activity and track changes to improve accessibility and readability. --- .../components/video-player/VideoJSPlayer.css | 55 ++++++++++++- .../components/video-player/VideoJSPlayer.jsx | 78 ++++++++++++++++++- 2 files changed, 129 insertions(+), 4 deletions(-) diff --git a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.css b/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.css index ed92efa5..e560cf28 100644 --- a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.css +++ b/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.css @@ -31,7 +31,60 @@ button { visibility: hidden !important; } -/* Adjust subtitle position when controls are visible */ +/* iOS Native Text Tracks - Position captions above control bar */ +/* Using ::cue which is the only way to style native tracks on iOS */ +video::cue { + line: -4; /* Move captions up by 4 lines from bottom */ +} + +/* Mobile-specific caption font size increases */ +@media (max-width: 767px) { + /* iOS native text tracks */ + video::cue { + font-size: 1em; + } + + /* Video.js text tracks for non-iOS */ + .video-js .vjs-text-track-display { + font-size: 1em !important; + } + + .video-js .vjs-text-track-cue { + font-size: 1em !important; + } +} + +/* Extra small screens - even larger captions */ +@media (max-width: 480px) { + video::cue { + font-size: 1.2em; + } + + .video-js .vjs-text-track-display { + font-size: 1.2em !important; + } + + .video-js .vjs-text-track-cue { + font-size: 1.2em !important; + } +} + +/* Tablet size - moderate increase */ +@media (min-width: 768px) and (max-width: 1024px) { + video::cue { + font-size: 1em; + } + + .video-js .vjs-text-track-display { + font-size: 1em !important; + } + + .video-js .vjs-text-track-cue { + font-size: 1em !important; + } +} + +/* Adjust subtitle position when controls are visible (for non-native Video.js tracks) */ /* When controls are VISIBLE (user is active), add extra bottom margin */ .video-js:not(.vjs-user-inactive) .vjs-text-track-display { margin-bottom: 2em; /* Adjust this value to move subtitles higher when controls are visible */ 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 fe2bc3d8..43c919dc 100644 --- a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx +++ b/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx @@ -185,6 +185,14 @@ function VideoJSPlayer({ videoId = 'default-video' }) { return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; }, []); + // Utility function to detect iOS devices + const isIOS = useMemo(() => { + return ( + /iPad|iPhone|iPod/.test(navigator.userAgent) || + (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) + ); + }, []); + // Environment-based development mode configuration const isDevMode = import.meta.env.VITE_DEV_MODE === 'true' || window.location.hostname.includes('vercel.app'); // Safely access window.MEDIA_DATA with fallback using useMemo @@ -2089,9 +2097,9 @@ function VideoJSPlayer({ videoId = 'default-video' }) { // Use native audio tracks instead of emulated - disabled for consistency nativeAudioTracks: false, - // Use Video.js text tracks for full positioning control on all devices - // Native tracks don't allow CSS positioning control and cause duplicates - nativeTextTracks: true, + // Use native text tracks on iOS for fullscreen caption support + // On other devices, use Video.js text tracks for full CSS positioning control + nativeTextTracks: isIOS, // Use native video tracks instead of emulated - disabled for consistency nativeVideoTracks: false, @@ -2559,6 +2567,70 @@ function VideoJSPlayer({ videoId = 'default-video' }) { playerRef.current.one('canplay', () => userPreferences.current.applySubtitlePreference(playerRef.current) ); + + // iOS-specific: Adjust native text track cues to position them above control bar + if (isIOS && hasSubtitles) { + const adjustIOSCues = (linePosition) => { + // If no line position specified, determine based on user activity + if (linePosition === undefined) { + const isUserInactive = playerRef.current.hasClass('vjs-user-inactive'); + linePosition = isUserInactive ? -2 : -4; + } + + const textTracks = playerRef.current.textTracks(); + for (let i = 0; i < textTracks.length; i++) { + const track = textTracks[i]; + if (track.kind === 'subtitles' || track.kind === 'captions') { + // Wait for cues to load + if (track.cues && track.cues.length > 0) { + for (let j = 0; j < track.cues.length; j++) { + const cue = track.cues[j]; + // Set line position to move captions up + // Negative values count from bottom, positive from top + // -4 when controls visible, -2 when controls hidden + cue.line = linePosition; + cue.size = 90; // Make width 90% to ensure it fits + cue.position = 'auto'; // Center horizontally + cue.align = 'center'; // Center align text + } + } else { + // If cues aren't loaded yet, listen for the cuechange event + const onCueChange = () => { + if (track.cues && track.cues.length > 0) { + for (let j = 0; j < track.cues.length; j++) { + const cue = track.cues[j]; + cue.line = linePosition; + cue.size = 90; + cue.position = 'auto'; + cue.align = 'center'; + } + track.removeEventListener('cuechange', onCueChange); + } + }; + track.addEventListener('cuechange', onCueChange); + } + } + } + }; + + // Try to adjust immediately and also after a delay + setTimeout(() => adjustIOSCues(), 100); + setTimeout(() => adjustIOSCues(), 500); + setTimeout(() => adjustIOSCues(), 1000); + + // Listen for user activity changes to adjust caption position dynamically + playerRef.current.on('userinactive', () => { + adjustIOSCues(-2); // Controls hidden - move captions closer to bottom + }); + + playerRef.current.on('useractive', () => { + adjustIOSCues(-4); // Controls visible - move captions higher + }); + + // Also adjust when tracks change + playerRef.current.textTracks().addEventListener('addtrack', () => adjustIOSCues()); + playerRef.current.textTracks().addEventListener('change', () => adjustIOSCues()); + } // END: Add subtitle tracks // BEGIN: Chapters Implementation