☰
Python多进程高效下载m3u8加密视频:从解析到合并全攻略
2026/10/15 17:17:43 网站建设 项目流程

做下载工具时,m3u8 算是最常见的流媒体格式之一。无论是 HLS 直播回放、在线课程视频,还是自己服务器上的视频资源,一套可靠的 m3u8 下载流程都是基本功。今天想和大家拆解的是:怎么用 Python 多进程把 m3u8 加密或非加密视频稳定地拉下来,同时把速度尽可能拉满。

网上类似的脚本很多,但大多停留在“单线程能跑就行”的阶段,遇到几千个分片的视频就原形毕露:速度上不去、超时一堆、内存还容易爆。这篇文章不打算只给一段能跑的代码,而是把拆解 m3u8、设计多进程任务、处理失败重试、合并分片这几个环节全部讲清楚,每一步都解释为什么这么做。适合刚接触流媒体下载、对 Python 并发编程有一定基础但没系统做过下载器的读者,也适合想把自己那个“能用但不好用”的脚本重写一遍的朋友。

先提醒一句:下面所有操作都只适用于你有权访问和保存的内容,比如自己上传的课程、已购买的视频、或者官方允许离线观看的资源。请勿用于任何侵权场景,技术本身没有立场,使用方式自己把握。

1. 先搞清楚 m3u8 到底是什么:分片拉流的原理与下载瓶颈

很多人写过 m3u8 下载脚本,但没认真看过 m3u8 文件内容长什么样。这不怪大家,毕竟它本质就是一个文本文件,光看内容看不出什么门道。可如果不懂它的结构,后面做多进程并发就是瞎忙活。

1.1 m3u8 文件里的关键标签逐行解读

m3u8 是 HLS(HTTP Live Streaming)协议的索引文件,标准的 UTF-8 文本格式。打开一个典型文件,内容大概是这样的:

#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:4.000000, hls segment https://example.com/path/segment1.ts #EXTINF:4.000000, hls segment https://example.com/path/segment2.ts ... #EXT-X-ENDLIST

关键标签只有几个:

  • #EXTM3U:文件头,表示这是 m3u8。
  • #EXT-X-TARGETDURATION:每个分片的最大时长,单位秒。可以用来估算总时长。
  • #EXTINF:紧跟其后那一行分片的持续时间,单位秒。逗号后面是描述信息,通常可以忽略。
  • #EXT-X-KEY:加密分片的关键标签,后面会单独讲。
  • #EXT-X-ENDLIST:文件结束标记。没有这个标签,说明是直播流,理论上可以一直拉下去。

还有#EXT-X-DISCONTINUITY、#EXT-X-MAP这类较少见的标签,分别表示分片不连续和音频初始化段。下载器第一版可以先不处理,等遇到后再针对性兼容。

把 m3u8 的内容梳理清楚,你就知道一个下载器最核心的骨架是什么:读文件、找分片地址、并发拉取、按顺序合并。

1.2 为什么单线程下载那么慢:并发瓶颈不在带宽而在串行等待

你可能会想:一个视频几百 MB,带宽够的话,单线程下载也没多慢吧?实测下来完全不是这么回事。我测过一个大约 40 分钟的课程视频,时长 40:11,分片 582 个,单线程逐个下载耗时 1 分 40 秒左右。看起来还行?那换成 2 小时的电影,分片 1800 个,单线程可能要 8 分钟以上。

问题出在串行等待上。每个分片虽然只有几百 KB,但从发起请求到数据完全落地,中间经历 TCP 握手、服务端响应、数据传输、连接关闭,一次完整的请求可能要 200 到 500 毫秒。其中真正传输数据的时间只占一小部分,大部分时间都在等待。你每发出一个请求,都要等它全部完成才发下一个,网络链路就像单车道,再宽的带宽也跑不满。

