PageSourceSearch

https://valefmaracuai.com.br/players/player-func.js?v=3.5.0

js valefmaracuai.com.br collected 2026-10-06 21:55:07 UTC 18,530 bytes, 534 lines download raw bytes

1/* Player controls, shared by every skin. No jQuery. */
2(function () {
3  "use strict";
4
5  // The Cast SDK fires this as soon as it loads, which happens before the DOM
6  // is ready: the hook is registered now and the setup runs when both are in.
7  var castApiReady = false;
8  var castSetup = null;
9
10  window.__onGCastApiAvailable = function (available) {
11    if (!available) return;
12
13    castApiReady = true;
14    if (castSetup) castSetup();
15  };
16
17  function ready(fn) {
18    if (document.readyState === "loading") {
19      document.addEventListener("DOMContentLoaded", fn);
20      return;
21    }
22    fn();
23  }
24
25  ready(function () {
26    var player = document.getElementById("fallplayer");
27    var source = document.getElementById("srcP");
28
29    if (!player || !source || player.dataset.brasReady) return;
30
31    // Guard against a second run: duplicated handlers used to break playback
32    player.dataset.brasReady = "1";
33
34    var config = window.brasPlayer || {};
35
36    var playBtn = document.querySelector(".fallplayer-play");
37    var pauseBtn = document.querySelector(".fallplayer-pause");
38    var volume = document.getElementById("volumeSlider");
39    var cover = document.querySelector(".bras-player-background");
40    var label1 = document.querySelector(".rf-playing-label1");
41    var label2 = document.querySelector(".rf-playing-label2");
42
43    var hasMediaSession = "mediaSession" in navigator;
44
45    function setPlaying(on) {
46      if (playBtn) {
47        playBtn.classList.toggle("active", !on);
48        playBtn.classList.toggle("inactive", on);
49      }
50      if (pauseBtn) {
51        pauseBtn.classList.toggle("active", on);
52        pauseBtn.classList.toggle("inactive", !on);
53      }
54      if (hasMediaSession) navigator.mediaSession.playbackState = on ? "playing" : "paused";
55    }
56
57    var STOP_KEY = "brasPlayerStopped";
58
59    function stoppedByHand(value) {
60      try {
61        if (value === undefined) return sessionStorage.getItem(STOP_KEY) === "1";
62        if (value) sessionStorage.setItem(STOP_KEY, "1");
63        else sessionStorage.removeItem(STOP_KEY);
64      } catch (error) {}
65
66      return false;
67    }
68
69    function togglePlay() {
70      // paused is enough: duration only arrives with the metadata, and until
71      // then a click on stop was starting the stream over instead
72      if (!player.paused) {
73        stoppedByHand(true);
74        setPlaying(false);
75        player.pause();
76        source.src = "";
77        player.currentTime = 0;
78        return;
79      }
80
81      stoppedByHand(false);
82      setPlaying(true);
83      player.currentTime = 0;
84      source.src = window.srcstream;
85      player.load();
86
87      var started = player.play();
88      if (started && started.catch) started.catch(function () { setPlaying(false); });
89    }
90
91    // Playback
92    source.addEventListener("error", function () { setPlaying(false); });
93    player.addEventListener("playing", function () { setPlaying(true); });
94    player.addEventListener("ended", function () { setPlaying(false); });
95    player.addEventListener("error", function () { setPlaying(false); });
96
97    document.querySelectorAll(".icopl").forEach(function (button) {
98      button.addEventListener("click", togglePlay);
99    });
100
101    // Notification and lock screen controls follow the same button
102    if (hasMediaSession) {
103      navigator.mediaSession.setActionHandler("play", function () { if (player.paused) togglePlay(); });
104      navigator.mediaSession.setActionHandler("pause", function () { if (!player.paused) togglePlay(); });
105      navigator.mediaSession.setActionHandler("stop", function () { if (!player.paused) togglePlay(); });
106    }
107
108    // Volume
109    var volumeBox = document.querySelector(".fallplayer-volume");
110    var muteBtn = document.querySelector(".fallplayer-mute-unmute");
111    var lastVolume = 100;
112
113    function isMuted() {
114      return player.muted || player.volume === 0;
115    }
116
117    function syncVolume() {
118      var off = isMuted();
119      var label = (off ? config.unmute : config.mute) || (off ? "Ativar som" : "Silenciar");
120
121      // Skins paint the filled part of the track with this, so the level shows
122      if (volume) volume.style.setProperty("--pl-vol", (off ? 0 : Number(volume.value)) + "%");
123
124      if (volumeBox) volumeBox.classList.toggle("is-muted", off);
125
126      if (muteBtn) {
127        muteBtn.setAttribute("title", label);
128        muteBtn.setAttribute("aria-label", label);
129        muteBtn.setAttribute("aria-pressed", off ? "true" : "false");
130      }
131    }
132
133    if (volume) {
134      volume.addEventListener("input", function () {
135        var value = Number(volume.value);
136
137        if (value > 0) lastVolume = value;
138        player.muted = value === 0;
139        player.volume = value / 100;
140        syncVolume();
141      });
142    }
143
144    if (muteBtn) {
145      muteBtn.addEventListener("click", function () {
146        var off = !isMuted();
147
148        player.muted = off;
149        if (!off) player.volume = lastVolume / 100;
150        if (volume) volume.value = off ? 0 : lastVolume;
151        syncVolume();
152      });
153    }
154
155    // Skins that fold the slider away: the button only opens and closes it, a
156    // click outside or Escape puts it back. Muting is the slider at zero.
157    var volumeToggle = document.querySelector(".fallplayer-volume-toggle");
158
159    if (volumeToggle && volumeBox) {
160      var closeVolume = function () {
161        volumeBox.classList.remove("is-open");
162        volumeToggle.setAttribute("aria-expanded", "false");
163      };
164
165      volumeToggle.addEventListener("click", function (e) {
166        e.stopPropagation();
167        var open = volumeBox.classList.toggle("is-open");
168        volumeToggle.setAttribute("aria-expanded", open ? "true" : "false");
169      });
170
171      document.addEventListener("click", function (e) {
172        if (!volumeBox.contains(e.target)) closeVolume();
173      });
174
175      document.addEventListener("keydown", function (e) {
176        if (e.key === "Escape") closeVolume();
177      });
178    }
179
180    syncVolume();
181
182    // Metadata
183    var bar = document.querySelector(".bras-player");
184
185    // App shortcuts stay folded until the dots button is pressed
186    var moreBtn = document.getElementById("playerMore");
187
188    if (moreBtn && bar) {
189      moreBtn.addEventListener("click", function (e) {
190        e.stopPropagation();
191        var open = bar.classList.toggle("is-apps");
192        moreBtn.setAttribute("aria-expanded", open ? "true" : "false");
193      });
194
195      document.addEventListener("click", function (e) {
196        if (bar.contains(e.target)) return;
197        bar.classList.remove("is-apps");
198        moreBtn.setAttribute("aria-expanded", "false");
199      });
200
201      document.addEventListener("keydown", function (e) {
202        if (e.key !== "Escape") return;
203        bar.classList.remove("is-apps");
204        moreBtn.setAttribute("aria-expanded", "false");
205      });
206    }
207
208    // Nothing on air: hide controls and metadata, show the offline notice
209    function setOffline(off) {
210      if (!bar) return;
211
212      bar.classList.toggle("is-offline", off);
213
214      if (off && !player.paused) {
215        player.pause();
216        source.src = "";
217        setPlaying(false);
218      }
219    }
220
221
222
223    function cssUrl(url) {
224      return 'url("' + String(url).replace(/["\\]/g, "") + '")';
225    }
226
227    function coverType(url) {
228      if (/^data:image\/svg/i.test(url)) return "image/svg+xml";
229      if (/\.png(\?|$)/i.test(url)) return "image/png";
230      if (/\.webp(\?|$)/i.test(url)) return "image/webp";
231      return "image/jpeg";
232    }
233
234    // The cover API sends the replacement character, sometimes escaped as text,
235    // where the accent was lost at the source
236    function cleanLabel(text) {
237      return String(text || "")
238        .replace(/\\u[0-9a-fA-F]{4}/g, "")
239        .replace(/\\x[0-9a-fA-F]{2}/g, "")
240        .replace(/�/g, "")
241        .replace(/\s{2,}/g, " ")
242        .replace(/[\s_\-]+$/, "")
243        .trim();
244    }
245
246    // The browser panel shows the same thing the player bar does: song or
247    // program, with the artwork that came with it
248    function updateMediaSession(current, song) {
249      if (!hasMediaSession) return;
250
251      var cover = (current && current.cover) || config.nocover;
252
253      try {
254        navigator.mediaSession.metadata = new MediaMetadata({
255          title: song || config.name || "",
256          artist: config.name || "",
257          artwork: cover ? [{ src: new URL(cover, location.href).href, sizes: "512x512", type: coverType(cover) }] : []
258        });
259      } catch (error) {}
260    }
261
262    // Skins that mark a label with pl-marquee slide it when the title is wider
263    // than its box. Skins that use the marquee element scroll on their own and
264    // only need the text, which is why the caller never rewrites it in place.
265    function syncMarquee(el) {
266      if (!el || !el.classList.contains("pl-marquee") || !el.parentElement) return;
267
268      el.classList.remove("is-scrolling");
269
270      var over = el.scrollWidth - el.parentElement.clientWidth;
271      if (over <= 4) return;
272
273      el.style.setProperty("--pl-marquee-shift", -over + "px");
274      el.style.setProperty("--pl-marquee-time", Math.min(30, Math.max(9, over / 14)) + "s");
275      el.classList.add("is-scrolling");
276    }
277
278    window.brasSyncMarquee = syncMarquee;
279
280    // The web font lands after the first render and changes how wide the title
281    // is: without measuring again a long title would sit still
282    if (document.fonts && document.fonts.ready) {
283      document.fonts.ready.then(function () { syncMarquee(label2); });
284    }
285
286    // One empty answer is usually a hiccup of the stream server, not the station
287    // going off air. The notice waits for three in a row, and those retries come
288    // faster than the normal poll so a real outage still shows up quickly.
289    var OFFLINE_TRIES = 3;
290    var misses = 0;
291    var retryTimer = null;
292
293    /* Whether the station is on air is a question for the stream, not for the
294       metadata: plenty of them broadcast with no title at all. Icecast answers
295       it in under a kilobyte, from the same host that serves the audio, so the
296       check costs our servers nothing. */
297    var statusUrl = String(window.srcstream || "").replace(/\/[^\/?#]*(\?.*)?$/, "/status-json.xsl");
298
299    function streamOnline(done) {
300      if (!statusUrl) { done(false); return; }
301
302      var stop = (window.AbortController ? new AbortController() : null);
303      var timer = window.setTimeout(function () { if (stop) stop.abort(); }, 4000);
304
305      fetch(statusUrl, { cache: "no-store", signal: (stop ? stop.signal : undefined) })
306        .then(function (response) { return response.ok ? response.json() : null; })
307        .then(function (data) {
308          var source = data && data.icestats ? data.icestats.source : null;
309
310          window.clearTimeout(timer);
311          done(!!source && (!Array.isArray(source) || source.length > 0));
312        })
313        .catch(function () {
314          window.clearTimeout(timer);
315
316          // The answer never came: the stream is the one to blame
317          done(false);
318        });
319    }
320
321    function render(current) {
322      if (!current) {
323        misses++;
324
325        if (misses < OFFLINE_TRIES) {
326          window.clearTimeout(retryTimer);
327          retryTimer = window.setTimeout(function () { playerData(); }, 5000);
328          return;
329        }
330
331        // No metadata after the retries: only the stream says if it is over
332        streamOnline(function (up) {
333          if (up) { paint({ label1: config.live || "", label2: config.name || "", cover: config.nocover }); return; }
334
335          paint(null);
336        });
337
338        return;
339      }
340
341      misses = 0;
342      window.clearTimeout(retryTimer);
343      paint(current);
344    }
345
346    function paint(current) {
347
348      var song = current ? cleanLabel(current.label2) : "";
349
350      setOffline(!current);
351      updateMediaSession(current, song);
352
353      if (label1) {
354        label1.textContent = (current && current.label1) || "";
355        label1.classList.remove("pl-skeleton");
356      }
357
358      if (label2) {
359        label2.classList.remove("pl-skeleton");
360
361        // Same song: leave the element alone. Rewriting it every poll restarted
362        // the marquee from the top, which read as the title freezing
363        if (label2.getAttribute("data-text") !== song) {
364          label2.setAttribute("data-text", song);
365          label2.textContent = song;
366          syncMarquee(label2);
367        }
368      }
369
370      if (cover) {
371        // The fallback stays as a second layer under the new artwork: while it
372        // downloads there is something in the frame instead of a hole
373        if (current && current.cover) cover.style.backgroundImage = cssUrl(current.cover) + (config.nocover ? ", " + cssUrl(config.nocover) : "");
374        cover.classList.remove("pl-skeleton");
375      }
376    }
377
378    // Song mode talks to the cover API directly, keeping the load off this server
379    function songData() {
380      fetch(config.coverApi, { cache: "reload" })
381        .then(function (response) { return response.ok ? response.json() : null; })
382        .then(function (data) {
383          var title = data && data.title ? data.title : "";
384          if (!title) { render(null); return; }
385
386          render({
387            label1: config.nowPlaying || "Tocando agora",
388            label2: title,
389            cover: (config.cover && data.image_url ? data.image_url : config.nocover)
390          });
391        })
392        .catch(function () { render(null); });
393    }
394
395    function scheduleData() {
396      fetch("/player-data", { credentials: "same-origin" })
397        .then(function (response) { return response.ok ? response.json() : null; })
398        .then(function (data) {
399          var current = data && data.current ? data.current : null;
400
401          var online = current && (current.online === undefined
402            ? !!(current.label1 || current.label2)
403            : !!Number(current.online));
404
405          render(online ? current : null);
406        })
407        .catch(function () { render(null); });
408    }
409
410    /* Google Cast. The launcher only shows up when the framework loads and a
411       device answers on the network, so nothing appears on browsers without
412       support (Firefox, Safari) or when there is no Chromecast around. */
413    var castBtn = document.getElementById("playerCast");
414    var castSession = null;
415
416    function castMedia() {
417      var media = new chrome.cast.media.MediaInfo(window.srcstream, "audio/mpeg");
418
419      // Song on top, station below it. The line above both is the receiver name
420      // and only a registered receiver id changes what it says
421      media.streamType = chrome.cast.media.StreamType.LIVE;
422      media.metadata = new chrome.cast.media.GenericMediaMetadata();
423      media.metadata.title = (label2 && label2.textContent) || config.name || "";
424      media.metadata.subtitle = config.name || "";
425
426      // The TV fetches the artwork on its own, so it needs the full address
427      // Only the first layer is the artwork: the second one is the fallback
428      var art = (cover && cover.style.backgroundImage.split('), ')[0].replace(/^url\(["\']?/, "").replace(/["\']?\)?$/, "")) || "";
429      if (art) {
430        try { media.metadata.images = [new chrome.cast.Image(new URL(art, location.href).href)]; } catch (error) {}
431      }
432
433      // Our own receiver dresses itself with this: name, logo, colours and
434      // where to read the song from, so the TV never asks our servers who we are
435      media.customData = Object.assign({}, config.cast, { coverApi: config.coverApi });
436
437      return new chrome.cast.media.LoadRequest(media);
438    }
439
440    function castStart() {
441      castSession = cast.framework.CastContext.getInstance().getCurrentSession();
442      if (!castSession) return;
443
444      // The stream now plays on the TV: the page keeps quiet
445      castSession.loadMedia(castMedia()).catch(function () {});
446
447      if (!player.paused) {
448        player.pause();
449        source.src = "";
450        setPlaying(false);
451      }
452    }
453
454    function initCast() {
455      var context = cast.framework.CastContext.getInstance();
456
457      context.setOptions({
458        receiverApplicationId: config.castApp || chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
459        autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED
460      });
461
462      // The button shows on every browser that has the framework: hiding it when
463      // no device answered yet left it missing while the network is scanned.
464      // States: NO_DEVICES_AVAILABLE, NOT_CONNECTED, CONNECTING, CONNECTED
465      function paintCast(state) {
466        if (castBtn) castBtn.hidden = false;
467        if (bar) bar.classList.toggle("is-casting", state === "CONNECTED" || state === "CONNECTING");
468      }
469
470      context.addEventListener(cast.framework.CastContextEventType.CAST_STATE_CHANGED, function (e) {
471        paintCast(e.castState);
472      });
473
474      context.addEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, function (e) {
475        if (e.sessionState === "SESSION_STARTED" || e.sessionState === "SESSION_RESUMED") castStart();
476        if (e.sessionState === "SESSION_ENDED") castSession = null;
477      });
478
479      if (castBtn) {
480        castBtn.addEventListener("click", function () {
481          if (context.getCastState() === "CONNECTED") {
482            context.endCurrentSession(true);
483            return;
484          }
485
486          context.requestSession().catch(function () {});
487        });
488      }
489
490      paintCast(context.getCastState());
491    }
492
493    castSetup = initCast;
494    if (castApiReady) initCast();
495
496    // Autoplay: browsers only allow it after the visitor has interacted with the
497    // site, so a blocked attempt arms the next click instead of failing quietly
498    if (config.autoplay && !stoppedByHand()) {
499      var armed = false;
500
501      var arm = function () {
502        if (armed) return;
503
504        armed = true;
505        document.addEventListener("click", fire, { once: true });
506        document.addEventListener("keydown", fire, { once: true });
507        document.addEventListener("touchstart", fire, { once: true });
508      };
509
510      var fire = function () {
511        if (player.paused) togglePlay();
512      };
513
514      var first = (function () {
515        setPlaying(true);
516        source.src = window.srcstream;
517        player.load();
518
519        return player.play();
520      })();
521
522      if (first && first.catch) {
523        first.catch(function () { setPlaying(false); arm(); });
524      }
525    }
526
527    var playerData = (config.song && config.coverApi ? songData : scheduleData);
528
529    window.addEventListener("resize", function () { syncMarquee(label2); });
530
531    playerData();
532    setInterval(playerData, 30000);
533  });
534})();

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.