当前位置:首页 > 新闻 > 正文

如何用Go语言搭建一个能跑的视频直播系统?从零开始的实战手记

  • 新闻
  • 2026-08-18 08:08:34
  • 44
摘要: 直播这事儿,Go语言到底行不行?你可能在网上看过各种吹得天花乱坠的教程,什么“三行代码搞定直播”“用Go实现抖音核心架构”……打...

直播这事儿,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封装数据,接下来你要自己做两件事:

  1. 解析FLV标签,提取出H.264的NALU和AAC的ADTS头,这一步可以配合github.com/deepch/vdk这个库,它把FLV解析封装得比较顺手。
  2. 缓存最近的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毫秒内,但代价是:

  1. 信令服务器(交换SDP和ICE候选)你得自己写,它就是个HTTP/WebSocket服务,倒是用Go很顺手。
  2. 需要STUN/TURN服务器穿NAT,公网环境还得用coturn。
  3. 带宽成本高,因为没用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,惬意得很,你也能搞定的。

如何用Go语言搭建一个能跑的视频直播系统?从零开始的实战手记