多进程解决的就是这个“等待”问题。把分片列表拆成 N 个任务,同时开 N 个进程去下载,每个进程独立走自己的网络请求,相当于把单车道变成多车道。这里的 N 一般取 CPU 核心数或略高一点,并不是越大越好,后面我会给实测数据。

1.3 加密与非加密视频的核心差异:多了一段 AES-128 解密

非加密的 m3u8 下起来最省心,拿到分片地址直接下载、合并、完事。加密的 m3u8 会多一个EXT-X-KEY标签,常见长这样:

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

意思是:分片用 AES-128 加密过,密钥在key.key这个文件里,初始向量是 IV 指定的 16 字节。下载完分片之后,需要用密钥对每个分片做 AES-128-CBC 解密,才能得到原始的 TS 数据。

很多教程对这部分一笔带过,要么直接让你调 FFmpeg 一把梭,要么贴个 openssl 命令就完事。我自己的经验是:能调 FFmpeg 就别自己写解密逻辑,因为key.key的获取、IV 的解析、分片边界对齐这些小细节容易踩坑,而 FFmpeg 把这一切都封装好了。

2. 加密流的处理边界与正确姿势:不逆向也不破解

关于“加密视频下载”,我必须先把边界划清楚。m3u8 的加密设计本身不是用来防止你离线观看的版权保护,更多是为了防止分片内容被随意篡改、保证传输完整性。所以处理加密流时,我们用的都是 HLS 规范里公开的标准流程,不是什么逆向破解。

2.1 什么情况下才允许你下载加密流

判断标准很简单:这个视频你有没有权限离线观看?

  • 自己在服务器上搭建的 HLS 流,密钥和分片都是自己的,随便怎么折腾。
  • 自己购买过的在线课程,平台明确支持离线缓存,那下载也是合理使用。
  • 平台没有离线功能、也没有授权你保存,这类就别碰了。技术手段上再容易,也不是你该做的事。

我这个项目测试用的是自己在某实验室服务器上部署的 HLS 视频,密钥故意设置成 AES-128 加密,方便跑通整套流程。千万不要拿别人平台的加密流来练手,一是侵权,二是很多平台的密钥接口还有 Referer 校验、Token 时效,也不值得为这个去折腾。

2.2 AES-128 加密流的正确下载方式:结合 FFmpeg 处理

我推荐的做法是:先下载密钥文件,再下载分片,然后交给 FFmpeg 解密合并,而不是自己写解密代码。FFmpeg 对EXT-X-KEY支持得非常好,你只需要把媒体文件重新封装一下就能拿到明文视频。

实际项目中我用的是两步方案:

第一步,解析 m3u8 时把EXT-X-KEY里的密钥地址提取出来,单独下载保存。第二步,把所有分片按顺序写入一个文本清单,然后用 FFmpeg 读取这个清单和密钥文件,解密并合并成最终的 MP4。

有人可能会问:既然最后都要交给 FFmpeg,那为什么不直接用 FFmpeg 拉 m3u8 链接一次搞定?因为在线直连下载对网络稳定性要求高,中途断了就得从头开始;而且 FFmpeg 默认是单线程拉分片,速度并不理想。先把分片用多进程拉回本地,再让 FFmpeg 处理本地文件,断点续传和并发速度都能兼顾。

3. 多进程下载器的整体设计:任务模型、进程池和解耦思路

写下载器最忌讳一上来就写代码,先把任务模型想清楚。我第一版就是边写边加功能,结果进程池、队列、下载逻辑全揉在一个文件里,排起错来想哭。第二版把功能拆成四个模块:解析模块、密钥处理模块、下载调度模块、合并模块。模块之间通过简单的数据结构传参,互不干扰。

3.1 模块划分:每个模块只干一件事

完整的项目结构大致是这样:

m3u8_downloader/ ├── parser.py # 解析 m3u8,提取分片列表和密钥信息 ├── downloader.py # 单个分片的下载函数,带重试 ├── scheduler.py # 多进程调度,管理进程池和任务分发 ├── merger.py # 合并分片,调用 FFmpeg └── main.py # 入口,串联完整流程

