☰
网页视频m3u8下载全攻略:ffmpeg从原理到实操
2026/10/23 15:02:30 网站建设 项目流程

花了点时间,把去年做得比较顺手的一套流程整理出来了。如果你只是想“把网页里的视频存下来”,那这篇应该能帮你少走很多弯路。先说清楚:本文只在技术层讲怎么下载网页中的 .m3u8 视频文件,也就意味着我们要面对的是分片视频——不是页面里直接给一个完整 MP4 的简单情况,而是几十甚至几百个 ts 分片加一个播放列表。文中所有案例均基于虚构的“某视频站”,纯讲原理和实操,不涉及任何具体站点的绕过手段,也请你只把相关方法用在你有权限保存的内容上。

以前我遇到这种情况时,第一反应是去找“网页视频下载”之类的工具,结果要么下载下来是乱码文件,要么装了一堆全家桶。后来看请求才发现,几乎所有网页中的长视频都不是一个完整文件,而是一个 m3u8 播放列表加一堆 ts 分片。这就解释了为什么传统的“嗅探单个视频地址”思路经常失灵——你真正需要保存的是一整组资源。理解了这个底层逻辑,剩下的步骤反而很简单:拿到 m3u8 地址,用 ffmpeg 按列表把分片拉下来,合并成单个 mp4。

这篇文章不会让你装什么来路不明的下载器,核心工具就一个开源命令行软件,剩下全是浏览器自带的 F12 开发者工具。我会从原理讲到实操,再讲几个我实际测试时踩过的坑,比如加密分片怎么处理、Referer 校验拦截了下载怎么办、为什么有的视频明明下载完了却播放不了。适合什么读者?想备份自己会员课程、想离线观看网页长视频的人,以及刚接触 HLS 流媒体、想弄明白 m3u8 到底是怎么工作的人。

1. 为什么网页视频越来越多用 m3u8:先搞懂它是什么

1.1 HLS 协议与分片存储

m3u8 本质上不是视频,它是一种播放列表文件。你打开网页里的视频时,播放器并不是去加载一个巨大的 MP4,而是先请求一个 m3u8 文件,拿到一串分片地址,再去逐个加载这些几秒钟一段的小视频片段。这种技术通常叫 HLS(HTTP Live Streaming,流媒体直播协议的一种实现)。它最早是流媒体领域为了解决“让视频在不同网速下都能顺畅播”而设计的,后来因为太实用,普通点播视频也全面转向了这套方案。

为什么网站要把一个完整视频切成几十上百个小片段?一个很直观的原因是“进度条拖动”更流畅。想象一下 CDN 的缓存策略:完整 MP4 需要从头到尾缓存,或者至少需要处理大量 HTTP Range 请求,对服务器压力不小。但分片方案下,每个 ts 片段都是独立的小文件,CDN 可以大量缓存这些片段,你想拖到哪一段,播放器就去请求对应那个分片,缓存命中率很高,首屏加载也更快。

另一个重要原因是防盗和可控性。分片模式下,视频被拆得七零八落,用户很难像“右键另存为”那样直接拿走一个完整文件。同时服务端可以随时更换分片地址,甚至给分片加动态 token,下载脚本一旦拿到过期的 m3u8 就会失败。这既是网站保护内容的手段,也是我们要学习怎么绕过(在合法前提下)的原因。

1.2 一段 m3u8 文件到底写了什么

看到这里,你最好先能读懂 m3u8 的“骨架”。我用一个比较典型的点播文件内容举例:

#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-KEY:METHOD=AES-128,URI="key.key" #EXTINF:10.0, segment_000.ts #EXTINF:10.0, segment_001.ts #EXTINF:10.0, segment_002.ts #EXT-X-ENDLIST

逐行解读一下:

  • #EXTM3U是文件头,表示这是一个 m3u 播放列表。
  • #EXT-X-TARGETDURATION:10表示每个分片最长约 10 秒。
  • #EXT-X-MEDIA-SEQUENCE:0是序号起点。
  • #EXT-X-KEY:METHOD=AES-128,URI="key.key"是很关键的字段,它表示这些分片被 AES-128 加密了,解密钥匙在 key.key 文件里。看到这个字段,意味着你如果只把 ts 全下下来,播放器依然放不了,还得有 key 参与解密。
  • 下面每一行#EXTINF都对应一个分片地址。地址可以是相对路径,也可以是完整的 http 链接。

