540 lines
21 KiB
JavaScript
540 lines
21 KiB
JavaScript
// 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');
|
|
|
|
})();
|