这五个文件分工明确。parser.py只负责把 m3u8 文本转换成 Python 字典,比如{"segments": ["url1", "url2"], "key": {"method": "AES-128", "uri": "key.key", "iv": "0x..."}}。downloader.py只负责下载一个分片到本地,不管它在整个视频中的顺序。scheduler.py拿到分片列表后,用multiprocessing.Pool分配任务。merger.py等所有分片下载完成后,按序号合并。

这种拆分最大的好处是方便测试。解析逻辑有问题,单测parser.py就够了;下载速度不理想,单独优化downloader.py,不用动其他模块。“低耦合”这个词在教科书上看着抽象,真写一个项目你就明白了。

3.2 进程池的调度模型:任务均分还是动态分配

多进程调度有两条路:一是把分片列表均分成 N 份,每个进程拿一份;二是用Pool.map或Pool.imap_unordered动态分配,哪个进程空闲了就从任务队列里取下一个分片。

实测下来,第二种方式明显更优。原因很简单:分片下载耗时不是均匀的,有的分片 100 毫秒,有的分片因为服务端波动要 800 毫秒。均分任务的话,某个进程可能分到一堆慢分片,其他进程早就干完了等着它,总体速度就被拉低。动态分配则天然解决了负载不均衡的问题。Python 的multiprocessing.Pool自带的imap_unordered就能实现动态分配,用起来也简单。

3.3 为什么选多进程而不是多线程:GIL 和数据隔离

标题里写了“多进程”,这里补充一下选型的理由。Python 的多线程受 GIL 限制,同一时刻只有一个线程在执行 Python 字节码。网络下载的大部分时间其实是阻塞在 socket 等待上,GIL 会释放,所以多线程也不是不能用。但多线程有一个隐患:共享同一个进程的全局状态,一旦某个线程崩了或者占用了异常多的资源,排查起来比较麻烦。

多进程则每个进程有独立的解释器和内存空间,互不干扰。某个分片下载失败导致子进程异常退出,不会影响其他进程。而且我们这里主要追求的是并发网络请求数量,多进程能轻松做到进程数 × 每个进程内的连接数,足够跑满大多数带宽了。

4. 核心代码拆解:从解析 m3u8 到并发拉片

设计讲完,进入代码环节。下面这段代码在算法上没有太高深的东西,重在把前面讲的设计落地,并且把我在实际运行中踩过的坑都标注出来。

4.1 解析 m3u8 并提取分片与密钥

解析最简单的方式是逐行扫描,遇到#EXTINF就读取下一行作为分片地址。注意地址可能是相对路径,需要和 m3u8 的 base URL 拼接。

from urllib.parse import urljoin def parse_m3u8(content: str, base_url: str) -> dict: lines = content.strip().splitlines() segments = [] key_info = None for i, line in enumerate(lines): line = line.strip() if line.startswith("#EXT-X-KEY"): method = "" uri = "" iv = None for part in line.split(",")[1:]: key, _, value = part.partition("=") if key == "METHOD": method = value elif key == "URI": uri = value.strip('"') elif key == "IV": iv = value if method == "AES-128" and uri: key_info = { "method": method, "uri": urljoin(base_url, uri), "iv": iv, } elif line.startswith("#EXTINF"): # 下一行是分片地址,但可能有多行注释,需要跳过 j = i + 1 while j < len(lines) and lines[j].startswith("#"): j += 1 if j < len(lines): segments.append(urljoin(base_url, lines[j].strip())) return {"segments": segments, "key": key_info}

这个解析函数有三个细节值得注意。第一,#EXT-X-KEY不一定出现在所有分片之前,有的流中间会切换密钥,这里先处理最常见的一种情况,即整个视频只有一个密钥。第二,分片地址可能不在#EXTINF的紧邻下一行,中间穿插了#EXT-X-DISCONTINUITY,所以要用 while 循环跳过注释行。第三,所有相对地址都要通过urljoin拼接成绝对地址,否则下载的时候会 404。

