2020-06-11 22:33:20 +02:00
|
|
|
/*
|
|
|
|
Viewer counting doesn't just count the number of websocket clients that are currently connected,
|
|
|
|
because people may be watching the stream outside of the web browser via any HLS video client.
|
|
|
|
Instead we keep track of requests and consider each unique IP as a "viewer".
|
|
|
|
As a signal, however, we do use the websocket disconnect from a client as a signal that a viewer
|
|
|
|
dropped and we call ViewerDisconnected().
|
|
|
|
*/
|
|
|
|
|
2020-06-11 08:52:55 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2020-06-11 22:33:20 +02:00
|
|
|
"fmt"
|
2020-06-11 08:52:55 +02:00
|
|
|
"io/ioutil"
|
2020-06-11 22:33:20 +02:00
|
|
|
"log"
|
2020-06-11 08:52:55 +02:00
|
|
|
"math"
|
|
|
|
"os"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Stats struct {
|
|
|
|
streamConnected bool `json:"-"`
|
|
|
|
SessionMaxViewerCount int `json:"sessionMaxViewerCount"`
|
|
|
|
OverallMaxViewerCount int `json:"overallMaxViewerCount"`
|
|
|
|
LastDisconnectTime time.Time `json:"lastDisconnectTime"`
|
2020-06-11 22:33:20 +02:00
|
|
|
|
|
|
|
clients map[string]time.Time
|
2020-06-11 08:52:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Stats) Setup() {
|
2020-06-11 22:33:20 +02:00
|
|
|
s.clients = make(map[string]time.Time)
|
|
|
|
|
|
|
|
statsSaveTimer := time.NewTicker(2 * time.Minute)
|
2020-06-11 08:52:55 +02:00
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
select {
|
2020-06-11 22:33:20 +02:00
|
|
|
case <-statsSaveTimer.C:
|
2020-06-11 08:52:55 +02:00
|
|
|
s.save()
|
2020-06-11 22:33:20 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
staleViewerPurgeTimer := time.NewTicker(5 * time.Second)
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-staleViewerPurgeTimer.C:
|
|
|
|
s.purgeStaleViewers()
|
2020-06-11 08:52:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2020-06-11 22:33:20 +02:00
|
|
|
func (s *Stats) purgeStaleViewers() {
|
|
|
|
for clientID, lastConnectedtime := range s.clients {
|
|
|
|
timeSinceLastActive := time.Since(lastConnectedtime).Minutes()
|
|
|
|
if timeSinceLastActive > 2 {
|
|
|
|
s.ViewerDisconnected(clientID)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2020-06-11 08:52:55 +02:00
|
|
|
}
|
|
|
|
|
2020-06-11 22:33:20 +02:00
|
|
|
func (s *Stats) IsStreamConnected() bool {
|
|
|
|
return s.streamConnected
|
2020-06-11 08:52:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Stats) GetViewerCount() int {
|
2020-06-11 22:33:20 +02:00
|
|
|
return len(s.clients)
|
2020-06-11 08:52:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Stats) GetSessionMaxViewerCount() int {
|
|
|
|
return s.SessionMaxViewerCount
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Stats) GetOverallMaxViewerCount() int {
|
|
|
|
return s.OverallMaxViewerCount
|
|
|
|
}
|
|
|
|
|
2020-06-11 22:33:20 +02:00
|
|
|
func (s *Stats) SetClientActive(clientID string) {
|
|
|
|
fmt.Println("Marking client active:", clientID)
|
|
|
|
|
|
|
|
s.clients[clientID] = time.Now()
|
|
|
|
s.SessionMaxViewerCount = int(math.Max(float64(s.GetViewerCount()), float64(s.SessionMaxViewerCount)))
|
|
|
|
s.OverallMaxViewerCount = int(math.Max(float64(s.SessionMaxViewerCount), float64(s.OverallMaxViewerCount)))
|
|
|
|
|
|
|
|
fmt.Println("Now", s.GetViewerCount(), "clients connected.")
|
2020-06-11 08:52:55 +02:00
|
|
|
}
|
|
|
|
|
2020-06-11 22:33:20 +02:00
|
|
|
func (s *Stats) ViewerDisconnected(clientID string) {
|
|
|
|
log.Println("Removed client", clientID)
|
|
|
|
|
|
|
|
delete(s.clients, clientID)
|
2020-06-11 08:52:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Stats) StreamConnected() {
|
|
|
|
s.streamConnected = true
|
|
|
|
|
|
|
|
timeSinceDisconnect := time.Since(s.LastDisconnectTime).Minutes()
|
|
|
|
if timeSinceDisconnect > 15 {
|
|
|
|
s.SessionMaxViewerCount = 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Stats) StreamDisconnected() {
|
|
|
|
s.streamConnected = false
|
|
|
|
s.LastDisconnectTime = time.Now()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Stats) save() {
|
|
|
|
jsonData, err := json.Marshal(&s)
|
|
|
|
verifyError(err)
|
|
|
|
|
|
|
|
f, err := os.Create("config/stats.json")
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
verifyError(err)
|
|
|
|
|
|
|
|
_, err = f.Write(jsonData)
|
|
|
|
verifyError(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getSavedStats() *Stats {
|
|
|
|
filePath := "config/stats.json"
|
|
|
|
|
|
|
|
if !fileExists(filePath) {
|
|
|
|
return &Stats{}
|
|
|
|
}
|
|
|
|
|
|
|
|
jsonFile, err := ioutil.ReadFile(filePath)
|
|
|
|
|
|
|
|
var stats Stats
|
|
|
|
err = json.Unmarshal(jsonFile, &stats)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &stats
|
|
|
|
}
|