// Video Player Module // Handles Video.js initialization, stream management, and player controls (function() { 'use strict'; AppModules.require('api', 'ui-controls'); AppModules.register('video-player'); // Stream status management function updateStreamStatus(status, color) { const badge = document.querySelector('.stream-badge'); if (badge) { badge.textContent = status; badge.style.background = color || 'var(--dodgers-red)'; AppLogger.log(`📊 Stream status updated: ${status} (${color || 'red'})`); } } // Video.js player initialization function initializePlayer() { AppLogger.log('đŸŽŦ Starting Video.js initialization...'); AppState.player = videojs('video-player', { html5: { vhs: { overrideNative: true, withCredentials: false, handlePartialData: true, smoothQualityChange: true, allowSeeksWithinUnsafeLiveWindow: true, handleManifestRedirects: true, useBandwidthFromLocalStorage: true, maxPlaylistRetries: 10, blacklistDuration: 5, bandwidth: 4194304 } }, liveui: true, liveTracker: { trackingThreshold: 30, liveTolerance: 15 }, errorDisplay: false, responsive: false, controlBar: { volumePanel: { inline: false }, pictureInPictureToggle: true } }); // Volume enforcement and restoration setupVolumeManagement(); // Stream status checking setupStreamMonitoring(); // Error handling setupPlayerErrorHandling(); // Event handlers setupPlayerEvents(); AppLogger.log('Video.js player initialized successfully'); } // Volume management system function setupVolumeManagement() { AppState.player.ready(function() { AppLogger.log('Player is ready'); // Immediate volume fix enforceSavedVolume(); // Load saved volume or set default const savedVolume = localStorage.getItem('playerVolume'); if (savedVolume !== null) { const volumeValue = parseFloat(savedVolume); AppState.player.volume(volumeValue); AppLogger.log('🔊 Restored saved volume:', volumeValue); } else { AppState.player.volume(AppConfig.video.defaultVolume); AppLogger.log('🔊 Set default volume:', AppConfig.video.defaultVolume); } // Volume monitoring and enforcement AppState.player.on('volumechange', function() { if (!enforceSavedVolume()) { // User's intentional change - save it if (!AppState.player.muted()) { const newVolume = AppState.player.volume(); localStorage.setItem('playerVolume', newVolume); AppLogger.log('💾 Saved volume setting:', newVolume); } } }); // PROACTIVE STREAM STATUS CHECK checkInitialStreamStatus(); }); } // Volume enforcement function function enforceSavedVolume() { const currentVolume = AppState.player.volume(); if (currentVolume === 1.0) { const savedVolume = parseFloat(localStorage.getItem('playerVolume') || AppConfig.video.defaultVolume.toString()); if (savedVolume !== 1.0) { AppLogger.log('đŸšĢ BLOCKED: Volume reset to 100% - forcing back to:', savedVolume); AppState.player.volume(savedVolume); return true; } } return false; } // Stream status and monitoring function checkInitialStreamStatus() { AppLogger.log('🔍 Checking initial stream status...'); API.checkStreamStatus() .then(isOnline => { if (!isOnline) { AppLogger.log('🔄 Stream offline on load - switching to placeholder immediately'); switchToPlaceholder(); } else { AppLogger.log('✅ Stream online on load - starting HLS playback'); attemptHLSPlayback(); startStreamMonitoring(); } }); } function startStreamMonitoring() { if (!AppState.recoveryInterval) { AppLogger.log('đŸ‘ī¸ Starting stream monitoring...'); AppState.recoveryInterval = setInterval(() => { if (AppState.streamMode !== 'placeholder') { API.checkStreamStatus() .then(data => { if (!data.online) { AppLogger.log('âš ī¸ Stream went offline during playback - switching to placeholder'); switchToPlaceholder(); } }) .catch(error => { AppLogger.error('❗ Stream monitoring check failed:', error); }); } }, AppConfig.video.streamMonitoringInterval); } } // HLS playback attempt function attemptHLSPlayback() { AppLogger.log('đŸŽŦ Attempting HLS stream playback...'); var playPromise = AppState.player.play(); if (playPromise !== undefined) { playPromise.catch(function(error) { AppLogger.log('Auto-play was prevented:', error); UIControls.showToast('Click play to start stream'); }); } } // Player error handling function setupPlayerErrorHandling() { // Warning events from Video.js AppState.player.on('warning', function(event) { AppLogger.log('VideoJS warning:', event.detail); if (!AppState.isRetrying && !AppState.isRecovering && AppState.streamMode === 'live') { AppState.errorRetryCount++; AppLogger.log('Warning count now:', AppState.errorRetryCount); if (AppState.errorRetryCount >= 3) { AppLogger.log('Threshold reached, switching to placeholder'); switchToPlaceholder(); } } }); // Error handling AppState.player.on('error', function() { var error = AppState.player.error(); AppLogger.log('Player error:', error); if (AppState.isRetrying || AppState.streamMode === 'placeholder') return; if (error && (error.code === 2 || error.code === 4)) { if (AppState.errorRetryCount < AppConfig.video.maxRetries) { AppState.isRetrying = true; AppState.errorRetryCount++; AppLogger.log(`Attempting recovery (${AppState.errorRetryCount}/${AppConfig.video.maxRetries})...`); updateStreamStatus('RECONNECTING...', '#ffc107'); setTimeout(() => { AppState.player.error(null); AppState.player.pause(); AppState.player.reset(); // Restore volume enforceSavedVolume(); // Try playing again if (AppState.player.paused()) { AppState.player.play().catch(e => AppLogger.log('Play attempt failed:', e)); } AppState.isRetrying = false; // Soft reload fallback setTimeout(() => { if (AppState.player.error()) { AppLogger.log('Soft reloading source...'); const currentTime = AppState.player.currentTime(); AppState.player.src(AppState.player.currentSrc()); AppState.player.play().catch(e => AppLogger.log('Play after reload failed:', e)); } }, 3000); }, 1000 * Math.min(AppState.errorRetryCount, 3)); } else { AppLogger.error('Max retries exceeded, switching to placeholder video'); switchToPlaceholder(); } } else if (error && error.code === 3) { AppLogger.error('Decode error, switching to placeholder:', error); switchToPlaceholder(); } }); } // Player event setup function setupPlayerEvents() { // Reset error count on successful playback AppState.player.on('loadeddata', function() { AppState.errorRetryCount = 0; AppState.isRetrying = false; if (AppState.streamMode === 'live') { updateStreamStatus('LIVE', 'var(--dodgers-red)'); } }); AppState.player.on('playing', function() { AppState.errorRetryCount = 0; AppState.isRetrying = false; if (AppState.streamMode === 'live') { updateStreamStatus('LIVE', 'var(--dodgers-red)'); } }); AppState.player.on('waiting', function() { AppLogger.log('Player is buffering...'); if (!AppState.isRetrying && AppState.errorRetryCount === 0 && AppState.streamMode === 'live') { updateStreamStatus('BUFFERING...', '#ffc107'); } // Extended buffering detection const waitingStartTime = Date.now(); let offlineConfirmCount = 0; function checkBufferingStatus(attempt = 1, maxAttempts = 5) { API.checkStreamStatus() .then(data => { if (!data.online) { offlineConfirmCount++; AppLogger.log(`âš ī¸ Confirmed offline - Attempts: ${offlineConfirmCount}/${maxAttempts}`); if (offlineConfirmCount >= 2 || attempt >= maxAttempts) { AppLogger.log('❌ Stream confirmed offline - switching to placeholder'); switchToPlaceholder(); return; } } else { offlineConfirmCount = 0; } if (offlineConfirmCount < 2 && attempt < maxAttempts) { setTimeout(() => checkBufferingStatus(attempt + 1, maxAttempts), 1500); } }) .catch(apiError => { AppLogger.log('❗ API check failed:', apiError); if (attempt < maxAttempts) { setTimeout(() => checkBufferingStatus(attempt + 1, maxAttempts), 1500); } }); } checkBufferingStatus(); }); // Context menu prevention document.getElementById('video-player').addEventListener('contextmenu', function(e) { e.preventDefault(); return false; }); AppState.player.ready(function() { const videoElement = AppState.player.el().querySelector('video'); if (videoElement) { videoElement.addEventListener('contextmenu', function(e) { e.preventDefault(); return false; }); } }); } // Stream switching functions function switchToPlaceholder() { if (AppState.streamMode === 'placeholder') { AppLogger.log('âš ī¸ Already in placeholder mode, ignoring switch request'); return; } AppLogger.log('🔄 START: Switching to placeholder video'); AppLogger.log('📊 State before switch:', { streamMode: AppState.streamMode, errorRetryCount: AppState.errorRetryCount, isRecovering: AppState.isRecovering }); AppState.streamMode = 'placeholder'; if (AppState.recoveryInterval) { clearInterval(AppState.recoveryInterval); AppState.recoveryInterval = null; } if (AppState.recoveryTimeout) { clearTimeout(AppState.recoveryTimeout); AppState.recoveryTimeout = null; } updateStreamStatus('SWITCHING...', '#ffc107'); try { AppState.player.pause(); AppState.player.reset(); enforceSavedVolume(); setTimeout(() => { AppState.player.src({ src: 'placeholder.mp4', type: 'video/mp4' }); AppState.player.load(); setTimeout(() => enforceSavedVolume(), 200); AppState.player.play() .then(() => { AppLogger.log('Placeholder video started successfully'); const savedVolume = localStorage.getItem('playerVolume') || AppConfig.video.defaultVolume.toString(); const targetVolume = parseFloat(savedVolume); AppState.player.volume(targetVolume); updateStreamStatus('PLACEHOLDER', '#17a2b8'); UIControls.showToast('Stream offline - showing placeholder video'); startRecoveryChecks(); }) .catch(e => { AppLogger.log('Placeholder play failed:', e); updateStreamStatus('PLACEHOLDER', '#17a2b8'); UIControls.showToast('Stream offline - showing placeholder video'); startRecoveryChecks(); }); }, 1000); } catch (e) { AppLogger.log('Error switching to placeholder:', e); updateStreamStatus('PLACEHOLDER', '#17a2b8'); startRecoveryChecks(); } } function switchToLive() { if (AppState.streamMode === 'live') { AppLogger.log('âš ī¸ Already in live mode, ignoring switch request'); return; } AppLogger.log('🔄 START: Switching back to live stream'); AppLogger.log('📊 State before switch:', { streamMode: AppState.streamMode, isRecovering: AppState.isRecovering }); AppState.streamMode = 'live'; AppState.isRecovering = false; if (AppState.recoveryInterval) { clearInterval(AppState.recoveryInterval); AppState.recoveryInterval = null; } updateStreamStatus('SWITCHING...', '#ffc107'); try { AppState.player.pause(); AppState.player.reset(); enforceSavedVolume(); setTimeout(() => { AppState.player.src({ src: '?proxy=stream', type: 'application/x-mpegURL' }); AppState.errorRetryCount = 0; AppState.isRetrying = false; AppState.player.load(); setTimeout(() => enforceSavedVolume(), 200); AppState.player.play() .then(() => { AppLogger.log('Live stream started successfully'); const savedVolume = localStorage.getItem('playerVolume') || AppConfig.video.defaultVolume.toString(); const targetVolume = parseFloat(savedVolume); AppState.player.volume(targetVolume); updateStreamStatus('RECONNECTING...', '#ffc107'); UIControls.showToast('Stream back online - switching to live'); setTimeout(() => { if (AppState.streamMode === 'live') { updateStreamStatus('LIVE', 'var(--dodgers-red)'); } }, 2000); }) .catch(e => { AppLogger.log('Live stream play failed:', e); const savedVolume = localStorage.getItem('playerVolume') || AppConfig.video.defaultVolume.toString(); const targetVolume = parseFloat(savedVolume); AppState.player.volume(targetVolume); updateStreamStatus('LIVE', 'var(--dodgers-red)'); UIControls.showToast('Stream back online - switching to live'); }); }, 1000); } catch (e) { AppLogger.log('Error switching to live:', e); AppState.player.src({ src: '?proxy=stream', type: 'application/x-mpegURL' }); AppState.player.load(); } } // Recovery check system function startRecoveryChecks() { AppLogger.log('🔍 Starting fast recovery checks every 2 seconds for 5 checks'); let recoveryCheckCount = 0; let recoveryTimeout; function checkRecovery() { if (AppState.streamMode !== 'placeholder' || recoveryCheckCount >= AppConfig.video.recoveryCheckCount) { if (recoveryCheckCount >= AppConfig.video.recoveryCheckCount) { AppLogger.log('âšī¸ Fast recovery checks complete - stream still offline'); } return; } recoveryCheckCount++; AppLogger.log(`🔍 Recovery check ${recoveryCheckCount}/${AppConfig.video.recoveryCheckCount}...`); API.checkStreamStatus() .then(data => { if (data.online && AppState.streamMode === 'placeholder') { AppLogger.log('✅ Stream back online during recovery check - switching'); AppState.isRecovering = true; switchToLive(); } else { if (recoveryCheckCount < AppConfig.video.recoveryCheckCount) { recoveryTimeout = setTimeout(checkRecovery, AppConfig.video.recoveryCheckInterval); } } }) .catch(error => { AppLogger.log(`❗ Recovery check ${recoveryCheckCount} failed:`, error); if (recoveryCheckCount < AppConfig.video.recoveryCheckCount) { recoveryTimeout = setTimeout(checkRecovery, AppConfig.video.recoveryCheckInterval); } }); } checkRecovery(); } // Manual recovery function function manualRecovery() { AppLogger.log('🔄 Manual recovery triggered by user'); AppState.errorRetryCount = 0; AppState.isRetrying = false; UIControls.showToast('🔄 Manual stream refresh triggered'); AppState.player.reset(); AppState.player.error(null); AppState.player.src({ src: '?proxy=stream', type: 'application/x-mpegURL' }); AppState.player.load(); setTimeout(() => { AppState.player.play().catch(e => AppLogger.log('Manual recovery play failed:', e)); }, 500); } // Public API window.VideoPlayer = { initializePlayer: initializePlayer, switchToPlaceholder: switchToPlaceholder, switchToLive: switchToLive, manualRecovery: manualRecovery, updateStreamStatus: updateStreamStatus }; // Initialize player when DOM and Video.js are ready document.addEventListener('DOMContentLoaded', function() { // Check if Video.js is loaded if (typeof videojs === 'undefined') { AppLogger.error('Video.js not found - video player will not initialize'); return; } // Small delay to ensure DOM is fully ready setTimeout(initializePlayer, 50); }); AppLogger.log('Video Player module loaded'); })();