
做下载工具时m3u8 算是最常见的流媒体格式之一。无论是 HLS 直播回放、在线课程视频还是自己服务器上的视频资源一套可靠的 m3u8 下载流程都是基本功。今天想和大家拆解的是怎么用 Python 多进程把 m3u8 加密或非加密视频稳定地拉下来同时把速度尽可能拉满。网上类似的脚本很多但大多停留在“单线程能跑就行”的阶段遇到几千个分片的视频就原形毕露速度上不去、超时一堆、内存还容易爆。这篇文章不打算只给一段能跑的代码而是把拆解 m3u8、设计多进程任务、处理失败重试、合并分片这几个环节全部讲清楚每一步都解释为什么这么做。适合刚接触流媒体下载、对 Python 并发编程有一定基础但没系统做过下载器的读者也适合想把自己那个“能用但不好用”的脚本重写一遍的朋友。先提醒一句下面所有操作都只适用于你有权访问和保存的内容比如自己上传的课程、已购买的视频、或者官方允许离线观看的资源。请勿用于任何侵权场景技术本身没有立场使用方式自己把握。1. 先搞清楚 m3u8 到底是什么分片拉流的原理与下载瓶颈很多人写过 m3u8 下载脚本但没认真看过 m3u8 文件内容长什么样。这不怪大家毕竟它本质就是一个文本文件光看内容看不出什么门道。可如果不懂它的结构后面做多进程并发就是瞎忙活。1.1 m3u8 文件里的关键标签逐行解读m3u8 是 HLSHTTP 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:METHODAES-128,URIkey.key,IV0x00000000000000000000000000000000意思是分片用 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, streamTrue, timeout(10, 30)) as r: r.raise_for_status() with open(save_path, wb) as f: for chunk in r.iter_content(chunk_size64 * 1024): f.write(chunk) return save_path def create_session(): session requests.Session() retries Retry( total3, backoff_factor0.5, status_forcelist[500, 502, 503, 504], allowed_methods[GET], ) adapter HTTPAdapter(max_retriesretries, pool_connections20, pool_maxsize20) session.mount(https://, adapter) session.mount(http://, adapter) return sessiontimeout(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, workers8): 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(processesworkers) as pool: for _ in pool.imap_unordered(worker, tasks, chunksize1): pass return save_paths这里chunksize1很重要。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/s8 个进程已经可以跑满加到 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_okTrue)下载完成后临时目录可以保留一段时间确认最终视频没问题再删除。不要看完视频立刻删万一分片下载不完整需要重新拉取还能从临时目录里复用一部分内容。6.2 把全部分片读进内存千万不要新手容易犯的错误是下载完分片后用列表把所有分片的二进制内容全部读进内存再一次性拼接写入目标文件。单个分片 200 KB1500 个分片就是 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, length1024 * 1024)copyfileobj会按 1 MB 的块拷贝内存占用始终控制在个位数 MB。这句话朴实但值钱多少人就是在这里被内存搞崩的。6.3 进程意外阻塞为什么需要设置超时和进程存活监控多进程下载最怕的不是某个分片失败而是某个进程序程卡住不退出导致整个Pool.imap_unordered永远等不到结果。我在测试中就遇到过某 CDN 节点的连接迟迟不返回数据timeout(10, 30)设了也没用因为服务器已经接受了 TCP 连接只是数据不流动读取超时虽然触发但判断的是 chunk 之间的间隔。加重试策略只解决了一部分问题。最终的兜底方案是给整个imap_unordered外层加一个超时控制with mp.Pool(processesworkers) as pool: for _ in pool.imap_unordered(worker, tasks, chunksize1): 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, chunksize1): completed 1 if completed % 50 0: print(fprogress: {completed}/{len(segments)}, flushTrue)进度打印放在主进程flushTrue写入缓冲立即刷新避免控制台看不到实时输出。日志级别也可以加一个--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 糊弄自己跑通了不代表能用这是我和真实项目摔过几回后最深的感受。