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

瓜仔直播vs视频下载,我用Go语言写了个下载器,结果被直播流虐惨了

  • 赛程
  • 2026-08-16 12:28:11
  • 34
摘要: 先说点大实话这事儿得从上周说起,我媳妇儿迷上了一个叫“瓜仔直播”的App,天天蹲在里面看各种户外主播钓鱼、赶海、修驴蹄子,她看得...

先说点大实话

这事儿得从上周说起,我媳妇儿迷上了一个叫“瓜仔直播”的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地址,更新下载列表。

核心逻辑:

  1. 启动时请求一次m3u8,记录所有切片地址
  2. 对每个切片发起下载请求(注意带上RefererUser-Agent头,不然CDN会拒绝)
  3. 如果下载409/403,立即重新拉取m3u8,替换后续地址
  4. 切片下完只存在内存里,攒满100MB就写入磁盘,避免内存爆炸

第三步:FLV流就简单粗暴了

如果cdn_typeflv,直接用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.FileSeek方法按偏移量写入,或者直接用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做转码。

但说实话,每次瓜仔直播一改协议,我的代码就要缝缝补补。 昨天晚上它又更新了,我现在又开始蹲在电脑前抓包了,如果你是圈内人,也遇到过类似的问题,欢迎在评论区唠唠你的解决方案——咱们一起跟这破直播流死磕到底。

瓜仔直播vs视频下载,我用Go语言写了个下载器,结果被直播流虐惨了