ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

m3u8视频下载全攻略:从HLS协议原理到在线工具实操与避坑指南

m3u8视频下载全攻略:从HLS协议原理到在线工具实操与避坑指南

简介:这是一款面向流媒体视频下载需求的 m3u8 在线下载与提取工具,适合需要将 HLS 流媒体保存为本地视频的普通用户、视频运营人员及前端开发者。工具围绕 M3U8 清单解析、TS 分片多线程下载和自动合并设计,同时提供网页端交互界面,部署后可通过浏览器输入链接完成视频抓取。压缩包共 47 个文件,大小 1.68MB,涵盖 15 个 JavaScript 脚本(含解析、下载、播放与解密模块)、7 个 HTML 页面、4 个 CSS 样式、2 个文本说明、字体图标、多张界面预览图,以及 PHP、配置与安全验证文件,目录结构接近一个完整可运行的网站项目。目前已有 5939 人浏览学习。借助此包可快速搭建 m3u8 下载服务,既能用于日常视频离线缓存,也可作为学习 m3u8 协议、前端播放器集成和下载流程设计的参考案例。

1. 为什么需要 m3u8 下载工具:你看到的视频其实是一堆 ts 碎片

A同学想离线缓存一门录播课,右键另存为灰掉,录屏又卡又糊,最后在开发者工具的 Network 面板里翻出几十条index.m3u8和几百个.ts请求,才明白自己看的不是一个完整的视频文件,而是一份播放清单加一堆分片。这就是 HLS 流媒体协议的工作方式:浏览器边下边播,绝不把分片拼成一个文件。m3u8 下载工具解决的就是这个诉求——把索引、分片、密钥按顺序拉回本地,再合并、转码成一个能随意拷贝的 mp4。这篇笔记从协议原理讲到具体操作,再列我在真实下载里踩过的坑,新手能照步骤跑通,熟手能直接看参数和边界条件。

2. 先看懂 m3e8 再动手:索引结构、加密分支与工具选型逻辑

2.1 m3u8 到底记录了什么:从播放列表到分片清单

m3u8 本质上是一个 UTF-8 编码的文本文件,里面按顺序写着视频分片的地址和播放时长。打开任何一个典型文件,内容都很直白:

#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.0, https://example.com/segment/0001.ts #EXTINF:10.0, https://example.com/segment/0002.ts #EXTINF:8.5, https://example.com/segment/0003.ts #EXT-X-ENDLIST

#EXTINF后面跟的是分片时长,单位是秒;下一行就是这个分片的下载地址。整份文件就是一张“播放清单”。下载工具读取这份清单后,会依次请求每一个分片,按顺序把二进制的 ts 数据拼接起来。这就像手里拿着一张零件清单,把每个零件依次拧在一起,最终得到一台整机。

还要注意#EXT-X-ENDLIST这一行。有它,表示这是点播(VOD)流,分片数量固定,下载可以收敛;没有它,说明是直播流,分片会持续追加,工具需要额外做“跟随拉取”的逻辑。在线下载工具界面里如果让你选“点播 / 直播”,多数情况下选点播就能正常结束任务。

2.2 加密与非加密流:EXT-X-KEY 是你绕不开的分支点

很多 m3u8 文件里会多出这样几行:

#EXT-X-KEY:METHOD=AES-128,URI="https://example.com/key/enc.key",IV=0x1234567890abcdef1234567890abcdef

看到EXT-X-KEY,就说明分片被加密了。浏览器播放时会在内存里解密,所以你能正常看,但直接下载下来的 ts 文件是无法播放的乱码。要合并出一个能用的 mp4,下载工具必须先请求这个URI指向的密钥文件,再配合IV(初始向量)对每个分片做 AES-128 解密,然后才能进入合并环节。

METHOD还有可能写成SAMPLE-AES,常见于部分 DRM 保护的流,处理逻辑比AES-128更复杂。遇到SAMPLE-AES,在线工具通常不支持完整解密,因为涉及版权保护和更底层的解码器操作,不要抱太大期望。绝大多数录播、课程视频走的是AES-128,这也是工具能处理的最主流分支。

2.3 在线工具 vs ffmpeg 命令行:我为什么留下这个方案

有人会问:直接用 ffmpeg 不就行了?一条命令ffmpeg -i https://example.com/index.m3u8 -c copy out.mp4确实能跑通,但现实往往没那么简单。很多场景下,m3u8 地址需要带 Referer、User-Agent、Cookie 才能访问,ffmpeg 虽然能用-headers传参,遇到重定向、鉴权中间页、自定义请求头嵌套时,命令行会变得冗长且极易写错,排错成本不低。

