瓜仔直播vs视频下载,我用Go语言写了个下载器,结果被直播流虐惨了
- 赛程
- 2026-08-16 12:28:11
- 46
先说点大实话
这事儿得从上周说起,我媳妇儿迷上了一个叫“瓜仔直播”的App,天天蹲在里面看各种户外主播钓鱼、赶海、修驴蹄子,她看得开心,我却愁得慌——这破App的直播流地址一会儿一变,缓存文件加密得跟粽子似的,想给家里老人存个回放视频都费劲。
作为一名半吊子Go程序员,我寻思着:“不就是个直播流吗?用Go写个下载器不就完了?”结果这一动手,差点把自己搭进去,今天就把我踩过的坑、翻过的车,以及最终怎么用Go把瓜仔直播的视频下载下来的完整过程,全给你抖搂出来。
瓜仔直播的流媒体协议,比想象中阴险
先别急着写代码,你得先搞清楚瓜仔直播到底用的什么协议,我抓包看了三天,总算摸清了套路。
HLS(HTTP Live Streaming)是主流,但瓜仔搞了个骚操作:
- 它的m3u8索引文件不是固定的,每5分钟换一次地址
- 切片(.ts文件)的下载链接带有时间戳签名,过期就403
- 部分高码率直播间用的是FLV over WebSocket,而不是标准的HTTP-FLV
- 关键帧间隔被拉到了8秒,快进时卡得你想砸电脑
市面上那些现成的下载工具,比如youtube-dl、you-get,对瓜仔这种“带签名的短时效HLS”基本没辙。这就是我为什么非得自己写Go程序的原因——灵活、能定制、编译完扔哪儿都能跑。
用Go写下载器的第一个坑:m3u8解析
我一开始想着:“不就解析个m3u8文件嘛,正则匹配一下完事儿。”结果瓜仔的m3u8里夹杂着各种自定义标签,比如#EXT-X-KEY:METHOD=AES-128,URI="key.php?token=xxx",而且token是根据你当前IP和时间动态生成的。
正确的做法是用官方库github.com/grafov/m3u8,而不是自己造轮子,给你看下我最初的代码,那叫一个惨不忍睹:
// 错误示范:千万别这么干
func parseM3U8(url string) []string {
resp, _ := http.Get(url)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var segments []string
for _, line := range strings.Split(string(body), "\n") {
if strings.Contains(line, ".ts") {
segments = append(segments, line)
}
}
return segments
}
这段代码在瓜仔直播面前就是个玩具。一旦加密key换了,你下回来的全是加密垃圾数据,播放器直接给你看马赛克。
第二个坑:直播流的“无限长”问题
录播和直播最大的区别是什么?直播没有明确的结束时间,瓜仔的主播能一口气播8个小时,如果你的下载器是“先下完再保存”,那你永远等不到结束。
我的解决方案是搞了个分段写入 + 监听退出信号的机制:
ctx, cancel := context.WithCancel(context.Background())
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
cancel()
}()
for {
select {
case <-ctx.Done():
// 手动保存当前进度,把已下载的片段合并
mergeSegments()
return
default:
downloadNextSegment()
}
}
这样我按Ctrl+C的时候,程序会先把已经拿到的片段合并成一个完整的视频文件,不至于白忙活。这个思路对瓜仔直播特别好使,因为你永远不知道主播什么时候下播。
真正好使的Go下载器长什么样
折腾了一周,最后我整理出了一套能打的方案。不废话,直接上干货。
第一步:手动拦截瓜仔直播的API响应
在手机上装个抓包工具(比如Stream抓包),找到瓜仔直播的“获取直播地址”接口,它返回的JSON里通常会包含:
| 字段名 | 说明 |
|---|---|
play_url |
真正的流地址(可能是m3u8也可能是flv) |
sign |
签名参数,有效期约10分钟 |
cdn_type |
是阿里云CDN还是腾讯云CDN,这影响处理逻辑 |
用Go的encoding/json解析时,注意字段名大小写,瓜仔的接口用的是蛇形命名(比如play_url),结构体标签要写对。
type LiveInfo struct {
PlayURL string `json:"play_url"`
Sign string `json:"sign"`
CDNType string `json:"cdn_type"`
}
第二步:针对HLS的动态刷新
因为签名只有10分钟,你需要开个goroutine定时刷新,每4分钟重新请求一次play_url,拿到新的m3u8地址,更新下载列表。
核心逻辑:
- 启动时请求一次m3u8,记录所有切片地址
- 对每个切片发起下载请求(注意带上
Referer和User-Agent头,不然CDN会拒绝) - 如果下载409/403,立即重新拉取m3u8,替换后续地址
- 切片下完只存在内存里,攒满100MB就写入磁盘,避免内存爆炸
第三步:FLV流就简单粗暴了
如果cdn_type是flv,直接用net/http发起GET请求,然后从头到尾写文件就行,FLV流是连续的,不存在拼接问题,唯一要注意的是:
resp, _ := http.Get(streamURL)
defer resp.Body.Close()
file, _ := os.Create("live.flv")
buf := make([]byte, 64*1024)
for {
n, err := resp.Body.Read(buf)
if n > 0 {
file.Write(buf[:n])
}
if err == io.EOF {
break
}
}
这段代码看着简单,但配合上前面说的context取消机制,用起来真香。我不需要知道直播什么时候结束,只要主播停播,连接自然断开,文件就收工了。
那些官方文档不会告诉你的细节
瓜仔的CDN有“防盗链”,必须带Referer
我一开始下载下来的ts文件全是522字节的无效文件,就是因为没带Referer: https://www.guazai.com/这个头。这个问题在Go的http.Client里特别容易忽略,因为你觉得一个“代码层面”的下载应该不用管这些,但实际上,CDN是根据请求头来判断请求来源的。
req, _ := http.NewRequest("GET", tsURL, nil)
req.Header.Set("Referer", "https://www.guazai.com/")
req.Header.Set("User-Agent", "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
下载速度被限制了怎么破
瓜仔对单IP的下载速度限到了200KB/s,一个1GB的直播录像你得下1个多小时。暴力解法是开多个连接同时下不同片段,Go的并发特性这时候就派上用场了:
sem := make(chan struct{}, 5) // 最多5个并发
var wg sync.WaitGroup
for _, seg := range segments {
wg.Add(1)
go func(seg string) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
downloadSegment(seg)
}(seg)
}
wg.Wait()
我实测并发5路,速度能跑到800KB/s左右,基本够用了。
合并ts文件不能用os.WriteFile挨个拷
这性能太垃圾,正确的做法是创建一个大文件,然后通过os.File的Seek方法按偏移量写入,或者直接用io.Copy()对每个ts文件分段写入,我是这样干的:
output, _ := os.Create("final.mp4")
for _, segPath := range segmentFiles {
f, err := os.Open(segPath)
if err != nil {
continue
}
io.Copy(output, f)
f.Close()
}
400MB的录像文件合并只需要2秒,而且不会爆内存。
最后的挣扎:代码是写出来了,但...
说实话,这玩意儿虽然能用,但离“傻瓜化”还差得远,我媳妇儿现在让我给她的手机上也装一个,我说“等我把界面设计好”,其实心里在打鼓——那些乱七八糟的参数设置,普通人怎么可能搞明白。
后来我又想到一个“歪招”:直接用Go在服务器上定时抓瓜仔直播,生成回放链接,用Hugo搭个静态页面给家里人看,这样全家人都能用浏览器看,不用装任何App,不过这就涉及到更多的Go后端逻辑了,比如用github.com/gorilla/websocket探测直播状态、用github.com/u2takey/ffmpeg-go做转码。
但说实话,每次瓜仔直播一改协议,我的代码就要缝缝补补。 昨天晚上它又更新了,我现在又开始蹲在电脑前抓包了,如果你是圈内人,也遇到过类似的问题,欢迎在评论区唠唠你的解决方案——咱们一起跟这破直播流死磕到底。
