From eaf87e20d8083eaf12696e59b9addc2764c34312 Mon Sep 17 00:00:00 2001 From: Yiannis Christodoulou Date: Tue, 22 Jul 2025 05:18:43 +0300 Subject: [PATCH] feat: Autoplay functionality --- frontend-tools/video-js/src/VideoJS.css | 45 +++ .../controls/AutoplayToggleButton.js | 119 +++++++ .../components/controls/NextVideoButton.js | 4 +- .../video-js/src/components/index.js | 2 + .../overlays/AutoplayCountdownOverlay.css | 248 ++++++++++++++ .../overlays/AutoplayCountdownOverlay.js | 225 +++++++++++++ .../components/video-player/VideoJSPlayer.jsx | 232 ++++++++----- .../video-js/src/utils/UserPreferences.js | 18 + frontend/package-lock.json | 24 +- frontend/package.json | 9 +- frontend/packages/player/package-lock.json | 30 +- frontend/packages/scripts/package-lock.json | 83 +++-- .../media-viewer/VideoViewer/index.js | 3 +- static/video_js/video-js.css | 2 +- static/video_js/video-js.js | 314 +++++++++++++----- static/video_js/video-js.js.map | 2 +- 16 files changed, 1118 insertions(+), 242 deletions(-) create mode 100644 frontend-tools/video-js/src/components/controls/AutoplayToggleButton.js create mode 100644 frontend-tools/video-js/src/components/overlays/AutoplayCountdownOverlay.css create mode 100644 frontend-tools/video-js/src/components/overlays/AutoplayCountdownOverlay.js diff --git a/frontend-tools/video-js/src/VideoJS.css b/frontend-tools/video-js/src/VideoJS.css index 64654740..c6227285 100644 --- a/frontend-tools/video-js/src/VideoJS.css +++ b/frontend-tools/video-js/src/VideoJS.css @@ -55,6 +55,43 @@ right: 10px; } +/* Autoplay Toggle Button Styles */ +.vjs-autoplay-toggle { + width: 3em !important; + height: 3em !important; + flex: none; + padding: 0 !important; + margin: 0 4px !important; + line-height: 3em !important; + position: relative; + border: none !important; + background: transparent !important; + color: #fff !important; + cursor: pointer !important; + transition: all 0.2s ease !important; +} + +.vjs-autoplay-toggle:hover { + color: #ff4444 !important; + transform: scale(1.1) !important; +} + +.vjs-autoplay-toggle .vjs-autoplay-icon { + width: 1.2em; + height: 1.2em; + display: flex; + align-items: center; + justify-content: center; + margin: auto; + pointer-events: none; +} + +.vjs-autoplay-toggle .vjs-autoplay-icon svg { + width: 100%; + height: 100%; + display: block; +} + .vjs-next-video-control .vjs-icon-placeholder { width: 1.2em; height: 1.2em; @@ -511,9 +548,17 @@ gap: 6px !important; } +/* Spacer element to push buttons to the right */ +.video-js .vjs-spacer-control { + flex: 1 !important; + min-width: 1px !important; + height: 100% !important; +} + /* Basic control bar styling - let JavaScript handle positioning */ .video-js .vjs-control-bar .vjs-control { /* Natural flex flow */ + flex: none !important; /* Prevent controls from growing */ } /* Seek Indicator Styles */ diff --git a/frontend-tools/video-js/src/components/controls/AutoplayToggleButton.js b/frontend-tools/video-js/src/components/controls/AutoplayToggleButton.js new file mode 100644 index 00000000..67ef0a3d --- /dev/null +++ b/frontend-tools/video-js/src/components/controls/AutoplayToggleButton.js @@ -0,0 +1,119 @@ +import videojs from 'video.js'; + +const Button = videojs.getComponent('Button'); + +// Custom Autoplay Toggle Button Component using modern Video.js API +class AutoplayToggleButton extends Button { + constructor(player, options) { + super(player, options); + this.userPreferences = options.userPreferences; + // Get autoplay preference from localStorage, default to false if not set + if (this.userPreferences) { + const savedAutoplay = this.userPreferences.getPreference('autoplay'); + this.isAutoplayEnabled = savedAutoplay === true; // Explicit boolean check + console.log('Autoplay button initialized with saved preference:', this.isAutoplayEnabled); + } else { + this.isAutoplayEnabled = false; + console.log('Autoplay button initialized with default (no userPreferences):', this.isAutoplayEnabled); + } + + // Bind methods + this.updateIcon = this.updateIcon.bind(this); + this.handleClick = this.handleClick.bind(this); + } + + createEl() { + const button = super.createEl('button', { + className: 'vjs-autoplay-toggle vjs-control vjs-button', + type: 'button', + title: this.isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off', + 'aria-label': this.isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off', + }); + + // Create simple text-based icon for now to ensure it works + this.iconSpan = videojs.dom.createEl('span', { + 'aria-hidden': 'true', + className: 'vjs-autoplay-icon', + }); + + // Set initial icon state directly + console.log('AutoplayToggleButton createEl: isAutoplayEnabled =', this.isAutoplayEnabled); + if (this.isAutoplayEnabled) { + this.iconSpan.innerHTML = ``; + console.log('Setting RED icon (autoplay ON)'); + } else { + this.iconSpan.innerHTML = ``; + console.log('Setting GRAY icon (autoplay OFF)'); + } + + // Create control text span + const controlTextSpan = videojs.dom.createEl('span', { + className: 'vjs-control-text', + }); + controlTextSpan.textContent = this.isAutoplayEnabled ? 'Autoplay is on' : 'Autoplay is off'; + + // Append both spans to button + button.appendChild(this.iconSpan); + button.appendChild(controlTextSpan); + + return button; + } + + updateIcon() { + if (this.isAutoplayEnabled) { + // Simple text icon for now + this.iconSpan.innerHTML = ``; + // Only update element properties if element exists + 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 { + // Simple text icon for now + this.iconSpan.innerHTML = ``; + // Only update element properties if element exists + 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'; + } + } + } + } + + handleClick() { + // Toggle autoplay state + this.isAutoplayEnabled = !this.isAutoplayEnabled; + + // Save preference if userPreferences is available + if (this.userPreferences) { + this.userPreferences.setAutoplayPreference(this.isAutoplayEnabled); + console.log('Autoplay preference saved to localStorage:', this.isAutoplayEnabled); + } + + // Update icon and accessibility attributes + this.updateIcon(); + + console.log('Autoplay toggled:', this.isAutoplayEnabled ? 'ON' : 'OFF'); + + // Trigger custom event for other components to listen to + this.player().trigger('autoplayToggle', { autoplay: this.isAutoplayEnabled }); + } + + // Method to update button state from external sources + setAutoplayState(enabled) { + this.isAutoplayEnabled = enabled; + this.updateIcon(); + } +} + +// Register the component +videojs.registerComponent('AutoplayToggleButton', AutoplayToggleButton); + +export default AutoplayToggleButton; diff --git a/frontend-tools/video-js/src/components/controls/NextVideoButton.js b/frontend-tools/video-js/src/components/controls/NextVideoButton.js index 61d05be2..5968039d 100644 --- a/frontend-tools/video-js/src/components/controls/NextVideoButton.js +++ b/frontend-tools/video-js/src/components/controls/NextVideoButton.js @@ -6,7 +6,7 @@ const Button = videojs.getComponent('Button'); class NextVideoButton extends Button { constructor(player, options) { super(player, options); - this.nextLink = options.nextLink || ''; + // this.nextLink = options.nextLink || ''; } createEl() { @@ -43,7 +43,7 @@ class NextVideoButton extends Button { } handleClick() { - console.log('NextVideoButton handleClick', this.nextLink); + // console.log('NextVideoButton handleClick', this.nextLink); this.player().trigger('nextVideo'); } } diff --git a/frontend-tools/video-js/src/components/index.js b/frontend-tools/video-js/src/components/index.js index 9a7ed504..7428eb6c 100644 --- a/frontend-tools/video-js/src/components/index.js +++ b/frontend-tools/video-js/src/components/index.js @@ -1,5 +1,7 @@ // Export all Video.js components export { default as VideoJSPlayer } from './video-player/VideoJSPlayer'; export { default as EndScreenOverlay } from './overlays/EndScreenOverlay'; +export { default as AutoplayCountdownOverlay } from './overlays/AutoplayCountdownOverlay'; export { default as ChapterMarkers } from './markers/ChapterMarkers'; export { default as NextVideoButton } from './controls/NextVideoButton'; +export { default as AutoplayToggleButton } from './controls/AutoplayToggleButton'; diff --git a/frontend-tools/video-js/src/components/overlays/AutoplayCountdownOverlay.css b/frontend-tools/video-js/src/components/overlays/AutoplayCountdownOverlay.css new file mode 100644 index 00000000..2f47063b --- /dev/null +++ b/frontend-tools/video-js/src/components/overlays/AutoplayCountdownOverlay.css @@ -0,0 +1,248 @@ +/* Autoplay Countdown Overlay Styles */ +.vjs-autoplay-countdown-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: calc(100% - 46px); /* Account for control bar */ + background: rgba(0, 0, 0, 0.85); + display: none; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 1000; + padding: 20px; + box-sizing: border-box; + backdrop-filter: blur(4px); + opacity: 0; + transition: opacity 0.3s ease-in-out; +} + +.vjs-autoplay-countdown-overlay.autoplay-countdown-show { + opacity: 1; +} + +.autoplay-countdown-content { + background: rgba(0, 0, 0, 0.9); + border-radius: 12px; + padding: 24px; + max-width: 400px; + 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); +} + +.autoplay-countdown-header { + margin-bottom: 20px; +} + +.autoplay-countdown-header h3 { + color: #fff; + font-size: 18px; + font-weight: 500; + margin: 0; + line-height: 1.4; +} + +.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; + align-items: center; + gap: 12px; + margin-bottom: 24px; + text-align: left; +} + +.next-video-thumbnail { + flex-shrink: 0; + width: 80px; + height: 45px; + border-radius: 6px; + overflow: hidden; + background: #333; +} + +.next-video-thumbnail img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.next-video-details { + flex-grow: 1; + min-width: 0; /* Allow text truncation */ +} + +.next-video-title { + color: #fff; + font-size: 14px; + font-weight: 600; + margin: 0 0 4px 0; + line-height: 1.3; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.next-video-author { + color: #aaa; + font-size: 12px; + margin: 0 0 2px 0; + line-height: 1.2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.next-video-duration { + color: #ccc; + font-size: 11px; + margin: 0; + line-height: 1.2; +} + +.autoplay-countdown-actions { + display: flex; + gap: 12px; + justify-content: center; + align-items: center; +} + +.autoplay-play-button, +.autoplay-cancel-button { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 10px 18px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + min-width: 100px; +} + +.autoplay-play-button { + background: #ff0000; + color: #fff; +} + +.autoplay-play-button:hover { + background: #e60000; + transform: translateY(-1px); +} + +.autoplay-play-button:active { + transform: translateY(0); +} + +.autoplay-cancel-button { + background: rgba(255, 255, 255, 0.1); + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.autoplay-cancel-button:hover { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.3); +} + +.autoplay-cancel-button:active { + background: rgba(255, 255, 255, 0.1); +} + +.autoplay-play-button svg, +.autoplay-cancel-button svg { + width: 1.2em; + height: 1.2em; + flex-shrink: 0; +} + +/* Autoplay Toggle Button Styles */ +.vjs-autoplay-toggle { + width: 3em !important; + height: 3em !important; + flex: none; + padding: 0 !important; + margin: 0 4px !important; + line-height: 3em !important; + position: relative; +} + +.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%; + height: 100%; + display: block; +} + +/* Responsive design */ +@media (max-width: 480px) { + .autoplay-countdown-content { + padding: 20px; + max-width: 320px; + } + + .autoplay-countdown-header h3 { + font-size: 16px; + } + + .countdown-timer { + font-size: 18px; + } + + .autoplay-countdown-video-info { + gap: 10px; + } + + .next-video-thumbnail { + width: 60px; + height: 34px; + } + + .next-video-title { + font-size: 13px; + } + + .autoplay-countdown-actions { + flex-direction: column; + gap: 8px; + } + + .autoplay-play-button, + .autoplay-cancel-button { + width: 100%; + } +} diff --git a/frontend-tools/video-js/src/components/overlays/AutoplayCountdownOverlay.js b/frontend-tools/video-js/src/components/overlays/AutoplayCountdownOverlay.js new file mode 100644 index 00000000..8f652dd8 --- /dev/null +++ b/frontend-tools/video-js/src/components/overlays/AutoplayCountdownOverlay.js @@ -0,0 +1,225 @@ +import videojs from 'video.js'; +import './AutoplayCountdownOverlay.css'; + +// Get the Component base class from Video.js +const Component = videojs.getComponent('Component'); + +class AutoplayCountdownOverlay extends Component { + constructor(player, options) { + super(player, options); + + this.nextVideoData = options.nextVideoData || null; + this.countdownSeconds = options.countdownSeconds || 5; + this.onPlayNext = options.onPlayNext || (() => {}); + this.onCancel = options.onCancel || (() => {}); + + this.currentCountdown = this.countdownSeconds; + this.countdownInterval = null; + this.isActive = false; + + // Bind methods + this.startCountdown = this.startCountdown.bind(this); + this.stopCountdown = this.stopCountdown.bind(this); + this.handlePlayNext = this.handlePlayNext.bind(this); + this.handleCancel = this.handleCancel.bind(this); + this.updateCountdownDisplay = this.updateCountdownDisplay.bind(this); + } + + createEl() { + const overlay = super.createEl('div', { + className: 'vjs-autoplay-countdown-overlay', + }); + + // Get next video title or fallback + const nextVideoTitle = this.nextVideoData?.title || 'Next Video'; + const nextVideoThumbnail = this.nextVideoData?.thumbnail || ''; + + overlay.innerHTML = ` +
+
+

Up next in ${this.countdownSeconds}

+
+ +
+ ${ + nextVideoThumbnail + ? `
+ ${nextVideoTitle} +
` + : '' + } +
+

${nextVideoTitle}

+ ${this.nextVideoData?.author ? `

${this.nextVideoData.author}

` : ''} + ${this.nextVideoData?.duration ? `

${this.formatDuration(this.nextVideoData.duration)}

` : ''} +
+
+ +
+ + +
+
+ `; + + // Add event listeners with explicit binding + const playButton = overlay.querySelector('.autoplay-play-button'); + const cancelButton = overlay.querySelector('.autoplay-cancel-button'); + + if (playButton) { + playButton.addEventListener('click', (e) => { + e.preventDefault(); + this.handlePlayNext(); + }); + } + + if (cancelButton) { + cancelButton.addEventListener('click', (e) => { + e.preventDefault(); + this.handleCancel(); + }); + } + + // Initially hide the overlay + overlay.style.display = 'none'; + + return overlay; + } + + startCountdown() { + this.isActive = true; + this.currentCountdown = this.countdownSeconds; + this.show(); + this.updateCountdownDisplay(); + + // Start countdown interval + this.countdownInterval = setInterval(() => { + this.currentCountdown--; + this.updateCountdownDisplay(); + + if (this.currentCountdown <= 0) { + this.stopCountdown(); + // Auto-play next video when countdown reaches 0 + this.handlePlayNext(); + } + }, 1000); + + console.log('Autoplay countdown started:', this.countdownSeconds, 'seconds'); + } + + stopCountdown() { + this.isActive = false; + if (this.countdownInterval) { + clearInterval(this.countdownInterval); + this.countdownInterval = null; + } + this.hide(); + console.log('Autoplay countdown stopped'); + } + + updateCountdownDisplay() { + const timerElement = this.el().querySelector('.countdown-timer'); + if (timerElement) { + timerElement.textContent = this.currentCountdown; + } + } + + handlePlayNext() { + console.log('Autoplay: Playing next video immediately'); + try { + this.stopCountdown(); + this.onPlayNext(); + } catch (error) { + console.error('Error in handlePlayNext:', error); + } + } + + handleCancel() { + console.log('Autoplay: Cancelled by user'); + try { + this.stopCountdown(); + this.onCancel(); + } catch (error) { + console.error('Error in handleCancel:', error); + } + } + + show() { + if (this.el()) { + this.el().style.display = 'flex'; + // Add animation class for smooth entrance + this.el().classList.add('autoplay-countdown-show'); + } + } + + hide() { + if (this.el()) { + this.el().style.display = 'none'; + this.el().classList.remove('autoplay-countdown-show'); + } + } + + formatDuration(seconds) { + if (!seconds || seconds === 0) return ''; + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } else { + return `${minutes}:${secs.toString().padStart(2, '0')}`; + } + } + + // Update next video data + updateNextVideoData(nextVideoData) { + this.nextVideoData = nextVideoData; + + // Re-render the content if the overlay exists + if (this.el()) { + const nextVideoTitle = this.nextVideoData?.title || 'Next Video'; + const nextVideoThumbnail = this.nextVideoData?.thumbnail || ''; + + const videoInfoElement = this.el().querySelector('.autoplay-countdown-video-info'); + if (videoInfoElement) { + videoInfoElement.innerHTML = ` + ${ + nextVideoThumbnail + ? `
+ ${nextVideoTitle} +
` + : '' + } +
+

${nextVideoTitle}

+ ${this.nextVideoData?.author ? `

${this.nextVideoData.author}

` : ''} + ${this.nextVideoData?.duration ? `

${this.formatDuration(this.nextVideoData.duration)}

` : ''} +
+ `; + } + } + } + + // Cleanup method + dispose() { + this.stopCountdown(); + super.dispose(); + } +} + +// Register the component +videojs.registerComponent('AutoplayCountdownOverlay', AutoplayCountdownOverlay); + +export default AutoplayCountdownOverlay; 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 8dc14f96..7c987f46 100644 --- a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx +++ b/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx @@ -4,8 +4,10 @@ import 'video.js/dist/video-js.css'; // Import the separated components import EndScreenOverlay from '../overlays/EndScreenOverlay'; +import AutoplayCountdownOverlay from '../overlays/AutoplayCountdownOverlay'; import ChapterMarkers from '../markers/ChapterMarkers'; import NextVideoButton from '../controls/NextVideoButton'; +import AutoplayToggleButton from '../controls/AutoplayToggleButton'; import CustomRemainingTime from '../controls/CustomRemainingTime'; import CustomChaptersOverlay from '../controls/CustomChaptersOverlay'; import CustomSettingsMenu from '../controls/CustomSettingsMenu'; @@ -561,7 +563,6 @@ function VideoJSPlayer() { frame: { width: 160, height: 90, seconds: 10 }, }, siteUrl: '', - hasNextLink: true, nextLink: 'https://demo.mediacms.io/view?m=YjGJafibO', }, [] @@ -589,7 +590,7 @@ function VideoJSPlayer() { poster: mediaData.siteUrl + mediaData.data?.poster_url || '', previewSprite: mediaData?.previewSprite || {}, related_media: mediaData.data?.related_media || [], - nextLink: mediaData.nextLink || '', + nextLink: mediaData?.nextLink || null, sources: mediaData.data?.original_media_url ? [ { @@ -661,8 +662,8 @@ function VideoJSPlayer() { // Player dimensions - removed for responsive design - // Autoplay behavior: false, true, 'muted', 'play', 'any' - autoplay: true, + // Autoplay behavior: Use 'muted' to comply with browser policies + autoplay: 'muted', // Start video over when it ends loop: false, @@ -951,7 +952,7 @@ function VideoJSPlayer() { setTimeout(() => { if (playerRef.current && !playerRef.current.isDisposed()) { playerRef.current.play().catch((error) => { - console.log('Autoplay was prevented:', error); + console.log('ℹ️ Browser prevented autoplay (normal behavior):', error.message); }); } }, 100); @@ -1024,7 +1025,8 @@ function VideoJSPlayer() { // END: Implement custom time display component // BEGIN: Implement custom next video button - if (mediaData.hasNextLink) { + if (mediaData?.nextLink) { + console.log('mediaData.nextLink edw', mediaData.nextLink); const nextVideoButton = new NextVideoButton(playerRef.current, { nextLink: mediaData.nextLink, }); @@ -1033,6 +1035,30 @@ function VideoJSPlayer() { } // END: Implement custom next video button + // BEGIN: Implement autoplay toggle button - simplified + try { + const autoplayToggleButton = new AutoplayToggleButton(playerRef.current, { + userPreferences: userPreferences.current, + }); + // Add it after the play button + const playToggleIndex = controlBar.children().indexOf(playToggle); + controlBar.addChild(autoplayToggleButton, {}, playToggleIndex + 1); + + // Store reference for later use + customComponents.current.autoplayToggleButton = autoplayToggleButton; + + // Force update icon after adding to DOM to ensure correct display + setTimeout(() => { + autoplayToggleButton.updateIcon(); + console.log('✓ Autoplay toggle button icon updated after DOM insertion'); + }, 100); + + console.log('✓ Autoplay toggle button added successfully'); + } catch (error) { + console.error('✗ Failed to add autoplay toggle button:', error); + } + // END: Implement autoplay toggle button + // Remove duplicate captions button and move chapters to end /* const cleanupControls = () => { // Log all current children for debugging @@ -1148,66 +1174,28 @@ function VideoJSPlayer() { } // END: Add chapter markers to progress control - // BEGIN: Move CC (subtitles) and PiP buttons to the right side + // BEGIN: Simple button layout fix - use CSS approach setTimeout(() => { - // Create a spacer element to push buttons to the right - const spacer = videojs.dom.createEl('div', { - className: 'vjs-spacer-control vjs-control', - }); - spacer.style.flex = '1'; - spacer.style.minWidth = '1px'; + console.log('Setting up simplified button layout...'); - // Find insertion point after time displays + // 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'); - const customRemainingTime = controlBar.getChild('customRemainingTime'); - const insertAfter = customRemainingTime || durationDisplay; - - if (insertAfter) { - const insertIndex = controlBar.children().indexOf(insertAfter) + 1; - controlBar.el().insertBefore(spacer, controlBar.children()[insertIndex]?.el() || null); - console.log('✓ Spacer added after time displays'); + if (durationDisplay && durationDisplay.el()) { + const controlBarEl = controlBar.el(); + const durationEl = durationDisplay.el(); + const nextSibling = durationEl.nextSibling; + controlBarEl.insertBefore(spacerDiv, nextSibling); + console.log('✓ Simple spacer added after duration display'); } - - // Find the subtitles/captions button (CC button) - const possibleTextTrackButtons = ['subtitlesButton', 'captionsButton', 'subsCapsButton']; - let textTrackButton = null; - - for (const buttonName of possibleTextTrackButtons) { - const button = controlBar.getChild(buttonName); - if (button) { - textTrackButton = button; - console.log(`Found text track button: ${buttonName}`); - break; - } - } - - // Find other buttons to move to the right - const pipButton = controlBar.getChild('pictureInPictureToggle'); - const playbackRateButton = controlBar.getChild('playbackRateMenuButton'); - const chaptersButton = controlBar.getChild('chaptersButton'); - - // Move buttons to the right side (after spacer) - const buttonsToMove = [ - playbackRateButton, - textTrackButton, - pipButton, - chaptersButton, - fullscreenToggle, - ].filter(Boolean); - - buttonsToMove.forEach((button) => { - if (button) { - try { - controlBar.removeChild(button); - controlBar.addChild(button); - console.log(`✓ Moved ${button.name_ || 'button'} to right side`); - } catch (e) { - console.log(`✗ Failed to move button:`, e); - } - } - }); - }, 100); - // END: Move CC (subtitles) and PiP buttons to the right side + }, 300); + // END: Simple button layout fix // BEGIN: Move chapters button after fullscreen toggle if (chaptersButton && fullscreenToggle) { @@ -1492,8 +1480,9 @@ function VideoJSPlayer() { console.log('Video paused'); }); - // Store reference to end screen for cleanup + // Store reference to end screen and autoplay countdown for cleanup let endScreen = null; + let autoplayCountdown = null; playerRef.current.on('ended', () => { console.log('Video ended'); @@ -1516,37 +1505,115 @@ function VideoJSPlayer() { } }, 50); - // Prevent creating multiple end screens - if (endScreen) { - console.log('End screen already exists, removing previous one'); - playerRef.current.removeChild(endScreen); - endScreen = null; + console.log('mediaData.previewSprite', mediaData.previewSprite); + console.log('mediaData.nextLink', mediaData.nextLink); + console.log('userPreferences', userPreferences); + + // Check if autoplay is enabled and there's a next video + const isAutoplayEnabled = userPreferences.current.getAutoplayPreference(); + const hasNextVideo = mediaData.nextLink !== null; + + console.log('isAutoplayEnabled', isAutoplayEnabled); + console.log('hasNextVideo', hasNextVideo); + + if (isAutoplayEnabled && hasNextVideo) { + // 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) { + // For now, use the first related video as the next video + // In a real implementation, you might want to find the specific next video + 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 + autoplayCountdown = new AutoplayCountdownOverlay(playerRef.current, { + nextVideoData: nextVideoData, + countdownSeconds: 5, + onPlayNext: () => { + console.log('Autoplay: Navigating to next video'); + goToNextVideo(); + }, + onCancel: () => { + console.log('Autoplay: User cancelled, showing related videos'); + // Hide countdown and show end screen instead + if (autoplayCountdown) { + playerRef.current.removeChild(autoplayCountdown); + autoplayCountdown = null; + } + showEndScreen(); + }, + }); + + playerRef.current.addChild(autoplayCountdown); + autoplayCountdown.startCountdown(); + } else { + // Autoplay disabled or no next video - show regular end screen + showEndScreen(); } - // Show end screen with related videos - endScreen = new EndScreenOverlay(playerRef.current, { - relatedVideos: relatedVideos, - }); + // Function to show the regular end screen + function showEndScreen() { + // Prevent creating multiple end screens + if (endScreen) { + console.log('End screen already exists, removing previous one'); + playerRef.current.removeChild(endScreen); + endScreen = null; + } - // Also store the data directly on the component as backup - endScreen.relatedVideos = relatedVideos; + // Show end screen with related videos + endScreen = new EndScreenOverlay(playerRef.current, { + relatedVideos: relatedVideos, + }); - playerRef.current.addChild(endScreen); - endScreen.show(); + // Also store the data directly on the component as backup + endScreen.relatedVideos = relatedVideos; + + playerRef.current.addChild(endScreen); + endScreen.show(); + } }); - // Hide end screen when user wants to replay + // Hide end screen and autoplay countdown when user wants to replay playerRef.current.on('play', () => { if (endScreen) { endScreen.hide(); } + if (autoplayCountdown) { + autoplayCountdown.stopCountdown(); + } }); - // Hide end screen when user seeks + // Hide end screen and autoplay countdown when user seeks playerRef.current.on('seeking', () => { if (endScreen) { endScreen.hide(); } + if (autoplayCountdown) { + autoplayCountdown.stopCountdown(); + } }); // Handle replay button functionality @@ -1602,7 +1669,7 @@ function VideoJSPlayer() { // Start playing the video immediately if autoplay is enabled if (playerRef.current.autoplay()) { playerRef.current.play().catch((error) => { - console.log('Autoplay prevented by browser:', error); + console.log('ℹ️ Browser prevented autoplay (normal behavior):', error.message); // If autoplay fails, we can still focus the element // so the user can manually start and use keyboard controls }); @@ -1663,12 +1730,7 @@ function VideoJSPlayer() { }; }, []); - return ( - <> -