fix-11 - viewer-count
All checks were successful
release-tag / release-image (push) Successful in 1m58s

This commit is contained in:
2025-09-21 22:13:20 +02:00
parent 5c21131f97
commit 949f07d420
3 changed files with 159 additions and 63 deletions

View File

@@ -87,6 +87,41 @@ func eq(a, b []byte) bool {
return true return true
} }
type mtxHLSPayload struct {
// v1.15.0 liefert einen Muxer mit Clients-Liste (Feldname kann "clients" heißen)
Item struct {
Name string `json:"name"`
Clients []interface{} `json:"clients"` // Anzahl = echte HLS-Zuschauer
} `json:"item"`
// manche Builds liefern flach:
Name string `json:"name"`
Clients []interface{} `json:"clients"`
}
func hlsViewers(ctx context.Context, base, user, pass, name string) (int, error) {
u := strings.TrimRight(base, "/") + "/v3/hlsmuxers/get/" + url.PathEscape(name)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if user != "" || pass != "" {
req.SetBasicAuth(user, pass)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return 0, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return 0, fmt.Errorf("hlsmuxers/get %s: %s", name, res.Status)
}
var out mtxHLSPayload
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
return 0, err
}
if out.Item.Clients != nil {
return len(out.Item.Clients), nil
}
return len(out.Clients), nil
}
func init() { func init() {
_ = mime.AddExtensionType(".css", "text/css") _ = mime.AddExtensionType(".css", "text/css")
_ = mime.AddExtensionType(".js", "application/javascript") _ = mime.AddExtensionType(".js", "application/javascript")
@@ -270,22 +305,26 @@ func main() {
continue continue
} }
viewers := 0 viewers := 0
{
// 1) Versuche echte HLS-Clients zu zählen
ctx2, cancel2 := context.WithTimeout(context.Background(), 2*time.Second) ctx2, cancel2 := context.WithTimeout(context.Background(), 2*time.Second)
v, err := fetchMTXViewers(ctx2, mtxAPI, os.Getenv("MTX_API_USER"), os.Getenv("MTX_API_PASS"), p.Name) if v, err := hlsViewers(ctx2, mtxAPI, os.Getenv("MTX_API_USER"), os.Getenv("MTX_API_PASS"), p.Name); err == nil {
cancel2() viewers = v
if err == nil { }
cancel2()
// 2) Fallback: wenn HLS-API nicht greift (z.B. stream per RTSP/WebRTC gelesen),
// nimm Pfad-Reader (kann >0 bei Nicht-HLS sein, bei HLS meist 1 = Muxer)
if viewers == 0 {
if v := p.Viewers(); v > 0 {
viewers = v viewers = v
} else {
// Fallback: falls API kurz zickt, nehme zur Not den alten Wert,
// aber brich das Live-UI nicht: viewers bleibt 0, Live bleibt p.Live()
// log.Printf("warn: mtx viewers %s: %v", p.Name, err)
} }
} }
out.Items = append(out.Items, item{ out.Items = append(out.Items, item{
Name: p.Name, Name: p.Name,
Live: p.Live(), // deine bisherige Live-Logik bleibt erhalten Live: p.Live(),
Viewers: viewers, // echte Leserzahl aus MediaMTX Viewers: viewers,
}) })
} }
@@ -397,22 +436,26 @@ func apiStreams(w http.ResponseWriter, r *http.Request) {
continue continue
} }
viewers := 0 viewers := 0
{
// 1) Versuche echte HLS-Clients zu zählen
ctx2, cancel2 := context.WithTimeout(context.Background(), 2*time.Second) ctx2, cancel2 := context.WithTimeout(context.Background(), 2*time.Second)
v, err := fetchMTXViewers(ctx2, mtxAPI, os.Getenv("MTX_API_USER"), os.Getenv("MTX_API_PASS"), p.Name) if v, err := hlsViewers(ctx2, mtxAPI, os.Getenv("MTX_API_USER"), os.Getenv("MTX_API_PASS"), p.Name); err == nil {
cancel2() viewers = v
if err == nil { }
cancel2()
// 2) Fallback: wenn HLS-API nicht greift (z.B. stream per RTSP/WebRTC gelesen),
// nimm Pfad-Reader (kann >0 bei Nicht-HLS sein, bei HLS meist 1 = Muxer)
if viewers == 0 {
if v := p.Viewers(); v > 0 {
viewers = v viewers = v
} else {
// Fallback: falls API kurz zickt, nehme zur Not den alten Wert,
// aber brich das Live-UI nicht: viewers bleibt 0, Live bleibt p.Live()
// log.Printf("warn: mtx viewers %s: %v", p.Name, err)
} }
} }
out.Items = append(out.Items, item{ out.Items = append(out.Items, item{
Name: p.Name, Name: p.Name,
Live: p.Live(), // deine bisherige Live-Logik bleibt erhalten Live: p.Live(),
Viewers: viewers, // echte Leserzahl aus MediaMTX Viewers: viewers,
}) })
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")

View File

