fix: Video.js improve browser/device compatibility, fixes and a lot improvements

This commit is contained in:
Yiannis Christodoulou
2025-09-15 14:41:28 +03:00
parent b14e1f609d
commit 543085c38f
21 changed files with 2866 additions and 1068 deletions
@@ -10,7 +10,7 @@
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1000;
z-index: 100000;
padding: 20px;
box-sizing: border-box;
backdrop-filter: blur(4px);
@@ -23,62 +23,104 @@
}
.autoplay-countdown-content {
background: rgba(0, 0, 0, 0.9);
border-radius: 12px;
padding: 24px;
max-width: 400px;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.95), rgba(20, 20, 20, 0.9));
border-radius: 20px;
padding: 50px;
max-width: 480px;
width: 100%;
text-align: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.4),
0 8px 32px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.15);
backdrop-filter: blur(20px);
position: relative;
overflow: hidden;
}
.autoplay-countdown-content::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
}
.autoplay-countdown-content::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 0, 0, 0.05) 0%, transparent 70%);
animation: backgroundPulse 4s ease-in-out infinite;
pointer-events: none;
}
@keyframes backgroundPulse {
0%, 100% {
opacity: 0.3;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.1);
}
}
.autoplay-countdown-header {
margin-bottom: 20px;
position: relative;
z-index: 2;
}
.autoplay-countdown-header h3 {
color: #fff;
font-size: 18px;
font-weight: 500;
margin: 0;
line-height: 1.4;
font-size: 26px;
font-weight: 400;
margin: 0 0 10px 0;
line-height: 1.3;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.autoplay-countdown-header h3 span{
font-weight:700;
}
.countdown-timer {
color: #ff4444;
font-weight: 700;
font-size: 20px;
animation: countdownPulse 1s infinite;
}
@keyframes countdownPulse {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
.autoplay-countdown-video-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
margin-bottom: 24px;
text-align: left;
gap: 20px;
text-align: center;
position: relative;
z-index: 2;
margin:0 0 50px;
}
.next-video-thumbnail {
flex-shrink: 0;
width: 80px;
height: 45px;
border-radius: 6px;
width: 180px;
height: 101px;
border-radius: 12px;
overflow: hidden;
background: #333;
position: relative;
box-shadow:
0 12px 32px rgba(0, 0, 0, 0.4),
0 4px 16px rgba(0, 0, 0, 0.2);
border: 2px solid rgba(255, 255, 255, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.next-video-thumbnail:hover {
transform: translateY(-4px) scale(1.02);
box-shadow:
0 16px 40px rgba(0, 0, 0, 0.5),
0 8px 24px rgba(0, 0, 0, 0.3);
}
.next-video-thumbnail img {
@@ -88,102 +130,250 @@
display: block;
}
.play-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
border-radius: 50%;
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
color: white;
transition: all 0.3s ease;
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.5),
0 4px 16px rgba(0, 0, 0, 0.3);
border: 3px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(8px);
}
.play-overlay:hover {
background: rgba(0, 0, 0, 0.9);
transform: translate(-50%, -50%) scale(1.1);
box-shadow:
0 12px 32px rgba(0, 0, 0, 0.6),
0 6px 20px rgba(0, 0, 0, 0.4);
}
.play-overlay svg {
margin-left: 4px;
width: 28px;
height: 28px;
}
.next-video-details {
flex-grow: 1;
min-width: 0; /* Allow text truncation */
min-width: 0;
text-align: center;
}
.next-video-title {
color: #fff;
font-size: 14px;
font-weight: 600;
margin: 0 0 4px 0;
line-height: 1.3;
color: #999;
font-size:18px;
font-weight:500;
margin: 0 0;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
letter-spacing: 0.5px;
}
.next-video-author {
color: #aaa;
font-size: 12px;
margin: 0 0 2px 0;
line-height: 1.2;
color: #bbb;
font-size: 16px;
margin: 0 0 8px 0;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.next-video-duration {
color: #ccc;
font-size: 11px;
color: #999;
font-size: 14px;
margin: 0;
line-height: 1.2;
font-weight: 500;
}
.autoplay-countdown-actions {
display: flex;
gap: 12px;
gap: 24px;
justify-content: center;
align-items: center;
position: relative;
z-index: 2;
padding: 0;
margin-top: 8px;
}
.autoplay-play-button,
.autoplay-cancel-button {
button.autoplay-play-button,
button.autoplay-cancel-button {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px 18px;
gap: 8px;
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
min-width: 100px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
min-width: 140px;
height: 48px;
position: relative;
overflow: hidden;
text-transform: uppercase;
letter-spacing: 0.3px;
line-height: 1;
white-space: nowrap;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1);
text-align: center;
}
.autoplay-play-button {
background: #ff0000;
button.autoplay-play-button {
background: linear-gradient(135deg, #ff0000, #e60000);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.autoplay-play-button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s ease;
}
.autoplay-play-button:hover {
background: #e60000;
transform: translateY(-1px);
background: linear-gradient(135deg, #ff1a1a, #cc0000);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(255, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.autoplay-play-button:hover::before {
left: 100%;
}
.autoplay-play-button:active {
transform: translateY(0);
transform: translateY(-1px);
box-shadow:
0 4px 15px rgba(255, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.autoplay-cancel-button {
background: rgba(255, 255, 255, 0.1);
background: linear-gradient(135deg, #404040, #2a2a2a);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 6px 20px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.autoplay-cancel-button:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.3);
background: linear-gradient(135deg, #505050, #3a3a3a);
transform: translateY(-2px);
box-shadow:
0 8px 25px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.autoplay-cancel-button:active {
background: rgba(255, 255, 255, 0.1);
transform: translateY(-1px);
box-shadow:
0 4px 15px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.autoplay-play-button svg,
.autoplay-cancel-button svg {
width: 1.2em;
height: 1.2em;
width: 18px;
height: 18px;
flex-shrink: 0;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
transition: transform 0.3s ease;
position: relative;
z-index: 1;
display: block;
margin: 0;
padding: 0;
vertical-align: middle;
}
.autoplay-play-button:hover svg {
transform: scale(1.05);
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4));
}
.autoplay-cancel-button svg {
width: 16px;
height: 16px;
}
.autoplay-cancel-button:hover svg {
transform: scale(1.05) rotate(90deg);
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4));
}
.autoplay-play-button span,
.autoplay-cancel-button span {
display: inline-block;
vertical-align: middle;
line-height: 1;
font-size: inherit;
font-weight: inherit;
letter-spacing: inherit;
text-transform: inherit;
position: relative;
z-index: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
margin: 0;
padding: 0;
height: auto;
align-self: center;
}
/* Ensure perfect alignment of button content */
.autoplay-play-button > *,
.autoplay-cancel-button > * {
vertical-align: middle;
display: inline-block;
}
/* Fix any baseline alignment issues */
.autoplay-play-button,
.autoplay-cancel-button {
align-items: center;
justify-content: center;
}
/* Ensure text and icons are perfectly aligned */
.autoplay-play-button svg,
.autoplay-cancel-button svg {
vertical-align: middle;
display: inline-block;
}
/* Autoplay Toggle Button Styles */
.vjs-autoplay-toggle {
/* .vjs-autoplay-toggle {
width: 3em !important;
height: 3em !important;
flex: none;
@@ -191,16 +381,16 @@
margin: 0 4px !important;
line-height: 3em !important;
position: relative;
}
} */
.vjs-autoplay-toggle .vjs-autoplay-icon {
/* .vjs-autoplay-toggle .vjs-autoplay-icon {
width: 1.2em;
height: 1.2em;
display: flex;
align-items: center;
justify-content: center;
margin: auto;
}
} */
.vjs-autoplay-toggle .vjs-autoplay-icon svg {
width: 100%;
@@ -209,40 +399,135 @@
}
/* Responsive design */
@media (max-width: 480px) {
@media (max-width: 767px) {
.autoplay-countdown-video-info {
margin-bottom:20px;
}
.autoplay-countdown-content {
padding: 20px;
max-width: 320px;
padding: 24px;
max-width: 400px;
}
.autoplay-countdown-header h3 {
font-size: 16px;
}
.countdown-timer {
font-size: 18px;
}
.autoplay-countdown-video-info {
gap: 10px;
font-size: 20px;
}
.next-video-thumbnail {
width: 60px;
height: 34px;
width: 140px;
height: 78px;
}
.play-overlay {
width: 48px;
height: 48px;
}
.play-overlay svg {
width: 20px;
height: 20px;
}
.next-video-title {
font-size: 13px;
font-size: 18px;
}
.autoplay-countdown-actions {
flex-direction: column;
gap: 8px;
.next-video-author {
font-size: 14px;
}
.autoplay-play-button,
.autoplay-cancel-button {
width: 100%;
padding: 12px 24px;
font-size: 14px;
min-width: 120px;
height: 44px;
gap: 6px;
align-items: center;
justify-content: center;
}
.autoplay-play-button svg {
width: 16px;
height: 16px;
vertical-align: middle;
}
.autoplay-cancel-button svg {
width: 14px;
height: 14px;
vertical-align: middle;
}
}
@media (max-width: 480px) {
.autoplay-countdown-content {
padding: 20px;
max-width: 350px;
}
.autoplay-countdown-header h3 {
font-size: 18px;
}
.countdown-timer {
font-size: 24px;
padding: 10px 16px;
}
.autoplay-countdown-video-info {
gap: 16px;
}
.next-video-thumbnail {
width: 120px;
height: 68px;
}
.play-overlay {
width: 40px;
height: 40px;
}
.play-overlay svg {
width: 16px;
height: 16px;
}
.next-video-title {
font-size: 16px;
}
.next-video-author {
font-size: 13px;
}
.autoplay-countdown-actions {
gap: 5px;
padding: 0;
}
button.autoplay-play-button, button.autoplay-cancel-button { padding:10px 20px; width:120px; height:40px; min-width:120px; }
.autoplay-play-button,
.autoplay-cancel-button {
width: 100%;
min-width: 100%;
height: 46px;
gap: 6px;
padding: 10px 20px;
font-size: 13px;
align-items: center;
justify-content: center;
}
.autoplay-play-button svg {
width: 14px;
height: 14px;
vertical-align: middle;
}
.autoplay-cancel-button svg {
width: 12px;
height: 12px;
vertical-align: middle;
}
}
@@ -45,6 +45,11 @@ class AutoplayCountdownOverlay extends Component {
nextVideoThumbnail
? `<div class="next-video-thumbnail">
<img src="${nextVideoThumbnail}" alt="${nextVideoTitle}" />
<div class="play-overlay">
<svg viewBox="0 0 24 24" width="28" height="28" fill="currentColor">
<path d="M8 5v14l11-7z"/>
</svg>
</div>
</div>`
: ''
}
@@ -199,6 +204,11 @@ class AutoplayCountdownOverlay extends Component {
nextVideoThumbnail
? `<div class="next-video-thumbnail">
<img src="${nextVideoThumbnail}" alt="${nextVideoTitle}" />
<div class="play-overlay">
<svg viewBox="0 0 24 24" width="28" height="28" fill="currentColor">
<path d="M8 5v14l11-7z"/>
</svg>
</div>
</div>`
: ''
}
@@ -26,10 +26,9 @@ class EndScreenOverlay extends Component {
// Get relatedVideos from options since createEl is called during super()
const relatedVideos = this.options_ && this.options_._relatedVideos ? this.options_._relatedVideos : [];
// Limit items based on screen size
const isMobile = window.innerWidth <= 768;
const maxItems = isMobile ? 6 : 12;
const videosToShow = relatedVideos.slice(0, maxItems);
// Limit videos based on screen size to fit grid properly
const maxVideos = this.getMaxVideosForScreen();
const videosToShow = relatedVideos.slice(0, maxVideos);
// console.log(
// 'Creating end screen with',
@@ -41,6 +40,12 @@ class EndScreenOverlay extends Component {
className: 'vjs-end-screen-overlay',
});
// Create title
const title = videojs.dom.createEl('div', {
className: 'vjs-related-videos-title',
});
title.textContent = 'More Videos';
// Create grid container
const grid = videojs.dom.createEl('div', {
className: 'vjs-related-videos-grid',
@@ -53,16 +58,15 @@ class EndScreenOverlay extends Component {
grid.appendChild(videoItem);
});
} else {
// Fallback message if no related videos
const noVideos = videojs.dom.createEl('div', {
className: 'vjs-no-related-videos',
// 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);
});
noVideos.textContent = 'No related videos available';
noVideos.style.color = 'white';
noVideos.style.textAlign = 'center';
grid.appendChild(noVideos);
}
overlay.appendChild(title);
overlay.appendChild(grid);
return overlay;
@@ -73,10 +77,18 @@ class EndScreenOverlay extends Component {
className: '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: 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', {
@@ -142,6 +154,166 @@ class EndScreenOverlay extends Component {
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 = [
'#FF6B6B', '#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() {
const width = window.innerWidth;
if (width >= 1200) {
return 12; // 4x3 grid for large desktop
} else if (width >= 1024) {
return 9; // 3x3 grid for desktop
} else if (width >= 768) {
return 6; // 3x2 grid for tablet
} else {
return 4; // 2x2 grid for mobile
}
}
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'
}
];
}
show() {
this.el().style.display = 'flex';
}