0

Add support for active viewer details API. Closes #1477 (#1747)

This commit is contained in:
Gabe Kangas 2022-03-06 17:31:47 -08:00 committed by GitHub
parent 92041c4c23
commit 98fce01b52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 96 additions and 23 deletions

View File

@ -4,7 +4,11 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/metrics" "github.com/owncast/owncast/metrics"
"github.com/owncast/owncast/models"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -17,3 +21,23 @@ func GetViewersOverTime(w http.ResponseWriter, r *http.Request) {
log.Errorln(err) log.Errorln(err)
} }
} }
// GetActiveViewers returns currently connected clients.
func GetActiveViewers(w http.ResponseWriter, r *http.Request) {
c := core.GetActiveViewers()
viewers := []models.Viewer{}
for _, v := range c {
viewers = append(viewers, *v)
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(viewers); err != nil {
controllers.InternalErrorHandler(w, err)
}
}
// ExternalGetActiveViewers returns currently connected clients.
func ExternalGetActiveViewers(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
GetConnectedChatClients(w, r)
}

View File

@ -10,6 +10,7 @@ import (
"github.com/owncast/owncast/config" "github.com/owncast/owncast/config"
"github.com/owncast/owncast/core" "github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/router/middleware" "github.com/owncast/owncast/router/middleware"
"github.com/owncast/owncast/utils" "github.com/owncast/owncast/utils"
) )
@ -42,8 +43,8 @@ func HandleHLSRequest(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-mpegURL") w.Header().Set("Content-Type", "application/x-mpegURL")
// Use this as an opportunity to mark this viewer as active. // Use this as an opportunity to mark this viewer as active.
id := utils.GenerateClientIDFromRequest(r) viewer := models.GenerateViewerFromRequest(r)
core.SetViewerIDActive(id) core.SetViewerActive(&viewer)
} else { } else {
cacheTime := utils.GetCacheDurationSecondsForPath(relativePath) cacheTime := utils.GetCacheDurationSecondsForPath(relativePath)
w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(cacheTime)) w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(cacheTime))

View File

@ -4,11 +4,11 @@ import (
"net/http" "net/http"
"github.com/owncast/owncast/core" "github.com/owncast/owncast/core"
"github.com/owncast/owncast/utils" "github.com/owncast/owncast/models"
) )
// Ping is fired by a client to show they are still an active viewer. // Ping is fired by a client to show they are still an active viewer.
func Ping(w http.ResponseWriter, r *http.Request) { func Ping(w http.ResponseWriter, r *http.Request) {
id := utils.GenerateClientIDFromRequest(r) viewer := models.GenerateViewerFromRequest(r)
core.SetViewerIDActive(id) core.SetViewerActive(&viewer)
} }

View File

