ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ffmpeg + m3u8:从HLS协议原理到视频下载实战指南

ffmpeg + m3u8:从HLS协议原理到视频下载实战指南 视频下载工具这个关键词下很多人在搜索引擎里找“万能下载器”但实际项目中真正稳定可用的往往不是某个一键工具而是 ffmpeg 这类开源命令配合对 m3u8 播放列表和 HTTP 传输方式的理解。这篇文章先从网络视频如何传输和播放讲起再进入环境搭建、m3u8 下载合并、最小脚本实现和排错方法最后统一说明版权边界。读完可以自己搭一套免费、可控、不依赖付费软件的视频素材处理环境也知道为什么有些视频能下载、有些不能以及哪些做法会越界。1. 视频下载工具的核心先理解网络视频是怎么送到浏览器的1.1 从“文件下载”到“流式播放”的差别普通文件下载是一个很简单的过程客户端向服务器发送 HTTP GET 请求服务器返回文件字节流客户端保存到磁盘。只要网络不断文件就会完整落地。你不需要理解视频编码也不需要知道帧率、关键帧、封装格式。网络视频不一样。视频文件通常很大一个小时的 1080p 视频可能有几个 GB如果等服务端把整个文件传完再播放用户早就离开了。所以现代播放器采用边下边播的方式先把前面几秒的数据加载到内存开始播放同时继续从网络上拉取后面的数据。这意味着播放器并不会一次性请求全部文件而是按需请求部分字节。服务器通过 HTTP Range 机制支持这种请求客户端带上Range: bytes0-1023服务器只返回这段内容。播放器根据当前播放位置分批请求数据。从下载工具的角度看理解这一点的价值在于下载工具要模仿的不只是“请求一个 URL”而是正确解析出拿到媒体数据的地址和请求方式。页面能播放不代表服务器愿意一次性把文件交给你它可能希望你把所有分片都拉到本地后再合并。1.2 三种常见的视频传输方式实际开发中遇到最多的视频传输方式是下面三种。传输方式典型格式原理适合场景直链文件MP4、MOV、WebM用户直接请求完整文件地址服务器返回文件小体积素材、自有文件、CDN 静态资源渐进式播放MP4播放器通过 Range 请求分段读取文件边下边播中长视频浏览器兼容性好HLSM3U8 TS 分片服务端把视频切成多个小分片客户端先下载 m3u8 播放列表再逐个请求分片直播、点播、跨平台播放DASHMPD 分片类似 HLS但分片格式和描述方式不同支持更灵活码率切换自适应码率点播Web 和移动端HLS 是互联网视频里最常见的一种。站点把一份长视频切割成多个 5 到 10 秒的 TS 小文件同时生成一个 m3u8 文本文件记录播放顺序。播放器先请求 m3u8然后按顺序请求里面的 TS 地址。直播场景下m3u8 文件会不断更新加入新的分片地址点播场景下m3u8 文件是静态的直到EXT-X-ENDLIST结束标记出现。1.3 HLS 的 m3u8 到底长什么样m3u8 本质是一个 UTF-8 编码的文本播放列表。下面是一个点播场景中最简单的例子。#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:10.0, segment_000.ts #EXTINF:10.0, segment_001.ts #EXTINF:10.0, segment_002.ts #EXT-X-ENDLIST逐行看#EXTM3U表示这是一个 m3u 家族播放列表。#EXT-X-VERSION是 HLS 协议版本号。#EXT-X-TARGETDURATION表示单个分片最大时长这里是 10 秒。#EXTINF:10.0,后面跟的视频分片时长单位是秒。segment_000.ts是分片文件名实际请求时需要拼接完整 URL。#EXT-X-ENDLIST表示点播结束后续没有更多分片。很多视频站的 m3u8 不是这么简单。分片文件名可能是一串带签名的参数也可能指向另一个 m3u8 文件做多级跳转还可能出现#EXT-X-KEY标记说明分片内容经过了 AES-128 加密需要先获取密钥才能解密播放。1.4 视频下载工具的合规边界下载工具只是技术手段用在哪里才是关键。下面这张表总结了常见的场景判断方式。内容类型是否可以下载说明自己录制的视频、自己制作的课程可以自己拥有版权保存和备份都合理开源社区或作者明确标注可下载的测试素材可以例如 CC0、CC BY 授权或作者提供直链下载地址平台提供离线缓存功能的视频按平台规则使用只能在 App 或客户端内离线观看不能提取文件二次分发版权归平台或第三方所有且未开放下载的视频不建议下载即使技术上能抓到 URL也可能违反服务条款或侵犯版权带 DRM 或者付费墙保护的内容不能尝试绕过任何绕过行为都涉及安全和版权问题这里要特别强调一点技术上能下载不等于合规。m3u8 地址写在网络请求里很多视频播放器都会先请求一个播放列表然后请求分片。从网络链路看这些请求并不神秘开发者用浏览器开发者工具就能观察到。但如果目标内容是受版权保护的仅仅因为“技术能做到”就去下载和分发是错误的方向。2. 环境准备用 ffmpeg 搭一个免费的可控环境2.1 安装 ffmpeg 并验证ffmpeg 是音视频领域最常用的开源命令行工具下载、转码、截取、合并、提取音频都能做。很多“视频下载软件”底层其实就是封装了 ffmpeg 的逻辑只是套了一个图形界面。在 Windows 上推荐从 ffmpeg 官方发布的 Windows 构建版本下载压缩包解压后把bin目录加入系统 PATH 环境变量或者直接使用包管理工具安装。在 macOS 上使用 Homebrew 最省事。# Windows 使用 winget winget install ffmpeg # macOS 使用 Homebrew brew install ffmpeg # Ubuntu / Debian sudo apt update sudo apt install ffmpeg安装完成后验证版本。ffmpeg -version ffprobe -version正常会输出版本号、编译配置和可用的编码器信息。ffmpeg负责转码和合成ffprobe负责查看媒体文件信息后面会经常用到。2.2 在本地生成一个 HLS 测试流为了不依赖外部站点先在本地生成一小段测试视频再把它切成 HLS 分片。这样后面所有下载、合并、排错都可以在本地复现没有版权和网络波动问题。下面这条命令用 ffmpeg 的测试信号源生成 30 秒视频画面是彩条音频是正弦波然后直接封装成 HLS 输出。ffmpeg -f lavfi -i testsrcduration30:size1280x720:rate25 \ -f lavfi -i sinefrequency440:duration30 \ -c:v libx264 -preset veryfast -c:a aac \ -hls_time 5 -hls_list_size 0 -hls_segment_filename segment_%03d.ts \ output.m3u8参数含义-f lavfi使用 ffmpeg 内置的虚拟输入源。testsrc生成测试彩条画面。sine生成正弦波音频。-c:v libx264视频使用 H.264 编码兼容性最好。-c:a aac音频使用 AAC 编码。-hls_time 5每个 TS 分片 5 秒。-hls_list_size 0生成的 m3u8 保留所有分片记录而不是只保留最近一段。-hls_segment_filename指定分片文件名格式。执行完成后当前目录会生成output.m3u8和多个 TS 分片文件。2.3 生成后的文件结构在命令行执行ls可以看到类似下面的文件列表。output.m3u8 segment_000.ts segment_001.ts segment_002.ts segment_003.ts segment_004.ts segment_005.ts这些文件加在一起才是完整视频。浏览器或播放器拿到output.m3u8后会按顺序读取分片并播放。下载工具要做的事情就是把 m3u8 解析出来把分片全部拉到本地再合并成一个 MP4 或 MKV。3. 从 m3u8 到本地视频下载、合并与转码实操3.1 下载直链视频curl 和 ffmpeg 的区别如果视频是直链文件也就是服务器直接返回一个 MP4最简单的办法是使用 curl。curl -L -C - -o video.mp4 https://example.com/path/to/video.mp4参数解释-L跟随重定向。-C -支持断点续传下载中断后再次执行会从断点继续。-o指定输出文件。curl 适合直链下载但它不理解 m3u8 播放列表也不会自动处理分片。要下载 HLS 视频用 ffmpeg 更方便因为 ffmpeg 内部支持 HLS 协议。3.2 下载 m3u8 并使用 ffmpeg 合并假设前面已经生成了本地output.m3u8把它当成要处理的播放列表。为了模拟网络场景可以先用一个本地 HTTP 服务托管测试目录也可以直接用相对路径。这里用最简单的方式展示 ffmpeg 如何读取 m3u8 并输出 MP4。ffmpeg -allowed_extensions ALL -i output.m3u8 -c copy -bsf:a aac_adtstoasc output.mp4如果是网络地址写法是ffmpeg -i https://example.com/path/to/index.m3u8 -c copy -bsf:a aac_adtstoasc output.mp4执行过程中ffmpeg 会显示每个分片被读取和写入的信息。正常情况下会把所有 TS 分片合并到output.mp4中。这里核心参数是-c copy表示不重新编码直接复制音视频流数据。这样做速度最快也能保证画质无损。但直接复制有时会留下问题比如 TS 分片里 AAC 音频的格式和 MP4 要求不一致所以加上-bsf:a aac_adtstoasc把音频流转换一下。这是 HLS 转 MP4 最常用的写法。3.3 流复制和重新编码什么时候不用-c copy-c copy是流复制相当于把视频流和音频流从 TS 容器搬到 MP4 容器几乎不消耗 CPU。但有几个情况它不合适。一是播放器兼容性问题。有些视频播放器对 MP4 里的音频格式要求严格需要执行一次转码。ffmpeg -i input.m3u8 -c:v copy -c:a aac output.mp4这种写法保留视频流不重新编码只把音频转成 AAC速度比完全转码快很多。二是容器格式不一致时。如果源视频来自某个特殊封装-c copy会报错这时需要让 ffmpeg 重新封装或转码。ffmpeg -i input.m3u8 -c:v libx264 -c:a aac output.mp4这个命令把视频重新编码为 H.264音频重新编码为 AAC。虽然耗时和 CPU 占用都会增加但兼容性最好。3.4 开源下载器的定位说明开源社区也有一些命令行下载器例如 yt-dlp它们的主要价值是解析网页播放页面自动抽取媒体地址再交给 ffmpeg 合并。这类工具能省去打开浏览器开发者工具分析请求的过程。使用时必须注意前提条件只能用于平台允许的媒体内容以及自己有权下载的公开资源。对受版权保护、DRM 加密或需要付费访问的内容下载器并不具备“合法化”作用。技术工具不会自动改变内容的版权归属最终责任在使用者。4. 关键参数详解写命令前先看懂参数4.1 ffmpeg 常用下载参数速查表ffmpeg 参数非常多实际下载合成场景最常用的如下。参数含义使用说明-i指定输入文件或 URL可以出现多次例如同时输入视频和音频-c copy流复制不重新编码速度快画质无损-c:v libx264视频重新编码为 H.264兼容性最好速度较慢-c:a aac音频重新编码为 AACMP4 容器最常用-bsf:a aac_adtstoasc将 ADTS 格式的 AAC 转成 MP4 需要的格式HLS 转 MP4 经常需要-y覆盖输出文件而不询问脚本化运行时使用-user_agent自定义 User-Agent模拟浏览器访问避免服务端拦截-headers添加自定义 HTTP 请求头用于 Referer、Token 等场景-timeout设置网络超时时间单位是微秒默认值可能偏短-rw_timeout设置读写超时时间避免网络波动造成卡死-t只处理指定时长用于试看前几十秒-ss从指定时间点开始处理用于截取片段一个带请求头的网络下载例子ffmpeg -user_agent Mozilla/5.0 \ -headers Referer: https://example.com/ \ -i https://example.com/path/to/index.m3u8 \ -c copy -bsf:a aac_adtstoasc output.mp4这里-headers可以接多个请求头用\r\n分隔。实际项目里需要通过开发者工具确认服务端要求哪些请求头不要凭空猜测。4.2 如何处理相对路径和动态签名m3u8 文件里的分片地址可能是相对路径。例如#EXTINF:5.0, hls/segment_000.ts播放器在请求分片时会基于 m3u8 的 URL 做拼接得到完整地址。如果直接用 ffmpeg 处理它会自动处理相对路径。但如果是自己写脚本解析就必须用 URL 拼接函数而不是简单地拼字符串。动态签名站点更特殊。m3u8 URL 和分片 URL 往往带有时效性参数比如?tokenxxxexpiresxxx。签名过期后请求会返回 403。遇到这类站点需要重新从播放页面获取最新地址不要长时间缓存 m3u8。4.3 多清晰度如何选择很多点播站点的 m3u8 是“主播放列表”里面不直接放分片地址而是放多个不同清晰度的子 m3u8。#EXTM3U #EXT-X-STREAM-INF:BANDWIDTH1280000,RESOLUTION1280x720 720p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH640000,RESOLUTION854x480 480p.m3u8这种结构叫 master playlist。直接把这个地址交给 ffmpeg它通常会选择带宽最高的那个流也就是 720p。如果想明确选某个清晰度可以直接把子播放列表地址作为输入也可以先用 ffprobe 查看列表内容再决定下载哪个变体。curl -s https://example.com/master.m3u8 | grep #EXT-X-STREAM-INF这条命令快速列出主播放列表里所有可用流。注意grep 只是辅助分析实际下载时仍然要遵守平台规则。5. 用 Python 写一个最小化的 m3u8 下载脚本5.1 脚本目标这里用 Python 标准库写一个最小脚本只做三件事解析本地 m3u8、下载所有 TS 分片、调用 ffmpeg 合并成 MP4。脚本不包含任何绕过限制的逻辑只适用于本地测试文件和公开可访问资源。5.2 解析 m3u8import os import subprocess import sys from urllib.parse import urljoin from urllib.request import urlretrieve def parse_m3u8(m3u8_path): segments [] with open(m3u8_path, r, encodingutf-8) as f: lines [line.strip() for line in f if line.strip()] for line in lines: if line.startswith(#): continue if line: if line.startswith(http://) or line.startswith(https://): segments.append(line) else: segments.append(os.path.join(os.path.dirname(m3u8_path), line)) return segments这个函数只处理最简单的情况跳过以#开头的标签行把剩下的行当作分片地址。本地文件用os.path.join拼接网络地址直接保留。5.3 下载分片并合并def download_segments(segments, out_dir): os.makedirs(out_dir, exist_okTrue) saved [] for idx, seg in enumerate(segments): local_path os.path.join(out_dir, fsegment_{idx:05d}.ts) print(fdownloading {seg} - {local_path}) urlretrieve(seg, local_path) saved.append(local_path) return saved def merge_segments(segments, output_file): list_file concat_list.txt with open(list_file, w, encodingutf-8) as f: for seg in segments: f.write(ffile {seg}\n) cmd [ ffmpeg, -y, -f, concat, -safe, 0, -i, list_file, -c, copy, output_file ] subprocess.run(cmd, checkTrue) def main(): if len(sys.argv) 2: print(usage: python download_hls.py /path/to/output.m3u8) return m3u8_path sys.argv[1] segments parse_m3u8(m3u8_path) saved download_segments(segments, segments_cache) merge_segments(saved, merged_video.mp4) if __name__ __main__: main()脚本思路很直接先把分片下载到本地临时目录再生成一个 concat 列表文件最后调用 ffmpeg 合并。第一个版本的缺点也很明显没有重试机制、没有并发、没有处理 AES 加密分片、没有清理临时文件。真实环境里这些问题必须补上。5.4 脚本的不足与优化方向一个生产可用的下载脚本至少还需要考虑下面这些点。重试机制单个分片下载失败时等待几秒后重新尝试。并发控制用线程池限制并发数避免对服务器造成压力。磁盘清理合并完成后删除临时 TS 文件。加密支持识别#EXT-X-KEY下载密钥并解密分片。日志记录记录每个分片的状态方便排查失败原因。校验完整性对比 m3u8 里声明的时长与实际下载分片数量。如果只是个人素材处理ffmpeg 一条命令可能比脚本更可靠。写脚本的价值在于理解 HLS 的工作方式以及为后续自动化处理打基础。6. 验证下载结果与常见问题排查6.1 用 ffprobe 验证文件下载完成后不要只看文件大小。一个只有几 MB 的“MP4”可能是损坏的。最直接的方法是使用 ffprobe 检查文件信息。ffprobe -v error -show_format -show_streams output.mp4正常输出会包含文件封装格式、时长、视频流编码、分辨率、音频流编码和采样率等信息。Input #0, mov,mp4,m4a,3gp,3g2,mj2, from output.mp4: Metadata: major_brand : isom ... Duration: 00:00:30.02, start: 0.000000, bitrate: 1234 kb/s Stream #0:0: Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1280x720, 25 fps Stream #0:1: Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp看到这两条流说明视频和音频都合并成功。如果只有画面没有声音说明音频流可能在合并过程中丢失。6.2 常见错误现象、原因与处理问题现象常见原因检查方式处理建议下载返回 403 Forbidden缺少 User-Agent、Referer或签名过期用浏览器开发者工具查看请求头添加-user_agent和-headers重新获取 m3u8分片 404 Not Found相对路径拼接错误或分片已过期对比 m3u8 里的地址与实际请求地址使用完整 URL或重新抓取播放列表下载到一半卡住网络波动、超时设置过短观察 ffmpeg 日志确认卡在哪个分片增加超时时间或改用自己写的重试脚本输出文件无法播放AAC 音频格式不兼容查看 ffprobe 输出检查 audio 流加上-bsf:a aac_adtstoasc或重新编码音频ffmpeg 报错 Invalid datam3u8 文件不完整或分片没下载完全打开 m3u8 内容确认#EXT-X-ENDLIST是否存在重新下载 m3u8检查网络代理是否干涉视频有画面没声音音频流丢失或音视频流选择错误ffprobe -show_streams看 audio 流是否存在用-map 0:v -map 0:a显式选择流6.3 排查问题的标准顺序遇到下载失败不要先怀疑工具按下面的顺序查。先确认内容本身是否允许下载版权边界不能靠技术手段去测试。再检查 m3u8 地址是否能直接访问用 curl 单独请求一次。打开 m3u8 文件确认分片地址是相对路径还是绝对路径。单独下载一个分片测试确认分片能正常访问排除服务端限制。再使用 ffmpeg 完整下载观察日志停留在哪个分片。如果和本地文件有关检查文件名、路径和后缀是否匹配特别是 Windows 下路径分隔符的问题。最后检查磁盘空间、网络代理、系统防火墙等因素。7. 最佳实践安全、合规、可维护7.1 下载前结构化自查每次准备下载视频素材前先用下面这个清单判断。检查项判断标准内容归属是我自己创作的还是获得授权的平台规则目标平台是否允许离线保存或下载技术手段是否涉及绕过鉴权、解密 DRM 或破解密码使用目的是个人备份、学习研究还是要公开分发潜在风险如果内容版权方追责我是否有完整授权链只要有一项不符合就不要继续。技术上能做到的事情很多但不是每一件都适合做。7.2 文件与元数据管理下载完成不等于工作完成。视频下载后的存储同样值得规划。使用明确的目录结构例如source/2025/04/课程名/。文件名包含日期或来源标识避免只保留video(3).mp4这类无意义名字。下载后立即用 ffprobe 记录时长、分辨率和编码信息。重要素材同时保留原始 m3u8方便后续重新处理。大文件建议计算 MD5 或 SHA-256防止磁盘损坏后无法发现。sha256sum output.mp4 output.mp4.sha256这条命令生成校验文件后续可以使用sha256sum -c output.mp4.sha256验证文件完整性。7.3 进一步学习方向如果读完这篇文章想继续深入建议按下面的路径推进。学习 HTTP Range 请求理解断点续传和视频拖动播放的原理。学习 HLS 协议规范重点看#EXT-X-KEY、#EXT-X-DISCONTINUITY、#EXT-X-PROGRAM-DATE-TIME等标签。学习 MP4 和 TS 容器结构的差异理解为什么有些封装格式更适合流媒体。实践用 ffmpeg 做截取、拼接、添加字幕、转码等多个任务而不是只停留在下载。阅读 yt-dlp 或 ffmpeg 的源码片段了解真实下载器如何处理多平台差异。这些内容学完之后很多视频处理问题都不再需要依赖某个“万能下载器”而是可以自己组合命令和脚本解决。这也是掌握基础协议和命令行工具带来的长期价值。
返回列表