// Constants const TARGET_BUFFER = 90; const MAX_DEBUG_ENTRIES = 100; const SEGMENT_LOAD_SAMPLES = 10; class DebugPlayer { constructor() { this.video = document.getElementById('videoPlayer'); this.debugInfo = document.getElementById('debugInfo'); this.streamStatus = document.getElementById('streamStatus'); this.segmentLoadTimes = []; this.lastSegmentLoadTime = Date.now(); this.elements = { bufferHealth: document.getElementById('bufferHealth'), bufferUsage: document.getElementById('bufferUsage'), currentPosition: document.getElementById('currentPosition'), timeToLive: document.getElementById('timeToLive'), bandwidth: document.getElementById('bandwidth'), droppedFrames: document.getElementById('droppedFrames'), segmentLoadTime: document.getElementById('segmentLoadTime'), playbackRate: document.getElementById('playbackRate') }; this.initializePlayer(); } initializePlayer() { if (!this.isHLSSupported()) { this.initializeFallbackPlayer(); return; } this.hls = new Hls({ debug: false, enableWorker: true, lowLatencyMode: true, backBufferLength: TARGET_BUFFER, xhrSetup: function(xhr, url) { if (url.includes('/playlist.m3u8') || url.includes('.ts')) { var select = document.getElementById('playlistSelect'); var playlist = 'active'; if (select && select.value) { playlist = select.value; } if (!url.includes('?')) { xhr.open('GET', url + '?playlist=' + playlist, true); return; } } xhr.open('GET', url, true); } }); this.setupHLSEvents(); this.loadStream(); this.startMetricsUpdate(); } isHLSSupported() { return typeof Hls !== 'undefined' && Hls.isSupported(); } initializeFallbackPlayer() { if (this.video.canPlayType('application/vnd.apple.mpegurl')) { this.video.src = '/playlists/active.m3u8'; this.updateStreamStatus('Using native HLS playback'); this.video.addEventListener('loadedmetadata', () => this.video.play()); } } setupHLSEvents() { this.hls.on(Hls.Events.MANIFEST_PARSED, () => { this.updateStreamStatus('Stream ready - manifest loaded'); this.video.play().catch(error => { console.log('Playback failed:', error); this.updateStreamStatus('Playback failed: ' + error.message); }); }); this.hls.on(Hls.Events.ERROR, (event, data) => { if (data.fatal) { this.handleFatalError(data); } this.addDebugEntry('Error: ' + data.details); }); this.hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => { this.addDebugEntry('Quality changed to ' + data.level); }); this.hls.on(Hls.Events.BUFFER_APPENDING, (event, data) => { this.addDebugEntry('Appending segment: ' + data.type); this.updateSegmentLoadTime(); }); } handleFatalError(data) { this.updateStreamStatus('Fatal error: ' + data.details); switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: this.hls.startLoad(); break; case Hls.ErrorTypes.MEDIA_ERROR: this.hls.recoverMediaError(); break; default: this.loadStream(); break; } } loadStream() { if (!this.hls) return; try { var select = document.getElementById('playlistSelect'); var playlist = 'active'; if (select && select.value) { playlist = select.value; } var playlistUrl = '/playlists/' + playlist + '.m3u8'; this.hls.loadSource(playlistUrl); this.hls.attachMedia(this.video); this.updateStreamStatus('Initializing player'); } catch (error) { console.error('Error initializing player:', error); this.updateStreamStatus('Error initializing player: ' + error.message); } } updateMetrics() { if (!this.hls || !this.video) return; try { var metrics = this.calculateMetrics(); this.updateMetricDisplays(metrics); this.updateTimelinePosition(metrics); } catch (error) { console.error('Error updating metrics:', error); } requestAnimationFrame(() => this.updateMetrics()); } calculateMetrics() { var buffered = this.video.buffered; var bufferHealth = 0; var bufferPercentage = 0; if (buffered && buffered.length > 0) { var bufferEnd = buffered.end(buffered.length - 1); bufferHealth = bufferEnd - this.video.currentTime; bufferPercentage = (bufferHealth / TARGET_BUFFER) * 100; } return { bufferHealth: bufferHealth, bufferPercentage: bufferPercentage, currentPosition: this.video.currentTime, bandwidth: this.hls.stats ? this.hls.stats.bw : 0, droppedFrames: this.getDroppedFrames(), segmentLoadTime: this.calculateAverageSegmentLoadTime(), timeToLive: this.video.duration ? this.video.duration - this.video.currentTime : 0 }; } updateMetricDisplays(metrics) { if (this.elements.bufferHealth) { this.elements.bufferHealth.textContent = metrics.bufferHealth.toFixed(1) + 's'; this.elements.bufferHealth.className = 'metric-value' + (metrics.bufferHealth < 10 ? ' error' : metrics.bufferHealth < 30 ? ' warning' : ''); } if (this.elements.bufferUsage) { this.elements.bufferUsage.textContent = Math.min(100, metrics.bufferPercentage).toFixed(0) + '%'; } if (this.elements.currentPosition) { this.elements.currentPosition.textContent = metrics.currentPosition.toFixed(1) + 's'; } if (this.elements.timeToLive) { this.elements.timeToLive.textContent = metrics.timeToLive.toFixed(1) + 's'; } if (this.elements.bandwidth && !isNaN(metrics.bandwidth)) { this.elements.bandwidth.textContent = (metrics.bandwidth / 1000000).toFixed(1) + 'Mbps'; } if (this.elements.droppedFrames) { this.elements.droppedFrames.textContent = metrics.droppedFrames.toString(); } if (this.elements.segmentLoadTime) { this.elements.segmentLoadTime.textContent = metrics.segmentLoadTime.toFixed(0) + 'ms'; } } updateTimelinePosition(metrics) { var timelinePosition = document.querySelector('.timeline-position'); if (timelinePosition && this.video.duration) { var position = (metrics.currentPosition / this.video.duration) * 100; timelinePosition.style.setProperty('--position', position + '%'); } } getDroppedFrames() { if (typeof this.video.webkitDroppedFrameCount === 'number') { return this.video.webkitDroppedFrameCount; } if (typeof this.video.mozDroppedFrames === 'number') { return this.video.mozDroppedFrames; } return 0; } updateSegmentLoadTime() { var now = Date.now(); var loadTime = now - this.lastSegmentLoadTime; this.lastSegmentLoadTime = now; this.segmentLoadTimes.push(loadTime); if (this.segmentLoadTimes.length > SEGMENT_LOAD_SAMPLES) { this.segmentLoadTimes.shift(); } } calculateAverageSegmentLoadTime() { if (this.segmentLoadTimes.length === 0) return 0; var total = 0; for (var i = 0; i < this.segmentLoadTimes.length; i++) { total += this.segmentLoadTimes[i]; } return total / this.segmentLoadTimes.length; } startMetricsUpdate() { requestAnimationFrame(() => this.updateMetrics()); } togglePlay() { if (this.video.paused) { this.video.play(); this.updateStreamStatus('Playback started'); } else { this.video.pause(); this.updateStreamStatus('Playback paused'); } } seekLive() { if (this.video.duration) { this.video.currentTime = this.video.duration; this.updateStreamStatus('Seeking to live point'); } } reloadStream() { this.updateStreamStatus('Reloading stream'); if (this.hls) { this.hls.destroy(); this.loadStream(); } } updateStreamStatus(message) { if (this.streamStatus) { this.streamStatus.textContent = message; } } addDebugEntry(message) { if (!this.debugInfo) return; var time = new Date().toLocaleTimeString(); var entry = document.createElement('div'); entry.className = 'status-entry'; entry.innerHTML = '' + time + '' + message; this.debugInfo.insertBefore(entry, this.debugInfo.firstChild); while (this.debugInfo.children.length > MAX_DEBUG_ENTRIES) { this.debugInfo.removeChild(this.debugInfo.lastChild); } } } // Initialize when DOM is loaded document.addEventListener('DOMContentLoaded', function() { var player = new DebugPlayer(); // Expose player instance globally for playlist selector window.debugPlayer = player; // Setup control handlers var playButton = document.querySelector('[onclick="togglePlay()"]'); if (playButton) playButton.addEventListener('click', function() { player.togglePlay(); }); var seekButton = document.querySelector('[onclick="seekLive()"]'); if (seekButton) seekButton.addEventListener('click', function() { player.seekLive(); }); var reloadButton = document.querySelector('[onclick="reloadStream()"]'); if (reloadButton) reloadButton.addEventListener('click', function() { player.reloadStream(); }); }); // Initialize server config const serverConfig = {"stream":{"startTimeMs":1740629640000,"timeZone":"America/Los_Angeles","startDate":"2025-02-27","startHour":4,"startMinute":14}};