Complete 1.2 JavaScript Separation and Organization - modularize all inline JavaScript into 6 dedicated modules (app.js, api.js, ui-controls.js, chat.js, video-player.js) for better maintainability and code organization

This commit is contained in:
VinnyNC 2025-09-28 22:26:21 -04:00
parent e87b1af062
commit 2d807852e8
5 changed files with 1574 additions and 0 deletions

222
api.js Normal file
View file

@ -0,0 +1,222 @@
// API Communication Module
// Handles all AJAX/Fetch calls and API interactions
(function() {
'use strict';
AppModules.register('api');
const API_BASE_URL = '';
// Generic fetch wrapper with error handling and retries
function apiRequest(endpoint, options = {}) {
const url = endpoint.startsWith('?') ? endpoint : `${API_BASE_URL}${endpoint}`;
return fetch(url, {
method: options.method || 'GET',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...options.headers
},
body: options.body
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.catch(error => {
AppLogger.error(`API request to ${endpoint} failed:`, error);
throw error;
});
}
// Stream status checking
function checkStreamStatus() {
return apiRequest('?api=stream_status')
.then(data => {
AppLogger.log('Stream status check:', data);
return data.online === true;
})
.catch(error => {
AppLogger.error('Stream status check failed:', error);
return false; // Assume offline if check fails
});
}
// User management
function getUserId() {
return apiRequest('', {
method: 'POST',
body: 'action=get_user_id'
})
.then(data => {
if (data.success) {
AppState.userId = data.user_id;
AppState.isAdmin = data.is_admin || false;
const userIdElement = document.getElementById('userId');
if (userIdElement) {
userIdElement.textContent = '#' + AppState.userId;
if (AppState.isAdmin) {
userIdElement.classList.add('admin');
userIdElement.textContent = '👑 #' + AppState.userId;
const adminControls = document.getElementById('adminControls');
if (adminControls) adminControls.style.display = 'flex';
}
}
return data;
} else {
throw new Error('Failed to get user ID');
}
});
}
// Heartbeat functionality
function sendHeartbeat() {
const nicknameValue = document.getElementById('nickname')?.value || 'Anonymous';
return apiRequest('', {
method: 'POST',
body: `action=heartbeat&nickname=${encodeURIComponent(nicknameValue)}`
})
.then(data => {
if (data.success && data.viewer_count !== undefined) {
AppState.viewerCount = data.viewer_count;
updateViewerCount(data.viewer_count);
}
})
.catch(error => {
AppLogger.error('Heartbeat failed:', error);
});
}
// Chat message operations
function sendMessage(nickname, message) {
if (!nickname || !message) {
throw new Error('Nickname and message are required');
}
const body = `action=send&nickname=${encodeURIComponent(nickname)}&message=${encodeURIComponent(message)}`;
return apiRequest('', {
method: 'POST',
body: body
});
}
function fetchMessages() {
return apiRequest('', {
method: 'POST',
body: `action=fetch&last_id=${encodeURIComponent(AppState.lastMessageId)}`
})
.then(data => {
if (data.success) {
// Update viewer count
if (data.viewer_count !== undefined) {
AppState.viewerCount = data.viewer_count;
updateViewerCount(data.viewer_count);
}
return data;
} else {
throw new Error('Failed to fetch messages');
}
})
.catch(error => {
AppLogger.error('Fetch messages failed:', error);
updateConnectionStatus(false);
throw error;
});
}
// Admin functions
function deleteMessage(messageId) {
if (!AppState.isAdmin) return Promise.reject(new Error('Admin access required'));
return apiRequest('', {
method: 'POST',
body: `action=delete_message&message_id=${encodeURIComponent(messageId)}`
})
.then(data => {
if (data.success) {
return data;
} else {
throw new Error('Failed to delete message');
}
});
}
function banUser(userId) {
if (!AppState.isAdmin) return Promise.reject(new Error('Admin access required'));
return apiRequest('', {
method: 'POST',
body: `action=ban_user&user_id=${encodeURIComponent(userId)}`
})
.then(data => {
if (data.success) {
return data;
} else {
throw new Error('Failed to ban user');
}
});
}
function clearChat() {
if (!AppState.isAdmin) return Promise.reject(new Error('Admin access required'));
return apiRequest('', {
method: 'POST',
body: 'action=clear_chat'
})
.then(data => {
if (data.success) {
return data;
} else {
throw new Error('Failed to clear chat');
}
});
}
// UI feedback functions (will be moved to ui-controls later, but needed here for now)
function updateViewerCount(count) {
const viewerElement = document.getElementById('viewerCount');
if (viewerElement) {
viewerElement.textContent = count + (count === 1 ? ' viewer' : ' viewers');
}
}
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...';
}
}
}
// Public API
window.API = {
checkStreamStatus: checkStreamStatus,
getUserId: getUserId,
sendHeartbeat: sendHeartbeat,
sendMessage: sendMessage,
fetchMessages: fetchMessages,
deleteMessage: deleteMessage,
banUser: banUser,
clearChat: clearChat
};
AppLogger.log('API module loaded');
})();