4.2 单个分片的下载函数与失败重试

单个分片下载是整个流程中最容易被低估的环节。很多人写下载函数只写一个requests.get(url),超时设置都不加,分片多的时候一半请求会卡死。我的做法是显式设置超时、加重试、限制单进程内的并发连接数。

import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def download_segment(session, seg_url, save_path): with session.get(seg_url, stream=True, timeout=(10, 30)) as r: r.raise_for_status() with open(save_path, "wb") as f: for chunk in r.iter_content(chunk_size=64 * 1024): f.write(chunk) return save_path def create_session(): session = requests.Session() retries = Retry( total=3, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504], allowed_methods=["GET"], ) adapter = HTTPAdapter(max_retries=retries, pool_connections=20, pool_maxsize=20) session.mount("https://", adapter) session.mount("http://", adapter) return session

timeout=(10, 30)指的是连接超时 10 秒、读取超时 30 秒。很多视频服务器响应速度并不稳定,不设超时会让整个进程池卡在少数几个坏分片上。Retry和HTTPAdapter是 requests 库自带的重试机制,比自己写循环判断状态码更规范,也能自动处理连接池复用。每个进程维护自己的Session,不要跨进程共享,后面讲坑的时候会细说。

4.3 多进程调度:用 Pool.imap_unordered 拉满并发

调度核心逻辑围绕multiprocessing.Pool展开。我选imap_unordered而不是Pool.map,因为前者不要求返回结果的顺序,哪个任务先完成就先返回,速度更快。分片下载完成后我们会根据序号重新排序,所以不关心下载顺序。

import multiprocessing as mp def worker(args): seg_index, seg_url, save_paths, key_path = args seg_path = save_paths.get(seg_index) download_segment(session, seg_url, seg_path) return seg_index, seg_path def download_all(segments, save_dir, workers=8): seg_count = len(segments) save_paths = {i: os.path.join(save_dir, f"{i:05d}.ts") for i in range(seg_count)} session = create_session() tasks = [(i, url, save_paths, key_path) for i, url in enumerate(segments)] with mp.Pool(processes=workers) as pool: for _ in pool.imap_unordered(worker, tasks, chunksize=1): pass return save_paths

这里chunksize=1很重要。imap_unordered默认会把任务分成大块,导致某个进程一次拿太多任务,动态分配的优势就没了。实测设成 1,每个任务独立分配,负载均衡效果最好。save_paths的命名用 5 位数字补零,比如00000.ts、00001.ts,目的是保证后续合并时按文件名排序不会错乱。00010.ts和0002.ts的字典序差异会直接影响排序结果,补零是从根上杜绝这个问题。

4.4 合并分片:直接拼 TS 还是交给 FFmpeg

合并这一步有两种选择。如果所有分片都是 TS 格式,可以用二进制拼接的方式直接合成一个.ts文件,然后通过 FFmpeg 转封装成.mp4。如果分片本身就是 MP4 格式的fMP4(常见于较新的 HLS 流),就不能直接拼了,必须交给 FFmpeg 处理。

稳妥起见,我统一走 FFmpeg 路线。把所有分片的本地路径写进一个文本清单,然后调用 FFmpeg 读取清单、处理解密、输出最终文件:

ffmpeg -y -allowed_extensions ALL -protocol_whitelist file,http,https,tls,tcp,crypto -i list.txt -c copy output.mp4

用-c copy的原因是不想重新编码。转封装只是改变容器格式,不涉及解码再编码,速度非常快,画质零损失。如果输出.ts格式,甚至不需要 FFmpeg,直接按顺序拼接二进制文件就行,但要保证每个分片都是完整的 TS 数据包。

5. 实测结果与调优记录:并发数到底开多少最合适

