PageSourceSearch

https://rqstradio.pages.dev/js/player.js?v=71.1-a16

js rqstradio.pages.dev collected 2026-10-03 05:54:42 UTC 11,269 bytes, 335 lines download raw bytes

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.