/** * Video Player Manager * Handles video playback, management, and integration with game modes */ class VideoPlayerManager { constructor() { this.currentPlayer = null; this.backgroundPlayer = null; this.overlayPlayer = null; this.videoLibrary = { background: [], task: [], reward: [], punishment: [] }; this.settings = { enabled: true, backgroundVideos: true, taskVideos: true, autoplay: true, loop: true, volume: 0.3, muted: false, playbackRate: 1.0, showControls: false, fadeTransitions: true }; this.isInitialized = false; this.loadSettings(); } /** * Initialize the video player system */ async init() { console.log('🎬 Initializing Video Player Manager...'); try { this.createVideoPlayers(); this.bindEvents(); await this.scanVideoLibrary(); this.isInitialized = true; console.log('✅ Video Player Manager initialized successfully'); } catch (error) { console.error('❌ Failed to initialize Video Player Manager:', error); } } /** * Create video player elements */ createVideoPlayers() { // Background video player (behind content) this.backgroundPlayer = this.createVideoElement('background-video-player', { loop: true, muted: true, autoplay: true, style: ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; object-fit: cover; z-index: -1; opacity: 0.3; pointer-events: none; ` }); // Task video player (in content area) this.currentPlayer = this.createVideoElement('task-video-player', { controls: this.settings.showControls, style: ` max-width: 100%; max-height: 70vh; border-radius: 8px; display: none; ` }); // Overlay video player (for popups/punishments) this.overlayPlayer = this.createVideoElement('overlay-video-player', { autoplay: true, style: ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 80vw; max-height: 80vh; z-index: 10000; border-radius: 8px; box-shadow: 0 0 50px rgba(0,0,0,0.8); display: none; ` }); // Add players to DOM document.body.appendChild(this.backgroundPlayer); document.body.appendChild(this.overlayPlayer); // Add task player to game container const gameContainer = document.querySelector('.game-container') || document.body; gameContainer.appendChild(this.currentPlayer); } /** * Create a video element with specified attributes */ createVideoElement(id, attributes = {}) { const video = document.createElement('video'); video.id = id; // Set default attributes video.preload = 'metadata'; video.playsInline = true; video.webkit = { playsinline: true }; // Apply custom attributes Object.entries(attributes).forEach(([key, value]) => { if (key === 'style') { video.style.cssText = value; } else { video[key] = value; } }); // Add event listeners video.addEventListener('loadstart', () => console.log(`🎬 Loading video: ${video.src}`)); video.addEventListener('canplay', () => console.log(`✅ Video ready: ${video.src}`)); video.addEventListener('error', (e) => console.error(`❌ Video error: ${e.message}`)); video.addEventListener('ended', (e) => this.handleVideoEnd(e)); return video; } /** * Scan video directory for available videos */ async scanVideoLibrary() { console.log('📁 Scanning video library...'); const categories = ['background', 'task', 'reward', 'punishment']; for (const category of categories) { try { // In Electron environment, use the desktop file manager if (window.electronAPI && window.desktopFileManager) { // Use unified video library - get all videos and filter by category if needed const allVideos = window.desktopFileManager.getAllVideos(); // For now, just use all videos for any category since we removed categories // In the future, we could add tags or other filtering mechanisms this.videoLibrary[category] = allVideos; } else { // In browser environment, try to load from unified storage const unifiedData = JSON.parse(localStorage.getItem('unifiedVideoLibrary') || '{}'); this.videoLibrary[category] = unifiedData.allVideos || []; } console.log(`📹 Found ${this.videoLibrary[category].length} ${category} videos (from unified library)`); } catch (error) { console.warn(`⚠️ Could not load ${category} videos:`, error); this.videoLibrary[category] = []; } } } /** * Get known videos for browser environment (fallback) */ getKnownVideos(category) { // This would need to be populated manually in browser environment // or through a manifest file return []; } /** * Reload video files from storage (called when new videos are imported) */ async loadVideoFiles() { console.log('🔄 Reloading video library...'); await this.scanVideoLibrary(); } /** * Play background video */ async playBackgroundVideo(videoPath = null) { if (!this.settings.backgroundVideos || !this.backgroundPlayer) return; try { if (!videoPath) { const backgroundVideos = this.videoLibrary.background; if (backgroundVideos.length === 0) return; videoPath = backgroundVideos[Math.floor(Math.random() * backgroundVideos.length)]; } const fullPath = this.getVideoPath(videoPath); console.log(`🎬 Playing background video: ${fullPath}`); if (this.settings.fadeTransitions) { await this.fadeOut(this.backgroundPlayer); } this.backgroundPlayer.src = fullPath; this.backgroundPlayer.volume = this.settings.volume * 0.5; // Background videos quieter this.backgroundPlayer.muted = this.settings.muted; this.backgroundPlayer.playbackRate = this.settings.playbackRate; await this.backgroundPlayer.play(); if (this.settings.fadeTransitions) { await this.fadeIn(this.backgroundPlayer); } } catch (error) { console.error('❌ Failed to play background video:', error); } } /** * Play task video */ async playTaskVideo(videoPath, container = null) { if (!this.settings.taskVideos || !this.currentPlayer) return; try { const fullPath = this.getVideoPath(videoPath); console.log(`🎬 Playing task video: ${fullPath}`); // Position player in specified container if (container) { container.appendChild(this.currentPlayer); } this.currentPlayer.src = fullPath; this.currentPlayer.volume = this.settings.volume; this.currentPlayer.muted = this.settings.muted; this.currentPlayer.controls = this.settings.showControls; this.currentPlayer.style.display = 'block'; if (this.settings.autoplay) { await this.currentPlayer.play(); } if (this.settings.fadeTransitions) { await this.fadeIn(this.currentPlayer); } } catch (error) { console.error('❌ Failed to play task video:', error); } } /** * Play overlay video (for punishments/rewards) */ async playOverlayVideo(videoPath, options = {}) { if (!this.overlayPlayer) return; try { const fullPath = this.getVideoPath(videoPath); console.log(`🎬 Playing overlay video: ${fullPath}`); const { duration = null, closeable = false, onComplete = null, volume = this.settings.volume } = options; this.overlayPlayer.src = fullPath; this.overlayPlayer.volume = volume; this.overlayPlayer.muted = this.settings.muted; this.overlayPlayer.style.display = 'block'; // Add close button if closeable if (closeable) { this.addCloseButton(this.overlayPlayer); } await this.overlayPlayer.play(); if (this.settings.fadeTransitions) { await this.fadeIn(this.overlayPlayer); } // Auto-close after duration if (duration) { setTimeout(() => { this.closeOverlayVideo(); if (onComplete) onComplete(); }, duration); } } catch (error) { console.error('❌ Failed to play overlay video:', error); } } /** * Close overlay video */ async closeOverlayVideo() { if (!this.overlayPlayer) return; if (this.settings.fadeTransitions) { await this.fadeOut(this.overlayPlayer); } this.overlayPlayer.pause(); this.overlayPlayer.style.display = 'none'; this.overlayPlayer.src = ''; // Remove close button if exists const closeBtn = this.overlayPlayer.nextElementSibling; if (closeBtn && closeBtn.classList.contains('video-close-btn')) { closeBtn.remove(); } } /** * Stop all videos */ stopAllVideos() { [this.backgroundPlayer, this.currentPlayer, this.overlayPlayer].forEach(player => { if (player) { player.pause(); player.currentTime = 0; } }); } /** * Pause all videos */ pauseAllVideos() { [this.backgroundPlayer, this.currentPlayer, this.overlayPlayer].forEach(player => { if (player && !player.paused) { player.pause(); } }); } /** * Resume all videos */ resumeAllVideos() { [this.backgroundPlayer, this.currentPlayer, this.overlayPlayer].forEach(player => { if (player && player.paused) { player.play().catch(console.error); } }); } /** * Get random video from category */ getRandomVideo(category) { const videos = this.videoLibrary[category] || []; if (videos.length === 0) return null; // Filter out disabled videos const disabledVideos = JSON.parse(localStorage.getItem('disabledVideos') || '[]'); const enabledVideos = videos.filter(video => { const videoPath = typeof video === 'string' ? video : video.path; return !disabledVideos.includes(videoPath); }); if (enabledVideos.length === 0) { console.warn('⚠️ All videos in category disabled, using full list'); return videos[Math.floor(Math.random() * videos.length)]; } return enabledVideos[Math.floor(Math.random() * enabledVideos.length)]; } /** * Get all enabled videos from a category (filters out disabled videos) */ getEnabledVideos(category) { const videos = this.videoLibrary[category] || []; if (videos.length === 0) return []; // Filter out disabled videos const disabledVideos = JSON.parse(localStorage.getItem('disabledVideos') || '[]'); const enabledVideos = videos.filter(video => { const videoPath = typeof video === 'string' ? video : video.path; return !disabledVideos.includes(videoPath); }); return enabledVideos; } /** * Get full path for video */ getVideoPath(videoPath) { // Handle null/undefined if (!videoPath) { console.error('❌ getVideoPath received null/undefined videoPath'); return ''; } // Handle objects (extract path property) if (typeof videoPath === 'object') { if (videoPath.path) { videoPath = videoPath.path; } else { console.error('❌ getVideoPath received object without path property:', videoPath); return ''; } } // Ensure it's a string if (typeof videoPath !== 'string') { console.error('❌ getVideoPath received non-string:', typeof videoPath, videoPath); return ''; } // Already a full URL if (videoPath.startsWith('http') || videoPath.startsWith('file:')) { return videoPath; } // Absolute path (Windows: C:\... or E:\..., Unix: /...) if (videoPath.match(/^[A-Z]:\\/i) || videoPath.startsWith('/')) { // Convert to file:// URL for Electron const normalizedPath = videoPath.replace(/\\/g, '/'); return `file:///${normalizedPath}`; } // Relative path - add video/ prefix return `video/${videoPath}`; } /** * Add close button to video */ addCloseButton(videoElement) { const closeBtn = document.createElement('button'); closeBtn.className = 'video-close-btn'; closeBtn.innerHTML = '✕'; closeBtn.style.cssText = ` position: fixed; top: calc(${videoElement.style.top} - 30px); right: calc(50% - ${videoElement.offsetWidth/2}px + ${videoElement.offsetWidth}px - 30px); z-index: 10001; background: rgba(0,0,0,0.8); color: white; border: none; width: 30px; height: 30px; border-radius: 50%; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; `; closeBtn.addEventListener('click', () => this.closeOverlayVideo()); videoElement.parentNode.insertBefore(closeBtn, videoElement.nextSibling); } /** * Fade in animation */ fadeIn(element, duration = 500) { return new Promise(resolve => { element.style.opacity = '0'; element.style.transition = `opacity ${duration}ms ease-in-out`; requestAnimationFrame(() => { element.style.opacity = element.id === 'background-video-player' ? '0.3' : '1'; setTimeout(resolve, duration); }); }); } /** * Fade out animation */ fadeOut(element, duration = 500) { return new Promise(resolve => { element.style.transition = `opacity ${duration}ms ease-in-out`; element.style.opacity = '0'; setTimeout(resolve, duration); }); } /** * Handle video end events */ handleVideoEnd(event) { const video = event.target; if (video === this.backgroundPlayer && this.settings.loop) { // Start next background video this.playBackgroundVideo(); } else if (video === this.overlayPlayer) { // Auto-close overlay videos this.closeOverlayVideo(); } } /** * Update settings */ updateSettings(newSettings) { this.settings = { ...this.settings, ...newSettings }; this.saveSettings(); this.applySettings(); } /** * Apply current settings to players */ applySettings() { [this.backgroundPlayer, this.currentPlayer, this.overlayPlayer].forEach(player => { if (player) { player.volume = this.settings.volume; player.muted = this.settings.muted; player.playbackRate = this.settings.playbackRate; if (player === this.currentPlayer) { player.controls = this.settings.showControls; } } }); } /** * Load settings from localStorage */ loadSettings() { const saved = localStorage.getItem('videoPlayerSettings'); if (saved) { this.settings = { ...this.settings, ...JSON.parse(saved) }; } } /** * Save settings to localStorage */ saveSettings() { localStorage.setItem('videoPlayerSettings', JSON.stringify(this.settings)); } /** * Bind event listeners */ bindEvents() { // Game pause/resume integration document.addEventListener('gamepaused', () => this.pauseAllVideos()); document.addEventListener('gameresumed', () => this.resumeAllVideos()); // Cleanup on page unload window.addEventListener('beforeunload', () => this.stopAllVideos()); } /** * Get video library info */ getLibraryInfo() { return { background: this.videoLibrary.background.length, task: this.videoLibrary.task.length, reward: this.videoLibrary.reward.length, punishment: this.videoLibrary.punishment.length, total: Object.values(this.videoLibrary).flat().length }; } } // Create global instance window.videoPlayerManager = new VideoPlayerManager(); // Auto-initialize when DOM is ready document.addEventListener('DOMContentLoaded', () => { if (window.videoPlayerManager) { window.videoPlayerManager.init(); } }); console.log('🎬 Video Player Manager loaded');