diff --git a/UI_UPDATE.MD b/UI_UPDATE.MD index 7e8623e..3bff8cb 100644 --- a/UI_UPDATE.MD +++ b/UI_UPDATE.MD @@ -54,40 +54,40 @@ Always come back and update UI_UPDATE.MD once complete with task and task item. ### Sub-task 1.2: JavaScript Separation and Organization #### 1.2.1: JavaScript Code Identification -- [x] Locate all JavaScript code within ` - - - - - + diff --git a/ui-controls.js b/ui-controls.js deleted file mode 100644 index 9161393..0000000 --- a/ui-controls.js +++ /dev/null @@ -1,282 +0,0 @@ -// UI Controls Module -// Handles UI toggle functions, keyboard shortcuts, notifications, and DOM utilities - -(function() { - 'use strict'; - - AppModules.require('api'); - AppModules.register('ui-controls'); - - // Toast notification system - function showToast(message, duration = AppConfig.ui.toastDuration) { - const toast = document.getElementById('toast'); - if (!toast) return; - - toast.textContent = message; - toast.classList.add('show'); - - if (duration > 0) { - setTimeout(() => { - toast.classList.remove('show'); - }, duration); - } - } - - function hideToast() { - const toast = document.getElementById('toast'); - if (toast) { - toast.classList.remove('show'); - } - } - - // Notification badge management - function updateNotificationBadge() { - const badge = document.getElementById('notificationBadge'); - - if (badge) { - if (AppState.unreadCount > 0) { - badge.textContent = AppState.unreadCount > 99 ? '99+' : AppState.unreadCount; - badge.classList.add('show'); - } else { - badge.classList.remove('show'); - } - } - } - - // Chat toggle functionality - function toggleChat() { - const chatSection = document.getElementById('chatSection'); - const videoSection = document.getElementById('videoSection'); - const toggleBtn = document.getElementById('chatToggleText'); - - if (!chatSection || !videoSection || !toggleBtn) return; - - AppState.chatCollapsed = !AppState.chatCollapsed; - - if (AppState.chatCollapsed) { - chatSection.classList.add('collapsed'); - videoSection.classList.add('expanded'); - toggleBtn.textContent = 'Show Chat'; - } else { - chatSection.classList.remove('collapsed'); - videoSection.classList.remove('expanded'); - toggleBtn.textContent = 'Hide Chat'; - // Clear unread count when reopening - AppState.unreadCount = 0; - updateNotificationBadge(); - } - } - - // Viewer count update - function updateViewerCount(count) { - const viewerElement = document.getElementById('viewerCount'); - if (viewerElement) { - viewerElement.textContent = count + (count === 1 ? ' viewer' : ' viewers'); - } - } - - // Connection status dot - function updateConnectionStatus(online) { - const statusDot = document.getElementById('statusDot'); - const statusText = document.getElementById('statusText'); - - if (statusDot && statusText) { - if (online) { - statusDot.classList.remove('offline'); - statusText.textContent = 'Connected'; - } else { - statusDot.classList.add('offline'); - statusText.textContent = 'Reconnecting...'; - } - } - } - - // Keyboard shortcuts handler - function handleKeyboardShortcuts(event) { - // Skip if typing in input field - if (event.target.matches('input, textarea')) { - return; - } - - switch (event.key) { - case 'c': - case 'C': - // Toggle chat - event.preventDefault(); - toggleChat(); - break; - case 'f': - case 'F': - // Toggle fullscreen - event.preventDefault(); - toggleFullscreen(); - break; - case '/': - // Focus chat input - event.preventDefault(); - const messageInput = document.getElementById('messageInput'); - if (messageInput) { - messageInput.focus(); - } - break; - case 'p': - case 'P': - // Toggle picture-in-picture - event.preventDefault(); - togglePictureInPicture(); - break; - } - } - - // Fullscreen functionality - function toggleFullscreen() { - if (!AppState.player) return; - - if (AppState.player.isFullscreen()) { - AppState.player.exitFullscreen(); - } else { - AppState.player.requestFullscreen(); - } - } - - // Picture-in-picture functionality - function togglePictureInPicture() { - const video = document.getElementById('video-player'); - if (!video || !video.requestPictureInPicture) return; - - if (document.pictureInPictureElement) { - document.exitPictureInPicture(); - } else { - video.requestPictureInPicture(); - } - } - - // Sound notification for new messages - function playNotificationSound() { - if (!AppState.soundEnabled || !document.hidden) return; - - try { - const audioContext = new (window.AudioContext || window.webkitAudioContext)(); - const oscillator = audioContext.createOscillator(); - const gainNode = audioContext.createGain(); - - oscillator.connect(gainNode); - gainNode.connect(audioContext.destination); - - oscillator.frequency.value = 800; - oscillator.type = 'sine'; - gainNode.gain.value = 0.1; - - oscillator.start(audioContext.currentTime); - oscillator.stop(audioContext.currentTime + 0.1); - } catch(e) { - AppLogger.log('Audio notification failed:', e); - } - } - - // Mobile responsive behavior - function handleWindowResize() { - const isMobile = window.innerWidth <= AppConfig.ui.mobileBreakpoint; - - if (isMobile && !AppState.chatCollapsed) { - // Auto-collapse chat on mobile for better viewing - toggleChat(); - } - } - - // Page visibility API handler - function handleVisibilityChange() { - if (!document.hidden && !AppState.chatCollapsed) { - // Clear unread count when page becomes visible - AppState.unreadCount = 0; - updateNotificationBadge(); - } - } - - // DOM utility functions - const DOMUtils = { - // Add event listener with proper cleanup tracking - addEvent: function(element, event, handler) { - if (!element) return; - - element.addEventListener(event, handler); - - // Store handler for potential cleanup - if (!element._handlers) { - element._handlers = new Map(); - } - if (!element._handlers.has(event)) { - element._handlers.set(event, []); - } - element._handlers.get(event).push(handler); - }, - - // Remove event listeners (for cleanup) - removeEvent: function(element, event, handler) { - if (!element || !element._handlers || !element._handlers.has(event)) return; - - const handlers = element._handlers.get(event); - const index = handlers.indexOf(handler); - if (index > -1) { - handlers.splice(index, 1); - element.removeEventListener(event, handler); - } - }, - - // Get element with optional error logging - getElement: function(id, required = false) { - const element = document.getElementById(id); - if (required && !element) { - AppLogger.error(`Required element with id '${id}' not found`); - } - return element; - }, - - // Add/remove classes safely - addClass: function(element, className) { - if (element) element.classList.add(className); - }, - - removeClass: function(element, className) { - if (element) element.classList.remove(className); - }, - - toggleClass: function(element, className) { - if (element) element.classList.toggle(className); - } - }; - - // Initialize event listeners - function initializeEventListeners() { - // Keyboard shortcuts - DOMUtils.addEvent(document, 'keydown', handleKeyboardShortcuts); - - // Window resize for mobile responsiveness - DOMUtils.addEvent(window, 'resize', handleWindowResize); - - // Page visibility for notification clearing - DOMUtils.addEvent(document, 'visibilitychange', handleVisibilityChange); - - AppLogger.log('UI controls event listeners initialized'); - } - - // Public API - window.UIControls = { - showToast: showToast, - hideToast: hideToast, - toggleChat: toggleChat, - toggleFullscreen: toggleFullscreen, - togglePictureInPicture: togglePictureInPicture, - updateViewerCount: updateViewerCount, - updateConnectionStatus: updateConnectionStatus, - updateNotificationBadge: updateNotificationBadge, - playNotificationSound: playNotificationSound, - DOMUtils: DOMUtils - }; - - // Initialize when DOM is ready - document.addEventListener('DOMContentLoaded', initializeEventListeners); - - AppLogger.log('UI Controls module loaded'); - -})(); diff --git a/video-player.js b/video-player.js deleted file mode 100644 index 085564a..0000000 --- a/video-player.js +++ /dev/null @@ -1,540 +0,0 @@ -// 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'); - -})();