微信小程序
This commit is contained in:
501
internal/mediaserver/rtmp.go
Normal file
501
internal/mediaserver/rtmp.go
Normal file
@@ -0,0 +1,501 @@
|
||||
/**
|
||||
* package mediaserver
|
||||
*
|
||||
* RTMP 服务(用于微信小程序 live-pusher/live-player)
|
||||
* 功能:
|
||||
* 1. 接收小程序推流
|
||||
* 2. 提供拉流服务
|
||||
* 3. 支持 HTTP-FLV 协议
|
||||
*
|
||||
* 注意:当前版本使用简化实现,实际 RTMP 协议处理需要集成完整的 RTMP 服务器
|
||||
* 生产环境建议使用 nginx-rtmp 或独立部署 livego
|
||||
*/
|
||||
package mediaserver
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// RTMPServer RTMP 服务器
|
||||
type RTMPServer struct {
|
||||
config *MediaServerConfig
|
||||
mu sync.RWMutex
|
||||
running bool
|
||||
streams map[string]*RTMPStream // streamID -> stream
|
||||
|
||||
// 服务器监听器
|
||||
rtmpListener net.Listener
|
||||
httpServer *http.Server
|
||||
}
|
||||
|
||||
// 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"`
|
||||
|
||||
// 流数据缓存
|
||||
dataChan chan []byte `json:"-"`
|
||||
stopChan chan struct{} `json:"-"`
|
||||
}
|
||||
|
||||
// NewRTMPServer 创建 RTMP 服务器
|
||||
func NewRTMPServer(config *MediaServerConfig) *RTMPServer {
|
||||
return &RTMPServer{
|
||||
config: config,
|
||||
streams: make(map[string]*RTMPStream),
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动 RTMP 服务器
|
||||
func (r *RTMPServer) Start() {
|
||||
r.mu.Lock()
|
||||
if r.running {
|
||||
r.mu.Unlock()
|
||||
return
|
||||
}
|
||||
r.running = true
|
||||
r.mu.Unlock()
|
||||
|
||||
// 启动 RTMP 服务器(简化版)
|
||||
go r.startRTMPServer()
|
||||
|
||||
// 启动 HTTP-FLV 服务器
|
||||
go r.startHTTPFLVServer()
|
||||
|
||||
// 启动流清理任务
|
||||
go r.cleanupTask()
|
||||
|
||||
log.Printf("🚀 [RTMP] RTMP 服务已启动 | RTMP端口: %d | HTTP-FLV端口: %d",
|
||||
r.config.RTMPPort, r.config.HTTPFLVPort)
|
||||
}
|
||||
|
||||
// startRTMPServer 启动 RTMP 服务(简化版)
|
||||
func (r *RTMPServer) startRTMPServer() {
|
||||
addr := fmt.Sprintf(":%d", r.config.RTMPPort)
|
||||
|
||||
var err error
|
||||
r.rtmpListener, err = net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
log.Printf("❌ [RTMP] RTMP 监听失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("🎬 [RTMP] RTMP 服务监听: %s", addr)
|
||||
|
||||
for {
|
||||
r.mu.RLock()
|
||||
if !r.running {
|
||||
r.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
|
||||
conn, err := r.rtmpListener.Accept()
|
||||
if err != nil {
|
||||
if !r.running {
|
||||
return
|
||||
}
|
||||
log.Printf("⚠️ [RTMP] 接受连接失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理 RTMP 连接
|
||||
go r.handleRTMPConnection(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// handleRTMPConnection 处理 RTMP 连接
|
||||
// 简化版:基本的 RTMP 握手和流处理
|
||||
func (r *RTMPServer) handleRTMPConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
// RTMP 协议处理
|
||||
// 步骤1: RTMP 握手
|
||||
if err := r.doHandshake(conn); err != nil {
|
||||
log.Printf("⚠️ [RTMP] 握手失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("🎬 [RTMP] 新连接: %s", conn.RemoteAddr().String())
|
||||
|
||||
// 步骤2: 读取 RTMP 消息
|
||||
// 简化实现:等待数据并转发
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
r.mu.RLock()
|
||||
if !r.running {
|
||||
r.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Printf("⚠️ [RTMP] 读取失败: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 解析 RTMP 消息并路由到对应的流
|
||||
_ = n
|
||||
}
|
||||
}
|
||||
|
||||
// doHandshake 执行 RTMP 握手
|
||||
func (r *RTMPServer) doHandshake(conn net.Conn) error {
|
||||
// RTMP 握手协议
|
||||
// C0 + C1 -> S0 + S1 + S2 -> C2
|
||||
|
||||
c0c1 := make([]byte, 1537) // C0 (1) + C1 (1536)
|
||||
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
if _, err := io.ReadFull(conn, c0c1); err != nil {
|
||||
return fmt.Errorf("read C0C1 failed: %w", err)
|
||||
}
|
||||
|
||||
// 验证版本号 (C0 应该是 0x03)
|
||||
if c0c1[0] != 0x03 {
|
||||
return fmt.Errorf("invalid RTMP version: %d", c0c1[0])
|
||||
}
|
||||
|
||||
// 发送 S0 + S1 + S2
|
||||
s0s1s2 := make([]byte, 3073) // S0 (1) + S1 (1536) + S2 (1536)
|
||||
s0s1s2[0] = 0x03 // RTMP version 3
|
||||
|
||||
// S1: 时间戳(4) + 零(4) + 随机数据(1528)
|
||||
// S2: 客户端时间戳(4) + 服务器时间戳(4) + 随机数据(1528)
|
||||
copy(s0s1s2[1:1537], c0c1[1:]) // S1 = C1
|
||||
copy(s0s1s2[1537:], c0c1[1:]) // S2 = C1
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
if _, err := conn.Write(s0s1s2); err != nil {
|
||||
return fmt.Errorf("write S0S1S2 failed: %w", err)
|
||||
}
|
||||
|
||||
// 读取 C2
|
||||
c2 := make([]byte, 1536)
|
||||
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
if _, err := io.ReadFull(conn, c2); err != nil {
|
||||
return fmt.Errorf("read C2 failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// startHTTPFLVServer 启动 HTTP-FLV 服务
|
||||
func (r *RTMPServer) startHTTPFLVServer() {
|
||||
addr := fmt.Sprintf(":%d", r.config.HTTPFLVPort)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/live/", r.handleFLVRequest)
|
||||
mux.HandleFunc("/api/streams", r.handleStreamsAPI)
|
||||
|
||||
r.httpServer = &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
log.Printf("🎬 [RTMP] HTTP-FLV 服务监听: %s", addr)
|
||||
|
||||
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) {
|
||||
// 解析流ID: /live/{streamID}.flv
|
||||
path := req.URL.Path
|
||||
if len(path) < 7 { // "/live/" = 6, 至少还需要1个字符
|
||||
http.Error(w, "Invalid path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 提取 streamID
|
||||
streamPath := path[6:] // 去掉 "/live/"
|
||||
if len(streamPath) > 4 && streamPath[len(streamPath)-4:] == ".flv" {
|
||||
streamPath = streamPath[:len(streamPath)-4]
|
||||
}
|
||||
|
||||
r.mu.RLock()
|
||||
stream, exists := r.streams[streamPath]
|
||||
r.mu.RUnlock()
|
||||
|
||||
if !exists || !stream.IsActive {
|
||||
http.Error(w, "Stream not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
w.Header().Set("Content-Type", "video/x-flv")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Transfer-Encoding", "chunked")
|
||||
|
||||
// TODO: 实际的 FLV 数据推送
|
||||
// 当前为占位实现
|
||||
log.Printf("🎬 [RTMP] FLV 请求: %s", streamPath)
|
||||
|
||||
// 发送 FLV header
|
||||
flvHeader := []byte{0x46, 0x4C, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00}
|
||||
w.Write(flvHeader)
|
||||
|
||||
// 保持连接直到客户端断开
|
||||
<-req.Context().Done()
|
||||
}
|
||||
|
||||
// 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", "*")
|
||||
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, `{"streams":%d,"data":%v}`, len(streams), streams)
|
||||
}
|
||||
|
||||
// Stop 停止 RTMP 服务器
|
||||
func (r *RTMPServer) Stop() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.running = false
|
||||
|
||||
// 关闭 RTMP 监听器
|
||||
if r.rtmpListener != nil {
|
||||
r.rtmpListener.Close()
|
||||
}
|
||||
|
||||
// 关闭 HTTP 服务器
|
||||
if r.httpServer != nil {
|
||||
r.httpServer.Close()
|
||||
}
|
||||
|
||||
// 关闭所有流
|
||||
for _, stream := range r.streams {
|
||||
if stream.stopChan != nil {
|
||||
close(stream.stopChan)
|
||||
}
|
||||
}
|
||||
r.streams = make(map[string]*RTMPStream)
|
||||
|
||||
log.Println("🛑 [RTMP] 已停止")
|
||||
}
|
||||
|
||||
// GenerateStreamURLs 为用户生成推拉流地址
|
||||
func (r *RTMPServer) GenerateStreamURLs(roomID, userID string) (*RTMPStream, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if !r.running {
|
||||
return nil, ErrRTMPNotReady
|
||||
}
|
||||
|
||||
// 生成唯一的流ID
|
||||
streamID := fmt.Sprintf("%s_%s_%d", roomID, userID, time.Now().UnixNano())
|
||||
|
||||
// 获取服务器配置
|
||||
publicIP := r.config.PublicIP
|
||||
if publicIP == "" {
|
||||
publicIP = viper.GetString("turn.public_ip")
|
||||
}
|
||||
|
||||
// 生成鉴权 Token
|
||||
token := generateStreamToken(streamID)
|
||||
|
||||
// 生成 URL
|
||||
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,
|
||||
dataChan: make(chan []byte, 100),
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
r.streams[streamID] = stream
|
||||
log.Printf("🎬 [RTMP] 创建流 | Room:%s User:%s Stream:%s", roomID, userID, streamID)
|
||||
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
// GetStream 获取流信息
|
||||
func (r *RTMPServer) GetStream(streamID string) *RTMPStream {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.streams[streamID]
|
||||
}
|
||||
|
||||
// GetStreamsByRoom 获取房间内所有流
|
||||
func (r *RTMPServer) GetStreamsByRoom(roomID string) []*RTMPStream {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
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 {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
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) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if stream, exists := r.streams[streamID]; exists {
|
||||
stream.IsActive = false
|
||||
if stream.stopChan != nil {
|
||||
close(stream.stopChan)
|
||||
}
|
||||
delete(r.streams, streamID)
|
||||
log.Printf("🎬 [RTMP] 移除流 | Stream:%s", streamID)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveStreamsByUser 移除用户的所有流
|
||||
func (r *RTMPServer) RemoveStreamsByUser(userID string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
for streamID, stream := range r.streams {
|
||||
if stream.UserID == userID {
|
||||
stream.IsActive = false
|
||||
if stream.stopChan != nil {
|
||||
close(stream.stopChan)
|
||||
}
|
||||
delete(r.streams, streamID)
|
||||
log.Printf("🎬 [RTMP] 移除用户流 | User:%s Stream:%s", userID, streamID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveStreamsByRoom 移除房间的所有流
|
||||
func (r *RTMPServer) RemoveStreamsByRoom(roomID string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
for streamID, stream := range r.streams {
|
||||
if stream.RoomID == roomID {
|
||||
stream.IsActive = false
|
||||
if stream.stopChan != nil {
|
||||
close(stream.stopChan)
|
||||
}
|
||||
delete(r.streams, streamID)
|
||||
log.Printf("🎬 [RTMP] 移除房间流 | Room:%s Stream:%s", roomID, streamID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetStreamCount 获取流数量
|
||||
func (r *RTMPServer) GetStreamCount() int {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.streams)
|
||||
}
|
||||
|
||||
// cleanupTask 清理过期流
|
||||
func (r *RTMPServer) cleanupTask() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
r.cleanupExpiredStreams()
|
||||
}
|
||||
|
||||
r.mu.RLock()
|
||||
if !r.running {
|
||||
r.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupExpiredStreams 清理过期的流
|
||||
func (r *RTMPServer) cleanupExpiredStreams() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
expireTime := time.Now().Add(-1 * time.Hour) // 1小时过期
|
||||
|
||||
for streamID, stream := range r.streams {
|
||||
if stream.CreatedAt.Before(expireTime) && !stream.IsActive {
|
||||
if stream.stopChan != nil {
|
||||
close(stream.stopChan)
|
||||
}
|
||||
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)
|
||||
|
||||
mac := hmac.New(sha1.New, []byte(secret))
|
||||
mac.Write([]byte(data))
|
||||
return base64.URLEncoding.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
// ValidateStreamToken 验证流鉴权 Token
|
||||
func ValidateStreamToken(streamID, token string) bool {
|
||||
expectedToken := generateStreamToken(streamID)
|
||||
return token == expectedToken
|
||||
}
|
||||
Reference in New Issue
Block a user