资讯动态

m3u8在线下载工具详解:抓包解析、分片并发与ffmpeg合并实践

发布时间:2026/10/18 2:55:42 来源:尧图企业网站定制
简介这是一份面向普通网民与开发者的M3U8流媒体视频下载、提取工具包解决M3U8视频无法直接保存到本地的问题。工具采用Python命令行与前端脚本结合的方式解析M3U8清单多线程下载TS分片并自动调用ffmpeg合并为MP4适合离线取流或部署为在线服务。压缩包共47个文件以JavaScript、HTML、CSS、PNG等类型为主其中JS与HTML负责下载交互界面CSS和图片负责视觉样式另有少量配置适配服务端环境整体仅1.68MB。已有5939人学习/下载。用户可从中获得一套可运行的在线M3U8下载网站源码及核心用户脚本理解流媒体解析、分片并发抓取、视频合并等关键细节也适合基于现有代码二次开发快速搭建私有视频提取工具或研究下载原理具备较高复用价值。1. m3u8在线下载工具到底是什么能播放却下载不了的视频它怎么解决m3u8本身并不是视频文件它只是一个文本索引播放器按行读取指向几十上百个.ts分片真正要做的下载工作其实是“把分片逐个拉下来再按顺序拼成一个可播放的视频”。m3u8在线下载工具解决的场景很具体网页里视频能正常播放但既没有下载按钮直接复制地址给普通下载器又只拿到一个几KB的文本这类工具一般做成网页插件或本地小服务粘贴m3u8地址、填好请求头、设置并发数就能把整段视频提取成mp4。适合给教学课件做离线备份、给播放器开发做素材抓取、给运营同学做内容留档。下面按一套可以照抄的流程拆开讲从地址解析一路讲到离线校验。2. 地址解析从抓包定位m3u8到看懂索引与密钥2.1 抓包定位m3u8真实地址藏在哪打开浏览器开发者工具切到“网络”面板先把过滤器切到 Media 或 Fetch/XHR再重新播放一次视频。多数视频站会在开始播放的几秒内发出一个或者多个xxx.m3u8请求响应类型是application/vnd.apple.mpegurl或者application/x-mpegURL。直接在列表里搜“m3u8”往往就能看到。有个更隐蔽的情况地址列显示的是blob:https://...说明站点先用 JavaScript 拉到了 m3u8 内容再把它交给 MediaSource 接口处理。遇到这种情况不要慌回到“网络”面板把过滤器切成Fetch/XHR搜索关键词m3u8或playlist通常能在某个 XHR 请求的响应体里看到完整的 m3u8 文本实际地址就在这里面。拿到地址后先验证一下再复制。把地址粘贴到浏览器地址栏打开如果能直接看到一堆以#EXTM3U开头、夹着.ts结尾路径的文本说明这个地址可以直接用如果打开是报错页或者登录页说明接口还依赖 Referer、Cookie 这类请求头需要把播放页里的请求头一起带过去。我一般会右键这条 m3u8 请求复制cURL (bash)作为完整请求来源后续下载工具里需要填的 Referer、UA、Cookie 都能从这里抄。2.2 解读 m3u8分片、时长、密钥都在每一行里一个典型的 m3u8 文件长这样#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-KEY:METHODAES-128,URIkey.key #EXTINF:10.0, segment0.ts #EXTINF:10.0, segment1.ts #EXT-X-ENDLIST其中#EXTINF后面是分片时长下一行就是这个分片的 URL#EXT-X-KEY表示这段流用了 AES-128 加密URI指向密钥文件解密时还要留意有没有IV字段如果没有则默认使用分片序号作为初始向量。常被忽略的是相对路径很多流里写的是segment0.ts而不是完整的https://example.com/media/segment0.ts拼地址时必须用当前 m3u8 所在的目录去拼接直接拼域名往往拼错。我这里写了一个最简解析脚本足够应付大多数流import re import json from urllib.parse import urljoin, urlparse def parse_m3u8(m3u8_url, headersNone): import requests resp requests.get(m3u8_url, headersheaders, timeout15) resp.raise_for_status() lines [line.strip() for line in resp.text.splitlines() if line.strip()] segments [] key_uri None key_method NONE for idx, line in enumerate(lines): if line.startswith(#EXT-X-KEY:): method_match re.search(rMETHOD([^,]), line) uri_match re.search(rURI([^]*), line) if method_match: key_method method_match.group(1) if uri_match: key_uri urljoin(m3u8_url, uri_match.group(1)) elif line.startswith(#EXTINF:): # 下一行就是分片地址跳过注释行 if idx 1 len(lines) and not lines[idx 1].startswith(#): seg_url lines[idx 1] if not seg_url.startswith(http): seg_url urljoin(m3u8_url, seg_url) segments.append(seg_url) result { playlist: m3u8_url, key_method: key_method, key_uri: key_uri, segment_count: len(segments), segments: segments, } return result if __name__ __main__: result parse_m3u8(https://example.com/media/playlist.m3u8, headers{Referer: https://example.com}) print(json.dumps(result, indent2, ensure_asciiFalse))这段代码的思路是先请求 m3u8 文本逐行扫描#EXT-X-KEY和#EXTINF拿到密钥地址和分片列表。urljoin处理相对路径是关键因为实际流里很少有写完整 URL 的。headers参数允许你把抓包得到的 Referer、UA 传进来这一步做不好后面下载大概率遇到 403。运行后如果看到segment_count大于零说明解析成功可以进入下载环节。2.3 索引可能嵌套主列表和子列表要分清有些视频不是单一列表而是嵌套结构也就是主 m3u8 里放的是多码率子列表#EXT-X-STREAM-INF:BANDWIDTH2500000,RESOLUTION1280x720 720p/index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH1200000,RESOLUTION640x360 360p/index.m3u8如果你的 dl 总大小很小或者解析出的 segment_count 是 2 而不是几百多半是抓到了主列表。这时要顺着#EXT-X-STREAM-INF后面的子列表地址再请求一次才是真正包含分片的分辨率子列表。在线工具里常见“清晰度选择”本质就是在不同子列表之间切换。3. 下载器实现并发分片、超时重试、临时文件与合并参数3.1 用线程池拉分片连接复用比一次次新建请求重要分片数量少则几十多则上千逐个顺序下载会很慢常见做法是开线程池并发拉取。这里有一个容易翻车的点不能给每个分片都新建一个 requests.SessionTCP 握手的开销会把并发优势浪费掉。我一般会全局创建一个 Session然后把同一个 Session 传给所有线程让底层连接复用。import os import time import requests from concurrent.futures import ThreadPoolExecutor, as_completed def download_segment(index, seg_url, headers, session, save_dir, retries3): local_path os.path.join(save_dir, f{index:05d}.ts) for attempt in range(retries): try: resp session.get(seg_url, headersheaders, timeout(10, 30)) if resp.status_code 403: # 403 重试也没用直接抛出提示头不对 raise PermissionError(fsegment {index} 403, check Referer/UA) resp.raise_for_status() with open(local_path, wb) as f: f.write(resp.content) return index, len(resp.content) except Exception as exc: wait 2 ** attempt time.sleep(min(wait, 8)) if attempt retries - 1: raise exc def download_playlist(parse_result, headers, workers8, save_dirtmp): os.makedirs(save_dir, exist_okTrue) session requests.Session() futures {} with ThreadPoolExecutor(max_workersworkers) as pool: for idx, seg_url in enumerate(parse_result[segments]): future pool.submit(download_segment, idx, seg_url, headers, session, save_dir) futures[future] idx for future in as_completed(futures): try: idx, size future.result() print(fsegment {idx} done, {size} bytes) except Exception as exc: print(fsegment {futures[future]} failed: {exc}) if __name__ __main__: result parse_m3u8(https://example.com/media/playlist.m3u8, headers{Referer: https://example.com}) download_playlist(result, headers{Referer: https://example.com}, workers8)代码里几个参数值得说清楚。timeout(10, 30)表示连接超时 10 秒、读取超时 30 秒分片长时间不出数据时会触发retries3配合指数退避2 的 n 次方秒最多退到 8 秒避免一下子把对方服务器打崩。workers8是一个相对安全的并发数家用带宽下 8 到 16 都能跑过高的并发会导致 CDN 限流典型表现是前几个分片飞速下载后面全部超时。3.2 分片排序与二进制拼接顺序错了就是花屏并发下载完成之后所有分片都在本地临时目录里文件名是我上面那种00000.ts到00499.ts的格式排序时要用数字序而不是字符串序否则10.ts会排在2.ts前面合并出来音画错乱。最简单的合并是用文件追加方式把分片按顺序写成一个 ts 文件def concat_ts(segment_files, output_path): with open(output_path, wb) as out_f: for seg_path in segment_files: with open(seg_path, rb) as in_f: out_f.write(in_f.read())如果分片是 H.264 AAC这种直接拼接得到的.ts文件是可以在多数播放器里播放的。但要注意有些流的音频是纯 AAC 裸流直接拼出来的 ts 放到播放器里能出声但无法拖动进度条或者提示解码失败这时需要交给 ffmpeg 做转封装。更好的做法是让 ffmpeg 读取原始 m3u8 并自动处理解密、解码、封装只保留视频流重新封装到 MP4ffmpeg -allowed_extensions ALL -protocol_whitelist file,http,https,crypto,tcp -i input.m3u8 -c copy -bsf:a aac_adtstoasc output.mp4-protocol_whitelist必须包含crypto否则 ffmpeg 遇到 AES-128 加密流会直接报错说协议被禁止-bsf:a aac_adtstoasc会把裸的 ADTS AAC 转成 MP4 需要的 ASC 格式不加这个参数很多播放器合并出来的视频只有画面没有声音。如果跑完发现文件时间轴正常说明流程通了。3.3 在线工具表单里那几个字段到底怎么填这套逻辑落到网页插件或在线工具上通常会暴露成几个表单字段。很多用户下载失败不是因为工具不行而是字段没填对。常见的字段和含义可以看这张表表单字段含义我常用的填法m3u8 地址分片索引地址抓包复制出来的原始地址不要填 blob:Referer来源页面发起播放请求的那个页面 URLUser-Agent浏览器标识从网络面板复制当前浏览器 UACookie登录态如果视频需要登录才能看必须带上并发数同时下载分片数量默认 8网络差时降到 4合并模式转封装或直接拼接有加密流选 ffmpeg 转封装填 Referer 和 UA 是最容易被忽略的。部分站点会校验请求头的Origin和Referer这两个值和播放页不一致时会返回 403。在线工具往往只留了一行“请求头”输入框你需要把抓包得到的完整 Headers 复制进去而不是只写一个 UA。4. m3u8视频提取避坑五个高频故障的现象、原因和处理办法4.1 五个高频故障记录第一个坑播放器里能正常播放下载却一直 403。现象是前几个分片下载成功到某个分片后请求突然被拒或者整个列表完全不能下载。原因是部分 CDN 校验了 Referer 和服务端 Cookie播放器发出的请求带着完整请求头下载器的请求头却不完整。解决方式是回到抓包面板复制视频流的完整 Headers把 Referer、UA、Cookie 原样填进下载工具并确认 Referer 的域名和路径精确匹配。第二个坑下载到一半卡住不动也没有报错。现象是进度停在 98% 之类的某个位置进程不退出但也没有新数据进来。原因是某个 ts 分片所在的 CDN 节点临时抽风或者连接被对端重置持续挂起。解决方式是在下载函数里给 requests 设置timeout(10, 30)让长时间没数据的请求主动抛异常配合重试逻辑跳过去而不是无限等待。第三个坑合并之后音画不同步或者只有画面没有声音。现象是 ffmpeg 转封装结束没有报错但播放器里音频轨缺失或声音延迟。原因是源流音频封装格式是 ADTS AAC直接复制进 MP4 容器后参数不对。解决方式是在 ffmpeg 命令里补上-bsf:a aac_adtstoasc这个 bitstream filter 会把 AAC 的 ADTS 头转换成 MP4 需要的格式改完之后通常一次就正常。第四个坑复制出来的地址是blob:https://...任何下载器都无法识别。现象是下载工具报不支持该协议或者只下载到一个几KB的blob文件。原因是站点用 JavaScript 拉了 m3u8 并通过 MediaSource 播放浏览器地址栏里的 blob URL 只在当前页面会话内有效根本不能跨页面访问。解决方式是在网络面板的 Fetch/XHR 请求里重新搜索 m3u8找到真实的分片列表地址如果连真实地址都隐藏了可以在请求列表里找包含m3u8?的接口往往是播放器 API再去这个接口的响应里拿真实播放列表。第五个坑本地手动拼接后的 ts 文件能播但用 ffmpeg 合并 mp4 时中途失败。现象是 ffmpeg 报Invalid data found when processing input或者末尾文件没有 moov 元数据。原因是下载的分片不完整某个分片实际拿到的只有部分字节。解决方式是先做完整性检查把分片次数、每个分片的 Content-Length 和本地文件大小对比一下确认没有 0 字节分片再合并。断点处重试下载而不是直接让 ffmpeg 硬合并。4.2 排查顺序先分清是索引问题还是分片问题遇到下载失败我一般先按两步定位。第一步看 m3u8 索引本身能不能访问。把地址在浏览器里打开检查返回的是不是完整文本如果返回 HTML 登录页那就是 Cookie 或 Referer 的问题。第二步看单分片能不能访问。复制第一个分片地址单独下载如果同样报 403那就是请求头的问题如果单分片能下但并发后大量失败那就是并发数太高导致的限流。提示在下载脚本里给每个分片记录状态码和耗时失败时看状态码比看报错文案更直接。403 限流和 500 服务端错误是两种完全不同的排查方向。5. 落地技巧完整性校验、断点续传与批量脚本化5.1 先校验再合并别让 ffmpeg 当垃圾桶分片全部下载完成后不要急着合并。我习惯先跑一遍大小校验请求每个分片的Content-Length和本地文件大小做对比有差异的记录出来重新拉取。校验写起来很轻def verify_segment(seg_url, local_path, headers): resp requests.head(seg_url, headersheaders, timeout10) expected int(resp.headers.get(Content-Length, -1)) actual os.path.getsize(local_path) return expected actual有些服务端不支持 HEAD 请求可以改成Range: bytes0-0的 GET 请求从响应头拿Content-Range里的总长度。校验通过的目录再做合并基本不会碰到合并到一半中断的情况。5.2 断点续传已经存在的分片别重复下载批量下载多集视频时任务跑到一半断掉是常有的事。断点续传不用写复杂的状态文件只要下载前检查临时文件名是否已存在且大小大于零即可。把download_segment开头改成这样local_path os.path.join(save_dir, f{index:05d}.ts) if os.path.exists(local_path) and os.path.getsize(local_path) 0: return index, os.path.getsize(local_path)这样脚本跑第二次时已成功的分片会直接跳过只补失败的少数几个相比全部重下省了大量时间。5.3 批量任务串行调度别开无限并发多集视频不能简单地把每集的 download_playlist 丢进更大的线程池否则几百个线程同时请求CDN 很快会限流。我一般写一个外层循环每集最多开 8 个分片线程集与集之间串行处理想加快可以在每集完成后立刻释放临时目录下一集直接复用磁盘空间。保存文件名建议包含清晰度和分辨率信息比如lesson01_720p.mp4方便后续归档。做视频备份时我习惯先拿一集十分钟的视频全流程跑通确认密钥、请求头、合并都正常再挂整批任务。之前直接整批跑导致大量返工从那以后我都强制先跑单集验证再放开并发另外下载脚本里始终保留日志输出失败时能直接看到是哪一个分片、什么原因。希望这些经验能帮到你少走几步弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