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

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 */

View File

@ -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;

View File

@ -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');
}
}

View File

@ -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';

View File

@ -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%;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -26,6 +26,7 @@
"@babel/core": "^7.26.9",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@types/minimatch": "^5.1.2",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"autoprefixer": "^10.4.21",
@ -13864,13 +13865,26 @@
"node": ">=8"
}
},
"node_modules/path2d-polyfill": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz",
"integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==",
"node_modules/path2d": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/path2d/-/path2d-0.1.1.tgz",
"integrity": "sha512-/+S03c8AGsDYKKBtRDqieTJv2GlkMb0bWjnqOgtF6MkjdUQ9a8ARAtxWf9NgKLGm2+WQr6+/tqJdU8HNGsIDoA==",
"license": "MIT",
"engines": {
"node": ">=8"
"node": ">=6"
}
},
"node_modules/path2d-polyfill": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.1.1.tgz",
"integrity": "sha512-4Rka5lN+rY/p0CdD8+E+BFv51lFaFvJOrlOhyQ+zjzyQrzyh3ozmxd1vVGGDdIbUFSBtIZLSnspxTgPT0iJhvA==",
"deprecated": "this package has been deprecated",
"license": "MIT",
"dependencies": {
"path2d": "0.1.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/pbkdf2": {

View File

@ -17,6 +17,7 @@
"@babel/core": "^7.26.9",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@types/minimatch": "^5.1.2",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"autoprefixer": "^10.4.21",
@ -40,18 +41,18 @@
"webpack": "^5.98.0"
},
"dependencies": {
"@react-pdf-viewer/core": "^3.9.0",
"@react-pdf-viewer/default-layout": "^3.9.0",
"axios": "^1.8.2",
"flux": "^4.0.4",
"mediacms-player": "file:packages/player",
"normalize.css": "^8.0.1",
"pdfjs-dist": "3.4.120",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-mentions": "^4.3.1",
"sortablejs": "^1.13.0",
"timeago.js": "^4.0.2",
"url-parse": "^1.5.10",
"pdfjs-dist": "3.4.120",
"@react-pdf-viewer/core": "^3.9.0",
"@react-pdf-viewer/default-layout": "^3.9.0"
"url-parse": "^1.5.10"
}
}

View File

@ -1302,9 +1302,9 @@
}
},
"node_modules/@babel/plugin-transform-regenerator": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.0.tgz",
"integrity": "sha512-LOAozRVbqxEVjSKfhGnuLoE4Kz4Oc5UJzuvFUhSsQzdCdaAQu06mG8zDv2GFSerM62nImUZ7K92vxnQcLSDlCQ==",
"version": "7.28.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.1.tgz",
"integrity": "sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1643,9 +1643,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz",
"integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==",
"version": "7.28.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz",
"integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2111,9 +2111,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.0.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
"integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
"version": "24.0.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz",
"integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3766,9 +3766,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.180",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.180.tgz",
"integrity": "sha512-ED+GEyEh3kYMwt2faNmgMB0b8O5qtATGgR4RmRsIp4T6p7B8vdMbIedYndnvZfsaXvSzegtpfqRMDNCjjiSduA==",
"version": "1.5.189",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.189.tgz",
"integrity": "sha512-y9D1ntS1ruO/pZ/V2FtLE+JXLQe28XoRpZ7QCCo0T8LdQladzdcOVQZH/IWLVJvCw12OGMb6hYOeOAjntCmJRQ==",
"dev": true,
"license": "ISC"
},
@ -5885,9 +5885,9 @@
}
},
"node_modules/rollup-plugin-visualizer/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {

View File

