fix-11 - viewer-count
All checks were successful
release-tag / release-image (push) Successful in 1m58s
All checks were successful
release-tag / release-image (push) Successful in 1m58s
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
58
compose.yml
58
compose.yml
@@ -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 Compose‑Netzwerk 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
|
||||||
75
mediamtx.yml
75
mediamtx.yml
@@ -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 Dashboard‑Status)
|
|
||||||
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 # 1–2s (Segment = GOP-Vielfaches, s.u.)
|
||||||
|
hlsPartDuration: 200ms # 200–350ms
|
||||||
|
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
|
|
||||||
Reference in New Issue
Block a user