当前位置:首页 > 赛程 > 正文

从零开始,用Go语言搭建一个能跑的视频直播系统

  • 赛程
  • 2026-07-14 14:00:35
  • 23
摘要: 说实话,我第一次听说要用Go语言做视频直播的时候,心里是有点懵的,因为大多数人一提到直播,脑子里蹦出来的都是FFmpeg、Ngi...

说实话,我第一次听说要用Go语言做视频直播的时候,心里是有点懵的,因为大多数人一提到直播,脑子里蹦出来的都是FFmpeg、Nginx-RTMP、OBS这些老面孔,Go?那不是写后端API、做微服务的吗?后来我查了一圈资料,发现还真有不少人拿Go写流媒体服务,而且跑得还挺欢,比如livego、monibuca这些开源项目,底层就是用Go写的,这事儿一下子就变得有意思了。

视频直播到底在“直播”什么?

要搞明白怎么做,得先搞清楚我们要处理的是什么,视频直播本质上就是一个实时数据传输的过程:你这边拿着手机或者摄像头,把画面和声音采集下来,压缩成视频流,然后通过网络推到服务器上,服务器再分发给所有观众,整个过程延迟越低越好,画面越清晰越好。

从技术层面拆开来看,核心就那么几步:

  1. 采集:从摄像头、麦克风拿原始数据
  2. 编码:用H.264、H.265之类的方式压缩
  3. 封装:把压缩后的数据包成FLV、TS这种格式
  4. 传输:用RTMP、HLS、WebRTC之类的协议发出去
  5. 分发:服务器收到流,转给所有观众
  6. 解码播放:观众那边还原成画面和声音

Go语言能干的,主要集中在传输分发这两个环节,采集和编码那部分,还是得靠C库或者FFmpeg出马,不过Go的优势在于高并发,一个Goroutine处理一路流,几千路并发轻轻松松。

推流?拉流?先搞清谁是主角

直播系统里有两个关键角色:推流端(主播)和拉流端(观众),推流端把视频流“推”到服务器上,拉流端再从服务器上“拉”下来看,中间那个服务器,就是流媒体服务器。

用Go写流媒体服务器,最直接的路子就是实现一个RTMP协议,RTMP是Adobe搞出来的老协议,现在还是直播推流的主流选择,它的本质是TCP长连接,把视频音频数据分包发过来,Go标准库的net包处理TCP连接是小菜一碟。

我试着写过一个极简的RTMP服务器,大概长这样:

package main
import (
    "net"
    "log"
)
func main() {
    listener, _ := net.Listen("tcp", ":1935")
    for {
        conn, _ := listener.Accept()
        go handleRTMPConnection(conn) // 每个连接一个Goroutine
    }
}
func handleRTMPConnection(conn net.Conn) {
    // 这里需要解析RTMP握手、读取chunk、处理数据
    // 省略两百行代码……
}

你看,核心思路就是这么简单:监听端口,接受连接,扔给Goroutine处理,但实际写起来嘛,RTMP协议的手握手、chunk分块、AMF编码这些东西,还是有点折腾的,如果不想从零造轮子,可以直接用现成的库,比如github.com/gwuhaolin/livego

选择协议:RTMP、HLS还是WebRTC?

推流到服务器,RTMP是当之无愧的主力,OBS、微信视频号、抖音这些平台都支持RTMP推流,但RTMP有个毛病——它用的是TCP长连接,延迟低,但容易被防火墙拦,在浏览器里也玩不转。

拉流这块,方案就多了:

  • HLS:苹果搞的,把视频切成一小段一小段的.ts文件,客户端不停地拉最新的片段,优点是兼容所有浏览器和手机,缺点是延迟高,至少有个5到10秒。
  • FLV over HTTP:用HTTP传输FLV格式的流,延迟比HLS低一点,大概3到5秒。
  • WebRTC:浏览器原生支持的实时通信协议,延迟能做到1秒以内,但实现复杂度高,对服务器要求也高。

选哪个取决于你的场景,做低延迟互动(比如游戏直播、连麦),上WebRTC;做无延迟容忍的常规直播(比如课程录播、活动转播),HLS完全够用。

下面是我做的一个对比表,方便你选型:

协议 延迟 浏览器支持 服务器开发难度 典型应用
RTMP 1-3秒 需要插件 中等 OBS推流、内部系统
HLS 5-15秒 原生支持 视频网站、点播
FLV over HTTP 3-5秒 需要flv.js 传统直播平台
WebRTC <1秒 原生支持 视频会议、连麦

