网页视频的下载和本地化保存,是很多做内容归档、离线学习、素材整理的人绕不开的一个需求。但真正动手去做的时候,你会发现事情远没有"右键另存为"那么简单——打开开发者工具,看到的不是一整段 mp4 地址,而是一个后缀为 m3u8 的索引文件,里面密密麻麻列着几百个 ts 分片,有些分片还经过了 AES-128 加密,直接下载下来根本没法播放。这套流程涉及 m3u8 索引解析、AES-128 解密、ts 分片合并三个核心环节,任何一个环节出错都会导致最终文件无法播放或者音画不同步。这篇内容就是把我自己在实际处理这类任务时踩过的坑、验证过的方案、以及每一步背后的原理完整梳理出来,适合有一定命令行基础、想搞清楚"为什么这样做"而不是只会复制粘贴命令的读者。全文围绕 m3u8 定位、AES-128 解密、ts 合并这条全链路展开,把每个环节的细节和常见故障都讲透。
1. 先搞清楚 m3u8 到底是什么,以及它为什么让下载变复杂
很多人第一次接触 m3u8 的时候会懵:明明浏览器里视频播放得好好的,为什么我拿到的地址打开是一堆文本?要理解这件事,得从流媒体传输的基本逻辑说起。
1.1 m3u8 的本质是一份"播放清单"
m3u8 文件本身不是视频,它是一个基于 UTF-8 编码的文本播放列表,属于 HLS(HTTP Live Streaming)协议的一部分。你可以把它理解成一份快递清单:清单上写着"第 1 段在 A 地址、第 2 段在 B 地址、第 3 段在 C 地址",播放器拿到这份清单后,按顺序把每一段下载下来,边下边播。这就是为什么你在浏览器里看视频很流畅,但想直接保存却找不到一个完整的视频文件——因为服务器根本就没有提供完整文件,它只提供了这份清单和一堆碎片。
一个典型的 m3u8 文件长这样:
#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-KEY:METHOD=AES-128,URI="https://example.com/key.key",IV=0x00000000000000000000000000000000 #EXTINF:10.000000, segment_000.ts #EXTINF:10.000000, segment_001.ts #EXTINF:10.000000, segment_002.ts #EXT-X-ENDLIST这里面几个关键标签必须认识:#EXT-X-TARGETDURATION表示每个分片的最大时长,#EXTINF表示下一个分片的实际时长,#EXT-X-KEY就是加密信息,#EXT-X-ENDLIST表示清单结束(说明是点播而非直播)。看懂这几个标签,你就掌握了整个下载流程的钥匙。
1.2 为什么会有 ts 分片这种设计
ts 是 MPEG-TS(Transport Stream)格式的缩写,原本是用于数字电视广播的容器格式。它被 HLS 选中的核心原因是容错性强:每个 ts 分片都是独立可解码的,即使中间某个分片下载失败,也不会影响其他分片的播放。这种设计对直播场景特别友好——服务器可以持续不断地生成新的分片,客户端只需要不断拉取最新的清单即可。
但对下载来说,这种设计就带来了麻烦:你需要把所有分片按顺序下载、按顺序拼接,少一个或者顺序错了,视频就会卡顿、花屏甚至完全无法播放。而且分片数量可能非常多,一个两小时的视频,按每个分片 10 秒计算,就是 720 个 ts 文件,手动下载显然不现实。
1.3 定位 m3u8 地址的几种实用方法
在实际操作中,第一步永远是找到那个 m3u8 地址。浏览器开发者工具是最直接的手段:打开 Network 面板,筛选m3u8或者media,然后刷新页面开始播放,通常就能看到请求。但有几个细节需要注意:
- 有些站点会把 m3u8 地址藏在多层跳转后面,你看到的可能是一个接口返回的 JSON,里面才包含真正的 m3u8 地址。这时候要顺着请求链往上找。
- 有些站点会对 m3u8 请求做 Referer 和 User-Agent 校验,直接复制地址到命令行工具里下载会返回 403。解决办法是在请求头里带上正确的 Referer。
- 移动端页面的 m3u8 地址往往和 PC 端不同,有时候移动端的限制更少,可以尝试切换 User-Agent 来获取。
提示:定位到 m3u8 地址后,先用浏览器直接打开这个地址,确认能看到文本内容再继续。如果浏览器都打不开,说明地址本身有问题或者需要鉴权。
2. AES-128 解密:为什么下载下来的 ts 播放不了
如果你下载完所有 ts 分片,合并之后发现播放器打不开或者只有声音没有画面,大概率是因为这些分片是加密的,而你下载的是密文。AES-128 是 HLS 中最常见的加密方式,理解它的工作原理是解决问题的前提。
2.1 AES-128 在 HLS 中的加密逻辑
HLS 的 AES-128 加密采用的是 CBC 模式。每个 ts 分片在加密前会被填充到 16 字节的整数倍,然后用一个 16 字节的密钥和一个 16 字节的初始向量(IV)进行加密。解密的时候需要同样的密钥和 IV。
密钥从哪里来?就在 m3u8 文件的#EXT-X-KEY标签里。URI指向密钥文件的地址,IV就是初始向量。如果 m3u8 里没有显式指定 IV,那么默认使用分片的序号作为 IV——这一点非常关键,很多解密失败就是因为忽略了 IV 的推导规则。
密钥文件本身通常是一个 16 字节的二进制文件,下载下来用十六进制查看器打开,应该能看到 32 个十六进制字符。如果你下载到的密钥文件是文本格式的,可能需要先做一次编码转换。
2.2 用 ffmpeg 一步到位处理加密流
对于大多数情况,ffmpeg 可以直接处理加密的 m3u8,你不需要手动下载密钥、手动解密。命令大致是这样的:
ffmpeg -headers "Referer: https://example.com/" \ -user_agent "Mozilla/5.0" \ -i "https://example.com/playlist.m3u8" \ -c copy \ -bsf:a aac_adtstoasc \ output.mp4这条命令的核心在于-c copy,它表示直接复制音视频流而不重新编码,速度非常快。-bsf:a aac_adtstoasc是音频比特流过滤器,用于把 AAC 的 ADTS 头转换为 ASC 格式,这是 ts 转 mp4 时经常需要的步骤,不加的话可能出现音频无法播放的问题。
但 ffmpeg 直接处理并不是万能的。我遇到过几种它搞不定的情况:密钥服务器需要特定的请求头才能访问、m3u8 地址是动态生成的只能用一次、分片地址是相对路径且需要特殊拼接。这些情况下就得走手动流程。
2.3 手动解密的完整步骤与参数计算
手动解密听起来复杂,拆开来看其实就几步。假设你已经下载好了所有 ts 分片和密钥文件:
第一步,确认密钥长度。用xxd key.key查看,正常的 AES-128 密钥应该是 16 字节。如果长度不对,说明下载的不是原始密钥文件。
第二步,确定 IV。如果 m3u8 里写了 IV,直接用;如果没写,IV 就是分片序号的 16 字节大端表示。比如第 0 个分片的 IV 是00000000000000000000000000000000,第 1 个是00000000000000000000000000000001,以此类推。
第三步,用 openssl 解密单个分片验证:
openssl aes-128-cbc -d -in segment_000.ts -out segment_000_dec.ts \ -K 你的密钥十六进制 -iv 你的IV十六进制这里有个容易踩的坑:-K和-iv后面跟的都是十六进制字符串,不是文件路径。如果你直接把密钥文件路径填进去,会报错。正确的做法是先用xxd -p key.key把密钥转成十六进制字符串。
第四步,验证解密结果。解密后的 ts 文件应该能被 ffplay 或者 VLC 正常播放。如果播放失败,检查密钥和 IV 是否正确,或者确认加密模式是不是 CBC(极少数情况会用其他模式)。
2.4 解密环节最常见的三个错误
在实际操作中,解密失败的原因高度集中,我把最常见的三个列出来:
| 错误现象 | 根本原因 | 解决办法 |
|---|---|---|
| 解密后花屏、绿屏 | IV 推导错误 | 确认 m3u8 是否显式指定 IV,未指定时用分片序号 |
| openssl 报 bad decrypt | 密钥格式错误 | 用 xxd -p 转十六进制,确认是 16 字节 |
| 部分分片正常部分异常 | 分片序号从非 0 开始 | 检查 EXT-X-MEDIA-SEQUENCE 的值,IV 要加上这个偏移 |
第三个错误特别隐蔽。有些 m3u8 的#EXT-X-MEDIA-SEQUENCE不是 0,这意味着第一个分片的序号不是 0 而是这个值。如果你还用 0 作为起始 IV,前面几个分片就会解密失败。这个细节很多教程都不会提,但实际项目中经常遇到。
3. ts 分片合并:顺序、工具与音画同步问题
所有分片下载并解密完成后,下一步就是把它们合并成一个完整的视频文件。这一步看似简单,但顺序错了、工具选错了,都会导致前功尽弃。
3.1 合并顺序为什么如此重要
ts 分片的合并必须严格按照 m3u8 中列出的顺序进行。这不是"差不多就行"的事情——ts 流内部有连续的时间戳(PTS/DTS),如果顺序错乱,播放器解码时时间戳会跳变,表现为画面突然倒退、卡死或者音画不同步。
那么顺序从哪里来?就是 m3u8 文件里#EXTINF下面那一行行文件名。注意,文件名不一定是数字递增的,有些站点会用时间戳命名,有些会用哈希值。所以千万不要想当然地按文件名排序,一定要按 m3u8 里的出现顺序来。
一个稳妥的做法是先把 m3u8 里的分片地址提取成一个列表文件:
grep -v '^#' playlist.m3u8 | grep -v '^$' > segments.txt然后按这个列表的顺序去下载和合并,就能保证顺序正确。
3.2 三种合并方式的对比与选择
合并 ts 分片有三种主流方式,各有适用场景:
第一种,直接用 cat 命令拼接。这是最简单的方式:
cat segment_*.ts > output.ts但这种方式有两个前提:分片必须是未加密的,且文件名排序必须和实际顺序一致。如果分片是加密的,拼接出来的是密文,没法播放。如果文件名排序不对,顺序就乱了。所以 cat 只适合简单场景。
第二种,用 ffmpeg 的 concat 协议。先创建一个文件列表:
file 'segment_000.ts' file 'segment_001.ts' file 'segment_002.ts'然后执行:
ffmpeg -f concat -safe 0 -i filelist.txt -c copy output.mp4这种方式的好处是能处理路径问题,而且-c copy不重新编码,速度快。-safe 0是必须的,否则 ffmpeg 会因为安全策略拒绝处理绝对路径。
第三种,用 ffmpeg 直接处理 m3u8。如果网络条件允许,直接让 ffmpeg 拉取 m3u8 并输出 mp4,省去手动下载和合并的步骤。这种方式在前面已经讲过,适合能直接访问的情况。
三种方式的对比:
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| cat 拼接 | 最简单、最快 | 无法处理加密、依赖文件名排序 | 未加密且命名规范的分片 |
| concat 协议 | 顺序可控、支持重编码 | 需要手动准备列表 | 已下载到本地的分片 |
| ffmpeg 直拉 | 一步到位 | 依赖网络和鉴权 | 能直接访问 m3u8 的场景 |
3.3 音画不同步的排查思路
合并完成后如果发现音画不同步,排查方向有这么几个:
先确认是不是所有分片都下载完整了。缺分片会导致时间戳断裂,播放器为了补偿就会调整同步策略。用文件大小对比是个简单办法——正常情况下相邻分片的大小应该差不多,如果某个分片明显偏小,很可能下载不完整。
再确认合并顺序。把合并后的文件用 ffprobe 看一下时长,和 m3u8 里所有#EXTINF累加的时长对比,如果差得多,说明顺序或者完整性有问题。
还有一种情况是原始流本身的问题。有些站点的音视频时间戳基准就不一致,这种情况下需要用 ffmpeg 重新封装并做时间戳校正:
ffmpeg -i output.ts -c copy -video_track_timescale 90000 -async 1 fixed.mp4-async 1会让音频流做拉伸对齐,-video_track_timescale统一视频时间基。这两个参数配合使用,能解决大部分轻微的音画不同步。
4. 全链路实操:从零到可播放文件的完整流程
前面把三个核心环节拆开讲了,这一节把它们串起来,走一遍完整流程。我会用一个假设的场景来演示,你可以对照自己的实际情况调整。
4.1 环境准备与工具清单
动手之前,先把工具准备好。这套流程需要的工具不多,但每个都要确认版本:
- ffmpeg:核心工具,用于拉流、合并、转封装。建议用较新版本,老版本对某些 HLS 特性支持不好。Windows 用户下载 essentials 版本即可,Linux 用户直接用包管理器安装。
- openssl:用于手动解密验证。Linux 和 macOS 自带,Windows 可以用 Git Bash 里带的版本。
- curl 或 wget:用于下载分片和密钥文件。curl 对自定义请求头支持更好,推荐优先用 curl。
- 一个文本编辑器:用于查看和编辑 m3u8、生成文件列表。
注意:ffmpeg 的安装路径最好加入系统环境变量,否则每次都要输入完整路径,很麻烦。Windows 下安装完成后在命令行输入
ffmpeg -version验证,能输出版本信息就说明配置成功。
4.2 抓取 m3u8 与分片下载脚本
假设你已经通过开发者工具拿到了 m3u8 地址,并且确认这个地址需要带 Referer 才能访问。第一步是把这个 m3u8 下载到本地:
curl -H "Referer: https://example.com/" \ -H "User-Agent: Mozilla/5.0" \ -o playlist.m3u8 \ "https://example.com/playlist.m3u8"下载完成后打开看看,确认里面有#EXTINF和分片地址。如果分片地址是相对路径,需要根据 m3u8 的 URL 拼接出完整地址。
接下来批量下载分片。写一个简单的循环脚本:
base_url="https://example.com/path/" while read -r line; do case "$line" in \#*|"") continue ;; esac curl -H "Referer: https://example.com/" \ -o "$line" \ "${base_url}${line}" done < playlist.m3u8这个脚本会跳过注释行和空行,只下载真正的分片文件。如果你的分片地址是完整的 URL,把${base_url}${line}改成$line即可。
下载过程中要注意观察是否有失败的分片。curl 默认失败不会重试,建议加上--retry 3参数。下载完成后用ls -la *.ts | wc -l统计数量,和 m3u8 里的分片数对比,确认没有遗漏。
4.3 解密与合并的串联执行
如果分片是加密的,在合并之前要先解密。手动逐个解密效率太低,写个循环:
key_hex=$(xxd -p key.key | tr -d '\n') iv_hex="00000000000000000000000000000000" for f in *.ts; do openssl aes-128-cbc -d -in "$f" -out "dec_$f" \ -K "$key_hex" -iv "$iv_hex" done注意这里的 IV 是固定的,仅适用于所有分片使用同一个 IV 的情况。如果 IV 随分片序号变化,需要在循环里动态计算。计算方法是把分片序号转成 16 字节的十六进制,比如序号 5 对应00000000000000000000000000000005。
解密完成后,生成合并列表并执行合并:
grep -v '^#' playlist.m3u8 | grep -v '^$' | sed 's/^/file /' > filelist.txt ffmpeg -f concat -safe 0 -i filelist.txt -c copy output.mp4这里有个细节:如果解密后的文件名加了dec_前缀,filelist.txt 里的文件名也要对应修改。可以用 sed 批量替换:
sed -i 's/^file /file dec_/' filelist.txt4.4 成品验证与常见故障速查
合并完成后,别急着删原始文件,先用 ffprobe 检查一下成品:
ffprobe -v error -show_format -show_streams output.mp4重点看几个信息:时长是否和预期一致、视频流和音频流是否都存在、编码格式是否正常。如果时长明显偏短,说明有分片没合并进去;如果没有音频流,可能是音频编码不被 mp4 容器支持,需要转码。
播放验证建议用 VLC 或 PotPlayer,这两个播放器对 ts 流的兼容性最好。如果播放器能正常播放且拖动进度条不卡顿,基本就没问题了。
常见故障速查表:
| 故障现象 | 可能原因 | 排查方向 |
|---|---|---|
| 无法播放 | 分片未解密 | 检查 m3u8 是否有 EXT-X-KEY 标签 |
| 只有前几秒能播 | 分片顺序错误 | 核对 filelist.txt 顺序 |
| 花屏、马赛克 | 分片下载不完整 | 对比文件大小,重新下载异常分片 |
| 音画不同步 | 时间戳问题 | 用 -async 1 重新封装 |
| 播放器报格式错误 | 容器不兼容 | 尝试输出为 mkv 或重新编码 |
5. 那些教程不会告诉你的实战经验
前面讲的都是标准流程,但实际操作中总会遇到各种意外。这一节分享几个我在多次处理这类任务后总结出来的经验,都是踩过坑才明白的。
5.1 关于请求头与鉴权的处理技巧
很多站点的 m3u8 和分片请求都需要正确的 Referer 和 User-Agent,但具体需要哪些头,不同站点不一样。我的做法是:先在浏览器开发者工具里找到成功的请求,右键选择"Copy as cURL",然后把这条命令粘贴到终端里执行一次,确认能拿到数据。接着从这条命令里提取出所有请求头,应用到自己的脚本里。
这样做的好处是不会遗漏任何必要的头。有些站点会校验Origin、Accept甚至自定义头,光靠猜是很难猜全的。另外要注意 Cookie 的处理,如果站点依赖登录态,需要把 Cookie 也带上。curl 的-b参数可以指定 Cookie 字符串。
还有一个细节:部分站点的密钥请求和分片请求需要的请求头不一样。密钥请求可能校验更严格。如果分片能下载但密钥下载失败,单独给密钥请求加上完整的请求头试试。
5.2 大文件处理时的资源管理
一个两小时的视频,分片数量可能上千,总体积可能好几个 GB。这种情况下有几个资源管理上的注意事项:
磁盘空间要提前确认。下载分片、解密后的副本、合并后的成品,三个阶段都会占用空间,峰值可能是最终文件的三倍左右。如果磁盘空间紧张,可以在解密后删除原始加密分片,合并后删除解密分片。
下载并发要控制。虽然并发下载能加快速度,但并发太高容易被服务器限流甚至封 IP。我的经验是并发数控制在 4 到 8 之间比较稳妥,既能提速又不容易触发风控。用xargs -P可以方便地控制并发:
cat segments.txt | xargs -P 4 -I {} curl -H "Referer: https://example.com/" -O "{}"内存占用也要注意。用 ffmpeg 合并时,如果分片很多,concat 协议会一次性读取列表,内存占用不大,但如果用其他方式把整个文件读进内存处理,就可能出问题。所以优先用 ffmpeg 的流式处理方式。
5.3 直播流与点播流的处理差异
前面讲的流程主要针对点播(VOD)场景,也就是 m3u8 里有#EXT-X-ENDLIST标签的情况。如果是直播流,m3u8 会不断更新,没有 ENDLIST 标签,处理方式完全不同。
直播流的下载需要持续轮询 m3u8,每次拉取新的分片。ffmpeg 可以直接处理直播流:
ffmpeg -i "https://example.com/live.m3u8" -c copy -t 3600 live_output.mp4-t 3600表示录制 3600 秒后停止。如果不加这个参数,ffmpeg 会一直录下去直到手动中断。
直播流还有个特点是分片会被服务器定期清理,所以下载速度必须跟得上生成速度,否则会丢分片。这种情况下建议直接用 ffmpeg 拉流,不要手动下载分片,因为手动流程的延迟太高,很容易跟不上。
5.4 批量处理与自动化思路
如果你需要经常处理这类任务,把流程脚本化会省很多事。我的做法是写一个 shell 脚本,接受 m3u8 地址和输出文件名作为参数,自动完成下载、解密、合并、验证的全流程。
脚本里要处理几个关键点:自动检测是否加密(检查 m3u8 里有没有 EXT-X-KEY)、自动推导 IV(有显式 IV 用显式的,没有就用序号)、自动生成合并列表、自动清理临时文件。这样每次只需要一条命令就能搞定。
不过自动化也有边界。遇到需要登录、需要特殊请求头、或者 m3u8 地址是动态生成的情况,还是得手动介入。我的建议是:把能自动化的部分自动化,把需要判断的部分留给自己,不要追求 100% 全自动,那样反而容易在异常情况下出错。
提示:脚本里加上错误处理和日志输出,每次执行后检查日志,确认没有异常。特别是分片下载环节,一定要验证下载数量是否和 m3u8 里的分片数一致。
6. 关于工具选型与方案取舍的一些个人看法
聊完了具体操作,最后说说工具和方案选择上的思考。这部分没有标准答案,更多是我自己的经验判断。
ffmpeg 几乎是这个领域绕不开的工具,它的优势在于功能全面、社区活跃、文档丰富。但 ffmpeg 也不是万能的,它的错误提示有时候很晦涩,遇到问题需要一定的经验才能定位。我的建议是把 ffmpeg 当成主力工具,但同时掌握手动流程,这样在 ffmpeg 搞不定的时候有备选方案。
手动流程的价值在于可控性。每一步你都知道发生了什么,出了问题能精确定位到是下载环节、解密环节还是合并环节。而 ffmpeg 一步到位虽然方便,但出错时你很难判断问题出在哪。所以我的习惯是:先用 ffmpeg 试一次,如果成功就省事了;如果失败,就切换到手动流程,逐步排查。
关于解密工具,openssl 是最通用的选择,几乎任何环境都有。但如果你需要处理大量分片,openssl 的启动开销会比较明显。这种情况下可以考虑用 Python 的 cryptography 库写个批量解密脚本,性能会好很多。不过对于偶尔处理一次的场景,openssl 完全够用,没必要为了性能去折腾。
最后说一个心态上的建议:这类任务本质上是在和服务器端的各种限制做博弈,遇到失败是常态,不要指望一次成功。保持耐心,逐步排查,把每个环节都验证清楚,最终一定能拿到可播放的文件。我在最开始做这类任务的时候,一个视频折腾了大半天,现在流程熟练了,大部分情况十几分钟就能搞定。经验积累的过程本身就是价值。