我保留在线工具的原因是:它把“下载分片 + 解密 + 合并 + 转码”封装成了可视化流程,密钥注入、请求头配置都有独立字段,失败了能直接看到在哪一步断掉。尤其是手上只有浏览器、不方便装命令行环境的时候,在线工具是成本最低的路径。当然,ffmpeg 在批量处理和可控性上更强,我在后面第 6 章会讲两者如何配合。

3. 在线下载 m3u8 视频的完整操作:从开发者工具取地址到生成 MP4

3.1 第一步:在 Network 面板里拿到真实 m3u8 地址

打开视频播放页面,按 F12 进入开发者工具,切到 Network 面板,刷新页面让视频重新加载,然后在过滤框里输入m3u8。正常情况下能看到一条或几条名称类似index.m3u8、playlist.m3u8的记录,点击它,右侧 Preview 标签页会直接展示分片列表。

拿地址时有个细节:部分站点会把 m3u8 地址包在blob:里,直接blob:https://...这种是无法下载的。这时候要看发起的请求,找到真正返回 m3u8 文本的那个 HTTP 请求,复制它的完整 URL。常见做法是右键这条请求,选择 Copy → Copy Link Address,得到的是带签名参数的完整地址,这类地址通常带?token=xxx&expires=xxx后缀,复制时一个字符都不能少,少一个参数就可能 403。

3.2 第二步:把 URL、请求头和密钥填进下载表单

拿到地址后,回到在线工具页面,填入三样东西:m3u8 地址、请求头信息、输出格式。

请求头是最容易漏掉的一步。工具界面上一般有一个“自定义请求头”或“Headers”输入区,格式类似 JSON:

{ "Referer": "https://example.com/video/1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0", "Cookie": "sessionid=abc123" }

这三个字段里,Referer决定了服务器是否认为你是从正常页面发起的请求,User-Agent告诉服务器你用的是浏览器而不是脚本,Cookie则用于维持登录态。很多视频站的鉴权逻辑是三者同时校验,只要缺一个,服务器就会返回 403 或者直接拒绝分片请求。

填写时不要用自己的浏览器 UA 简写,尽量从开发者工具里完整复制。因为有些服务器会检查浏览器指纹,UA 不对也会被拒。

3.3 第三步:等待合并转码,理解 TS→MP4 的转换逻辑

点击开始后,工具会先拉取 m3u8 索引,然后逐个下载分片。下载完所有 ts 后,工具开始做合并。这里要说明的是:ts 文件本身是 MPEG-TS 封装,里面可能包含 H.264 视频流和 AAC 音频流;合并操作只是把这堆二进制块按顺序拼成一个大的 ts 文件,然后通过转封装(remux)把视频轨和音频轨重新封装进 MP4 容器,而不做重新编码。

转封装速度远快于重新编码,因为像素数据没有发生变化,只是换了“包装盒”。所以工具显示“合并中”时,CPU 占用率不会特别高,硬盘读写才是主要瓶颈。如果工具在合并时做了转码选项,耗时就会显著增加。日常下载录播课时,选“直接合并 / 复制流”就能得到可播放的 mp4,没必要做重复编码。

3.4 关键参数说明表

参数/选项作用建议取值
m3u8 URL分片索引地址完整复制,保留签名参数
Referer告诉服务器来源页面与播放页 URL 保持一致
User-Agent模拟浏览器身份从开发者工具完整复制
Cookie维持登录态按站点实际情况填写
输出格式合并后的封装格式一般选 mp4
分片并发数同时下载多少个 ts5~10,过高容易触发限流
密钥处理自动/手动注入密钥默认自动,失败时手动填

分片并发数是一个容易忽略的选项。并发太高,服务器会认为你在 CC 攻击,轻则限速,重则封 IP;并发太低,几百个分片要等很久。我一般设 8,兼顾速度和安全性。

4. 加密 m3u8 的 Decrypt 流程:密钥、IV 与自定义请求头

4.1 判定加密类型:看到 METHOD=AES-128 后要做什么

先打开 m3u8 文件,搜索EXT-X-KEY字段。前面已经讲过,METHOD=AES-128是加密流的主流形态。它意味着每个分片在 HTTP 传输过程中是被加密的密文,但加密算法本身是标准 AES 块密码,只要有密钥和 IV,就能恢复原始内容。

