iptv-stream-web/assets/js/video-player.js

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');
})();