这份列表对播放器来说就是“菜谱”:先读菜谱,知道有多少道菜、每道菜在哪个锅里,然后按顺序去端。对下载工具来说也一样,它只需要照着这个菜谱一个个请求 ts 文件,再按顺序拼起来。

1.3 “能播放不等于能下载”:MP4 与分片视频的本质区别

很多人第一次尝试时遇到的现象是:在网页里能流畅播放,但用浏览器自带功能保存下来的往往不是几十 KB 的播放页面,就是另一个 m3u8 小文件,根本不是视频本体。原因在于:播放器已经拿着 m3u8 去动态加载分片了,视频的“现成文件”压根不存在——唯一存在的是一个又一个的小片段。类似做菜时你看到的是一桌菜,但菜不是真空包装好的成品,而是后厨一个个盘子端上来的。

所以,我们需要做的并不是“下载某一个视频文件”,而是“模拟播放器的行为”:请求 m3u8 列表 → 下载所有分片 → 按顺序合并 → 如果有加密,解密 → 最终封装成一个 MP4。这也是接下来所有方案的核心思想。明白了这一点,网上那些号称“一键嗅探”的下载工具为什么经常失效也好理解了——它们能嗅到 m3u8,但后续的分片拉取、解密、合并做得不完整,一旦遇到 token 限制或加密就歇菜。

2. 先跑通最小流程:一条命令把视频从网页里抠出来

如果只想收藏一个视频,最快的方式不是写什么复杂脚本,而是用 ffmpeg。它既能下载分片,也能负责合并和转封装。这一节我先给你一条能直接用起来的命令,再解释它为什么这么写。

2.1 前提:在开发者工具里快速找到 m3u8 地址

在动手之前,你得先能从目标网页里找到那个 m3u8 地址。最简单的办法是用浏览器自带调试功能:

  1. 在视频页面按 F12 打开开发者工具,切到 Network(网络)面板。
  2. 把过滤条件选为“All”或“Media”,然后刷新页面让视频重新加载。
  3. 在请求列表里查找类型为application/vnd.apple.mpegurl或文件名以.m3u8结尾的请求。

多数情况下这个地址会以相对路径出现,比如/2025/04/01/xxxxxxxx/1000k_hls/index.m3u8,复制时记得补全为完整 URL。如果 Network 里看不到,再试试在 Source 面板搜索关键词m3u8或.ts,通常能定位到播放器初始化脚本里的地址拼接逻辑。

2.2 ffmpeg 一行命令下载并合并

当你拿到 m3u8 地址后,在终端里执行这一行:

ffmpeg -i "https://example.com/path/playlist.m3u8" -c copy -bsf:a aac_adtstoasc video.mp4

解释一下:

  • -i后面是 m3u8 的完整地址。ffmpeg 自己会解析列表,然后并发或顺序拉取所有分片,这不需要你额外写下载循环。
  • -c copy表示“不重新编码,直接复制原始流”。因为视频分片本身就是 H.264/AAC 编码,我们只是把它们换个容器,不需要再次压制,所以速度和画质都更优。
  • -bsf:a aac_adtstoasc是个容易被忽略但很重要的参数。很多 ts 分片里的音频是 ADTS 格式的 AAC,直接放进 MP4 容器会导致部分播放器无法识别或音画不同步;这个参数会把音频流转换成更通用的格式。

跑完后你应当得到一个.mp4文件。建议立刻用ffprobe检查一下:

ffprobe video.mp4

重点看 Duration(时长)是否和网页里的视频时长一致,stream 列表里是否有 video 和 audio 两路。有的视频只有视频流没有音频流,说明你是从纯视频轨道页面里抓的,属正常现象。

2.3 为什么不重新编码