116
app.js Normal file
View file

@ -0,0 +1,116 @@
// Core Dodgers Stream Application
// Configuration and module loading
const AppConfig = {
// API configuration
api: {
baseUrl: '',
heartbeatInterval: 5000,
chatPollInterval: 2000,
maxRetries: 3,
retryDelay: 1000
},
// Video player configuration
video: {
defaultVolume: 0.3,
maxRetries: 3,
recoveryTimeout: 10000,
streamMonitoringInterval: 10000,
recoveryCheckCount: 5,
recoveryCheckInterval: 2000
},
// UI configuration
ui: {
animationDuration: 300,
mobileBreakpoint: 768,
toastDuration: 3000
},
// Chat configuration
chat: {
maxMessages: 100,
maxMessageLength: 1000,
maxNicknameLength: 20,
typingTimeout: 1000
}
};
// Global application state
const AppState = {
player: null,
userId: '',
nickname: '',
isAdmin: false,
streamMode: 'live', // 'live' or 'placeholder'
chatCollapsed: false,
unreadCount: 0,
soundEnabled: true,
lastMessageId: '',
allMessages: [],
viewerCount: 0,
errorRetryCount: 0,
isRetrying: false,
isRecovering: false,
recoveryInterval: null,
typingTimer: null,
isTyping: false
};
// Global error handling and logging
const AppLogger = {
log: (message, ...args) => {
console.log(`[${new Date().toLocaleTimeString()}] ${message}`, ...args);
},
error: (message, ...args) => {
console.error(`[${new Date().toLocaleTimeString()}] ERROR: ${message}`, ...args);
},
warn: (message, ...args) => {
console.warn(`[${new Date().toLocaleTimeString()}] WARNING: ${message}`, ...args);
}
};
// Module registry for dependencies
const AppModules = {
loaded: new Set(),
register: function(name) {
this.loaded.add(name);
AppLogger.log(`Module '${name}' registered`);
},
isLoaded: function(name) {
return this.loaded.has(name);
},
require: function(...names) {
for (const name of names) {
if (!this.isLoaded(name)) {
throw new Error(`Required module '${name}' is not loaded`);
}
}
}
};
// Global error handler
window.addEventListener('unhandledrejection', (event) => {
AppLogger.error('Unhandled promise rejection:', event.reason);
});
window.addEventListener('error', (event) => {
AppLogger.error('Unhandled error:', event.error);
});
// Initialize application
document.addEventListener('DOMContentLoaded', () => {
AppLogger.log('Dodgers Stream Theater initializing...');
// Start with basic UI setup
const nicknameInput = document.getElementById('nickname');
if (nicknameInput && localStorage.getItem('chatNickname')) {
nicknameInput.value = localStorage.getItem('chatNickname');
AppState.nickname = nicknameInput.value;
}
AppLogger.log('Core application initialized');
});
AppLogger.log('Core app.js loaded');

414
chat.js Normal file
View file

