如何用Go语言搭建一个能跑的视频直播系统?从零开始的实战手记
- 新闻
- 2026-08-18 08:08:34
- 44
直播这事儿,Go语言到底行不行?
你可能在网上看过各种吹得天花乱坠的教程,什么“三行代码搞定直播”“用Go实现抖音核心架构”……打住,我用Go写直播服务也有两年多了,说实话,Go在并发处理和流媒体转发这块,确实有天然优势——goroutine轻量到你能同时挂几万个连接不心疼,但你要是想用它直接推流给浏览器,原生库是真的啥也没有,所以咱们这篇文章,不整虚的,就讲清楚怎么用Go把视频直播的关键骨架搭起来,从协议选择到代码实现,每一步都说人话。
第一步:先搞明白直播到底在传什么
你手机上看直播,背后其实是一连串的“快递”过程:主播的摄像头采集画面 → 编码成视频流(比如H.264) → 推送到服务器 → 服务器转码/分发 → 观众拉流解码播放。
这里有个关键点:浏览器不认RTMP,但认HLS和WebRTC,所以你要是想做个网页直播,服务器得做“协议翻译官”,Go擅长做的,就是这一层。
三种主流协议,选哪个?
| 协议 | 延迟 | 适用场景 | Go的成熟库 |
|---|---|---|---|
| RTMP | 2-5秒 | 主播推流(OBS) | github.com/zhangpeihao/gortmp |
| HLS | 5-15秒 | 网页播放(苹果系) | github.com/grafov/m3u8 |
| WebRTC | <1秒 | 互动直播 | github.com/pion/webrtc |
我的建议:推流端用RTMP(OBS默认支持),播放端用HLS(兼容性最好),如果要做低延迟互动,再单独上WebRTC,别一开始就追求全都要,会崩。
第二步:搭建RTMP接收端——你的Go服务器得先“听得懂”摄像头说的方言
我看过太多人一上来就写WebRTC,结果被SDP协商搞得头秃,最稳妥的入门路径是:先接收RTMP流,再转成HLS切片。
这里用gortmp库做个简单的服务器:
package main
import (
"fmt"
"github.com/zhangpeihao/gortmp"
)
func main() {
handler := &gortmp.EventHandler{
OnStreamBegin: func(stream *gortmp.Stream) {
fmt.Println("收到推流:", stream.URL)
// 这里可以启动转码/切片逻辑
go transcodeAndSeg(stream.URL)
},
}
server, _ := gortmp.NewServer(":1935", handler)
server.Serve()
}
注意:这个库只做RTMP握手和消息解析,你拿到的是*gortmp.Stream,里面是原始的FLV封装数据,接下来你要自己做两件事:
- 解析FLV标签,提取出H.264的NALU和AAC的ADTS头,这一步可以配合
github.com/deepch/vdk这个库,它把FLV解析封装得比较顺手。 - 缓存最近的GOP(关键帧),因为HLS切片必须从关键帧开始切,不然观众打开页面就是绿屏。
// 伪代码,示意思路
func transcodeAndSeg(rawURL string) {
cache := make([]byte, 0, 1024*1024) // 存最近的2秒数据
var isKeyFrame bool
for {
tag := readFLVTag() // 从stream里读
if tag.Type == STREAM_TYPE_VIDEO && isKeyframe(tag) {
// 遇到关键帧,就把之前cache的写成一个.ts文件
writeTSFile(cache)
cache = cache[:0]
}
cache = append(cache, tag.Payload...)
}
}
第三步:HLS切片——把你的“水流”切成“面包片”
HLS的原理很简单:把视频切成一个个2-10秒的.ts小文件,再写一个.m3u8索引文件告诉播放器“下一个吃哪片面包”。
Go的github.com/grafov/m3u8库能帮你生成索引,但切片本身得靠FFmpeg二进制,别嫌麻烦,这是目前最靠谱的方式,你可以在Go里用os/exec调用系统FFmpeg:
cmd := exec.Command("ffmpeg",
"-i", "pipe:0", // 从stdin读流
"-c:v", "copy", // 不重新编码,直接复制视频流
"-f", "hls",
"-hls_time", "4",
"-hls_list_size", "6",
"/www/live/stream.m3u8",
)
cmd.Stdin = rtmpStream // 把上一步的GOP缓存输进去
cmd.Run()
这里有个大坑:-hls_list_size 6意味着只保留最近6个切片,也就是24秒的时长,如果你的观众是回看,那就得配合点播服务器了,但做直播,这种“内存式”的临时目录反而省事。
第四步:给浏览器播放器喂饭——一个简单的HTML播放器
现在你的Go服务器已经能输出http://你的IP/live/stream.m3u8这个地址了,用原生HTML5的<video>标签就能播,但Safari和iOS原生支持HLS,Chrome和Firefox却不行。
这时候得用hls.js库(别怕,就这么一个外链):
<video id="video" controls autoplay muted></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
<script>
if (Hls.isSupported()) {
var hls = new Hls();
hls.loadSource('http://你的服务器/live/stream.m3u8');
hls.attachMedia(document.getElementById('video'));
} else {
// 原生支持就直接赋值
document.getElementById('video').src = 'http://你的服务器/live/stream.m3u8';
}
</script>
小坑提醒:muted属性一定要加,尤其是移动端,不然浏览器会拦截自动播放,别问我是怎么知道的……
第五步:并发保护——你的服务器不能崩在“一万人同时看”上
Go的并发很好,但每个HTTP请求拉HLS的.ts文件时,你要注意“惊群效应”,比如1000个人同时刷新播放器,你的服务器瞬间收到1000个对segment_004.ts的请求,如果都去读同一个文件,瞬间IO就爆了。
解决办法之一:给每个.ts文件设置共享内存缓存。github.com/patrickmn/go-cache就能干这事:
c := cache.New(5*time.Minute, 10*time.Minute)
c.Set("segment_004.ts", fileBytes, cache.DefaultExpiration)
// 每次请求前先查缓存,没有再去读磁盘
data, found := c.Get("segment_004.ts")
if found {
w.Write(data.([]byte))
return
}
这个库内部用了sync.RWMutex,并发读安全得很,至于写切片文件的时候,本来就只有FFmpeg一个进程在写,不存在竞争。
关于WebRTC——你真的需要它吗?
如果说HLS是“录制好的磁带寄给观众”,那WebRTC就是“现场视频电话”,延迟能压到500毫秒内,但代价是:
- 信令服务器(交换SDP和ICE候选)你得自己写,它就是个HTTP/WebSocket服务,倒是用Go很顺手。
- 需要STUN/TURN服务器穿NAT,公网环境还得用coturn。
- 带宽成本高,因为没用CDN,你服务器得多线路转发。
我的建议:别一上来就WebRTC,先跑通HLS,感受到“咦,我居然能做成直播”的快乐,再考虑用Pion库去做WebRTC,Pion这个库是真牛,纯Go实现,但配置复杂度也不是盖的,你至少得先弄懂“什么是UDP打洞”,不然连错误日志都看不懂。
最后聊点实用技巧
- 推流端OBS设置:输出模式选“高级”,键帧间隔设为2秒,音频用AAC,视频用H.264,这样切出来的CSS帧比较准,HLS切片不太容易花屏。
- 本地开发:建议用
ngrok把电脑上的服务暴露出去,不然你手机和电脑不在同一个WiFi下,怎么测都断流。 - 别迷信全链路自研:如果你不是要搞个大项目,直接把流推给
SRS(一个C++写的开源流媒体服务器),然后让Go做业务API层,比如鉴权、踢人、统计在线人数,自己用Go从零写推拉流,真的会劝退。
我做这个项目时,光调通RTMP的时序就熬了三个晚上,中间一度怀疑是不是库有bug,后来发现是自己没设置ChunkSize导致数据分片错乱,所以如果你也遇到了“视频卡在第一帧”,先检查OBS里是不是开了动态比特率,关掉它,固定到2500Kbps,能省很多事。
这行字写完,我刚把家里的树莓派改成直播中转站,现在一边隔着手厅看球赛直播,一边在这儿敲键盘——服务器跑着Go写的转发程序,内存才占78MB,惬意得很,你也能搞定的。