@@ -1,45 +1,63 @@
version: "3.9" version: "3.9"
services: services:
mediamtx: mediamtx:
image: bluenviron/mediamtx:1.9.2 image: bluenviron/mediamtx:1.15.0
container_name: mediamtx container_name: mediamtx
restart: unless-stopped restart: unless-stopped
ports: ports:
- "1935:1935" # RTMP ingest - 1935:1935 # RTMP ingest
- "8888:8888" # HLS/HTTP (optional extern, Dashboard proxyt ohnehin) - 8888:8888 # HLS
- 9997:9997 # API
- 9998:9998 # metrics (optional)
volumes: volumes:
- ./mediamtx.yml:/mediamtx.yml:ro - /docker/streaming/mediamtx.yml:/mediamtx.yml:ro
command: ["/mediamtx", "/mediamtx.yml"] command:
- /mediamtx.yml
networks:
- traefik-net
dashboard: dashboard:
build: image: git.send.nrw/sendnrw/nginx-stream-server:latest
context: .
dockerfile: Dockerfile
container_name: go-stream-dashboard container_name: go-stream-dashboard
restart: unless-stopped restart: unless-stopped
labels:
- traefik.enable=true
- traefik.http.routers.streaming.rule=Host(`streaming.domain.tld`)
- traefik.http.routers.streaming.service=streaming
- traefik.http.services.streaming.loadbalancer.server.port=8080
- traefik.http.routers.streaming.entrypoints=websecure
- traefik.http.routers.streaming.tls=true
- traefik.http.routers.streaming.tls.certresolver=letsencrypt
- traefik.protocol=http
environment: environment:
# Dashboard # Dashboard
- LISTEN=:8080 - LISTEN=:8080
- BASIC_AUTH_USER=${BASIC_AUTH_USER:-} - BASIC_AUTH_USER=${BASIC_AUTH_USER:-}
- BASIC_AUTH_PASS=${BASIC_AUTH_PASS:-} - BASIC_AUTH_PASS=${BASIC_AUTH_PASS:-}
- STREAMS=${STREAMS:-} - STREAMS=${STREAMS:-}
# MediaMTX Endpunkte (im ComposeNetzwerk erreichbar)
- MTX_API=http://mediamtx:9997 - MTX_API=http://mediamtx:9997
- MTX_HLS=http://mediamtx:8888 - MTX_HLS=http://mediamtx:8888
- MTX_API_USER=${MTX_API_USER:-admin} - MTX_API_USER=admin
- MTX_API_PASS=${MTX_API_PASS:-starkes-passwort} - MTX_API_PASS=admin
ports: #ports:
- "8080:8080" #- "8080:8080"
depends_on: depends_on:
- mediamtx - mediamtx
healthcheck: #healthcheck:
test: ["CMD", "/app/dashboard", "-healthcheck"] # optionaler Schalter, siehe unten #test:
interval: 15s #- CMD
timeout: 3s #- /app/dashboard
retries: 5 #- -healthcheck
# optionaler Schalter, siehe unten
#interval: 15s
#timeout: 3s
#retries: 5
security_opt: security_opt:
- no-new-privileges:true - no-new-privileges:true
read_only: true read_only: true
tmpfs: tmpfs:
- /tmp - /tmp
networks:
- traefik-net
networks:
traefik-net:
external: true

View File

@@ -1,29 +1,64 @@
# lauscht auf den Standard-Ports # --- Logging ---
rtmp: yes logLevel: info
hls: yes
# optional: enable ll-hls: yes
httpAddress: :8888
rtmpAddress: :1935
# --- Control API (geschützt) ---
# Control API (für DashboardStatus)
api: yes api: yes
apiAddress: :9997 apiAddress: :9997
# Auth für API empfehlenswert apiEncryption: no
apiUser: admin apiAllowOrigin: "*"
apiPass: starkes-passwort
# --- Metrics (optional) ---
# Metriken (optional)
metrics: yes metrics: yes
metricsAddress: :9998 metricsAddress: :9998
metricsEncryption: no
metricsAllowOrigin: "*"
# --- RTMP (Ingest) ---
rtmp: yes
rtmpAddress: :1935
rtmpEncryption: "no"
# Allgemeine Pfadeinstellungen (alle Streams) # --- HLS (Playback) ---
hls: yes
hlsAddress: :8888
hlsEncryption: no
hlsVariant: lowLatency # <— statt mpegts
hlsSegmentDuration: 1s # 12s (Segment = GOP-Vielfaches, s.u.)
hlsPartDuration: 200ms # 200350ms
hlsSegmentCount: 7 # hat keinen Einfluss auf Latenz, aber genügt
hlsAlwaysRemux: yes # schneller Join / weniger Anlaufzeit
# Nicht benötigt
rtsp: no
webrtc: no
srt: no
pprof: no
playback: no
# --- AUTH: Intern (API + Publish) ---
# authMethod: internal
authInternalUsers:
# Admin für API (und optional metrics)
- user: sha256:jGl25bVBBBW96Qi9Te4V37Fnqchz/Eu4qB9vKrRIqRg=
pass: sha256:jGl25bVBBBW96Qi9Te4V37Fnqchz/Eu4qB9vKrRIqRg=
permissions:
- action: api
- action: metrics
# Publisher für stream1
- user: sha256:CCgj5KGMqEZgxOc5o4drwj9BVDg46eOLxuPMHhSRKAc=
pass: sha256:XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg=
permissions:
- action: publish
path: stream1
# NEU: jeder darf lesen (HLS/RTMP/RTSP)
- user: any
permissions:
- action: read
path:
# --- Paths ---
paths: paths:
stream1: # keine Legacy-Creds mehr hier!
publishUser: ingest1 all: {}
publishPass: supersecret1
stream2:
publishUser: ingest2
publishPass: supersecret2