@ -0,0 +1,414 @@
// Chat System Module
// Handles chat functionality, message handling, and user authentication
(function() {
'use strict';
AppModules.require('api', 'ui-controls');
AppModules.register('chat');
// Initialize chat system
function initializeChat() {
AppLogger.log('Initializing chat system...');
// Get user ID first
API.getUserId()
.then(data => {
AppLogger.log('Chat initialized with user:', AppState.userId, AppState.isAdmin ? '(admin)' : '');
// Show admin toast if applicable
if (AppState.isAdmin) {
UIControls.showToast('Admin mode activated');
}
// Set up nickname handling
const nicknameInput = document.getElementById('nickname');
if (nicknameInput) {
// Load saved nickname
const savedNickname = localStorage.getItem('chatNickname');
if (savedNickname) {
nicknameInput.value = savedNickname;
}
// Handle nickname changes
UIControls.DOMUtils.addEvent(nicknameInput, 'change', function() {
const nickname = this.value.trim();
localStorage.setItem('chatNickname', nickname);
AppState.nickname = nickname;
});
}
// Set up message input handling
const messageInput = document.getElementById('messageInput');
if (messageInput) {
// Enter key to send message
UIControls.DOMUtils.addEvent(messageInput, 'keypress', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// Typing indicator
UIControls.DOMUtils.addEvent(messageInput, 'input', function() {
handleTypingIndicator();
});
}
// Set up send button
const sendButton = document.querySelector('.chat-input button') ||
document.querySelector('#messageInput').nextElementSibling;
if (sendButton && sendButton.tagName === 'BUTTON') {
UIControls.DOMUtils.addEvent(sendButton, 'click', sendMessage);
}
// Start polling for messages
startMessagePolling();
// Send initial heartbeat
API.sendHeartbeat();
AppLogger.log('Chat system initialized successfully');
})
.catch(error => {
AppLogger.error('Failed to initialize chat:', error);
UIControls.showToast('Failed to initialize chat');
});
}
// Send message functionality
function sendMessage() {
const messageInput = document.getElementById('messageInput');
const nicknameInput = document.getElementById('nickname');
const message = messageInput?.value?.trim() || '';
const nickname = nicknameInput?.value?.trim() || '';
if (!nickname) {
if (nicknameInput) {
nicknameInput.style.borderColor = 'var(--dodgers-red)';
nicknameInput.focus();
}
UIControls.showToast('Please enter a nickname');
setTimeout(() => {
if (nicknameInput) nicknameInput.style.borderColor = '#e0e0e0';
}, 2000);
return;
}
if (!message) {
return; // Just return quietly for empty message
}
// Send message via API
API.sendMessage(nickname, message)
.then(data => {
if (data.success) {
if (messageInput) messageInput.value = '';
AppState.nickname = nickname;
// Message will be added automatically by polling
} else if (data.error) {
UIControls.showToast(data.error);
UIControls.updateConnectionStatus(false);
}
})
.catch(error => {
AppLogger.error('Error sending message:', error);
UIControls.showToast('Failed to send message');
UIControls.updateConnectionStatus(false);
});
}
// Delete message (admin only)
function deleteMessage(messageId) {
if (!AppState.isAdmin) return;
if (!confirm('Delete this message?')) return;
API.deleteMessage(messageId)
.then(data => {
if (data.success) {
removeMessageFromUI(messageId);
UIControls.showToast('Message deleted');
} else {
UIControls.showToast('Failed to delete message');
}
})
.catch(error => {
AppLogger.error('Error deleting message:', error);
UIControls.showToast('Failed to delete message');
});
}
// Ban user (admin only)
function banUser(userId) {
if (!AppState.isAdmin) return;
if (!confirm('Ban this user from chat?')) return;
API.banUser(userId)
.then(data => {
if (data.success) {
UIControls.showToast('User banned');
} else {
UIControls.showToast('Failed to ban user');
}
})
.catch(error => {
AppLogger.error('Error banning user:', error);
UIControls.showToast('Failed to ban user');
});
}
// Clear chat (admin only)
function clearChat() {
if (!AppState.isAdmin) return;
if (!confirm('Clear all chat messages?')) return;
API.clearChat()
.then(data => {
if (data.success) {
clearMessagesUI();
UIControls.showToast('Chat cleared');
AppState.lastMessageId = '';
API.sendHeartbeat(); // Force refresh all users
} else {
UIControls.showToast('Failed to clear chat');
}
})
.catch(error => {
AppLogger.error('Error clearing chat:', error);
UIControls.showToast('Failed to clear chat');
});
}
// Message polling system
function startMessagePolling() {
// Poll for new messages
setInterval(fetchMessages, AppConfig.api.chatPollInterval);
// Send heartbeat periodically
setInterval(API.sendHeartbeat, AppConfig.api.heartbeatInterval);
// Initial fetch
fetchMessages();
}
// Fetch and display messages
function fetchMessages() {
API.fetchMessages()
.then(data => {
if (data.success) {
// Handle initial load or message updates
if (data.all_messages !== null) {
// Initial load or forced refresh (like after admin clear)
handleInitialMessageLoad(data);
} else {
// Incremental updates
handleNewMessages(data.messages);
}
// Check for admin clear
if (data.message_count === 0 && AppState.allMessages.length > 0) {
handleChatCleared();
}
// Update last message ID
if (AppState.allMessages.length > 0) {
AppState.lastMessageId = AppState.allMessages[AppState.allMessages.length - 1].id;
}
UIControls.updateConnectionStatus(true);
}
})
.catch(error => {
AppLogger.error('Error fetching messages:', error);
UIControls.updateConnectionStatus(false);
});
}
// Handle initial message load
function handleInitialMessageLoad(data) {
AppState.allMessages = data.all_messages || [];
displayAllMessages();
}
// Handle new messages
function handleNewMessages(messages) {
if (!messages || messages.length === 0) return;
messages.forEach(msg => {
appendMessage(msg);
// Show notification if chat is collapsed and message is not from current user
if (AppState.chatCollapsed && msg.user_id !== AppState.userId) {
AppState.unreadCount++;
UIControls.updateNotificationBadge();
UIControls.playNotificationSound();
}
});
}
// Handle chat cleared by admin
function handleChatCleared() {
const chatMessages = document.getElementById('chatMessages');
if (chatMessages) {
chatMessages.innerHTML = '<div class="system-message">Chat was cleared by admin</div>';
}
AppState.allMessages = [];
AppState.lastMessageId = '';
}
// Typing indicator system
function handleTypingIndicator() {
const messageInput = document.getElementById('messageInput');
if (!messageInput) return;
const hasContent = messageInput.value.length > 0;
if (hasContent && !AppState.isTyping) {
AppState.isTyping = true;
}
clearTimeout(AppState.typingTimer);
AppState.typingTimer = setTimeout(() => {
AppState.isTyping = false;
}, AppConfig.chat.typingTimeout);
}
// UI manipulation functions
function displayAllMessages() {
const chatMessages = document.getElementById('chatMessages');
if (!chatMessages) return;
if (AppState.allMessages.length === 0) {
chatMessages.innerHTML = '<div class="empty-chat">No messages yet. Be the first to say hello! 👋</div>';
AppState.lastMessageCount = 0;
return;
}
chatMessages.innerHTML = '';
AppState.allMessages.forEach(msg => {
appendMessageElement(msg, false);
});
// Scroll to bottom
chatMessages.scrollTop = chatMessages.scrollHeight;
AppState.lastMessageCount = AppState.allMessages.length;
}
function appendMessage(msg) {
AppState.allMessages.push(msg);
appendMessageElement(msg, true);
AppState.lastMessageCount = AppState.allMessages.length;
}
function appendMessageElement(msg, animate) {
const chatMessages = document.getElementById('chatMessages');
if (!chatMessages) return;
// Remove empty chat message if it exists
const emptyChat = chatMessages.querySelector('.empty-chat');
if (emptyChat) {
emptyChat.remove();
}
const messageDiv = document.createElement('div');
messageDiv.className = 'message';
messageDiv.setAttribute('data-message-id', msg.id);
// Own message styling
if (msg.user_id === AppState.userId) {
messageDiv.className += ' own-message';
}
// Admin message styling
if (msg.is_admin) {
messageDiv.className += ' admin-message';
}
// Disable animation for initial load
if (!animate) {
messageDiv.style.animation = 'none';
}
// Create admin actions if user is admin
const adminActions = (AppState.isAdmin && msg.user_id !== AppState.userId) ?
`<div class="message-actions">
<button class="delete-btn" onclick="Chat.deleteMessage('${msg.id}')">Delete</button>
<button class="ban-btn" onclick="Chat.banUser('${msg.user_id}')">Ban</button>
</div>` : '';
// Create admin badge
const adminBadge = msg.is_admin ? '👑 ' : '';
messageDiv.innerHTML = `
<div class="message-header">
<span class="message-nickname${msg.is_admin ? ' admin' : ''}">${adminBadge}${msg.nickname}</span>
<span class="message-id">#${msg.user_id}</span>
<span class="message-time">${msg.time}</span>
</div>
<div class="message-text">${escapeHtml(msg.message)}</div>
${adminActions}
`;
chatMessages.appendChild(messageDiv);
// Auto-scroll to bottom on new messages
if (animate) {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
}
function removeMessageFromUI(messageId) {
const messageElement = document.querySelector(`[data-message-id="${messageId}"]`);
if (messageElement) {
messageElement.style.animation = 'slideOut 0.3s';
setTimeout(() => {
messageElement.remove();
}, 300);
}
// Remove from internal array
AppState.allMessages = AppState.allMessages.filter(msg => msg.id !== messageId);
}
function clearMessagesUI() {
const chatMessages = document.getElementById('chatMessages');
if (chatMessages) {
chatMessages.innerHTML = '<div class="system-message">Chat cleared by admin</div>';
}
AppState.allMessages = [];
AppState.lastMessageId = '';
}
// Utility function for HTML escaping
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Public API
window.Chat = {
initializeChat: initializeChat,
sendMessage: sendMessage,
deleteMessage: deleteMessage,
banUser: banUser,
clearChat: clearChat,
displayAllMessages: displayAllMessages,
appendMessage: appendMessage
};
// Initialize when DOM is ready and modules are loaded
document.addEventListener('DOMContentLoaded', function() {
// Small delay to ensure all modules are loaded
setTimeout(initializeChat, 100);
});
AppLogger.log('Chat module loaded');
})();

282
ui-controls.js Normal file
View file

@ -0,0 +1,282 @@
// 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');
})();

540
video-player.js Normal file
View file

@ -0,0 +1,540 @@
// 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');
})();