很多人看到“视频要格式转换”,第一反应是拿起格式工厂这种工具去压一遍。但在下载分片 + 合并的场景里,重新编码是最后的选择。原因很简单:H.264 到 H.264 的再压制只会让画质下降,耗时还会是原视频时长的好几倍。而-c copy是“把原材料原封不动换个箱子”,一分钟的视频几秒就合并完了。

我把两条命令的实验结果放在一起对比过:

处理方式耗时(10分钟视频)画质文件大小
-c copy封装合并约 5-10 秒无损失与原码率基本一致
重新编码压制约 3-10 分钟肉眼可见劣化取决于压缩参数

所以只要分片文件本身是完整的、解码器能识别的数据,完全没必要二次编码。只有在极少数情况下,比如你拿到的分片解码参数不一致、合并后播放器挑毛病,才会考虑用重新编码来修复容错问题。

2.4 验证“真的下载完整”的三种方法

下载完成不等于万事大吉。我每次下载完都会做这三件事:

  1. 看时长:ffprobe video.mp4里的 Duration 是否接近网页播放器里的时长。明显偏短说明有分片没拉全。
  2. 看末尾是否完整:静静拖进度条到最后一秒,播放器能正常结束、不黑屏,说明最后一个分片没问题。
  3. 看日志里有没有报错:ffmpeg 下载过程中如果某个分片连续失败,会在 stderr 里打出Failed to fetch segment之类的话。忽略它可能导致合并后中间缺一段。

如果发现缺分片,不要急着用同一个命令重跑——重跑很可能会再次失败。应该参考后面第 5 章的方法,先判断是网络波动、请求限制还是 token 过期。

3. 真正麻烦的一步:在网页里定位 m3u8 地址

直接给地址下载是最理想的情况,但现实里这一步往往最耗时间,而且不同网站的播放器实现差异很大。很多网页里的 m3u8 地址并不会老老实实出现在 Network 列表里,或者你复制出来后请求直接 403。

3.1 F12 找不到 m3u8?换个搜索思路

我见过不少这样的“某视频站”:Network 面板刷新后能看到一堆 JS、CSS、图片,唯独没有 m3u8。原因通常是播放器通过 JavaScript 里的fetch或XMLHttpRequest动态拼接了地址,而且地址带一个很短的时效 token。刷新页面之后 token 已经失效,Network 里自然看不到。

这时候别硬刷新,换两个方向:

  • 直接搜 js 源码。在 Sources 面板打开播放器相关的 JS 文件,按 Ctrl+Shift+F 全局搜索m3u8、index.m3u8、playlist、ts。这类关键词经常能定位到地址拼装代码。拼装的 URL 参数里有签名和时间戳的话,直接用它生成的地址很可能仍然有效,因为它是在你当前会话上下文里动态算出来的。
  • 搜接口名。有些站点会先用 XHR 请求一个 JSON 接口,再由 JS 把接口返回的地址拼成 m3u8。把接口请求翻出来,在响应里搜url或m3u8字段。这个方向优先,因为接口返回的地址往往还带着当次请求的认证信息,直接复用成功率更高。

3.2 实战案例:某视频站的地址追踪过程

用一个虚构的“某视频站”来串一遍完整流程,方便你照着练手:

  1. 打开页面并登录,视频开始播放后先在播放器里右键,选择“复制调试信息”(不同播放器文案略有不同),能拿到当前播放地址相关的 JSON,里面常常直接包含带签名的 m3u8 链接。这一步是很多脚本都忽略的“捷径”,值得先试。
  2. F12 打开 Network,过滤 m3u8,如果没看到,清空面板,再点一下播放器的画质切换按钮,强制播放器重新请求一次播放列表。此时新的 m3u8 请求就会出现在面板里。
  3. 点击该请求,查看 Headers。重点看Request URL、Referer和User-Agent。这三个信息后面在命令行里要原样带上。
  4. 复制 URL 后用终端测试。先不要急着下载,只发一个 HEAD 或 GET 请求看状态码。如果返回 403,说明请求头校验没通过,你得用后面 3.3 的方法手动补头部信息。

