2025-12-15 09:04:14 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* package mediaserver
|
|
|
|
|
|
*
|
|
|
|
|
|
* RTMP 服务(用于微信小程序 live-pusher/live-player)
|
2025-12-15 21:57:21 +08:00
|
|
|
|
* 自实现 RTMP 协议,解决第三方库的 SetChunkSize panic 问题
|
2025-12-15 11:26:41 +08:00
|
|
|
|
*
|
2025-12-15 09:04:14 +08:00
|
|
|
|
* 功能:
|
2025-12-15 11:26:41 +08:00
|
|
|
|
* 1. 接收小程序推流(publish)
|
|
|
|
|
|
* 2. 提供拉流服务(play)
|
2025-12-15 09:04:14 +08:00
|
|
|
|
* 3. 支持 HTTP-FLV 协议
|
|
|
|
|
|
*/
|
|
|
|
|
|
package mediaserver
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"crypto/hmac"
|
|
|
|
|
|
"crypto/sha1"
|
|
|
|
|
|
"encoding/base64"
|
2025-12-15 21:57:21 +08:00
|
|
|
|
"encoding/binary"
|
2025-12-15 09:04:14 +08:00
|
|
|
|
"fmt"
|
|
|
|
|
|
"log"
|
|
|
|
|
|
"net"
|
|
|
|
|
|
"net/http"
|
2025-12-15 11:26:41 +08:00
|
|
|
|
"strings"
|
2025-12-15 09:04:14 +08:00
|
|
|
|
"sync"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/spf13/viper"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// RTMPServer RTMP 服务器
|
|
|
|
|
|
type RTMPServer struct {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
config *MediaServerConfig
|
|
|
|
|
|
mu sync.RWMutex
|
|
|
|
|
|
running bool
|
|
|
|
|
|
streams map[string]*RTMPStream // streamID -> stream
|
|
|
|
|
|
streamsMu sync.RWMutex
|
|
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
// 服务器监听器
|
|
|
|
|
|
rtmpListener net.Listener
|
|
|
|
|
|
httpServer *http.Server
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
|
|
|
|
|
// 订阅者管理
|
|
|
|
|
|
subscribers map[string]map[string]*Subscriber // streamID -> subscriberID -> subscriber
|
|
|
|
|
|
subMu sync.RWMutex
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RTMPStream RTMP 流信息
|
|
|
|
|
|
type RTMPStream struct {
|
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
|
RoomID string `json:"room_id"`
|
|
|
|
|
|
UserID string `json:"user_id"`
|
|
|
|
|
|
PushURL string `json:"push_url"`
|
|
|
|
|
|
PullURL string `json:"pull_url"`
|
|
|
|
|
|
FLVURL string `json:"flv_url"`
|
|
|
|
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
|
|
IsActive bool `json:"is_active"`
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
|
|
|
|
|
// 流数据
|
|
|
|
|
|
mu sync.RWMutex
|
|
|
|
|
|
flvHeader []byte // FLV header
|
|
|
|
|
|
metaData []byte // FLV metadata tag
|
|
|
|
|
|
videoHeader []byte // Video sequence header (AVC)
|
|
|
|
|
|
audioHeader []byte // Audio sequence header (AAC)
|
|
|
|
|
|
gopCache [][]byte // GOP cache for new subscribers
|
|
|
|
|
|
|
|
|
|
|
|
// 控制
|
|
|
|
|
|
stopChan chan struct{} `json:"-"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Subscriber 订阅者
|
|
|
|
|
|
type Subscriber struct {
|
|
|
|
|
|
ID string
|
|
|
|
|
|
StreamID string
|
|
|
|
|
|
DataChan chan []byte
|
|
|
|
|
|
Done chan struct{}
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewRTMPServer 创建 RTMP 服务器
|
|
|
|
|
|
func NewRTMPServer(config *MediaServerConfig) *RTMPServer {
|
|
|
|
|
|
return &RTMPServer{
|
2025-12-15 11:26:41 +08:00
|
|
|
|
config: config,
|
|
|
|
|
|
streams: make(map[string]*RTMPStream),
|
|
|
|
|
|
subscribers: make(map[string]map[string]*Subscriber),
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Start 启动 RTMP 服务器
|
|
|
|
|
|
func (r *RTMPServer) Start() {
|
|
|
|
|
|
r.mu.Lock()
|
|
|
|
|
|
if r.running {
|
|
|
|
|
|
r.mu.Unlock()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
r.running = true
|
|
|
|
|
|
r.mu.Unlock()
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
|
|
|
|
|
// 启动 RTMP 服务器
|
2025-12-15 09:04:14 +08:00
|
|
|
|
go r.startRTMPServer()
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
// 启动 HTTP-FLV 服务器
|
|
|
|
|
|
go r.startHTTPFLVServer()
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
// 启动流清理任务
|
|
|
|
|
|
go r.cleanupTask()
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
|
|
|
|
|
log.Printf("🚀 [RTMP] RTMP 服务已启动 | RTMP端口: %d | HTTP-FLV端口: %d",
|
2025-12-15 09:04:14 +08:00
|
|
|
|
r.config.RTMPPort, r.config.HTTPFLVPort)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 11:26:41 +08:00
|
|
|
|
// startRTMPServer 启动 RTMP 服务
|
2025-12-15 09:04:14 +08:00
|
|
|
|
func (r *RTMPServer) startRTMPServer() {
|
|
|
|
|
|
addr := fmt.Sprintf(":%d", r.config.RTMPPort)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
var err error
|
|
|
|
|
|
r.rtmpListener, err = net.Listen("tcp", addr)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Printf("❌ [RTMP] RTMP 监听失败: %v", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
log.Printf("🎬 [RTMP] RTMP 服务监听: %s", addr)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
for {
|
|
|
|
|
|
conn, err := r.rtmpListener.Accept()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
r.mu.RLock()
|
|
|
|
|
|
running := r.running
|
|
|
|
|
|
r.mu.RUnlock()
|
|
|
|
|
|
if !running {
|
|
|
|
|
|
return
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
2025-12-15 21:57:21 +08:00
|
|
|
|
log.Printf("⚠️ [RTMP] Accept 失败: %v", err)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
go r.handleConnection(conn)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// handleConnection 处理 RTMP 连接
|
|
|
|
|
|
func (r *RTMPServer) handleConnection(conn net.Conn) {
|
|
|
|
|
|
remoteAddr := conn.RemoteAddr().String()
|
|
|
|
|
|
log.Printf("🎬 [RTMP] 新连接: %s", remoteAddr)
|
|
|
|
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
|
|
if err := recover(); err != nil {
|
|
|
|
|
|
log.Printf("❌ [RTMP] 连接处理 panic: %v", err)
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
2025-12-15 21:57:21 +08:00
|
|
|
|
conn.Close()
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 执行握手
|
|
|
|
|
|
if err := DoHandshake(conn, 30*time.Second); err != nil {
|
|
|
|
|
|
log.Printf("❌ [RTMP] 握手失败: %v", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 创建读写器
|
|
|
|
|
|
reader := NewChunkReader(conn)
|
|
|
|
|
|
writer := NewChunkWriter(conn)
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 创建连接处理器
|
|
|
|
|
|
handler := &ConnectionHandler{
|
|
|
|
|
|
server: r,
|
|
|
|
|
|
conn: conn,
|
|
|
|
|
|
reader: reader,
|
|
|
|
|
|
writer: writer,
|
|
|
|
|
|
remoteAddr: remoteAddr,
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
2025-12-15 21:57:21 +08:00
|
|
|
|
|
|
|
|
|
|
// 4. 消息处理循环
|
|
|
|
|
|
handler.serve()
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// ConnectionHandler RTMP 连接处理器
|
|
|
|
|
|
type ConnectionHandler struct {
|
|
|
|
|
|
server *RTMPServer
|
|
|
|
|
|
conn net.Conn
|
|
|
|
|
|
reader *ChunkReader
|
|
|
|
|
|
writer *ChunkWriter
|
|
|
|
|
|
remoteAddr string
|
|
|
|
|
|
|
|
|
|
|
|
// 连接状态
|
|
|
|
|
|
streamID string
|
|
|
|
|
|
isPublish bool
|
|
|
|
|
|
appName string
|
|
|
|
|
|
msgStreamID uint32
|
|
|
|
|
|
|
|
|
|
|
|
// 统计信息
|
|
|
|
|
|
audioCount int64
|
|
|
|
|
|
videoCount int64
|
|
|
|
|
|
firstAudio bool
|
|
|
|
|
|
firstVideo bool
|
2025-12-15 11:26:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// serve 消息处理循环
|
|
|
|
|
|
func (h *ConnectionHandler) serve() {
|
|
|
|
|
|
log.Printf("🎬 [RTMP] OnServe: %s", h.remoteAddr)
|
|
|
|
|
|
|
|
|
|
|
|
msgCount := int64(0)
|
|
|
|
|
|
lastLogTime := time.Now()
|
|
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
|
msg, err := h.reader.ReadMessage()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Printf("🔌 [RTMP] 连接关闭: %s (err: %v)", h.remoteAddr, err)
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
msgCount++
|
|
|
|
|
|
|
|
|
|
|
|
// 每 100 条消息或每 5 秒记录一次统计信息
|
|
|
|
|
|
if msgCount%100 == 0 || time.Since(lastLogTime) > 5*time.Second {
|
|
|
|
|
|
log.Printf("📊 [RTMP] 消息统计: stream=%s 总消息=%d audio=%d video=%d remote=%s",
|
|
|
|
|
|
h.streamID, msgCount, h.audioCount, h.videoCount, h.remoteAddr)
|
|
|
|
|
|
lastLogTime = time.Now()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := h.handleMessage(msg); err != nil {
|
|
|
|
|
|
log.Printf("❌ [RTMP] 处理消息失败: %v", err)
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 连接关闭时的清理
|
|
|
|
|
|
h.onClose()
|
2025-12-15 11:26:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// handleMessage 处理单个消息
|
|
|
|
|
|
func (h *ConnectionHandler) handleMessage(msg *RTMPMessage) error {
|
|
|
|
|
|
switch msg.TypeID {
|
|
|
|
|
|
case RTMP_MSG_CHUNK_SIZE:
|
|
|
|
|
|
return h.handleSetChunkSize(msg)
|
|
|
|
|
|
case RTMP_MSG_ACK:
|
|
|
|
|
|
// 忽略 ACK
|
|
|
|
|
|
return nil
|
|
|
|
|
|
case RTMP_MSG_USER_CONTROL:
|
|
|
|
|
|
// 忽略 User Control
|
|
|
|
|
|
return nil
|
|
|
|
|
|
case RTMP_MSG_WIN_ACK_SIZE:
|
|
|
|
|
|
// 忽略 Window Ack Size
|
|
|
|
|
|
return nil
|
|
|
|
|
|
case RTMP_MSG_SET_PEER_BW:
|
|
|
|
|
|
// 忽略 Set Peer Bandwidth
|
|
|
|
|
|
return nil
|
|
|
|
|
|
case RTMP_MSG_AUDIO:
|
|
|
|
|
|
return h.handleAudio(msg)
|
|
|
|
|
|
case RTMP_MSG_VIDEO:
|
|
|
|
|
|
return h.handleVideo(msg)
|
|
|
|
|
|
case RTMP_MSG_AMF0_DATA:
|
|
|
|
|
|
return h.handleDataMessage(msg)
|
|
|
|
|
|
case RTMP_MSG_AMF0_CMD:
|
|
|
|
|
|
return h.handleCommand(msg)
|
|
|
|
|
|
default:
|
|
|
|
|
|
// 忽略未知消息
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// handleSetChunkSize 处理 SetChunkSize 消息
|
|
|
|
|
|
func (h *ConnectionHandler) handleSetChunkSize(msg *RTMPMessage) error {
|
|
|
|
|
|
if len(msg.Data) < 4 {
|
|
|
|
|
|
return fmt.Errorf("SetChunkSize 数据不足")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
newSize := binary.BigEndian.Uint32(msg.Data)
|
|
|
|
|
|
log.Printf("📝 [RTMP] SetChunkSize: %d -> %d (remote: %s)", h.reader.GetChunkSize(), newSize, h.remoteAddr)
|
|
|
|
|
|
|
|
|
|
|
|
// 关键:立即更新 chunk size
|
|
|
|
|
|
h.reader.SetChunkSize(newSize)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// handleCommand 处理 AMF0 命令
|
|
|
|
|
|
func (h *ConnectionHandler) handleCommand(msg *RTMPMessage) error {
|
|
|
|
|
|
values, err := DecodeAMF0(msg.Data)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Printf("⚠️ [RTMP] 解析命令失败: %v", err)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(values) == 0 {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
command, ok := values[0].(string)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
transactionID := float64(0)
|
|
|
|
|
|
if len(values) > 1 {
|
|
|
|
|
|
if tid, ok := values[1].(float64); ok {
|
|
|
|
|
|
transactionID = tid
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log.Printf("🎬 [RTMP] 命令: %s (tid: %.0f) from %s", command, transactionID, h.remoteAddr)
|
|
|
|
|
|
|
|
|
|
|
|
switch command {
|
|
|
|
|
|
case "connect":
|
|
|
|
|
|
return h.handleConnect(values, transactionID)
|
|
|
|
|
|
case "releaseStream":
|
|
|
|
|
|
return h.handleReleaseStream(values)
|
|
|
|
|
|
case "FCPublish":
|
|
|
|
|
|
return h.handleFCPublish(values)
|
|
|
|
|
|
case "createStream":
|
|
|
|
|
|
return h.handleCreateStream(transactionID)
|
|
|
|
|
|
case "publish":
|
|
|
|
|
|
return h.handlePublish(values, msg.StreamID)
|
|
|
|
|
|
case "play":
|
|
|
|
|
|
return h.handlePlay(values, msg.StreamID)
|
|
|
|
|
|
case "deleteStream":
|
|
|
|
|
|
return h.handleDeleteStream(values)
|
|
|
|
|
|
case "FCUnpublish":
|
|
|
|
|
|
return h.handleFCUnpublish(values)
|
|
|
|
|
|
default:
|
|
|
|
|
|
// 忽略未知命令
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// handleConnect 处理 connect 命令
|
|
|
|
|
|
func (h *ConnectionHandler) handleConnect(values []interface{}, tid float64) error {
|
|
|
|
|
|
// 解析 app 名称
|
|
|
|
|
|
if len(values) > 2 {
|
|
|
|
|
|
if obj, ok := values[2].(AMF0Object); ok {
|
|
|
|
|
|
if app, ok := obj["app"].(string); ok {
|
|
|
|
|
|
h.appName = app
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
log.Printf("🎬 [RTMP] OnConnect: app=%s", h.appName)
|
|
|
|
|
|
|
|
|
|
|
|
// 发送 Window Ack Size
|
|
|
|
|
|
if err := h.writer.WriteWindowAckSize(DEFAULT_WINDOW_SIZE); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送 Set Peer Bandwidth
|
|
|
|
|
|
if err := h.writer.WriteSetPeerBandwidth(DEFAULT_WINDOW_SIZE, 2); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送 Set Chunk Size
|
|
|
|
|
|
if err := h.writer.WriteSetChunkSize(4096); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送 _result
|
|
|
|
|
|
result := EncodeConnectResult(tid)
|
|
|
|
|
|
if err := h.writer.WriteCommand(3, 0, result); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送 onBWDone
|
|
|
|
|
|
bwDone := EncodeOnBWDone()
|
|
|
|
|
|
return h.writer.WriteCommand(3, 0, bwDone)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// handleReleaseStream 处理 releaseStream 命令
|
|
|
|
|
|
func (h *ConnectionHandler) handleReleaseStream(values []interface{}) error {
|
|
|
|
|
|
streamName := ""
|
|
|
|
|
|
if len(values) > 3 {
|
|
|
|
|
|
if name, ok := values[3].(string); ok {
|
|
|
|
|
|
streamName = name
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
if idx := strings.Index(streamName, "?"); idx != -1 {
|
|
|
|
|
|
streamName = streamName[:idx]
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
log.Printf("🎬 [RTMP] OnReleaseStream: %s", streamName)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// handleFCPublish 处理 FCPublish 命令
|
|
|
|
|
|
func (h *ConnectionHandler) handleFCPublish(values []interface{}) error {
|
|
|
|
|
|
streamName := ""
|
|
|
|
|
|
if len(values) > 3 {
|
|
|
|
|
|
if name, ok := values[3].(string); ok {
|
|
|
|
|
|
streamName = name
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
log.Printf("🎬 [RTMP] OnFCPublish: %s", streamName)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// handleCreateStream 处理 createStream 命令
|
|
|
|
|
|
func (h *ConnectionHandler) handleCreateStream(tid float64) error {
|
|
|
|
|
|
log.Printf("🎬 [RTMP] OnCreateStream")
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
h.msgStreamID = 1
|
|
|
|
|
|
|
|
|
|
|
|
// 发送 _result
|
|
|
|
|
|
result := EncodeCreateStreamResult(tid, float64(h.msgStreamID))
|
|
|
|
|
|
return h.writer.WriteCommand(3, 0, result)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// handlePublish 处理 publish 命令
|
|
|
|
|
|
func (h *ConnectionHandler) handlePublish(values []interface{}, streamID uint32) error {
|
|
|
|
|
|
publishingName := ""
|
|
|
|
|
|
publishingType := ""
|
|
|
|
|
|
|
|
|
|
|
|
if len(values) > 3 {
|
|
|
|
|
|
if name, ok := values[3].(string); ok {
|
|
|
|
|
|
publishingName = name
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(values) > 4 {
|
|
|
|
|
|
if ptype, ok := values[4].(string); ok {
|
|
|
|
|
|
publishingType = ptype
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log.Printf("🎬 [RTMP] OnPublish: name=%s, type=%s, remote=%s", publishingName, publishingType, h.remoteAddr)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
|
|
|
|
|
// 解析 stream name,去掉 token 参数
|
2025-12-15 21:57:21 +08:00
|
|
|
|
streamName := publishingName
|
2025-12-15 11:26:41 +08:00
|
|
|
|
if idx := strings.Index(streamName, "?"); idx != -1 {
|
|
|
|
|
|
streamName = streamName[:idx]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.streamID = streamName
|
|
|
|
|
|
h.isPublish = true
|
2025-12-15 21:57:21 +08:00
|
|
|
|
h.audioCount = 0
|
|
|
|
|
|
h.videoCount = 0
|
|
|
|
|
|
h.firstAudio = false
|
|
|
|
|
|
h.firstVideo = false
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
|
|
|
|
|
log.Printf("🎬 [RTMP] 解析后的流ID: %s", h.streamID)
|
|
|
|
|
|
|
|
|
|
|
|
// 检查流是否已注册
|
|
|
|
|
|
h.server.streamsMu.RLock()
|
|
|
|
|
|
stream, exists := h.server.streams[h.streamID]
|
|
|
|
|
|
h.server.streamsMu.RUnlock()
|
|
|
|
|
|
|
|
|
|
|
|
if !exists {
|
|
|
|
|
|
log.Printf("⚠️ [RTMP] 流未注册,自动创建: %s", h.streamID)
|
|
|
|
|
|
stream = &RTMPStream{
|
|
|
|
|
|
ID: h.streamID,
|
|
|
|
|
|
CreatedAt: time.Now(),
|
|
|
|
|
|
IsActive: true,
|
|
|
|
|
|
stopChan: make(chan struct{}),
|
|
|
|
|
|
gopCache: make([][]byte, 0),
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
h.server.streamsMu.Lock()
|
|
|
|
|
|
h.server.streams[h.streamID] = stream
|
|
|
|
|
|
h.server.streamsMu.Unlock()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
log.Printf("✅ [RTMP] 找到已注册的流: %s", h.streamID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stream.mu.Lock()
|
|
|
|
|
|
stream.IsActive = true
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// 重置流数据
|
|
|
|
|
|
stream.audioHeader = nil
|
|
|
|
|
|
stream.videoHeader = nil
|
|
|
|
|
|
stream.metaData = nil
|
|
|
|
|
|
stream.gopCache = make([][]byte, 0)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
stream.mu.Unlock()
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// 发送 Stream Begin
|
|
|
|
|
|
if err := h.writer.WriteStreamBegin(streamID); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送 onStatus (NetStream.Publish.Start)
|
|
|
|
|
|
status := EncodeOnStatus("NetStream.Publish.Start", "status", "Publishing started.")
|
|
|
|
|
|
if err := h.writer.WriteCommand(5, streamID, status); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
log.Printf("✅ [RTMP] 开始推流: %s (等待音视频数据...)", h.streamID)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// handlePlay 处理 play 命令
|
|
|
|
|
|
func (h *ConnectionHandler) handlePlay(values []interface{}, streamID uint32) error {
|
|
|
|
|
|
streamName := ""
|
|
|
|
|
|
if len(values) > 3 {
|
|
|
|
|
|
if name, ok := values[3].(string); ok {
|
|
|
|
|
|
streamName = name
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
log.Printf("🎬 [RTMP] OnPlay: name=%s", streamName)
|
|
|
|
|
|
|
|
|
|
|
|
h.streamID = streamName
|
2025-12-15 11:26:41 +08:00
|
|
|
|
h.isPublish = false
|
|
|
|
|
|
|
|
|
|
|
|
// 检查流是否存在
|
|
|
|
|
|
h.server.streamsMu.RLock()
|
|
|
|
|
|
stream, exists := h.server.streams[h.streamID]
|
|
|
|
|
|
h.server.streamsMu.RUnlock()
|
|
|
|
|
|
|
|
|
|
|
|
if !exists || !stream.IsActive {
|
|
|
|
|
|
log.Printf("⚠️ [RTMP] 流不存在或未激活: %s", h.streamID)
|
2025-12-15 21:57:21 +08:00
|
|
|
|
status := EncodeOnStatus("NetStream.Play.StreamNotFound", "error", "Stream not found.")
|
|
|
|
|
|
return h.writer.WriteCommand(5, streamID, status)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送 Stream Begin
|
|
|
|
|
|
if err := h.writer.WriteStreamBegin(streamID); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送 onStatus (NetStream.Play.Start)
|
|
|
|
|
|
status := EncodeOnStatus("NetStream.Play.Start", "status", "Playing started.")
|
|
|
|
|
|
if err := h.writer.WriteCommand(5, streamID, status); err != nil {
|
|
|
|
|
|
return err
|
2025-12-15 11:26:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log.Printf("✅ [RTMP] 开始播放: %s", h.streamID)
|
2025-12-15 21:57:21 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// handleDeleteStream 处理 deleteStream 命令
|
|
|
|
|
|
func (h *ConnectionHandler) handleDeleteStream(values []interface{}) error {
|
|
|
|
|
|
log.Printf("🎬 [RTMP] OnDeleteStream")
|
2025-12-15 11:26:41 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// handleFCUnpublish 处理 FCUnpublish 命令
|
|
|
|
|
|
func (h *ConnectionHandler) handleFCUnpublish(values []interface{}) error {
|
|
|
|
|
|
streamName := ""
|
|
|
|
|
|
if len(values) > 3 {
|
|
|
|
|
|
if name, ok := values[3].(string); ok {
|
|
|
|
|
|
streamName = name
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
log.Printf("🎬 [RTMP] OnFCUnpublish: %s", streamName)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// handleDataMessage 处理数据消息 (@setDataFrame)
|
|
|
|
|
|
func (h *ConnectionHandler) handleDataMessage(msg *RTMPMessage) error {
|
|
|
|
|
|
log.Printf("🎬 [RTMP] OnSetDataFrame: stream=%s payloadLen=%d audioCount=%d videoCount=%d",
|
|
|
|
|
|
h.streamID, len(msg.Data), h.audioCount, h.videoCount)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
|
|
|
|
|
if h.streamID == "" {
|
2025-12-15 21:57:21 +08:00
|
|
|
|
log.Printf("⚠️ [RTMP] OnSetDataFrame: streamID 为空")
|
2025-12-15 11:26:41 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h.server.streamsMu.RLock()
|
|
|
|
|
|
stream, exists := h.server.streams[h.streamID]
|
|
|
|
|
|
h.server.streamsMu.RUnlock()
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
if exists && len(msg.Data) > 0 {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
stream.mu.Lock()
|
2025-12-15 21:57:21 +08:00
|
|
|
|
stream.metaData = make([]byte, len(msg.Data))
|
|
|
|
|
|
copy(stream.metaData, msg.Data)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
stream.mu.Unlock()
|
2025-12-15 21:57:21 +08:00
|
|
|
|
log.Printf("✅ [RTMP] OnSetDataFrame: 已保存 metadata")
|
2025-12-15 11:26:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// handleAudio 处理音频数据
|
|
|
|
|
|
func (h *ConnectionHandler) handleAudio(msg *RTMPMessage) error {
|
|
|
|
|
|
h.audioCount++
|
|
|
|
|
|
|
|
|
|
|
|
if !h.firstAudio {
|
|
|
|
|
|
h.firstAudio = true
|
|
|
|
|
|
log.Printf("🎵 [RTMP] OnAudio 首帧: stream=%s timestamp=%d isPublish=%v", h.streamID, msg.Timestamp, h.isPublish)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
if h.streamID == "" || !h.isPublish {
|
|
|
|
|
|
return nil
|
2025-12-15 11:26:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
data := msg.Data
|
2025-12-15 11:26:41 +08:00
|
|
|
|
if len(data) == 0 {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
if h.audioCount%100 == 0 {
|
|
|
|
|
|
log.Printf("🎵 [RTMP] OnAudio 统计: stream=%s audioCount=%d videoCount=%d", h.streamID, h.audioCount, h.videoCount)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 11:26:41 +08:00
|
|
|
|
h.server.streamsMu.RLock()
|
|
|
|
|
|
stream, exists := h.server.streams[h.streamID]
|
|
|
|
|
|
h.server.streamsMu.RUnlock()
|
|
|
|
|
|
|
|
|
|
|
|
if !exists {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存 AAC sequence header
|
|
|
|
|
|
if len(data) > 1 {
|
|
|
|
|
|
soundFormat := (data[0] >> 4) & 0x0f
|
|
|
|
|
|
if soundFormat == 10 { // AAC
|
|
|
|
|
|
aacPacketType := data[1]
|
|
|
|
|
|
if aacPacketType == 0 { // Sequence header
|
|
|
|
|
|
stream.mu.Lock()
|
|
|
|
|
|
stream.audioHeader = make([]byte, len(data))
|
|
|
|
|
|
copy(stream.audioHeader, data)
|
|
|
|
|
|
stream.mu.Unlock()
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建 FLV audio tag
|
2025-12-15 21:57:21 +08:00
|
|
|
|
flvData := createFLVTag(8, msg.Timestamp, data)
|
|
|
|
|
|
h.broadcastToSubscribers(h.streamID, flvData)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// handleVideo 处理视频数据
|
|
|
|
|
|
func (h *ConnectionHandler) handleVideo(msg *RTMPMessage) error {
|
|
|
|
|
|
h.videoCount++
|
|
|
|
|
|
|
|
|
|
|
|
if !h.firstVideo {
|
|
|
|
|
|
h.firstVideo = true
|
|
|
|
|
|
log.Printf("🎥 [RTMP] OnVideo 首帧: stream=%s timestamp=%d isPublish=%v", h.streamID, msg.Timestamp, h.isPublish)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
if h.streamID == "" || !h.isPublish {
|
|
|
|
|
|
return nil
|
2025-12-15 11:26:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
data := msg.Data
|
2025-12-15 11:26:41 +08:00
|
|
|
|
if len(data) == 0 {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
if h.videoCount == 1 {
|
|
|
|
|
|
log.Printf("🎥 [RTMP] OnVideo 首帧数据: stream=%s len=%d", h.streamID, len(data))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if h.videoCount%30 == 0 {
|
|
|
|
|
|
log.Printf("🎥 [RTMP] OnVideo 统计: stream=%s audioCount=%d videoCount=%d", h.streamID, h.audioCount, h.videoCount)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 11:26:41 +08:00
|
|
|
|
h.server.streamsMu.RLock()
|
|
|
|
|
|
stream, exists := h.server.streams[h.streamID]
|
|
|
|
|
|
h.server.streamsMu.RUnlock()
|
|
|
|
|
|
|
|
|
|
|
|
if !exists {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析视频帧信息
|
|
|
|
|
|
frameType := (data[0] >> 4) & 0x0f
|
|
|
|
|
|
codecID := data[0] & 0x0f
|
|
|
|
|
|
|
|
|
|
|
|
// 保存 AVC sequence header
|
|
|
|
|
|
if codecID == 7 && len(data) > 1 { // AVC
|
|
|
|
|
|
avcPacketType := data[1]
|
|
|
|
|
|
if avcPacketType == 0 { // Sequence header
|
|
|
|
|
|
stream.mu.Lock()
|
|
|
|
|
|
stream.videoHeader = make([]byte, len(data))
|
|
|
|
|
|
copy(stream.videoHeader, data)
|
|
|
|
|
|
stream.mu.Unlock()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关键帧时清空 GOP 缓存
|
|
|
|
|
|
if frameType == 1 { // Keyframe
|
|
|
|
|
|
stream.mu.Lock()
|
|
|
|
|
|
stream.gopCache = make([][]byte, 0)
|
|
|
|
|
|
stream.mu.Unlock()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建 FLV video tag
|
2025-12-15 21:57:21 +08:00
|
|
|
|
flvData := createFLVTag(9, msg.Timestamp, data)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// 缓存到 GOP
|
|
|
|
|
|
stream.mu.Lock()
|
|
|
|
|
|
stream.gopCache = append(stream.gopCache, flvData)
|
|
|
|
|
|
if len(stream.gopCache) > 300 {
|
|
|
|
|
|
stream.gopCache = stream.gopCache[len(stream.gopCache)-300:]
|
2025-12-15 11:26:41 +08:00
|
|
|
|
}
|
2025-12-15 21:57:21 +08:00
|
|
|
|
stream.mu.Unlock()
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
h.broadcastToSubscribers(h.streamID, flvData)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// broadcastToSubscribers 向所有订阅者广播数据
|
|
|
|
|
|
func (h *ConnectionHandler) broadcastToSubscribers(streamID string, data []byte) {
|
|
|
|
|
|
h.server.subMu.RLock()
|
|
|
|
|
|
subs, exists := h.server.subscribers[streamID]
|
|
|
|
|
|
if !exists {
|
|
|
|
|
|
h.server.subMu.RUnlock()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
for _, sub := range subs {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case sub.DataChan <- data:
|
|
|
|
|
|
default:
|
|
|
|
|
|
// 缓冲区满,跳过
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
h.server.subMu.RUnlock()
|
2025-12-15 11:26:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// onClose 连接关闭
|
|
|
|
|
|
func (h *ConnectionHandler) onClose() {
|
|
|
|
|
|
log.Printf("🔌 [RTMP] OnClose: stream=%s, publish=%v, remote=%s, audioCount=%d, videoCount=%d",
|
|
|
|
|
|
h.streamID, h.isPublish, h.remoteAddr, h.audioCount, h.videoCount)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
if h.isPublish && h.audioCount > 0 && h.videoCount == 0 {
|
|
|
|
|
|
log.Printf("⚠️ [RTMP] 警告: 只收到音频(%d帧),没有收到视频!stream=%s", h.audioCount, h.streamID)
|
|
|
|
|
|
log.Printf("⚠️ [RTMP] 可能原因: 1.推流端未启用视频 2.视频编码格式不支持 3.连接过早断开")
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
if h.isPublish && h.streamID != "" {
|
|
|
|
|
|
h.server.streamsMu.Lock()
|
|
|
|
|
|
if stream, exists := h.server.streams[h.streamID]; exists {
|
|
|
|
|
|
stream.mu.Lock()
|
|
|
|
|
|
stream.IsActive = false
|
|
|
|
|
|
stream.mu.Unlock()
|
|
|
|
|
|
}
|
|
|
|
|
|
h.server.streamsMu.Unlock()
|
|
|
|
|
|
log.Printf("⏹️ [RTMP] 停止推流: %s (总计: 音频%d帧, 视频%d帧)", h.streamID, h.audioCount, h.videoCount)
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// createFLVTag 创建 FLV tag
|
|
|
|
|
|
func createFLVTag(tagType byte, timestamp uint32, data []byte) []byte {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
dataSize := len(data)
|
|
|
|
|
|
tagSize := 11 + dataSize + 4
|
|
|
|
|
|
|
|
|
|
|
|
tag := make([]byte, tagSize)
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
tag[0] = tagType
|
2025-12-15 11:26:41 +08:00
|
|
|
|
tag[1] = byte((dataSize >> 16) & 0xff)
|
|
|
|
|
|
tag[2] = byte((dataSize >> 8) & 0xff)
|
|
|
|
|
|
tag[3] = byte(dataSize & 0xff)
|
|
|
|
|
|
tag[4] = byte((timestamp >> 16) & 0xff)
|
|
|
|
|
|
tag[5] = byte((timestamp >> 8) & 0xff)
|
|
|
|
|
|
tag[6] = byte(timestamp & 0xff)
|
|
|
|
|
|
tag[7] = byte((timestamp >> 24) & 0xff)
|
|
|
|
|
|
tag[8] = 0
|
|
|
|
|
|
tag[9] = 0
|
|
|
|
|
|
tag[10] = 0
|
|
|
|
|
|
|
|
|
|
|
|
copy(tag[11:], data)
|
|
|
|
|
|
|
|
|
|
|
|
prevTagSize := 11 + dataSize
|
|
|
|
|
|
tag[11+dataSize] = byte((prevTagSize >> 24) & 0xff)
|
|
|
|
|
|
tag[11+dataSize+1] = byte((prevTagSize >> 16) & 0xff)
|
|
|
|
|
|
tag[11+dataSize+2] = byte((prevTagSize >> 8) & 0xff)
|
|
|
|
|
|
tag[11+dataSize+3] = byte(prevTagSize & 0xff)
|
|
|
|
|
|
|
|
|
|
|
|
return tag
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
// startHTTPFLVServer 启动 HTTP-FLV 服务
|
|
|
|
|
|
func (r *RTMPServer) startHTTPFLVServer() {
|
|
|
|
|
|
addr := fmt.Sprintf(":%d", r.config.HTTPFLVPort)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
|
|
mux.HandleFunc("/live/", r.handleFLVRequest)
|
|
|
|
|
|
mux.HandleFunc("/api/streams", r.handleStreamsAPI)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
r.httpServer = &http.Server{
|
|
|
|
|
|
Addr: addr,
|
|
|
|
|
|
Handler: mux,
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
log.Printf("🎬 [RTMP] HTTP-FLV 服务监听: %s", addr)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
if err := r.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
|
|
|
|
log.Printf("❌ [RTMP] HTTP-FLV 启动失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// handleFLVRequest 处理 FLV 请求
|
|
|
|
|
|
func (r *RTMPServer) handleFLVRequest(w http.ResponseWriter, req *http.Request) {
|
|
|
|
|
|
path := req.URL.Path
|
2025-12-15 11:26:41 +08:00
|
|
|
|
if len(path) < 7 {
|
2025-12-15 09:04:14 +08:00
|
|
|
|
http.Error(w, "Invalid path", http.StatusBadRequest)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
streamPath := path[6:] // 去掉 "/live/"
|
|
|
|
|
|
if len(streamPath) > 4 && streamPath[len(streamPath)-4:] == ".flv" {
|
|
|
|
|
|
streamPath = streamPath[:len(streamPath)-4]
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
log.Printf("🎬 [RTMP] FLV 请求: %s (from: %s)", streamPath, req.RemoteAddr)
|
|
|
|
|
|
|
|
|
|
|
|
// 等待流就绪
|
|
|
|
|
|
var stream *RTMPStream
|
|
|
|
|
|
var exists bool
|
|
|
|
|
|
maxWait := 10 * time.Second
|
|
|
|
|
|
waitInterval := 500 * time.Millisecond
|
|
|
|
|
|
waited := time.Duration(0)
|
|
|
|
|
|
|
|
|
|
|
|
for waited < maxWait {
|
|
|
|
|
|
r.streamsMu.RLock()
|
|
|
|
|
|
stream, exists = r.streams[streamPath]
|
|
|
|
|
|
r.streamsMu.RUnlock()
|
|
|
|
|
|
|
|
|
|
|
|
if exists && stream.IsActive {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
|
case <-req.Context().Done():
|
|
|
|
|
|
log.Printf("⚠️ [RTMP] FLV 请求已取消: %s", streamPath)
|
|
|
|
|
|
return
|
|
|
|
|
|
default:
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if waited == 0 {
|
|
|
|
|
|
log.Printf("⏳ [RTMP] 等待流就绪: %s", streamPath)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
time.Sleep(waitInterval)
|
|
|
|
|
|
waited += waitInterval
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
if !exists || !stream.IsActive {
|
2025-12-15 21:57:21 +08:00
|
|
|
|
log.Printf("❌ [RTMP] 流不存在或未激活: %s (waited: %v)", streamPath, waited)
|
|
|
|
|
|
http.Error(w, "Stream not found or not active", http.StatusNotFound)
|
2025-12-15 09:04:14 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
log.Printf("✅ [RTMP] 流已就绪,开始 FLV 传输: %s (waited: %v)", streamPath, waited)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
// 设置响应头
|
|
|
|
|
|
w.Header().Set("Content-Type", "video/x-flv")
|
|
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
|
|
|
w.Header().Set("Transfer-Encoding", "chunked")
|
2025-12-15 11:26:41 +08:00
|
|
|
|
w.Header().Set("Connection", "keep-alive")
|
|
|
|
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
// 发送 FLV header
|
|
|
|
|
|
flvHeader := []byte{0x46, 0x4C, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00}
|
|
|
|
|
|
w.Write(flvHeader)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
|
|
|
|
|
if f, ok := w.(http.Flusher); ok {
|
|
|
|
|
|
f.Flush()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建订阅者
|
|
|
|
|
|
subID := fmt.Sprintf("%d", time.Now().UnixNano())
|
|
|
|
|
|
sub := &Subscriber{
|
|
|
|
|
|
ID: subID,
|
|
|
|
|
|
StreamID: streamPath,
|
|
|
|
|
|
DataChan: make(chan []byte, 100),
|
|
|
|
|
|
Done: make(chan struct{}),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 注册订阅者
|
|
|
|
|
|
r.subMu.Lock()
|
|
|
|
|
|
if r.subscribers[streamPath] == nil {
|
|
|
|
|
|
r.subscribers[streamPath] = make(map[string]*Subscriber)
|
|
|
|
|
|
}
|
|
|
|
|
|
r.subscribers[streamPath][subID] = sub
|
|
|
|
|
|
r.subMu.Unlock()
|
|
|
|
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
|
|
r.subMu.Lock()
|
|
|
|
|
|
delete(r.subscribers[streamPath], subID)
|
|
|
|
|
|
r.subMu.Unlock()
|
|
|
|
|
|
close(sub.Done)
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
// 发送缓存的头信息
|
|
|
|
|
|
stream.mu.RLock()
|
2025-12-15 21:57:21 +08:00
|
|
|
|
hasMetadata := len(stream.metaData) > 0
|
|
|
|
|
|
hasVideoHeader := len(stream.videoHeader) > 0
|
|
|
|
|
|
hasAudioHeader := len(stream.audioHeader) > 0
|
|
|
|
|
|
gopLen := len(stream.gopCache)
|
|
|
|
|
|
|
|
|
|
|
|
log.Printf("📦 [RTMP] FLV 缓存状态: stream=%s metadata=%v videoHeader=%v audioHeader=%v gopLen=%d",
|
|
|
|
|
|
streamPath, hasMetadata, hasVideoHeader, hasAudioHeader, gopLen)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
if hasMetadata {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
w.Write(stream.metaData)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
if hasVideoHeader {
|
|
|
|
|
|
flvTag := createFLVTag(9, 0, stream.videoHeader)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
w.Write(flvTag)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
if hasAudioHeader {
|
|
|
|
|
|
flvTag := createFLVTag(8, 0, stream.audioHeader)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
w.Write(flvTag)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _, data := range stream.gopCache {
|
|
|
|
|
|
w.Write(data)
|
|
|
|
|
|
}
|
|
|
|
|
|
stream.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
|
|
if f, ok := w.(http.Flusher); ok {
|
|
|
|
|
|
f.Flush()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
log.Printf("▶️ [RTMP] FLV 开始实时传输: stream=%s subscriber=%s", streamPath, subID)
|
|
|
|
|
|
|
2025-12-15 11:26:41 +08:00
|
|
|
|
// 持续发送数据
|
2025-12-15 21:57:21 +08:00
|
|
|
|
dataCount := 0
|
2025-12-15 11:26:41 +08:00
|
|
|
|
for {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case <-req.Context().Done():
|
2025-12-15 21:57:21 +08:00
|
|
|
|
log.Printf("⏹️ [RTMP] FLV 传输结束 (客户端断开): stream=%s dataCount=%d", streamPath, dataCount)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
return
|
|
|
|
|
|
case data := <-sub.DataChan:
|
|
|
|
|
|
if _, err := w.Write(data); err != nil {
|
2025-12-15 21:57:21 +08:00
|
|
|
|
log.Printf("⏹️ [RTMP] FLV 传输结束 (写入失败): stream=%s dataCount=%d err=%v", streamPath, dataCount, err)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-12-15 21:57:21 +08:00
|
|
|
|
dataCount++
|
2025-12-15 11:26:41 +08:00
|
|
|
|
if f, ok := w.(http.Flusher); ok {
|
|
|
|
|
|
f.Flush()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
// handleStreamsAPI 处理流列表 API
|
|
|
|
|
|
func (r *RTMPServer) handleStreamsAPI(w http.ResponseWriter, req *http.Request) {
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
|
|
|
|
|
r.streamsMu.RLock()
|
|
|
|
|
|
defer r.streamsMu.RUnlock()
|
|
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
streams := make([]map[string]interface{}, 0)
|
|
|
|
|
|
for _, s := range r.streams {
|
|
|
|
|
|
if s.IsActive {
|
|
|
|
|
|
streams = append(streams, map[string]interface{}{
|
|
|
|
|
|
"id": s.ID,
|
|
|
|
|
|
"room_id": s.RoomID,
|
|
|
|
|
|
"user_id": s.UserID,
|
|
|
|
|
|
"pull_url": s.PullURL,
|
|
|
|
|
|
"flv_url": s.FLVURL,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
fmt.Fprintf(w, `{"streams":%d,"data":%v}`, len(streams), streams)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Stop 停止 RTMP 服务器
|
|
|
|
|
|
func (r *RTMPServer) Stop() {
|
|
|
|
|
|
r.mu.Lock()
|
|
|
|
|
|
defer r.mu.Unlock()
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
r.running = false
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
// 关闭 RTMP 监听器
|
|
|
|
|
|
if r.rtmpListener != nil {
|
|
|
|
|
|
r.rtmpListener.Close()
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
// 关闭 HTTP 服务器
|
|
|
|
|
|
if r.httpServer != nil {
|
|
|
|
|
|
r.httpServer.Close()
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
// 关闭所有流
|
2025-12-15 11:26:41 +08:00
|
|
|
|
r.streamsMu.Lock()
|
2025-12-15 09:04:14 +08:00
|
|
|
|
for _, stream := range r.streams {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
stream.IsActive = false
|
2025-12-15 09:04:14 +08:00
|
|
|
|
if stream.stopChan != nil {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
select {
|
|
|
|
|
|
case <-stream.stopChan:
|
|
|
|
|
|
default:
|
|
|
|
|
|
close(stream.stopChan)
|
|
|
|
|
|
}
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
r.streams = make(map[string]*RTMPStream)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
r.streamsMu.Unlock()
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭所有订阅者
|
|
|
|
|
|
r.subMu.Lock()
|
|
|
|
|
|
for _, subs := range r.subscribers {
|
|
|
|
|
|
for _, sub := range subs {
|
|
|
|
|
|
close(sub.DataChan)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
r.subscribers = make(map[string]map[string]*Subscriber)
|
|
|
|
|
|
r.subMu.Unlock()
|
|
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
log.Println("🛑 [RTMP] 已停止")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GenerateStreamURLs 为用户生成推拉流地址
|
|
|
|
|
|
func (r *RTMPServer) GenerateStreamURLs(roomID, userID string) (*RTMPStream, error) {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
r.mu.RLock()
|
2025-12-15 09:04:14 +08:00
|
|
|
|
if !r.running {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
r.mu.RUnlock()
|
2025-12-15 09:04:14 +08:00
|
|
|
|
return nil, ErrRTMPNotReady
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
r.mu.RUnlock()
|
|
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
streamID := fmt.Sprintf("%s_%s_%d", roomID, userID, time.Now().UnixNano())
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
publicIP := r.config.PublicIP
|
|
|
|
|
|
if publicIP == "" {
|
|
|
|
|
|
publicIP = viper.GetString("turn.public_ip")
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
token := generateStreamToken(streamID)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
stream := &RTMPStream{
|
|
|
|
|
|
ID: streamID,
|
|
|
|
|
|
RoomID: roomID,
|
|
|
|
|
|
UserID: userID,
|
|
|
|
|
|
PushURL: fmt.Sprintf("rtmp://%s:%d/live/%s?token=%s", publicIP, r.config.RTMPPort, streamID, token),
|
|
|
|
|
|
PullURL: fmt.Sprintf("rtmp://%s:%d/live/%s", publicIP, r.config.RTMPPort, streamID),
|
|
|
|
|
|
FLVURL: fmt.Sprintf("http://%s:%d/live/%s.flv", publicIP, r.config.HTTPFLVPort, streamID),
|
|
|
|
|
|
CreatedAt: time.Now(),
|
|
|
|
|
|
IsActive: true,
|
|
|
|
|
|
stopChan: make(chan struct{}),
|
2025-12-15 11:26:41 +08:00
|
|
|
|
gopCache: make([][]byte, 0),
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
|
|
|
|
|
r.streamsMu.Lock()
|
2025-12-15 09:04:14 +08:00
|
|
|
|
r.streams[streamID] = stream
|
2025-12-15 11:26:41 +08:00
|
|
|
|
r.streamsMu.Unlock()
|
|
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
log.Printf("🎬 [RTMP] 创建流 | Room:%s User:%s Stream:%s", roomID, userID, streamID)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
return stream, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetStream 获取流信息
|
|
|
|
|
|
func (r *RTMPServer) GetStream(streamID string) *RTMPStream {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
r.streamsMu.RLock()
|
|
|
|
|
|
defer r.streamsMu.RUnlock()
|
2025-12-15 09:04:14 +08:00
|
|
|
|
return r.streams[streamID]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetStreamsByRoom 获取房间内所有流
|
|
|
|
|
|
func (r *RTMPServer) GetStreamsByRoom(roomID string) []*RTMPStream {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
r.streamsMu.RLock()
|
|
|
|
|
|
defer r.streamsMu.RUnlock()
|
|
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
streams := make([]*RTMPStream, 0)
|
|
|
|
|
|
for _, s := range r.streams {
|
|
|
|
|
|
if s.RoomID == roomID && s.IsActive {
|
|
|
|
|
|
streams = append(streams, s)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return streams
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetStreamsByUser 获取用户的所有流
|
|
|
|
|
|
func (r *RTMPServer) GetStreamsByUser(userID string) []*RTMPStream {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
r.streamsMu.RLock()
|
|
|
|
|
|
defer r.streamsMu.RUnlock()
|
|
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
streams := make([]*RTMPStream, 0)
|
|
|
|
|
|
for _, s := range r.streams {
|
|
|
|
|
|
if s.UserID == userID && s.IsActive {
|
|
|
|
|
|
streams = append(streams, s)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return streams
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RemoveStream 移除流
|
|
|
|
|
|
func (r *RTMPServer) RemoveStream(streamID string) {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
r.streamsMu.Lock()
|
|
|
|
|
|
defer r.streamsMu.Unlock()
|
|
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
if stream, exists := r.streams[streamID]; exists {
|
|
|
|
|
|
stream.IsActive = false
|
|
|
|
|
|
if stream.stopChan != nil {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
select {
|
|
|
|
|
|
case <-stream.stopChan:
|
|
|
|
|
|
default:
|
|
|
|
|
|
close(stream.stopChan)
|
|
|
|
|
|
}
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
delete(r.streams, streamID)
|
|
|
|
|
|
log.Printf("🎬 [RTMP] 移除流 | Stream:%s", streamID)
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
|
|
|
|
|
r.subMu.Lock()
|
|
|
|
|
|
delete(r.subscribers, streamID)
|
|
|
|
|
|
r.subMu.Unlock()
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RemoveStreamsByUser 移除用户的所有流
|
|
|
|
|
|
func (r *RTMPServer) RemoveStreamsByUser(userID string) {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
r.streamsMu.Lock()
|
|
|
|
|
|
defer r.streamsMu.Unlock()
|
|
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
for streamID, stream := range r.streams {
|
|
|
|
|
|
if stream.UserID == userID {
|
|
|
|
|
|
stream.IsActive = false
|
|
|
|
|
|
if stream.stopChan != nil {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
select {
|
|
|
|
|
|
case <-stream.stopChan:
|
|
|
|
|
|
default:
|
|
|
|
|
|
close(stream.stopChan)
|
|
|
|
|
|
}
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
delete(r.streams, streamID)
|
|
|
|
|
|
log.Printf("🎬 [RTMP] 移除用户流 | User:%s Stream:%s", userID, streamID)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
|
|
|
|
|
r.subMu.Lock()
|
|
|
|
|
|
delete(r.subscribers, streamID)
|
|
|
|
|
|
r.subMu.Unlock()
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RemoveStreamsByRoom 移除房间的所有流
|
|
|
|
|
|
func (r *RTMPServer) RemoveStreamsByRoom(roomID string) {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
r.streamsMu.Lock()
|
|
|
|
|
|
defer r.streamsMu.Unlock()
|
|
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
for streamID, stream := range r.streams {
|
|
|
|
|
|
if stream.RoomID == roomID {
|
|
|
|
|
|
stream.IsActive = false
|
|
|
|
|
|
if stream.stopChan != nil {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
select {
|
|
|
|
|
|
case <-stream.stopChan:
|
|
|
|
|
|
default:
|
|
|
|
|
|
close(stream.stopChan)
|
|
|
|
|
|
}
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
delete(r.streams, streamID)
|
|
|
|
|
|
log.Printf("🎬 [RTMP] 移除房间流 | Room:%s Stream:%s", roomID, streamID)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
|
|
|
|
|
r.subMu.Lock()
|
|
|
|
|
|
delete(r.subscribers, streamID)
|
|
|
|
|
|
r.subMu.Unlock()
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetStreamCount 获取流数量
|
|
|
|
|
|
func (r *RTMPServer) GetStreamCount() int {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
r.streamsMu.RLock()
|
|
|
|
|
|
defer r.streamsMu.RUnlock()
|
2025-12-15 09:04:14 +08:00
|
|
|
|
return len(r.streams)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// cleanupTask 清理过期流
|
|
|
|
|
|
func (r *RTMPServer) cleanupTask() {
|
|
|
|
|
|
ticker := time.NewTicker(5 * time.Minute)
|
|
|
|
|
|
defer ticker.Stop()
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
for {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
|
r.cleanupExpiredStreams()
|
|
|
|
|
|
}
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
r.mu.RLock()
|
|
|
|
|
|
if !r.running {
|
|
|
|
|
|
r.mu.RUnlock()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
r.mu.RUnlock()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// cleanupExpiredStreams 清理过期的流
|
|
|
|
|
|
func (r *RTMPServer) cleanupExpiredStreams() {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
r.streamsMu.Lock()
|
|
|
|
|
|
defer r.streamsMu.Unlock()
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
expireTime := time.Now().Add(-1 * time.Hour)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
for streamID, stream := range r.streams {
|
|
|
|
|
|
if stream.CreatedAt.Before(expireTime) && !stream.IsActive {
|
|
|
|
|
|
if stream.stopChan != nil {
|
2025-12-15 11:26:41 +08:00
|
|
|
|
select {
|
|
|
|
|
|
case <-stream.stopChan:
|
|
|
|
|
|
default:
|
|
|
|
|
|
close(stream.stopChan)
|
|
|
|
|
|
}
|
2025-12-15 09:04:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
delete(r.streams, streamID)
|
|
|
|
|
|
log.Printf("🗑️ [RTMP] 清理过期流 | Stream:%s", streamID)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// generateStreamToken 生成流鉴权 Token
|
|
|
|
|
|
func generateStreamToken(streamID string) string {
|
|
|
|
|
|
secret := viper.GetString("turn.shared_secret")
|
|
|
|
|
|
timestamp := time.Now().Add(24 * time.Hour).Unix()
|
|
|
|
|
|
data := fmt.Sprintf("%s:%d", streamID, timestamp)
|
2025-12-15 11:26:41 +08:00
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
mac := hmac.New(sha1.New, []byte(secret))
|
|
|
|
|
|
mac.Write([]byte(data))
|
|
|
|
|
|
return base64.URLEncoding.EncodeToString(mac.Sum(nil))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 21:57:21 +08:00
|
|
|
|
// GenerateStreamToken 生成流鉴权 Token (公开接口)
|
|
|
|
|
|
func GenerateStreamToken(streamID string) string {
|
|
|
|
|
|
return generateStreamToken(streamID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 09:04:14 +08:00
|
|
|
|
// ValidateStreamToken 验证流鉴权 Token
|
|
|
|
|
|
func ValidateStreamToken(streamID, token string) bool {
|
|
|
|
|
|
expectedToken := generateStreamToken(streamID)
|
|
|
|
|
|
return token == expectedToken
|
|
|
|
|
|
}
|