Add web HTTP-FLV playback path

This commit is contained in:
2026-04-01 11:30:52 +08:00
parent 01b25883e1
commit 48dc6c7b26
11 changed files with 455 additions and 93 deletions

View File

@@ -18,9 +18,11 @@ func main() {
// Initialize Chat WebSocket Hub
chat.InitChat()
srv := stream.NewRTMPServer()
// Start the API server in a goroutine so it doesn't block the RTMP server
go func() {
r := api.SetupRouter()
r := api.SetupRouter(srv)
log.Println("[INFO] API Server is listening on :8080...")
if err := r.Run(":8080"); err != nil {
log.Fatalf("Failed to start API server: %v", err)
@@ -29,7 +31,6 @@ func main() {
// Setup and start the RTMP server
log.Println("[INFO] Ready to receive RTMP streams from OBS.")
srv := stream.NewRTMPServer()
if err := srv.Start(":1935"); err != nil {
log.Fatalf("Failed to start RTMP server: %v", err)
}

View File

@@ -2,10 +2,12 @@ package api
import (
"github.com/gin-gonic/gin"
"hightube/internal/stream"
)
// SetupRouter configures the Gin router and defines API endpoints
func SetupRouter() *gin.Engine {
func SetupRouter(streamServer *stream.RTMPServer) *gin.Engine {
// 设置为发布模式,消除 "[WARNING] Running in debug mode" 警告
gin.SetMode(gin.ReleaseMode)
@@ -21,6 +23,7 @@ func SetupRouter() *gin.Engine {
r.POST("/api/register", Register)
r.POST("/api/login", Login)
r.GET("/api/rooms/active", GetActiveRooms)
r.GET("/live/:room_id", streamServer.HandleHTTPFLV)
// WebSocket endpoint for live chat
r.GET("/api/ws/room/:room_id", WSHandler)

View File

@@ -3,12 +3,15 @@ package stream
import (
"fmt"
"io"
"net/http"
"strings"
"sync"
"github.com/gin-gonic/gin"
"github.com/nareix/joy4/av/avutil"
"github.com/nareix/joy4/av/pubsub"
"github.com/nareix/joy4/format"
"github.com/nareix/joy4/format/flv"
"github.com/nareix/joy4/format/rtmp"
"hightube/internal/chat"
@@ -28,6 +31,16 @@ type RTMPServer struct {
mutex sync.RWMutex
}
type writeFlusher struct {
httpFlusher http.Flusher
io.Writer
}
func (w writeFlusher) Flush() error {
w.httpFlusher.Flush()
return nil
}
// NewRTMPServer creates and initializes a new media server
func NewRTMPServer() *RTMPServer {
s := &RTMPServer{
@@ -140,3 +153,46 @@ func (s *RTMPServer) Start(addr string) error {
fmt.Printf("[INFO] RTMP Server is listening on %s...\n", addr)
return s.server.ListenAndServe()
}
// HandleHTTPFLV serves browser-compatible HTTP-FLV playback for web clients.
func (s *RTMPServer) HandleHTTPFLV(c *gin.Context) {
streamPath := fmt.Sprintf("/live/%s", c.Param("room_id"))
s.mutex.RLock()
q, ok := s.channels[streamPath]
s.mutex.RUnlock()
if !ok {
c.JSON(http.StatusNotFound, gin.H{"error": "Stream not found or inactive"})
return
}
flusher, ok := c.Writer.(http.Flusher)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Streaming is not supported by the current server"})
return
}
c.Header("Content-Type", "video/x-flv")
c.Header("Transfer-Encoding", "chunked")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("Access-Control-Allow-Origin", "*")
c.Status(http.StatusOK)
flusher.Flush()
muxer := flv.NewMuxerWriteFlusher(writeFlusher{
httpFlusher: flusher,
Writer: c.Writer,
})
cursor := q.Latest()
if err := avutil.CopyFile(muxer, cursor); err != nil && err != io.EOF {
errStr := err.Error()
if strings.Contains(errStr, "broken pipe") || strings.Contains(errStr, "connection reset by peer") {
fmt.Printf("[INFO] HTTP-FLV viewer disconnected normally: %s\n", streamPath)
return
}
fmt.Printf("[ERROR] HTTP-FLV playback error on %s: %v\n", streamPath, err)
}
}