当前位置:首页 > 篮球 > 正文

用 Golang 做视频直播?这事儿真没那么玄乎

  • 篮球
  • 2026-07-29 18:26:58
  • 49
摘要: 说实话,我第一次听说“用 Go 做视频直播”的时候,脑子里蹦出来的第一个念头是:这玩意儿不是该用 C++ 或者 Node.js...

说实话,我第一次听说“用 Go 做视频直播”的时候,脑子里蹦出来的第一个念头是:这玩意儿不是该用 C++ 或者 Node.js 吗? 后来真上手了才发现,Golang 在直播这块儿不仅能干,而且干得还挺利索,它的并发模型、内存管理、还有那个让人又爱又恨的 goroutine,放在视频流处理这种场景里,简直像量身定做的。

你可能会问:视频直播到底是个啥?简单讲,就是把摄像头拍到的画面,一帧一帧地打包成数据流,通过网络实时传给成百上千个观众,这里面最关键的两个字是“实时”——延迟要低,数据不能丢,画面还不能卡,用 Go 来做,恰恰能在这些点上找到平衡。

先搞清楚直播的“骨架”

在开始敲代码之前,得先明白一个视频直播系统到底长啥样,我把它拆成四个部分:

  1. 采集端:用摄像头或者桌面录制软件,把画面变成原始视频帧
  2. 编码器:把原始数据压缩成 H.264 或者 H.265 格式,不然网络扛不住
  3. 流媒体服务器:接收推流,再转发给所有观众
  4. 播放端:浏览器、手机 App 或者 VLC 这种播放器

在 Go 里做直播,通常不会从头写一个编码器——那是 FFmpeg 和 x264 的活儿,我们要干的是把封装传输分发这几个环节用 Go 串起来。

那些绕不开的协议

视频直播靠的是网络协议,Go 的标准库对网络支持得特别好,但直播协议需要自己再包一层,常用的有:

  • RTMP:老牌协议,推流端主力,Adobe 搞出来的,现在开源了
  • HLS:苹果家的协议,把视频切成一个个小片段,播放端像看幻灯片一样连续拉取,延迟略高但兼容性好
  • WebRTC:现在最火的选择,浏览器原生支持,延迟能做到亚秒级,Go 社区有 pion/webrtc 这个库特别能打

我个人的经验是:如果是做低延迟直播,优先选 WebRTC;如果需要兼容老旧设备,HLS 加个 CDN 更稳;RTMP 适合用来对接 OBS 这类推流工具。

第一步:怎么用 Go 接受视频流

假设你已经用 OBS 或者手机推流软件,把视频流以 RTMP 协议推到了某个地址,现在要用 Go 写一个收流的服务。

Go 里有个宝藏库叫 github.com/nareix/joy4,它实现了 RTMP 的接收端,代码写起来大概是这样:

package main
import (
    "github.com/nareix/joy4/av"
    "github.com/nareix/joy4/format/rtmp"
)
func main() {
    // 监听 RTMP 推流地址
    server := &rtmp.Server{}
    server.HandlePlay = func(conn *rtmp.Conn) {
        // 这里拿到视频流
        for {
            pkt, err := conn.ReadPacket()
            if err != nil {
                break
            }
            // pkt 就是一帧视频或者音频数据
            processFrame(pkt)
        }
    }
    server.ListenAndServe()
}

这里 processFrame 就是你要处理每一帧的地方,你可以把它转存,或者直接转发到另一个地方,注意一下:joy4 这个库最近更新不频繁,但做原型验证完全够用,生产环境的话,可以看看 github.com/yutopp/go-rtmp,那个更活跃。

真实场景里踩过的坑

我第一次用 joy4 的时候,遇到过一个特别尴尬的问题——推流几秒后画面就卡死了,查了半天发现是 ReadPacket 在读取时没有及时清理缓冲区,导致内存暴涨,解决办法其实很土:每个连接分配一个 goroutine 独立处理,并且在读取时设置超时

conn.SetReadDeadline(time.Now().Add(5 * time.Second))
pkt, err := conn.ReadPacket()

别小看这句,不加的话,某个推流端掉线了,你的服务可能还在傻等数据。

第二步:视频转码与封装

拿到原始的视频流之后,通常需要做两件事:

  1. 转码:把 H.264 转成其他格式,或者调整码率(给不同网速的用户看不同分辨率)
  2. 封装:把视频流包成 HLS 分段,或者放在 WebRTC 轨道里

Go 本身没有视频编解码器,得借助 FFmpeg,最优雅的方式是调用 FFmpeg 的命令行,但用 Go 来管理进程生命周期。

我写过一个小工具,专门用来把 RTMP 流转成 HLS:

