diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index 00b4deb..d2c3aed 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -87,6 +87,41 @@ func eq(a, b []byte) bool { 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() { _ = mime.AddExtensionType(".css", "text/css") _ = mime.AddExtensionType(".js", "application/javascript") @@ -270,22 +305,26 @@ func main() { continue } viewers := 0 - { - 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) - cancel2() - if err == nil { + + // 1) Versuche echte HLS-Clients zu zählen + ctx2, cancel2 := context.WithTimeout(context.Background(), 2*time.Second) + if v, err := hlsViewers(ctx2, mtxAPI, os.Getenv("MTX_API_USER"), os.Getenv("MTX_API_PASS"), p.Name); err == nil { + viewers = v + } + 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 - } 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{ Name: p.Name, - Live: p.Live(), // deine bisherige Live-Logik bleibt erhalten - Viewers: viewers, // echte Leserzahl aus MediaMTX + Live: p.Live(), + Viewers: viewers, }) } @@ -397,22 +436,26 @@ func apiStreams(w http.ResponseWriter, r *http.Request) { continue } viewers := 0 - { - 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) - cancel2() - if err == nil { + + // 1) Versuche echte HLS-Clients zu zählen + ctx2, cancel2 := context.WithTimeout(context.Background(), 2*time.Second) + if v, err := hlsViewers(ctx2, mtxAPI, os.Getenv("MTX_API_USER"), os.Getenv("MTX_API_PASS"), p.Name); err == nil { + viewers = v + } + 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 - } 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{ Name: p.Name, - Live: p.Live(), // deine bisherige Live-Logik bleibt erhalten - Viewers: viewers, // echte Leserzahl aus MediaMTX + Live: p.Live(), + Viewers: viewers, }) } w.Header().Set("Content-Type", "application/json") diff --git a/compose.yml b/compose.yml index c154260..7489d50 100644 --- a/compose.yml +++ b/compose.yml @@ -1,45 +1,63 @@ version: "3.9" - services: mediamtx: - image: bluenviron/mediamtx:1.9.2 + image: bluenviron/mediamtx:1.15.0 container_name: mediamtx restart: unless-stopped ports: - - "1935:1935" # RTMP ingest - - "8888:8888" # HLS/HTTP (optional extern, Dashboard proxyt ohnehin) + - 1935:1935 # RTMP ingest + - 8888:8888 # HLS + - 9997:9997 # API + - 9998:9998 # metrics (optional) volumes: - - ./mediamtx.yml:/mediamtx.yml:ro - command: ["/mediamtx", "/mediamtx.yml"] - + - /docker/streaming/mediamtx.yml:/mediamtx.yml:ro + command: + - /mediamtx.yml + networks: + - traefik-net dashboard: - build: - context: . - dockerfile: Dockerfile + image: git.send.nrw/sendnrw/nginx-stream-server:latest container_name: go-stream-dashboard 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: # Dashboard - LISTEN=:8080 - BASIC_AUTH_USER=${BASIC_AUTH_USER:-} - BASIC_AUTH_PASS=${BASIC_AUTH_PASS:-} - STREAMS=${STREAMS:-} - # MediaMTX Endpunkte (im Compose‑Netzwerk erreichbar) - MTX_API=http://mediamtx:9997 - MTX_HLS=http://mediamtx:8888 - - MTX_API_USER=${MTX_API_USER:-admin} - - MTX_API_PASS=${MTX_API_PASS:-starkes-passwort} - ports: - - "8080:8080" + - MTX_API_USER=admin + - MTX_API_PASS=admin + #ports: + #- "8080:8080" depends_on: - mediamtx - healthcheck: - test: ["CMD", "/app/dashboard", "-healthcheck"] # optionaler Schalter, siehe unten - interval: 15s - timeout: 3s - retries: 5 + #healthcheck: + #test: + #- CMD + #- /app/dashboard + #- -healthcheck + # optionaler Schalter, siehe unten + #interval: 15s + #timeout: 3s + #retries: 5 security_opt: - no-new-privileges:true read_only: true tmpfs: - - /tmp \ No newline at end of file + - /tmp + networks: + - traefik-net +networks: + traefik-net: + external: true \ No newline at end of file diff --git a/mediamtx.yml b/mediamtx.yml index 9ee0da6..2ab317b 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -1,29 +1,64 @@ -# lauscht auf den Standard-Ports -rtmp: yes -hls: yes -# optional: enable ll-hls: yes -httpAddress: :8888 -rtmpAddress: :1935 +# --- Logging --- +logLevel: info - -# Control API (für Dashboard‑Status) +# --- Control API (geschützt) --- api: yes apiAddress: :9997 -# Auth für API empfehlenswert -apiUser: admin -apiPass: starkes-passwort +apiEncryption: no +apiAllowOrigin: "*" - -# Metriken (optional) +# --- Metrics (optional) --- metrics: yes 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: - stream1: - publishUser: ingest1 - publishPass: supersecret1 - stream2: - publishUser: ingest2 - publishPass: supersecret2 \ No newline at end of file + # keine Legacy-Creds mehr hier! + all: {} \ No newline at end of file