(() => { 'use strict'; // Utility functions const fetchWithTimeout = async (resource, options = {}) => { const { timeout = 30000 } = options; const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); const response = await fetch(resource, { ...options, signal: controller.signal, }); clearTimeout(id); return response; }; // Measure the average time it takes to get a response from the url const ping = async (url) => { const start = performance.now(); await fetchWithTimeout(url, { timeout: 1000, mode: 'cors', }); const end = performance.now(); return end - start; }; // Async sleep const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const measureLatency = async (url, count, gap = 100) => { const results = []; for (let i = 0; i < count; i++) { const latency = await ping(url).catch(() => null); if (latency) results.push(latency); await sleep(gap); } if (results.length === 0) return null; return results.reduce((a, b) => a + b, 0) / count; }; const measureUpstreamLatency = async (upstream, n = 3) => { const playlistURL = upstream.url + window.restream_config.playlist; const latency = await measureLatency(playlistURL, n); upstream.latency = latency; updateMirrorList(); }; const updateMirrorList = () => { const mirrorSelect = document.getElementById('mirror-select'); const current = mirrorSelect.value; // Clear the current list while (mirrorSelect.firstChild) { mirrorSelect.removeChild(mirrorSelect.firstChild); } // Add each upstream server to the list for (const upstream of window.restream_config.upstreams) { const option = document.createElement('option'); option.value = upstream.url; option.text = `[${upstream.region}] ${upstream.url} (` + [ typeof upstream.latency === 'number' ? Math.floor(upstream.latency) + 'ms' : 'error', (upstream.active_viewers || '0') + ' viewers', ].join(', ') + ')'; mirrorSelect.add(option); } // Restore the previous selection if (current) mirrorSelect.value = current; }; const reloadVideo = () => { const mirrorSelect = document.getElementById('mirror-select'); const mirrorLink = document.getElementById('mirror-link'); const playlistURL = mirrorSelect.value + window.restream_config.playlist; if (window.restream_config.activeURL === playlistURL) return; console.log('Reloading video from', playlistURL); if (typeof window.player === 'undefined') { window.player = videojs('video', { liveui: true, }); window.player.hlsQualitySelector({ displayCurrentQuality: true, }); window.player.volume(0.5); } window.player.src({ src: playlistURL, type: 'application/x-mpegURL', }); window.player.play(); window.restream_config.activeURL = playlistURL; mirrorLink.href = playlistURL; mirrorLink.innerText = '[m3u8]'; }; const doViewCount = async () => { const viewCount = document.getElementById('viewer-count-value'); const active_mirror = window.restream_config.upstreams.findIndex( (upstream) => upstream.url === document.getElementById('mirror-select').value ); if (active_mirror < 0) { await sleep(1000); return; } const response = await fetchWithTimeout('/viewcount', { method: 'POST', timeout: 1000, mode: 'cors', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ active_mirror }), }); const { total_count, by_mirror } = await response.json(); viewCount.innerText = total_count; for (let i = 0; i < window.restream_config.upstreams.length; i++) { window.restream_config.upstreams[i].active_viewers = i in by_mirror ? by_mirror[i] : 0; } updateMirrorList(); }; const viewCountLoop = async () => { while (true) { try { await doViewCount(); } catch (e) { console.error(e); } await sleep(5000); } }; const latencyMeasurementLoop = async () => { while (true) { try { for (const upstream of window.restream_config.upstreams) { await measureUpstreamLatency(upstream, 1); } } catch (e) { console.error(e); } await sleep(10000); } }; const init = async () => { const mirrorSelect = document.getElementById('mirror-select'); const mirrorPopup = document.getElementById('mirror-popup'); const { upstreams } = window.restream_config; // Set up event listeners mirrorSelect.addEventListener('change', () => reloadVideo()); // Measure latency await Promise.all( upstreams.map((upstream) => measureUpstreamLatency(upstream)) ); // Choose the fastest upstream server const upstream = upstreams .filter((upstream) => typeof upstream.latency === 'number') .sort((a, b) => a.latency - b.latency)[0]; if (!upstream) { document.querySelector('#mirror-popup .popup-box').innerText = 'No upstream servers found'; return; } mirrorSelect.value = upstream.url; reloadVideo(); // Destroy the popup mirrorPopup.style.opacity = 0; mirrorPopup.style.pointerEvents = 'none'; setTimeout(() => { mirrorPopup.remove(); }, 1000); // Init loops viewCountLoop(); latencyMeasurementLoop(); }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })();