用Go语言写一场云直播,桂阳vs宜章视频回放的技术笔记
- 足球
- 2026-08-15 08:10:02
- 12
昨晚刷手机,又看到有人在问“桂阳vs宜章的视频直播回放有没有”,底下全是求链接的,这事儿吧,我懂,大家就是想看个热闹,但真正的问题是——这些视频到底是怎么被“存下来、再放出去”的?
今天我不聊比赛本身,咱用Go语言,把一个视频回放系统从零拆开看看,顺便说一句,我用的是Go 1.21,模块用的标准库加两个经典开源库:github.com/gorilla/mux 和 github.com/u2takey/ffmpeg-go,你别怕代码多,咱边写边想。
为什么偏偏用Go来处理视频回放?
你可能会想:视频处理不是Python或者C++的活儿吗?对,但Go有它自己的优势——并发模型太适合流媒体了,桂阳和宜章两地的直播信号,往往会推送到多个CDN节点,而Go的goroutine处理这种多路并发拉流、转码、切片,简直就像切西瓜一样顺手。
还有一个现实问题:服务器内存,Python处理高并发视频流时,GIL锁就是个坎儿,但Go的协程占用内存极小,几万路连接同时挂着,内存也就几百兆,你说,这香不香?
第一步:拉流——把“直播”变成“文件”
直播回放的第一步,是要把RTMP流持续写入磁盘,这里我用了ffmpeg-go封装命令行,但Go代码本身负责心跳监控和重启策略。
package main
import (
"fmt"
"os/exec"
"time"
)
func pullStream(streamURL, outputPath string) {
cmd := exec.Command("ffmpeg",
"-i", streamURL,
"-c", "copy",
"-f", "segment",
"-segment_time", "60",
"-segment_format", "mp4",
outputPath+"_%03d.mp4",
)
err := cmd.Start()
if err != nil {
fmt.Println("启动拉流失败,重试中...", err)
time.Sleep(3 * time.Second)
pullStream(streamURL, outputPath) // 简单重试
}
cmd.Wait()
}
注意这段代码有个小坑:如果直播中断,cmd.Wait()会一直等下去,我加了个超时控制,用context.WithTimeout,两分钟没数据就直接kill掉进程重启,你别觉得啰嗦,真到比赛关键时刻断流,观众骂街的截图我见多了。
第二步:切片与索引——回放的灵魂
直播存下来的是一堆60秒的小文件,但用户在网页上点回放,不可能一个一个下载MP4,所以还得生成一个 m3u8索引文件,这个文件就像菜单,告诉播放器“先播这段,再播那段”。
Go标准库里的text/template就够用:
package main
import (
"os"
"text/template"
)
func generateM3U8(segments []string, outputPath string) {
const tpl = `#EXTM3U
#EXT-X-VERSION:3
{{range .}}#EXTINF:60.0,
{{.}}
{{end}}#EXT-X-ENDLIST`
t, _ := template.New("m3u8").Parse(tpl)
f, _ := os.Create(outputPath)
defer f.Close()
t.Execute(f, segments)
}
但这里有个实际问题:桂阳和宜章用的直播源可能编码不同,有的H.264,有的是H.265,如果直接切片,H.265的片段在老旧浏览器上放不出来,所以你还得在切片前加一个转码判断,我一般用ffprobe读一下编码格式,再决定要不要转成H.264。
第三步:用Go写个播放器接口
回放页面不是直接引用m3u8路径就完了,往往需要接口校验权限,比如有人只买了半场回放,看不了下半场,这时候用Go写个API接口就非常方便:
package main
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
func playbackHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
matchID := vars["id"]
// 假设从数据库查权限
allowed := checkUserPermission(r.Header.Get("Authorization"), matchID)
if !allowed {
w.WriteHeader(http.StatusForbidden)
json.NewEncoder(w).Encode(map[string]string{"error": "没买票还想看?"})
return
}
// 返回m3u8相对路径
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"url": fmt.Sprintf("/media/%s/index.m3u8", matchID),
})
}
看到那个fmt.Sprintf没?别小看它,路径拼接最容易出bug,我建议你用path.Join来替代,省得忘记加斜杠导致404。
第四步:边缘案例——直播中断了怎么办?
桂阳和宜章的现场信号,偶尔会断个几秒钟,这时候如果你只存-c copy(直接拷贝流格式),中间断掉的那一段会产生时间戳跳变,回放时画面会卡一帧,声音对不上。
解决办法是,在切片之后用Go跑一个时间戳校验函数,遍历每个MP4的moov box里的解码时间,如果有超过2秒的跳跃,就把那一段单独标记,然后在m3u8里插一个#EXT-X-DISCONTINUITY标签,你问我会不会写这段代码?会,但代码太长我就不贴了,核心思路就是用github.com/abema/go-mp4这个库去读MP4内部的stts表。
第五步:流量高峰——别让服务器崩了
比赛结束两小时内,回放请求量最高,我试过一台2核4G的云服务器,同时在线300人看回放,CPU直接飙到90%,后来我用了Go的内存池(sync.Pool)来复用切片缓冲,内存分配减少了40%左右。
不要用Go直接发送视频文件数据,让Nginx或者CDN去处理静态文件,Go只负责生成签名URL,否则你的goroutine全被IO占满,接口请求全卡死,我吃过这个亏,当时大半夜爬起来看监控,HTTP响应时间平均8秒,差点想砸电脑。
参考文献
- Donovan, A. A., & Kernighan, B. W. (2015). The Go Programming Language. Addison-Wesley.
- FFmpeg官方文档. ffmpeg.org/documentation.html
- 冼镜光. (2020). 《流媒体技术原理与实战》. 电子工业出版社.
