563 lines
20 KiB
JavaScript
563 lines
20 KiB
JavaScript
/**
|
|
* 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;
|
|
} |