这个流程看起来啰嗦,但它就是所有“某视频站”下载工具的核心逻辑:拿地址 → 带合法头部 → 拉分片 → 合并。无论页面怎么变,都绕不开这四步。

3.3 带上 Referer 和 User-Agent 才能下载

拿到 m3u8 地址后,直接用 ffmpeg 跑大概率会失败,因为很多站点会校验请求来源。最常见的两个字段就是 Referer 和 User-Agent。Referer 表示“我来自哪个页面”,部分站点只允许特定页面发起的请求;User-Agent 则用于识别是不是浏览器环境。

在 ffmpeg 里可以用-headers参数补上,命令示例:

ffmpeg -headers "Referer: https://video.example.com/play/123" -user_agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" -i "https://example.com/path/playlist.m3u8" -c copy -bsf:a aac_adtstoasc video.mp4

注意两点:Referer值就是你在浏览器开发者工具里看到的请求头里的 Referer,别随手填网页首页;User-Agent尽量复制浏览器的真实 UA,不要用 ffmpeg 默认的Lavf开头的 UA,很多站一看到就拦截。

如果你不想每次手动复制,也可以把请求头放到一个名为headers.txt的文件里,然后用-headers读取。我一般会为每一个站点保存一份独立的请求头文件,因为不同站点校验逻辑不一样。

3.4 动态 token 和请求签名怎么办

最麻烦的一类视频站,m3u8 地址动不动就带?sign=xxx&t=1730000000这样的参数。sign 是根据当前时间、视频 ID 和密钥算出来的签名,一般半小时内有效。这种情况下,靠“复制一个旧地址”是下载不了的,因为地址一过期就 403。

处理思路有两种:

  1. 在浏览器里“现取现用”。把 ffmpeg 下载操作做成一个小脚本:先用浏览器开发者工具手动触发播放器拿到当前有效的地址,立刻交回给 ffmpeg。这个过程窗口期短,但简单直接。
  2. 用支持脚本注入的下载器。少部分命令行下载工具允许你指定一个小插件来动态获取 m3u8 地址,从而在每次请求前刷新签名。这个方案需要写一点代码,适合要批量下载时投入。

从规避网站风控的角度说,我个人不推荐写过于自动化的批量脚本去绕过签名机制。偶尔保存几个自己购买的课程,用方式一就够了。如果你发现自己需要频繁绕签名才能下载,那该反思的可能是使用边界问题了。

4. 遇到加密内容:AES-128 解密与合并

分片下下来之后发现播放器提示“无法打开”“解码错误”,十有八九是遇到了加密流。HLS 最常见的加密方式是 AES-128,播放器从 m3u8 的EXT-X-KEY字段里找到密钥地址,然后对每个分片解密后再播放。你要做的也是同一件事。

4.1 看看 m3u8 里的 KEY 字段

回到最开始的 m3u8 样例,关键行是:

#EXT-X-KEY:METHOD=AES-128,URI="key.key"

这表示分片采用 AES-128 加密,密钥文件是相对路径key.key。密钥文件本身非常小,通常只有 16 个字节的琐碎内容。下载它时同样要带上 Referer 和合适的 UA,否则也会被拦。

还有一个常被忽略的点:有些 m3u8 里 KEY 字段还会带IV=参数,比如:

#EXT-X-KEY:METHOD=AES-128,URI="key.key",IV=0x1234567890abcdef1234567890abcdef

IV 是 AES 解密时需要用到的初始向量。如果没写,很多实现默认用分片的序号补足。这一点在手动解密时特别容易出问题,建议优先依赖播放器框架自动处理,不要自己算。

4.2 让 ffmpeg 自动解密

好消息是 ffmpeg 对 HLS 的 AES-128 解密支持得很完整。当你把带EXT-X-KEY的 m3u8 地址直接丢给它时,它会自动请求密钥文件并完成解密。因此,前面的命令不需要任何额外参数:

ffmpeg -i "https://example.com/path/encrypted/playlist.m3u8" -c copy -bsf:a aac_adtstoasc video.mp4

