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.