Compare commits

...

5 Commits

Author SHA1 Message Date
Yiannis Christodoulou
5625239c9d build assets 2025-10-20 01:16:31 +03:00
Yiannis Christodoulou
f90e03740e 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.
2025-10-20 01:10:50 +03:00
Yiannis Christodoulou
c33b4a6a17 Keep player controls visible when settings menu is open
Introduces methods to keep the video player controls visible while the custom settings menu is open by periodically setting the player to active. Ensures the interval is cleared when the menu is closed or the component is disposed to prevent unintended behavior.
2025-10-20 00:29:58 +03:00
Yiannis Christodoulou
41b0bdcb50 For embed players, use longer timeout to keep controls visible 2025-10-20 00:29:29 +03:00
Yiannis Christodoulou
61bfa67e42 Comment out stopPropagation in settings menu events
Replaces all e.stopPropagation() calls with comments in CustomSettingsMenu.js. This change allows event propagation for touch and click events, potentially improving compatibility with other UI components or event handlers.
2025-10-20 00:22:22 +03:00
6 changed files with 302 additions and 140 deletions

View File

@ -213,7 +213,7 @@ class CustomSettingsMenu extends Component {
// Only trigger if it's a tap (not a swipe)
if (distance < 50) {
e.preventDefault();
e.stopPropagation();
// e.stopPropagation();
touchHandled = true;
this.toggleSettings(e);
}
@ -227,7 +227,7 @@ class CustomSettingsMenu extends Component {
// Only handle click if touch wasn't already handled
if (!touchHandled) {
e.preventDefault();
e.stopPropagation();
// e.stopPropagation();
this.toggleSettings(e);
}
touchHandled = false; // Reset for next interaction
@ -347,7 +347,7 @@ class CustomSettingsMenu extends Component {
this.settingsOverlay.addEventListener(
'touchstart',
(e) => {
e.stopPropagation();
// e.stopPropagation();
},
{ passive: true }
);
@ -562,16 +562,6 @@ class CustomSettingsMenu extends Component {
}
}
// Cleanup method
dispose() {
this.stopSubtitleSync();
// Remove event listeners
document.removeEventListener('click', this.handleClickOutside);
// Remove text track change listener
if (this.player()) {
this.player().off('texttrackchange');
}
}
getAvailableQualities() {
// Priority: provided options -> MEDIA_DATA JSON -> player sources -> default
@ -656,7 +646,7 @@ class CustomSettingsMenu extends Component {
const closeButton = this.settingsOverlay.querySelector('.settings-close-btn');
if (closeButton) {
const closeFunction = (e) => {
e.stopPropagation();
// e.stopPropagation();
this.settingsOverlay.classList.remove('show');
this.settingsOverlay.style.display = 'none';
this.speedSubmenu.style.display = 'none';
@ -681,7 +671,7 @@ class CustomSettingsMenu extends Component {
// Settings item clicks
this.settingsOverlay.addEventListener('click', (e) => {
e.stopPropagation();
// e.stopPropagation();
if (e.target.closest('[data-setting="playback-speed"]')) {
this.speedSubmenu.style.display = 'flex';
@ -722,7 +712,7 @@ class CustomSettingsMenu extends Component {
this.settingsOverlay.addEventListener(
'touchend',
(e) => {
e.stopPropagation();
// e.stopPropagation();
if (this.isTouchScrolling) {
this.isTouchScrolling = false;
return;
@ -760,7 +750,7 @@ class CustomSettingsMenu extends Component {
'touchend',
(e) => {
e.preventDefault();
e.stopPropagation();
// e.stopPropagation();
this.speedSubmenu.style.display = 'none';
},
{ passive: false }
@ -775,7 +765,7 @@ class CustomSettingsMenu extends Component {
'touchend',
(e) => {
e.preventDefault();
e.stopPropagation();
// e.stopPropagation();
this.qualitySubmenu.style.display = 'none';
},
{ passive: false }
@ -790,7 +780,7 @@ class CustomSettingsMenu extends Component {
'touchend',
(e) => {
e.preventDefault();
e.stopPropagation();
// e.stopPropagation();
this.subtitlesSubmenu.style.display = 'none';
},
{ passive: false }
@ -826,7 +816,7 @@ class CustomSettingsMenu extends Component {
this.speedSubmenu.addEventListener(
'touchend',
(e) => {
e.stopPropagation();
// e.stopPropagation();
if (this.isTouchScrolling) {
this.isTouchScrolling = false;
return;
@ -870,7 +860,7 @@ class CustomSettingsMenu extends Component {
this.qualitySubmenu.addEventListener(
'touchend',
(e) => {
e.stopPropagation();
// e.stopPropagation();
if (this.isTouchScrolling) {
this.isTouchScrolling = false;
return;
@ -915,7 +905,7 @@ class CustomSettingsMenu extends Component {
this.subtitlesSubmenu.addEventListener(
'touchend',
(e) => {
e.stopPropagation();
// e.stopPropagation();
if (this.isTouchScrolling) {
this.isTouchScrolling = false;
return;
@ -953,13 +943,16 @@ class CustomSettingsMenu extends Component {
}
toggleSettings(e) {
e.stopPropagation();
// e.stopPropagation();
const isVisible = this.settingsOverlay.classList.contains('show');
if (isVisible) {
this.settingsOverlay.classList.remove('show');
this.settingsOverlay.style.display = 'none';
// Stop keeping controls visible
this.stopKeepingControlsVisible();
// Restore body scroll on mobile when closing
if (this.isMobile) {
document.body.style.overflow = '';
@ -970,6 +963,9 @@ class CustomSettingsMenu extends Component {
this.settingsOverlay.classList.add('show');
this.settingsOverlay.style.display = 'block';
// Keep controls visible while settings menu is open
this.keepControlsVisible();
// Add haptic feedback on mobile when opening
if (this.isMobile && navigator.vibrate) {
navigator.vibrate(30);
@ -1029,11 +1025,40 @@ class CustomSettingsMenu extends Component {
return this.settingsOverlay && this.settingsOverlay.classList.contains('show');
}
// Keep controls visible while settings menu is open
keepControlsVisible() {
const player = this.player();
if (!player) return;
// Keep player in active state
player.userActive(true);
// Set up interval to periodically keep player active
this.controlsVisibilityInterval = setInterval(() => {
if (this.isMenuOpen()) {
player.userActive(true);
} else {
this.stopKeepingControlsVisible();
}
}, 1000); // Check every second
}
// Stop keeping controls visible
stopKeepingControlsVisible() {
if (this.controlsVisibilityInterval) {
clearInterval(this.controlsVisibilityInterval);
this.controlsVisibilityInterval = null;
}
}
// Close the settings menu
closeMenu() {
if (this.settingsOverlay) {
this.settingsOverlay.classList.remove('show');
this.settingsOverlay.style.display = 'none';
// Stop keeping controls visible
this.stopKeepingControlsVisible();
this.speedSubmenu.style.display = 'none';
if (this.qualitySubmenu) this.qualitySubmenu.style.display = 'none';
if (this.subtitlesSubmenu) this.subtitlesSubmenu.style.display = 'none';
@ -1447,6 +1472,17 @@ class CustomSettingsMenu extends Component {
}
dispose() {
// Stop subtitle sync and clear interval
this.stopSubtitleSync();
// Stop keeping controls visible
this.stopKeepingControlsVisible();
// Remove text track change listener
if (this.player()) {
this.player().off('texttrackchange');
}
// Remove event listeners
document.removeEventListener('click', this.handleClickOutside);

View File

@ -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 */

View File

@ -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
@ -1592,14 +1600,16 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
// Default sample video
return [
/* {
src: '/videos/sample-video-white.mp4',
type: 'video/mp4',
}, */
{
src: '/videos/sample-video.mp4',
// src: '/videos/sample-video-white.mp4',
//src: '/videos/sample-video.big.mp4',
type: 'video/mp4',
},
/* {
src: '/videos/sample-video.mp3',
type: 'audio/mpeg',
},
}, */
];
};
@ -1921,8 +1931,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
// Milliseconds of inactivity before user considered inactive (0 = never)
// For embed players, use longer timeout to keep controls visible
//inactivityTimeout: isEmbedPlayer ? 5000 : 2000,
inactivityTimeout: 2000,
inactivityTimeout: isEmbedPlayer || isTouchDevice ? 5000 : 2000,
// Language code for player (e.g., 'en', 'es', 'fr')
language: 'en',
@ -2088,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,
@ -2558,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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long