feat: Create the component CustomRemainingTime

This commit is contained in:
Yiannis Christodoulou 2025-07-13 15:34:36 +03:00
parent b314f7d628
commit 3d08f3b29f
3 changed files with 293 additions and 211 deletions

View File

@ -35,15 +35,6 @@
box-sizing: border-box;
}
/* Responsive App container */
.App {
width: 100%;
max-width: 1400px;
margin: 0 auto;
padding: 0 20px;
box-sizing: border-box;
}
/* Ensure video.js responsive behavior */
.video-js.vjs-fluid {
width: 100% !important;
@ -248,9 +239,8 @@
.vjs-chapter-floating-tooltip {
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif !important;
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif !important;
line-height: 1.4 !important;
animation: fadeIn 0.2s ease-in-out;
}
@ -353,3 +343,106 @@
font-size: 12px !important;
}
}
/* Played portion, buffered portion, unplayed portion */
.vjs-play-progress {
background-color: #019932 !important;
}
.vjs-load-progress {
background: rgba(255, 255, 255, 0.5) !important;
}
.vjs-progress-holder {
background: rgba(255, 255, 255, 0.5) !important;
}
/* Move progress control out of control bar and position it above */
.video-js .vjs-progress-control {
position: absolute !important;
bottom: 46px !important;
left: 0 !important;
right: 0 !important;
width: 100% !important;
height: 0 !important;
z-index: 3 !important;
padding: 0 !important;
margin: 0 auto !important;
}
/* Hide the original progress control from the control bar */
.video-js .vjs-control-bar .vjs-progress-control {
display: none !important;
}
/* Optional: Ensure the progress control is visible */
.video-js .vjs-progress-control.vjs-control {
display: block !important;
}
/* Make the seek bar thicker */
/* .video-js .vjs-play-progress,
.video-js .vjs-load-progress,
.video-js .vjs-progress-holder {
height: 4px !important;
} */
/* Remove the semi-transparent background from control bar */
.video-js .vjs-control-bar {
background: transparent !important;
background-color: transparent !important;
background-image: none !important;
display: flex !important;
flex-direction: row !important;
align-items: center !important;
justify-content: flex-start !important;
gap: 6px !important;
}
/* Push specific buttons to the right */
.video-js .vjs-playback-rate,
.video-js .vjs-picture-in-picture-control,
.video-js .vjs-fullscreen-control {
margin-left: auto !important;
order: 999 !important;
}
.video-js .vjs-picture-in-picture-control {
margin-left: 0 !important;
}
.video-js .vjs-fullscreen-control {
margin-left: 0 !important;
}
/* Make all control bar icons bigger */
.video-js .vjs-control-bar .vjs-icon-placeholder,
.video-js .vjs-control-bar .vjs-button .vjs-icon-placeholder,
.video-js .vjs-control-bar [class*="vjs-icon-"] {
font-size: 1.5em !important; /* 1.5x bigger */
transform: translateY(-28px) !important; /* Move icons up by 3px */
}
.video-js .vjs-control-bar svg {
width: 3em !important;
height: 3em !important;
transform: translateY(-16px) !important;
}
/******** BEGIN: Custom Remaining Time Styles *********/
.vjs-control-bar .custom-remaining-time .vjs-remaining-time-display {
font-size: 14px !important; /* Increase font size */
font-weight: 500;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #fff;
}
/* Ensure proper vertical alignment within control bar */
.vjs-control-bar .custom-remaining-time {
top: -5px;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
/********* END: Custom Remaining Time Styles *********/

View File

@ -0,0 +1,106 @@
// components/controls/CustomRemainingTime.js
import videojs from 'video.js';
// Get the Component base class from Video.js
const Component = videojs.getComponent('Component');
class CustomRemainingTime extends Component {
constructor(player, options) {
super(player, options);
// Bind methods to ensure correct 'this' context
this.updateContent = this.updateContent.bind(this);
// Set up event listeners
this.on(player, 'timeupdate', this.updateContent);
this.on(player, 'durationchange', this.updateContent);
this.on(player, 'loadedmetadata', this.updateContent);
// Store custom options
this.options_ = {
displayNegative: false,
customPrefix: '',
customSuffix: '',
...options,
};
}
/**
* Create the component's DOM element
*/
createEl() {
const el = videojs.dom.createEl('div', {
className: 'vjs-remaining-time vjs-time-control vjs-control custom-remaining-time',
});
// Add ARIA accessibility
el.innerHTML = `
<span class="vjs-control-text" role="presentation">Time Display&nbsp;</span>
<span class="vjs-remaining-time-display" role="presentation">0:00 / 0:00</span>
`;
return el;
}
/**
* Update the time display
*/
updateContent() {
const player = this.player();
const currentTime = player.currentTime();
const duration = player.duration();
const display = this.el().querySelector('.vjs-remaining-time-display');
if (display) {
const formattedCurrentTime = this.formatTime(isNaN(currentTime) ? 0 : currentTime);
const formattedDuration = this.formatTime(isNaN(duration) ? 0 : duration);
display.textContent = `${formattedCurrentTime} / ${formattedDuration}`;
}
}
/**
* Format time with custom logic
*/
formatTime(seconds) {
const { customPrefix, customSuffix } = this.options_;
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
let timeString;
if (hours > 0) {
timeString = `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
timeString = `${minutes}:${secs.toString().padStart(2, '0')}`;
}
return `${customPrefix}${timeString}${customSuffix}`;
}
/**
* Add click handler for additional functionality
*/
handleClick() {
// Example: Toggle between different time formats
console.log('Time display clicked');
// Could toggle between current/duration vs remaining time
}
/**
* Component disposal cleanup
*/
dispose() {
// Clean up any additional resources if needed
super.dispose();
}
}
// Set component name for Video.js
CustomRemainingTime.prototype.controlText_ = 'Time Display';
// Register the component with Video.js
videojs.registerComponent('CustomRemainingTime', CustomRemainingTime);
export default CustomRemainingTime;

View File

@ -6,6 +6,7 @@ import 'video.js/dist/video-js.css';
import EndScreenOverlay from '../overlays/EndScreenOverlay';
import ChapterMarkers from '../markers/ChapterMarkers';
import NextVideoButton from '../controls/NextVideoButton';
import CustomRemainingTime from '../controls/CustomRemainingTime';
function VideoJSPlayer() {
const videoRef = useRef(null);
@ -47,9 +48,7 @@ function VideoJSPlayer() {
sources: mediaData.data?.original_media_url
? [
{
src:
mediaData.siteUrl +
mediaData.data.original_media_url,
src: mediaData.siteUrl + mediaData.data.original_media_url,
type: 'video/mp4',
},
]
@ -155,11 +154,7 @@ function VideoJSPlayer() {
const timer = setTimeout(() => {
// Double-check that we still don't have a player and element exists
if (
!playerRef.current &&
videoRef.current &&
!videoRef.current.player
) {
if (!playerRef.current && videoRef.current && !videoRef.current.player) {
playerRef.current = videojs(videoRef.current, {
// ===== STANDARD <video> ELEMENT OPTIONS =====
@ -318,9 +313,7 @@ function VideoJSPlayer() {
// Function to override play/pause key (default: 'k' and Space)
playPauseKey: function (event) {
return (
event.which === 75 || event.which === 32
); // 'k' or Space
return event.which === 75 || event.which === 32; // 'k' or Space
},
},
},
@ -345,9 +338,10 @@ function VideoJSPlayer() {
},
},
// Remaining time display configuration
remainingTimeDisplay: {
remainingTimeDisplay: false,
/* remainingTimeDisplay: {
displayNegative: true,
},
}, */
// Volume panel configuration
volumePanel: {
@ -425,22 +419,14 @@ function VideoJSPlayer() {
// Event listeners
playerRef.current.on('ready', () => {
// Auto-play video when navigating from next button
const urlParams = new URLSearchParams(
window.location.search
);
const urlParams = new URLSearchParams(window.location.search);
const hasVideoParam = urlParams.get('m');
if (hasVideoParam) {
// Small delay to ensure everything is loaded
setTimeout(() => {
if (
playerRef.current &&
!playerRef.current.isDisposed()
) {
if (playerRef.current && !playerRef.current.isDisposed()) {
playerRef.current.play().catch((error) => {
console.log(
'Autoplay was prevented:',
error
);
console.log('Autoplay was prevented:', error);
});
}
}, 100);
@ -471,16 +457,11 @@ function VideoJSPlayer() {
);
// Create a text track for chapters programmatically
const chaptersTrack = playerRef.current.addTextTrack(
'chapters',
'Chapters',
'en'
);
const chaptersTrack = playerRef.current.addTextTrack('chapters', 'Chapters', 'en');
// Add cues to the chapters track
chaptersData.forEach((chapter) => {
const cue = new (window.VTTCue ||
window.TextTrackCue)(
const cue = new (window.VTTCue || window.TextTrackCue)(
chapter.startTime,
chapter.endTime,
chapter.text
@ -494,44 +475,45 @@ function VideoJSPlayer() {
.getChild('controlBar')
.getChild('progressControl');
if (progressControl) {
const seekBar =
progressControl.getChild('seekBar');
const seekBar = progressControl.getChild('seekBar');
if (seekBar) {
const markers =
seekBar.getChild('ChapterMarkers');
if (
markers &&
markers.updateChapterMarkers
) {
const markers = seekBar.getChild('ChapterMarkers');
if (markers && markers.updateChapterMarkers) {
markers.updateChapterMarkers();
}
}
}
}, 500);
console.log(
'Subtitles loaded but disabled by default - use CC button to enable'
);
console.log('Subtitles loaded but disabled by default - use CC button to enable');
});
// Add Next Video button to control bar and reorder chapters button
playerRef.current.ready(() => {
const controlBar =
playerRef.current.getChild('controlBar');
const nextVideoButton = new NextVideoButton(
playerRef.current
);
// Insert after play button
const controlBar = playerRef.current.getChild('controlBar');
const playToggle = controlBar.getChild('playToggle');
const playToggleIndex = controlBar
.children()
.indexOf(playToggle);
controlBar.addChild(
nextVideoButton,
{},
playToggleIndex + 1
);
const currentTimeDisplay = controlBar.getChild('currentTimeDisplay');
// Implement custom time display component
const customRemainingTime = new CustomRemainingTime(playerRef.current, {
displayNegative: false,
customPrefix: '',
customSuffix: '',
});
// Insert it in the desired position (e.g., after current time display)
if (currentTimeDisplay) {
const currentTimeIndex = controlBar.children().indexOf(currentTimeDisplay);
controlBar.addChild(customRemainingTime, {}, currentTimeIndex + 1);
} else {
controlBar.addChild(customRemainingTime, {}, 2);
}
// Implement custom next video button
const nextVideoButton = new NextVideoButton(playerRef.current);
const playToggleIndex = controlBar.children().indexOf(playToggle); // Insert it after play button
controlBar.addChild(nextVideoButton, {}, playToggleIndex + 1);
// Remove duplicate captions button and move chapters to end
const cleanupControls = () => {
@ -539,10 +521,7 @@ function VideoJSPlayer() {
const allChildren = controlBar.children();
// Try to find and remove captions/subs-caps button (but keep subtitles)
const possibleCaptionButtons = [
'captionsButton',
'subsCapsButton',
];
const possibleCaptionButtons = ['captionsButton', 'subsCapsButton'];
possibleCaptionButtons.forEach((buttonName) => {
const button = controlBar.getChild(buttonName);
if (button) {
@ -550,47 +529,29 @@ function VideoJSPlayer() {
controlBar.removeChild(button);
console.log(`✓ Removed ${buttonName}`);
} catch (e) {
console.log(
`✗ Failed to remove ${buttonName}:`,
e
);
console.log(`✗ Failed to remove ${buttonName}:`, e);
}
}
});
// Alternative: hide buttons we can't remove
allChildren.forEach((child, index) => {
const name = (
child.name_ ||
child.constructor.name ||
''
).toLowerCase();
if (
name.includes('caption') &&
!name.includes('subtitle')
) {
const name = (child.name_ || child.constructor.name || '').toLowerCase();
if (name.includes('caption') && !name.includes('subtitle')) {
child.hide();
console.log(
`✓ Hidden button at index ${index}: ${name}`
);
console.log(`✓ Hidden button at index ${index}: ${name}`);
}
});
// Move chapters button to the very end
const chaptersButton =
controlBar.getChild('chaptersButton');
const chaptersButton = controlBar.getChild('chaptersButton');
if (chaptersButton) {
try {
controlBar.removeChild(chaptersButton);
controlBar.addChild(chaptersButton);
console.log(
'✓ Chapters button moved to last position'
);
console.log('✓ Chapters button moved to last position');
} catch (e) {
console.log(
'✗ Failed to move chapters button:',
e
);
console.log('✗ Failed to move chapters button:', e);
}
}
};
@ -604,15 +565,10 @@ function VideoJSPlayer() {
setTimeout(() => {
const setupClickableMenus = () => {
// Find all menu buttons (chapters, subtitles, etc.)
const menuButtons = [
'chaptersButton',
'subtitlesButton',
'playbackRateMenuButton',
];
const menuButtons = ['chaptersButton', 'subtitlesButton', 'playbackRateMenuButton'];
menuButtons.forEach((buttonName) => {
const button =
controlBar.getChild(buttonName);
const button = controlBar.getChild(buttonName);
if (button && button.menuButton_) {
// Override the menu button behavior
const menuButton = button.menuButton_;
@ -623,75 +579,40 @@ function VideoJSPlayer() {
// Add click-to-toggle behavior
menuButton.on('click', function () {
if (
this.menu.hasClass(
'vjs-lock-showing'
)
) {
this.menu.removeClass(
'vjs-lock-showing'
);
if (this.menu.hasClass('vjs-lock-showing')) {
this.menu.removeClass('vjs-lock-showing');
this.menu.hide();
} else {
this.menu.addClass(
'vjs-lock-showing'
);
this.menu.addClass('vjs-lock-showing');
this.menu.show();
}
});
console.log(
`✓ Made ${buttonName} clickable`
);
console.log(`✓ Made ${buttonName} clickable`);
} else if (button) {
// For buttons without menuButton_ property
const buttonEl = button.el();
if (buttonEl) {
// Add click handler to show/hide menu
buttonEl.addEventListener(
'click',
function (e) {
buttonEl.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
const menu =
buttonEl.querySelector(
'.vjs-menu'
);
const menu = buttonEl.querySelector('.vjs-menu');
if (menu) {
if (
menu.style
.display ===
'block'
) {
menu.style.display =
'none';
if (menu.style.display === 'block') {
menu.style.display = 'none';
} else {
// Hide other menus first
document
.querySelectorAll(
'.vjs-menu'
)
.forEach(
(m) => {
if (
m !==
menu
)
m.style.display =
'none';
}
);
menu.style.display =
'block';
document.querySelectorAll('.vjs-menu').forEach((m) => {
if (m !== menu) m.style.display = 'none';
});
menu.style.display = 'block';
}
}
}
);
});
console.log(
`✓ Added click handler to ${buttonName}`
);
console.log(`✓ Added click handler to ${buttonName}`);
}
}
});
@ -701,15 +622,11 @@ function VideoJSPlayer() {
}, 1500);
// Add chapter markers to progress control
const progressControl =
controlBar.getChild('progressControl');
const progressControl = controlBar.getChild('progressControl');
if (progressControl) {
const progressHolder =
progressControl.getChild('seekBar');
const progressHolder = progressControl.getChild('seekBar');
if (progressHolder) {
const chapterMarkers = new ChapterMarkers(
playerRef.current
);
const chapterMarkers = new ChapterMarkers(playerRef.current);
progressHolder.addChild(chapterMarkers);
}
}
@ -738,23 +655,16 @@ function VideoJSPlayer() {
// Keep controls active after video ends
setTimeout(() => {
if (
playerRef.current &&
!playerRef.current.isDisposed()
) {
if (playerRef.current && !playerRef.current.isDisposed()) {
// Remove vjs-ended class if it disables controls
const playerEl = playerRef.current.el();
if (playerEl) {
// Keep the visual ended state but ensure controls work
const controlBar =
playerRef.current.getChild(
'controlBar'
);
const controlBar = playerRef.current.getChild('controlBar');
if (controlBar) {
controlBar.show();
controlBar.el().style.opacity = '1';
controlBar.el().style.pointerEvents =
'auto';
controlBar.el().style.pointerEvents = 'auto';
}
}
}
@ -762,9 +672,7 @@ function VideoJSPlayer() {
// Prevent creating multiple end screens
if (endScreen) {
console.log(
'End screen already exists, removing previous one'
);
console.log('End screen already exists, removing previous one');
playerRef.current.removeChild(endScreen);
endScreen = null;
}
@ -809,26 +717,15 @@ function VideoJSPlayer() {
});
playerRef.current.on('fullscreenchange', () => {
console.log(
'Fullscreen changed:',
playerRef.current.isFullscreen()
);
console.log('Fullscreen changed:', playerRef.current.isFullscreen());
});
playerRef.current.on('volumechange', () => {
console.log(
'Volume changed:',
playerRef.current.volume(),
'Muted:',
playerRef.current.muted()
);
console.log('Volume changed:', playerRef.current.volume(), 'Muted:', playerRef.current.muted());
});
playerRef.current.on('ratechange', () => {
console.log(
'Playback rate changed:',
playerRef.current.playbackRate()
);
console.log('Playback rate changed:', playerRef.current.playbackRate());
});
playerRef.current.on('texttrackchange', () => {
@ -853,18 +750,13 @@ function VideoJSPlayer() {
// Focus the player element
if (playerRef.current.el()) {
playerRef.current.el().focus();
console.log(
'Video player focused for keyboard controls'
);
console.log('Video player focused for keyboard controls');
}
// Start playing the video immediately if autoplay is enabled
if (playerRef.current.autoplay()) {
playerRef.current.play().catch((error) => {
console.log(
'Autoplay prevented by browser:',
error
);
console.log('Autoplay prevented by browser:', error);
// If autoplay fails, we can still focus the element
// so the user can manually start and use keyboard controls
});
@ -915,21 +807,12 @@ function VideoJSPlayer() {
setTimeout(focusVideo, 500);
return () => {
document.removeEventListener(
'visibilitychange',
handleVisibilityChange
);
document.removeEventListener('visibilitychange', handleVisibilityChange);
window.removeEventListener('focus', handleWindowFocus);
};
}, []);
return (
<video
ref={videoRef}
className='video-js vjs-default-skin'
tabIndex='0'
/>
);
return <video ref={videoRef} className="video-js vjs-default-skin" tabIndex="0" />;
}
export default VideoJSPlayer;