用Go写一个简单的转流服务

假设你已经有推流端用RTMP把视频推到服务器了,现在需要把这个流转成HLS分发给浏览器,这个过程叫转流或者转封装,Go怎么干?

核心工作有两步:

  1. 接收RTMP流:解析RTMP包,拿到里面封装的视频和音频数据
  2. 切成HLS片段:把数据写进一个个MPEG-TS文件,再生成一个.m3u8索引文件

这里有个麻烦——解析视频编码格式,H.264的NALU(网络抽象层单元)和AAC的ADTS头,这些东西Go的标准库里没有,得自己写解析器,或者用CGO调用FFmpeg,懒人办法是用现成的库,比如github.com/nareix/joy4,它支持RTMP和HLS的解析。

我写过一个粗糙的版本,伪代码大概这样:

// 接收RTMP流
rtmpStream := joy4.NewRTMPStream(conn)
for packet := range rtmpStream.ReadPacket() {
    // 判断是视频还是音频
    switch packet.Type {
    case joy4.H264:
        // 提取NALU,写入TS文件
        writeToTSFile(packet.Data)
    case joy4.AAC:
        // 处理音频帧
        writeAudioToTSFile(packet.Data)
    }
    // 每2秒生成一个TS片段
    if time.Since(lastSegmentTime) > 2*time.Second {
        generateM3U8()
        lastSegmentTime = time.Now()
    }
}

实际跑起来会发现,切HLS的难点在于时序对齐——视频和音频的PTS(显示时间戳)必须一致,否则观众看到的画面和声音是错位的,这个问题我搞了两天才搞定,最后发现是TS头里的PCR写错了。

分发:把流送到观众面前

流已经切成HLS片段了,怎么让观众看到?最简单粗暴的办法:用HTTP静态文件服务器,把.ts和.m3u8文件丢给浏览器,Go的net/http包几行代码就能搞定:

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

但实际生产环境里,文件数量会爆炸,一场直播几个小时,每2秒一个ts文件,几千个文件堆在那儿,磁盘迟早撑不住,更优雅的做法是不落盘,直接在内存里维护一个循环队列,只保留最近几十秒的数据,旧的直接丢掉,用Go的sync.Pool或者环形缓冲区可以实现。

对于高并发场景,分发还有一个痛点——推流带宽,如果一千个观众同时拉流,服务器就要发一千份同样的数据,单台机器扛不住,这时候需要做回源+边缘节点,或者用转推协议(比如把HLS推到CDN上),不过那是架构层面的故事了,单机玩的话,扛个几百人问题不大。

实战经验:我踩过的几个坑

  • 内存泄漏:Goroutine里读取RTMP流的时候,如果某个Goroutine被阻塞了(比如网络断开),它会一直占用内存,一定要加超时控制和context取消机制。
  • GOP结构:HLS切片的起始点必须是I帧,如果不注意GOP(画面组)边界,第一个切片可能画不出来,解决办法是等收到I帧再开始切。
  • 时间戳回绕:RTMP的时间戳是用24位整数存的,超过00:30:00左右会溢出,Go里得自己转成64位,不然时间戳一跳回0,流就断了。
  • 音频不连续:AAC音频帧的采样率必须恒定,否则切换片段时会出现爆音或停顿。

要不要用FFmpeg?

很多人会问:“都用Go了,干嘛不直接上FFmpeg?”答案是:依赖管理太痛苦,CGO交叉编译、FFmpeg版本兼容、动态链接库路径,这些东西在容器化部署时容易把人搞疯,如果你的团队熟悉CGO,用Go调用libavcodec确实能写出高性能的转码服务(参考Livepeer的方案),但如果只是做简单的转流,纯Go方案够用,还省事儿。

如果说有什么时刻让我觉得“Go真香”,那就是测试的时候,用I/O多路复用的语言(比如C++)写流媒体服务,调试网络问题得像破案一样翻日志,而Go的pprof一开,哪个Goroutine在干嘛,内存分配在哪,一目了然。

最后说点实在的:别想着一次性做个对标B站的直播系统出来,先从单机、单流、单协议跑通,把RTMP收进来,转成HLS扔出去,用VLC或者浏览器放出来,看到画面动的那一刻,你就算入门了,后面再加并发、加协议、加优化,都是水到渠成的事。

从零开始,用Go语言搭建一个能跑的视频直播系统