From 2ef9f0ecef06037e0ef0832279f01c623085ba84 Mon Sep 17 00:00:00 2001 From: Hizkia Felix Winata Date: Sun, 30 Jan 2022 23:03:01 +0800 Subject: [PATCH] feat(player): per-mirror viewer count --- player/main.go | 105 +++++++++++++++++++++++++++++++++++---- player/static/index.html | 23 +++++++++ player/static/player.css | 42 ++++++++++++++++ player/static/player.js | 74 ++++++++++++++++++++++----- 4 files changed, 220 insertions(+), 24 deletions(-) diff --git a/player/main.go b/player/main.go index 60d0d83..d5cbfeb 100644 --- a/player/main.go +++ b/player/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "flag" "fmt" "net/http" @@ -8,30 +9,69 @@ import ( "time" ) +func getIp(r *http.Request) string { + // Get the remote address + var ip = r.RemoteAddr + + // If behind a proxy, get the real IP + if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" { + ip = fwd + } + + return ip +} + +type Viewer struct { + LastActive int64 + ActiveMirror int +} + +type ViewCountRequest struct { + ActiveMirror int `json:"active_mirror"` +} + +type ViewCountResponse struct { + TotalCount int `json:"total_count"` + ByMirror map[int]int `json:"by_mirror"` +} + func main() { var addr = flag.String("addr", ":8080", "http service address") flag.Parse() - // Map of active viewers and their last active time - var viewers = make(map[string]int64) + var viewers = make(map[string]Viewer) + var mirrorActivity = make(map[int]int) + // Handle view counting http.HandleFunc("/viewcount", func(w http.ResponseWriter, r *http.Request) { // Get the current time var now = time.Now().Unix() // Get the remote address - var ip = r.RemoteAddr + var ip = getIp(r) - // If behind a proxy, get the real IP - if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" { - ip = fwd + // Decode body JSON as a ViewerMetric + var metric ViewCountRequest + if err := json.NewDecoder(r.Body).Decode(&metric); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return } // Update the last active time - viewers[ip] = now + viewers[ip] = Viewer{ + LastActive: now, + ActiveMirror: metric.ActiveMirror, + } - // Respond with the number of viewers - fmt.Fprintf(w, "%d", len(viewers)) + // Respond + var response = ViewCountResponse{ + TotalCount: len(viewers), + ByMirror: mirrorActivity, + } + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } }) // Periodically remove inactive viewers @@ -43,8 +83,8 @@ func main() { var now = time.Now().Unix() // Remove any viewers that have not been active in the last 10 seconds - for ip, lastActive := range viewers { - if now-lastActive > 10 { + for ip, viewer := range viewers { + if now-viewer.LastActive > 10 { delete(viewers, ip) } } @@ -77,6 +117,49 @@ func main() { } }() + // Periodically aggregate viewer count per mirror + go func() { + // Prepare file to log to + var f, ferr = os.OpenFile("viewers_per_mirror.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if ferr != nil { + fmt.Println("Error opening file:", ferr) + fmt.Println("mirror statistics will not be logged") + return + } + + // Loop forever + for { + var now = time.Now().Format(time.RFC3339) + var counts = make(map[int]int) + + // Loop through all viewers + for _, viewer := range viewers { + // Increment the mirror count + counts[viewer.ActiveMirror]++ + } + + // Check if the counts have changed + var changed = false + for mirror, count := range counts { + if count != mirrorActivity[mirror] { + changed = true + break + } + } + + // If the counts have changed, log them + if changed { + for mirror, count := range counts { + fmt.Fprintf(f, "%s\t%d\t%d\n", now, mirror, count) + } + } + + // Update the mirror activity + mirrorActivity = counts + time.Sleep(time.Second) + } + }() + // Handle static files http.Handle("/", http.FileServer(http.Dir("./static"))) diff --git a/player/static/index.html b/player/static/index.html index 7a45d68..fc3244a 100644 --- a/player/static/index.html +++ b/player/static/index.html @@ -48,6 +48,29 @@ 0 viewers +
+ + +
+ diff --git a/player/static/player.css b/player/static/player.css index a9ccec0..0881f79 100644 --- a/player/static/player.css +++ b/player/static/player.css @@ -53,6 +53,28 @@ a:active { text-decoration: underline; } +.popup-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(10px); + z-index: 99; +} +.popup-box { + background-color: #333; + padding: 1.5em 2em; + border-radius: 0.5em; + + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 100; +} + #viewer-count { flex: 1; text-align: right; @@ -65,3 +87,23 @@ a:active { background-color: black; overflow: hidden; } + +.loader { + width: 48px; + height: 48px; + border: 5px solid #fff; + border-bottom-color: #ff3d00; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; +} + +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/player/static/player.js b/player/static/player.js index 7ddf9d6..79cc006 100644 --- a/player/static/player.js +++ b/player/static/player.js @@ -44,6 +44,7 @@ const updateMirrorList = () => { const mirrorSelect = document.getElementById('mirror-select'); + const current = mirrorSelect.value; // Clear the current list while (mirrorSelect.firstChild) { @@ -56,13 +57,19 @@ option.value = upstream.url; option.text = `[${upstream.region}] ${upstream.url}` + - (typeof upstream.latency === 'number' - ? ` (${Math.floor(upstream.latency)}ms)` - : upstream.latency === null - ? ' (unavailable)' - : ''); + '(' + + [ + 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 = () => { @@ -85,17 +92,46 @@ mirrorLink.innerText = '[m3u8]'; }; - const viewCountLoop = async () => { + const doViewCount = async () => { const viewCount = document.getElementById('viewer-count-value'); - const url = '/viewcount'; + 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) { - const response = await fetchWithTimeout(url, { - timeout: 1000, - mode: 'cors', - }); - const count = Number(await response.text()); - viewCount.innerText = count; + try { + await doViewCount(); + } catch (e) { + console.error(e); + } await sleep(5000); } }; @@ -103,6 +139,8 @@ 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'); // Init viewer counter viewCountLoop(); @@ -118,11 +156,14 @@ const { upstreams } = window.restream_config; + let n = 0; for (const upstream of upstreams) { + n += 1; const playlistURL = upstream.url + window.restream_config.playlist; const latency = await measureLatency(playlistURL, 3); upstream.latency = latency; updateMirrorList(); + mirrorLoadProgress.innerText = '(' + n + '/' + upstreams.length + ')'; } // Choose the fastest upstream server @@ -131,6 +172,13 @@ .sort((a, b) => a.latency - b.latency)[0]; mirrorSelect.value = upstream.url; reloadVideo(); + + // Destroy the popup + mirrorPopup.style.opacity = 0; + mirrorPopup.style.pointerEvents = 'none'; + setTimeout(() => { + mirrorPopup.remove(); + }, 1000); }; if (document.readyState === 'loading')