226 lines
6.0 KiB
JavaScript
226 lines
6.0 KiB
JavaScript
(() => {
|
|
'use strict';
|
|
|
|
/**
|
|
* @typedef {Object} Upstream
|
|
* @property {string} region - The region of the upstream.
|
|
* @property {string} url - The URL of the upstream.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} RestreamConfig
|
|
* @property {Upstream[]} upstreams - Array of upstreams.
|
|
* @property {string} playlist - The playlist URL.
|
|
*/
|
|
|
|
/** @type {RestreamConfig} */
|
|
let restream_config = {};
|
|
|
|
// 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 + 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 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 + restream_config.playlist;
|
|
|
|
if (restream_config.activeURL === playlistURL) return;
|
|
|
|
console.log('Reloading video from', playlistURL);
|
|
if (typeof window.player === 'undefined') {
|
|
window.player = videojs('video', {
|
|
liveui: true,
|
|
liveTracker: {
|
|
liveTolerance: 5,
|
|
},
|
|
});
|
|
window.player.volume(0.5);
|
|
}
|
|
window.player.src({
|
|
src: playlistURL,
|
|
type: 'application/x-mpegURL',
|
|
});
|
|
window.player.play();
|
|
try {
|
|
window.player.hlsQualitySelector({
|
|
displayCurrentQuality: true,
|
|
});
|
|
} catch (e) {}
|
|
|
|
restream_config.activeURL = playlistURL;
|
|
mirrorLink.href = playlistURL;
|
|
mirrorLink.innerText = '[m3u8]';
|
|
};
|
|
|
|
const doViewCount = async () => {
|
|
const viewCount = document.getElementById('viewer-count-value');
|
|
|
|
const activeMirrorUrl = document.getElementById('mirror-select').value;
|
|
if (!activeMirrorUrl) {
|
|
await sleep(1000);
|
|
return;
|
|
}
|
|
|
|
const response = await fetchWithTimeout('/viewcount', {
|
|
method: 'POST',
|
|
timeout: 1000,
|
|
mode: 'cors',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ mirror: activeMirrorUrl }),
|
|
});
|
|
const { total_count, by_mirror } = await response.json();
|
|
viewCount.innerText = total_count;
|
|
|
|
restream_config.upstreams.forEach((stream) => {
|
|
const { url } = stream;
|
|
stream.active_viewers = by_mirror[url] ?? 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 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');
|
|
|
|
// Fetch stream configuration
|
|
restream_config = await fetch('/config').then((r) => r.json());
|
|
const { upstreams } = 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();
|
|
})();
|