我把视频号直播回放「搬」到了本地:一个小白从抓包到合成的全过程,和 15 个差点把我送走的坑写在前面:这不是一篇「官方教程」,而是一个编程新手自己动手做完一个工具之后,把自己理解到的原理、流程,还有开发时踩过的坑,尽可能用大白话讲清楚。我一开始连 m3u8 是啥都不知道,所以文中尽量不用黑话;实在要用,我会先
详细介绍
我把视频号直播回放「搬」到了本地:一个小白从抓包到合成的全过程,和 15 个差点把我送走的坑
写在前面:这不是一篇「官方教程」,而是一个编程新手自己动手做完一个工具之后,
把自己理解到的原理、流程,还有开发时踩过的坑,尽可能用大白话讲清楚。
我一开始连m3u8是啥都不知道,所以文中尽量不用黑话;实在要用,我会先解释。
另外必须写在最前面:这个工具只能用来备份你自己有权访问的内容,请遵守平台条款和著作权法。
技术是用来学习和备份的,不是用来侵权的。
目录
一、起因:一个「凭啥不能下」的执念
事情的起因很朴素:我在微信里看了一场视频号直播,回放挺好的,想存到本地留着以后看,
结果发现小程序里没有下载按钮。
我这个人的毛病就是,越没有按钮越想搞明白:明明都播到我手机/电脑上了,
凭什么存不下来?
于是一路折腾,从「拿个软件录屏」开始,到最后真正搞懂它的原理、写出一个能一键下载的工具
(wxreplay,打包后叫 WxReplayDownloader.exe)。这篇文章就是把这整个过程讲给
跟我一样「不太懂但想弄明白」的人。
先说结论,免得你被吓退:**这件事的核心其实只有一句话——想办法拿到那个带 m3u8 的播放地址,
剩下的就是「按清单把一节节小视频下载下来拼好」。**
难的不是下载,难的是怎么把那个地址「看」到。因为互联网上的流量是加了密的。
二、先搞懂:直播回放到底是什么东西在网络上跑
在讲工具之前,得先补三个最基础的概念。我用做菜的比喻来讲,你肯定能懂。
1)m3u8 —— 一份「菜单」
视频号回放(以及绝大多数直播、长视频)用的是一种叫 HLS 的技术。
用大白话讲:它不会把一整部视频当成一个巨大的文件传给你,
而是切成很多很多几秒钟的短视频小片(一般是 .ts 结尾,叫「分片」),
然后在旁边放一份「菜单」,告诉你:
第 1 片在哪、第 2 片在哪……一共多少片、每片几秒、有没有加密。
这份「菜单」文件,名字就叫 m3u8(也常叫「清单」「播放列表」)。
所以对下载来说,m3u8 才是钥匙:拿到它,就等于拿到了所有小片的地址清单。
2)点播 vs 直播 —— 菜单有没有「结尾」
同样是 m3u8,里面差别很重要:
菜单最后有
#EXT-X-ENDLIST这句:表示「就这些了,不会再变」——这是点播回放,
可以慢慢下、中断了再续。
没有这句:表示菜单还会不断追加新的小片——这是直播,得边播边录。
工具里判断直播还是回放,看的就是这一句。
3)AES-128 —— 小片可能会「上锁」
有的回放,那些 .ts 小片是加密过的,直接下下来是乱码。
菜单里会额外写一行 #EXT-X-KEY:...,告诉你「用的什么算法、钥匙从哪个地址拿」。
看不懂没关系,你只要知道:菜单里既有小片地址,也可能有钥匙地址,
下载的时候要先拿钥匙、再把每一片解开。
4)还有一种「不是 HLS」的情况
有些回放(比如企业微信直播)根本不是切片的,就是一个完整的大 MP4 直链。
这种情况不用清单,直接下那个 .mp4 就行。工具里用另一个下载器处理它。
三、这个工具一共由哪几块拼起来
wxreplay 不是一个程序,而是几个各干一件事的小模块拼起来的。
我把它们列清楚,后面讲原理时你就不会迷路:
| 模块 | 文件 | 一句话作用 |
|---|---|---|
| 图形界面 + 内置代理 | wxreplay/app.py | 点按钮就能用;程序内部自己搭了个「中间人代理」来偷看流量 |
| 命令行抓包插件 | wxreplay/sniffer.py | 配合 mitmproxy 用,自动识别播放地址并打印下载命令 |
| HLS 下载器 | wxreplay/downloader.py | 读 m3u8 → 并发下分片 → 解密 → 拼接 → 转 mp4 |
| 直链下载器 | wxreplay/fetch_media.py | 专门下那种一整个的 MP4/m4s/ts 直链,支持分段并发 |
| 配套小脚本 | tools/.ps1、tools/.py | 开关系统代理、找窗口、截图、分析抓包文件等「打杂」的 |
| 打包好的成品 | dist/WxReplayDownloader.exe | 上面这些东西打成一个免安装的 exe,双击就能用 |
对小白来说,你只需要用 dist/WxReplayDownloader.exe,其他的都是为了理解原理。
整个工具的「骨架流程」是这样一条线:
┌──────────────────────────────────────────────────────────────┐
│ 你在小程序里点「观看回放」 │
└───────────────────────────┬──────────────────────────────────┘
│ 小程序向腾讯的 CDN 请求 m3u8 清单
▼
┌──────────────────────────────────────────────────────────────┐
│ 工具内置的「迷你代理」偷偷看着这条请求 │
│ (解密 → 看到地址 → 记下来;其它网站的流量原样放行不动) │
└───────────────────────────┬──────────────────────────────────┘
│ 拿到 m3u8 / 或 MP4 直链
▼
┌──────────────────────────────────────────────────────────────┐
│ 下载器:读清单 → 并发下载一节节小片 → AES 解密 → 按序拼接 │
│ 最后交给 ffmpeg「无损转封装」成一个规规矩矩的 mp4 │
└───────────────────────────┬──────────────────────────────────┘
▼
本地出现 replay.mp4
四、核心原理:从「点播放」到「本地出 mp4」
这一节是全文最重要的部分。我把每一步「为什么这么做」讲清楚。
4.1 第一个拦路虎:HTTPS 是加密的
我最开始的幼稚想法是:它播放的时候,视频地址不就发出来了吗?我盯着看不就行了?
答案是:看不到。因为现在几乎所有 App 都走 HTTPS——流量在传输前就被加密了,
你就算把它截下来,看到的也只是一堆乱码。
(就像快递是装在密封信封里寄的,你能拦下包裹,但读不了内容。)
怎么办?两条路:
让 App 自己把地址告诉我们(比如浏览器里按 F12 看网络请求)——能用但麻烦,小程序里没有这个「调试台」。
我们自己当一次「中间人」——在 App 和服务器之间架一个代理,把信封拆开看,看完再原样封好。
这就是 app.py 里那个「迷你 HTTPS 代理」干的事。
4.2 自己当一次「中间人」:自签根证书
「中间人」听着像黑客,但原理很朴素,叫 MITM(中间人代理)。
问题在于:正常情况下,App 是会验货的——它会检查「对面服务器的证书是不是真的」。
你一个来路不明的代理冒出来,App 一看证书不对,直接拒绝连接。
所以中间人必须先「自证身份」。做法是:
工具自己在本地生成一个根证书(
CertAuthority类,存在
%LOCALAPPDATA%\WxReplayDownloader\certs\)。
把这个根证书装进你当前用户的「受信任根证书」里(用的 Windows 的
certutil,
装到「当前用户」,不需要管理员权限)。
之后,App 再验货时,看到的是「我信任的根证书签发的证书」,就放行了。
你可以把它理解成:我们给自己办了一张「能签发通行证的总部证件」,
然后给每个要访问的网站现场签一张「临时通行证」。因为总部证件已经进了系统白名单,
App 就认了。
这一步是全程唯一有点「危险感」的操作,所以工具做了两件事来兜底:
只对跟视频有关的域名做解密(下面的 4.2.1)。
程序正常退出 / 你点「停止」时,自动把根证书移除。
4.2.1 只拆「该拆的信封」:白名单透传
如果为了抓视频,把所有 HTTPS 流量都拆开,那也太夸张了——你聊天、
其他软件全都在裸奔。所以工具里有一张「感兴趣域名」白名单
(INTERESTING_SUFFIXES + INTERESTING_SUBSTR):
腾讯相关的:
myqcloud.com、qcloud.com、gtimg.*、qq.com、servicewechat.com……
或者域名里带
video/vod/live的。
只有命中白名单的连接才解密检查,其它连接一律原样透传(只搬运、不偷看)。
这样抓视频的同时,不会影响你其它软件的正常使用。
具体代码在 ProxyHandler.handleconnect 里:
decrypt = server.intercept_all or is_interesting(host, server.extra_hosts())
if decrypt:
# 用自签证书接管 TLS,然后逐字节转发 + 顺手扫描
...
else:
# 原样透传,一个字节都不改
upstream = server.connect_upstream(host, port, tls=False)
self._pump(sock, upstream, None)
4.3 抓到了什么:请求行 + 响应体,两头都扫
代理架好了,接下来得在流量里认出「这是播放地址」。工具用了两个「扫描器」,
一个盯请求、一个盯响应:
RequestScanner:盯着解密后的请求方向。播放器请求xxx.m3u8、
xxx.mp4 这种地址,一定会出现在「请求行」里,顺手还能把 Referer 抓下来(后面有用)。
ResponseScanner:盯着响应方向。因为有些接口是把播放地址塞在 JSON 里返回的,
地址不出现在请求行、而是藏在响应正文里;还有一种更刁钻的情况——播放器命中了本地缓存,
根本不回源了。这时只能从响应体里「捞」地址。
捞地址用的是一个正则 MEDIA_URL_RE,专门匹配结尾是 .mp4/.m3u8/.m4s/... 的链接。
这里有个小细节:JSON 里的斜杠经常被转义成 \/ 或 \u002F,
所以扫描前先做了一次「反转移」:
probe = text.replace("\\/", "/").replace("\\u002F", "/").replace("\\u002f", "/")
这一步抓到的地址会一行行显示在界面列表里,还能存进urls.json,关掉程序再打开也还在,可以事后补下。
4.4 下载环节:清单 → 分片 → 解密 → 拼接 → 转封装
拿到 m3u8 之后,downloader.py 出来干活。它的流水线是:
第一步:解析清单。parse_playlist() 一行行读 m3u8,把分片地址、时长、
加密信息(#EXT-X-KEY)、初始化分片(#EXT-X-MAP)都解析出来。
清单有两种:
主清单:里面是几档清晰度,每档指向一个子清单。这时要先选一档。
工具按「码率(bandwidth)」排序,--quality best 选最高、worst 选最低,也能填序号。
媒体清单:里面就是真正的分片列表。
第二步:并发下载分片。
一个一个下太慢,所以用线程池同时下好几片(默认 8 个并发,-c 可调)。
每片下完先写成一个临时文件,成功后再原子改名(os.replace),
并在旁边放一个 .ok 小标记。中断后重跑,看到 .ok 就跳过——这就是断点续传。
第三步:解密(如果有)。
遇到 AES-128 加密的分片,先拿钥匙再做 AES-CBC 解密,最后去掉补位(strip_pkcs7)。
密钥地址会缓存起来,不会每片都去请求一次。
第四步:按顺序拼接。
因为分片本来就是按顺序切的,所以直接把一堆 .ts 按字节首尾相接就是完整视频。
第五步:转封装成 mp4。
拼接出来的 .ts 其实也能播,但兼容性一般。如果本机装了 ffmpeg,
就用它把 .ts「无损转封装」成 .mp4——注意是 -c copy,
只换容器不重新编码,画质分毫不损,速度也快:
ffmpeg -i merged.ts -c copy -bsf:a aac_adtstoasc -movflags +faststart replay.mp4
(aac_adtstoasc 是音频格式转换必须的;+faststart 让 mp4 的索引放到文件头部,
方便边下边播。没装 ffmpeg?工具会直接输出 .ts,照样能看。)
4.5 另一种情况:直接就是一个 MP4 直链
如果抓到的是一整条 MP4 直链,就轮到 fetch_media.py。它的聪明之处是:
先发一个很轻的请求(先
HEAD,不行就Range: bytes=0-0)去试探:
这文件多大?支不支持「分段下载」?
如果支持,就把文件切成一块块(默认每块 8M),并发下载,
每块带 Range 请求头,下完的块同样放 .ok 标记,天然支持续传。
如果服务器不支持分段,就老老实实单连接下。
五、完整使用流程(小白照做版)
把原理放一边,实际用起来其实很简单:
在小程序里进到那场直播回放的页面(先别点播放)。
双击
dist\WxReplayDownloader.exe,点程序上的 「开始下载」。
程序会自动:装根证书 → 开本机代理。
回到小程序,点 「观看回放」,让画面真的开始播一会儿。
抓到的地址会一行行出现在 「捕获到的视频」 列表里(序号 / 标题 / 链接 / 状态)。
选中一行(或双击),点 「下载选中」,文件就存到你设的「保存到」目录了。
如果点了「观看回放」却半天抓不到地址,大概率是这段回放你以前看过、
已经被小程序缓存了(下面坑 1 详解)。解决方案:
关掉小程序窗口 → 点 「清除小程序缓存」 → 重新打开小程序点「观看回放」。
六、开发中的避坑清单(重头戏)
这一节才是这篇文章的「精华」。下面每一条,都是我(或这套代码的作者)
真金白银踩出来的。我按「现象 → 原因 → 怎么解决」来写。
坑 1:回放「以前看过」,地址怎么都抓不到 ⭐最坑
现象:一切正常,代理也开了,证书也装了,就是列表里一条地址都没有。
原因:这段回放你之前看过,小程序把整段视频缓存到了本地磁盘。
这次播放它直接从本地读,压根不走网络——不走网络,代理自然什么都看不到。
解法:清掉小程序的网页缓存,逼它重新联网。
工具里的 on_clear_cache 会去 %APPDATA%\Tencent\<微信>\radium\web\profiles\*
下找纯缓存目录(Cache / Code Cache / GPUCache / DawnGraphiteCache / DawnWebGPUCache)删掉。
连带坑:
必须先关掉小程序——缓存文件被占用时删不掉。工具对「没清干净的」会如实报告,不假装成功。
只删纯缓存,绝不动登录态和聊天记录。这一点是红线,别为了抓个视频把人家微信搞坏了。
判断「有没有删干净」时留了 1MB 的余量(
CACHE_STUCK_MIN):
因为 webview 被清空后会立刻重建几十字节的索引文件,不能算作「没清掉」。
坑 2:改了系统代理注册表,程序却「没反应」
现象:代理明明写进了注册表(
ProxyEnable=1),但小程序/浏览器还是不走代理。
原因:Windows 的代理设置,光写注册表不算数。正在运行的程序不会自己去读,
得主动「广播」一下告诉它们「设置变了」。
解法:改完注册表后,调用 WinINET 的
InternetSetOption(选项 39 和 37)
刷新,再用 SendMessageTimeoutW 向所有窗口广播WM_SETTINGCHANGE(0x001A)。代码在 WinProxy._refresh():
wininet.InternetSetOptionW(None, 39, None, 0) # SETTINGS_CHANGED
wininet.InternetSetOptionW(None, 37, None, 0) # REFRESH
user32.SendMessageTimeoutW(0xFFFF, 0x001A, 0, "Software\\...\\Internet Settings", ...)
这个坑很典型:不是「做了」,而是「做了并且通知了」,两回事。
坑 3:程序退出了,系统代理没还原 → 全家断网
现象:抓完视频,关掉工具,结果浏览器全打不开了——因为代理还指着那个已经关掉的端口。
原因:只「开启代理」却忘了「还原代理」,是最常见的事故。
解法:开启代理前,先把原来的三个值(
ProxyEnable/ProxyServer/ProxyOverride)
备份到文件(proxy_backup.json)。停止、退出时,从备份原样写回并再次广播刷新。
这样即便正常路径没走到,下次也有据可查地还原。
坑 4:certutil 删证证书弹框卡死 / 无控制台直接卡住
现象 A:程序在「移除根证书」这一步永久卡住,界面转圈不动。
原因:删受信任的根证书时,
certutil会弹一个确认框。
程序是无人值守的,没人去点「是」,就一直等——卡死。
解法:删除时必须加
-f(强制),跳过确认框。
现象 B:打包成 exe(没有控制台窗口)后,调用
certutil直接卡住。
原因:这类程序没有标准输入,
certutil可能去等输入,等不到就挂起。
解法:调用时显式指定
stdin=subprocess.DEVNULL,并加超时。
subprocess.run(["certutil"] + args, capture_output=True,
stdin=subprocess.DEVNULL, timeout=20, creationflags=CREATE_NO_WINDOW)
坑 5:连上游网站时,TLS 握手失败
现象:抓某个域名时日志报「与 xxx 建立 TLS 失败」。
原因:上游服务器(有些是 CDN)用的证书,
Python的默认校验不通过。
解法:先按标准校验连一次;失败了再降级用「不校验」的方式重连一次。
这样既保留了对正常服务器的严格校验,又不会因为个别特殊情况整个卡死。
代码在 ProxyServer.connect_upstream。
注意这是「代理自己连上游」时的行为,和我们在 App 面前「冒充服务器」是两码事,
别混淆。
坑 6:抓视频把别的软件也「拆了信封」
现象(如果没做白名单):一开抓包,所有软件的 HTTPS 都能被看到,安全隐患巨大。
原因:偷懒的话,中间人会「什么都解密」。
解法:见 4.2.1——白名单透传。只有视频相关域名才解密,其余原样搬运。
这是「能抓视频」和「不祸害别的软件」之间的平衡点。
坑 7:地址藏在 JSON 里 / 斜杠被转义了
现象:请求行里翻遍了,没有
.m3u8、没有.mp4,但视频确实在播。
原因:播放地址是某个接口放在 JSON 响应体里返回的;
而且 JSON 里的斜杠还会被转义成 \/、\u002F。
解法:不光扫请求,还要扫响应体;
扫之前先把 \/、\u002F 还原成 /,再上正则。
代码就是 ResponseScanner + MEDIA_URL_RE 那段。
坑 8:AES 解密的「IV」到底从哪来
现象:解密出来的视频是花屏 / 报错,提示 IV 不对。
原因:HLS 的 AES-128 是 CBC 模式,解密需要一个 16 字节的初始向量 IV。
这个 IV 不一定写在清单里。
解法(两种来源,按规矩来):
清单的
#EXT-X-KEY里有IV=→ 用它;规范要求 16 字节,短了要在左边补零。没有
IV→ 用分片的序号(sequence)当 IV(大端序补齐 16 字节)。
def _iv_for(segment):
if segment.key and segment.key.iv:
return segment.key.iv
return segment.seq.to_bytes(16, "big") # 没给 IV 就用序号推
坑 9:纯 Python 算 AES 太慢
现象:带加密的回放,下载速度慢得离谱。
原因:为了保证「零依赖也能跑」,工具内置了一份纯 Python 的 AES 实现。
纯 Python 逐字节算,慢是必然的。
解法:优先用现成的加速库——
pycryptodome、没有就用cryptography,
都没有才退回内置实现。代码里用 AESBACKEND 选后端。
所以 pip install pycryptodome 是「可选但强烈推荐」。
坑 10:ffmpeg 相关的两个小坑
坑 A:本机没装 ffmpeg,转封装这一步直接失败。
→ 解法:检测不到 ffmpeg 就优雅降级,直接输出 .ts(能播),并提示用户。
不要因为一个锦上添花的步骤,把整个下载弄崩。
坑 B:转出来的 mp4 能下不能秒开、有些播放器音频异常。
→ 解法:转封装参数别省——-c copy(无损)、-bsf:a aac_adtstoasc(音频)、-movflags +faststart(把索引挪到文件头,方便边下边播)。
坑 11:断点续传的「原子性」
现象:下到一半断了,重跑后续传,结果拼出来的视频某一段是坏的。
原因:分片文件一边下一边写,断了以后留下一个不完整的文件,
但程序以为「这个文件存在 = 下好了」,下次就跳过了。
解法:写临时文件 → 下完整 → 原子改名 → 再放一个
.ok标记。
只有「文件存在 且 .ok 存在 且 大小不为 0」才认作完成:
if self.resume and os.path.exists(mark) and os.path.getsize(path) > 0:
self.skipped += 1
return path
判断「完成」不能只看文件在不在,要看「标记在不在」。
坑 12:直链的签名参数不能「手贱」删
现象:MP4 直链下载报 403 / 被拒。
原因:这种直链地址通常带一长串签名参数(跟在
?后面),
是 CDN 用来校验你有没有权限的。你手动把参数删了、或者只复制一半,就失效了。
另外很多 CDN 还会校验 Referer。
解法:
地址要整条复制,一个字都别改。
抓包时顺手把真实的
Referer一起抓下来,下载时带上。
工具在抓包时就会记录 Referer,直链下载也支持 --referer 参数。
坑 13:打包成 exe 之后,print 看不见了
现象:源码里跑得好好的,打包成 exe 后没有控制台,
所有 print 的输出石沉大海,界面上啥也看不到。
解法:把
sys.stdout/sys.stderr重定向到一个队列,
界面线程再从这个队列里取出来显示(QueueWriter 类)。
这样在「没有控制台」的前提下,进度和日志照样能看到。
连带坑:PyInstaller 打包时,
exclude掉一大堆用不到的库
(numpy、PIL、mitmproxy……)缩小体积,
同时把动态导入的 downloader、fetch_media 加进 hiddenimports,否则会「打包能过、运行报错」。
坑 14:高分屏下界面糊、鼠标点偏
现象:在 2K/4K 屏上,界面发虚,而且鼠标点到的位置和实际控件错位。
原因:程序没做 DPI 感知时,Windows 会帮你缩放整套界面,
于是坐标全乱套。
解法:启动时调用
SetProcessDpiAwarenessContext/SetProcessDpiAwareness
声明「我自己处理缩放」。这样高分屏清晰,坐标也不偏。
连配套的 PowerShell 窗口脚本(tools/gui.ps1)里也做了同样的处理。
坑 15:直播地址「一直在变」
现象:直播场景下,同一条流不断冒出新地址,列表被刷屏。
原因:直播清单会不断追加新分片,
而且每次拉清单,URL 里的签名参数(query)都会变,但「同一个流」其实是同一条。
解法:去重时只看
scheme://host/path,把 query 去掉(dedupe_key)。
清单去重后,再靠分片序号判断哪些是新来的,只下新的。
直播地址还会过期,所以工具始终保留最新一条到 latest_live_url.txt。
七、合规与边界
技术讲完了,但有些话必须说在前面:
这个工具只能用来备份你自己有权访问的内容。
请遵守平台服务条款和著作权法,不要用它去传播、售卖别人的作品。
抓包会临时把自签根证书装进系统、临时改系统代理,请务必在结束后
停止并移除(工具正常退出会自动做这些;手动用 tools\capture.ps1 的,
记得抓完执行 netproxy.ps1 -Action off 并 certutil -delstore Root mitmproxy)。
抓包期间,代理能看到被解密域名的流量,所以别在开着代理时登录敏感账号。
八、写给和我一样是小白的人
做完这个工具,我最大的感受是:
很多看起来「很黑客」的事,拆开看,原理其实都很朴素。
「抓包」= 在中间拆信封、看完再封好;
「下载视频」= 按菜单把一节节小片下下来拼起来;
「解密」= 菜单里给了钥匙地址,先拿钥匙再开锁。
真正难的,不是某个高深的算法,而是一连串琐碎但致命的细节:
证书会不会卡死、代理会不会忘记还原、缓存会不会让你「看不到流量」、
断点续传的文件是不是完整的……
这份「避坑清单」几乎每一条,都是从「看起来应该没问题」到「实际就是不行」之间的距离。
如果你也在做类似的东西,希望它能帮你少熬几个夜。
最后再强调一遍:备份自己能访问的内容是权利,传播别人的作品是侵权。 心里有这条线就好。
内容版权声明
内容链接:https://www.diezanrpa.com/jinengjiqiao/1409.html
内容标题:我把视频号直播回放「搬」到了本地:一个小白从抓包到合成的全过程,和 15 个差点把我送走的坑
当前文章由平台【超级管理员】原创文案写作,内容版权归当前平台所有,用户转载,请注意来源!
本文最后更新发布于【2026-10-13】,某些文章具有时效性,若有错误或已失效,请联系客服
争议处理:针对本文内容若有异义,亦可直接与【法律顾问:易兴俊,律师联系电话:13825799821】直接联系沟通