写代码容易,调参数难。我把这套下载器在测试环境中跑了十几轮,环境是某云服务器(模拟项目X的测试机),线路并不算特别优质,单线程下载速度约 1.5 MB/s。测试视频是一个 20 分钟左右的加密 HLS 流,分片 300 个,每个分片约 200 KB,总计约 60 MB。

5.1 并发数从 1 到 16 的耗时对比

测试结果如下:

进程数总耗时平均单分片耗时是否出现失败重试
148.6s162ms无
226.9s89ms无
414.8s49ms无
89.7s32ms无
128.9s29ms1 次
168.5s28ms3 次

前 8 个进程的效果非常明显,耗时从 48 秒降到 9.7 秒。再往上加,耗时减少就比较有限了,但失败重试次数开始增加。原因不难理解:这台服务器的带宽上限大约 6 MB/s,8 个进程已经可以跑满;加到 16 个进程后,请求过于密集,反倒容易触发服务端的频率限制。

5.2 所以并发数怎么选:看带宽和分片大小

我的经验公式是:先测一下单进程下载速度,记为v_single,再测一下带宽上限,记为v_max,理想的进程数大约是v_max / v_single,再留 10% 余量。如果懒得测,直接用 CPU 核心数乘以 2 是比较稳的起点。分片特别小的时候(比如 50 KB),可以适当调高进程数,因为单分片传输时间短,多数时间花在建立连接上,稍微多点并发不容易撞上频率限制。

5.3 实测中的意外情况:服务器 403 和 Referer 校验

调优过程中遇到最恼火的问题是部分分片返回 403。开始以为是并发太高被封了,后来发现是某些服务器的 CDN 会校验 Referer 和 User-Agent。请求头里没有带正确的 Referer,服务器直接拒绝。

解决方法是在创建 Session 时统一加上请求头:

session.headers.update({ "User-Agent": "Mozilla/5.0 (...) AppleWebKit/537.36", "Referer": base_url, })

Referer 一般取 m3u8 所在页面的地址,而不是 m3u8 文件本身的地址。这个细节很容易被忽略,但它决定了你是顺利下载还是被 CDN 拦在门外。

6. 分片管理中的高频坑:临时文件、内存峰值、进程阻塞

写完能跑,只是开始。真正让下载器从“能跑”变“好用”的,是处理好这一堆小问题。我把自己踩过并且修复过的坑整理在这里,每一个都是真实消耗过时间的。

6.1 分片保存的目录结构和磁盘空间

分片文件虽然小,但架不住数量多。一个 2 小时的电影,分片数量轻松超过 1500 个,全部堆在同一级目录下没问题,但删除的时候特别慢。而且分片文件命名如果不统一,很容易出现覆盖或缺失。我建议为每次下载单独建一个临时目录,目录名用任务开始时间或视频标题的哈希值:

temp_dir = os.path.join(download_root, "temp_" + str(int(time.time()))) os.makedirs(temp_dir, exist_ok=True)

下载完成后,临时目录可以保留一段时间,确认最终视频没问题再删除。不要看完视频立刻删,万一分片下载不完整需要重新拉取,还能从临时目录里复用一部分内容。

6.2 把全部分片读进内存?千万不要

新手容易犯的错误是:下载完分片后,用列表把所有分片的二进制内容全部读进内存,再一次性拼接写入目标文件。单个分片 200 KB,1500 个分片就是 300 MB,再加上 Python 对象本身的额外开销,内存峰值直接超过 500 MB。在我的测试机上,这已经让系统开始使用交换分区,速度骤降。

正确做法是合并时逐个读取分片,写入目标文件后立即释放:

with open(output_ts, "wb") as out_f: for seg_path in sorted(save_paths.values()): with open(seg_path, "rb") as in_f: shutil.copyfileobj(in_f, out_f, length=1024 * 1024)

copyfileobj会按 1 MB 的块拷贝,内存占用始终控制在个位数 MB。这句话朴实但值钱,多少人就是在这里被内存搞崩的。