ffmpeg 在拉取分片时会边下载边解密,产出已经是解密的 MP4。这个流程对大多数点播视频都有效。

我实测中有一次失败是在密钥地址上也加了时效签名,导致 ffmpeg 请求密钥时得到 403。当场改了好一会儿才意识到问题不在分片、不在播放列表,而在那个几 KB 的 key 文件上。如果你遇到“从日志看分片能下载但解密失败”的情况,先手动访问一下 m3u8 里URI=指向的地址,看看是否 403,这是最快的定位方式。

4.3 手动解密流程(备份方案)

如果 ffmpeg 因为某些原因无法直接解密,比如密钥地址还需要额外请求头而你不想折腾脚本,可以退一步手动操作。流程如下:

  1. 先用下载工具把 m3u8 里所有 ts 分片下到本地。
  2. 把 key 文件也下到本地。
  3. 用 OpenSSL 解密密文:
openssl aes-128-cbc -d -in segment_000.ts -out segment_000.dec.ts -K $(xxd -p -c 256 key.key) -iv 00000000000000000000000000000000

这里-K参数需要 32 位十六进制密钥,-iv是 32 位十六进制初始向量。如果 m3u8 里没指定 IV,通常用分片的序列号(MEDIA-SEQUENCE + 当前序号)填成 16 字节的十六进制数;但不同实现不太一样,所以手动方案只适合确定知道 IV 规律的场景。更省事的做法是先用 ffmpeg 自动解密,把手动解密当作理解原理的手段,而不是日常操作。

4.4 多码率主索引

有时候你复制的 m3u8 可能不是真正用来播放的那一个,而是主索引文件。它里面没有分片地址,只有多个子索引。典型的像这样:

#EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,RESOLUTION=1920x1080 1080p/index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=1280x720 720p/index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=400000,RESOLUTION=640x360 360p/index.m3u8

抓到了这种主索引,要么去选一个清晰度对应的子地址下载,要么交给 ffmpeg 让它自己按默认规则选。如果想指定清晰度,就把-i参数直接指向子索引地址。这个小知识点能避免你哪天抓到一个列表却发现“没有 ts”的困惑。

5. 下载中断与合并失败的高发问题排查

直接在终端跑 ffmpeg 下载十次,总有两三回会遇到奇怪的问题。这一章全是实际测试中踩过的坑。

5.1 下载到一半突然失败

表现是:进度条走到 60% 左右停止,日志反复出现Failed to fetch segment。产生原因一般有两个:

  • 分片地址过期。动态 token 的地址有效期很短,前几分钟有效,之后旧地址全部 403。这种问题重试没用,必须回到浏览器里重新拿一次新的 m3u8 地址再跑。
  • 请求被封。短时间高并发请求触发风控,返回 403 或 429。这种情况我建议把 ffmpeg 的并发控制调低,比如用-http_multiple 0或加-reconnect参数;如果是命令行脚本,降低整体频率,隔几秒再拉下一个分片。
ffmpeg -i "https://example.com/path/playlist.m3u8" -c copy -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 video.mp4

-reconnect参数在稳定性和速度的平衡上很有帮助,遇到单次网络闪断会自动重试,不至于让整个任务报废。

5.2 多线程下载与本地合并不冲突

如果你发现单线程下载一个 2 小时的视频太慢了,也可以用支持多线程的下载器先把所有分片快速拉到本地,然后用 ffmpeg 做本地合并。本地合并的命令如下:

for f in segment_*.ts; do echo "file '$f'" >> filelist.txt; done ffmpeg -f concat -safe 0 -i filelist.txt -c copy -bsf:a aac_adtstoasc video.mp4

这种方式有几个天然的优势:多线程拉分片可以大幅提升速度;本地合并不依赖网络稳定性,合并时失败可以反复重试;而且你可以用脚本控制并发,不会把站点打爆。唯一要注意的是filelist.txt里的文件顺序必须按原始序号排,否则音画顺序会错乱。

5.3 下载成功了但播放不了

