微信小程序\推拉流

This commit is contained in:
2025-12-15 13:21:08 +08:00
parent 3994d40f5a
commit 2fca6e5c43
3 changed files with 180 additions and 59 deletions

View File

@@ -59,9 +59,10 @@ type WebRTCICERequest struct {
type JoinCallRoomResponse struct {
RoomID string `json:"room_id"`
Platform string `json:"platform"`
ICEServers []ICEServerConfig `json:"ice_servers,omitempty"` // H5/App 用
PushURL string `json:"push_url,omitempty"` // 小程序用
PullURLs []PullURLInfo `json:"pull_urls,omitempty"` // 小程序用
ICEServers []ICEServerConfig `json:"ice_servers,omitempty"` // H5/App 用
FlvPullURLs []PullURLInfo `json:"flv_pull_urls,omitempty"` // H5/App 拉取小程序流的 FLV 地址
PushURL string `json:"push_url,omitempty"` // 小程序用
PullURLs []PullURLInfo `json:"pull_urls,omitempty"` // 小程序用
Participants []ParticipantInfo `json:"participants"`
}
@@ -164,6 +165,20 @@ func JoinCallRoomHandler(c *gin.Context) {
// 返回 ICE 服务器配置
response.ICEServers = getICEServers(req.UserID)
// 获取房间内小程序用户的 FLV 拉流地址(用于 Web 端播放小程序流)
flvPullURLs := make([]PullURLInfo, 0)
for _, p := range room.GetOtherParticipants(req.UserID) {
// 只返回小程序用户的 FLV 地址
if p.Type == mediaserver.ParticipantTypeRTMP && p.FLVURL != "" {
flvPullURLs = append(flvPullURLs, PullURLInfo{
UserID: p.UserID,
URL: p.PullURL, // RTMP 地址
FLVURL: p.FLVURL, // HTTP-FLV 地址
})
}
}
response.FlvPullURLs = flvPullURLs
case "miniprogram":
// 小程序使用 RTMP
pType := mediaserver.ParticipantTypeRTMP
@@ -187,9 +202,10 @@ func JoinCallRoomHandler(c *gin.Context) {
}
// 更新参与者的流地址
room.SetParticipantRTMPURLs(req.UserID, stream.PushURL, stream.PullURL, stream.ID)
room.SetParticipantRTMPURLs(req.UserID, stream.PushURL, stream.PullURL, stream.FLVURL, stream.ID)
participant.PushURL = stream.PushURL
participant.PullURL = stream.PullURL
participant.FLVURL = stream.FLVURL
participant.StreamID = stream.ID
response.PushURL = stream.PushURL
@@ -201,6 +217,7 @@ func JoinCallRoomHandler(c *gin.Context) {
pullURLs = append(pullURLs, PullURLInfo{
UserID: p.UserID,
URL: p.PullURL,
FLVURL: p.FLVURL,
})
}
}

View File

@@ -25,18 +25,19 @@ const (
// Participant 房间参与者
type Participant struct {
UserID string `json:"user_id"`
Type ParticipantType `json:"type"`
JoinedAt time.Time `json:"joined_at"`
HasAudio bool `json:"has_audio"`
HasVideo bool `json:"has_video"`
UserID string `json:"user_id"`
Type ParticipantType `json:"type"`
JoinedAt time.Time `json:"joined_at"`
HasAudio bool `json:"has_audio"`
HasVideo bool `json:"has_video"`
// WebRTC 相关
PeerConnection interface{} `json:"-"` // *webrtc.PeerConnection
// RTMP 相关
PushURL string `json:"push_url,omitempty"` // 推流地址
PullURL string `json:"pull_url,omitempty"` // 拉流地址
PullURL string `json:"pull_url,omitempty"` // 拉流地址 (RTMP)
FLVURL string `json:"flv_url,omitempty"` // HTTP-FLV 拉流地址 (给 Web 端用)
StreamID string `json:"stream_id,omitempty"` // 流ID
}
@@ -46,24 +47,24 @@ type Room struct {
CreatedAt time.Time `json:"created_at"`
MaxSize int `json:"max_size"`
Participants map[string]*Participant `json:"participants"`
// 房间状态
CallType string `json:"call_type"` // audio/video
IsGroupCall bool `json:"is_group_call"`
mu sync.RWMutex
closeChan chan struct{}
closed bool
CallType string `json:"call_type"` // audio/video
IsGroupCall bool `json:"is_group_call"`
mu sync.RWMutex
closeChan chan struct{}
closed bool
}
// RoomInfo 房间信息用于API返回
type RoomInfo struct {
ID string `json:"id"`
CreatedAt time.Time `json:"created_at"`
ParticipantCount int `json:"participant_count"`
CallType string `json:"call_type"`
IsGroupCall bool `json:"is_group_call"`
Participants []ParticipantInfo `json:"participants"`
ID string `json:"id"`
CreatedAt time.Time `json:"created_at"`
ParticipantCount int `json:"participant_count"`
CallType string `json:"call_type"`
IsGroupCall bool `json:"is_group_call"`
Participants []ParticipantInfo `json:"participants"`
}
// ParticipantInfo 参与者信息用于API返回
@@ -99,30 +100,30 @@ func (r *Room) SetCallType(callType string, isGroup bool) {
func (r *Room) AddParticipant(userID string, pType ParticipantType) (*Participant, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.closed {
return nil, ErrRoomClosed
}
// 检查是否已存在
if p, exists := r.Participants[userID]; exists {
return p, nil
}
// 检查房间容量
if len(r.Participants) >= r.MaxSize {
return nil, ErrRoomFull
}
participant := &Participant{
UserID: userID,
Type: pType,
JoinedAt: time.Now(),
}
r.Participants[userID] = participant
log.Printf("🎥 [Room:%s] 用户 %s 加入 (类型: %d)", r.ID, userID, pType)
return participant, nil
}
@@ -130,7 +131,7 @@ func (r *Room) AddParticipant(userID string, pType ParticipantType) (*Participan
func (r *Room) RemoveParticipant(userID string) {
r.mu.Lock()
defer r.mu.Unlock()
if p, exists := r.Participants[userID]; exists {
// 清理 PeerConnection
if p.PeerConnection != nil {
@@ -139,7 +140,7 @@ func (r *Room) RemoveParticipant(userID string) {
delete(r.Participants, userID)
log.Printf("🎥 [Room:%s] 用户 %s 离开", r.ID, userID)
}
// 如果房间空了,标记为可清理
if len(r.Participants) == 0 {
log.Printf("🎥 [Room:%s] 房间已空,等待清理", r.ID)
@@ -157,7 +158,7 @@ func (r *Room) GetParticipant(userID string) *Participant {
func (r *Room) GetAllParticipants() []*Participant {
r.mu.RLock()
defer r.mu.RUnlock()
participants := make([]*Participant, 0, len(r.Participants))
for _, p := range r.Participants {
participants = append(participants, p)
@@ -169,7 +170,7 @@ func (r *Room) GetAllParticipants() []*Participant {
func (r *Room) GetOtherParticipants(excludeUserID string) []*Participant {
r.mu.RLock()
defer r.mu.RUnlock()
participants := make([]*Participant, 0, len(r.Participants)-1)
for _, p := range r.Participants {
if p.UserID != excludeUserID {
@@ -204,7 +205,7 @@ func (r *Room) IsFull() bool {
func (r *Room) GetInfo() *RoomInfo {
r.mu.RLock()
defer r.mu.RUnlock()
participants := make([]ParticipantInfo, 0, len(r.Participants))
for _, p := range r.Participants {
typeName := "WebRTC"
@@ -220,7 +221,7 @@ func (r *Room) GetInfo() *RoomInfo {
JoinedAt: p.JoinedAt,
})
}
return &RoomInfo{
ID: r.ID,
CreatedAt: r.CreatedAt,
@@ -235,7 +236,7 @@ func (r *Room) GetInfo() *RoomInfo {
func (r *Room) UpdateParticipantMedia(userID string, hasAudio, hasVideo bool) {
r.mu.Lock()
defer r.mu.Unlock()
if p, exists := r.Participants[userID]; exists {
p.HasAudio = hasAudio
p.HasVideo = hasVideo
@@ -243,13 +244,14 @@ func (r *Room) UpdateParticipantMedia(userID string, hasAudio, hasVideo bool) {
}
// SetParticipantRTMPURLs 设置参与者的RTMP推拉流地址
func (r *Room) SetParticipantRTMPURLs(userID, pushURL, pullURL, streamID string) {
func (r *Room) SetParticipantRTMPURLs(userID, pushURL, pullURL, flvURL, streamID string) {
r.mu.Lock()
defer r.mu.Unlock()
if p, exists := r.Participants[userID]; exists {
p.PushURL = pushURL
p.PullURL = pullURL
p.FLVURL = flvURL
p.StreamID = streamID
}
}
@@ -258,14 +260,14 @@ func (r *Room) SetParticipantRTMPURLs(userID, pushURL, pullURL, streamID string)
func (r *Room) Close() {
r.mu.Lock()
defer r.mu.Unlock()
if r.closed {
return
}
r.closed = true
close(r.closeChan)
// 清理所有参与者
for userID, p := range r.Participants {
if p.PeerConnection != nil {
@@ -273,7 +275,7 @@ func (r *Room) Close() {
}
delete(r.Participants, userID)
}
log.Printf("🎥 [Room:%s] 已关闭", r.ID)
}
@@ -288,7 +290,7 @@ func (r *Room) IsClosed() bool {
func (r *Room) Broadcast(excludeUserID string, message interface{}) {
r.mu.RLock()
defer r.mu.RUnlock()
for userID := range r.Participants {
if userID != excludeUserID {
// TODO: 通过 WebSocket 发送消息
@@ -296,4 +298,3 @@ func (r *Room) Broadcast(excludeUserID string, message interface{}) {
}
}
}

View File

@@ -8,6 +8,7 @@ package middleware
import (
"bytes"
"encoding/json"
"fmt"
"io"
"strings"
"time"
@@ -18,6 +19,90 @@ import (
"gorm.io/gorm"
)
// 二进制 Content-Type 前缀列表
var binaryContentTypes = []string{
"image/",
"audio/",
"video/",
"application/octet-stream",
"application/pdf",
"application/zip",
"application/x-rar",
"application/x-7z",
"application/gzip",
"application/x-tar",
"font/",
}
// 不需要记录响应体的路由前缀
var skipResponseBodyRoutes = []string{
"/uploads/",
"/static/",
}
// isBinaryContentType 检测是否为二进制 Content-Type
func isBinaryContentType(contentType string) bool {
contentType = strings.ToLower(contentType)
for _, prefix := range binaryContentTypes {
if strings.HasPrefix(contentType, prefix) {
return true
}
}
return false
}
// isBinaryData 检测数据是否为二进制(通过检查是否包含非 UTF-8 字符)
func isBinaryData(data []byte) bool {
if len(data) == 0 {
return false
}
// 检查前 512 字节是否包含二进制特征
checkLen := len(data)
if checkLen > 512 {
checkLen = 512
}
for i := 0; i < checkLen; i++ {
// 检测常见的二进制文件头
if data[i] == 0 {
return true
}
}
// 检查是否以常见的二进制文件头开始
if len(data) >= 2 {
// JPEG: FF D8
if data[0] == 0xFF && data[1] == 0xD8 {
return true
}
// PNG: 89 50
if data[0] == 0x89 && data[1] == 0x50 {
return true
}
// GIF: 47 49
if data[0] == 0x47 && data[1] == 0x49 {
return true
}
// PDF: 25 50
if data[0] == 0x25 && data[1] == 0x50 {
return true
}
// ZIP/DOCX/XLSX: 50 4B
if data[0] == 0x50 && data[1] == 0x4B {
return true
}
}
return false
}
// shouldSkipResponseBody 检测是否应该跳过响应体记录
func shouldSkipResponseBody(path string) bool {
for _, prefix := range skipResponseBodyRoutes {
if strings.HasPrefix(path, prefix) {
return true
}
}
return false
}
// RequestLogMiddleware 请求日志中间件
type RequestLogMiddleware struct {
DB *gorm.DB
@@ -39,7 +124,7 @@ func (m *RequestLogMiddleware) Handler() gin.HandlerFunc {
// 获取请求IP
ip := getClientIP(c)
// 本地IP不记录
if utils.GetIPLocation(ip) == "本地" {
c.Next()
@@ -67,7 +152,7 @@ func (m *RequestLogMiddleware) Handler() gin.HandlerFunc {
// 创建响应写入器
writer := &responseWriter{
ResponseWriter: c.Writer,
body: &bytes.Buffer{},
body: &bytes.Buffer{},
}
c.Writer = writer
@@ -89,24 +174,43 @@ func (m *RequestLogMiddleware) logRequest(c *gin.Context, ip, userID string, req
// 获取响应code从响应体中解析
responseCode := 0
if len(responseBody) > 0 {
// 尝试解析响应体获取code
if len(responseBody) > 0 && !isBinaryData(responseBody) {
// 尝试解析响应体获取code(仅对非二进制数据)
var resp model.ApiResponse
if err := json.Unmarshal(responseBody, &resp); err == nil {
responseCode = resp.Code
}
}
// 限制请求参数长度(避免存储过大)
requestParams := string(requestBody)
if len(requestParams) > 5000 {
requestParams = requestParams[:5000] + "...(truncated)"
// 处理请求参数
var requestParams string
requestContentType := c.GetHeader("Content-Type")
if isBinaryContentType(requestContentType) || isBinaryData(requestBody) {
// 二进制请求体,只记录大小
requestParams = fmt.Sprintf("[binary data: %d bytes]", len(requestBody))
} else {
requestParams = string(requestBody)
if len(requestParams) > 5000 {
requestParams = requestParams[:5000] + "...(truncated)"
}
}
// 限制返回参数长度
responseParams := string(responseBody)
if len(responseParams) > 5000 {
responseParams = responseParams[:5000] + "...(truncated)"
// 处理响应参数
var responseParams string
responseContentType := c.Writer.Header().Get("Content-Type")
requestPath := c.Request.URL.Path
if shouldSkipResponseBody(requestPath) {
// 静态文件路由,跳过响应体记录
responseParams = fmt.Sprintf("[static file: %d bytes]", len(responseBody))
} else if isBinaryContentType(responseContentType) || isBinaryData(responseBody) {
// 二进制响应体,只记录大小
responseParams = fmt.Sprintf("[binary data: %d bytes]", len(responseBody))
} else {
responseParams = string(responseBody)
if len(responseParams) > 5000 {
responseParams = responseParams[:5000] + "...(truncated)"
}
}
// 创建日志记录
@@ -170,4 +274,3 @@ func (w *responseWriter) WriteHeader(statusCode int) {
w.status = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}