From c8ab9b74abc2be48f29effa1006e03f45323ecd2 Mon Sep 17 00:00:00 2001 From: liqi Date: Tue, 16 Dec 2025 15:12:03 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BD=91=E7=AB=99=E9=80=9A=E8=AF=9D=E4=BD=BF?= =?UTF-8?q?=E7=94=A8webrtc=EF=BC=8C=E6=96=B0=E5=A2=9Effmpeg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/mediaserver/ffmpeg_transcoder.go | 52 +++++++++++++++++------ internal/mediaserver/rtmp.go | 32 ++++++++++---- internal/mediaserver/ws_rtmp_proxy.go | 49 ++++++++++++++++++--- 3 files changed, 103 insertions(+), 30 deletions(-) diff --git a/internal/mediaserver/ffmpeg_transcoder.go b/internal/mediaserver/ffmpeg_transcoder.go index c455043..3ff29ea 100644 --- a/internal/mediaserver/ffmpeg_transcoder.go +++ b/internal/mediaserver/ffmpeg_transcoder.go @@ -19,6 +19,9 @@ import ( // TranscoderConfig 转码器配置 type TranscoderConfig struct { + // 模式配置 + CopyMode bool // 是否使用 copy 模式(H.264 输入时使用,只重封装不重新编码) + // 视频配置 VideoCodec string // 输出视频编解码器,默认 libx264 VideoPreset string // x264 预设,默认 ultrafast @@ -96,20 +99,41 @@ func (t *FFmpegTranscoder) Start() error { } // 构建 FFmpeg 命令 - // ffmpeg -f webm -i pipe:0 -c:v libx264 -preset ultrafast -tune zerolatency -c:a aac -f flv pipe:1 - args := []string{ - "-f", "webm", // 输入格式 - "-i", "pipe:0", // 从 stdin 读取 - "-c:v", t.config.VideoCodec, - "-preset", t.config.VideoPreset, - "-tune", "zerolatency", // 零延迟模式 - "-b:v", t.config.VideoBitrate, - "-c:a", t.config.AudioCodec, - "-b:a", t.config.AudioBitrate, - "-ar", fmt.Sprintf("%d", t.config.AudioSampleRate), - "-ac", fmt.Sprintf("%d", t.config.AudioChannels), - "-f", "flv", // 输出格式 - "pipe:1", // 输出到 stdout + var args []string + + if t.config.CopyMode { + // H.264 copy 模式:只重封装,不重新编码(低延迟) + // ffmpeg -f webm -i pipe:0 -c:v copy -c:a aac -f flv pipe:1 + args = []string{ + "-f", "webm", // 输入格式 + "-i", "pipe:0", // 从 stdin 读取 + "-c:v", "copy", // 视频直接复制,不重新编码 + "-c:a", t.config.AudioCodec, + "-b:a", t.config.AudioBitrate, + "-ar", fmt.Sprintf("%d", t.config.AudioSampleRate), + "-ac", fmt.Sprintf("%d", t.config.AudioChannels), + "-f", "flv", // 输出格式 + "pipe:1", // 输出到 stdout + } + log.Printf("🎬 [FFmpegTranscoder] 使用 copy 模式(H.264 重封装)") + } else { + // VP8/VP9 转码模式:重新编码为 H.264 + // ffmpeg -f webm -i pipe:0 -c:v libx264 -preset ultrafast -tune zerolatency -c:a aac -f flv pipe:1 + args = []string{ + "-f", "webm", // 输入格式 + "-i", "pipe:0", // 从 stdin 读取 + "-c:v", t.config.VideoCodec, + "-preset", t.config.VideoPreset, + "-tune", "zerolatency", // 零延迟模式 + "-b:v", t.config.VideoBitrate, + "-c:a", t.config.AudioCodec, + "-b:a", t.config.AudioBitrate, + "-ar", fmt.Sprintf("%d", t.config.AudioSampleRate), + "-ac", fmt.Sprintf("%d", t.config.AudioChannels), + "-f", "flv", // 输出格式 + "pipe:1", // 输出到 stdout + } + log.Printf("🎬 [FFmpegTranscoder] 使用转码模式(VP8/VP9 → H.264)") } t.cmd = exec.Command(t.ffmpegPath, args...) diff --git a/internal/mediaserver/rtmp.go b/internal/mediaserver/rtmp.go index c0c928f..3d780fd 100644 --- a/internal/mediaserver/rtmp.go +++ b/internal/mediaserver/rtmp.go @@ -181,9 +181,9 @@ type ConnectionHandler struct { remoteAddr string // 连接状态 - streamID string - isPublish bool - appName string + streamID string + isPublish bool + appName string msgStreamID uint32 // 统计信息 @@ -773,6 +773,18 @@ func (r *RTMPServer) startHTTPFLVServer() { // handleFLVRequest 处理 FLV 请求 func (r *RTMPServer) handleFLVRequest(w http.ResponseWriter, req *http.Request) { + // 添加 CORS 头,允许跨域访问 + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Range") + w.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Range") + + // 处理 OPTIONS 预检请求 + if req.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + path := req.URL.Path if len(path) < 7 { http.Error(w, "Invalid path", http.StatusBadRequest) @@ -1007,12 +1019,14 @@ func (r *RTMPServer) GenerateStreamURLs(roomID, userID string) (*RTMPStream, err token := generateStreamToken(streamID) 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), + 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), + // 使用 HTTPS 域名,通过 Nginx 反向代理到 HTTP-FLV 服务 + // 避免浏览器混合内容安全策略阻止 HTTP 请求 + FLVURL: fmt.Sprintf("https://g-ws.nailaoyun.cn/live/%s.flv", streamID), CreatedAt: time.Now(), IsActive: true, stopChan: make(chan struct{}), diff --git a/internal/mediaserver/ws_rtmp_proxy.go b/internal/mediaserver/ws_rtmp_proxy.go index 5995717..90cbed1 100644 --- a/internal/mediaserver/ws_rtmp_proxy.go +++ b/internal/mediaserver/ws_rtmp_proxy.go @@ -346,17 +346,23 @@ func (p *WebMToRTMPProxy) handleTextMessage(session *ProxySession, data []byte) log.Printf("🎬 [WSProxy] 收到编解码器信息 | Stream:%s Video:%s Audio:%s MIME:%s", session.StreamID, codecInfo.VideoCodec, codecInfo.AudioCodec, codecInfo.MimeType) - // 判断是否需要转码 - // H.264 可以直接封装到 FLV,VP8/VP9 需要转码 + // 判断编解码器类型并初始化对应的转码器 + // 所有格式都通过 FFmpeg 处理,但 H.264 使用 copy 模式(只重封装),VP8/VP9 需要转码 switch codecInfo.VideoCodec { case "h264", "avc1": - session.needTranscode = false - log.Printf("✅ [WSProxy] H.264 编码,无需转码,直接重封装为 FLV") + // H.264: 使用 FFmpeg copy 模式,只重封装为 FLV,不重新编码 + session.needTranscode = true + log.Printf("✅ [WSProxy] H.264 编码,使用 FFmpeg copy 模式重封装为 FLV") + + if err := p.initTranscoderForH264(session); err != nil { + log.Printf("⚠️ [WSProxy] 初始化 H.264 转码器失败: %v,尝试直接解析", err) + session.needTranscode = false + } case "vp8", "vp9": + // VP8/VP9: 需要 FFmpeg 转码为 H.264 session.needTranscode = true log.Printf("🔄 [WSProxy] %s 编码,需要 FFmpeg 转码为 H.264", codecInfo.VideoCodec) - // 初始化转码器 if err := p.initTranscoder(session); err != nil { log.Printf("⚠️ [WSProxy] 初始化转码器失败: %v,将使用非标准 FLV 格式", err) session.needTranscode = false @@ -369,7 +375,7 @@ func (p *WebMToRTMPProxy) handleTextMessage(session *ProxySession, data []byte) return nil } -// initTranscoder 初始化 FFmpeg 转码器 +// initTranscoder 初始化 FFmpeg 转码器(VP8/VP9 转码模式) func (p *WebMToRTMPProxy) initTranscoder(session *ProxySession) error { transcoder, err := NewFFmpegTranscoder(nil) if err != nil { @@ -385,7 +391,36 @@ func (p *WebMToRTMPProxy) initTranscoder(session *ProxySession) error { // 启动协程读取转码后的 FLV 数据并广播 go p.readTranscodedOutput(session) - log.Printf("✅ [WSProxy] FFmpeg 转码器已初始化 | Stream:%s", session.StreamID) + log.Printf("✅ [WSProxy] FFmpeg 转码器已初始化(转码模式)| Stream:%s", session.StreamID) + return nil +} + +// initTranscoderForH264 初始化 FFmpeg 转码器(H.264 copy 模式) +// H.264 视频只需要重封装,不需要重新编码,使用 copy 模式可以大大减少延迟 +func (p *WebMToRTMPProxy) initTranscoderForH264(session *ProxySession) error { + config := &TranscoderConfig{ + CopyMode: true, // 使用 copy 模式 + AudioCodec: "aac", + AudioBitrate: "64k", + AudioSampleRate: 44100, + AudioChannels: 2, + } + + transcoder, err := NewFFmpegTranscoder(config) + if err != nil { + return fmt.Errorf("创建 H.264 转码器失败: %w", err) + } + + if err := transcoder.Start(); err != nil { + return fmt.Errorf("启动 H.264 转码器失败: %w", err) + } + + session.transcoder = transcoder + + // 启动协程读取转码后的 FLV 数据并广播 + go p.readTranscodedOutput(session) + + log.Printf("✅ [WSProxy] FFmpeg 转码器已初始化(H.264 copy 模式)| Stream:%s", session.StreamID) return nil }