func transcodeToHLS(inputURL string, outputDir string) {
    cmd := exec.Command("ffmpeg",
        "-i", inputURL,
        "-c:v", "libx264",
        "-hls_time", "2",
        "-hls_list_size", "10",
        fmt.Sprintf("%s/stream.m3u8", outputDir),
    )
    cmd.Stdout = nil
    cmd.Stderr = nil
    cmd.Run()
}

等等——这个函数有个大问题。cmd.Run() 是阻塞的,FFmpeg 没跑完它不会返回,直播场景下,推流一直不停,FFmpeg 也不会停,所以这个函数永远跑不完。

正确的做法是用 cmd.Start(),然后通过监听日志或者文件变化来判断是否初始化成功。

cmd := exec.Command("ffmpeg", ...)
cmd.Start()
// 轮询检查 m3u8 文件是否生成
for i := 0; i < 10; i++ {
    if _, err := os.Stat(outputDir + "/stream.m3u8"); err == nil {
        break
    }
    time.Sleep(500 * time.Millisecond)
}

有点粗暴,但管用,生产环境可以用 inotify 来监听文件事件。

WebRTC 的转码思路

如果用 WebRTC,情况又不一样,WebRTC 通常直接传编码后的 H.264 流,不需要再封装成文件,但不同浏览器支持的 H.264 编码参数不一样,有时候需要做“联播”——同时发送多个分辨率的流。

Go 里可以用 pion/webrtc 创建多个 Track,每个 Track 对应一个分辨率的视频流,接收端根据网络状况自动选择。

// 创建 WebRTC 连接
peerConnection, _ := webrtc.NewPeerConnection(webrtc.Configuration{})
// 添加视频轨道
videoTrack, _ := webrtc.NewTrackLocalStaticRTP(
    webrtc.RTPCodecCapability{MimeType: "video/H264"},
    "video", "pion",
)
rtpSender, _ := peerConnection.AddTrack(videoTrack)
// 用 goroutine 不断发送 RTP 包
go func() {
    ticker := time.NewTicker(time.Millisecond * 33) // 30fps
    for range ticker.C {
        // 从某处拿编码后的帧,写入 track
        videoTrack.WriteRTP(packet)
    }
}()

第三步:分发的艺术

视频流收进来了,也转码好了,接下来怎么发给观众?

基于 HLS 的分发

HLS 模式下,观众是通过 HTTP 拉取 .ts 文件,最简单的方式,就是用 Go 内置的 net/http 把生成的 m3u8 和 ts 文件暴露出去:

http.Handle("/hls/", http.StripPrefix("/hls/", http.FileServer(http.Dir("./hls_output"))))
http.ListenAndServe(":8080", nil)

但有个细节——浏览器缓存,HLS 的播放器会持续拉新的 .ts 文件,HTTP 响应头里有 Cache-Control: max-age 之类的,播放器可能拿到的是几秒前的旧片段,导致延迟越来越大。

解决办法是把每个请求的响应头里加上:

w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")

基于 WebRTC 的实时分发

WebRTC 不需要文件服务器,它用的是 P2P 或者通过 SFU(Selective Forwarding Unit)转发,SFU 在 Go 里的实现可以参考 pion/sfu 这个库。

它的原理特别简单:每个观众创建一个 PeerConnection,SFU 把从推流端收到的 RTP 包复制一份,转发给所有观众。

func handleViewer(offer webrtc.SessionDescription) webrtc.SessionDescription {
    // 创建新的 PeerConnection
    pc, _ := webrtc.NewPeerConnection(config)
    // 把推流端的视频轨道添加进去
    for _, track := range publisherTracks {
        pc.AddTrack(track)
    }
    // 处理信令
    pc.SetRemoteDescription(offer)
    answer, _ := pc.CreateAnswer(nil)
    pc.SetLocalDescription(answer)
    return *answer
}

实际部署时,你会发现一个问题:每个观众都开一个 goroutine,如果同时有 1000 个观众,那就要处理 1000 个 goroutine 之间的数据拷贝,Go 的调度器能扛住,但内存占用会涨得很快。

一个优化技巧是:使用 共享缓冲区,所有观众共用一个环形缓冲区,推流端往里面写,观众从里面读,这样写一次数据,所有观众都能拿到,避免了 N 次拷贝。

那些容易忽略的“小事”

讲完了流程,再聊几个实践中会冒出来的小麻烦。

时间戳同步

视频流里的每一帧都带有一个 PTS(显示时间戳),不同编码器出来的时间戳单位可能不一样——有的用毫秒,有的用 90kHz 时钟,Go 里收到数据后,得先归一化。

