feat: Autoplay functionality

This commit is contained in:
Yiannis Christodoulou
2025-07-22 05:18:43 +03:00
parent de520e9faa
commit eaf87e20d8
16 changed files with 1118 additions and 242 deletions
+45
View File
@@ -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 */
@@ -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 = `<span style="font-size: 1.2em; color: #ff4444;">●</span>`;
console.log('Setting RED icon (autoplay ON)');
} else {
this.iconSpan.innerHTML = `<span style="font-size: 1.2em; color: #ccc;">○</span>`;
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 = `<span style="font-size: 1.2em; color: #ff4444;">●</span>`;
// 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 = `<span style="font-size: 1.2em; color: #ccc;">○</span>`;
// 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;
@@ -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');
}
}
@@ -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';
@@ -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%;
}
}
@@ -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 = `
<div class="autoplay-countdown-content">
<div class="autoplay-countdown-header">
<h3>Up next in <span class="countdown-timer">${this.countdownSeconds}</span></h3>
</div>
<div class="autoplay-countdown-video-info">
${
nextVideoThumbnail
? `<div class="next-video-thumbnail">
<img src="${nextVideoThumbnail}" alt="${nextVideoTitle}" />
</div>`
: ''
}
<div class="next-video-details">
<h4 class="next-video-title">${nextVideoTitle}</h4>
${this.nextVideoData?.author ? `<p class="next-video-author">${this.nextVideoData.author}</p>` : ''}
${this.nextVideoData?.duration ? `<p class="next-video-duration">${this.formatDuration(this.nextVideoData.duration)}</p>` : ''}
</div>
</div>
<div class="autoplay-countdown-actions">
<button class="autoplay-play-button" type="button">
<svg viewBox="0 0 24 24" width="1.2em" height="1.2em" fill="currentColor">
<path d="M8 5v14l11-7z"/>
</svg>
Play Now
</button>
<button class="autoplay-cancel-button" type="button">
<svg viewBox="0 0 24 24" width="1.2em" height="1.2em" fill="currentColor">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
Cancel
</button>
</div>
</div>
`;
// 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
? `<div class="next-video-thumbnail">
<img src="${nextVideoThumbnail}" alt="${nextVideoTitle}" />
</div>`
: ''
}
<div class="next-video-details">
<h4 class="next-video-title">${nextVideoTitle}</h4>
${this.nextVideoData?.author ? `<p class="next-video-author">${this.nextVideoData.author}</p>` : ''}
${this.nextVideoData?.duration ? `<p class="next-video-duration">${this.formatDuration(this.nextVideoData.duration)}</p>` : ''}
</div>
`;
}
}
}
// Cleanup method
dispose() {
this.stopCountdown();
super.dispose();
}
}
// Register the component
videojs.registerComponent('AutoplayCountdownOverlay', AutoplayCountdownOverlay);
export default AutoplayCountdownOverlay;
@@ -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 (
<>
<video ref={videoRef} className="video-js vjs-default-skin" tabIndex="0" />
<em>nextLink: {currentVideo.nextLink}</em>
</>
);
return <video ref={videoRef} className="video-js vjs-default-skin" tabIndex="0" />;
}
export default VideoJSPlayer;
@@ -11,6 +11,7 @@ class UserPreferences {
quality: 'auto', // Auto quality
subtitleLanguage: null, // No subtitles by default
muted: false,
autoplay: false, // Autoplay disabled by default
};
}
@@ -517,6 +518,23 @@ class UserPreferences {
console.error('❌ Error force saving subtitle language:', error);
}
}
/**
* Get autoplay preference
* @returns {boolean} Autoplay preference
*/
getAutoplayPreference() {
return this.getPreference('autoplay');
}
/**
* Set autoplay preference
* @param {boolean} autoplay - Autoplay setting
*/
setAutoplayPreference(autoplay) {
this.setPreference('autoplay', autoplay);
console.log('Autoplay preference saved:', autoplay);
}
}
export default UserPreferences;