PageSourceSearch

https://radiodelay.com/script.js?v=20260602b

js radiodelay.com collected 2026-10-02 01:40:26 UTC 15,241 bytes, 496 lines download raw bytes

1// === Option B: Simplified Buffering Display & Pause Countdown ===
2
3let audioCtx, sourceNode, delayNode, gainNode, mediaElement;
4let playing = false;
5let isMuted = false;
6let bufferInterval = null;
7let pauseCountdownInterval = null;
8let reconnectAttempts = 0;
9let reconnectTimer = null;
10let stableTimer = null;
11let stationNameFetched = false;
12const MAX_RECONNECT = 6;
13const STABLE_PLAY_MS = 15000;
14
15const delaySlider = document.getElementById("delaySlider");
16const delayStatus = document.getElementById("delayStatus");
17const resetBtn = document.getElementById("resetBtn");
18const streamUrlField = document.getElementById("streamUrl");
19const playerStatus = document.getElementById("playerStatus");
20const playPauseBtn = document.getElementById("playPauseBtn");
21const volumeSlider = document.getElementById("volumeSlider");
22const volumeIcon = document.getElementById("volumeIcon");
23const volumeContainer = document.getElementById("volumeContainer");
24const feedbackToast = document.getElementById("feedbackToast");
25const loadBtn = document.getElementById("loadBtn");
26const stationNameEl = document.getElementById("stationName");
27const volumeTooltip = document.getElementById("volumeTooltip");
28const howToLink = document.getElementById("howToLink");
29const howToModal = document.getElementById("howToModal");
30const closeHowToModalBtn = document.getElementById("closeHowToModal");
31const streamTooltip = streamUrlField.nextElementSibling;
32streamTooltip.textContent = streamUrlField.value || streamUrlField.placeholder;
33
34
35// --- UI Helpers ---
36function setPlayerStatus(msg, mode = "") {
37  playerStatus.textContent = msg;
38  playerStatus.className = "player-status";
39  if (mode) playerStatus.classList.add(mode);
40}
41
42function showFeedback(message) {
43  if (!feedbackToast) return;
44  feedbackToast.textContent = message;
45  feedbackToast.classList.add("show");
46  clearTimeout(showFeedback.timeout);
47  showFeedback.timeout = setTimeout(
48    () => feedbackToast.classList.remove("show"),
49    700
50  );
51}
52
53function updateDelayDisplay(value) {
54  const v = Number(value);
55  if (v === 0) {
56    delayStatus.innerHTML =
57      '<span style="color:red;font-weight:bold">🔴 LIVE</span>';
58  } else {
59    delayStatus.textContent = `${v} sec delay`;
60  }
61}
62
63function openHowToModal() {
64  if (!howToModal) return;
65  howToModal.hidden = false;
66  document.body.style.overflow = "hidden";
67  closeHowToModalBtn?.focus();
68}
69
70function closeHowToModal() {
71  if (!howToModal) return;
72  howToModal.hidden = true;
73  document.body.style.overflow = "";
74}
75
76// --- Station Name ---
77function setStationName(name) {
78  if (!stationNameEl) return;
79  if (name) {
80    stationNameEl.textContent = name;
81    stationNameEl.classList.add("visible");
82  } else {
83    stationNameEl.textContent = "";
84    stationNameEl.classList.remove("visible");
85  }
86}
87
88async function fetchStationName(url) {
89  try {
90    const controller = new AbortController();
91    const res = await fetch(url, {
92      headers: { "Icy-MetaData": "1" },
93      signal: controller.signal,
94      cache: "no-store",
95    });
96    controller.abort();
97    const name = res.headers.get("icy-name");
98    if (name && name.trim()) return name.trim();
99  } catch {}
100
101  try {
102    const res = await fetch(
103      `https://de1.api.radio-browser.info/json/stations/byurl/${encodeURIComponent(url)}`
104    );
105    if (!res.ok) return null;
106    const stations = await res.json();
107    if (Array.isArray(stations) && stations.length > 0 && stations[0].name) {
108      return stations[0].name.trim() || null;
109    }
110  } catch {}
111
112  return null;
113}
114
115// --- Volume ---
116function setVolume(value) {
117  if (gainNode) gainNode.gain.value = value;
118  localStorage.setItem("volume", value);
119}
120
121function updateVolumeIcon(volume) {
122  if (isMuted || volume == 0) {
123    volumeIcon.innerHTML =
124      '<path d="M3 9v6h4l5 5V4L7 9H3z"/><path d="M16 9l5 6M21 9l-5 6" stroke="white" stroke-width="2" fill="none"/>';
125  } else {
126    volumeIcon.innerHTML =
127      '<path d="M3 9v6h4l5 5V4L7 9H3z"/><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v8.06c1.48-.74 2.5-2.26 2.5-4.03z"/>';
128  }
129}
130
131volumeContainer.addEventListener("click", (e) => {
132  if (e.target === volumeSlider) return;
133  isMuted = !isMuted;
134  updateVolumeIcon(volumeSlider.value);
135  setVolume(isMuted ? 0 : volumeSlider.value);
136  showFeedback(isMuted ? "🔇 Muted" : "🔊 Unmuted");
137});
138
139volumeSlider.addEventListener("input", () => {
140  isMuted = volumeSlider.value == 0;
141  updateVolumeIcon(volumeSlider.value);
142  setVolume(volumeSlider.value);
143  showFeedback(`Volume: ${(volumeSlider.value * 100).toFixed(0)}%`);
144  updateVolumeTooltip();
145});
146
147// --- Volume Tooltip ---
148function updateVolumeTooltip() {
149  if (!volumeSlider || !volumeTooltip) return;
150  const min = parseFloat(volumeSlider.min);
151  const max = parseFloat(volumeSlider.max);
152  const val = parseFloat(volumeSlider.value);
153  const percent = (val - min) / (max - min);
154  const sliderWidth = volumeSlider.offsetWidth;
155  const thumbWidth = 14;
156  const offset = percent * (sliderWidth - thumbWidth) + thumbWidth / 2;
157  volumeTooltip.style.left = `${offset}px`;
158  volumeTooltip.textContent = `${Math.round(val * 100)}%`;
159}
160window.addEventListener("load", updateVolumeTooltip);
161
162// --- Buffering Display (simplified) ---
163function startBufferingDisplay() {
164  if (!delayNode || !mediaElement) return;
165
166  if (bufferInterval) clearInterval(bufferInterval);
167
168  setPlayerStatus("Buffering…", "loading");
169
170  bufferInterval = setInterval(() => {
171    const delayTime = delayNode.delayTime.value;
172    if (delayTime <= 0) {
173      clearInterval(bufferInterval);
174      bufferInterval = null;
175      setPlayerStatus("Playing LIVE (no delay)", "playing");
176      return;
177    }
178
179    // stop buffering message if playback has reached the delay
180    // (approximation: can be removed if you want "always buffering" until user interaction)
181    // For simplicity, here we just clear after a short interval
182    clearInterval(bufferInterval);
183    bufferInterval = null;
184    setPlayerStatus(`Playing (${delayTime}s delay)`, "playing");
185  }, 300);
186}
187
188// --- Pause Countdown ---
189function startPauseCountdown() {
190  if (!delayNode) return;
191
192  clearInterval(pauseCountdownInterval);
193  let countdown = Math.ceil(delayNode.delayTime.value);
194  pauseCountdownInterval = setInterval(() => {
195    if (countdown <= 0) {
196      clearInterval(pauseCountdownInterval);
197      pauseCountdownInterval = null;
198      setPlayerStatus("Paused", "paused");
199    } else {
200      setPlayerStatus(`Pausing in ${countdown}s…`, "loading");
201      countdown--;
202    }
203  }, 1000);
204}
205
206// --- Load and Initialize Stream ---
207async function loadStream(isReconnect = false) {
208  const url = streamUrlField.value.trim();
209  if (!url) {
210    setPlayerStatus("Please enter a valid stream URL", "error");
211    alert("Please enter a valid stream URL");
212    return;
213  }
214
215  clearTimeout(reconnectTimer);
216  clearTimeout(stableTimer);
217  if (!isReconnect) {
218    reconnectAttempts = 0;
219    stationNameFetched = false;
220    setStationName(null);
221  }
222  showFeedback("Loading stream…");
223  setPlayerStatus("Loading…", "loading");
224
225  if (audioCtx) await audioCtx.close();
226  audioCtx = new AudioContext();
227
228  // Safe max delay
229  let maxDelayTime = audioCtx.maxDelayTime;
230  if (typeof maxDelayTime !== "number" || !isFinite(maxDelayTime))
231    maxDelayTime = 180;
232  const safeMaxDelay = Math.min(180, maxDelayTime - 0.001);
233
234  // Safe initial delay
235  let initialDelay = Number(delaySlider.value);
236  if (!isFinite(initialDelay) || initialDelay < 0) initialDelay = 0;
237
238  mediaElement = new Audio(url);
239  mediaElement.crossOrigin = "anonymous";
240  mediaElement.autoplay = true;
241  mediaElement.preload = "auto";
242
243  sourceNode = audioCtx.createMediaElementSource(mediaElement);
244  delayNode = audioCtx.createDelay(safeMaxDelay);
245  delayNode.delayTime.value = initialDelay;
246
247  gainNode = audioCtx.createGain();
248  gainNode.gain.value = Number(volumeSlider.value) || 1;
249
250  sourceNode.connect(delayNode).connect(gainNode).connect(audioCtx.destination);
251
252  mediaElement.addEventListener("playing", () => {
253    clearTimeout(stableTimer);
254    stableTimer = setTimeout(() => { reconnectAttempts = 0; }, STABLE_PLAY_MS);
255    setPlayerStatus(
256      delayNode.delayTime.value === 0
257        ? "Playing LIVE (no delay)"
258        : `Playing (${delayNode.delayTime.value}s delay)`,
259      "playing"
260    );
261    if (!stationNameFetched) {
262      stationNameFetched = true;
263      fetchStationName(url).then(name => { if (name) setStationName(name); });
264    }
265  });
266
267  mediaElement.addEventListener("waiting", () => {
268    setPlayerStatus("Buffering…", "loading");
269  });
270
271  mediaElement.addEventListener("error", () => {
272    clearTimeout(stableTimer);
273    if (!playing) return;
274    const url = streamUrlField.value.trim();
275    fetch(url, { method: "HEAD", cache: "no-store" })
276      .then((res) => {
277        if (res.status >= 400 && res.status < 500) {
278          playing = false;
279          fadeIcon("play");
280          setPlayerStatus("Stream expired — reload a fresh URL", "error");
281          return;
282        }
283        scheduleReconnect();
284      })
285      .catch(scheduleReconnect);
286  });
287
288  function scheduleReconnect() {
289    if (reconnectAttempts >= MAX_RECONNECT) {
290      setPlayerStatus("Stream unavailable — try reloading", "error");
291      return;
292    }
293    reconnectAttempts++;
294    const delay = Math.min(2000 * reconnectAttempts, 15000);
295    setPlayerStatus(`Stream interrupted — reconnecting in ${delay / 1000}s…`, "loading");
296    clearTimeout(reconnectTimer);
297    reconnectTimer = setTimeout(() => {
298      if (playing) loadStream(true);
299    }, delay);
300  }
301
302  try {
303    await audioCtx.resume();
304    await mediaElement.play();
305    playing = true;
306    fadeIcon("pause");
307    showFeedback("▶️ Playing");
308    localStorage.setItem("streamUrl", url);
309  } catch (err) {
310    console.error(err);
311    if (err.name === "NotAllowedError") {
312      setPlayerStatus("Click Load to play stream");
313    } else {
314      setPlayerStatus("Playback failed — check stream or CORS", "error");
315    }
316  }
317}
318
319// --- Play/Pause ---
320async function togglePlayback() {
321  if (!playing) {
322    await loadStream();
323  } else {
324    startPauseCountdown();
325    mediaElement.pause();
326    playing = false;
327    fadeIcon("play");
328    showFeedback("⏸️ Paused");
329  }
330}
331
332// --- Delay Slider On-the-fly ---
333delaySlider.addEventListener("input", () => {
334  let value = Number(delaySlider.value);
335  if (!isFinite(value) || value < 0) value = 0;
336  updateDelayDisplay(value);
337  if (delayNode) {
338    delayNode.delayTime.value = value;
339    startBufferingDisplay();
340  }
341  localStorage.setItem("delay", value);
342  setPlayerStatus(
343    value === 0 ? "Playing LIVE (no delay)" : `Playing (${value}s delay)`,
344    "playing"
345  );
346});
347
348// --- Reset to LIVE ---
349resetBtn.addEventListener("click", () => {
350  delaySlider.value = 0;
351  updateDelayDisplay(0);
352  if (delayNode) delayNode.delayTime.value = 0;
353  setPlayerStatus("🔴 LIVE (no delay)", "live");
354  showFeedback("LIVE (0 delay)");
355});
356
357// --- Buttons ---
358playPauseBtn.addEventListener("click", togglePlayback);
359loadBtn.addEventListener("click", togglePlayback);
360
361// --- Keyboard shortcuts ---
362window.addEventListener("keydown", (e) => {
363  if (e.key === "Escape" && howToModal && !howToModal.hidden) {
364    closeHowToModal();
365    howToLink?.focus();
366    return;
367  }
368  if (e.target.tagName === "INPUT") return;
369  switch (e.key.toLowerCase()) {
370    case " ":
371      e.preventDefault();
372      togglePlayback();
373      break;
374    case "arrowup":
375      e.preventDefault();
376      volumeSlider.value = Math.min(
377        1,
378        parseFloat(volumeSlider.value) + 0.05
379      ).toFixed(2);
380      setVolume(volumeSlider.value);
381      updateVolumeIcon(volumeSlider.value);
382      updateVolumeTooltip();
383      showFeedback(`Volume: ${(volumeSlider.value * 100).toFixed(0)}%`);
384      break;
385    case "arrowdown":
386      e.preventDefault();
387      volumeSlider.value = Math.max(
388        0,
389        parseFloat(volumeSlider.value) - 0.05
390      ).toFixed(2);
391      setVolume(volumeSlider.value);
392      updateVolumeIcon(volumeSlider.value);
393      updateVolumeTooltip();
394      showFeedback(`Volume: ${(volumeSlider.value * 100).toFixed(0)}%`);
395      break;
396    case "arrowleft":
397      e.preventDefault();
398      delaySlider.value = Math.max(0, parseInt(delaySlider.value) - 1);
399      if (delayNode) delayNode.delayTime.value = delaySlider.value;
400      updateDelayDisplay(delaySlider.value);
401      startBufferingDisplay();
402      break;
403    case "arrowright":
404      e.preventDefault();
405      delaySlider.value = Math.min(180, parseInt(delaySlider.value) + 1);
406      if (delayNode) delayNode.delayTime.value = delaySlider.value;
407      updateDelayDisplay(delaySlider.value);
408      startBufferingDisplay();
409      break;
410    case "l":
411      e.preventDefault();
412      loadBtn.click();
413      break;
414    case "m":
415      e.preventDefault();
416      isMuted = !isMuted;
417      updateVolumeIcon(volumeSlider.value);
418      setVolume(isMuted ? 0 : volumeSlider.value);
419      showFeedback(isMuted ? "🔇 Muted" : "🔊 Unmuted");
420      break;
421    case "g":
422      e.preventDefault();
423      resetBtn.click();
424      break;
425  }
426});
427
428// --- Play/Pause icon fade ---
429function fadeIcon(state) {
430  playPauseBtn.classList.add("fade-out");
431  setTimeout(() => {
432    playPauseBtn.innerHTML =
433      state === "pause"
434        ? '<div class="icon pause"><span></span><span></span></div>'
435        : '<div class="icon play"></div>';
436    playPauseBtn.classList.remove("fade-out");
437    playPauseBtn.classList.add("fade-in");
438    setTimeout(() => playPauseBtn.classList.remove("fade-in"), 150);
439  }, 120);
440}
441
442//abn
443
444howToLink?.addEventListener("click", (e) => {
445  e.preventDefault();
446  openHowToModal();
447});
448
449closeHowToModalBtn?.addEventListener("click", () => {
450  closeHowToModal();
451  howToLink?.focus();
452});
453
454howToModal?.addEventListener("click", (e) => {
455  if (e.target === howToModal) {
456    closeHowToModal();
457  }
458});
459
460// --- On page load ---
461window.addEventListener("DOMContentLoaded", () => {
462  const queryParams = new URLSearchParams(window.location.search);
463  const streamFromQuery = queryParams.get("stream") || queryParams.get("url");
464  const savedUrl = localStorage.getItem("streamUrl");
465  const savedDelay = localStorage.getItem("delay");
466  const savedVol = localStorage.getItem("volume");
467  if (streamFromQuery) {
468    streamUrlField.value = streamFromQuery;
469    localStorage.setItem("streamUrl", streamFromQuery);
470  } else if (savedUrl) {
471    streamUrlField.value = savedUrl;
472  }
473  if (savedDelay) {
474    delaySlider.value = savedDelay;
475    updateDelayDisplay(savedDelay);
476  }
477  if (savedVol) volumeSlider.value = savedVol;
478  setPlayerStatus("Ready — enter a stream URL");
479
480  // Load main title SVG
481  const titleEl = document.getElementById("title");
482  if (!titleEl) return;
483  fetch("rdtitle.svg")
484    .then((response) => response.text())
485    .then((svgText) => {
486      const parser = new DOMParser();
487      const svgDoc = parser.parseFromString(svgText, "image/svg+xml");
488      const svgElement = svgDoc.documentElement;
489      svgElement.removeAttribute("width");
490      svgElement.removeAttribute("height");
491      titleEl.appendChild(svgElement);
492    })
493    .catch((err) => console.error("SVG load error:", err));
494});
495
496document.getElementById("copyright-year").textContent = new Date().getFullYear() || 2026;

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.