工具通常会在解析索引时自动发现EXT-X-KEY,并尝试请求密钥文件。如果密钥地址是相对路径,比如/key/enc.key,工具会自动拼上 m3u8 所在域名。这一步手动做的时候容易漏:密钥 URL 拼接错误是解密失败的头号原因。在线工具的日志区一般会打印“Found KEY: https://...”,你确认一下这个地址能不能在浏览器里直接打开。如果打不开,说明密钥接口同样需要鉴权请求头。

4.2 注入密钥与 IV:常见做法与参数写法

有的工具界面上有“密钥 / Key”和“IV”两个手动输入框。当自动获取失败,或者密钥地址会过期时,手动注入是唯一出路。IV 常见格式是0x开头的十六进制串,长度 32 位(16 字节),和 AES 块大小严格对应。

// 一个典型的手动密钥配置对象,用于填入在线工具的密钥输入区 const keyConfig = { method: "AES-128", keyUri: "https://example.com/keys/enc.key", iv: "0x9f7e6d5c4b3a29180716253443526170" };

这段配置的逻辑是:method指定解密算法;keyUri告诉工具去哪里拿密钥本体;iv是与第一个分片对应的初始向量。iv取不到时,很多工具默认会用分片的media sequence自动推导 IV,这与标准规范一致——部分实现会忽略 IV 字段,按序列号算 IV。

填完这组参数后,工具会先下载 key 文件(通常 16 字节),再用 AES-CBC 模式逐个解密 ts。命令行下,对应的 ffmpeg 参数是-decryption_key,但在线工具里只需要把keyUri和iv填对即可。

4.3 加密流下载失败时,优先检查请求头而不是密钥

这是我实际下载中反复遇到的一个情况:密钥地址明明能在浏览器里打开,但工具解密失败,日志却显示“decrypt error”。排查时第一反应是去改密钥值,折腾半天没效果。后来我注意到,工具请求密钥时用的是一组与分片不同的请求头,而密钥接口恰好校验了Referer或Cookie,导致密钥返回的不是 200 而是 403 页面。工具拿到 403 页面仍尝试按 16 字节解析,自然解密失败。

所以我的排查顺序是:先看工具日志里密钥请求的 HTTP 状态码,再看返回内容长度是不是 16 字节,最后才是密钥内容本身。如果你看到“decrypt error”并且日志里没有明确写密钥获取失败,先确认工具是否把自定义请求头同时应用到了密钥请求上。部分工具默认只把请求头用于分片请求,密钥请求走的是空 Headers,这是最容易翻车的地方。

5. 避坑排查:下载 m3u8 时我遇到的五个实际问题

5.1 分片进度卡在 99%:超时重试机制与线程数设置

现象:进度条走到 99% 后长时间不动,日志显示大量分片重试,最后任务失败。

原因:尾部几个分片体积较大,下载耗时超过了工具默认的单分片超时时间,或者服务器对高频请求做了临时限流。99% 这个位置特别容易让人误以为是合并阶段卡住,实际上还在下载最后一个分片。

解决:把并发数从 8 降到 4,同时把单分片超时时间从默认的 10 秒调大到 30 秒。工具设置里找到“超时”或“Timeout”选项,改成 30 再重试。如果仍然失败,复制失败分片的完整 URL,粘贴到浏览器地址栏手动下载一次,确认是不是这个分片本身已经损坏。

5.2 合并后没有声音:音频轨可能是另一条 m3u8

现象:mp4 正常生成,画面流畅,但全程无声。

原因:部分播放器站点把视频轨和音频轨拆成两条独立的 m3u8 流。视频 m3u8 里只有纯视频分片,音频在另一个audio.m3u8中。只下载视频索引再合并,产物自然没有音轨。

解决:回到 Network 面板,过滤m3u8,看是不是同时存在.ts请求指向不同域名或不同目录。如果看到两组分片,一组是video/路径、一组是audio/路径,说明有分离流。下载时需要同时填入视频 m3u8 和音频 m3u8,工具会把两条流合并进同一 mp4。部分在线工具支持“附加音轨 URL”字段,找不到就手动下载两条流,再用ffmpeg -i video.mp4 -i audio.m4a -c copy final.mp4合并。

5.3 报 403 Forbidden:Referer 和 User-Agent 不匹配

现象:分片请求返回 403,但浏览器里同一个 URL 可以正常播放。

