PageSourceSearch

https://radioalvoradanet.com/players/player05/player-func.js?v=1.1.0

js radioalvoradanet.com collected 2026-10-08 10:19:46 UTC 10,217 bytes, 283 lines download raw bytes

1const audio    = document.getElementById('ap-audio');
2const player   = document.getElementById('player');
3const toggle   = document.getElementById('ap-toggle');
4const titleEl  = document.getElementById('ap-title');
5const cover    = document.getElementById('ap-cover');
6const volEl    = document.getElementById('ap-volume');
7const volIcon  = document.getElementById('vol-icon');
8const ripple   = document.getElementById('ripple');
9const eqBanner = document.getElementById('eq-banner');
10
11const STREAM_SERVER = window.PlayerConfig.streamServer;
12const STREAM_PORT   = window.PlayerConfig.streamPort;
13const STREAM_URL    = 'https://' + STREAM_SERVER + '.brascast.com:' + STREAM_PORT + '/live';
14const COVER_API     = '/api_player.php?p=' + STREAM_PORT;
15const ICECAST_BASE  = 'https://' + STREAM_SERVER + '.brascast.com:' + STREAM_PORT;
16const OFFLINE_TEXT  = window.PlayerConfig.offlineText || 'Offline';
17const LIVE_TEXT     = window.PlayerConfig.liveText || 'Ao vivo';
18
19(function buildEQ() {
20    for (let i = 0; i < 80; i++) {
21        const s = document.createElement('span');
22        s.style.setProperty('--dur',   (0.4 + Math.random() * 0.9).toFixed(2) + 's');
23        s.style.setProperty('--delay', (Math.random() * 0.6).toFixed(2) + 's');
24        eqBanner.appendChild(s);
25    }
26})();
27
28function posRipple() {
29    const b = toggle.getBoundingClientRect();
30    const p = player.getBoundingClientRect();
31    ripple.style.left = (b.left - p.left + b.width  / 2) + 'px';
32    ripple.style.top  = (b.top  - p.top  + b.height / 2) + 'px';
33}
34window.addEventListener('resize', posRipple);
35setTimeout(posRipple, 80);
36
37function setPlayState(isPlaying) {
38    if (isPlaying) {
39        player.classList.add('on');
40    } else {
41        player.classList.remove('on');
42    }
43    posRipple();
44}
45
46audio.addEventListener('playing', () => setPlayState(true));
47audio.addEventListener('pause',   () => setPlayState(false));
48audio.addEventListener('error',   () => setPlayState(false));
49
50toggle.addEventListener('click', () => {
51    if (audio.paused) {
52        audio.src = STREAM_URL + '?t=' + Date.now();
53        audio.play().catch(() => setPlayState(false));
54    } else {
55        audio.pause();
56    }
57});
58
59function syncVol() {
60    const v = parseInt(volEl.value);
61    volEl.style.backgroundSize = v + '% 100%';
62    volIcon.className = 'vol-icon fa-solid ' +
63        (v > 50 ? 'fa-volume-high' : v > 0 ? 'fa-volume-low' : 'fa-volume-xmark');
64}
65volEl.addEventListener('input', () => { audio.volume = volEl.value / 100; syncVol(); });
66audio.volume = 0.8;
67syncVol();
68
69function applyMarquee() {
70    const wrap = titleEl.parentElement;
71
72    titleEl.classList.remove('sc');
73    titleEl.style.animationDuration = '';
74    titleEl.style.removeProperty('--scroll-dist');
75
76    const raw = titleEl.dataset.raw || titleEl.textContent;
77    titleEl.textContent = '' + raw;
78
79    requestAnimationFrame(() => {
80        if (titleEl.scrollWidth > wrap.offsetWidth) {
81            const sep = '\u00a0\u00a0\u00a0\u2022\u00a0\u00a0\u00a0';
82
83            titleEl.textContent = raw + sep;
84            const distanceToScroll = titleEl.scrollWidth;
85
86            titleEl.textContent = raw + sep + raw;
87
88            const dur = Math.max(8, distanceToScroll / 30);
89            titleEl.style.setProperty('--scroll-dist', `${distanceToScroll}px`);
90            titleEl.style.animationDuration = dur + 's';
91            titleEl.classList.add('sc');
92        }
93    });
94}
95
96function updateTitle(text) {
97    if (titleEl.dataset.raw === text) return;
98    titleEl.dataset.raw = text;
99    titleEl.textContent = text;
100    setTimeout(applyMarquee, 250);
101}
102
103function updateCover(url) {
104    const noImg = !url || url.includes('no-cover') || url.includes('nocover');
105    if (!noImg) {
106        cover.style.backgroundImage = `url('${url}')`;
107        cover.innerHTML = '';
108    } else {
109        cover.style.backgroundImage = '';
110        cover.innerHTML = '<i class="fa-solid fa-music"></i>';
111    }
112}
113
114function fetchIcecastMeta() {
115    fetch(ICECAST_BASE + '/status-json.xsl?t=' + Date.now())
116        .then(r => r.json())
117        .then(d => {
118            let src = d && d.icestats && d.icestats.source;
119            if (!src) return;
120            if (Array.isArray(src)) src = src[0];
121            const title = src.title || src.song_title || '';
122            if (title) updateTitle(title);
123        })
124        .catch(() => {});
125}
126
127/* On air or not is a question for the mount, never for the metadata: a station
128   broadcasting with no title is still on air. Icecast answers in under a
129   kilobyte from the same host that serves the audio. */
130function streamOnline(done) {
131    const stop = (window.AbortController ? new AbortController() : null);
132    const timer = window.setTimeout(() => { if (stop) stop.abort(); }, 4000);
133
134    fetch(ICECAST_BASE + '/status-json.xsl?t=' + Date.now(), { cache: 'no-store', signal: (stop ? stop.signal : undefined) })
135        .then(r => (r.ok ? r.json() : null))
136        .then(d => {
137            const source = d && d.icestats ? d.icestats.source : null;
138
139            window.clearTimeout(timer);
140            done(!!source && (!Array.isArray(source) || source.length > 0));
141        })
142        .catch(() => { window.clearTimeout(timer); done(false); });
143}
144
145function setOffline() {
146    updateTitle(OFFLINE_TEXT);
147    updateCover('');
148
149    if (!audio.paused) { audio.pause(); }
150}
151
152function fetchNowPlaying() {
153    fetch(COVER_API + '&t=' + Date.now(), { cache: 'no-store' })
154        .then(r => r.json())
155        .then(d => {
156            d = d.current;
157            const track = d.title || d.song || d.track || '';
158
159            if (!track) { streamOnline(up => { if (up) { updateTitle(LIVE_TEXT); updateCover(''); } else { setOffline(); } }); return; }
160
161            updateTitle(track);
162            updateCover(d.image_url || d.cover || d.art || '');
163        })
164        .catch(() => fetchIcecastMeta());
165}
166
167if (window.PlayerConfig.playerAuto) {
168    audio.src = STREAM_URL + '?t=' + Date.now();
169    const initPlay = audio.play();
170    if (initPlay !== undefined) {
171        initPlay
172            .then(() => setPlayState(true))
173            .catch(() => setPlayState(false));
174    }
175}
176
177fetchNowPlaying();
178setInterval(fetchNowPlaying, 30000);
179window.addEventListener('resize', applyMarquee);
180/* Google Cast. Same identity of the other players, from window.brasPlayer:
181   this skin has controls of its own, so only the session lives here. */
182(function () {
183    const btn = document.getElementById('playerCast');
184    const config = window.brasPlayer || {};
185
186    if (!btn) return;
187
188    let session = null;
189
190    function media() {
191        const info = new chrome.cast.media.MediaInfo(STREAM_URL, 'audio/mpeg');
192
193        info.streamType = chrome.cast.media.StreamType.LIVE;
194        info.metadata = new chrome.cast.media.GenericMediaMetadata();
195        info.metadata.title = (titleEl && titleEl.textContent) || config.name || '';
196        info.metadata.subtitle = config.name || '';
197
198        // The TV fetches the artwork on its own, so it needs the full address
199        const art = cover && cover.style.backgroundImage
200            ? cover.style.backgroundImage.split('), ')[0].replace(/^url\(["']?/, '').replace(/["']?\)?$/, '')
201            : '';
202
203        if (art) {
204            try { info.metadata.images = [new chrome.cast.Image(new URL(art, location.href).href)]; } catch (error) {}
205        }
206
207        // Our own receiver dresses itself with this: name, logo and colours
208        info.customData = Object.assign({}, config.cast, { coverApi: config.coverApi });
209
210        return new chrome.cast.media.LoadRequest(info);
211    }
212
213    function start() {
214        session = cast.framework.CastContext.getInstance().getCurrentSession();
215        if (!session) return;
216
217        session.loadMedia(media()).catch(() => {});
218
219        // The stream now plays on the TV: the page keeps quiet
220        if (!audio.paused) {
221            audio.pause();
222            audio.src = '';
223            setPlayState(false);
224        }
225    }
226
227    function init() {
228        const context = cast.framework.CastContext.getInstance();
229
230        context.setOptions({
231            receiverApplicationId: config.castApp || chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
232            autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED
233        });
234
235        function paint(state) {
236            btn.hidden = false;
237            player.classList.toggle('is-casting', state === 'CONNECTED' || state === 'CONNECTING');
238        }
239
240        context.addEventListener(cast.framework.CastContextEventType.CAST_STATE_CHANGED, (e) => paint(e.castState));
241
242        context.addEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, (e) => {
243            if (e.sessionState === 'SESSION_STARTED' || e.sessionState === 'SESSION_RESUMED') start();
244            if (e.sessionState === 'SESSION_ENDED') session = null;
245        });
246
247        // The same button connects and disconnects: asking for a session while
248        // one is open only reopens the device list
249        btn.addEventListener('click', () => {
250            if (context.getCastState() === 'CONNECTED') {
251                context.endCurrentSession(true);
252                return;
253            }
254
255            context.requestSession().catch(() => {});
256        });
257
258        paint(context.getCastState());
259    }
260
261    let started = false;
262
263    function boot() {
264        if (started) { return true; }
265        if (!window.cast || !cast.framework || !window.chrome || !chrome.cast) { return false; }
266
267        started = true;
268        init();
269
270        return true;
271    }
272
273    // The SDK can be ready before this file runs or long after it, and its
274    // callback only fires once: this answers both, and gives up after ten seconds
275    window.__onGCastApiAvailable = function (available) { if (available) boot(); };
276
277    if (!boot()) {
278        let tries = 0;
279        const timer = setInterval(() => {
280            if (boot() || ++tries > 40) { clearInterval(timer); }
281        }, 250);
282    }
283})();

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.