v2: mpegts.js + single-upstream fan-out hub (no FFmpeg/HLS)

Replaces the FFmpeg -> MediaMTX -> HLS pipeline with a raw MPEG-TS
fan-out architecture:

- Go stream.Hub holds ONE upstream connection per active channel and
  broadcasts the raw TS bytes to all connected browsers via /ts.
  The IPTV provider only ever sees one IP (the server), no matter how
  many viewers are watching (shared single-channel model).
- Frontend uses mpegts.js to play the raw TS in-browser via MSE,
  fixing HEVC/H.265 4K streams that HLS could not package.
- Dropped FFmpeg, MediaMTX, RTMP/RTSP, HLS proxy, playback/ingest
  modes, the OBS source toggle, and FFmpeg health stats.
- UI look preserved (fullscreen video, collapsible sidebar, status
  bar, upstream-down banner, hourly playlist auto-refresh).

Tradeoff: loses iOS Safari (no MSE/mpegts.js); fixes desktop/Android
playback of both H.264 and H.265.
This commit is contained in:
2026-06-12 19:33:06 -07:00
parent 88a189de34
commit 723ae1f1ac
18 changed files with 432 additions and 899 deletions

45
backend/api/ts.go Normal file
View File

@@ -0,0 +1,45 @@
package api
import (
"net/http"
)
// HandleTS streams the shared channel's raw MPEG-TS bytes to a viewer.
// Every connected browser subscribes to the same upstream via the hub, so the
// server holds only one connection to the IPTV source regardless of how many
// clients are watching.
// GET /ts
func (a *App) HandleTS(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "video/mp2t")
w.Header().Set("Cache-Control", "no-cache, no-store")
w.Header().Set("Connection", "keep-alive")
w.WriteHeader(http.StatusOK)
flusher.Flush()
sub, unsub := a.Hub.Subscribe()
defer unsub()
ctx := r.Context()
for {
select {
case <-ctx.Done():
return
case <-sub.Done():
return
case chunk, ok := <-sub.Chan():
if !ok {
return
}
if _, err := w.Write(chunk); err != nil {
return
}
flusher.Flush()
}
}
}