mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-10-06 22:32:41 +07:00
refactor everything related to video.js
This commit is contained in:
@@ -23,3 +23,4 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
.env
|
||||
yt.readme.md
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>VideoJS</title>
|
||||
</head>
|
||||
<body style="padding: 0; margin: 0">
|
||||
<div id="page-embed">
|
||||
<div id="video-js-root-embed-old" class="video-js-root-embed-old"></div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>VideoJS</title>
|
||||
</head>
|
||||
<body style="padding: 0; margin: 0">
|
||||
<div id="video-js-root-main-old" class="video-js-root-main-old"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,6 +8,7 @@
|
||||
</head>
|
||||
<body style="padding: 0; margin: 0">
|
||||
<div id="video-js-root-main" class="video-js-root-main"></div>
|
||||
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -515,14 +515,6 @@ html {
|
||||
background: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
/* Hide HLS buffer segment boundaries while keeping overall load progress */
|
||||
/* Target only the HLS buffer segment divs (they have data-start and data-end attributes) */
|
||||
.vjs-load-progress > div[data-start][data-end] {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.video-js .vjs-progress-control {
|
||||
position: absolute !important;
|
||||
bottom: 42px !important; /* Moved up to prevent overlap with controls */
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { VideoJSPlayerNew } from './components';
|
||||
|
||||
function VideoJSNew({ videoId = 'default-video', ...props }) {
|
||||
return <VideoJSPlayerNew videoId={videoId} {...props} />;
|
||||
}
|
||||
|
||||
export default VideoJSNew;
|
||||
@@ -1,9 +1,19 @@
|
||||
/* ===== AUTOPLAY TOGGLE BUTTON STYLES ===== */
|
||||
|
||||
.vjs-autoplay-toggle .vjs-autoplay-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
/* Font icon styles for autoplay button */
|
||||
.vjs-autoplay-toggle .vjs-icon-placeholder:before {
|
||||
font-size: 1.5em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Use play icon when autoplay is OFF (clicking will turn it ON) */
|
||||
.vjs-autoplay-toggle .vjs-icon-play:before {
|
||||
content: "\f101"; /* VideoJS play icon */
|
||||
}
|
||||
|
||||
/* Use pause icon when autoplay is ON (clicking will turn it OFF) */
|
||||
.vjs-autoplay-toggle .vjs-icon-pause:before {
|
||||
content: "\f103"; /* VideoJS pause icon */
|
||||
}
|
||||
|
||||
.video-js .vjs-autoplay-toggle {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import videojs from 'video.js';
|
||||
import './AutoplayToggleButton.css';
|
||||
import autoPlayIconUrl from '../../assets/icons/autoplay-video-js-play.svg';
|
||||
import autoPauseIconUrl from '../../assets/icons/autoplay-video-js-pause.svg';
|
||||
// import './AutoplayToggleButton.css';
|
||||
|
||||
const Button = videojs.getComponent('Button');
|
||||
|
||||
@@ -9,6 +7,20 @@ const Button = videojs.getComponent('Button');
|
||||
class AutoplayToggleButton extends Button {
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
|
||||
// Check if this is a touch device - don't create button on touch devices
|
||||
const isTouchDevice =
|
||||
options.isTouchDevice ||
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0;
|
||||
|
||||
if (isTouchDevice) {
|
||||
// Hide the button on touch devices
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
this.userPreferences = options.userPreferences;
|
||||
// Get autoplay preference from localStorage, default to false if not set
|
||||
if (this.userPreferences) {
|
||||
@@ -31,18 +43,14 @@ class AutoplayToggleButton extends Button {
|
||||
'aria-label': this.isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off',
|
||||
});
|
||||
|
||||
// Create simple text-based icon for now to ensure it works
|
||||
// Create icon placeholder using VideoJS icon system
|
||||
this.iconSpan = videojs.dom.createEl('span', {
|
||||
'aria-hidden': 'true',
|
||||
className: 'vjs-autoplay-icon',
|
||||
className: 'vjs-icon-placeholder vjs-autoplay-icon',
|
||||
});
|
||||
|
||||
// Set initial icon state directly
|
||||
if (this.isAutoplayEnabled) {
|
||||
this.iconSpan.innerHTML = `<img src="${autoPauseIconUrl}" alt="Autoplay on" style="width: 26px; height: 26px;" />`;
|
||||
} else {
|
||||
this.iconSpan.innerHTML = `<img src="${autoPlayIconUrl}" alt="Autoplay off" style="width: 26px; height: 26px;" />`;
|
||||
}
|
||||
// Set initial icon state using font icons
|
||||
this.updateIconClass();
|
||||
|
||||
// Create control text span
|
||||
const controlTextSpan = videojs.dom.createEl('span', {
|
||||
@@ -60,29 +68,33 @@ class AutoplayToggleButton extends Button {
|
||||
return button;
|
||||
}
|
||||
|
||||
updateIconClass() {
|
||||
// Remove existing icon classes
|
||||
this.iconSpan.className = 'vjs-icon-placeholder vjs-autoplay-icon';
|
||||
|
||||
// Add appropriate icon class based on state
|
||||
if (this.isAutoplayEnabled) {
|
||||
this.iconSpan.classList.add('vjs-icon-spinner');
|
||||
} else {
|
||||
this.iconSpan.classList.add('vjs-icon-play-circle');
|
||||
}
|
||||
}
|
||||
|
||||
updateIcon() {
|
||||
// Add transition and start fade-out
|
||||
this.iconSpan.style.transition = 'opacity 0.1s ease';
|
||||
this.iconSpan.style.opacity = '0';
|
||||
|
||||
// After fade-out complete, update innerHTML and fade back in
|
||||
// After fade-out complete, update icon class and fade back in
|
||||
setTimeout(() => {
|
||||
if (this.isAutoplayEnabled) {
|
||||
this.iconSpan.innerHTML = `<img src="${autoPauseIconUrl}" alt="Autoplay on" style="width: 26px; height: 26px;" />`;
|
||||
if (this.el()) {
|
||||
this.el().title = 'Autoplay is on';
|
||||
this.el().setAttribute('aria-label', 'Autoplay is on');
|
||||
const controlText = this.el().querySelector('.vjs-control-text');
|
||||
if (controlText) controlText.textContent = 'Autoplay is on';
|
||||
}
|
||||
} else {
|
||||
this.iconSpan.innerHTML = `<img src="${autoPlayIconUrl}" alt="Autoplay off" style="width: 26px; height: 26px;" />`;
|
||||
if (this.el()) {
|
||||
this.el().title = 'Autoplay is off';
|
||||
this.el().setAttribute('aria-label', 'Autoplay is off');
|
||||
const controlText = this.el().querySelector('.vjs-control-text');
|
||||
if (controlText) controlText.textContent = 'Autoplay is off';
|
||||
}
|
||||
this.updateIconClass();
|
||||
|
||||
if (this.el()) {
|
||||
this.el().title = this.isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off';
|
||||
this.el().setAttribute('aria-label', this.isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off');
|
||||
const controlText = this.el().querySelector('.vjs-control-text');
|
||||
if (controlText)
|
||||
controlText.textContent = this.isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off';
|
||||
}
|
||||
|
||||
// Fade back in
|
||||
@@ -127,14 +139,12 @@ class AutoplayToggleButton extends Button {
|
||||
}
|
||||
|
||||
let touchStartTime = 0;
|
||||
let touchHandled = false;
|
||||
|
||||
// Touch start
|
||||
button.addEventListener(
|
||||
'touchstart',
|
||||
(e) => {
|
||||
() => {
|
||||
touchStartTime = Date.now();
|
||||
touchHandled = false;
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
@@ -153,7 +163,6 @@ class AutoplayToggleButton extends Button {
|
||||
|
||||
// Show tooltip briefly
|
||||
button.classList.add('touch-active');
|
||||
touchHandled = true;
|
||||
|
||||
// Hide tooltip after shorter delay on mobile
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ReactComponent as PlayIcon } from '/autoplay-video-js-play.svg';
|
||||
import { ReactComponent as PauseIcon } from '/autoplay-video-js-pause.svg';
|
||||
|
||||
const AutoplayToggleButton = ({ isAutoplayEnabled, onToggle, className = '' }) => {
|
||||
const handleClick = () => {
|
||||
onToggle(!isAutoplayEnabled);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`vjs-autoplay-toggle vjs-control vjs-button ${className}`}
|
||||
type="button"
|
||||
title={isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off'}
|
||||
aria-label={isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off'}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span aria-hidden="true" className="vjs-autoplay-icon">
|
||||
{isAutoplayEnabled ? (
|
||||
<PlayIcon style={{ width: '26px', height: '26px' }} />
|
||||
) : (
|
||||
<PauseIcon style={{ width: '26px', height: '26px' }} />
|
||||
)}
|
||||
</span>
|
||||
<span className="vjs-control-text">{isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off'}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoplayToggleButton;
|
||||
@@ -0,0 +1,210 @@
|
||||
/* ===== UNIFIED BUTTON TOOLTIP SYSTEM ===== */
|
||||
/* Comprehensive tooltip styles for all VideoJS buttons */
|
||||
|
||||
/* Base tooltip styles using ::after pseudo-element */
|
||||
.video-js .vjs-control-bar .vjs-control {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Universal tooltip styling for all buttons */
|
||||
.video-js .vjs-control-bar .vjs-control::after {
|
||||
content: attr(title);
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
visibility 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
z-index: 1000;
|
||||
margin-bottom: 10px;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
|
||||
"Droid Sans", "Helvetica Neue", sans-serif;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
/* Show tooltip on hover and focus for desktop */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.video-js .vjs-control-bar .vjs-control:hover::after,
|
||||
.video-js .vjs-control-bar .vjs-control:focus::after {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateX(-50%) translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Specific button tooltips - override content when needed */
|
||||
.video-js .vjs-play-control::after {
|
||||
content: attr(title);
|
||||
}
|
||||
|
||||
.video-js .vjs-mute-control::after {
|
||||
content: attr(title);
|
||||
}
|
||||
|
||||
.video-js .vjs-fullscreen-control::after {
|
||||
content: attr(title);
|
||||
}
|
||||
|
||||
.video-js .vjs-picture-in-picture-toggle::after {
|
||||
content: attr(title);
|
||||
}
|
||||
|
||||
.video-js .vjs-subtitles-button::after {
|
||||
content: attr(title);
|
||||
}
|
||||
|
||||
.video-js .vjs-chapters-button::after {
|
||||
content: attr(title);
|
||||
}
|
||||
|
||||
/* Custom button tooltips */
|
||||
.video-js .vjs-autoplay-toggle::after {
|
||||
content: attr(title);
|
||||
}
|
||||
|
||||
.video-js .vjs-next-video-button::after {
|
||||
content: attr(title);
|
||||
}
|
||||
|
||||
.video-js .vjs-settings-button::after {
|
||||
content: attr(title);
|
||||
}
|
||||
|
||||
.video-js .vjs-remaining-time::after {
|
||||
content: attr(title);
|
||||
}
|
||||
|
||||
/* Touch device support - show tooltips on tap */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
/* Hide tooltips by default on touch devices */
|
||||
.video-js .vjs-control-bar .vjs-control::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show tooltip when touch-active class is added */
|
||||
.video-js .vjs-control-bar .vjs-control.touch-tooltip-active::after {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateX(-50%) translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet-specific adjustments */
|
||||
@media (min-width: 768px) and (max-width: 1024px) and (hover: none) {
|
||||
.video-js .vjs-control-bar .vjs-control.touch-tooltip-active::after {
|
||||
font-size: 14px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile-specific adjustments */
|
||||
@media (max-width: 767px) {
|
||||
.video-js .vjs-control-bar .vjs-control.touch-tooltip-active::after {
|
||||
font-size: 12px;
|
||||
padding: 6px 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Exclude volume and time components from tooltips */
|
||||
.video-js .vjs-volume-panel::after,
|
||||
.video-js .vjs-volume-panel::before,
|
||||
.video-js .vjs-mute-control::after,
|
||||
.video-js .vjs-mute-control::before,
|
||||
.video-js .vjs-volume-control::after,
|
||||
.video-js .vjs-volume-control::before,
|
||||
.video-js .vjs-volume-bar::after,
|
||||
.video-js .vjs-volume-bar::before,
|
||||
.video-js .vjs-remaining-time::after,
|
||||
.video-js .vjs-current-time-display::after,
|
||||
.video-js .vjs-duration-display::after,
|
||||
.video-js .vjs-progress-control::after {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
/* Specifically target volume panel and all its children to remove tooltips */
|
||||
.video-js .vjs-volume-panel[title],
|
||||
.video-js .vjs-volume-panel *[title],
|
||||
.video-js .vjs-mute-control[title],
|
||||
.video-js .vjs-volume-control[title],
|
||||
.video-js .vjs-volume-control *[title],
|
||||
.video-js .vjs-volume-bar[title] {
|
||||
/* These selectors target elements with title attributes */
|
||||
}
|
||||
|
||||
/* Force remove tooltips from volume components using attribute selector */
|
||||
.video-js .vjs-volume-panel,
|
||||
.video-js .vjs-mute-control,
|
||||
.video-js .vjs-volume-control {
|
||||
/* Remove title attribute via CSS (not possible, but we can override the tooltip) */
|
||||
}
|
||||
|
||||
.video-js .vjs-volume-panel:hover::after,
|
||||
.video-js .vjs-volume-panel:focus::after,
|
||||
.video-js .vjs-mute-control:hover::after,
|
||||
.video-js .vjs-mute-control:focus::after {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
/* Tooltip arrow removed - no more triangles */
|
||||
.video-js .vjs-control-bar .vjs-control::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Disable native VideoJS tooltips to prevent conflicts */
|
||||
.video-js .vjs-control-bar .vjs-control .vjs-control-text {
|
||||
position: absolute !important;
|
||||
left: -9999px !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(1px, 1px, 1px, 1px) !important;
|
||||
}
|
||||
|
||||
/* Specifically hide play/pause button text that appears inside the icon */
|
||||
.video-js .vjs-play-control .vjs-control-text,
|
||||
.video-js .vjs-play-control span.vjs-control-text {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
/* Override VideoJS native control text tooltips completely */
|
||||
.video-js button.vjs-button:hover span.vjs-control-text {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Re-enable for screen readers only when focused */
|
||||
.video-js .vjs-control-bar .vjs-control:focus .vjs-control-text {
|
||||
position: absolute !important;
|
||||
left: -9999px !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(1px, 1px, 1px, 1px) !important;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/* ===== CUSTOM REMAINING TIME STYLES ===== */
|
||||
|
||||
.vjs-control-bar .custom-remaining-time .vjs-remaining-time-display {
|
||||
font-size: 14px !important;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.vjs-control-bar .custom-remaining-time {
|
||||
flex-shrink: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* Responsive time display sizing */
|
||||
@media (max-width: 767px) {
|
||||
.vjs-control-bar .custom-remaining-time .vjs-remaining-time-display {
|
||||
font-size: 13px !important;
|
||||
padding: 0 3px;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.vjs-control-bar .custom-remaining-time .vjs-remaining-time-display {
|
||||
font-size: 12px !important;
|
||||
padding: 0 2px;
|
||||
letter-spacing: -0.2px;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.vjs-control-bar .custom-remaining-time .vjs-remaining-time-display {
|
||||
font-size: 11px !important;
|
||||
padding: 0 2px;
|
||||
letter-spacing: -0.2px;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 399px) {
|
||||
.vjs-control-bar .custom-remaining-time .vjs-remaining-time-display {
|
||||
font-size: 10px !important;
|
||||
padding: 0 1px;
|
||||
letter-spacing: -0.3px;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
// components/controls/CustomRemainingTime.js
|
||||
import videojs from 'video.js';
|
||||
import './CustomRemainingTime.css';
|
||||
|
||||
// Get the Component base class from Video.js
|
||||
const Component = videojs.getComponent('Component');
|
||||
@@ -31,18 +29,71 @@ class CustomRemainingTime extends Component {
|
||||
*/
|
||||
createEl() {
|
||||
const el = videojs.dom.createEl('div', {
|
||||
className: 'vjs-remaining-time vjs-time-control vjs-control custom-remaining-time',
|
||||
className: 'vjs-remaining-time vjs-time-control vjs-control',
|
||||
});
|
||||
|
||||
// Add ARIA accessibility
|
||||
el.innerHTML = `
|
||||
<span class="vjs-control-text" role="presentation">Time Display </span>
|
||||
<span class="vjs-remaining-time-display" role="presentation">0:00 / 0:00</span>
|
||||
<span class="vjs-remaining-time-display" role="timer" aria-live="off">0:00 / 0:00</span>
|
||||
`;
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add touch tooltip support for mobile devices
|
||||
*/
|
||||
addTouchTooltipSupport(element) {
|
||||
// Check if device is touch-enabled
|
||||
const isTouchDevice =
|
||||
'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
||||
|
||||
// Only add touch tooltip support on actual touch devices
|
||||
if (!isTouchDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
let touchStartTime = 0;
|
||||
let tooltipTimeout = null;
|
||||
|
||||
// Touch start
|
||||
element.addEventListener(
|
||||
'touchstart',
|
||||
() => {
|
||||
touchStartTime = Date.now();
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
|
||||
// Touch end
|
||||
element.addEventListener(
|
||||
'touchend',
|
||||
(e) => {
|
||||
const touchDuration = Date.now() - touchStartTime;
|
||||
|
||||
// Only show tooltip for quick taps (not swipes)
|
||||
if (touchDuration < 300) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Show tooltip briefly
|
||||
element.classList.add('touch-tooltip-active');
|
||||
|
||||
// Clear any existing timeout
|
||||
if (tooltipTimeout) {
|
||||
clearTimeout(tooltipTimeout);
|
||||
}
|
||||
|
||||
// Hide tooltip after delay
|
||||
tooltipTimeout = setTimeout(() => {
|
||||
element.classList.remove('touch-tooltip-active');
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
{ passive: false }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the time display
|
||||
*/
|
||||
@@ -90,7 +141,7 @@ class CustomRemainingTime extends Component {
|
||||
}
|
||||
|
||||
// Set component name for Video.js
|
||||
CustomRemainingTime.prototype.controlText_ = 'Time Display';
|
||||
CustomRemainingTime.prototype.controlText_ = '';
|
||||
|
||||
// Register the component with Video.js
|
||||
videojs.registerComponent('CustomRemainingTime', CustomRemainingTime);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// components/controls/CustomSettingsMenu.js
|
||||
import videojs from 'video.js';
|
||||
import './CustomSettingsMenu.css';
|
||||
import './SettingsButton.css';
|
||||
// import './SettingsButton.css';
|
||||
import UserPreferences from '../../utils/UserPreferences';
|
||||
|
||||
// Get the Component base class from Video.js
|
||||
@@ -53,19 +53,26 @@ class CustomSettingsMenu extends Component {
|
||||
// Create settings button
|
||||
this.settingsButton = controlBar.addChild('button', {
|
||||
controlText: 'Settings',
|
||||
className: 'vjs-settings-button settings-clicked',
|
||||
className: 'vjs-settings-button vjs-control vjs-button settings-clicked',
|
||||
});
|
||||
|
||||
// Style the settings button (gear icon)
|
||||
const settingsButtonEl = this.settingsButton.el();
|
||||
settingsButtonEl.innerHTML = `
|
||||
<span><svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%"><use class="ytp-svg-shadow" xlink:href="#ytp-id-19"></use><path d="m 23.94,18.78 c .03,-0.25 .05,-0.51 .05,-0.78 0,-0.27 -0.02,-0.52 -0.05,-0.78 l 1.68,-1.32 c .15,-0.12 .19,-0.33 .09,-0.51 l -1.6,-2.76 c -0.09,-0.17 -0.31,-0.24 -0.48,-0.17 l -1.99,.8 c -0.41,-0.32 -0.86,-0.58 -1.35,-0.78 l -0.30,-2.12 c -0.02,-0.19 -0.19,-0.33 -0.39,-0.33 l -3.2,0 c -0.2,0 -0.36,.14 -0.39,.33 l -0.30,2.12 c -0.48,.2 -0.93,.47 -1.35,.78 l -1.99,-0.8 c -0.18,-0.07 -0.39,0 -0.48,.17 l -1.6,2.76 c -0.10,.17 -0.05,.39 .09,.51 l 1.68,1.32 c -0.03,.25 -0.05,.52 -0.05,.78 0,.26 .02,.52 .05,.78 l -1.68,1.32 c -0.15,.12 -0.19,.33 -0.09,.51 l 1.6,2.76 c .09,.17 .31,.24 .48,.17 l 1.99,-0.8 c .41,.32 .86,.58 1.35,.78 l .30,2.12 c .02,.19 .19,.33 .39,.33 l 3.2,0 c .2,0 .36,-0.14 .39,-0.33 l .30,-2.12 c .48,-0.2 .93,-0.47 1.35,-0.78 l 1.99,.8 c .18,.07 .39,0 .48,-0.17 l 1.6,-2.76 c .09,-0.17 .05,-0.39 -0.09,-0.51 l -1.68,-1.32 0,0 z m -5.94,2.01 c -1.54,0 -2.8,-1.25 -2.8,-2.8 0,-1.54 1.25,-2.8 2.8,-2.8 1.54,0 2.8,1.25 2.8,2.8 0,1.54 -1.25,2.8 -2.8,2.8 l 0,0 z" fill="#fff" id="ytp-id-19"></path></svg></span>
|
||||
<span class="vjs-icon-placeholder vjs-icon-cog"></span>
|
||||
<span class="vjs-control-text">Settings</span>
|
||||
`;
|
||||
|
||||
// Add tooltip attributes
|
||||
settingsButtonEl.setAttribute('title', 'Settings');
|
||||
settingsButtonEl.setAttribute('aria-label', 'Settings');
|
||||
|
||||
// Position the settings button at the end of the control bar
|
||||
this.positionButton();
|
||||
|
||||
// Add touch tooltip support
|
||||
this.addTouchTooltipSupport(settingsButtonEl);
|
||||
|
||||
// Add mobile touch handling and unified click handling
|
||||
const buttonEl = this.settingsButton.el();
|
||||
if (buttonEl) {
|
||||
@@ -1154,6 +1161,57 @@ class CustomSettingsMenu extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Add touch tooltip support for mobile devices
|
||||
addTouchTooltipSupport(button) {
|
||||
// Check if device is touch-enabled
|
||||
const isTouchDevice =
|
||||
'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
||||
|
||||
// Only add touch tooltip support on actual touch devices
|
||||
if (!isTouchDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
let touchStartTime = 0;
|
||||
let tooltipTimeout = null;
|
||||
|
||||
// Touch start
|
||||
button.addEventListener(
|
||||
'touchstart',
|
||||
() => {
|
||||
touchStartTime = Date.now();
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
|
||||
// Touch end
|
||||
button.addEventListener(
|
||||
'touchend',
|
||||
(e) => {
|
||||
const touchDuration = Date.now() - touchStartTime;
|
||||
|
||||
// Only show tooltip for quick taps (not swipes)
|
||||
if (touchDuration < 300) {
|
||||
// Don't prevent default here as it might interfere with the settings menu
|
||||
|
||||
// Show tooltip briefly
|
||||
button.classList.add('touch-tooltip-active');
|
||||
|
||||
// Clear any existing timeout
|
||||
if (tooltipTimeout) {
|
||||
clearTimeout(tooltipTimeout);
|
||||
}
|
||||
|
||||
// Hide tooltip after delay
|
||||
tooltipTimeout = setTimeout(() => {
|
||||
button.classList.remove('touch-tooltip-active');
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// Remove event listeners
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import videojs from 'video.js';
|
||||
import './NextVideoButton.css';
|
||||
// import './NextVideoButton.css';
|
||||
|
||||
const Button = videojs.getComponent('Button');
|
||||
|
||||
@@ -11,39 +11,98 @@ class NextVideoButton extends Button {
|
||||
}
|
||||
|
||||
createEl() {
|
||||
const button = super.createEl('button', {
|
||||
className: 'vjs-next-video-control vjs-control vjs-button',
|
||||
// Create button element directly without wrapper div
|
||||
const button = videojs.dom.createEl('button', {
|
||||
className: 'vjs-next-video-button vjs-control vjs-button',
|
||||
type: 'button',
|
||||
title: 'Next Video',
|
||||
'aria-label': 'Next Video',
|
||||
'aria-disabled': 'false',
|
||||
});
|
||||
|
||||
// Create the icon span using Video.js core icon
|
||||
const iconSpan = videojs.dom.createEl('span', {
|
||||
// Create the icon placeholder span (Video.js standard structure)
|
||||
const iconPlaceholder = videojs.dom.createEl('span', {
|
||||
className: 'vjs-icon-placeholder',
|
||||
'aria-hidden': 'true',
|
||||
});
|
||||
|
||||
// Create SVG that matches Video.js icon dimensions
|
||||
iconSpan.innerHTML = `
|
||||
<svg width="34" height="34" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 34L28.1667 24L14 14V34ZM30.6667 14V34H34V14H30.6667Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
`;
|
||||
|
||||
// Create control text span
|
||||
// Create control text span (Video.js standard structure)
|
||||
const controlTextSpan = videojs.dom.createEl('span', {
|
||||
className: 'vjs-control-text',
|
||||
'aria-live': 'polite',
|
||||
});
|
||||
controlTextSpan.textContent = 'Next Video';
|
||||
|
||||
// Append both spans to button
|
||||
button.appendChild(iconSpan);
|
||||
// Create custom icon span with SVG
|
||||
const customIconSpan = videojs.dom.createEl('span');
|
||||
customIconSpan.innerHTML = `
|
||||
<svg width="11" height="11" viewBox="14 14 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 34L28.1667 24L14 14V34ZM30.6667 14V34H34V14H30.6667Z" fill="currentColor"></path>
|
||||
</svg>`;
|
||||
|
||||
// Append spans to button in Video.js standard order
|
||||
button.appendChild(iconPlaceholder);
|
||||
button.appendChild(controlTextSpan);
|
||||
button.appendChild(customIconSpan);
|
||||
|
||||
// Add touch tooltip support
|
||||
this.addTouchTooltipSupport(button);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
// Add touch tooltip support for mobile devices
|
||||
addTouchTooltipSupport(button) {
|
||||
// Check if device is touch-enabled
|
||||
const isTouchDevice =
|
||||
'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
||||
|
||||
// Only add touch tooltip support on actual touch devices
|
||||
if (!isTouchDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
let touchStartTime = 0;
|
||||
let tooltipTimeout = null;
|
||||
|
||||
// Touch start
|
||||
button.addEventListener(
|
||||
'touchstart',
|
||||
() => {
|
||||
touchStartTime = Date.now();
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
|
||||
// Touch end
|
||||
button.addEventListener(
|
||||
'touchend',
|
||||
(e) => {
|
||||
const touchDuration = Date.now() - touchStartTime;
|
||||
|
||||
// Only show tooltip for quick taps (not swipes)
|
||||
if (touchDuration < 300) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Show tooltip briefly
|
||||
button.classList.add('touch-tooltip-active');
|
||||
|
||||
// Clear any existing timeout
|
||||
if (tooltipTimeout) {
|
||||
clearTimeout(tooltipTimeout);
|
||||
}
|
||||
|
||||
// Hide tooltip after delay
|
||||
tooltipTimeout = setTimeout(() => {
|
||||
button.classList.remove('touch-tooltip-active');
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
{ passive: false }
|
||||
);
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
this.player().trigger('nextVideo');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import videojs from 'video.js';
|
||||
import './SeekIndicator.css';
|
||||
// import './SeekIndicator.css';
|
||||
|
||||
const Component = videojs.getComponent('Component');
|
||||
|
||||
@@ -10,6 +10,17 @@ class SeekIndicator extends Component {
|
||||
this.seekAmount = options.seekAmount || 5; // Default seek amount in seconds
|
||||
this.isEmbedPlayer = options.isEmbedPlayer || false; // Store embed mode flag
|
||||
this.showTimeout = null;
|
||||
|
||||
// Detect touch devices - if touch is supported, native browser controls will handle icons
|
||||
this.isTouchDevice = this.detectTouchDevice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if the device supports touch
|
||||
* @returns {boolean} True if touch is supported
|
||||
*/
|
||||
detectTouchDevice() {
|
||||
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
||||
}
|
||||
|
||||
createEl() {
|
||||
@@ -39,6 +50,11 @@ class SeekIndicator extends Component {
|
||||
* @param {number} seconds - Number of seconds to seek (only used for forward/backward)
|
||||
*/
|
||||
show(direction, seconds = this.seekAmount) {
|
||||
// Skip showing icons on touch devices as native browser controls handle them
|
||||
if (this.isTouchDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = this.el();
|
||||
const iconEl = el.querySelector('.vjs-seek-indicator-icon');
|
||||
const textEl = el.querySelector('.vjs-seek-indicator-text');
|
||||
@@ -230,6 +246,11 @@ class SeekIndicator extends Component {
|
||||
* Show pause icon for mobile (uses 500ms from main show method)
|
||||
*/
|
||||
showMobilePauseIcon() {
|
||||
// Skip showing icons on touch devices as native browser controls handle them
|
||||
if (this.isTouchDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.show('pause-mobile'); // This will auto-hide after 500ms
|
||||
|
||||
// Make the icon clickable for mobile
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import videojs from 'video.js';
|
||||
|
||||
const TransientButton = videojs.getComponent('TransientButton');
|
||||
|
||||
class TestButton extends TransientButton {
|
||||
constructor(player, options) {
|
||||
super(player, {
|
||||
controlText: 'Test Button',
|
||||
position: ['bottom', 'right'],
|
||||
className: 'test-button',
|
||||
...options,
|
||||
});
|
||||
this.setupVisibilityHandling();
|
||||
}
|
||||
|
||||
setupVisibilityHandling() {
|
||||
// Add CSS transition for smooth fade out like control bar
|
||||
this.el().style.transition = 'opacity 0.3s ease';
|
||||
|
||||
this.player().on('mouseenter', () => {
|
||||
this.showWithFade();
|
||||
});
|
||||
|
||||
this.player().on('mouseleave', () => {
|
||||
// Only hide if video is playing
|
||||
setTimeout(() => {
|
||||
if (!this.player().paused()) {
|
||||
this.hideWithFade();
|
||||
}
|
||||
}, 3000); // Hide after 3 seconds delay like control bar
|
||||
});
|
||||
|
||||
// Add touch events
|
||||
this.player().on('touchstart', () => {
|
||||
this.showWithFade();
|
||||
});
|
||||
|
||||
this.player().on('touchend', () => {
|
||||
// Hide after a delay to allow for interaction, but only if playing
|
||||
setTimeout(() => {
|
||||
if (!this.player().paused()) {
|
||||
this.hideWithFade();
|
||||
}
|
||||
}, 3000); // Hide after 3 seconds delay
|
||||
});
|
||||
|
||||
// Alternative: Use user activity events (recommended)
|
||||
this.player().on('useractive', () => {
|
||||
this.showWithFade();
|
||||
});
|
||||
|
||||
this.player().on('userinactive', () => {
|
||||
// Only hide if video is playing
|
||||
if (!this.player().paused()) {
|
||||
this.hideWithFade();
|
||||
}
|
||||
});
|
||||
|
||||
// Show when paused, hide when playing
|
||||
this.player().on('pause', () => {
|
||||
this.showWithFade();
|
||||
});
|
||||
|
||||
this.player().on('play', () => {
|
||||
// Hide when playing starts, unless user is actively interacting
|
||||
if (!this.player().userActive()) {
|
||||
this.hideWithFade();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showWithFade() {
|
||||
this.show();
|
||||
this.el().style.opacity = '1';
|
||||
this.el().style.visibility = 'visible';
|
||||
}
|
||||
|
||||
hideWithFade() {
|
||||
// Start fade out transition
|
||||
this.el().style.opacity = '0';
|
||||
|
||||
// Hide element after transition completes (300ms like control bar)
|
||||
setTimeout(() => {
|
||||
if (this.el().style.opacity === '0') {
|
||||
this.hide();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
alert('testButton - controls were hidden');
|
||||
// Add your custom functionality here
|
||||
}
|
||||
}
|
||||
|
||||
videojs.registerComponent('TestButton', TestButton);
|
||||
export default TestButton;
|
||||
@@ -1,5 +1,6 @@
|
||||
// Export all Video.js components
|
||||
export { default as VideoJSPlayer } from './video-player/VideoJSPlayer';
|
||||
export { default as VideoJSPlayerNew } from './video-player/VideoJSPlayerNew';
|
||||
export { default as EndScreenOverlay } from './overlays/EndScreenOverlay';
|
||||
export { default as AutoplayCountdownOverlay } from './overlays/AutoplayCountdownOverlay';
|
||||
export { default as ChapterMarkers } from './markers/ChapterMarkers';
|
||||
|
||||
@@ -63,21 +63,28 @@
|
||||
width: 166px !important;
|
||||
max-width: 100% !important;
|
||||
height: 96px;
|
||||
margin: 0 auto 10px;
|
||||
margin: 10px auto 10px;
|
||||
border-radius: 6px;
|
||||
border: 3px solid #fff;
|
||||
}
|
||||
|
||||
.vjs-chapter-floating-tooltip .chapter-title {
|
||||
font-size: 16px;
|
||||
margin: 0 0 10px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 5px;
|
||||
word-break: break-all;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.vjs-chapter-floating-tooltip .position-info,
|
||||
.vjs-chapter-floating-tooltip .chapter-info {
|
||||
font-size: 15px;
|
||||
display: inline-block;
|
||||
margin: 0 0 5px;
|
||||
line-height: normal;
|
||||
vertical-align: top;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.vjs-chapter-floating-tooltip .position-info {
|
||||
font-size: 15px;
|
||||
display: inline-block;
|
||||
margin: 0 0 2px;
|
||||
@@ -96,63 +103,3 @@
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Disable chapter marker tooltips on touch devices */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.vjs-chapter-marker:hover .vjs-chapter-marker-tooltip {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
.vjs-chapter-floating-tooltip {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 899px) {
|
||||
/* Disable chapter marker tooltips on tablets */
|
||||
.vjs-chapter-marker:hover .vjs-chapter-marker-tooltip {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* Disable chapter floating tooltips on tablets */
|
||||
.vjs-chapter-floating-tooltip {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
/* Disable chapter marker tooltips on mobile */
|
||||
.vjs-chapter-marker:hover .vjs-chapter-marker-tooltip {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* Disable chapter floating tooltips on mobile */
|
||||
.vjs-chapter-floating-tooltip {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* Disable chapter marker tooltips on small mobile */
|
||||
.vjs-chapter-marker:hover .vjs-chapter-marker-tooltip {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* Disable chapter floating tooltips on small mobile */
|
||||
.vjs-chapter-floating-tooltip {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ class ChapterMarkers extends Component {
|
||||
Object.assign(this.tooltip.style, {
|
||||
position: 'absolute',
|
||||
zIndex: '1000',
|
||||
bottom: '45px',
|
||||
bottom: '25px',
|
||||
transform: 'translateX(-50%)',
|
||||
display: 'none',
|
||||
minWidth: '160px',
|
||||
@@ -148,10 +148,10 @@ class ChapterMarkers extends Component {
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
// Append all elements to tooltip
|
||||
// Append all elements to tooltip - duration after title, then image
|
||||
this.tooltip.appendChild(this.chapterTitle);
|
||||
this.tooltip.appendChild(this.chapterImage);
|
||||
this.tooltip.appendChild(this.chapterInfo);
|
||||
this.tooltip.appendChild(this.chapterImage);
|
||||
this.tooltip.appendChild(this.positionInfo);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,39 +26,3 @@
|
||||
border-radius: 6px;
|
||||
border: 3px solid #fff;
|
||||
}
|
||||
|
||||
/* Disable sprite preview tooltips on touch devices */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.vjs-sprite-preview-tooltip {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 899px) {
|
||||
/* Disable sprite preview tooltips on tablets */
|
||||
.vjs-sprite-preview-tooltip {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
/* Disable sprite preview tooltips on mobile */
|
||||
.vjs-sprite-preview-tooltip {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* Disable sprite preview tooltips on small mobile */
|
||||
.vjs-sprite-preview-tooltip {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class SpritePreview extends Component {
|
||||
bottom: '45px',
|
||||
transform: 'translateX(-50%)',
|
||||
display: 'none',
|
||||
minWidth: '172px', // Accommodate 166px image + 3px border on each side
|
||||
minWidth: '172px',
|
||||
maxWidth: '172px',
|
||||
width: '172px',
|
||||
});
|
||||
|
||||
@@ -1,689 +1,3 @@
|
||||
/* ===== END SCREEN OVERLAY STYLES ===== */
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(100% - 80px); /* Reduce reserved space for seekbar */
|
||||
background: #000000;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: center; /* Center the grid vertically */
|
||||
align-items: center;
|
||||
padding: 40px 40px 40px 40px; /* Equal visual margins on all sides */
|
||||
box-sizing: border-box;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Hide poster image when video ends and end screen is shown */
|
||||
.video-js.vjs-ended .vjs-poster {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
z-index: -1 !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
/* Hide video element completely when ended */
|
||||
.video-js.vjs-ended video {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* Ensure the overlay covers everything with maximum z-index */
|
||||
.video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
z-index: 99999 !important;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* Embed-specific full page overlay */
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: calc(100vh - 80px) !important; /* Reduce reserved space for controls */
|
||||
z-index: 9998 !important; /* Below controls but above video */
|
||||
display: flex !important;
|
||||
padding: 120px 40px 40px 40px !important; /* Top padding for embed info + equal visual margins */
|
||||
justify-content: center !important; /* Center the grid vertically */
|
||||
}
|
||||
|
||||
/* Small player size optimization - 2 items horizontally for better title readability */
|
||||
/* This applies to both embed and regular players when they're small */
|
||||
.vjs-end-screen-overlay.vjs-small-player .vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
grid-template-rows: 1fr !important;
|
||||
gap: 20px !important;
|
||||
max-width: 600px; /* Limit width for better proportions */
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay.vjs-small-player {
|
||||
height: calc(100% - 60px) !important;
|
||||
padding: 30px !important;
|
||||
}
|
||||
|
||||
/* Hide items beyond the first 2 for small players */
|
||||
.vjs-end-screen-overlay.vjs-small-player .vjs-related-video-item:nth-child(n + 3) {
|
||||
.vjs-ended .vjs-poster {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Embed-specific adjustments for small sizes */
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay.vjs-small-player {
|
||||
height: calc(100vh - 60px) !important;
|
||||
padding: 80px 30px 30px 30px !important;
|
||||
}
|
||||
|
||||
/* Fallback media query for cases where class detection might not work */
|
||||
@media (max-height: 500px), (max-width: 600px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
grid-template-rows: 1fr !important;
|
||||
gap: 20px !important;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 60px) !important;
|
||||
padding: 30px !important;
|
||||
}
|
||||
|
||||
.vjs-related-video-item:nth-child(n + 3) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 60px) !important;
|
||||
padding: 80px 30px 30px 30px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Very small player size - further optimize spacing (class-based detection) */
|
||||
.vjs-end-screen-overlay.vjs-very-small-player .vjs-related-videos-grid {
|
||||
gap: 15px !important;
|
||||
max-width: 500px !important;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay.vjs-very-small-player {
|
||||
height: calc(100% - 50px) !important;
|
||||
padding: 25px !important;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay.vjs-very-small-player .vjs-related-video-item {
|
||||
min-height: 80px !important;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay.vjs-very-small-player {
|
||||
height: calc(100vh - 50px) !important;
|
||||
padding: 60px 25px 25px 25px !important;
|
||||
}
|
||||
|
||||
/* Fallback media query for very small sizes */
|
||||
@media (max-height: 400px), (max-width: 400px) {
|
||||
.vjs-related-videos-grid {
|
||||
gap: 15px !important;
|
||||
max-width: 500px !important;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 50px) !important;
|
||||
padding: 25px !important;
|
||||
}
|
||||
|
||||
.vjs-related-video-item {
|
||||
min-height: 80px !important;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 50px) !important;
|
||||
padding: 60px 25px 25px 25px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure controls stay visible over the black background */
|
||||
.video-js.vjs-ended .vjs-control-bar {
|
||||
z-index: 10000 !important;
|
||||
position: absolute !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100% !important;
|
||||
display: flex !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-progress-control {
|
||||
z-index: 10001 !important;
|
||||
position: absolute !important;
|
||||
bottom: 48px !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100% !important;
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Embed-specific controls handling when ended */
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-control-bar {
|
||||
position: fixed !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100vw !important;
|
||||
z-index: 10000 !important;
|
||||
display: flex !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-progress-control {
|
||||
position: fixed !important;
|
||||
bottom: 48px !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100vw !important;
|
||||
z-index: 10001 !important;
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Ensure embed info overlay (title/avatar) stays visible when ended */
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-embed-info-overlay {
|
||||
z-index: 10002 !important;
|
||||
display: flex !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Hide big play button when end screen is active */
|
||||
.video-js.vjs-ended .vjs-big-play-button {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* Hide seek indicator (play icon) when end screen is active */
|
||||
.video-js.vjs-ended .vjs-seek-indicator {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* Make control bar and seekbar background black when video ends */
|
||||
.video-js.vjs-ended .vjs-control-bar {
|
||||
background: #000000 !important;
|
||||
background-color: #000000 !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-progress-control {
|
||||
background: #000000 !important;
|
||||
background-color: #000000 !important;
|
||||
}
|
||||
|
||||
/* Also ensure the gradient overlay is black when ended */
|
||||
.video-js.vjs-ended::after {
|
||||
background: #000000 !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
/* Remove any white elements or gradients */
|
||||
.video-js.vjs-ended::before {
|
||||
background: #000000 !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
/* Ensure all VideoJS overlays are black but preserve seekbar colors */
|
||||
.video-js.vjs-ended .vjs-loading-spinner,
|
||||
.video-js.vjs-ended .vjs-mouse-display {
|
||||
background: #000000 !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
/* Only change the background holder, preserve progress colors */
|
||||
.video-js.vjs-ended .vjs-progress-holder {
|
||||
background: rgba(255, 255, 255, 0.3) !important; /* Keep original transparent background */
|
||||
}
|
||||
|
||||
/* Hide any remaining VideoJS elements that might show white */
|
||||
.video-js.vjs-ended .vjs-tech,
|
||||
.video-js.vjs-ended .vjs-poster-overlay {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
.vjs-related-videos-title {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.vjs-related-videos-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0; /* Remove margin since parent handles centering */
|
||||
box-sizing: border-box;
|
||||
justify-items: stretch;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
align-content: center; /* Center grid content */
|
||||
overflow: hidden;
|
||||
grid-auto-rows: 1fr;
|
||||
}
|
||||
|
||||
.vjs-related-video-item {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
aspect-ratio: 16/9;
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Apply rounded corners only when useRoundedCorners is true */
|
||||
.video-js.video-js-rounded-corners .vjs-related-video-item {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.vjs-related-video-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.vjs-related-video-thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
/* border-radius: 8px; */
|
||||
background: #1a1a1a; /* Fallback background */
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.vjs-related-video-item:hover .vjs-related-video-thumbnail {
|
||||
transform: scale(1.02); /* Subtle zoom like YouTube */
|
||||
}
|
||||
|
||||
.vjs-related-video-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.9));
|
||||
color: white;
|
||||
padding: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vjs-related-video-item:hover .vjs-related-video-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Show overlay by default on touch devices - match default hover behavior exactly */
|
||||
.vjs-related-video-item.vjs-touch-device .vjs-related-video-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.vjs-related-video-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 1.3;
|
||||
color: white;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vjs-related-video-meta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vjs-related-video-author {
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.vjs-related-video-views {
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.vjs-related-video-author::after {
|
||||
content: "•";
|
||||
margin-left: 8px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.vjs-related-video-duration {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* Apply rounded corners to duration badge only when useRoundedCorners is true */
|
||||
.video-js.video-js-rounded-corners .vjs-related-video-duration {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.vjs-related-video-item:hover .vjs-related-video-duration {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Show duration by default on touch devices */
|
||||
.vjs-related-video-item.vjs-touch-device .vjs-related-video-duration {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-control-bar {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-control-bar .vjs-control {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-control-bar button {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-control-bar .vjs-control.vjs-volume-control {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-control-bar .vjs-volume-panel.vjs-hover .vjs-volume-control {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Responsive grid adjustments for different screen sizes */
|
||||
@media (max-width: 1200px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 70px);
|
||||
padding: 35px;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 70px) !important;
|
||||
padding: 115px 35px 35px 35px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 60px);
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 60px) !important;
|
||||
padding: 110px 30px 30px 30px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 50px);
|
||||
padding: 25px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 50px) !important;
|
||||
padding: 105px 25px 25px 25px !important;
|
||||
}
|
||||
|
||||
.vjs-related-video-item {
|
||||
min-height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 40px);
|
||||
padding: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 40px) !important;
|
||||
padding: 100px 20px 20px 20px !important;
|
||||
}
|
||||
|
||||
.vjs-related-video-item {
|
||||
min-height: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-play-control {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-progress-control {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-volume-panel {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* Responsive grid layouts */
|
||||
@media (min-width: 1200px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 80px);
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 80px) !important;
|
||||
padding: 120px 40px 40px 40px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
.vjs-related-video-item:nth-child(n + 10) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* iPad Pro and larger tablets */
|
||||
@media (min-width: 1024px) and (max-width: 1199px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Allow up to 9 videos on larger tablets */
|
||||
.vjs-related-video-item:nth-child(n + 10) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Large tablets like iPad Pro */
|
||||
@media (min-width: 900px) and (max-width: 1024px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Allow up to 9 videos on large tablets */
|
||||
.vjs-related-video-item:nth-child(n + 10) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 899px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 60px);
|
||||
padding: 30px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 60px) !important;
|
||||
padding: 110px 30px 30px 30px !important;
|
||||
}
|
||||
|
||||
/* Allow up to 9 videos on regular tablets */
|
||||
.vjs-related-video-item:nth-child(n + 10) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.vjs-related-video-item:nth-child(n + 5) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
padding: 12px;
|
||||
justify-content: center;
|
||||
height: calc(100% - 105px);
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 105px) !important;
|
||||
padding: 80px 12px 12px 12px !important;
|
||||
}
|
||||
|
||||
.vjs-related-video-thumbnail {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 574px) {
|
||||
.vjs-related-video-item:nth-child(n + 5) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
padding: 10px;
|
||||
justify-content: center;
|
||||
height: calc(100% - 100px);
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 100px) !important;
|
||||
padding: 80px 10px 10px 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 439px) {
|
||||
.vjs-related-video-item:nth-child(n + 5) {
|
||||
display: none;
|
||||
}
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
padding: 8px;
|
||||
justify-content: center;
|
||||
height: calc(100% - 98px);
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 98px) !important;
|
||||
padding: 80px 8px 8px 8px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.vjs-related-video-thumbnail {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,367 +1,303 @@
|
||||
import videojs from 'video.js';
|
||||
import './EndScreenOverlay.css';
|
||||
|
||||
const Component = videojs.getComponent('Component');
|
||||
|
||||
class EndScreenOverlay extends Component {
|
||||
constructor(player, options) {
|
||||
// Store relatedVideos in options before calling super
|
||||
// so it's available during createEl()
|
||||
if (options && options.relatedVideos) {
|
||||
options._relatedVideos = options.relatedVideos;
|
||||
}
|
||||
|
||||
super(player, options);
|
||||
// Safely initialize relatedVideos with multiple fallbacks
|
||||
this.relatedVideos = options?.relatedVideos || options?._relatedVideos || this.options_?.relatedVideos || [];
|
||||
console.log('relatedVideos1', this.relatedVideos);
|
||||
this.isTouchDevice = this.detectTouchDevice();
|
||||
|
||||
// Now set the instance property after super() completes
|
||||
this.relatedVideos = options && options.relatedVideos ? options.relatedVideos : [];
|
||||
// Bind methods to preserve 'this' context
|
||||
this.getVideosToShow = this.getVideosToShow.bind(this);
|
||||
this.getGridConfig = this.getGridConfig.bind(this);
|
||||
this.createVideoItem = this.createVideoItem.bind(this);
|
||||
}
|
||||
|
||||
// Method to update related videos after initialization
|
||||
setRelatedVideos(videos) {
|
||||
this.relatedVideos = videos || [];
|
||||
console.log('Updated relatedVideos:', this.relatedVideos);
|
||||
}
|
||||
|
||||
createEl() {
|
||||
// Get relatedVideos from options since createEl is called during super()
|
||||
const relatedVideos = this.options_ && this.options_._relatedVideos ? this.options_._relatedVideos : [];
|
||||
|
||||
// Limit videos based on screen size to fit grid properly
|
||||
const maxVideos = this.getMaxVideosForScreen();
|
||||
const videosToShow = relatedVideos.slice(0, maxVideos);
|
||||
|
||||
// Determine if player is small and add appropriate class
|
||||
const playerEl = this.player().el();
|
||||
const playerWidth = playerEl ? playerEl.offsetWidth : window.innerWidth;
|
||||
const playerHeight = playerEl ? playerEl.offsetHeight : window.innerHeight;
|
||||
const isSmallPlayer = playerHeight <= 500 || playerWidth <= 600;
|
||||
const isVerySmallPlayer = playerHeight <= 400 || playerWidth <= 400;
|
||||
|
||||
let overlayClasses = 'vjs-end-screen-overlay';
|
||||
if (isVerySmallPlayer) {
|
||||
overlayClasses += ' vjs-very-small-player vjs-small-player';
|
||||
} else if (isSmallPlayer) {
|
||||
overlayClasses += ' vjs-small-player';
|
||||
}
|
||||
|
||||
const overlay = super.createEl('div', {
|
||||
className: overlayClasses,
|
||||
className: 'vjs-end-screen-overlay',
|
||||
});
|
||||
|
||||
// Create grid container
|
||||
const grid = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-videos-grid',
|
||||
});
|
||||
|
||||
// Create video items
|
||||
if (videosToShow && Array.isArray(videosToShow) && videosToShow.length > 0) {
|
||||
videosToShow.forEach((video) => {
|
||||
const videoItem = this.createVideoItem(video);
|
||||
grid.appendChild(videoItem);
|
||||
});
|
||||
} else {
|
||||
// Create sample videos for testing if no related videos provided
|
||||
const sampleVideos = this.createSampleVideos();
|
||||
sampleVideos.slice(0, this.getMaxVideosForScreen()).forEach((video) => {
|
||||
const videoItem = this.createVideoItem(video);
|
||||
grid.appendChild(videoItem);
|
||||
});
|
||||
}
|
||||
// Position overlay above control bar
|
||||
overlay.style.position = 'absolute';
|
||||
overlay.style.top = '0';
|
||||
overlay.style.left = '0';
|
||||
overlay.style.right = '0';
|
||||
overlay.style.bottom = '60px'; // Leave space for control bar
|
||||
overlay.style.display = 'none'; // Hidden by default
|
||||
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
|
||||
overlay.style.zIndex = '100';
|
||||
|
||||
// Create responsive grid
|
||||
const grid = this.createGrid();
|
||||
overlay.appendChild(grid);
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
createGrid() {
|
||||
const grid = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-videos-grid',
|
||||
});
|
||||
|
||||
// Responsive grid styling
|
||||
grid.style.display = 'grid';
|
||||
grid.style.gap = '12px';
|
||||
grid.style.padding = '20px';
|
||||
grid.style.height = '100%';
|
||||
grid.style.overflowY = 'auto';
|
||||
|
||||
// Responsive grid columns based on player size
|
||||
const { columns, maxVideos } = this.getGridConfig();
|
||||
grid.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;
|
||||
|
||||
// Get videos to show - access directly from options during createEl
|
||||
const relatedVideos = this.options_?.relatedVideos || this.relatedVideos || [];
|
||||
console.log('createGrid relatedVideos:', relatedVideos);
|
||||
|
||||
const videosToShow =
|
||||
relatedVideos.length > 0
|
||||
? relatedVideos.slice(0, maxVideos)
|
||||
: this.createSampleVideos().slice(0, maxVideos);
|
||||
|
||||
// Create video items
|
||||
videosToShow.forEach((video) => {
|
||||
const videoItem = this.createVideoItem(video);
|
||||
grid.appendChild(videoItem);
|
||||
});
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
getGridConfig() {
|
||||
const playerEl = this.player().el();
|
||||
const playerWidth = playerEl?.offsetWidth || window.innerWidth;
|
||||
const playerHeight = playerEl?.offsetHeight || window.innerHeight;
|
||||
|
||||
// Responsive grid configuration
|
||||
if (playerWidth >= 1200) {
|
||||
return { columns: 4, maxVideos: 8 }; // 4x2 grid for large screens
|
||||
} else if (playerWidth >= 800) {
|
||||
return { columns: 3, maxVideos: 6 }; // 3x2 grid for medium screens
|
||||
} else if (playerWidth >= 500) {
|
||||
return { columns: 2, maxVideos: 4 }; // 2x2 grid for small screens
|
||||
} else {
|
||||
return { columns: 1, maxVideos: 3 }; // 1 column for very small screens
|
||||
}
|
||||
}
|
||||
|
||||
getVideosToShow(maxVideos) {
|
||||
// Safely check if relatedVideos exists and has content
|
||||
console.log('relatedVideos', this.relatedVideos);
|
||||
if (this.relatedVideos && Array.isArray(this.relatedVideos) && this.relatedVideos.length > 0) {
|
||||
return this.relatedVideos.slice(0, maxVideos);
|
||||
}
|
||||
// Fallback to sample videos for testing
|
||||
return this.createSampleVideos().slice(0, maxVideos);
|
||||
}
|
||||
|
||||
createVideoItem(video) {
|
||||
// Detect touch device
|
||||
const isTouchDevice = this.isTouchDevice();
|
||||
|
||||
const item = videojs.dom.createEl('div', {
|
||||
className: isTouchDevice ? 'vjs-related-video-item vjs-touch-device' : 'vjs-related-video-item',
|
||||
className: 'vjs-related-video-item',
|
||||
});
|
||||
|
||||
// Use real YouTube thumbnail or fallback to placeholder
|
||||
const thumbnailSrc = video.thumbnail || this.getPlaceholderImage(video.title);
|
||||
// Item styling
|
||||
item.style.position = 'relative';
|
||||
item.style.backgroundColor = '#1a1a1a';
|
||||
item.style.borderRadius = '8px';
|
||||
item.style.overflow = 'hidden';
|
||||
item.style.cursor = 'pointer';
|
||||
item.style.transition = 'transform 0.2s ease, box-shadow 0.2s ease';
|
||||
|
||||
const thumbnail = videojs.dom.createEl('img', {
|
||||
className: 'vjs-related-video-thumbnail',
|
||||
src: thumbnailSrc,
|
||||
alt: video.title,
|
||||
loading: 'lazy', // Lazy load for better performance
|
||||
onerror: () => {
|
||||
// Fallback to placeholder if image fails to load
|
||||
thumbnail.src = this.getPlaceholderImage(video.title);
|
||||
},
|
||||
});
|
||||
|
||||
const overlay = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-video-overlay',
|
||||
});
|
||||
|
||||
const title = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-video-title',
|
||||
});
|
||||
title.textContent = video.title;
|
||||
|
||||
// Create meta container for author and views
|
||||
const meta = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-video-meta',
|
||||
});
|
||||
|
||||
const author = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-video-author',
|
||||
});
|
||||
author.textContent = video.author;
|
||||
|
||||
const views = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-video-views',
|
||||
});
|
||||
views.textContent = video.views;
|
||||
|
||||
// Add author and views to meta container
|
||||
meta.appendChild(author);
|
||||
meta.appendChild(views);
|
||||
|
||||
// Add duration display (positioned absolutely in bottom right)
|
||||
const duration = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-video-duration',
|
||||
});
|
||||
|
||||
// Format duration from seconds to MM:SS
|
||||
const formatDuration = (seconds) => {
|
||||
if (!seconds || seconds === 0) return '';
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
duration.textContent = formatDuration(video.duration);
|
||||
|
||||
// Structure: title at top, meta at bottom
|
||||
overlay.appendChild(title);
|
||||
overlay.appendChild(meta);
|
||||
|
||||
item.appendChild(thumbnail);
|
||||
item.appendChild(overlay);
|
||||
|
||||
// Add duration to the item (positioned absolutely)
|
||||
if (video.duration && video.duration > 0) {
|
||||
item.appendChild(duration);
|
||||
// Hover/touch effects
|
||||
if (this.isTouchDevice) {
|
||||
item.style.touchAction = 'manipulation';
|
||||
} else {
|
||||
item.addEventListener('mouseenter', () => {
|
||||
item.style.transform = 'scale(1.05)';
|
||||
item.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)';
|
||||
});
|
||||
item.addEventListener('mouseleave', () => {
|
||||
item.style.transform = 'scale(1)';
|
||||
item.style.boxShadow = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Add click handler
|
||||
item.addEventListener('click', () => {
|
||||
// Check if this is an embed player - use multiple methods for reliability
|
||||
const playerId = this.player().id() || this.player().options_.id;
|
||||
const isEmbedPlayer =
|
||||
playerId === 'video-embed' ||
|
||||
window.location.pathname.includes('/embed') ||
|
||||
window.location.search.includes('embed') ||
|
||||
window.parent !== window; // Most reliable check for iframe
|
||||
// Create thumbnail
|
||||
const thumbnail = this.createThumbnail(video);
|
||||
item.appendChild(thumbnail);
|
||||
|
||||
if (isEmbedPlayer) {
|
||||
// Open in new tab/window for embed players
|
||||
window.open(`/view?m=${video.id}`, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
// Navigate in same window for regular players
|
||||
window.location.href = `/view?m=${video.id}`;
|
||||
}
|
||||
});
|
||||
// Create info overlay
|
||||
const info = this.createVideoInfo(video);
|
||||
item.appendChild(info);
|
||||
|
||||
// Add click handler
|
||||
this.addClickHandler(item, video);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
createThumbnail(video) {
|
||||
const thumbnail = videojs.dom.createEl('img', {
|
||||
className: 'vjs-related-video-thumbnail',
|
||||
src: video.thumbnail || this.getPlaceholderImage(video.title),
|
||||
alt: video.title,
|
||||
});
|
||||
|
||||
// Thumbnail styling
|
||||
thumbnail.style.width = '100%';
|
||||
thumbnail.style.height = '120px';
|
||||
thumbnail.style.objectFit = 'cover';
|
||||
thumbnail.style.display = 'block';
|
||||
|
||||
// Add duration badge if available
|
||||
if (video.duration && video.duration > 0) {
|
||||
const duration = videojs.dom.createEl('div', {
|
||||
className: 'vjs-video-duration',
|
||||
});
|
||||
duration.textContent = this.formatDuration(video.duration);
|
||||
duration.style.position = 'absolute';
|
||||
duration.style.bottom = '50px';
|
||||
duration.style.right = '8px';
|
||||
duration.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
|
||||
duration.style.color = 'white';
|
||||
duration.style.padding = '2px 6px';
|
||||
duration.style.borderRadius = '4px';
|
||||
duration.style.fontSize = '12px';
|
||||
duration.style.fontWeight = 'bold';
|
||||
|
||||
// Add duration to parent item (will be added later)
|
||||
thumbnail.durationBadge = duration;
|
||||
}
|
||||
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
createVideoInfo(video) {
|
||||
const info = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-video-info',
|
||||
});
|
||||
|
||||
// Info styling
|
||||
info.style.padding = '12px';
|
||||
info.style.color = 'white';
|
||||
|
||||
// Title
|
||||
const title = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-video-title',
|
||||
});
|
||||
title.textContent = video.title;
|
||||
title.style.fontSize = '14px';
|
||||
title.style.fontWeight = 'bold';
|
||||
title.style.marginBottom = '4px';
|
||||
title.style.lineHeight = '1.3';
|
||||
title.style.overflow = 'hidden';
|
||||
title.style.textOverflow = 'ellipsis';
|
||||
title.style.display = '-webkit-box';
|
||||
title.style.webkitLineClamp = '2';
|
||||
title.style.webkitBoxOrient = 'vertical';
|
||||
|
||||
// Author and views
|
||||
const meta = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-video-meta',
|
||||
});
|
||||
meta.textContent = `${video.author} • ${video.views}`;
|
||||
meta.style.fontSize = '12px';
|
||||
meta.style.color = '#aaa';
|
||||
meta.style.overflow = 'hidden';
|
||||
meta.style.textOverflow = 'ellipsis';
|
||||
meta.style.whiteSpace = 'nowrap';
|
||||
|
||||
info.appendChild(title);
|
||||
info.appendChild(meta);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
addClickHandler(item, video) {
|
||||
const clickHandler = () => {
|
||||
const isEmbedPlayer = this.player().id() === 'video-embed' || window.parent !== window;
|
||||
|
||||
if (isEmbedPlayer) {
|
||||
window.open(`/view?m=${video.id}`, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
window.location.href = `/view?m=${video.id}`;
|
||||
}
|
||||
};
|
||||
|
||||
if (this.isTouchDevice) {
|
||||
item.addEventListener('touchend', (e) => {
|
||||
e.preventDefault();
|
||||
clickHandler();
|
||||
});
|
||||
} else {
|
||||
item.addEventListener('click', clickHandler);
|
||||
}
|
||||
|
||||
// Add duration badge if it exists
|
||||
if (item.querySelector('img').durationBadge) {
|
||||
item.appendChild(item.querySelector('img').durationBadge);
|
||||
}
|
||||
}
|
||||
|
||||
formatDuration(seconds) {
|
||||
if (!seconds || seconds === 0) return '';
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
getPlaceholderImage(title) {
|
||||
// Generate a placeholder image using a service or create a data URL
|
||||
// For now, we'll use a simple colored placeholder based on the title
|
||||
const colors = [
|
||||
'#009931',
|
||||
'#4ECDC4',
|
||||
'#45B7D1',
|
||||
'#96CEB4',
|
||||
'#FFEAA7',
|
||||
'#DDA0DD',
|
||||
'#98D8C8',
|
||||
'#F7DC6F',
|
||||
'#BB8FCE',
|
||||
'#85C1E9',
|
||||
];
|
||||
const colors = ['#009931', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'];
|
||||
|
||||
// Use title hash to consistently assign colors
|
||||
let hash = 0;
|
||||
for (let i = 0; i < title.length; i++) {
|
||||
hash = title.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
const colorIndex = Math.abs(hash) % colors.length;
|
||||
const color = colors[colorIndex];
|
||||
|
||||
// Create a simple placeholder with the first letter of the title
|
||||
const color = colors[Math.abs(hash) % colors.length];
|
||||
const firstLetter = title.charAt(0).toUpperCase();
|
||||
|
||||
// Create a data URL for a simple placeholder image
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 320;
|
||||
canvas.height = 180;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Background
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(0, 0, 320, 180);
|
||||
|
||||
// Add a subtle pattern
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
|
||||
for (let i = 0; i < 20; i++) {
|
||||
ctx.fillRect(Math.random() * 320, Math.random() * 180, 2, 2);
|
||||
}
|
||||
|
||||
// Add the first letter
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = 'bold 48px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(firstLetter, 160, 90);
|
||||
|
||||
return canvas.toDataURL();
|
||||
// Create simple SVG placeholder
|
||||
return `data:image/svg+xml,${encodeURIComponent(`
|
||||
<svg width="320" height="180" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="320" height="180" fill="${color}"/>
|
||||
<text x="160" y="90" font-family="Arial" font-size="48" font-weight="bold"
|
||||
text-anchor="middle" dominant-baseline="middle" fill="white">${firstLetter}</text>
|
||||
</svg>
|
||||
`)}`;
|
||||
}
|
||||
|
||||
getMaxVideosForScreen() {
|
||||
// Get actual player dimensions instead of window dimensions
|
||||
const playerEl = this.player().el();
|
||||
const playerWidth = playerEl ? playerEl.offsetWidth : window.innerWidth;
|
||||
const playerHeight = playerEl ? playerEl.offsetHeight : window.innerHeight;
|
||||
|
||||
// Check if this is an embed player
|
||||
const playerId = this.player().id() || this.player().options_.id;
|
||||
const isEmbedPlayer =
|
||||
playerId === 'video-embed' ||
|
||||
document.getElementById('page-embed') ||
|
||||
window.location.pathname.includes('embed');
|
||||
|
||||
// For small player sizes, limit to 2 items for better readability
|
||||
// This works for both embed and regular players when they're small
|
||||
if (playerHeight <= 500 || playerWidth <= 600) {
|
||||
return 2; // 2x1 grid for small player sizes
|
||||
}
|
||||
|
||||
// Use player width for responsive decisions
|
||||
if (playerWidth >= 1200) {
|
||||
return 12; // 4x3 grid for large player
|
||||
} else if (playerWidth >= 1024) {
|
||||
return 9; // 3x3 grid for desktop-sized player
|
||||
} else if (playerWidth >= 768) {
|
||||
return 6; // 3x2 grid for tablet-sized player
|
||||
} else {
|
||||
return 4; // 2x2 grid for mobile-sized player
|
||||
}
|
||||
detectTouchDevice() {
|
||||
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
}
|
||||
|
||||
createSampleVideos() {
|
||||
return [
|
||||
{
|
||||
id: 'sample1',
|
||||
title: 'React Full Course for Beginners',
|
||||
author: 'Bro Code',
|
||||
views: '2.1M views',
|
||||
duration: 1800,
|
||||
thumbnail: 'https://img.youtube.com/vi/dGcsHMXbSOA/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample2',
|
||||
title: 'JavaScript ES6+ Features',
|
||||
author: 'Tech Tutorials',
|
||||
views: '850K views',
|
||||
duration: 1200,
|
||||
thumbnail: 'https://img.youtube.com/vi/WZQc7RUAg18/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample3',
|
||||
title: 'CSS Grid Layout Masterclass',
|
||||
author: 'Web Dev Academy',
|
||||
views: '1.2M views',
|
||||
duration: 2400,
|
||||
thumbnail: 'https://img.youtube.com/vi/0xMQfnTU6oo/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample4',
|
||||
title: 'Node.js Backend Development',
|
||||
author: 'Code Master',
|
||||
views: '650K views',
|
||||
duration: 3600,
|
||||
thumbnail: 'https://img.youtube.com/vi/fBNz6F-Cowg/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample5',
|
||||
title: 'Vue.js Complete Guide',
|
||||
author: 'Frontend Pro',
|
||||
views: '980K views',
|
||||
duration: 2800,
|
||||
thumbnail: 'https://img.youtube.com/vi/qZXt1Aom3Cs/maxresdefault.jpg',
|
||||
},
|
||||
{ id: 'sample1', title: 'React Full Course', author: 'Bro Code', views: '2.1M views', duration: 1800 },
|
||||
{ id: 'sample2', title: 'JavaScript ES6+', author: 'Tech Tutorials', views: '850K views', duration: 1200 },
|
||||
{ id: 'sample3', title: 'CSS Grid Layout', author: 'Web Dev Academy', views: '1.2M views', duration: 2400 },
|
||||
{ id: 'sample4', title: 'Node.js Backend', author: 'Code Master', views: '650K views', duration: 3600 },
|
||||
{ id: 'sample5', title: 'Vue.js Guide', author: 'Frontend Pro', views: '980K views', duration: 2800 },
|
||||
{
|
||||
id: 'sample6',
|
||||
title: 'Python Data Science',
|
||||
author: 'Data Academy',
|
||||
views: '1.5M views',
|
||||
duration: 4200,
|
||||
thumbnail: 'https://img.youtube.com/vi/ua-CiDNNj30/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample7',
|
||||
title: 'TypeScript Fundamentals',
|
||||
author: 'TypeScript Expert',
|
||||
views: '720K views',
|
||||
duration: 2100,
|
||||
thumbnail: 'https://img.youtube.com/vi/BwuLxPH8IDs/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample8',
|
||||
title: 'MongoDB Database Tutorial',
|
||||
author: 'Database Pro',
|
||||
views: '890K views',
|
||||
duration: 1800,
|
||||
thumbnail: 'https://img.youtube.com/vi/-56x56UppqQ/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample9',
|
||||
title: 'Docker Containerization',
|
||||
author: 'DevOps Master',
|
||||
views: '1.1M views',
|
||||
duration: 3200,
|
||||
thumbnail: 'https://img.youtube.com/vi/pTFZFxd4hOI/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample10',
|
||||
title: 'AWS Cloud Services',
|
||||
author: 'Cloud Expert',
|
||||
views: '1.3M views',
|
||||
duration: 4500,
|
||||
thumbnail: 'https://img.youtube.com/vi/ITcXLS3h2qU/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample11',
|
||||
title: 'GraphQL API Design',
|
||||
author: 'API Specialist',
|
||||
views: '680K views',
|
||||
duration: 2600,
|
||||
thumbnail: 'https://img.youtube.com/vi/ed8SzALpx1Q/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample12',
|
||||
title: 'Machine Learning Basics',
|
||||
author: 'AI Academy',
|
||||
views: '2.3M views',
|
||||
duration: 5400,
|
||||
thumbnail: 'https://img.youtube.com/vi/i_LwzRVP7bg/maxresdefault.jpg',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
isTouchDevice() {
|
||||
// Multiple methods to detect touch devices
|
||||
return (
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0 ||
|
||||
window.matchMedia('(pointer: coarse)').matches
|
||||
);
|
||||
}
|
||||
|
||||
show() {
|
||||
this.el().style.display = 'flex';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,689 @@
|
||||
/* ===== END SCREEN OVERLAY STYLES ===== */
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(100% - 80px); /* Reduce reserved space for seekbar */
|
||||
background: #000000;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: center; /* Center the grid vertically */
|
||||
align-items: center;
|
||||
padding: 40px 40px 40px 40px; /* Equal visual margins on all sides */
|
||||
box-sizing: border-box;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Hide poster image when video ends and end screen is shown */
|
||||
.video-js.vjs-ended .vjs-poster {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
z-index: -1 !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
/* Hide video element completely when ended */
|
||||
.video-js.vjs-ended video {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* Ensure the overlay covers everything with maximum z-index */
|
||||
.video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
z-index: 99999 !important;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* Embed-specific full page overlay */
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: calc(100vh - 80px) !important; /* Reduce reserved space for controls */
|
||||
z-index: 9998 !important; /* Below controls but above video */
|
||||
display: flex !important;
|
||||
padding: 120px 40px 40px 40px !important; /* Top padding for embed info + equal visual margins */
|
||||
justify-content: center !important; /* Center the grid vertically */
|
||||
}
|
||||
|
||||
/* Small player size optimization - 2 items horizontally for better title readability */
|
||||
/* This applies to both embed and regular players when they're small */
|
||||
.vjs-end-screen-overlay.vjs-small-player .vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
grid-template-rows: 1fr !important;
|
||||
gap: 20px !important;
|
||||
max-width: 600px; /* Limit width for better proportions */
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay.vjs-small-player {
|
||||
height: calc(100% - 60px) !important;
|
||||
padding: 30px !important;
|
||||
}
|
||||
|
||||
/* Hide items beyond the first 2 for small players */
|
||||
.vjs-end-screen-overlay.vjs-small-player .vjs-related-video-item:nth-child(n + 3) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Embed-specific adjustments for small sizes */
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay.vjs-small-player {
|
||||
height: calc(100vh - 60px) !important;
|
||||
padding: 80px 30px 30px 30px !important;
|
||||
}
|
||||
|
||||
/* Fallback media query for cases where class detection might not work */
|
||||
@media (max-height: 500px), (max-width: 600px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
grid-template-rows: 1fr !important;
|
||||
gap: 20px !important;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 60px) !important;
|
||||
padding: 30px !important;
|
||||
}
|
||||
|
||||
.vjs-related-video-item:nth-child(n + 3) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 60px) !important;
|
||||
padding: 80px 30px 30px 30px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Very small player size - further optimize spacing (class-based detection) */
|
||||
.vjs-end-screen-overlay.vjs-very-small-player .vjs-related-videos-grid {
|
||||
gap: 15px !important;
|
||||
max-width: 500px !important;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay.vjs-very-small-player {
|
||||
height: calc(100% - 50px) !important;
|
||||
padding: 25px !important;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay.vjs-very-small-player .vjs-related-video-item {
|
||||
min-height: 80px !important;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay.vjs-very-small-player {
|
||||
height: calc(100vh - 50px) !important;
|
||||
padding: 60px 25px 25px 25px !important;
|
||||
}
|
||||
|
||||
/* Fallback media query for very small sizes */
|
||||
@media (max-height: 400px), (max-width: 400px) {
|
||||
.vjs-related-videos-grid {
|
||||
gap: 15px !important;
|
||||
max-width: 500px !important;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 50px) !important;
|
||||
padding: 25px !important;
|
||||
}
|
||||
|
||||
.vjs-related-video-item {
|
||||
min-height: 80px !important;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 50px) !important;
|
||||
padding: 60px 25px 25px 25px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure controls stay visible over the black background */
|
||||
.video-js.vjs-ended .vjs-control-bar {
|
||||
z-index: 10000 !important;
|
||||
position: absolute !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100% !important;
|
||||
display: flex !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-progress-control {
|
||||
z-index: 10001 !important;
|
||||
position: absolute !important;
|
||||
bottom: 48px !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100% !important;
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Embed-specific controls handling when ended */
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-control-bar {
|
||||
position: fixed !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100vw !important;
|
||||
z-index: 10000 !important;
|
||||
display: flex !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-progress-control {
|
||||
position: fixed !important;
|
||||
bottom: 48px !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100vw !important;
|
||||
z-index: 10001 !important;
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Ensure embed info overlay (title/avatar) stays visible when ended */
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-embed-info-overlay {
|
||||
z-index: 10002 !important;
|
||||
display: flex !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Hide big play button when end screen is active */
|
||||
.video-js.vjs-ended .vjs-big-play-button {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* Hide seek indicator (play icon) when end screen is active */
|
||||
.video-js.vjs-ended .vjs-seek-indicator {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* Make control bar and seekbar background black when video ends */
|
||||
.video-js.vjs-ended .vjs-control-bar {
|
||||
background: #000000 !important;
|
||||
background-color: #000000 !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-progress-control {
|
||||
background: #000000 !important;
|
||||
background-color: #000000 !important;
|
||||
}
|
||||
|
||||
/* Also ensure the gradient overlay is black when ended */
|
||||
.video-js.vjs-ended::after {
|
||||
background: #000000 !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
/* Remove any white elements or gradients */
|
||||
.video-js.vjs-ended::before {
|
||||
background: #000000 !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
/* Ensure all VideoJS overlays are black but preserve seekbar colors */
|
||||
.video-js.vjs-ended .vjs-loading-spinner,
|
||||
.video-js.vjs-ended .vjs-mouse-display {
|
||||
background: #000000 !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
/* Only change the background holder, preserve progress colors */
|
||||
.video-js.vjs-ended .vjs-progress-holder {
|
||||
background: rgba(255, 255, 255, 0.3) !important; /* Keep original transparent background */
|
||||
}
|
||||
|
||||
/* Hide any remaining VideoJS elements that might show white */
|
||||
.video-js.vjs-ended .vjs-tech,
|
||||
.video-js.vjs-ended .vjs-poster-overlay {
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
.vjs-related-videos-title {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.vjs-related-videos-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0; /* Remove margin since parent handles centering */
|
||||
box-sizing: border-box;
|
||||
justify-items: stretch;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
align-content: center; /* Center grid content */
|
||||
overflow: hidden;
|
||||
grid-auto-rows: 1fr;
|
||||
}
|
||||
|
||||
.vjs-related-video-item {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
aspect-ratio: 16/9;
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Apply rounded corners only when useRoundedCorners is true */
|
||||
.video-js.video-js-rounded-corners .vjs-related-video-item {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.vjs-related-video-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.vjs-related-video-thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
/* border-radius: 8px; */
|
||||
background: #1a1a1a; /* Fallback background */
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.vjs-related-video-item:hover .vjs-related-video-thumbnail {
|
||||
transform: scale(1.02); /* Subtle zoom like YouTube */
|
||||
}
|
||||
|
||||
.vjs-related-video-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.9));
|
||||
color: white;
|
||||
padding: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vjs-related-video-item:hover .vjs-related-video-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Show overlay by default on touch devices - match default hover behavior exactly */
|
||||
.vjs-related-video-item.vjs-touch-device .vjs-related-video-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.vjs-related-video-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 1.3;
|
||||
color: white;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vjs-related-video-meta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vjs-related-video-author {
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.vjs-related-video-views {
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.vjs-related-video-author::after {
|
||||
content: "•";
|
||||
margin-left: 8px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.vjs-related-video-duration {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* Apply rounded corners to duration badge only when useRoundedCorners is true */
|
||||
.video-js.video-js-rounded-corners .vjs-related-video-duration {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.vjs-related-video-item:hover .vjs-related-video-duration {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Show duration by default on touch devices */
|
||||
.vjs-related-video-item.vjs-touch-device .vjs-related-video-duration {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-control-bar {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-control-bar .vjs-control {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-control-bar button {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-control-bar .vjs-control.vjs-volume-control {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-control-bar .vjs-volume-panel.vjs-hover .vjs-volume-control {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Responsive grid adjustments for different screen sizes */
|
||||
@media (max-width: 1200px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 70px);
|
||||
padding: 35px;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 70px) !important;
|
||||
padding: 115px 35px 35px 35px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 60px);
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 60px) !important;
|
||||
padding: 110px 30px 30px 30px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 50px);
|
||||
padding: 25px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 50px) !important;
|
||||
padding: 105px 25px 25px 25px !important;
|
||||
}
|
||||
|
||||
.vjs-related-video-item {
|
||||
min-height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 40px);
|
||||
padding: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 40px) !important;
|
||||
padding: 100px 20px 20px 20px !important;
|
||||
}
|
||||
|
||||
.vjs-related-video-item {
|
||||
min-height: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-play-control {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-progress-control {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.video-js.vjs-ended .vjs-volume-panel {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* Responsive grid layouts */
|
||||
@media (min-width: 1200px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 80px);
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 80px) !important;
|
||||
padding: 120px 40px 40px 40px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
.vjs-related-video-item:nth-child(n + 10) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* iPad Pro and larger tablets */
|
||||
@media (min-width: 1024px) and (max-width: 1199px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Allow up to 9 videos on larger tablets */
|
||||
.vjs-related-video-item:nth-child(n + 10) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Large tablets like iPad Pro */
|
||||
@media (min-width: 900px) and (max-width: 1024px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Allow up to 9 videos on large tablets */
|
||||
.vjs-related-video-item:nth-child(n + 10) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 899px) {
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
height: calc(100% - 60px);
|
||||
padding: 30px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 60px) !important;
|
||||
padding: 110px 30px 30px 30px !important;
|
||||
}
|
||||
|
||||
/* Allow up to 9 videos on regular tablets */
|
||||
.vjs-related-video-item:nth-child(n + 10) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.vjs-related-video-item:nth-child(n + 5) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
padding: 12px;
|
||||
justify-content: center;
|
||||
height: calc(100% - 105px);
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 105px) !important;
|
||||
padding: 80px 12px 12px 12px !important;
|
||||
}
|
||||
|
||||
.vjs-related-video-thumbnail {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 574px) {
|
||||
.vjs-related-video-item:nth-child(n + 5) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
padding: 10px;
|
||||
justify-content: center;
|
||||
height: calc(100% - 100px);
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 100px) !important;
|
||||
padding: 80px 10px 10px 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 439px) {
|
||||
.vjs-related-video-item:nth-child(n + 5) {
|
||||
display: none;
|
||||
}
|
||||
.vjs-related-videos-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vjs-end-screen-overlay {
|
||||
padding: 8px;
|
||||
justify-content: center;
|
||||
height: calc(100% - 98px);
|
||||
}
|
||||
|
||||
#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-end-screen-overlay {
|
||||
height: calc(100vh - 98px) !important;
|
||||
padding: 80px 8px 8px 8px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.vjs-related-video-thumbnail {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
import videojs from 'video.js';
|
||||
import './EndScreenOverlay.css';
|
||||
|
||||
const Component = videojs.getComponent('Component');
|
||||
|
||||
class EndScreenOverlay extends Component {
|
||||
constructor(player, options) {
|
||||
// Store relatedVideos in options before calling super
|
||||
// so it's available during createEl()
|
||||
if (options && options.relatedVideos) {
|
||||
options._relatedVideos = options.relatedVideos;
|
||||
}
|
||||
|
||||
super(player, options);
|
||||
|
||||
// Now set the instance property after super() completes
|
||||
this.relatedVideos = options && options.relatedVideos ? options.relatedVideos : [];
|
||||
}
|
||||
|
||||
createEl() {
|
||||
// Get relatedVideos from options since createEl is called during super()
|
||||
const relatedVideos = this.options_ && this.options_._relatedVideos ? this.options_._relatedVideos : [];
|
||||
|
||||
// Limit videos based on screen size to fit grid properly
|
||||
const maxVideos = this.getMaxVideosForScreen();
|
||||
const videosToShow = relatedVideos.slice(0, maxVideos);
|
||||
|
||||
// Determine if player is small and add appropriate class
|
||||
const playerEl = this.player().el();
|
||||
const playerWidth = playerEl ? playerEl.offsetWidth : window.innerWidth;
|
||||
const playerHeight = playerEl ? playerEl.offsetHeight : window.innerHeight;
|
||||
const isSmallPlayer = playerHeight <= 500 || playerWidth <= 600;
|
||||
const isVerySmallPlayer = playerHeight <= 400 || playerWidth <= 400;
|
||||
|
||||
let overlayClasses = 'vjs-end-screen-overlay';
|
||||
if (isVerySmallPlayer) {
|
||||
overlayClasses += ' vjs-very-small-player vjs-small-player';
|
||||
} else if (isSmallPlayer) {
|
||||
overlayClasses += ' vjs-small-player';
|
||||
}
|
||||
|
||||
const overlay = super.createEl('div', {
|
||||
className: overlayClasses,
|
||||
});
|
||||
|
||||
// Create grid container
|
||||
const grid = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-videos-grid',
|
||||
});
|
||||
|
||||
// Create video items
|
||||
if (videosToShow && Array.isArray(videosToShow) && videosToShow.length > 0) {
|
||||
videosToShow.forEach((video) => {
|
||||
const videoItem = this.createVideoItem(video);
|
||||
grid.appendChild(videoItem);
|
||||
});
|
||||
} else {
|
||||
// Create sample videos for testing if no related videos provided
|
||||
const sampleVideos = this.createSampleVideos();
|
||||
sampleVideos.slice(0, this.getMaxVideosForScreen()).forEach((video) => {
|
||||
const videoItem = this.createVideoItem(video);
|
||||
grid.appendChild(videoItem);
|
||||
});
|
||||
}
|
||||
|
||||
overlay.appendChild(grid);
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
createVideoItem(video) {
|
||||
// Detect touch device
|
||||
const isTouchDevice = this.isTouchDevice();
|
||||
|
||||
const item = videojs.dom.createEl('div', {
|
||||
className: isTouchDevice ? 'vjs-related-video-item vjs-touch-device' : 'vjs-related-video-item',
|
||||
});
|
||||
|
||||
// Use real YouTube thumbnail or fallback to placeholder
|
||||
const thumbnailSrc = video.thumbnail || this.getPlaceholderImage(video.title);
|
||||
|
||||
const thumbnail = videojs.dom.createEl('img', {
|
||||
className: 'vjs-related-video-thumbnail',
|
||||
src: thumbnailSrc,
|
||||
alt: video.title,
|
||||
loading: 'lazy', // Lazy load for better performance
|
||||
onerror: () => {
|
||||
// Fallback to placeholder if image fails to load
|
||||
thumbnail.src = this.getPlaceholderImage(video.title);
|
||||
},
|
||||
});
|
||||
|
||||
const overlay = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-video-overlay',
|
||||
});
|
||||
|
||||
const title = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-video-title',
|
||||
});
|
||||
title.textContent = video.title;
|
||||
|
||||
// Create meta container for author and views
|
||||
const meta = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-video-meta',
|
||||
});
|
||||
|
||||
const author = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-video-author',
|
||||
});
|
||||
author.textContent = video.author;
|
||||
|
||||
const views = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-video-views',
|
||||
});
|
||||
views.textContent = video.views;
|
||||
|
||||
// Add author and views to meta container
|
||||
meta.appendChild(author);
|
||||
meta.appendChild(views);
|
||||
|
||||
// Add duration display (positioned absolutely in bottom right)
|
||||
const duration = videojs.dom.createEl('div', {
|
||||
className: 'vjs-related-video-duration',
|
||||
});
|
||||
|
||||
// Format duration from seconds to MM:SS
|
||||
const formatDuration = (seconds) => {
|
||||
if (!seconds || seconds === 0) return '';
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
duration.textContent = formatDuration(video.duration);
|
||||
|
||||
// Structure: title at top, meta at bottom
|
||||
overlay.appendChild(title);
|
||||
overlay.appendChild(meta);
|
||||
|
||||
item.appendChild(thumbnail);
|
||||
item.appendChild(overlay);
|
||||
|
||||
// Add duration to the item (positioned absolutely)
|
||||
if (video.duration && video.duration > 0) {
|
||||
item.appendChild(duration);
|
||||
}
|
||||
|
||||
// Add click handler
|
||||
item.addEventListener('click', () => {
|
||||
// Check if this is an embed player - use multiple methods for reliability
|
||||
const playerId = this.player().id() || this.player().options_.id;
|
||||
const isEmbedPlayer =
|
||||
playerId === 'video-embed' ||
|
||||
window.location.pathname.includes('/embed') ||
|
||||
window.location.search.includes('embed') ||
|
||||
window.parent !== window; // Most reliable check for iframe
|
||||
|
||||
if (isEmbedPlayer) {
|
||||
// Open in new tab/window for embed players
|
||||
window.open(`/view?m=${video.id}`, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
// Navigate in same window for regular players
|
||||
window.location.href = `/view?m=${video.id}`;
|
||||
}
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
getPlaceholderImage(title) {
|
||||
// Generate a placeholder image using a service or create a data URL
|
||||
// For now, we'll use a simple colored placeholder based on the title
|
||||
const colors = [
|
||||
'#009931',
|
||||
'#4ECDC4',
|
||||
'#45B7D1',
|
||||
'#96CEB4',
|
||||
'#FFEAA7',
|
||||
'#DDA0DD',
|
||||
'#98D8C8',
|
||||
'#F7DC6F',
|
||||
'#BB8FCE',
|
||||
'#85C1E9',
|
||||
];
|
||||
|
||||
// Use title hash to consistently assign colors
|
||||
let hash = 0;
|
||||
for (let i = 0; i < title.length; i++) {
|
||||
hash = title.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
const colorIndex = Math.abs(hash) % colors.length;
|
||||
const color = colors[colorIndex];
|
||||
|
||||
// Create a simple placeholder with the first letter of the title
|
||||
const firstLetter = title.charAt(0).toUpperCase();
|
||||
|
||||
// Create a data URL for a simple placeholder image
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 320;
|
||||
canvas.height = 180;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Background
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(0, 0, 320, 180);
|
||||
|
||||
// Add a subtle pattern
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
|
||||
for (let i = 0; i < 20; i++) {
|
||||
ctx.fillRect(Math.random() * 320, Math.random() * 180, 2, 2);
|
||||
}
|
||||
|
||||
// Add the first letter
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = 'bold 48px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(firstLetter, 160, 90);
|
||||
|
||||
return canvas.toDataURL();
|
||||
}
|
||||
|
||||
getMaxVideosForScreen() {
|
||||
// Get actual player dimensions instead of window dimensions
|
||||
const playerEl = this.player().el();
|
||||
const playerWidth = playerEl ? playerEl.offsetWidth : window.innerWidth;
|
||||
const playerHeight = playerEl ? playerEl.offsetHeight : window.innerHeight;
|
||||
|
||||
// Check if this is an embed player
|
||||
const playerId = this.player().id() || this.player().options_.id;
|
||||
const isEmbedPlayer =
|
||||
playerId === 'video-embed' ||
|
||||
document.getElementById('page-embed') ||
|
||||
window.location.pathname.includes('embed');
|
||||
|
||||
// For small player sizes, limit to 2 items for better readability
|
||||
// This works for both embed and regular players when they're small
|
||||
if (playerHeight <= 500 || playerWidth <= 600) {
|
||||
return 2; // 2x1 grid for small player sizes
|
||||
}
|
||||
|
||||
// Use player width for responsive decisions
|
||||
if (playerWidth >= 1200) {
|
||||
return 12; // 4x3 grid for large player
|
||||
} else if (playerWidth >= 1024) {
|
||||
return 9; // 3x3 grid for desktop-sized player
|
||||
} else if (playerWidth >= 768) {
|
||||
return 6; // 3x2 grid for tablet-sized player
|
||||
} else {
|
||||
return 4; // 2x2 grid for mobile-sized player
|
||||
}
|
||||
}
|
||||
|
||||
createSampleVideos() {
|
||||
return [
|
||||
{
|
||||
id: 'sample1',
|
||||
title: 'React Full Course for Beginners',
|
||||
author: 'Bro Code',
|
||||
views: '2.1M views',
|
||||
duration: 1800,
|
||||
thumbnail: 'https://img.youtube.com/vi/dGcsHMXbSOA/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample2',
|
||||
title: 'JavaScript ES6+ Features',
|
||||
author: 'Tech Tutorials',
|
||||
views: '850K views',
|
||||
duration: 1200,
|
||||
thumbnail: 'https://img.youtube.com/vi/WZQc7RUAg18/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample3',
|
||||
title: 'CSS Grid Layout Masterclass',
|
||||
author: 'Web Dev Academy',
|
||||
views: '1.2M views',
|
||||
duration: 2400,
|
||||
thumbnail: 'https://img.youtube.com/vi/0xMQfnTU6oo/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample4',
|
||||
title: 'Node.js Backend Development',
|
||||
author: 'Code Master',
|
||||
views: '650K views',
|
||||
duration: 3600,
|
||||
thumbnail: 'https://img.youtube.com/vi/fBNz6F-Cowg/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample5',
|
||||
title: 'Vue.js Complete Guide',
|
||||
author: 'Frontend Pro',
|
||||
views: '980K views',
|
||||
duration: 2800,
|
||||
thumbnail: 'https://img.youtube.com/vi/qZXt1Aom3Cs/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample6',
|
||||
title: 'Python Data Science',
|
||||
author: 'Data Academy',
|
||||
views: '1.5M views',
|
||||
duration: 4200,
|
||||
thumbnail: 'https://img.youtube.com/vi/ua-CiDNNj30/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample7',
|
||||
title: 'TypeScript Fundamentals',
|
||||
author: 'TypeScript Expert',
|
||||
views: '720K views',
|
||||
duration: 2100,
|
||||
thumbnail: 'https://img.youtube.com/vi/BwuLxPH8IDs/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample8',
|
||||
title: 'MongoDB Database Tutorial',
|
||||
author: 'Database Pro',
|
||||
views: '890K views',
|
||||
duration: 1800,
|
||||
thumbnail: 'https://img.youtube.com/vi/-56x56UppqQ/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample9',
|
||||
title: 'Docker Containerization',
|
||||
author: 'DevOps Master',
|
||||
views: '1.1M views',
|
||||
duration: 3200,
|
||||
thumbnail: 'https://img.youtube.com/vi/pTFZFxd4hOI/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample10',
|
||||
title: 'AWS Cloud Services',
|
||||
author: 'Cloud Expert',
|
||||
views: '1.3M views',
|
||||
duration: 4500,
|
||||
thumbnail: 'https://img.youtube.com/vi/ITcXLS3h2qU/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample11',
|
||||
title: 'GraphQL API Design',
|
||||
author: 'API Specialist',
|
||||
views: '680K views',
|
||||
duration: 2600,
|
||||
thumbnail: 'https://img.youtube.com/vi/ed8SzALpx1Q/maxresdefault.jpg',
|
||||
},
|
||||
{
|
||||
id: 'sample12',
|
||||
title: 'Machine Learning Basics',
|
||||
author: 'AI Academy',
|
||||
views: '2.3M views',
|
||||
duration: 5400,
|
||||
thumbnail: 'https://img.youtube.com/vi/i_LwzRVP7bg/maxresdefault.jpg',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
isTouchDevice() {
|
||||
// Multiple methods to detect touch devices
|
||||
return (
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0 ||
|
||||
window.matchMedia('(pointer: coarse)').matches
|
||||
);
|
||||
}
|
||||
|
||||
show() {
|
||||
this.el().style.display = 'flex';
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.el().style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Register the component
|
||||
videojs.registerComponent('EndScreenOverlay', EndScreenOverlay);
|
||||
|
||||
export default EndScreenOverlay;
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useEffect, useRef, useMemo } from 'react';
|
||||
import videojs from 'video.js';
|
||||
import 'video.js/dist/video-js.css';
|
||||
import '../../VideoJS.css';
|
||||
// import '../../VideoJS.css';
|
||||
import '../../styles/embed.css';
|
||||
import '../controls/SubtitlesButton.css';
|
||||
//import '../controls/SubtitlesButton.css';
|
||||
|
||||
// Import the separated components
|
||||
import EndScreenOverlay from '../overlays/EndScreenOverlay';
|
||||
@@ -2307,29 +2307,14 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
// BEGIN: Wrap play button in custom div container
|
||||
const playButtonEl = playToggle.el();
|
||||
const playButtonWrapper = document.createElement('div');
|
||||
playButtonWrapper.className =
|
||||
'vjs-play-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button';
|
||||
/* playButtonWrapper.className =
|
||||
'vjs-play-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button'; */
|
||||
|
||||
// Insert wrapper before the play button and move play button inside
|
||||
playButtonEl.parentNode.insertBefore(playButtonWrapper, playButtonEl);
|
||||
playButtonWrapper.appendChild(playButtonEl);
|
||||
// playButtonEl.parentNode.insertBefore(playButtonWrapper, playButtonEl);
|
||||
// playButtonWrapper.appendChild(playButtonEl);
|
||||
// END: Wrap play button in custom div container
|
||||
|
||||
// BEGIN: Implement custom time display component
|
||||
const customRemainingTime = new CustomRemainingTime(playerRef.current, {
|
||||
displayNegative: false,
|
||||
customPrefix: '',
|
||||
customSuffix: '',
|
||||
});
|
||||
|
||||
// Insert it early in control bar - right after play button for priority
|
||||
const playToggleIndex = controlBar.children().indexOf(playToggle);
|
||||
controlBar.addChild(customRemainingTime, {}, playToggleIndex + 1);
|
||||
|
||||
// Store reference for cleanup
|
||||
customComponents.current.customRemainingTime = customRemainingTime;
|
||||
// END: Implement custom time display component
|
||||
|
||||
// BEGIN: Implement custom next video button
|
||||
if (!isEmbedPlayer && (mediaData?.nextLink || isDevMode)) {
|
||||
// it seems that the nextLink is not always available, and it is need the this.player().trigger('nextVideo'); from NextVideoButton.js // TODO: remove the 1===1 and the mediaData?.nextLink
|
||||
@@ -2337,24 +2322,35 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
|
||||
nextLink: mediaData.nextLink,
|
||||
});
|
||||
const playToggleIndex = controlBar.children().indexOf(playToggle); // Insert it after play button
|
||||
controlBar.addChild(nextVideoButton, {}, playToggleIndex + 2); // After time display
|
||||
controlBar.addChild(nextVideoButton, {}, playToggleIndex + 1); // After time display
|
||||
|
||||
// Wrap next video button in custom div container
|
||||
setTimeout(() => {
|
||||
const nextVideoButtonEl = nextVideoButton.el();
|
||||
if (nextVideoButtonEl) {
|
||||
const nextVideoWrapper = document.createElement('div');
|
||||
nextVideoWrapper.className =
|
||||
'vjs-next-video-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button';
|
||||
// setTimeout(() => {
|
||||
// const nextVideoButtonEl = nextVideoButton.el();
|
||||
// if (nextVideoButtonEl) {
|
||||
// const nextVideoWrapper = document.createElement('div');
|
||||
// /* nextVideoWrapper.className =
|
||||
// 'vjs-next-video-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button'; */
|
||||
|
||||
// Insert wrapper before the next video button and move button inside
|
||||
nextVideoButtonEl.parentNode.insertBefore(nextVideoWrapper, nextVideoButtonEl);
|
||||
nextVideoWrapper.appendChild(nextVideoButtonEl);
|
||||
}
|
||||
}, 100); // Small delay to ensure button is fully rendered
|
||||
// // Insert wrapper before the next video button and move button inside
|
||||
// nextVideoButtonEl.parentNode.insertBefore(nextVideoWrapper, nextVideoButtonEl);
|
||||
// nextVideoWrapper.appendChild(nextVideoButtonEl);
|
||||
// }
|
||||
// }, 2000); // Small delay to ensure button is fully rendered
|
||||
}
|
||||
// END: Implement custom next video button
|
||||
|
||||
// BEGIN: Implement custom time display component
|
||||
const customRemainingTime = new CustomRemainingTime(playerRef.current, {
|
||||
displayNegative: false,
|
||||
customPrefix: '',
|
||||
customSuffix: '',
|
||||
});
|
||||
const playToggleIndex = controlBar.children().indexOf(playToggle);
|
||||
controlBar.addChild(customRemainingTime, {}, playToggleIndex + 2);
|
||||
customComponents.current.customRemainingTime = customRemainingTime;
|
||||
// END: Implement custom time display component
|
||||
|
||||
// BEGIN: Wrap volume panel in custom div container
|
||||
setTimeout(() => {
|
||||
const volumePanel = controlBar.getChild('volumePanel');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
/* ===== VIDEO.JS ROUNDED CORNERS STYLES ===== */
|
||||
/* Extracted from VideoJS.css for modular import */
|
||||
|
||||
.video-js-root-main .video-js.video-js-rounded-corners,
|
||||
.video-js-root-main .video-js.video-js-rounded-corners.vjs-has-started,
|
||||
.video-js-root-main .video-js.video-js-rounded-corners.vjs-fullscreen,
|
||||
.video-js-root-main .video-js.video-js-rounded-corners.vjs-paused,
|
||||
.video-js-root-main .video-js.video-js-rounded-corners.vjs-ended,
|
||||
.video-js-root-main .video-js.video-js-rounded-corners.chapters-open {
|
||||
/* background-color: transparent !important; */
|
||||
outline: none !important;
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.video-js-root-main .video-js.video-js-rounded-corners .vjs-poster {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.video-js-root-main .video-js.video-js-rounded-corners video {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
/* YouTube-style bottom gradient overlay - covers entire video bottom when controls active */
|
||||
/* .video-js-root-main .video-js.video-js-rounded-corners::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 120px;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgba(0, 0, 0, 0.8) 0%,
|
||||
rgba(0, 0, 0, 0.6) 25%,
|
||||
rgba(0, 0, 0, 0.4) 50%,
|
||||
rgba(0, 0, 0, 0.2) 75%,
|
||||
transparent 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 2;
|
||||
} */
|
||||
|
||||
/* Show overlay when controls are active - YouTube style */
|
||||
.video-js-root-main .video-js.video-js-rounded-corners.vjs-user-active::after,
|
||||
.video-js-root-main .video-js.video-js-rounded-corners.vjs-paused::after,
|
||||
.video-js-root-main .video-js.video-js-rounded-corners.vjs-ended::after {
|
||||
border-bottom-left-radius: 12px !important;
|
||||
border-bottom-right-radius: 12px !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Mobile responsive adjustments */
|
||||
@media (max-width: 767px) {
|
||||
/* Remove rounded corners on mobile screens */
|
||||
.video-js-root-main .video-js.video-js-rounded-corners,
|
||||
.video-js-root-main .video-js.video-js-rounded-corners.vjs-has-started,
|
||||
.video-js-root-main .video-js.video-js-rounded-corners.vjs-fullscreen,
|
||||
.video-js-root-main .video-js.video-js-rounded-corners.vjs-paused,
|
||||
.video-js-root-main .video-js.video-js-rounded-corners.vjs-ended,
|
||||
.video-js-root-main .video-js.video-js-rounded-corners.chapters-open {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.video-js-root-main .video-js.video-js-rounded-corners .vjs-poster {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.video-js-root-main .video-js.video-js-rounded-corners video {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,51 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './VideoJS.css';
|
||||
|
||||
import VideoJS from './VideoJS.jsx';
|
||||
import VideoJSNew from './VideoJSNew.jsx';
|
||||
|
||||
// Mount the components when the DOM is ready
|
||||
const mountComponents = () => {
|
||||
// Mount main video player
|
||||
const rootContainerMain = document.getElementById('video-js-root-main');
|
||||
const rootContainerMain = document.getElementById('video-js-root-main-old');
|
||||
if (rootContainerMain && !rootContainerMain.hasChildNodes()) {
|
||||
const rootMain = createRoot(rootContainerMain);
|
||||
rootMain.render(
|
||||
<StrictMode>
|
||||
<VideoJS videoId="video-main" />
|
||||
<VideoJS videoId="video-main-old" />
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
// Mount embed video player
|
||||
const rootContainerEmbed = document.getElementById('video-js-root-embed');
|
||||
const rootContainerEmbed = document.getElementById('video-js-root-embed-old');
|
||||
if (rootContainerEmbed && !rootContainerEmbed.hasChildNodes()) {
|
||||
const rootEmbed = createRoot(rootContainerEmbed);
|
||||
rootEmbed.render(
|
||||
<StrictMode>
|
||||
<VideoJS videoId="video-embed" />
|
||||
<VideoJS videoId="video-embed-old" />
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
// Mount main video player
|
||||
const rootContainerMainNew = document.getElementById('video-js-root-main');
|
||||
if (rootContainerMainNew && !rootContainerMainNew.hasChildNodes()) {
|
||||
const rootMain = createRoot(rootContainerMainNew);
|
||||
rootMain.render(
|
||||
<StrictMode>
|
||||
<VideoJSNew videoId="video-main" />
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
// Mount embed video player
|
||||
const rootContainerEmbedNew = document.getElementById('video-js-root-embed');
|
||||
if (rootContainerEmbedNew && !rootContainerEmbedNew.hasChildNodes()) {
|
||||
const rootEmbed = createRoot(rootContainerEmbedNew);
|
||||
rootEmbed.render(
|
||||
<StrictMode>
|
||||
<VideoJSNew videoId="video-embed" />
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
export class AutoplayHandler {
|
||||
constructor(player, mediaData, userPreferences) {
|
||||
this.player = player;
|
||||
this.mediaData = mediaData;
|
||||
this.userPreferences = userPreferences;
|
||||
}
|
||||
|
||||
hasUserInteracted() {
|
||||
return (
|
||||
document.hasFocus() ||
|
||||
document.visibilityState === 'visible' ||
|
||||
sessionStorage.getItem('userInteracted') === 'true'
|
||||
);
|
||||
}
|
||||
|
||||
async handleAutoplay() {
|
||||
// Don't attempt autoplay if already playing or loading
|
||||
if (!this.player.paused() || this.player.seeking()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Define variables outside try block so they're accessible in catch
|
||||
const userInteracted = this.hasUserInteracted();
|
||||
const savedMuteState = this.userPreferences.getPreference('muted');
|
||||
|
||||
try {
|
||||
// Respect user's saved mute preference, but try unmuted if user interacted and hasn't explicitly muted
|
||||
if (!this.mediaData.urlMuted && userInteracted && savedMuteState !== true) {
|
||||
this.player.muted(false);
|
||||
}
|
||||
|
||||
// First attempt: try to play with current mute state
|
||||
await this.player.play();
|
||||
} catch (error) {
|
||||
// Fallback to muted autoplay unless user explicitly wants to stay unmuted
|
||||
if (!this.player.muted()) {
|
||||
try {
|
||||
this.player.muted(true);
|
||||
await this.player.play();
|
||||
|
||||
// Only try to restore sound if user hasn't explicitly saved mute=true
|
||||
if (savedMuteState !== true) {
|
||||
this.restoreSound(userInteracted);
|
||||
}
|
||||
} catch (mutedError) {
|
||||
console.error('❌ Even muted autoplay was blocked:', mutedError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
restoreSound(userInteracted) {
|
||||
const restoreSound = () => {
|
||||
if (this.player && !this.player.isDisposed()) {
|
||||
this.player.muted(false);
|
||||
this.player.trigger('notify', '🔊 Sound enabled!');
|
||||
}
|
||||
};
|
||||
|
||||
// Try to restore sound immediately if user has interacted
|
||||
if (userInteracted) {
|
||||
setTimeout(restoreSound, 100);
|
||||
} else {
|
||||
// Show notification for manual interaction
|
||||
setTimeout(() => {
|
||||
if (this.player && !this.player.isDisposed()) {
|
||||
this.player.trigger('notify', '🔇 Click anywhere to enable sound');
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Set up interaction listeners
|
||||
const enableSound = () => {
|
||||
restoreSound();
|
||||
// Mark user interaction for future videos
|
||||
sessionStorage.setItem('userInteracted', 'true');
|
||||
// Remove listeners
|
||||
document.removeEventListener('click', enableSound);
|
||||
document.removeEventListener('keydown', enableSound);
|
||||
document.removeEventListener('touchstart', enableSound);
|
||||
};
|
||||
|
||||
document.addEventListener('click', enableSound, { once: true });
|
||||
document.addEventListener('keydown', enableSound, { once: true });
|
||||
document.addEventListener('touchstart', enableSound, { once: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import EndScreenOverlay from '../components/overlays/EndScreenOverlay';
|
||||
import AutoplayCountdownOverlay from '../components/overlays/AutoplayCountdownOverlay';
|
||||
|
||||
export class EndScreenHandler {
|
||||
constructor(player, options) {
|
||||
this.player = player;
|
||||
this.options = options;
|
||||
this.endScreen = null;
|
||||
this.autoplayCountdown = null;
|
||||
|
||||
this.setupEndScreenHandling();
|
||||
}
|
||||
|
||||
setupEndScreenHandling() {
|
||||
// Handle video ended event
|
||||
this.player.on('ended', () => {
|
||||
this.handleVideoEnded();
|
||||
});
|
||||
|
||||
// Hide end screen and autoplay countdown when user wants to replay
|
||||
const hideEndScreenAndStopCountdown = () => {
|
||||
if (this.endScreen) {
|
||||
this.endScreen.hide();
|
||||
}
|
||||
if (this.autoplayCountdown) {
|
||||
this.autoplayCountdown.stopCountdown();
|
||||
}
|
||||
};
|
||||
|
||||
this.player.on('play', hideEndScreenAndStopCountdown);
|
||||
this.player.on('seeking', hideEndScreenAndStopCountdown);
|
||||
}
|
||||
|
||||
handleVideoEnded() {
|
||||
const { isEmbedPlayer, userPreferences, mediaData, currentVideo, relatedVideos, goToNextVideo } = this.options;
|
||||
|
||||
// For embed players, show big play button when video ends
|
||||
if (isEmbedPlayer) {
|
||||
const bigPlayButton = this.player.getChild('bigPlayButton');
|
||||
if (bigPlayButton) {
|
||||
bigPlayButton.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Keep controls active after video ends
|
||||
setTimeout(() => {
|
||||
if (this.player && !this.player.isDisposed()) {
|
||||
const playerEl = this.player.el();
|
||||
if (playerEl) {
|
||||
// Hide poster image when end screen is shown - multiple approaches
|
||||
const posterImage = this.player.getChild('posterImage');
|
||||
if (posterImage) {
|
||||
posterImage.hide();
|
||||
posterImage.el().style.display = 'none';
|
||||
posterImage.el().style.visibility = 'hidden';
|
||||
posterImage.el().style.opacity = '0';
|
||||
}
|
||||
|
||||
// Hide all poster elements directly
|
||||
const posterElements = playerEl.querySelectorAll('.vjs-poster');
|
||||
posterElements.forEach((posterEl) => {
|
||||
posterEl.style.display = 'none';
|
||||
posterEl.style.visibility = 'hidden';
|
||||
posterEl.style.opacity = '0';
|
||||
});
|
||||
|
||||
// Set player background to dark to match end screen
|
||||
playerEl.style.backgroundColor = '#000';
|
||||
|
||||
// Keep video element visible but ensure it doesn't show poster
|
||||
const videoEl = playerEl.querySelector('video');
|
||||
if (videoEl) {
|
||||
// Remove poster attribute from video element
|
||||
videoEl.removeAttribute('poster');
|
||||
videoEl.style.backgroundColor = '#000';
|
||||
}
|
||||
|
||||
// Keep the visual ended state but ensure controls work
|
||||
const controlBar = this.player.getChild('controlBar');
|
||||
if (controlBar) {
|
||||
controlBar.show();
|
||||
controlBar.el().style.opacity = '1';
|
||||
controlBar.el().style.pointerEvents = 'auto';
|
||||
|
||||
// Style progress bar to match dark end screen background
|
||||
const progressControl = controlBar.getChild('progressControl');
|
||||
if (progressControl) {
|
||||
progressControl.show();
|
||||
progressControl.el().style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
|
||||
|
||||
// Also style the progress holder for seamless look
|
||||
const progressHolder = progressControl.el().querySelector('.vjs-progress-holder');
|
||||
if (progressHolder) {
|
||||
progressHolder.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
|
||||
// Check if autoplay is enabled and there's a next video
|
||||
const isAutoplayEnabled = userPreferences.getAutoplayPreference();
|
||||
const hasNextVideo = mediaData.nextLink !== null;
|
||||
|
||||
if (!isEmbedPlayer && isAutoplayEnabled && hasNextVideo) {
|
||||
// If it's a playlist, skip countdown and play directly
|
||||
if (currentVideo.isPlayList) {
|
||||
this.cleanupOverlays();
|
||||
goToNextVideo();
|
||||
} else {
|
||||
this.showAutoplayCountdown(relatedVideos, goToNextVideo);
|
||||
}
|
||||
} else {
|
||||
// Autoplay disabled or no next video - show regular end screen
|
||||
this.showEndScreen(relatedVideos);
|
||||
}
|
||||
}
|
||||
|
||||
showAutoplayCountdown(relatedVideos, goToNextVideo) {
|
||||
// Get next video data for countdown display - find the next video in related videos
|
||||
let nextVideoData = {
|
||||
title: 'Next Video',
|
||||
author: '',
|
||||
duration: 0,
|
||||
thumbnail: '',
|
||||
};
|
||||
|
||||
// Try to find the next video by URL matching or just use the first related video
|
||||
if (relatedVideos.length > 0) {
|
||||
const nextVideo = relatedVideos[0];
|
||||
nextVideoData = {
|
||||
title: nextVideo.title || 'Next Video',
|
||||
author: nextVideo.author || '',
|
||||
duration: nextVideo.duration || 0,
|
||||
thumbnail: nextVideo.thumbnail || '',
|
||||
};
|
||||
}
|
||||
|
||||
// Clean up any existing overlays
|
||||
this.cleanupOverlays();
|
||||
|
||||
// Show autoplay countdown immediately!
|
||||
this.autoplayCountdown = new AutoplayCountdownOverlay(this.player, {
|
||||
nextVideoData: nextVideoData,
|
||||
countdownSeconds: 5,
|
||||
onPlayNext: () => {
|
||||
goToNextVideo();
|
||||
},
|
||||
onCancel: () => {
|
||||
// Hide countdown and show end screen instead
|
||||
if (this.autoplayCountdown) {
|
||||
this.player.removeChild(this.autoplayCountdown);
|
||||
this.autoplayCountdown = null;
|
||||
}
|
||||
this.showEndScreen(relatedVideos);
|
||||
},
|
||||
});
|
||||
|
||||
this.player.addChild(this.autoplayCountdown);
|
||||
// Start countdown immediately without any delay
|
||||
setTimeout(() => {
|
||||
if (this.autoplayCountdown && !this.autoplayCountdown.isDisposed()) {
|
||||
this.autoplayCountdown.startCountdown();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
showEndScreen(relatedVideos) {
|
||||
// Prevent creating multiple end screens
|
||||
if (this.endScreen) {
|
||||
this.player.removeChild(this.endScreen);
|
||||
this.endScreen = null;
|
||||
}
|
||||
|
||||
// Show end screen with related videos
|
||||
this.endScreen = new EndScreenOverlay(this.player, {
|
||||
relatedVideos: relatedVideos,
|
||||
});
|
||||
|
||||
// Also store the data directly on the component as backup and update it
|
||||
this.endScreen.relatedVideos = relatedVideos;
|
||||
if (this.endScreen.setRelatedVideos) {
|
||||
this.endScreen.setRelatedVideos(relatedVideos);
|
||||
}
|
||||
|
||||
this.player.addChild(this.endScreen);
|
||||
this.endScreen.show();
|
||||
}
|
||||
|
||||
cleanupOverlays() {
|
||||
// Clean up any existing overlays
|
||||
if (this.endScreen) {
|
||||
this.player.removeChild(this.endScreen);
|
||||
this.endScreen = null;
|
||||
}
|
||||
if (this.autoplayCountdown) {
|
||||
this.player.removeChild(this.autoplayCountdown);
|
||||
this.autoplayCountdown = null;
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.cleanupOverlays();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* KeyboardHandler - Utility for handling video player keyboard controls
|
||||
*
|
||||
* Provides comprehensive keyboard event handling for video players including:
|
||||
* - Space bar for play/pause
|
||||
* - Arrow keys for seeking
|
||||
* - Input field detection to avoid conflicts
|
||||
*/
|
||||
|
||||
class KeyboardHandler {
|
||||
constructor(playerRef, customComponents = null, options = {}) {
|
||||
this.playerRef = playerRef;
|
||||
this.customComponents = customComponents;
|
||||
this.options = {
|
||||
seekAmount: 5, // Default seek amount in seconds
|
||||
...options,
|
||||
};
|
||||
this.eventHandler = null;
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an input element is currently focused
|
||||
* @returns {boolean} True if an input element has focus
|
||||
*/
|
||||
isInputFocused() {
|
||||
const activeElement = document.activeElement;
|
||||
return (
|
||||
activeElement &&
|
||||
(activeElement.tagName === 'INPUT' ||
|
||||
activeElement.tagName === 'TEXTAREA' ||
|
||||
activeElement.contentEditable === 'true')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle space key for play/pause functionality
|
||||
* @param {KeyboardEvent} event - The keyboard event
|
||||
*/
|
||||
handleSpaceKey(event) {
|
||||
if (event.code === 'Space' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
if (this.playerRef.current) {
|
||||
if (this.playerRef.current.paused()) {
|
||||
this.playerRef.current.play();
|
||||
} else {
|
||||
this.playerRef.current.pause();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle arrow keys for seeking functionality
|
||||
* @param {KeyboardEvent} event - The keyboard event
|
||||
*/
|
||||
handleArrowKeys(event) {
|
||||
const { seekAmount } = this.options;
|
||||
|
||||
if (event.key === 'ArrowRight' || event.keyCode === 39) {
|
||||
event.preventDefault();
|
||||
this.seekForward(seekAmount);
|
||||
return true;
|
||||
} else if (event.key === 'ArrowLeft' || event.keyCode === 37) {
|
||||
event.preventDefault();
|
||||
this.seekBackward(seekAmount);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek forward by specified amount
|
||||
* @param {number} amount - Seconds to seek forward
|
||||
*/
|
||||
seekForward(amount) {
|
||||
if (!this.playerRef.current) return;
|
||||
|
||||
const currentTime = this.playerRef.current.currentTime();
|
||||
const duration = this.playerRef.current.duration();
|
||||
const newTime = Math.min(currentTime + amount, duration);
|
||||
|
||||
this.playerRef.current.currentTime(newTime);
|
||||
|
||||
// Show seek indicator if available
|
||||
if (this.customComponents?.current?.seekIndicator) {
|
||||
this.customComponents.current.seekIndicator.show('forward', amount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek backward by specified amount
|
||||
* @param {number} amount - Seconds to seek backward
|
||||
*/
|
||||
seekBackward(amount) {
|
||||
if (!this.playerRef.current) return;
|
||||
|
||||
const currentTime = this.playerRef.current.currentTime();
|
||||
const newTime = Math.max(currentTime - amount, 0);
|
||||
|
||||
this.playerRef.current.currentTime(newTime);
|
||||
|
||||
// Show seek indicator if available
|
||||
if (this.customComponents?.current?.seekIndicator) {
|
||||
this.customComponents.current.seekIndicator.show('backward', amount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main keyboard event handler
|
||||
* @param {KeyboardEvent} event - The keyboard event
|
||||
*/
|
||||
handleKeyboardEvent = (event) => {
|
||||
// Only handle if no input elements are focused
|
||||
if (this.isInputFocused()) {
|
||||
return; // Don't interfere with input fields
|
||||
}
|
||||
|
||||
// Handle space key for play/pause
|
||||
if (this.handleSpaceKey(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle arrow keys for seeking
|
||||
if (this.handleArrowKeys(event)) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize keyboard event handling
|
||||
*/
|
||||
init() {
|
||||
if (this.isActive) {
|
||||
console.warn('KeyboardHandler is already active');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add keyboard event listener to the document
|
||||
document.addEventListener('keydown', this.handleKeyboardEvent);
|
||||
this.isActive = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up keyboard event handling
|
||||
*/
|
||||
destroy() {
|
||||
if (!this.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.removeEventListener('keydown', this.handleKeyboardEvent);
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update options
|
||||
* @param {Object} newOptions - New options to merge
|
||||
*/
|
||||
updateOptions(newOptions) {
|
||||
this.options = { ...this.options, ...newOptions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update player reference
|
||||
* @param {Object} newPlayerRef - New player reference
|
||||
*/
|
||||
updatePlayerRef(newPlayerRef) {
|
||||
this.playerRef = newPlayerRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update custom components reference
|
||||
* @param {Object} newCustomComponents - New custom components reference
|
||||
*/
|
||||
updateCustomComponents(newCustomComponents) {
|
||||
this.customComponents = newCustomComponents;
|
||||
}
|
||||
}
|
||||
|
||||
export default KeyboardHandler;
|
||||
@@ -0,0 +1,65 @@
|
||||
export class OrientationHandler {
|
||||
constructor(player, isTouchDevice) {
|
||||
this.player = player;
|
||||
this.isTouchDevice = isTouchDevice;
|
||||
this.orientationChangeHandler = null;
|
||||
this.screenOrientationHandler = null;
|
||||
}
|
||||
|
||||
setupOrientationHandling() {
|
||||
// Only apply to mobile/touch devices
|
||||
if (!this.isTouchDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Modern approach using Screen Orientation API
|
||||
if (screen.orientation) {
|
||||
this.screenOrientationHandler = () => {
|
||||
const type = screen.orientation.type;
|
||||
|
||||
if (type.includes('landscape')) {
|
||||
// Device rotated to landscape - enter fullscreen
|
||||
if (!this.player.isFullscreen()) {
|
||||
this.player.requestFullscreen();
|
||||
}
|
||||
} else if (type.includes('portrait')) {
|
||||
// Device rotated to portrait - exit fullscreen
|
||||
if (this.player.isFullscreen()) {
|
||||
this.player.exitFullscreen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
screen.orientation.addEventListener('change', this.screenOrientationHandler);
|
||||
}
|
||||
// Fallback for older iOS devices
|
||||
else {
|
||||
this.orientationChangeHandler = () => {
|
||||
// window.orientation: 0 = portrait, 90/-90 = landscape
|
||||
const isLandscape = Math.abs(window.orientation) === 90;
|
||||
|
||||
// Small delay to ensure orientation change is complete
|
||||
setTimeout(() => {
|
||||
if (isLandscape && !this.player.isFullscreen()) {
|
||||
this.player.requestFullscreen();
|
||||
} else if (!isLandscape && this.player.isFullscreen()) {
|
||||
this.player.exitFullscreen();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
window.addEventListener('orientationchange', this.orientationChangeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
// Remove event listeners
|
||||
if (this.screenOrientationHandler && screen.orientation) {
|
||||
screen.orientation.removeEventListener('change', this.screenOrientationHandler);
|
||||
}
|
||||
|
||||
if (this.orientationChangeHandler) {
|
||||
window.removeEventListener('orientationchange', this.orientationChangeHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* PlaybackEventHandler - Utility for handling video player playback events
|
||||
*
|
||||
* Provides comprehensive playback event handling for video players including:
|
||||
* - Play event handling with seek indicators and embed player visibility
|
||||
* - Pause event handling with poster management
|
||||
* - Quality change detection to prevent unnecessary indicators
|
||||
*/
|
||||
|
||||
class PlaybackEventHandler {
|
||||
constructor(playerRef, customComponents = null, options = {}) {
|
||||
this.playerRef = playerRef;
|
||||
this.customComponents = customComponents;
|
||||
this.options = {
|
||||
isEmbedPlayer: false,
|
||||
showSeekIndicators: true,
|
||||
...options,
|
||||
};
|
||||
this.eventHandlers = {};
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle play event
|
||||
* Shows play indicator and manages embed player visibility
|
||||
*/
|
||||
handlePlayEvent = () => {
|
||||
const player = this.playerRef.current;
|
||||
if (!player) return;
|
||||
|
||||
// Only show play indicator if not changing quality and indicators are enabled
|
||||
if (
|
||||
!player.isChangingQuality &&
|
||||
this.options.showSeekIndicators &&
|
||||
this.customComponents?.current?.seekIndicator
|
||||
) {
|
||||
this.customComponents.current.seekIndicator.show('play');
|
||||
}
|
||||
|
||||
// For embed players, ensure video becomes visible when playing
|
||||
if (this.options.isEmbedPlayer) {
|
||||
this.handleEmbedPlayerVisibility('play');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle pause event
|
||||
* Shows pause indicator and manages embed player poster visibility
|
||||
*/
|
||||
handlePauseEvent = () => {
|
||||
const player = this.playerRef.current;
|
||||
if (!player) return;
|
||||
|
||||
// Only show pause indicator if not changing quality and indicators are enabled
|
||||
if (
|
||||
!player.isChangingQuality &&
|
||||
this.options.showSeekIndicators &&
|
||||
this.customComponents?.current?.seekIndicator
|
||||
) {
|
||||
this.customComponents.current.seekIndicator.show('pause');
|
||||
}
|
||||
|
||||
// For embed players, show poster when paused at beginning
|
||||
if (this.options.isEmbedPlayer && player.currentTime() === 0) {
|
||||
this.handleEmbedPlayerVisibility('pause');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle embed player visibility for play/pause states
|
||||
* @param {string} action - 'play' or 'pause'
|
||||
*/
|
||||
handleEmbedPlayerVisibility(action) {
|
||||
const player = this.playerRef.current;
|
||||
if (!player) return;
|
||||
|
||||
const playerEl = player.el();
|
||||
const videoEl = playerEl.querySelector('video');
|
||||
const posterEl = playerEl.querySelector('.vjs-poster');
|
||||
const bigPlayButton = player.getChild('bigPlayButton');
|
||||
|
||||
if (action === 'play') {
|
||||
// Make video visible and hide poster
|
||||
if (videoEl) {
|
||||
videoEl.style.opacity = '1';
|
||||
}
|
||||
if (posterEl) {
|
||||
posterEl.style.opacity = '0';
|
||||
}
|
||||
// Hide big play button when video starts playing
|
||||
if (bigPlayButton) {
|
||||
bigPlayButton.hide();
|
||||
}
|
||||
} else if (action === 'pause') {
|
||||
// Hide video and show poster
|
||||
if (videoEl) {
|
||||
videoEl.style.opacity = '0';
|
||||
}
|
||||
if (posterEl) {
|
||||
posterEl.style.opacity = '1';
|
||||
}
|
||||
// Show big play button when paused at beginning
|
||||
if (bigPlayButton) {
|
||||
bigPlayButton.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize playback event handling
|
||||
*/
|
||||
init() {
|
||||
if (this.isActive) {
|
||||
console.warn('PlaybackEventHandler is already active');
|
||||
return;
|
||||
}
|
||||
|
||||
const player = this.playerRef.current;
|
||||
if (!player) {
|
||||
console.error('Player reference is not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add event listeners
|
||||
player.on('play', this.handlePlayEvent);
|
||||
player.on('pause', this.handlePauseEvent);
|
||||
|
||||
// Store event handlers for cleanup
|
||||
this.eventHandlers = {
|
||||
play: this.handlePlayEvent,
|
||||
pause: this.handlePauseEvent,
|
||||
};
|
||||
|
||||
this.isActive = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up playback event handling
|
||||
*/
|
||||
destroy() {
|
||||
if (!this.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const player = this.playerRef.current;
|
||||
if (player && this.eventHandlers) {
|
||||
// Remove event listeners
|
||||
Object.entries(this.eventHandlers).forEach(([event, handler]) => {
|
||||
player.off(event, handler);
|
||||
});
|
||||
}
|
||||
|
||||
this.eventHandlers = {};
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update options
|
||||
* @param {Object} newOptions - New options to merge
|
||||
*/
|
||||
updateOptions(newOptions) {
|
||||
this.options = { ...this.options, ...newOptions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update player reference
|
||||
* @param {Object} newPlayerRef - New player reference
|
||||
*/
|
||||
updatePlayerRef(newPlayerRef) {
|
||||
this.playerRef = newPlayerRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update custom components reference
|
||||
* @param {Object} newCustomComponents - New custom components reference
|
||||
*/
|
||||
updateCustomComponents(newCustomComponents) {
|
||||
this.customComponents = newCustomComponents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable seek indicators
|
||||
* @param {boolean} enabled - Whether to show seek indicators
|
||||
*/
|
||||
setSeekIndicatorsEnabled(enabled) {
|
||||
this.options.showSeekIndicators = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set embed player mode
|
||||
* @param {boolean} isEmbed - Whether this is an embed player
|
||||
*/
|
||||
setEmbedPlayerMode(isEmbed) {
|
||||
this.options.isEmbedPlayer = isEmbed;
|
||||
}
|
||||
}
|
||||
|
||||
export default PlaybackEventHandler;
|
||||
File diff suppressed because one or more lines are too long
+179
-173
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user