/** * Base Video Player * Reusable video player component extracted from Porn Cinema * Provides core video playback functionality for use across game modes */ class BaseVideoPlayer { constructor(containerSelector, options = {}) { // Configuration options this.options = { showControls: options.showControls !== false, autoHide: options.autoHide !== false, showProgress: options.showProgress !== false, showVolume: options.showVolume !== false, showFullscreen: options.showFullscreen !== false, showQuality: options.showQuality !== false, showSpeed: options.showSpeed !== false, keyboardShortcuts: options.keyboardShortcuts !== false, minimal: options.minimal || false, ...options }; // Video state this.currentVideo = null; this.isPlaying = false; this.isFullscreen = false; this.volume = options.initialVolume || 0.7; this.playbackRate = 1.0; this.hideControlsTimeout = null; this.showControlsDebounce = false; this.lastLoggedTime = -1; // Get container and initialize this.container = document.querySelector(containerSelector); if (!this.container) { throw new Error(`Video container not found: ${containerSelector}`); } this.initializeElements(); this.attachEventListeners(); this.setVolume(this.volume); } initializeElements() { // Find video elements within the container this.videoElement = this.container.querySelector('video') || this.container.querySelector('.main-video'); if (!this.videoElement) { console.warn('No video element found in container'); return; } this.videoSource = this.videoElement.querySelector('source'); // Find control elements (optional - may not exist in minimal mode) this.controls = {}; this.controls.container = this.container.querySelector('.video-controls'); this.controls.playPause = this.container.querySelector('#play-pause-btn') || this.container.querySelector('.play-pause-btn'); this.controls.progressBar = this.container.querySelector('#progress-bar') || this.container.querySelector('.progress-bar'); this.controls.progressFilled = this.container.querySelector('#progress-filled') || this.container.querySelector('.progress-filled'); this.controls.progressThumb = this.container.querySelector('#progress-thumb') || this.container.querySelector('.progress-thumb'); this.controls.currentTime = this.container.querySelector('#current-time') || this.container.querySelector('.current-time'); this.controls.totalTime = this.container.querySelector('#total-time') || this.container.querySelector('.total-time'); // Debug progress bar elements console.log('🎬 Progress bar elements found:', { progressBar: !!this.controls.progressBar, progressFilled: !!this.controls.progressFilled, progressThumb: !!this.controls.progressThumb, currentTime: !!this.controls.currentTime, totalTime: !!this.controls.totalTime }); // Additional debugging - check if elements exist in DOM const domProgressBar = document.getElementById('progress-bar'); const domProgressFilled = document.getElementById('progress-filled'); console.log('🎬 DOM elements check:', { 'progress-bar in DOM': !!domProgressBar, 'progress-filled in DOM': !!domProgressFilled, 'container has progress elements': this.container.querySelectorAll('.progress-bar, #progress-bar').length > 0 }); this.controls.volume = this.container.querySelector('#volume-slider') || this.container.querySelector('.volume-slider'); this.controls.volumePercentage = this.container.querySelector('#volume-percentage') || this.container.querySelector('.volume-percentage'); this.controls.mute = this.container.querySelector('#mute-btn') || this.container.querySelector('.mute-btn'); this.controls.fullscreen = this.container.querySelector('#fullscreen-btn') || this.container.querySelector('.fullscreen-btn'); this.controls.quality = this.container.querySelector('#quality-select') || this.container.querySelector('.quality-select'); this.controls.speed = this.container.querySelector('#speed-select') || this.container.querySelector('.speed-select'); // Large play button overlay this.playButtonLarge = this.container.querySelector('#play-button-large') || this.container.querySelector('.play-button-large'); this.playOverlay = this.container.querySelector('#play-overlay') || this.container.querySelector('.play-overlay'); this.videoOverlay = this.container.querySelector('#video-overlay') || this.container.querySelector('.video-overlay'); this.videoLoading = this.container.querySelector('#video-loading') || this.container.querySelector('.video-loading'); } attachEventListeners() { if (!this.videoElement) return; // Video events this.videoElement.addEventListener('timeupdate', () => this.updateProgress()); this.videoElement.addEventListener('loadedmetadata', () => this.onMetadataLoaded()); this.videoElement.addEventListener('play', () => this.onPlay()); this.videoElement.addEventListener('pause', () => this.onPause()); this.videoElement.addEventListener('ended', () => this.onEnded()); this.videoElement.addEventListener('error', (e) => this.onError(e)); this.videoElement.addEventListener('loadstart', () => this.showLoading()); this.videoElement.addEventListener('canplay', () => this.hideLoading()); // Control events (if controls exist) if (this.playButtonLarge) { this.playButtonLarge.addEventListener('click', () => this.togglePlayPause()); } if (this.controls.playPause) { this.controls.playPause.addEventListener('click', () => this.togglePlayPause()); } if (this.controls.progressBar) { this.controls.progressBar.addEventListener('click', (e) => this.seekToPosition(e)); } if (this.controls.volume) { this.controls.volume.addEventListener('input', (e) => this.setVolume(e.target.value / 100)); } if (this.controls.mute) { this.controls.mute.addEventListener('click', () => this.toggleMute()); } if (this.controls.fullscreen) { this.controls.fullscreen.addEventListener('click', () => this.toggleFullscreen()); } if (this.controls.quality) { this.controls.quality.addEventListener('change', (e) => this.setQuality(e.target.value)); } if (this.controls.speed) { this.controls.speed.addEventListener('change', (e) => this.setPlaybackRate(e.target.value)); } // Auto-hide controls behavior if (this.options.autoHide && this.controls.container) { this.container.addEventListener('mouseenter', () => this.showControls()); this.container.addEventListener('mouseleave', () => this.hideControls()); this.container.addEventListener('mousemove', () => this.showControls()); } // Keyboard shortcuts if (this.options.keyboardShortcuts) { document.addEventListener('keydown', (e) => this.handleKeyboardShortcut(e)); } // Fullscreen events document.addEventListener('fullscreenchange', () => this.onFullscreenChange()); document.addEventListener('webkitfullscreenchange', () => this.onFullscreenChange()); document.addEventListener('mozfullscreenchange', () => this.onFullscreenChange()); } // ===== CORE PLAYBACK METHODS ===== loadVideo(videoPath, autoPlay = false) { if (!this.videoElement) { console.error('No video element available'); return; } console.log(`🎬 Loading video: ${videoPath}`); this.currentVideo = videoPath; this.showLoading(); if (this.videoSource) { this.videoSource.src = videoPath; } else { this.videoElement.src = videoPath; } this.videoElement.load(); // Show controls when video loads this.showControls(); if (autoPlay) { this.videoElement.addEventListener('loadeddata', () => this.play(), { once: true }); } } togglePlayPause() { if (!this.videoElement) { console.warn('No video element available'); return; } // Check if video has a source - either direct src or via source element const hasSource = this.videoElement.src || (this.videoSource && this.videoSource.src) || this.currentVideo; if (!hasSource) { console.warn('No video loaded'); return; } console.log('🎬 Toggle play/pause clicked, currently paused:', this.videoElement.paused); if (this.videoElement.paused) { this.play(); } else { this.pause(); } } play() { if (!this.videoElement) return; console.log('🎬 Attempting to play video...'); const playPromise = this.videoElement.play(); if (playPromise !== undefined) { playPromise.then(() => { this.isPlaying = true; this.updatePlayButton(); this.hidePlayOverlay(); console.log('🎬 Video playing, isPlaying:', this.isPlaying); }).catch(error => { console.error('Error playing video:', error); this.showError('Failed to play video'); }); } } pause() { if (!this.videoElement) return; console.log('🎬 Attempting to pause video...'); this.videoElement.pause(); this.isPlaying = false; this.updatePlayButton(); this.showPlayOverlay(); console.log('🎬 Video paused, isPlaying:', this.isPlaying); } seek(seconds) { if (this.videoElement && this.videoElement.duration) { const newTime = Math.max(0, Math.min(this.videoElement.duration, this.videoElement.currentTime + seconds)); this.videoElement.currentTime = newTime; } } seekToPosition(event) { if (this.videoElement && this.videoElement.duration && this.controls.progressBar) { const rect = this.controls.progressBar.getBoundingClientRect(); const pos = (event.clientX - rect.left) / rect.width; this.videoElement.currentTime = pos * this.videoElement.duration; } } // ===== VOLUME AND AUDIO CONTROLS ===== setVolume(volume) { this.volume = Math.max(0, Math.min(1, volume)); if (this.videoElement) { this.videoElement.volume = this.volume; } if (this.controls.volume) { this.controls.volume.value = this.volume * 100; } if (this.controls.volumePercentage) { this.controls.volumePercentage.textContent = Math.round(this.volume * 100) + '%'; } this.updateMuteButton(); } adjustVolume(delta) { this.setVolume(this.volume + delta); } toggleMute() { if (this.videoElement) { this.videoElement.muted = !this.videoElement.muted; this.updateMuteButton(); } } updateMuteButton() { if (this.controls.mute) { const isMuted = this.videoElement && this.videoElement.muted; const isZeroVolume = this.volume === 0; this.controls.mute.textContent = (isMuted || isZeroVolume) ? '🔇' : '🔊'; } } // ===== PLAYBACK RATE AND QUALITY ===== setPlaybackRate(rate) { this.playbackRate = parseFloat(rate); if (this.videoElement) { this.videoElement.playbackRate = this.playbackRate; } } setQuality(quality) { // Quality implementation would depend on available video sources console.log(`Quality set to: ${quality}`); } // ===== FULLSCREEN ===== toggleFullscreen() { if (!document.fullscreenElement) { this.enterFullscreen(); } else { this.exitFullscreen(); } } enterFullscreen() { if (this.container.requestFullscreen) { this.container.requestFullscreen(); } else if (this.container.webkitRequestFullscreen) { this.container.webkitRequestFullscreen(); } else if (this.container.mozRequestFullScreen) { this.container.mozRequestFullScreen(); } } exitFullscreen() { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } } onFullscreenChange() { this.isFullscreen = !!document.fullscreenElement; if (this.controls.fullscreen) { this.controls.fullscreen.textContent = this.isFullscreen ? '⛶' : '⛶'; } } // ===== UI UPDATES ===== updateProgress() { if (!this.videoElement || !this.videoElement.duration) return; const progress = (this.videoElement.currentTime / this.videoElement.duration) * 100; // Only log every 5 seconds to reduce console spam if (Math.floor(this.videoElement.currentTime) % 5 === 0 && Math.floor(this.videoElement.currentTime) !== this.lastLoggedTime) { console.log(`🎬 Progress update: ${progress.toFixed(1)}% (${this.videoElement.currentTime.toFixed(1)}s / ${this.videoElement.duration.toFixed(1)}s)`); console.log('🎬 Progress elements check:', { progressFilled: !!this.controls.progressFilled, hasStyle: this.controls.progressFilled ? 'width: ' + this.controls.progressFilled.style.width : 'N/A' }); this.lastLoggedTime = Math.floor(this.videoElement.currentTime); } if (this.controls.progressFilled) { this.controls.progressFilled.style.width = progress + '%'; } else { console.warn('🎬 progressFilled element not found during update'); } if (this.controls.currentTime) { this.controls.currentTime.textContent = this.formatTime(this.videoElement.currentTime); } if (this.controls.totalTime) { this.controls.totalTime.textContent = this.formatTime(this.videoElement.duration); } } updatePlayButton() { const playText = this.isPlaying ? '⏸' : '▶'; console.log('🎬 Updating play button:', playText, 'isPlaying:', this.isPlaying); if (this.controls.playPause) { this.controls.playPause.textContent = playText; console.log('🎬 Updated main pause button'); } if (this.playButtonLarge) { this.playButtonLarge.textContent = playText; console.log('🎬 Updated large play button'); } } formatTime(seconds) { if (!seconds || isNaN(seconds)) return '0:00'; const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; } // ===== CONTROL VISIBILITY ===== showControls() { // Prevent rapid-fire calls with debouncing if (this.showControlsDebounce) return; this.showControlsDebounce = true; setTimeout(() => { this.showControlsDebounce = false; }, 100); if (this.controls.container) { this.controls.container.classList.add('visible'); this.container.classList.remove('hide-controls', 'auto-hide'); } else { console.warn('🎬 No controls container found'); } // Clear existing timeout if (this.hideControlsTimeout) { clearTimeout(this.hideControlsTimeout); } // Set timeout to auto-hide controls after 3 seconds of no interaction if (this.videoElement && !this.videoElement.paused && this.options.autoHide) { this.hideControlsTimeout = setTimeout(() => { this.container.classList.add('auto-hide'); console.log('🎬 Auto-hiding controls after 3 seconds'); }, 3000); } } hideControls() { if (!this.videoElement || this.videoElement.paused) return; if (this.options.autoHide) { this.container.classList.add('auto-hide'); } } showPlayOverlay() { if (this.playOverlay) { this.playOverlay.style.display = 'flex'; } if (this.videoOverlay) { this.videoOverlay.classList.remove('hidden'); } } hidePlayOverlay() { if (this.playOverlay) { this.playOverlay.style.display = 'none'; } if (this.videoOverlay) { this.videoOverlay.classList.add('hidden'); } } showLoading() { if (this.videoLoading) { this.videoLoading.style.display = 'block'; } } hideLoading() { if (this.videoLoading) { this.videoLoading.style.display = 'none'; } } // ===== EVENT HANDLERS ===== onMetadataLoaded() { this.hideLoading(); this.updateProgress(); console.log(`📺 Video metadata loaded: ${this.formatTime(this.videoElement.duration)}`); } onPlay() { this.isPlaying = true; this.updatePlayButton(); this.hidePlayOverlay(); } onPause() { this.isPlaying = false; this.updatePlayButton(); this.showPlayOverlay(); } onEnded() { this.isPlaying = false; this.updatePlayButton(); this.showPlayOverlay(); console.log('📺 Video playback ended'); } onError(event) { console.error('📺 Video error:', event); this.hideLoading(); this.showError('Video failed to load'); } showError(message) { console.error(`📺 ${message}`); this.hideLoading(); // Override in subclass for custom error display } handleKeyboardShortcut(event) { // Only handle shortcuts if this player is active/focused if (!this.container.matches(':hover') && !this.isFullscreen) return; const shortcuts = { ' ': () => { event.preventDefault(); this.togglePlayPause(); }, 'ArrowLeft': () => { event.preventDefault(); this.seek(-10); }, 'ArrowRight': () => { event.preventDefault(); this.seek(10); }, 'ArrowUp': () => { event.preventDefault(); this.adjustVolume(0.1); }, 'ArrowDown': () => { event.preventDefault(); this.adjustVolume(-0.1); }, 'f': () => { event.preventDefault(); this.toggleFullscreen(); }, 'F': () => { event.preventDefault(); this.toggleFullscreen(); }, 'm': () => { event.preventDefault(); this.toggleMute(); }, 'M': () => { event.preventDefault(); this.toggleMute(); }, 'Escape': () => { event.preventDefault(); this.exitFullscreen(); } }; const handler = shortcuts[event.key]; if (handler) { handler(); } } // ===== PUBLIC API ===== destroy() { // Clean up event listeners and timeouts if (this.hideControlsTimeout) { clearTimeout(this.hideControlsTimeout); } // Remove keyboard event listener document.removeEventListener('keydown', this.handleKeyboardShortcut); console.log('📺 BaseVideoPlayer destroyed'); } // Getters for external access get duration() { return this.videoElement ? this.videoElement.duration : 0; } get currentTime() { return this.videoElement ? this.videoElement.currentTime : 0; } get isPaused() { return this.videoElement ? this.videoElement.paused : true; } } // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = BaseVideoPlayer; } // Make available globally for browser use if (typeof window !== 'undefined') { window.BaseVideoPlayer = BaseVideoPlayer; }