6.3 进程意外阻塞:为什么需要设置超时和进程存活监控

多进程下载最怕的不是某个分片失败,而是某个进程序程卡住不退出,导致整个Pool.imap_unordered永远等不到结果。我在测试中就遇到过:某 CDN 节点的连接迟迟不返回数据,timeout=(10, 30)设了也没用,因为服务器已经接受了 TCP 连接,只是数据不流动,读取超时虽然触发,但判断的是 chunk 之间的间隔。

加重试策略只解决了一部分问题。最终的兜底方案是给整个imap_unordered外层加一个超时控制:

with mp.Pool(processes=workers) as pool: for _ in pool.imap_unordered(worker, tasks, chunksize=1): pass pool.terminate() # 防止长时间未完成的进程阻塞主进程

这里用到的是进程池的上下文管理特性,配合pool.terminate(),当主流程判断超时后强制结束所有子进程,避免脚本挂在那里一动不动。正常情况下还是要等所有任务完成,所以 terminate 只作为异常路径的兜底,不要放在正常路径里。

6.4 日志与进度输出:不要用 print 堆刷屏

多进程环境下用print输出进度有个问题:多个进程同时写 stdout,内容会交叉在一起。更麻烦的是,进度信息无法区分是哪个分片完成的。我的做法是让每个 worker 只返回(seg_index, seg_path)这样的纯数据,由主进程统一负责统计和进度显示:

completed = 0 for seg_index, seg_path in pool.imap_unordered(worker, tasks, chunksize=1): completed += 1 if completed % 50 == 0: print(f"progress: {completed}/{len(segments)}", flush=True)

进度打印放在主进程,flush=True写入缓冲立即刷新,避免控制台看不到实时输出。日志级别也可以加一个--quiet参数控制,正常下载只打印摘要信息,排错时才开 verbose 模式。

7. 加密流在合并阶段的两个隐藏注意事项

加密流的坑不在下载阶段,而在合并阶段。前面说过用 FFmpeg 解密合并,但如果密钥文件和分片文件组织得不对,FFmpeg 会以各种奇怪的方式拒绝。

7.1 密钥文件命名不能带特殊符号

密钥文件一般叫key.key或者encrypt.key,下载后最好重命名为简单的文件名,比如key.bin。有次我把密钥文件名命名为别人提供的原始名称,里面带了个@符号,FFmpeg 把它当成协议关键字的一部分,直接报错。还有一次密钥文件下载时没有加binary模式,导致密钥文件比实际多了几个字节,解密出来的视频花屏。

key_resp = session.get(key_uri, timeout=(10, 30)) key_resp.raise_for_status() key_path = os.path.join(temp_dir, "key.bin") with open(key_path, "wb") as kf: kf.write(key_resp.content)

这里注意两点:content属性返回的是已经解码的字节对象,iter_content流式写入更省内存,但密钥文件通常很小,直接用content即可。写入模式必须是wb,如果误用w,会触发 Unicode 编码错误。

7.2 m3u8 里没有 IV 时不能瞎猜

#EXT-X-KEY里的 IV 字段在某些流里是缺省的。HLS 规范这时规定默认使用分片的序列号作为 IV,序列号由#EXT-X-MEDIA-SEQUENCE指定,每个分片的 IV 跟它的序列号挂钩。如果 IV 缺省,FFmpeg 会自动处理,不需要手动指定。但如果你是自己解密,就必须计算每个分片的 IV 了,这也从侧面说明为什么优先交给 FFmpeg 处理更省心。

另外,有些 m3u8 文件里的密钥地址是相对路径,比如key.bin或./keys/key.bin,解析时必须基于 m3u8 文件自身的 URL 做拼接。很多人在这一步拼接错误,导致密钥 404,然后就以为视频“加密复杂,下载不了”。其实问题不在加密,而在 URL 拼接没做对。

8. 实测完整流程与一些额外优化思路

核心功能跑通以后,我还做了一些锦上添花的优化。不复杂,但在实际使用中很提升体验。