原因:服务器校验的是请求头组合,而不是单个字段。有一种典型情况是:Referer 填对了,但 User-Agent 是空的或者是 Python 的默认 UA,两者不匹配导致服务端拒绝。

解决:严格从浏览器复制 UA,完整粘贴到工具里。检查 UA 里是否包含多个空格换行,有些浏览器扩展会导致 UA 被改写。另一个容易漏的点是Accept头,部分站点要求Accept: */*,在线工具默认可能不带这字段,手动补上即可。

5.4 下载完成但时长只有几秒:你抓到了播放列表的“分页”而非完整索引

现象:下载过程正常,合并出的 mp4 播放时长只有 5~8 秒,但原视频是一小时。

原因:有些站点做分页加载,首屏只给前几条分片,后续分片通过 JS 异步追加或请求一个带start_seq的扩展 m3u8。你复制的是首屏索引,里面只有演示用的几条分片。

解决:在 Network 面板里把过滤条件改成m3u8,刷新页面后不要立即暂停,等直播流跑十几秒,观察是否出现第二个 m3u8 请求,且它的#EXTINF列表数量明显更多。取最后一条。另外,手动打开复制到的 m3u8,数一数#EXTINF出现次数,如果只有不到 10 条,基本可以判定是分页列表。

5.5 输出是 .ts 而不是 .mp4:搞清合并与转码的差异

现象:任务显示“合并完成”,但输出文件后缀是.ts,一些播放器不认。

原因:工具默认只用“复制流”模式做拼接,不进行转封装。ts 封装和 mp4 封装的流格式不完全兼容,工具判断目标格式时可能选择了“保持原样”。

解决:在输出格式下拉框里明确选择 mp4。如果工具没有 mp4 选项,可以用一行 ffmpeg 补救:

ffmpeg -i input.ts -c copy -movflags +faststart output.mp4

-c copy不做重新编码,只变换容器,速度很快;-movflags +faststart会把 moov 元数据移到文件头部,保证在网页播放器里拖进度条不卡顿。没有这一步,有些播放器要等整个文件下载完才能拖进度条。

6. 进阶用法:批量下载多个 m3u8 并自动校验完整性的脚本思路

6.1 用脚本管理多个任务的下载流程

单个视频用在线工具够用,但遇到课程合集动辄几十集,手动粘贴 URL 会把人逼疯。我的习惯是在本地跑一个 Python 脚本,把 m3u8 地址统一交给 ffmpeg,在线工具只在需要手动注入密钥或特殊请求头时才介入。脚本逻辑很简单:

import subprocess import json tasks = [ {"name": "chapter01", "url": "https://example.com/01/index.m3u8"}, {"name": "chapter02", "url": "https://example.com/02/index.m3u8"} ] headers = "Referer: https://example.com/course\nUser-Agent: Mozilla/5.0" for task in tasks: cmd = [ "ffmpeg", "-y", "-headers", headers, "-i", task["url"], "-c", "copy", f"{task['name']}.mp4" ] result = subprocess.run(cmd, capture_output=True, text=True) print(f"{task['name']} finished: {result.returncode}")

这段脚本把每个任务的头信息统一注入,循环调用 ffmpeg 下载合并。-y是当场覆盖同名文件,-c copy跳过重新编码。跑完一遍后,检查returncode,如果是0说明 ffmpeg 正常退出;非零则需要去看stderr里的具体报错。

6.2 校验输出文件:时长对比、ts 数量与 ffprobe

下载完并不等于下载对,时长差十秒这种问题脚本发现不了。我的验证习惯是:下载前先统计 m3u8 里#EXTINF的总时长,下载后用 ffprobe 读取实际时长,两者差值在 1 秒内视为正常。

# 读取本地 mp4 的实际时长(单位:秒) ffprobe -v quiet -show_entries format=duration -of json output.mp4

这个命令输出的 JSON 里能拿到duration字段。把它和从 m3u8 计算出的理论时长做对比,差异超过 1.5 秒就标记为重试。另一点是检查文件内是否包含音频轨:ffprobe -show_streams里出现codec_type=audio才完整。我曾在批量下载后只抽检了画面,结果一半文件没有音轨,全部重新来过。

从那以后,我每次跑完批量任务都会强制把所有输出文件过一遍 ffprobe,把时长偏差大于 1.5 秒或缺少音频轨的文件挑出来重新下。这一改,再也没有交付过半成品。希望这套排查思路能帮到你——拿到工具只是第一步,知道它每一步在做什么、失败时从哪查起,才是能稳定复现的落地能力。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进