🌐ENZH

M3U8 转 MP4 不能只拼接分片:相对地址、字节范围和初始化段的坑

开始做 M3U8 转 MP4 时,最容易想到的流程是“读取列表、下载分片、拼成一个文件”。实际实现很快暴露了问题:上传的列表不知道资源来自哪里,一些分片只是同一个文件的不同区域,另一些流需要独立初始化段,音频还可能在另一个列表里。

这篇文章按照数据处理顺序记录这些问题。示例使用虚构资源地址,重点是解释结构和诊断方法。开发验证依据本项目 2026 年 10 月 1 日的受控浏览器测试:覆盖了相对地址、独立音轨、字节范围返回 200 的处理和 fMP4 初始化段。示例里的资源名不对应可以直接下载的公开视频。

本地 M3U8 文件可以解析,为什么无法转换?

M3U8 是文本索引,不包含完整视频内容。一个本地文件可以保留分片名称、时长和标签,却丢失原来的网络位置。例如文件内容是:

#EXTM3U
#EXT-X-TARGETDURATION:6
#EXTINF:6.0,
segment-01.ts
#EXTINF:6.0,
segment-02.ts
#EXT-X-ENDLIST

查看解析工具 不需要下载视频,就可以告诉你这份列表有两个分片、声明时长合计 12 秒。但转换工具必须知道 segment-01.ts 位于哪个地址。浏览器上传的本地文件不会自动携带它在 CDN 上的原始目录,也不能假设分片位于我们的工具网站。

因此,上传文件或粘贴文本后,如果里面有相对 URI,需要补充原始播放列表的 HTTP(S) 地址。这里应填写该文件原来对应的地址,而不是任意网站首页。若文件原本来自 https://media.example.com/course/hls/index.m3u8,分片才会被解析到相同目录下。

这也解释了两个功能的差异:查看文本的结构可以离线完成一部分工作;取得完整视频需要可访问的资源地址。看到分片表格并不表示这些分片已经下载或通过可用性检查。

地址解析不能依赖字符串拼接

我们采用 URL 解析规则处理资源位置,而不是截取目录后直接拼字符串。同一个列表中的这些 URI 意义并不一样:

列表中的 URI基准地址为 https://media.example.com/course/hls/index.m3u8 时的结果
segment.tshttps://media.example.com/course/hls/segment.ts
../audio/index.m3u8https://media.example.com/course/audio/index.m3u8
/keys/key.keyhttps://media.example.com/keys/key.key
https://cdn.example.com/segment.ts保持这个独立的完整地址

对应代码可以使用 new URL(resourceUri, playlistUrl)。关于相对地址的处理,可参阅 MDN 的 URL 构造函数说明。对通过网络取得的列表,我们以响应最终地址作为基准,避免重定向之后仍然使用旧目录。

另一个常见误区是把入口地址的查询参数自动复制到所有子请求。index.m3u8?token=... 不等于每个分片都应该附加同一个参数。来源可能已经在分片 URI 中提供签名,也可能通过其他方式授权。无条件复制容易破坏签名。本工具按 URI 本身解析,不擅自发明来源的鉴权规则。

检查地址问题时,应该看浏览器实际请求了什么 URL。若路径错误,先修正基准;若路径正确但返回 403,再检查来源授权和有效期。反复更换文件扩展名通常不能解决这类问题。

一个主列表还可能指向独立音轨

主列表除了清晰度,也可以通过音频组指向另一份媒体列表。只下载视频分支,有可能得到无声的输出。我们目前选择最高声明带宽的清晰度;存在独立音轨时,优先选择默认音轨,没有默认项则选择第一个音轨,再将视频和音频作为两个输入交给媒体引擎。

这是一项明确的选择策略,并不是自动保留全部音轨。要判断输出是否符合预期,应先在解析页面查看清晰度、编码声明以及音频组,再核对选中的分支。带宽值也只是列表的声明,不能保证某个分支一定比其他分支画质更好。

解析属性还有一个小陷阱:CODECS 字符串内部可以包含逗号。简单地按逗号切开整个属性行,会把一个带引号的值拆坏。解析器需要识别引号范围,否则清晰度和音轨关联可能从第一步就出错。

字节范围分片不是“下载同一个 URL 多次”

一些媒体列表不为每个分片创建独立文件,而是让多个分片指向同一个媒体文件的不同区域:

#EXTM3U
#EXT-X-TARGETDURATION:4
#EXTINF:4.0,
#EXT-X-BYTERANGE:1000@0
media.ts
#EXTINF:4.0,
#EXT-X-BYTERANGE:1200@1000
media.ts
#EXT-X-ENDLIST

这里的数字只用于说明范围,并非真实可播放媒体。第一段需要字节 0 到 999,第二段需要字节 1000 到 2199。若把 media.ts 整个文件下载两遍当成两个分片,内容会重复,结果当然可能出错。

我们的请求带上相应 Range 范围。服务器返回 206 时,检查取得的数据是否符合预期长度,并在可读取相应响应头时核对范围。还有一种需要处理的情况:服务器忽略 Range,直接返回 200 和完整文件。此时不能把整个响应当成目标分片,而要从取得的数据中截取指定区间,同时将实际下载量计入预算。

如果省略 @offset,偏移并不是一律从零开始,它依赖符合要求的前一个范围。缺少可推导的上下文时应该报错。范围标签的定义见 RFC 8216 第 4.3.2.2 节。这种结构也要求缓存标识包含字节范围,不能只按 URL 去重。

fMP4 为什么不能漏掉 EXT-X-MAP?

fMP4 的媒体片段通常还需要初始化段提供解码和轨道信息。列表可以用 EXT-X-MAP 指定它,例如:

#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:4
#EXT-X-MAP:URI="init.mp4"
#EXTINF:4.0,
part-01.m4s
#EXT-X-ENDLIST

如果只看到 .m4s 就下载媒体片段,遗漏 init.mp4,媒体引擎可能无法识别轨道。我们的处理会下载初始化段,把它写入浏览器中媒体引擎的文件系统,并改写本地列表里的引用。

初始化段也可能带范围或加密信息。特别是加密状态在后续发生变化时,不能用列表最后出现的密钥去解释之前声明的初始化段。相关加密约束见 RFC 8216 第 4.3.2.5 节,实际修复经过记录在 AES-128 加密误判文章。

下载之后仍需要正确封装

我们没有把远程分片的原始地址直接交给媒体引擎继续联网。转换流程先取得需要的数据,生成本地文件名,再改写一份引用本地资源的列表。分片已经截取为独立数据时,原来针对远程大文件的范围偏移也不能原样保留。时间声明、初始化段和不连续标记则需要按其含义保留。

最后执行的是复制音视频编码的封装,而非重新编码。这样可以避免额外画质损失和昂贵的转码计算,但原始编码必须能够装入目标 MP4。成功解析列表、成功下载数据和成功得到可播放 MP4,是三个独立的检查结果。

当前工具要求已结束的点播列表,即包含 EXT-X-ENDLIST,不负责持续录制直播。存在缺口、仅关键帧列表或其他不支持的结构时,也会停止并提示。对没有结束标记的直播列表,解析出的时长只是当前窗口内分片声明的总和,不能解释为整场直播的长度。

排查转换问题时,先检查基准地址和实际请求,再核对视频与音轨选择,随后查看范围、初始化段及加密信息。只有这些都正确,才进入编码和封装层面的排查。这样能把“下载失败”拆成可验证的问题,而不是对所有故障都尝试简单拼接。