8.1 断点续传:没下载完的分片怎么处理

下载到一半程序崩了,最浪费的不是时间,而是已经下载下来的几百个分片。给下载器加上断点续传其实很简单:每个分片下载前先检查目标文件是否存在且大小大于 0,如果存在就跳过。

def download_segment(session, seg_url, save_path): if os.path.exists(save_path) and os.path.getsize(save_path) > 0: return True # 实际的下载逻辑...

注意光看文件存在还不够,还要看大小是否大于 0。有些异常情况下会创建空文件,如果直接跳过,后续合并时就会出现视频片段缺失。更严谨一点可以做大小比对,但分片在服务端的 Content-Length 不一定准确,我一般只检查非空。

8.2 提速小技巧:每个进程内部再用协程或异步

多进程 + 多线程/协程的组合拳,能把速度再往上提一截。比如 8 个进程,每个进程内部再用asyncio同时发起 4 个请求,相当于同时有 32 个连接在跑。

import asyncio import aiohttp async def fetch_segment(session, seg_url, save_path): async with session.get(seg_url) as resp: with open(save_path, "wb") as f: async for chunk in resp.content.iter_chunked(64 * 1024): f.write(chunk) async def download_group(group): async with aiohttp.ClientSession() as session: tasks = [fetch_segment(session, url, path) for _, url, path in group] await asyncio.gather(*tasks)

这么做的好处是更充分地利用 CPU 和网络带宽,尤其在带宽上限很高的环境下,纯多进程可能受限于进程数,加入协程后能压榨出更多并发能力。缺点是需要额外学习和调试成本,如果不是追求极限速度,可以先不加。

8.3 一个好的下载器还应该有的辅助功能:从版本信息、播放时长到字幕处理

我最后在 main 里加了一个“任务摘要”输出,显示视频总片数、估算时长、是否加密、密钥地址等关键信息。这个习惯帮了大忙——很多 m3u8 文件里面有多个不同码率的版本,我先通过摘要确认当前解析的是主版本,再决定要不要下载备选版本。

相关逻辑并不复杂,在parse_m3u8里一并返回#EXTINF的总和作为估算时长:

total_duration = sum(info.get("duration", 0) for info in segment_infos)

字幕分片(EXT-X-MEDIA里的SUBTITLES)要不要下载可以根据参数控制,第一版可以忽略,后续需要再加。视频和音频分离的双流同样可以先放一放,等基本流程稳定后处理。

9. 最后再分享一点个人体会

这套多进程 m3u8 下载器前前后后改了三版,从最初的零碎脚本到现在结构清晰的模块化工具,最大的收获不是速度提升了多少倍,而是明白了:并发编程真正的难点不在“并发”本身,而在于怎么把网络请求的不可靠性、文件系统的持久性、进程的生命周期管理这三件事组织好。

现在回到开头说的“加密或非加密”——加密也好,非加密也罢,核心流程都是一样的:解析、并发下载、合并。无非是加密时多了密钥处理和 FFmpeg 解密这一步。把基础流程跑稳了,加密流只是加一个环节的问题,不会让人手足无措。

如果你照着这篇从头搭一遍,大概率会遇到一些我这里没提到的具体报错。这太正常了,毕竟各家 CDN 的脾气不同,m3u8 的变种也多。排查的方向一般都是固定的:先看解析出来的分片地址对不对,再看某个分片下载时的响应头和状态码,最后看合并时 FFmpeg 的报错信息。把这三步走通, 90% 的问题都能定位。

最后给一个小建议:做这种工具类项目,调试时一定要挑一个分片数量多的真实视频来测。分片少,很多并发问题根本暴露不出来;分片多了,你才能看到进程池、超时、重试、内存这些东西在极限情况下的真实表现。别用几个分片的 demo 糊弄自己,跑通了不代表能用,这是我和真实项目摔过几回后最深的感受。

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

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

立即咨询