diff --git a/frontend-tools/video-js/src/components/overlays/EndScreenOverlay_OLD.css b/frontend-tools/video-js/src/components/overlays/EndScreenOverlay_OLD.css deleted file mode 100644 index 83de1471..00000000 --- a/frontend-tools/video-js/src/components/overlays/EndScreenOverlay_OLD.css +++ /dev/null @@ -1,689 +0,0 @@ -/* ===== 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%; - } -} diff --git a/frontend-tools/video-js/src/components/overlays/EndScreenOverlay_OLD.js b/frontend-tools/video-js/src/components/overlays/EndScreenOverlay_OLD.js deleted file mode 100644 index 27d7768f..00000000 --- a/frontend-tools/video-js/src/components/overlays/EndScreenOverlay_OLD.js +++ /dev/null @@ -1,377 +0,0 @@ -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; diff --git a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx b/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx index 578fb528..b011af40 100644 --- a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx +++ b/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx @@ -1,7 +1,6 @@ import React, { useEffect, useRef, useMemo } from 'react'; import videojs from 'video.js'; import 'video.js/dist/video-js.css'; -// import '../../VideoJS.css'; import '../../styles/embed.css'; import '../controls/SubtitlesButton.css'; import './VideoJSPlayer.css'; @@ -9,8 +8,6 @@ import './VideoJSPlayerRoundedCorners.css'; import '../controls/ButtonTooltips.css'; // Import the separated components -import EndScreenOverlay from '../overlays/EndScreenOverlay'; -import AutoplayCountdownOverlay from '../overlays/AutoplayCountdownOverlay'; import EmbedInfoOverlay from '../overlays/EmbedInfoOverlay'; import ChapterMarkers from '../markers/ChapterMarkers'; import SpritePreview from '../markers/SpritePreview'; @@ -87,9 +84,6 @@ const enableStandardButtonTooltips = (player) => { buttonEl.setAttribute('title', tooltipText); buttonEl.setAttribute('aria-label', tooltipText); - // Add touch tooltip support for mobile devices - //addTouchTooltipSupport(buttonEl); - // For dynamic tooltips (play/pause, fullscreen), update on state change if (buttonName === 'playToggle') { player.on('play', () => { @@ -175,56 +169,6 @@ const enableStandardButtonTooltips = (player) => { }, 500); // Delay to ensure all components are ready }; -// Helper function to add touch tooltip support -const 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', - () => { - const touchDuration = Date.now() - touchStartTime; - - // Only show tooltip for quick taps (not swipes) - if (touchDuration < 300) { - // Don't prevent default for most buttons to maintain functionality - - // 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: true } - ); -}; - function VideoJSPlayer({ videoId = 'default-video' }) { const videoRef = useRef(null); const playerRef = useRef(null); // Track the player instance @@ -2090,14 +2034,6 @@ function VideoJSPlayer({ videoId = 'default-video' }) { progressControl: { seekBar: { loadProgressBar: false }, // Hide the buffered/loaded progress indicator }, - /* progressControl: { - seekBar: { - timeTooltip: { - // Customize TimeTooltip behavior - displayNegative: false, // Don't show negative time - }, - }, - }, */ // Remaining time display configuration currentTimeDisplay: false, durationDisplay: false, @@ -2152,9 +2088,9 @@ function VideoJSPlayer({ videoId = 'default-video' }) { // Use native audio tracks instead of emulated - disabled for consistency nativeAudioTracks: false, - // Use Video.js text tracks for full positioning control on mobile - // Native tracks don't allow CSS positioning control - nativeTextTracks: isTouchDevice, + // Use Video.js text tracks for full positioning control on all devices + // Native tracks don't allow CSS positioning control and cause duplicates + nativeTextTracks: true, // Use native video tracks instead of emulated - disabled for consistency nativeVideoTracks: false, @@ -2273,185 +2209,9 @@ function VideoJSPlayer({ videoId = 'default-video' }) { } } - /* // Detect if user has interacted with the page - const hasUserInteracted = () => { - // Check various indicators of user interaction - return ( - document.hasFocus() || - document.visibilityState === 'visible' || - sessionStorage.getItem('userInteracted') === 'true' - ); - }; - - // Handle autoplay while respecting user's saved preferences - const handleAutoplay = async () => { - const userInteracted = hasUserInteracted(); - const savedMuteState = userPreferences.current.getPreference('muted'); - - try { - // Respect user's saved mute preference, but try unmuted if user interacted and hasn't explicitly muted - if (!mediaData.urlMuted && userInteracted && savedMuteState !== true) { - playerRef.current.muted(false); - } - - // First attempt: try to play with current mute state - await playerRef.current.play(); - } catch (error) { - // Fallback to muted autoplay unless user explicitly wants to stay unmuted - if (!playerRef.current.muted()) { - try { - playerRef.current.muted(true); - await playerRef.current.play(); - - // Only try to restore sound if user hasn't explicitly saved mute=true - if (savedMuteState !== true) { - // Aggressively try to restore sound - const restoreSound = () => { - if (playerRef.current && !playerRef.current.isDisposed()) { - playerRef.current.muted(false); - playerRef.current.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 (playerRef.current && !playerRef.current.isDisposed()) { - playerRef.current.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 }); - } - } - } catch (mutedError) { - console.error('❌ Even muted autoplay was blocked:', mutedError.message); - } - } - } - }; */ - // Skip autoplay for embed players to show poster if (!isEmbedPlayer) { - /* if (mediaData?.urlAutoplay) { - // Explicit autoplay requested via URL parameter - autoplayHandler.handleAutoplay(); - } else { - // Auto-start video on page load/reload with fallback strategy - autoplayHandler.handleAutoplay(); - } */ autoplayHandler.handleAutoplay(); - } else { - // For embed players, setup clean appearance with hidden controls - /* setTimeout(() => { - const bigPlayButton = playerRef.current.getChild('bigPlayButton'); - const controlBar = playerRef.current.getChild('controlBar'); - - if (bigPlayButton) { - bigPlayButton.show(); - // Ensure big play button is prominently displayed in center - const bigPlayEl = bigPlayButton.el(); - if (bigPlayEl) { - bigPlayEl.style.display = 'block'; - bigPlayEl.style.visibility = 'visible'; - bigPlayEl.style.opacity = '1'; - // Make it more prominent for embed - bigPlayEl.style.zIndex = '10'; - } - } - - if (controlBar) { - // Hide controls by default for embed players - controlBar.hide(); - const controlBarEl = controlBar.el(); - if (controlBarEl) { - controlBarEl.style.opacity = '0'; - controlBarEl.style.visibility = 'hidden'; - controlBarEl.style.transition = 'opacity 0.3s ease'; - } - } - - // Fix potential duplicate image issue by ensuring proper poster/video layering - const embedPlayerEl = playerRef.current.el(); - const videoEl = embedPlayerEl.querySelector('video'); - const posterEl = embedPlayerEl.querySelector('.vjs-poster'); - - if (videoEl && posterEl) { - // Ensure video is behind poster when paused - videoEl.style.opacity = '0'; - posterEl.style.zIndex = '1'; - posterEl.style.position = 'absolute'; - posterEl.style.top = '0'; - posterEl.style.left = '0'; - posterEl.style.width = '100%'; - posterEl.style.height = '100%'; - } - - // Set player to inactive state to hide controls initially - playerRef.current.userActive(false); - - // Setup hover behavior to show/hide controls for embed - if (embedPlayerEl) { - const showControls = () => { - if (controlBar) { - controlBar.show(); - const controlBarEl = controlBar.el(); - if (controlBarEl) { - controlBarEl.style.opacity = '1'; - controlBarEl.style.visibility = 'visible'; - } - } - playerRef.current.userActive(true); - }; - - const hideControls = () => { - // Only hide if video is paused (embed behavior) - if (playerRef.current.paused()) { - if (controlBar) { - const controlBarEl = controlBar.el(); - if (controlBarEl) { - controlBarEl.style.opacity = '0'; - controlBarEl.style.visibility = 'hidden'; - } - setTimeout(() => { - if (playerRef.current.paused()) { - controlBar.hide(); - } - }, 300); - } - playerRef.current.userActive(false); - } - }; - - embedPlayerEl.addEventListener('mouseenter', showControls); - embedPlayerEl.addEventListener('mouseleave', hideControls); - - // Store cleanup function - customComponents.current.embedControlsCleanup = () => { - embedPlayerEl.removeEventListener('mouseenter', showControls); - embedPlayerEl.removeEventListener('mouseleave', hideControls); - }; - } - }, 100); */ } const setupMobilePlayPause = () => { @@ -2640,10 +2400,6 @@ function VideoJSPlayer({ videoId = 'default-video' }) { const progressEl = progressControl.el(); const controlBarEl = controlBar.el(); controlBarEl.style.gap = 0; - // controlBarEl.style.background = 'none'; - // progressEl.style.background = 'none'; - // controlBarEl.style.background = 'yellow'; - // progressEl.style.background = 'red'; // Remove progress control from control bar controlBar.removeChild(progressControl); @@ -2740,48 +2496,6 @@ function VideoJSPlayer({ videoId = 'default-video' }) { // END: Move progress bar below control bar - // Debug: Check if progress control exists and is visible on touch devices - /* if (isTouchDevice) { - console.log('🔍 Touch device detected - Progress control debug:'); - console.log('- progressControl exists:', !!progressControl); - console.log('- progressControl element:', progressControl?.el()); - console.log('- progressControl visible:', progressControl?.el()?.style.display !== 'none'); - console.log('- seekBar exists:', !!seekBar); - console.log('- seekBar element:', seekBar?.el()); - - if (progressControl?.el()) { - const progressEl = progressControl.el(); - console.log('- progressControl computed styles:', { - display: window.getComputedStyle(progressEl).display, - visibility: window.getComputedStyle(progressEl).visibility, - opacity: window.getComputedStyle(progressEl).opacity, - position: window.getComputedStyle(progressEl).position, - bottom: window.getComputedStyle(progressEl).bottom, - }); - - // Force show progress control on touch devices - console.log('🔧 Forcing progress control to be visible on touch device...'); - progressEl.style.display = 'block'; - progressEl.style.visibility = 'visible'; - progressEl.style.opacity = '1'; - progressEl.style.position = 'absolute'; - progressEl.style.bottom = '42px'; - progressEl.style.left = '0'; - progressEl.style.right = '0'; - progressEl.style.width = '100%'; - progressEl.style.zIndex = '10'; - progressEl.style.pointerEvents = 'auto'; - - // Also ensure the progress control component is shown - if (progressControl.show) { - progressControl.show(); - } - } - } - */ - // Remove from current position - // controlBar.removeChild(fullscreenToggle); - // Auto-play video when navigating from next button (skip for embed players) if (!isEmbedPlayer) { const urlParams = new URLSearchParams(window.location.search); @@ -2860,25 +2574,6 @@ function VideoJSPlayer({ videoId = 'default-video' }) { }); } - // Force chapter markers update after chapters are loaded - /* setTimeout(() => { - if (chapterMarkers && chapterMarkers.updateChapterMarkers) { - chapterMarkers.updateChapterMarkers(); - } - }, 500); */ - // END: Chapters Implementation - - // 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'; */ - - // Insert wrapper before the play button and move play button inside - // playButtonEl.parentNode.insertBefore(playButtonWrapper, playButtonEl); - // playButtonWrapper.appendChild(playButtonEl); - // END: Wrap play button in custom div container - // 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 @@ -2887,20 +2582,6 @@ function VideoJSPlayer({ videoId = 'default-video' }) { }); const playToggleIndex = controlBar.children().indexOf(playToggle); // Insert it after play button 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'; */ - - // // 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 @@ -2953,49 +2634,13 @@ function VideoJSPlayer({ videoId = 'default-video' }) { } // END: Add spacer - // BEGIN: Wrap volume panel in custom div container - /* setTimeout(() => { - const volumePanel = controlBar.getChild('volumePanel'); - if (volumePanel) { - const volumePanelEl = volumePanel.el(); - if (volumePanelEl) { - const volumeWrapper = document.createElement('div'); - volumeWrapper.className = - 'vjs-volume-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button'; - - // Insert wrapper before the volume panel and move panel inside - volumePanelEl.parentNode.insertBefore(volumeWrapper, volumePanelEl); - volumeWrapper.appendChild(volumePanelEl); - } - } - }, 100); // Small delay to ensure volume panel is fully rendered */ - // END: Wrap volume panel in custom div container - // BEGIN: Implement autoplay toggle button - simplified if (!isEmbedPlayer) { try { - // 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 - */ - const autoplayToggleButton = new AutoplayToggleButton(playerRef.current, { userPreferences: userPreferences.current, isTouchDevice: isTouchDevice, }); - // Add it before the chapters button (or at a suitable position) - /* const chaptersButtonIndex = chaptersButton - ? controlBar.children().indexOf(chaptersButton) - : -1; - const insertIndex = - chaptersButtonIndex > 0 ? chaptersButtonIndex : controlBar.children().length - 3; */ controlBar.addChild(autoplayToggleButton, {}, 11); // Store reference for later use @@ -3005,20 +2650,6 @@ function VideoJSPlayer({ videoId = 'default-video' }) { setTimeout(() => { autoplayToggleButton.updateIcon(); }, 0); - - // Wrap autoplay toggle button in custom div container - /* setTimeout(() => { - const autoplayButtonEl = autoplayToggleButton.el(); - if (autoplayButtonEl) { - const autoplayWrapper = document.createElement('div'); - autoplayWrapper.className = - 'vjs-autoplay-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button'; - - // Insert wrapper before the autoplay button and move button inside - autoplayButtonEl.parentNode.insertBefore(autoplayWrapper, autoplayButtonEl); - // autoplayWrapper.appendChild(autoplayButtonEl); - } - }, 150); // Slightly longer delay to ensure button is fully rendered and updated */ } catch (error) { console.error('✗ Failed to add autoplay toggle button:', error); } @@ -3194,7 +2825,9 @@ function VideoJSPlayer({ videoId = 'default-video' }) { false, true ); - } catch (e) {} + } catch (e) { + console.error('✗ Failed to set subtitleEnabled to false:', e); + } } else { // Show using previously chosen language only; do not change it const preferred = @@ -3221,14 +2854,24 @@ function VideoJSPlayer({ videoId = 'default-video' }) { first, true ); - } catch (e) {} + } catch (e) { + console.error( + '✗ Failed to set subtitleLanguage to first:', + e + ); + } try { userPreferences.current.setPreference( 'subtitleEnabled', true, true ); - } catch (e) {} + } catch (e) { + console.error( + '✗ Failed to set subtitleEnabled to true:', + e + ); + } el.classList.add('vjs-subs-active'); } return; @@ -3250,7 +2893,9 @@ function VideoJSPlayer({ videoId = 'default-video' }) { true, true ); - } catch (e) {} + } catch (e) { + console.error('✗ Failed to set subtitleEnabled to true:', e); + } } } }; @@ -3334,26 +2979,6 @@ function VideoJSPlayer({ videoId = 'default-video' }) { } // END: Add chapter markers and sprite preview to progress control - // BEGIN: Simple button layout fix - use CSS approach - /* setTimeout(() => { - // Add a simple spacer div using DOM manipulation (simpler approach) - const spacerDiv = document.createElement('div'); - spacerDiv.className = 'vjs-spacer-control vjs-control'; - spacerDiv.style.flex = '1'; - spacerDiv.style.minWidth = '1px'; - spacerDiv.style.height = '100%'; - - // Find insertion point after duration display - const durationDisplay = controlBar.getChild('durationDisplay'); - if (durationDisplay && durationDisplay.el()) { - const controlBarEl = controlBar.el(); - const durationEl = durationDisplay.el(); - const nextSibling = durationEl.nextSibling; - controlBarEl.insertBefore(spacerDiv, nextSibling); - } - }, 300); */ - // END: Simple button layout fix - // BEGIN: Move Picture-in-Picture and Fullscreen buttons to the very end setTimeout(() => { try { @@ -3424,161 +3049,6 @@ function VideoJSPlayer({ videoId = 'default-video' }) { // Ignore errors when setting up settings menu quality updates } - // Wrap settings button in custom div container - /* setTimeout(() => { - const controlBar = playerRef.current.getChild('controlBar'); - if ( - controlBar && - customComponents.current.settingsMenu && - customComponents.current.settingsMenu.settingsButton - ) { - const settingsButtonEl = customComponents.current.settingsMenu.settingsButton.el(); - if (settingsButtonEl) { - const settingsWrapper = document.createElement('div'); - settingsWrapper.className = - 'vjs-settings-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button'; - - // Insert wrapper before the settings button and move button inside - settingsButtonEl.parentNode.insertBefore(settingsWrapper, settingsButtonEl); - settingsWrapper.appendChild(settingsButtonEl); - } - } - }, 200); // Longer delay to ensure settings button is fully created */ - - // Wrap picture-in-picture button in custom div container - /* setTimeout(() => { - const controlBar = playerRef.current.getChild('controlBar'); - const pipControl = controlBar?.getChild('pictureInPictureToggle'); - if (pipControl) { - const pipButtonEl = pipControl.el(); - if (pipButtonEl) { - const pipWrapper = document.createElement('div'); - pipWrapper.className = - 'vjs-pip-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button'; - - // Insert wrapper before the pip button and move button inside - pipButtonEl.parentNode.insertBefore(pipWrapper, pipButtonEl); - pipWrapper.appendChild(pipButtonEl); - } - } - }, 100); // Small delay to ensure pip button is available */ - - // Wrap fullscreen button in custom div container - /* setTimeout(() => { - const controlBar = playerRef.current.getChild('controlBar'); - const fullscreenControl = controlBar?.getChild('fullscreenToggle'); - if (fullscreenControl) { - const fullscreenButtonEl = fullscreenControl.el(); - if (fullscreenButtonEl) { - const fullscreenWrapper = document.createElement('div'); - fullscreenWrapper.className = - 'vjs-fullscreen-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button'; - - // Insert wrapper before the fullscreen button and move button inside - fullscreenButtonEl.parentNode.insertBefore(fullscreenWrapper, fullscreenButtonEl); - fullscreenWrapper.appendChild(fullscreenButtonEl); - } - } - }, 100); // Small delay to ensure fullscreen button is available */ - - // END: Add Settings Menu Component - - // Store components reference for potential cleanup - - // BEGIN: Fix Android seekbar touch functionality - /* if (isTouchDevice) { - setTimeout(() => { - const progressControl = playerRef.current - .getChild('controlBar') - ?.getChild('progressControl'); - const seekBar = progressControl?.getChild('seekBar'); - - if (seekBar && seekBar.el()) { - const seekBarEl = seekBar.el(); - const progressHolder = seekBarEl.querySelector('.vjs-progress-holder'); - - if (progressHolder) { - let isDragging = false; - - const handleTouchStart = (e) => { - isDragging = true; - e.preventDefault(); - e.stopPropagation(); // Prevent event from reaching video element - playerRef.current.userActive(true); - - // Mark that we're interacting with seekbar to prevent play/pause - playerRef.current.seekbarTouching = true; - - // Temporarily disable big play button - const bigPlayButton = playerRef.current.getChild('bigPlayButton'); - if (bigPlayButton && bigPlayButton.el()) { - bigPlayButton.el().style.pointerEvents = 'none'; - bigPlayButton.el().style.touchAction = 'none'; - } - }; - - const handleTouchMove = (e) => { - if (!isDragging) return; - e.preventDefault(); - e.stopPropagation(); // Prevent event from reaching video element - - const touch = e.touches[0]; - const rect = progressHolder.getBoundingClientRect(); - const percentage = Math.max( - 0, - Math.min(1, (touch.clientX - rect.left) / rect.width) - ); - const duration = playerRef.current.duration(); - - if (duration && !isNaN(duration)) { - const newTime = percentage * duration; - playerRef.current.currentTime(newTime); - } - }; - - const handleTouchEnd = () => { - isDragging = false; - // e.preventDefault(); - // e.stopPropagation(); // Prevent event from reaching video element - - // Re-enable big play button - const bigPlayButton = playerRef.current.getChild('bigPlayButton'); - if (bigPlayButton && bigPlayButton.el()) { - setTimeout(() => { - bigPlayButton.el().style.pointerEvents = ''; - bigPlayButton.el().style.touchAction = ''; - }, 200); - } - - // Clear the seekbar touching flag after a longer delay to prevent conflicts - setTimeout(() => { - if (playerRef.current) { - playerRef.current.seekbarTouching = false; - } - }, 300); - }; - - // Add touch event listeners specifically for Android - progressHolder.addEventListener('touchstart', handleTouchStart, { - passive: false, - }); - progressHolder.addEventListener('touchmove', handleTouchMove, { - passive: false, - }); - progressHolder.addEventListener('touchend', handleTouchEnd, { passive: false }); - - // Store cleanup function - customComponents.current.cleanupSeekbarTouch = () => { - progressHolder.removeEventListener('touchstart', handleTouchStart); - progressHolder.removeEventListener('touchmove', handleTouchMove); - progressHolder.removeEventListener('touchend', handleTouchEnd); - }; - } - } - }, 500); - } */ - // END: Fix Android seekbar touch functionality - // BEGIN: Initialize keyboard handler keyboardHandler.current = new KeyboardHandler( playerRef, @@ -3629,202 +3099,6 @@ function VideoJSPlayer({ videoId = 'default-video' }) { }; // END: Initialize playback event handler - // Store reference to end screen and autoplay countdown for cleanup - /* let endScreen = null; - let autoplayCountdown = null; - - - - playerRef.current.on('ended', () => { - // For embed players, show big play button when video ends - if (isEmbedPlayer) { - const bigPlayButton = playerRef.current.getChild('bigPlayButton'); - if (bigPlayButton) { - bigPlayButton.show(); - } - } - - // Keep controls active after video ends - setTimeout(() => { - if (playerRef.current && !playerRef.current.isDisposed()) { - // Remove vjs-ended class if it disables controls - const playerEl = playerRef.current.el(); - if (playerEl) { - // Keep the visual ended state but ensure controls work - const controlBar = playerRef.current.getChild('controlBar'); - if (controlBar) { - controlBar.show(); - controlBar.el().style.opacity = '1'; - controlBar.el().style.pointerEvents = 'auto'; - } - } - } - }, 50); - - // Check if autoplay is enabled and there's a next video - const isAutoplayEnabled = userPreferences.current.getAutoplayPreference(); - const hasNextVideo = mediaData.nextLink !== null; - - if (!isEmbedPlayer && isAutoplayEnabled && hasNextVideo) { - // If it's a playlist, skip countdown and play directly - if (currentVideo.isPlayList) { - // Clean up any existing overlays - if (endScreen) { - playerRef.current.removeChild(endScreen); - endScreen = null; - } - if (autoplayCountdown) { - playerRef.current.removeChild(autoplayCountdown); - autoplayCountdown = null; - } - - // Play next video directly without countdown - goToNextVideo(); - } else { - // 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 - if (endScreen) { - playerRef.current.removeChild(endScreen); - endScreen = null; - } - if (autoplayCountdown) { - playerRef.current.removeChild(autoplayCountdown); - autoplayCountdown = null; - } - - // Show autoplay countdown immediately! - autoplayCountdown = new AutoplayCountdownOverlay(playerRef.current, { - nextVideoData: nextVideoData, - countdownSeconds: 5, - onPlayNext: () => { - goToNextVideo(); - }, - onCancel: () => { - // Hide countdown and show end screen instead - if (autoplayCountdown) { - playerRef.current.removeChild(autoplayCountdown); - autoplayCountdown = null; - } - showEndScreen(); - }, - }); - - playerRef.current.addChild(autoplayCountdown); - // Start countdown immediately without any delay - setTimeout(() => { - if (autoplayCountdown && !autoplayCountdown.isDisposed()) { - autoplayCountdown.startCountdown(); - } - }, 0); - } - } else { - // Autoplay disabled or no next video - show regular end screen - showEndScreen(); - } - - // Function to show the regular end screen - function showEndScreen() { - // Prevent creating multiple end screens - if (endScreen) { - playerRef.current.removeChild(endScreen); - endScreen = null; - } - - // Show end screen with related videos - endScreen = new EndScreenOverlay(playerRef.current, { - relatedVideos: relatedVideos, - }); - - // Also store the data directly on the component as backup - endScreen.relatedVideos = relatedVideos; - - playerRef.current.addChild(endScreen); - endScreen.show(); - } - }); */ - - // Hide end screen and autoplay countdown when user wants to replay - /* playerRef.current.on('play', () => { - console.log('play'); - if (endScreen) { - endScreen.hide(); - } - if (autoplayCountdown) { - autoplayCountdown.stopCountdown(); - } - }); */ - - /* const hideEndScreenAndStopCountdown = () => { - if (endScreen) { - endScreen.hide(); - } - if (autoplayCountdown) { - autoplayCountdown.stopCountdown(); - } - }; - - playerRef.current.on('play', hideEndScreenAndStopCountdown); - playerRef.current.on('seeking', hideEndScreenAndStopCountdown); */ - - // Hide end screen and autoplay countdown when user seeks - /* playerRef.current.on('seeking', () => { // TODO: after the end screen is implemented stop countdown - if (endScreen) { - endScreen.hide(); - } - if (autoplayCountdown) { - autoplayCountdown.stopCountdown(); - } - }); */ - - // Handle replay button functionality - /* playerRef.current.on('replay', () => { - alert('replay'); - console.log('replay'); - if (endScreen) { - endScreen.hide(); - } - playerRef.current.currentTime(0); - playerRef.current.play(); - }); */ - - // playerRef.current.on('error', (error) => { - // // console.error('Video.js error:', error); - // }); - - // playerRef.current.on('fullscreenchange', () => { - // // console.log('Fullscreen changed:', playerRef.current.isFullscreen()); - // }); - - // playerRef.current.on('volumechange', () => { - // console.log('Volume changed:', playerRef.current.volume(), 'Muted:', playerRef.current.muted()); - // }); - - // playerRef.current.on('ratechange', () => { - // // console.log('Playback rate changed:', playerRef.current.playbackRate()); - // }); - - // playerRef.current.on('texttrackchange', () => { - // // console.log('Text track changed'); - // }); - // Focus the player element so keyboard controls work // This ensures keyboard events work properly in both normal and fullscreen modes playerRef.current.ready(() => { @@ -3836,109 +3110,11 @@ function VideoJSPlayer({ videoId = 'default-video' }) { videoElement.focus(); } }); - - // Handle focus when entering/exiting fullscreen to ensure keyboard events work - /* playerRef.current.on('fullscreenchange', () => { - setTimeout(() => { - if (playerRef.current && playerRef.current.el()) { - const videoElement = playerRef.current.el(); - videoElement.setAttribute('tabindex', '0'); - videoElement.focus(); - - // In fullscreen mode, ensure the player container has focus - if (playerRef.current.isFullscreen()) { - // Focus the fullscreen element to ensure keyboard events are captured - const fullscreenElement = - document.fullscreenElement || - document.webkitFullscreenElement || - document.mozFullScreenElement || - document.msFullscreenElement; - if (fullscreenElement) { - fullscreenElement.setAttribute('tabindex', '0'); - fullscreenElement.focus(); - } - } - } - }, 100); // Small delay to ensure fullscreen transition is complete - }); */ } //}, 0); - - /* return () => { - clearTimeout(timer); - }; */ } - - // Cleanup function - /* return () => { - // Clean up keyboard event listeners if they exist - if (customComponents.current && customComponents.current.cleanupKeyboardHandler) { - customComponents.current.cleanupKeyboardHandler(); - } - - // Clean up playback event handlers if they exist - if (customComponents.current && customComponents.current.cleanupPlaybackEventHandler) { - customComponents.current.cleanupPlaybackEventHandler(); - } - - // Clean up seekbar touch handlers if they exist - if (customComponents.current && customComponents.current.cleanupSeekbarTouch) { - customComponents.current.cleanupSeekbarTouch(); - } - - // Clean up embed controls event listeners if they exist - if (customComponents.current && customComponents.current.embedControlsCleanup) { - customComponents.current.embedControlsCleanup(); - } - - // Clean up force hide interval if it exists - if (customComponents.current && customComponents.current.forceHideInterval) { - clearInterval(customComponents.current.forceHideInterval); - } - - if (playerRef.current && !playerRef.current.isDisposed()) { - playerRef.current.dispose(); - playerRef.current = null; - } - }; */ }, []); - // Additional effect to ensure video gets focus for keyboard controls on page load - /* useEffect(() => { - const focusVideo = () => { - if (playerRef.current && playerRef.current.el()) { - const videoElement = playerRef.current.el(); - videoElement.setAttribute('tabindex', '0'); - videoElement.focus(); - } - }; - - // Focus when the page becomes visible or gains focus - const handleVisibilityChange = () => { - if (!document.hidden) { - setTimeout(focusVideo, 100); - } - }; - - const handleWindowFocus = () => { - setTimeout(focusVideo, 100); - }; - - // Add event listeners - document.addEventListener('visibilitychange', handleVisibilityChange); - window.addEventListener('focus', handleWindowFocus); - - // Multiple attempts to ensure focus on page load - const focusAttempts = [100, 500, 1000, 2000]; - const timeouts = focusAttempts.map((delay) => setTimeout(focusVideo, delay)); - - return () => { - document.removeEventListener('visibilitychange', handleVisibilityChange); - window.removeEventListener('focus', handleWindowFocus); - timeouts.forEach(clearTimeout); - }; - }, []); */ - return (