ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从M3U8到MP4:流媒体下载、分片合并与格式转换实战指南

从M3U8到MP4:流媒体下载、分片合并与格式转换实战指南 这次我们直接聊一个实践向的问题面对 M3U8/HLS、DASH 这种分片流媒体怎么把它们完整下载成本地 MP4同时处理 ts 分片合并、格式转换和批量任务。现状是很多视频站点为了做清晰度切换和带宽自适应把视频切成大量小分片通过 M3U8 索引文件对外提供播放。用户在浏览器里看得很流畅但想“下载一个 MP4”时发现根本没有直链。DASH 方案也类似它把视频轨、音频轨拆开再通过 MPD 清单文件组织播放。这导致下载和转换不再是“右键另存为”那么简单需要一套完整的工具链来完成拉取、解密、合并和封装。这篇文章就以“M3U8(HLS)、DASH、MP4 视频下载器”为主题梳理这类工具到底能做什么、最低需要什么环境、怎么处理加密分片、怎么做批量任务以及最常见的失败原因和排查路径。如果你之前被“m3u8 转 MP4 失败”“ts 合并顺序乱掉”“AES-128 加密分片无法下载”这些问题卡住过这篇文章可以直接对照排查。1. 核心能力速览能力项说明支持格式M3U8 / HLS、DASH 清单、MP4 直链、TS 分片文件主要功能流媒体下载、ts 分片合并、m3u8 转 MP4、视频格式转换核心依赖ffmpeg、Python可选、网络环境可访问目标资源运行平台Windows / macOS / Linux 均可是否支持 CPU 推理不涉及 AI 推理CPU 性能足够瓶颈主要在解码和磁盘 I/O是否支持批量任务支持可通过脚本或队列逐条处理多个链接是否支持接口 API可以通过 Python 封装成 HTTP 服务或命令行封装后供其他程序调用适合场景本地视频备份、离线观看、音视频素材整理、教学视频存档版权边界只允许下载自己拥有版权或有明确授权的内容这里要强调一个基础判断M3U8 并不是视频文件它是一份文本索引里面记录了一个个分片文件的地址和播放顺序。下载器真正做的事情是读取这份索引把 ts 分片全部拉下来再按顺序拼接成完整文件。DASH 的 MPD 清单逻辑类似只是分片可能是 m4s 或 mp4 片段音视频轨更倾向于分离存储。2. M3U8、HLS、DASH、MP4、TS 格式辨析2.1 HLS 与 M3U8 的关系HLS 是苹果推的流媒体协议M3U8 是 HLS 的索引清单文件格式。播放器先请求 M3U8 地址拿到分片 URL 列表再逐个拉取 .ts 分片进行连续播放。一个典型的 M3U8 文件内容如下#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.000, segment_000.ts #EXTINF:10.000, segment_001.ts #EXT-X-ENDLIST如果文件末尾有#EXT-X-ENDLIST表示这是一个点播视频可以完整下载。如果没有这个标记通常是直播流下载时更适合持续记录而不是一次性合并。2.2 DASH 与 HLS 的差异DASH 使用 MPD 清单文件全称是 Media Presentation Description。它和 HLS 最大的区别在于HLS 通常把某个清晰度下的一路音视频封装在同一个 ts 分片里而 DASH 经常把视频轨和音频轨完全分离分别编码播放器在本地实时合成。这也是很多 DASH 流无法像 M3U8 那样“下载即合并”的原因。下载 DASH 流时你需要同时拿到视频轨分片列表和音频轨分片列表分别下载后再通过 ffmpeg 合并。搜索词里能看到不少“dash 视频下载后没声音”的问题绝大多数就是因为只下载了视频轨没有处理音频轨。2.3 MP4、TS、M4S、M4A 后缀的区别.tsMPEG-2 Transport Stream流媒体分片常见格式适合网络传输但不适合直接剪辑。.mp4ISO BMFF 容器适合本地播放、剪辑、上传也是最终交付首选。.m4sDASH 分片常见格式本质是碎片的 MP4不能直接播放。.m4aAAC 音频封装常见于 DASH 音频轨。下载器的核心工作之一就是把这些“碎片”重新封装成标准 MP4这也是标题里“ts 合并转换、m3u8 转 MP4、视频格式转换”这些能力的来源。3. 环境准备与前置条件在做任何下载操作前建议先确认本机环境。下面是一套通用检查清单检查项要求说明操作系统Windows 10 / macOS 12 / Ubuntu 20.04无强制版本要求但新版本对编解码器兼容更好ffmpeg建议 4.4 及以上负责协议拉流、解码、转封装、音视频合并Python3.8 及以上用于写批量下载脚本和接口服务时使用磁盘空间视频体积的 2 倍以上下载中间分片和最终成片需要临时空间网络能访问目标服务器即可内网测试就访问内网地址公网测试需要目标资源允许访问解码能力CPU 支持即可大部分场景-c copy不做重编码CPU 只需完成解封装端口如果启用 API 服务预留 8000-9000避免与其他服务冲突3.1 安装 ffmpegWindows 可以下载 ffmpeg 官方编译包解压后把 bin 目录加入 PATH。macOS 使用 Homebrewbrew install ffmpegUbuntu / Debian 使用 aptsudo apt update sudo apt install ffmpeg安装完成后执行ffmpeg -version能输出版本信息即安装成功。实测中最常见的坑是 PATH 没配好导致ffmpeg命令找不到后续脚本全部报FileNotFoundError。3.2 验证网络与目标地址下载之前先用 curl 测试 M3U8 索引是否可访问curl -I https://example.com/video/index.m3u8如果返回200 OK说明索引文件可达。如果返回403 Forbidden或触发了重定向说明服务器有防盗链、Referer 校验或 Cookie 校验。这个时候光靠 ffmpeg 直接拉流通常会失败需要把请求头一并带上。观察 M3U8 内容时还要看有没有#EXT-X-KEY标签#EXT-X-KEY:METHODAES-128,URIkey.key,IV0x00000000000000000000000000000000有这一行说明分片经过 AES-128 加密下载时必须同步处理密钥逻辑否则就算把 ts 分片全部拉下来合并出来的 MP4 也无法正常解码。4. 实操使用 ffmpeg 下载 M3U8 并转换为 MP44.1 最简单的完整下载命令对绝大多数未加密、可公开访问的 M3U8 点播流一条命令就能完成下载和转封装ffmpeg -i https://example.com/video/index.m3u8 -c copy -bsf:a aac_adtstoasc output.mp4参数含义参数作用-i指定输入地址或本地文件-c copy不重新编码直接复制视频和音频流速度最快、画质无损-bsf:a aac_adtstoasc把 AAC 音频从 ADTS 格式转换为 ASC 格式解决合并后播放器无法识别音频的问题output.mp4输出文件名后缀决定封装格式实测中-c copy是首选因为视频流本身已经是 H.264 / H.265 编码不需要二次压缩速度快且无画质损失。只有当你确实需要把编码格式从 H.265 转成 H.264 时才考虑重编码。4.2 携带 Referer 和 User-Agent 下载很多站点做了防盗链不带请求头直接拉流会返回 403。ffmpeg 支持通过-headers传入自定义请求头ffmpeg -headers Referer: https://example.com/play -user_agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) -i https://example.com/video/index.m3u8 -c copy output.mp4需要注意-headers的格式是Key: Value\r\n多个请求头之间用\r\n分隔。写成一行时部分平台会有转义问题更稳妥的方式是写成多行比如ffmpeg \ -headers $Referer: https://example.com/play\r\nOrigin: https://example.com\r\n \ -user_agent Mozilla/5.0 \ -i https://example.com/video/index.m3u8 \ -c copy output.mp4这里的核心思路是浏览器能播说明你带着正确的 Referer 和 Cookie 就能访问那下载器同样要带上这些信息。4.3 只下载音频轨遇到只需要音轨的场景比如提取课程音频可以这样写ffmpeg -i https://example.com/audio/index.m3u8 -vn -acodec copy output.m4a-vn表示丢弃视频流-acodec copy表示音频流不重编码。最终得到的就是纯音频文件。4.4 从本地 ts 分片合并成 MP4如果已经通过其他方式把分片下载到了本地目录可以使用 concat 协议合并ffmpeg -f concat -safe 0 -i filelist.txt -c copy output.mp4filelist.txt的格式如下file segment_000.ts file segment_001.ts file segment_002.ts合并时要注意分片顺序错误顺序会导致画面跳变或时间戳错乱。更稳妥的方式是先按文件名排序再生成列表。这个操作在批量处理场景中非常常见。5. 实操Python 脚本实现分片下载、解密与合并直接用 ffmpeg 下载 M3U8 虽然简单但遇到需要临时保存分片、检查完整性、处理加密逻辑或接入批量任务队列时用 Python 写一个可控性更强的脚本更有优势。5.1 安装依赖pip install requests m3u8 tqdmm3u8库负责解析 M3U8 清单requests负责下载分片tqdm用于显示下载进度。5.2 基础下载脚本import os import m3u8 import requests from tqdm import tqdm def download_m3u8(m3u8_url, output_ts, headersNone): playlist m3u8.load(m3u8_url, headersheaders) segment_list playlist.segments with open(output_ts, wb) as f: for segment in tqdm(segment_list, descdownloading): seg_url segment.uri if seg_url.startswith(http): url seg_url else: url os.path.join(playlist.base_uri, seg_url) resp requests.get(url, headersheaders) resp.raise_for_status() f.write(resp.content) print(fsaved: {output_ts}) if __name__ __main__: url https://example.com/video/index.m3u8 headers { Referer: https://example.com, User-Agent: Mozilla/5.0, } download_m3u8(url, output.ts, headers)这个脚本把所有分片顺序写入一个大的 .ts 文件之后再用 ffmpeg 转封装成 MP4ffmpeg -i output.ts -c copy -bsf:a aac_adtstoasc output.mp45.3 处理 AES-128 加密分片遇到#EXT-X-KEY:METHODAES-128的清单时需要先下载密钥文件再用密钥对每个分片做 AES-128 解密。示例逻辑如下import os from Crypto.Cipher import AES import requests import m3u8 def download_encrypted_m3u8(m3u8_url, output_ts, headersNone): playlist m3u8.load(m3u8_url, headersheaders) key_uri playlist.keys[0].uri if playlist.keys else None iv playlist.keys[0].iv if playlist.keys and playlist.keys[0].iv else None key_url key_uri if key_uri.startswith(http) else os.path.join(playlist.base_uri, key_uri) key_resp requests.get(key_url, headersheaders) key key_resp.content if iv: iv_bytes bytes.fromhex(iv.replace(0x, )) else: iv_bytes b\x00 * 16 cipher AES.new(key, AES.MODE_CBC, iviv_bytes) with open(output_ts, wb) as f: for segment in playlist.segments: seg_url segment.uri if not seg_url.startswith(http): seg_url os.path.join(playlist.base_uri, seg_url) seg_resp requests.get(seg_url, headersheaders) decrypted cipher.decrypt(seg_resp.content) f.write(decrypted) print(fdecrypted and saved: {output_ts})需要安装pip install pycryptodome这里的关键点是密钥文件地址可能是相对路径iv可能是 32 位十六进制字符串解析时要统一处理成字节。很多“m3u8 解密失败”的报错本质上是iv没解析对或者密钥文件本身需要带 Referer 才能下载。5.4 已有 ts 分片时直接合并如果你的场景已经从缓存目录里拿到了全部 ts 分片只需要合并那脚本会更简单import os import glob def merge_ts_to_mp4(input_dir, output_ts): ts_files sorted(glob.glob(os.path.join(input_dir, *.ts))) with open(output_ts, wb) as out: for ts_file in ts_files: with open(ts_file, rb) as f: out.write(f.read())合并完成后再调用 ffmpeg 转换成 MP4。也可以直接让 Python 通过subprocess调用 ffmpeg把合并和转封装合并到一个流程里import subprocess subprocess.run([ ffmpeg, -i, output.ts, -c, copy, -bsf:a, aac_adtstoasc, output.mp4 ], checkTrue)6. 批量任务与接口 API6.1 批量下载多个 M3U8 链接实际使用中经常遇到“把 20 个视频批量下载成 MP4”的需求。可以把链接放进一个 txt 文件脚本逐行读取并处理https://example.com/video/01.m3u8 https://example.com/video/02.m3u8 https://example.com/video/03.m3u8import os import subprocess def download_from_list(list_file, output_dir): os.makedirs(output_dir, exist_okTrue) with open(list_file, r, encodingutf-8) as f: urls [line.strip() for line in f if line.strip()] for i, url in enumerate(urls, 1): output_name os.path.join(output_dir, fvideo_{i:03d}.mp4) print(f[{i}/{len(urls)}] downloading: {url}) subprocess.run([ ffmpeg, -i, url, -c, copy, -bsf:a, aac_adtstoasc, output_name ], checkTrue) print(done) if __name__ __main__: download_from_list(urls.txt, ./output)这种方式适合数据量不大的批量任务。如果视频数量很多建议加上进度记录、失败重试和日志避免中途崩溃后无法断点续传。6.2 封装成 HTTP API 服务把下载能力封装成接口方便其他工具或前端页面调用。这里给一个基于 Flask 的通用示例from flask import Flask, request, jsonify import subprocess import uuid import os app Flask(__name__) download_dir ./downloads os.makedirs(download_dir, exist_okTrue) app.route(/api/download, methods[POST]) def download(): data request.get_json() m3u8_url data.get(m3u8_url) if not m3u8_url: return jsonify({error: m3u8_url is required}), 400 task_id str(uuid.uuid4()) output_path os.path.join(download_dir, f{task_id}.mp4) try: subprocess.run([ ffmpeg, -i, m3u8_url, -c, copy, -bsf:a, aac_adtstoasc, output_path ], checkTrue, timeout600) return jsonify({ task_id: task_id, status: success, file: output_path }) except subprocess.TimeoutExpired: return jsonify({error: download timeout}), 500 except Exception as e: return jsonify({error: str(e)}), 500 if __name__ __main__: app.run(host0.0.0.0, port8000)启动python api_server.py调用示例curl -X POST http://127.0.0.1:8000/api/download \ -H Content-Type: application/json \ -d {m3u8_url: https://example.com/video/index.m3u8}这个服务本身不包含页面只提供接口能力。生产环境下要考虑任务队列、异步执行和文件清理避免同步阻塞导致接口长时间无响应。7. 资源占用与性能观察7.1 CPU 与内存使用-c copy转封装时ffmpeg 不做视频编码CPU 占用率一般不会太高主要消耗在解封装和写盘。如果使用重编码方式比如把 H.265 转成 H.264CPU 占用会明显上升此时建议开启硬件编码器ffmpeg -i input.m3u8 -c:v h264_nvenc -c:a copy output.mp4N 卡可以用h264_nvencIntel 核显可以用h264_qsv。具体可用编码器可以通过ffmpeg -encoders查看。内存方面下载分片是按顺序写入文件的不会把整个视频读进内存内存占用一般在几十到几百 MB 之间。但如果编写脚本时把所有分片内容先追加到 list 再一次性写入就有可能撑爆内存。7.2 磁盘空间分片下载过程中中间 .ts 文件和最终 .mp4 会同时存在磁盘占用峰值大约是视频体积的 1.2 到 2 倍。如果磁盘剩余空间不足下载到一半会报No space left on device。建议处理完临时文件后及时清理rm -f output.ts7.3 网络带宽与并发默认情况下 ffmpeg 是串行拉取分片网络利用率相对平稳。Python 脚本可以通过ThreadPoolExecutor提高并发下载速度from concurrent.futures import ThreadPoolExecutor import requests def download_one(url, headers, out_file): resp requests.get(url, headersheaders) return resp.content with ThreadPoolExecutor(max_workers8) as executor: contents list(executor.map( lambda seg: download_one(seg, headers, None), segment_urls ))并发提高后需要观察目标服务器是否限速或封 IP。如果分片地址本身是 CDN通常能承受较高并发如果是小服务器建议控制在 4 到 8 个并发。还要注意并发写入同一个文件时不要破坏顺序更稳妥的做法是每个分片先落临时文件最后再拼接。8. 常见问题与排查方法问题现象可能原因排查方式解决方案ffmpeg 提示Protocol not found未安装完整 ffmpeg 或 URL 拼接错误检查输入地址是否完整确认https://前缀重新安装 ffmpeg下载返回 403缺 Referer、User-Agent 或 Cookie用 curl 测试裸访问观察响应头在命令中补充-headers、-user_agent合并后视频无声音DASH 音视频轨分离或 AAC 未转 ASC用ffprobe查看输出文件流信息使用-bsf:a aac_adtstoasc或单独下载音频轨并合成合并后画面乱序分片文件名排序错误检查 filelist 顺序按数字位补零排序或按 M3U8 中的原始顺序读取AES-128 解密后花屏IV 解析错误或 Key 下载失败检查 M3U8 的#EXT-X-KEY字段确认 IV 是 16 字节密钥带请求头下载No space left on device磁盘空间不足df -h查看剩余空间清理中间文件预留 2 倍空间Invalid data found when processing input分片下载不完整或索引损坏重新执行下载对比文件大小使用脚本校验每个分片失败自动重试直播流 M3U8 下载后文件不结束直播流没有#EXT-X-ENDLIST查看文件尾部直播流使用持续录制模式而非一次性合并接口服务长时间不返回同步下载阻塞查看日志检查任务是否超时改成异步任务加入任务队列批量任务中途失败单条链接失效或网络波动查看日志定位失败编号增加 try/except、失败重试和断点续传9. 合规使用与最佳实践视频下载工具本身是中性技术但使用边界必须明确。9.1 版权与授权只下载以下内容自己创建并上传的视频。明确标注允许下载或已授权下载的内容。用于个人学习、备份且有合法来源的内容。不得将下载器用于批量抓取他人平台视频、绕过付费墙、二次传播或商业牟利。涉及人脸、隐私、商业版权素材时必须确认授权后再处理。9.2 本地工程化建议下载目录按date或batch_id分目录避免所有视频堆在一起。保留原始 M3U8 链接清单方便追溯来源。中间 .ts 文件处理完及时删除节省磁盘空间。批量任务必须打日志记录每个链接的开始时间、结束状态和输出文件路径。一个推荐的目录结构downloads/ ├── 20250101/ │ ├── urls.txt │ ├── logs/ │ │ └── download.log │ └── video/ │ ├── 001.mp4 │ ├── 002.mp4 │ └── 003.mp49.3 API 服务安全如果起了 HTTP 接口服务建议只监听127.0.0.1不要直接暴露到公网。如果必须内网访问可以加一层 Token 校验和请求频率限制。import functools def require_token(func): functools.wraps(func) def wrapper(*args, **kwargs): token request.headers.get(X-Token) if token ! your-secret-token: return jsonify({error: unauthorized}), 401 return func(*args, **kwargs) return wrapper10. 总结与下一步这个方向最值得尝试的点是能用一套统一流程把 M3U8、DASH、MP4、TS 这些不同来源的视频全部归一化成标准 MP4。先跑通 ffmpeg 单条命令再补上 Python 的批量下载与 AES-128 解密逻辑最后按需封装成接口服务这套链路是完整且可复用的。最先应该验证的功能是拿一个未加密的测试 M3U8 地址跑通“下载-转封装-播放”全流程。最容易踩的坑有两个一个是防盗链导致的 403需要补请求头另一个是 DASH 音视频轨分离需要确认音频轨也下载并合并。后续可以继续扩展的方向包括接入 HLS 直播流的持续录制、用任务队列替代同步下载、增加用户上传 M3U8 链接的管理后台、以及把成品视频自动归档到 NAS 或对象存储。先把基础链路跑稳再逐步增加工程化能力这套工具就可以从“自己用的小脚本”升级成“团队可用的下载服务”。
返回列表