@ -1386,9 +1386,9 @@
}
},
"node_modules/@babel/plugin-transform-regenerator": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.0.tgz",
"integrity": "sha512-LOAozRVbqxEVjSKfhGnuLoE4Kz4Oc5UJzuvFUhSsQzdCdaAQu06mG8zDv2GFSerM62nImUZ7K92vxnQcLSDlCQ==",
"version": "7.28.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.1.tgz",
"integrity": "sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==",
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
@ -1770,9 +1770,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz",
"integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==",
"version": "7.28.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz",
"integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@ -2536,9 +2536,9 @@
}
},
"node_modules/@types/express-serve-static-core": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz",
"integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==",
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz",
"integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
@ -2597,9 +2597,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.0.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
"integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
"version": "24.0.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz",
"integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.8.0"
@ -3000,6 +3000,18 @@
"node": ">=0.4.0"
}
},
"node_modules/acorn-import-phases": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
},
"peerDependencies": {
"acorn": "^8.14.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
@ -4534,16 +4546,16 @@
}
},
"node_modules/compression": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz",
"integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==",
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"compressible": "~2.0.18",
"debug": "2.6.9",
"negotiator": "~0.6.4",
"on-headers": "~1.0.2",
"on-headers": "~1.1.0",
"safe-buffer": "5.2.1",
"vary": "~1.1.2"
},
@ -5744,9 +5756,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.180",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.180.tgz",
"integrity": "sha512-ED+GEyEh3kYMwt2faNmgMB0b8O5qtATGgR4RmRsIp4T6p7B8vdMbIedYndnvZfsaXvSzegtpfqRMDNCjjiSduA==",
"version": "1.5.189",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.189.tgz",
"integrity": "sha512-y9D1ntS1ruO/pZ/V2FtLE+JXLQe28XoRpZ7QCCo0T8LdQladzdcOVQZH/IWLVJvCw12OGMb6hYOeOAjntCmJRQ==",
"license": "ISC"
},
"node_modules/elliptic": {
@ -9178,9 +9190,9 @@
"license": "MIT"
},
"node_modules/nan": {
"version": "2.22.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz",
"integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==",
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
"license": "MIT",
"optional": true
},
@ -9611,9 +9623,9 @@
}
},
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
@ -11609,9 +11621,9 @@
}
},
"node_modules/rollup-plugin-visualizer/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
@ -13921,21 +13933,22 @@
}
},
"node_modules/webpack": {
"version": "5.99.9",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz",
"integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==",
"version": "5.100.2",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.100.2.tgz",
"integrity": "sha512-QaNKAvGCDRh3wW1dsDjeMdDXwZm2vqq3zn6Pvq4rHOEOGSaUMgOOjG2Y9ZbIGzpfkJk9ZYTHpDqgDfeBDcnLaw==",
"license": "MIT",
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6",
"@types/estree": "^1.0.8",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1",
"acorn": "^8.14.0",
"acorn": "^8.15.0",
"acorn-import-phases": "^1.0.3",
"browserslist": "^4.24.0",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.17.1",
"enhanced-resolve": "^5.17.2",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
@ -13949,7 +13962,7 @@
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1",
"webpack-sources": "^3.2.3"
"webpack-sources": "^3.3.3"
},
"bin": {
"webpack": "bin/webpack.js"

View File

@ -541,7 +541,6 @@ export default class VideoViewer extends React.PureComponent {
onLoad: () => console.log('Video js loaded in VideoViewer'),
onError: (error) => console.error('Video js error in VideoViewer:', error),
})} */}
<div
key={(this.props.inEmbed ? 'embed-' : '') + 'player-container'}
className={'player-container' + (this.videoSources.length ? '' : ' player-container-error')}
@ -558,6 +557,7 @@ export default class VideoViewer extends React.PureComponent {
<SiteConsumer>
{(site) => {
return React.createElement(VideoJSEmbed, {
nextLink: nextLink,
data: this.props.data,
playerVolume: this.browserCache.get('player-volume'),
playerSoundMuted: this.browserCache.get('player-sound-muted'),
@ -592,7 +592,6 @@ export default class VideoViewer extends React.PureComponent {
) : null}
</div>
</div>
{/* <div
key={(this.props.inEmbed ? 'embed-' : '') + 'player-container'}
className={'player-container' + (this.videoSources.length ? '' : ' player-container-error')}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long