这是很令人崩溃的情况:文件有几十 MB,ffprobe也能识别,但拖进度条卡顿、最后几秒黑屏,甚至有的播放器直接打不开。排查顺序我固定为:

  1. 先用 ffprobe 看流信息。如果只有 video 没有 audio,检查是否原始视频就没有音频轨道。
  2. 看封装时间戳。ts 转 MP4 时偶尔会遇到非零时间戳起点,导致部分播放器偏移严重。可以用-start_at_zero和-avoid_negative_ts make_zero修正。
ffmpeg -i video.mp4 -c copy -avoid_negative_ts make_zero -start_at_zero video_fixed.mp4
  1. 看容器格式完整性。如果是从某个带 m4s 分片的站点下载的,ffmpeg 对 fMP4 的自动处理能力不如 ts 好,建议优先用浏览器控制台确认当前播放的是不是 ts 分片,而不是 m4s。

踩过几次坑之后,我现在拿到任何合并产物都会先跑一遍 ffprobe,再抽查进度条中部和尾部,确认没问题才归档。这一步花不了几秒钟,能省掉后面无数次“怎么放不出”的自我怀疑。

5.4 音画不同步和时间轴错乱

合并后音画不同步,最常见的原因是分片文件的音频采样率或编码参数不完全一致。这在理论上不该发生,因为播放器是按标准流来处理的,但下载时抽到的部分分片可能来自不同转码档位。

解决办法有三个,按优先级排列:

  1. 回到 m3u8 列表,确认所有分片是否同一个 band 的,不要混用了不同码率的切片。
  2. 用ffmpeg -i video.mp4 -c copy -map 0:v:0 -map 0:a:0 -shortest output.mp4强制只保留第一路视频和第一路音频,丢到多余流。
  3. 极少数情况下需要做一次转码重同步,也就是把音视频都重新编码成统一时间基。这属于最后手段,因为会损失画质。

6. 关于“保存网页视频”这件事的边界与后续玩法

6.1 哪些场景值得动手,哪些不要碰

技术本身是中性的,但用在什么地方需要有点分寸。我一般只建议保存下面两类内容:自己付费购买的在线课程或会员内容,出于离线学习的目的;以及无版权的公开视频、自己上传的素材备份等。对于平台明确禁止下载、或者明显是其他人创作的作品,建议不要碰。

原因很现实:从技术上讲,任何 HLS 视频都能通过这套方法下载;但从法律和习惯上讲,观看权和复制权是两回事。批量爬取视频不仅容易封 IP,还可能涉及侵权。这个边界不是道德说教,而是我用教训换来的建议——有一次我批量下载某平台的教程,第二天账号就被风控限速了。

6.2 批量处理的自动化脚本思路

如果只是偶尔存一个两个,手工操作完全够用。但如果你确实需要批量处理(比如把自己上传的视频归档),可以考虑这么做:

  1. 先手动下载一两个,确认请求头、m3u8 地址规律、解密情况都摸清了。
  2. 写一个 Shell 或 Python 脚本,步骤为:读取视频 ID 列表 → 请求播放接口获取 m3u8 地址 → 带请求头调用 ffmpeg → 验证产物。
  3. 脚本里务必加“频率控制”,单个视频完成后 sleep 几秒,而不是无间隔连发。

这个思路的代码量不大,但引入自动化之前,先保证手动的三步都能跑通。自动化是放大手工流程的效率,不是帮你绕过手动流程中遇到的坑。

6.3 最后分享一个我常用的工作流

作为一个经常要处理在线视频的人,我现在的工作流固定为:打开开发者工具 → 复制当前播放地址 → 先用 ffmpeg 试着拉完整文件 → 下载完成后 ffprobe 验时长 → 发现问题再回到 m3u8 列表逐行检查。整个过程听起来慢,实际上熟练后一个 10 分钟的视频在两三分钟内就搞定了。

如果让我给新手一个最直接的起点建议,那就是:先拿任何一条不带加密、地址直接可见的 m3u8 链接,把第 2 章的 ffmpeg 命令跑通。只要见过一次“一堆 ts 变成一个 mp4”的过程,后面所有关于定位地址、解密、合并的困惑,都会变得好理解得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询