const clockRate = 90000 // H.264 的标准时钟频率
ptsInMs := float64(pts) / float64(clockRate) * 1000

丢包处理

RTMP 用 TCP,理论上不丢包,但 WebRTC 跑在 UDP 上,丢包是家常便饭。pion/webrtc 内部实现了 NACK(重传请求),但如果你自己处理 RTP 流,记得要预留 NACK 的处理逻辑。

// 收到 NACK 时,从历史缓冲区中重发对应的包
func handleNACK(sequenceNumbers []uint16) {
    for _, seq := range sequenceNumbers {
        if pkt, ok := historyBuffer[seq]; ok {
            sendPacket(pkt)
        }
    }
}

推流鉴权

总不能让任何人都能往你的服务器推流吧,最简单的方式是在 RTMP URL 里加 Token:

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.URL.Query().Get("token")
        if !validateToken(token) {
            http.Error(w, "Forbidden", 403)
            return
        }
        next.ServeHTTP(w, r)
    })
}

对于 RTMP,可以在 rtmp.ServerHandleConn 里检查 URL 参数,如果推流端用的是 OBS,可以在推流地址里加上 ?token=xxx,OBS 会原样传过去。

性能和瓶颈在哪

用 Go 做视频直播,性能瓶颈通常不在 CPU,而在内存带宽网络 IO

一个 1080p 的 H.264 流,平均码率大概 5Mbps,如果有 100 个观众同时观看,服务器就要输出 500Mbps 的流量,千兆网卡的极限是 1000Mbps,200 个观众差不多到顶了,解决办法是:

  • 用多网卡绑定(bonding)
  • 用 CDN 做边缘节点
  • 或者干脆只用 Go 处理推流,分发交给 Nginx-RTMP 或者云厂商的直播服务

我个人的偏好是:用 Go 做控制面,用 Nginx 做数据面,Go 负责信令、鉴权、统计、转码任务调度,Nginx 负责流量转发,两者通过 Unix Socket 或者共享内存通信,性能很稳。

到底值不值得用 Go?

如果你只是想搭一个“能跑”的直播服务,FFmpeg + Nginx 就够用了,学 Go 的成本似乎不划算,但如果你需要:

  • 动态调整码率:根据观众的网络质量实时切换分辨率
  • 自定义鉴权逻辑:比如对接自家的 OAuth 系统
  • 精细化统计:每路流的延迟、丢包率、观看时长
  • 和业务系统深度集成:直播数据直接落库,关联用户行为

那 Go 的开发效率运维便利性就体现出来了,写一次编译成二进制,扔到服务器上就能跑,不像 Python 需要操心依赖版本,也不像 C++ 那样调试编译就得等半天。

还有一点,Go 的 pprof 工具在排查性能问题的时候特别好用。go tool pprof http://localhost:6060/debug/pprof/profile 就能看到哪个函数吃 CPU、哪个 goroutine 在阻塞,我曾靠这个找出一个内存泄漏,是一个 map 忘记删除已断开的连接——这种问题在 C++ 里你可能得用 Valgrind 跑半天。

心里没底的时候怎么办

网上有很多现成的开源项目可以参考,GitHub 上搜 golang live streaming,能翻到不少。gwuhaoding/livego 用 Go 实现了 RTMP、HLS 和 WebRTC 的完整的服务,我刚开始学的时候就是照着它的源码一行一行读的,遇到不懂的协议细节,就去翻 RFC 文档。

RFC 文档读起来确实枯燥,但关键部分也就几页,RTMP 的核心握手过程,读懂了 RFC 就可以从网络抓包中验证自己的实现,有一个叫 tsduck 的工具,可以帮你分析 TS 流的结构,调试 HLS 时巨好用。

还有一点想说的是,别怕初始化失败,我第一次搭 WebRTC 信令服务器时,ICE 候选地址死活配对不上,查了三天发现是 STUN 服务器的端口写错了,这种问题在文档里有写,但真到自己动手的时候,全忘光了,多试几次,多摔几次,慢慢就记住了。

视频直播这件事,说复杂是真复杂——编解码、网络协议、拥塞控制、同步机制,每一项单拎出来都是深坑,但说简单也简单,核心就是把视频数据从 A 点搬到 B 点,Go 恰好擅长做搬运工这件事,用好 goroutine 和 channel,把控制流和数据流分开,再加一层熔断和限流,基本就稳了。

好了,到这该去写代码了,别纠结太多,先让画面跑起来,哪怕画面是绿色的、声音是沙沙的噪声,只要它动了,你就已经比一个小时前的自己进步了一大截。

用 Golang 做视频直播?这事儿真没那么玄乎