@ -8,12 +8,14 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/geoip"
"github.com/owncast/owncast/models" "github.com/owncast/owncast/models"
) )
var ( var (
l = &sync.RWMutex{} l = &sync.RWMutex{}
_activeViewerPurgeTimeout = time.Second * 15 _activeViewerPurgeTimeout = time.Second * 15
_geoIPClient = geoip.NewClient()
) )
func setupStats() error { func setupStats() error {
@ -63,30 +65,45 @@ func RemoveChatClient(clientID string) {
l.Unlock() l.Unlock()
} }
// SetViewerIDActive sets a client as active and connected. // SetViewerActive sets a client as active and connected.
func SetViewerIDActive(id string) { func SetViewerActive(viewer *models.Viewer) {
// Don't update viewer counts if a live stream session is not active.
if !_stats.StreamConnected {
return
}
l.Lock() l.Lock()
defer l.Unlock() defer l.Unlock()
_stats.Viewers[id] = time.Now() // Asynchronously, optionally, fetch GeoIP data.
go func(viewer *models.Viewer) {
viewer.Geo = _geoIPClient.GetGeoFromIP(viewer.IPAddress)
}(viewer)
// Don't update viewer counts if a live stream session is not active. if _, exists := _stats.Viewers[viewer.ClientID]; exists {
if _stats.StreamConnected { _stats.Viewers[viewer.ClientID].LastSeen = time.Now()
_stats.SessionMaxViewerCount = int(math.Max(float64(len(_stats.Viewers)), float64(_stats.SessionMaxViewerCount))) } else {
_stats.OverallMaxViewerCount = int(math.Max(float64(_stats.SessionMaxViewerCount), float64(_stats.OverallMaxViewerCount))) _stats.Viewers[viewer.ClientID] = viewer
} }
_stats.SessionMaxViewerCount = int(math.Max(float64(len(_stats.Viewers)), float64(_stats.SessionMaxViewerCount)))
_stats.OverallMaxViewerCount = int(math.Max(float64(_stats.SessionMaxViewerCount), float64(_stats.OverallMaxViewerCount)))
}
// GetActiveViewers will return the active viewers.
func GetActiveViewers() map[string]*models.Viewer {
return _stats.Viewers
} }
func pruneViewerCount() { func pruneViewerCount() {
viewers := make(map[string]time.Time) viewers := make(map[string]*models.Viewer)
l.Lock() l.Lock()
defer l.Unlock() defer l.Unlock()
for viewerID := range _stats.Viewers { for viewerID, viewer := range _stats.Viewers {
viewerLastSeenTime := _stats.Viewers[viewerID] viewerLastSeenTime := _stats.Viewers[viewerID].LastSeen
if time.Since(viewerLastSeenTime) < _activeViewerPurgeTimeout { if time.Since(viewerLastSeenTime) < _activeViewerPurgeTimeout {
viewers[viewerID] = viewerLastSeenTime viewers[viewerID] = viewer
} }
} }
@ -112,7 +129,7 @@ func getSavedStats() models.Stats {
result := models.Stats{ result := models.Stats{
ChatClients: make(map[string]models.Client), ChatClients: make(map[string]models.Client),
Viewers: make(map[string]time.Time), Viewers: make(map[string]*models.Viewer),
SessionMaxViewerCount: data.GetPeakSessionViewerCount(), SessionMaxViewerCount: data.GetPeakSessionViewerCount(),
OverallMaxViewerCount: data.GetPeakOverallViewerCount(), OverallMaxViewerCount: data.GetPeakOverallViewerCount(),
LastDisconnectTime: savedLastDisconnectTime, LastDisconnectTime: savedLastDisconnectTime,

View File

@ -1,8 +1,6 @@
package models package models
import ( import (
"time"
"github.com/owncast/owncast/utils" "github.com/owncast/owncast/utils"
) )
@ -12,8 +10,8 @@ type Stats struct {
OverallMaxViewerCount int `json:"overallMaxViewerCount"` OverallMaxViewerCount int `json:"overallMaxViewerCount"`
LastDisconnectTime *utils.NullTime `json:"lastDisconnectTime"` LastDisconnectTime *utils.NullTime `json:"lastDisconnectTime"`
StreamConnected bool `json:"-"` StreamConnected bool `json:"-"`
LastConnectTime *utils.NullTime `json:"-"` LastConnectTime *utils.NullTime `json:"-"`
ChatClients map[string]Client `json:"-"` ChatClients map[string]Client `json:"-"`
Viewers map[string]time.Time `json:"-"` Viewers map[string]*Viewer `json:"-"`
} }

30
models/viewer.go Normal file
View File

@ -0,0 +1,30 @@
package models
import (
"net/http"
"time"
"github.com/owncast/owncast/geoip"
"github.com/owncast/owncast/utils"
)
// Viewer represents a single video viewer.
type Viewer struct {
FirstSeen time.Time `json:"firstSeen"`
LastSeen time.Time `json:"-"`
UserAgent string `json:"userAgent"`
IPAddress string `json:"ipAddress"`
ClientID string `json:"clientID"`
Geo *geoip.GeoDetails `json:"geo"`
}
// GenerateViewerFromRequest will return a chat client from a http request.
func GenerateViewerFromRequest(req *http.Request) Viewer {
return Viewer{
FirstSeen: time.Now(),
LastSeen: time.Now(),
UserAgent: req.UserAgent(),
IPAddress: utils.GetIPAddressFromRequest(req),
ClientID: utils.GenerateClientIDFromRequest(req),
}
}

View File

@ -97,6 +97,9 @@ func Start() error {
// Get viewer count over time // Get viewer count over time
http.HandleFunc("/api/admin/viewersOverTime", middleware.RequireAdminAuth(admin.GetViewersOverTime)) http.HandleFunc("/api/admin/viewersOverTime", middleware.RequireAdminAuth(admin.GetViewersOverTime))
// Get active viewers
http.HandleFunc("/api/admin/viewers", middleware.RequireAdminAuth(admin.GetActiveViewers))
// Get hardware stats // Get hardware stats
http.HandleFunc("/api/admin/hardwarestats", middleware.RequireAdminAuth(admin.GetHardwareStats)) http.HandleFunc("/api/admin/hardwarestats", middleware.RequireAdminAuth(admin.GetHardwareStats))