1(function () { 2 const cfg = () => window.RQST_CONFIG; 3 4 // AUTO privilegia Icecast (HI) y conmuta a Shoutcast (LO) si el stream 5 // queda esperando datos. HI y LO mantienen selección manual sin failover. 6 const modeState = { 1: 'auto', 2: 'auto' }; 7 const sourceState = { 1: 'high', 2: 'high' }; 8 const reloadOnPlay = { 1: false, 2: false }; 9 const timers = { 1: null, 2: null }; 10 const elapsedSeconds = { 1: 0, 2: 0 }; 11 const healthTimers = { 1: null, 2: null }; 12 const switchingSource = { 1: false, 2: false }; 13 const lastAutoSwitchAt = { 1: 0, 2: 0 }; 14 15 const WAITING_FAILOVER_MS = 2000; 16 const STALLED_FAILOVER_MS = 500; 17 const AUTO_SWITCH_GUARD_MS = 1200; 18 19 function audio(signalId) { 20 return document.getElementById(`audio-${signalId}`); 21 } 22 23 function currentSrc(signalId) { 24 const signal = cfg().signals[signalId]; 25 return sourceState[signalId] === 'high' ? signal.high : signal.low; 26 } 27 28 function clearHealthTimer(signalId) { 29 if (healthTimers[signalId]) { 30 window.clearTimeout(healthTimers[signalId]); 31 healthTimers[signalId] = null; 32 } 33 } 34 35 function formatTime(totalSeconds) { 36 const hours = Math.floor(totalSeconds / 3600); 37 const minutes = Math.floor((totalSeconds % 3600) / 60); 38 const seconds = totalSeconds % 60; 39 if (hours > 0) { 40 return `${String(hours).padStart(2, '0')}
40:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; 41 } 42 return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; 43 } 44 45 function renderTime(signalId) { 46 const display = document.getElementById(`time-${signalId}`); 47 if (display) display.textContent = formatTime(elapsedSeconds[signalId]); 48 } 49 50 function resetTimer(signalId) { 51 stopTimer(signalId); 52 elapsedSeconds[signalId] = 0; 53 renderTime(signalId); 54 } 55 56 function startTimer(signalId) { 57 stopTimer(signalId); 58 timers[signalId] = window.setInterval(() => { 59 elapsedSeconds[signalId] += 1; 60 renderTime(signalId); 61 }, 1000); 62 } 63 64 function stopTimer(signalId) { 65 if (timers[signalId]) { 66 window.clearInterval(timers[signalId]); 67 timers[signalId] = null; 68 } 69 } 70 71 function setPlayButton(signalId, playing) { 72 const button = document.getElementById(`play-${signalId}`); 73 if (!button) return; 74 button.textContent = playing ? 'ââ' : 'â¶'; 75 button.setAttribute('aria-label', `${playing ? 'Pause' : 'Play'} signal ${signalId}`); 76 button.setAttribute('aria-pressed', playing ? 'true' : 'false'); 77 } 78 79 function renderModeButton(signalId) { 80 const button = document.getElementById(`quality-${signalId}`); 81 const statusMode = document.getElementById(`status-mode-${signalId}`); 82 if (!button) return; 83 84 const mode = modeState[signalId]; 85 const sourceShort = sourceState[signalId] === 'high' ? 'HI' : 'LO'; 86 const sourceName = sourceState[signalId] === 'high' ? 'ICECAST' : 'SHOUTCAST'; 87 button.textContent = mode === 'auto' ? 'AUTO' : sourceShort; 88 if (statusMode) statusMode.textContent = sourceName; 89 90 const activeSource = sourceState[signalId] === 'high' ? 'HI (Icecast)' : 'LO (Shoutcast)'; 91 const modeLabel = mode === 'auto' ? `AUTO, active source ${activeSource}` : `manual ${activeSource}`; 92 button.setAttribute('aria-label', `Change signal ${signalId} mode. Current mode: ${modeLabel}`); 93 button.title = mode === 'auto' ? `AUTO · ${activeSource}` : activeSource; 94 } 95 96 function setAudioSource(signalId, state = 'standby') { 97 const el = audio(signalId); 98 if (!el) return; 99 clearHealthTimer(signalId); 100 el.src = currentSrc(signalId); 101 el.load(); 102 window.RQST_UI.setAudioState(signalId, state); 103 window.RQST_AUDIO_METER?.prepare(signalId); 104 renderModeButton(signalId); 105 } 106 107 function playCurrentSource(signalId, { resetElapsed = true } = {}) { 108 const el = audio(signalId); 109 if (!el) return; 110 111 clearHealthTimer(signalId); 112 reloadOnPlay[signalId] = true; 113 if (resetElapsed) resetTimer(signalId); 114 115 el.pause(); 116 el.src = currentSrc(signalId); 117 el.load(); 118 119 window.RQST_UI.setAudioState(signalId, 'loading'); 120 window.RQST_AUDIO_METER?.prepare(signalId); 121 renderModeButton(signalId); 122 123 const playPromise = el.play(); 124 if (playPromise?.catch) { 125 playPromise.catch((err) => { 126 reloadOnPlay[signalId] = false; 127 switchingSource[signalId] = false; 128 console.warn(`No se pudo cargar la señal ${signalId}:`, err); 129 window.RQST_UI.setAudioState(signalId, 'error'); 130 }); 131 } 132 } 133 134 function failoverToAlternate(signalId, reason) { 135 const el = audio(signalId); 136 if (!el || el.paused) return; 137 if (modeState[signalId] !== 'auto') return; 138 if (switchingSource[signalId]) return; 139 140 const now = Date.now(); 141 if (now - lastAutoSwitchAt[signalId] < AUTO_SWITCH_GUARD_MS) return; 142 143 switchingSource[signalId] = true; 144 clearHealthTimer(signalId); 145 146 const previous = sourceState[signalId]; 147 sourceState[signalId] = previous === 'high' ? 'low' : 'high'; 148 lastAutoSwitchAt[signalId] = now; 149 150 const fromLabel = previous === 'high' ? 'HI/Icecast' : 'LO/Shoutcast'; 151 const toLabel = sourceState[signalId] === 'high' ? 'HI/Icecast' : 'LO/Shoutcast'; 152 console.warn(`RQST señal ${signalId}: ${reason}. AUTO cambia de ${fromLabel} a ${toLabel}.`); 153 playCurrentSource(signalId, { resetElapsed: false }); 154 } 155 156 function scheduleFailover(signalId, delayMs, reason) { 157 const el = audio(signalId); 158 if (!el || el.paused) return; 159 if (modeState[signalId] !== 'auto') return; 160 if (healthTimers[signalId] || switchingSource[signalId]) return; 161 162 healthTimers[signalId] = window.setTimeout(() => { 163 healthTimers[signalId] = null; 164 const current = audio(signalId); 165 if (!current || current.paused) return; 166
167 // Si sigue sin datos suficientes para reproducir, prueba la otra fuente. 168 if (current.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) { 169 failoverToAlternate(signalId, reason); 170 } 171 }, delayMs); 172 } 173 174 function cycleMode(signalId) { 175 const el = audio(signalId); 176 const wasPlaying = el && !el.paused; 177 const currentMode = modeState[signalId]; 178 const nextMode = currentMode === 'auto' ? 'high' : currentMode === 'high' ? 'low' : 'auto'; 179 180 clearHealthTimer(signalId); 181 switchingSource[signalId] = false; 182 modeState[signalId] = nextMode; 183 sourceState[signalId] = nextMode === 'low' ? 'low' : 'high'; 184 renderModeButton(signalId); 185 186 if (wasPlaying) { 187 playCurrentSource(signalId); 188 } else { 189 setAudioSource(signalId); 190 } 191 } 192 193 function fadeOutAndPause(el) { 194 if (!el || el.paused) return; 195 let volume = el.volume; 196 const timer = setInterval(() => { 197 volume = Math.max(0, volume - 0.05); 198 el.volume = volume; 199 if (volume <= 0) { 200 clearInterval(timer); 201 el.pause(); 202 el.volume = 1; 203 } 204 }, 50); 205 } 206 207 function reloadStreamAtLiveEdge(signalId, el) { 208 reloadOnPlay[signalId] = true; 209 resetTimer(signalId); 210 211 el.pause(); 212 el.src = currentSrc(signalId); 213 el.load(); 214 215 window.RQST_UI.setAudioState(signalId, 'loading'); 216 window.RQST_AUDIO_METER?.prepare(signalId); 217 218 const playPromise = el.play(); 219 if (playPromise?.catch) { 220 playPromise.catch((err) => { 221 reloadOnPlay[signalId] = false; 222 console.warn(`No se pudo recargar la señal ${signalId}:`, err); 223 window.RQST_UI.setAudioState(signalId, 'error'); 224 }); 225 } 226 } 227 228 function bindAudioHealth(signalId, el) { 229 el.addEventListener('loadstart', () => window.RQST_UI.setAudioState(signalId, 'loading')); 230 el.addEventListener('loadedmetadata', () => window.RQST_UI.setAudioState(signalId, 'ready')); 231 el.addEventListener('canplay', () => { 232 clearHealthTimer(signalId); 233 window.RQST_UI.setAudioState(signalId, 'ready'); 234 }); 235 el.addEventListener('playing', () => { 236 clearHealthTimer(signalId); 237 switchingSource[signalId] = false; 238 reloadOnPlay[signalId] = false; 239 setPlayButton(signalId, true); 240 startTimer(signalId); 241 renderModeButton(signalId); 242 window.RQST_UI.setAudioState(signalId, 'online'); 243 }); 244 el.addEventListener('waiting', () => { 245 window.RQST_UI.setAudioState(signalId, 'buffering'); 246 scheduleFailover(signalId, WAITING_FAILOVER_MS, `waiting por más de ${WAITING_FAILOVER_MS / 1000} s`); 247 }); 248 el.addEventListener('stalled', () => { 249 window.RQST_UI.setAudioState(signalId, 'buffering'); 250 scheduleFailover(signalId, STALLED_FAILOVER_MS, 'stream stalled'); 251 }); 252 el.addEventListener('error', () => { 253 console.warn(`Audio signal ${signalId} error:`, el.error); 254 clearHealthTimer(signalId); 255 256 if (modeState[signalId] === 'auto' && !el.paused) { 257 failoverToAlternate(signalId, `error de ${sourceState[signalId] === 'high' ? 'Icecast' : 'Shoutcast'}`); 258 return; 259 } 260 261 switchingSource[signalId] = false; 262 stopTimer(signalId); 263 setPlayButton(signalId, false); 264 window.RQST_UI.setAudioState(signalId, 'error'); 265 }); 266 } 267 268 function togglePlayback(signalId) { 269 const el = audio(signalId); 270 if (!el) return; 271 if (el.paused) { 272 // AUTO siempre comienza una nueva sesión en HI. Si falla, conmuta a LO. 273 if (modeState[signalId] === 'auto') sourceState[signalId] = 'high'; 274 renderModeButton(signalId); 275 el.play().catch((err) => { 276 console.warn(`No se pudo reproducir la señal ${signalId}:`, err); 277 window.RQST_UI.setAudioState(signalId, 'error'); 278 }); 279 } else { 280 el.pause(); 281 } 282 } 283 284 function bindSignal(signalId) { 285 const el = audio(signalId); 286 if (!el) return; 287 setAudioSource(signalId); 288 bindAudioHealth(signalId, el); 289 resetTimer(signalId); 290 setPlayButton(signalId, false); 291 renderModeButton(signalId); 292 293 document.getElementById(`play-${signalId}`)?.addEventListener('click', () => togglePlayback(signalId)); 294 document.getElementById(`quality-${signalId}`)?.addEventListener('click', () => cycleMode(signalId)); 295 296 el.addEventListener('play', () => { 297 const otherId = signalId === 1 ? 2 : 1; 298 fadeOutAndPause(audio(otherId)); 299 300 if (!reloadOnPlay[signalId]) { 301 reloadStreamAtLiveEdge(signalId, el); 302 return; 303 } 304 305 window.RQST_UI.setPlaying(signalId, true); 306 window.RQST_UI.setAudioState(signalId, 'loading'); 307 window.RQST_AUDIO_METER?.start(signalId); 308 }); 309 el.addEventListener('pause', () => { 310 clearHealthTimer(signalId); 311 switchingSource[signalId] = false;
312 stopTimer(signalId); 313 setPlayButton(signalId, false); 314 window.RQST_UI.setPlaying(signalId, false); 315 window.RQST_UI.setAudioState(signalId, 'standby'); 316 window.RQST_AUDIO_METER?.stop(signalId); 317 }); 318 el.addEventListener('ended', () => { 319 clearHealthTimer(signalId); 320 switchingSource[signalId] = false; 321 stopTimer(signalId); 322 setPlayButton(signalId, false); 323 window.RQST_UI.setPlaying(signalId, false); 324 window.RQST_UI.setAudioState(signalId, 'standby'); 325 window.RQST_AUDIO_METER?.stop(signalId); 326 }); 327 } 328 329 window.RQST_PLAYER = { 330 init() { 331 bindSignal(1); 332 bindSignal(2); 333 } 334 }; 335})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.