Files
restream-tools/player/static/player.js
T
2022-01-31 01:27:43 +08:00

212 lines
5.9 KiB
JavaScript

(() => {
'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 video = document.getElementById('video');
const mirrorSelect = document.getElementById('mirror-select');
const mirrorLink = document.getElementById('mirror-link');
const playlistURL = mirrorSelect.value + window.restream_config.playlist;
console.log('Reloading video from', playlistURL);
if (Hls.isSupported()) {
window.restream_config.hls.destroy();
window.restream_config.hls = new Hls();
window.restream_config.hls.attachMedia(video);
window.restream_config.hls.loadSource(playlistURL);
} else {
video.src = 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 video = document.getElementById('video');
const mirrorSelect = document.getElementById('mirror-select');
const mirrorPopup = document.getElementById('mirror-popup');
const mirrorLoadProgress = document.getElementById('mirror-load-progress');
const { upstreams } = window.restream_config;
// Set up hls.js
if (Hls.isSupported()) {
window.restream_config.hls = new Hls();
window.restream_config.hls.attachMedia(video);
}
// Set up event listeners
mirrorSelect.addEventListener('change', () => reloadVideo());
// Measure latency
let n = 0;
for (const upstream of upstreams) {
n += 1;
await measureUpstreamLatency(upstream);
mirrorLoadProgress.innerText = '(' + n + '/' + upstreams.length + ')';
}
// 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();
})();