# M3u8Parse
**Repository Path**: net_yc60/m3u8-parse
## Basic Information
- **Project Name**: M3u8Parse
- **Description**: 使用Java语言开发的,简便处理M3u8文件的方法,仅用最简单的字符串处理方法
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2025-11-26
- **Last Updated**: 2026-07-10
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# M3u8Parse
> 🎬 一个简洁高效的 **Java M3u8 (HLS) 文件解析工具包**
## 项目简介
这是一个使用 **Java 21** 开发的 M3u8 (HLS) 文件解析工具包,用于简便地处理和分析 M3u8 文件。
> 想了解内部架构与实现原理,请看 [开发文档 DEVELOPMENT.md](./DEVELOPMENT.md)。
### 依赖项
- **Lombok** - 简化代码
- **Apache 工具包** - 提供基础工具支持
> ⚠️ **注意**:本工具包基于 **Java 21** 构建,运行环境需为 Java 21 及以上。
---
## 安装教程
使用 Maven 简单打包即可:
```bash
# 打包源码并安装到本地仓库(跳过测试: testDownloadM3u8 依赖真实网络下载, 按需执行)
mvn clean source:jar install -DskipTests
```
---
## 使用说明
### 1. Maven 中引用该依赖
在项目的 `pom.xml` 中添加:
```xml
com.lang
m3u8-parse
1.4.2
```
### 2. Java 中调用
> **核心只有 `M3u8Parse` 一个类**(唯一入口,负责「解析 + 本地化」)。它返回的数据模型 `M3u8Info`、标签常量 `M3u8Label`、统一异常 `M3u8Exception` 是核心的数据契约;除此之外的 `UrlLocalizer` / `M3u8Editor` / `AdLikelihood` / `Definition` / `PathUtils` 均为**附属工具**,可按需取用,不用也不影响核心解析(见下方第 7、8 节)。
核心操作方法只有一个:
```java
/**
* 获取m3u8文件信息
*
* @param saveDir m3u8文件保存目录
* @param m3u8Url m3u8链接
* @param inputStreamFunction 通过链接获取m3u8文件流
* @return m3u8数据信息
*/
M3u8Info M3u8Parse.getM3u8Info(saveDir, m3u8Url, inputStreamFunction);
```
### 3. 代码示例
```java
M3u8Info m3u8Info = M3u8Parse.getM3u8Info(
"/home/kyll/data",
"http://localhost:8080/test/index.m3u8",
(url) -> (InputStream) HttpClient.get(url)
);
```
**参数说明:**
| 参数 | 说明 |
|------|------|
| `saveDir` | 保存目录。一般 M3u8 (HLS) 都是由一堆小文件组成的,所以需要指定一个专门的目录存放。 |
| `m3u8Url` | M3u8 的网络链接。 |
| `inputStreamFunction` | 需要提供一个 `java.util.Function`,用于获取指定链接的数据流(这个 Function 一般是 `HttpClient.get()` 之类的方法,需要自行实现)。 |
**返回值:** 一个 `M3u8Info` 对象,里面包含了对该 M3u8 链接的数据分析结果。
### 4. M3u8Info 对象详细信息
```
M3u8Info
├── m3u8Url # M3u8 (HLS) 链接
├── version # M3u8 (HLS) 版本
├── streams # 视频或音频流 (List)
│ │
│ │ 说明:streams 包含着这个链接的数据流,这是一个列表。
│ │ 不过大多数情况下里面只有一个对象,因为大部分链接只会提供一种清晰度的视频。
│ │ 但不排除有些链接可以提供多种清晰度,比如标清 / 高清 / 超清。
│ │
│ └── StreamInfo
│ │ 说明:StreamInfo 是数据流对象,里面包含了数据流的基本信息,例如分辨率、编码、格式等等。
│ │ 其中最重要的是 m3u8Detail,里面是详细数据链接。
│ │
│ ├── bandWidth # 码率 BANDWIDTH
│ ├── resolution # 分辨率 RESOLUTION(如 1280x720)
│ ├── definition # 清晰度档位(依据 resolution 高度自动推断,见下方说明,枚举 Definition)
│ ├── codecs # 编码 CODECS
│ ├── audio # 关联音频轨道组(可空)
│ ├── subtitles # 关联字幕轨道组(可空)
│ ├── m3u8Uri # 该流的子清单链接
│ └── m3u8Detail
│ ├── targetDuration # 最大片段时长(秒)
│ ├── mediaSequence # 起始媒体序列号
│ ├── playListType # 播放列表类型(VOD 或 EVENT)
│ └── m3u8ExtList # 具体数据信息流 (List)
│ │ 说明:按 #EXT-X-DISCONTINUITY 不连续点切分的分段列表。
│ └── ExtInfo
│ ├── extInfoKey # 数据流加密信息 #EXT-X-KEY(可空)
│ ├── map # fMP4/CMAF 初始化段 #EXT-X-MAP(可空,fMP4 流必需)
│ ├── mapByteRange # #EXT-X-MAP 的字节范围子属性(可空)
│ └── extInfoDataList # 数据流列表 (List)
│ ├── time # 播放时长
│ ├── byteRange # #EXT-X-BYTERANGE 字节范围(可空,单文件分片时使用)
│ └── fileLink # 链接
│
├── mediaList # 轨道流(可以是音频、字幕)(List)
│
├── localPathMap # <文件网络链接, 本地存储路径> 映射,不含密钥 (Map)
│
├── keyLocalPathMap # <密钥网络链接, 本地存储路径> 映射;密钥已在解析期下载到 saveDir/key/ (Map)
│
└── m3u8ContentLinkInfoList # 各级 M3u8 清单的内容快照 (List)
└── M3u8LinkInfo
├── m3u8Url # M3u8 网络链接
├── originalFilePath # 原版 M3u8 本地存放路径(下载所得原样)
├── originalM3u8Content # 原版 M3u8 内容(内部 URI 保持下载时原样)
├── filePath # 本地化版本 M3u8 存放路径(整个目录可拷贝至任意位置直接播放)
└── m3u8Content # 本地化版本 M3u8 内容(内部 URI 改写为本地相对路径)
```
**使用说明:**
1. **多清晰度视频流筛选**:在实际应用中,我们可能需要对当前链接的数据流进行筛选,比如选择不同清晰度的视频流。这个时候要灵活使用 `streams` 对象,在这个列表中选择你所需的清晰度视频流。
> 清晰度便捷判断:每个 `StreamInfo` 提供 `getDefinition()`,会依据 `RESOLUTION` 的**高度**自动归档为 `Definition` 枚举(`流畅 LD / 标清 SD / 高清 HD / 超清 FHD / 蓝光2K QHD / 4K超高清 UHD_4K / 8K UHD_8K`,无分辨率则为 `UNKNOWN`)。例如挑选最高清晰度:
> ```java
> m3u8Info.getStreams().stream()
> .max(Comparator.comparingInt(s -> s.getDefinition().getMinHeight()))
> .ifPresent(best -> System.out.println("最高清晰度: " + best.getDefinition().getLabel()));
> ```
> 各档位中文名可通过 `Definition#getLabel()` 获取,便于直接展示给用户。
2. **多段视频处理**:有些视频包含多段不同内容(常见于中间插播广告)。这种情况需要深入研究 `M3u8Info#streams#m3u8Detail#m3u8ExtList` 对象——它按 `#EXT-X-DISCONTINUITY` 不连续点切分为若干分段(`ExtInfo`)。每个分段携带各自的加密信息(`extInfoKey`)、fMP4 初始化段(`map`)与片段列表(`extInfoDataList`),调用方可据此自行重组、裁剪或合并视频流。
### 5. 注意事项
- **面向点播(VOD)**:本工具针对点播清单设计,**不支持直播 / 低延迟 HLS(LL-HLS)**——无 `#EXT-X-ENDLIST` 的滚动直播、以及 `#EXT-X-PART` / `#EXT-X-PRELOAD-HINT` / `#EXT-X-SERVER-CONTROL` 等 LL-HLS 标签不会被解析。
- **编码要求(UTF-8)**:清单须为 **UTF-8** 编码(可带 BOM,会自动剥除)。遇到非 UTF-8(如 GBK)编码的清单会**直接抛出 `M3u8Exception` 干净失败**,而不会静默产生乱码内容——请先转码为 UTF-8 再解析。
- **空清单与未知标签容错**:只有头部(`#EXTM3U`)、或有 `#EXT-X-TARGETDURATION` 却无任何片段的空清单,会被安全解析为空结果而非抛异常;未识别的标签(如 `#EXT-X-GAP` 等)会被安全忽略,不影响其前后片段的正常解析。
- **M3u8 文件自动下载**:方法运行中会把 M3u8 文件都下载下来,当然也包含该 M3u8 文件中引用的其他 M3u8 文件都会一并下载,并根据相对路径关系存放在指定的文件夹 `` 中。
- **媒体文件不自动下载**:M3u8 文件中指向的多媒体小文件(ts 片段、fMP4 初始化段等),本方法是不会自动下载的,因为本方法只分析 M3u8 文件里面的信息。**唯一例外是加密密钥**(见下条)。
- **密钥自动下载**:若清单含加密密钥(`#EXT-X-KEY`),解析期会**顺手把所有密钥直接下载**到 `/key/` 下(含段内被轮换的旧密钥、以及跨域外部密钥;同名密钥按顺序改名避免覆盖)。密钥体积很小又是解密播放的前置条件,下好后整个 `` 拷到任意位置即可离线播放。密钥的 `<网络链接, 本地路径>` 映射单独通过 `m3u8Info.getKeyLocalPathMap()` 获取,**不再出现在 `localPathMap` 中**。
- **获取文件映射关系**:用户可以通过以下方法获取到 `<文件网络连接, 本地存储路径>` 的映射关系:
```java
Map webLinkMapLocalPath = m3u8Info.getLocalPathMap();
```
> 这个映射**覆盖除 m3u8 清单与密钥外的全部文件**——所有清晰度 / 音轨的 ts 片段、fMP4 初始化段(`#EXT-X-MAP`)。遍历它即可一次性下载整个视频流所需的多媒体文件(密钥已在解析期自动下好,无需再管)。**含跨域外部 ts**:跳出主清单目录的资源(同站另一目录或真正跨域外链)会镜像到 `/_ext/{目录短哈希}/文件名`,用调用方传入的同一个 `inputStreamFunction` 即可下载,本地化清单也据此改写为相对路径——因此 `localPathMap` 里**不再出现 key == value**,value 恒为 `` 内的本地路径。
>
> **安全保证(可放心据此写盘)**:`localPathMap` 的每个 value 都经过目录穿越校验(`normalize()` + 必须落在 `` 之内),任何试图逃逸 `` 的清单内 URI 都会在解析期直接抛异常。所以调用方按「下载 key,写入 value」的方式落盘,**不会产生路径遍历(Zip-Slip)漏洞**。
> 扩展操作: 关于链接,本项目统一用的是com.lang.hls.M3u8Info.FUrl类。
可以通过webLinkMapLocalPath.get(FUrl.getFullUrl())方法获取到对应的本地存储路径。
- **获取 M3u8 内容集合**:用户可以通过以下方法获取到 `[{本地M3u8文件存放路径, m3u8网络链接, M3u8中的具体内容}]` 的内容集合,方便后期重写:
```java
List m3u8ContentLinkInfoList = m3u8Info.getM3u8ContentLinkInfoList();
```
### 6. 一个简单的用于下载视频的示例
位于测试类 `com.lang.hls.M3u8ParseTest.testDownloadM3u8` 中,里面有一段多媒体视频(香港老剧)的示例。
执行里面的方法后,即可以进行简单的 M3u8 文件流下载。
> 当然,这只不过是一个简单示例而已;如要实现真正的下载功能,需要考虑使用线程池及其他监控速度、进度监控等实现。
下载完成后,在对应的 M3u8 文件所在文件夹内就可以播放了:
```bash
ffplay index.m3u8
```
### 7. 附属工具:广告归纳与去广告(`M3u8Editor`)
`M3u8Editor` 是一个**附属工具类(独立可选)**,解析核心(`M3u8Parse`)不会因它改动一行——它只读取解析阶段已存下的清单原文,不参与解析。
它做三件事:
1. **归纳视频时间段**:`segments(m3u8Info, streamInfo)` 会读取该清晰度媒体清单的原文,重扫 `#EXT-X-CUE-OUT` / `#EXT-X-CUE-IN` 广告插播标记与 `#EXT-X-DISCONTINUITY` 拼接边界,按「**广告状态翻转 或 遇到不连续点**」(正片 → 广告 → 正片……)把片段切成若干**时间段** `AdSegment`。每个时间段可能含 1 个长片段,也可能含多个短片段,整段**共享**一个广告可能性档位。
- 点播里广告常只用 `#EXT-X-DISCONTINUITY` 分隔而不带任何 CUE 标记(如 `/adjump/` 段),故不连续点也作为切段边界;不连续边界 + 时长偏短的段会被提示为「极可能」是广告。
- 清单里既无 CUE 标记也无不连续点时,不会瞎猜造广告,全程归为单一正片段。
- 多清晰度用 `segments(m3u8Info)`,返回 `Map>`,各清晰度各自独立时间轴,互不混合。
2. **打标前预览**:`AdSegment#toM3u8()` 能单独塑造出**该段**的本地化清单文本(相对路径),可直接 `ffplay` 试看,确认是不是广告后再打标。
3. **去广告重塑**:给认定为广告的段 `setAd(true)`,再调用 `filterAd(段列表)` 得到 `FilteredM3u8`。它**只**剔除被显式打标的段(`AdLikelihood` 只是提示,绝不据此自动删段),`FilteredM3u8#toM3u8()` 拼出一份完整、去广告、本地相对路径的点播清单——整个目录拷到哪都能 `ffplay` 播放。
> **逐片段(per-ts)加解密**:`AdSegment#getTsList()` 返回 `List`,每个 `AdTs` 自带其生效的加密信息(`getExtInfoKey()`)与 fMP4 初始化段(`getMap()` / `getMapByteRange()`)——即使同一时间段内 `#EXT-X-KEY` 中途轮换,也已正确绑定到每一片,逐片取用即可。`toM3u8()` 输出的清单全程使用**本地相对路径**(密钥指向解析期已下载到 `saveDir/key/` 的本地文件),整目录拷走即可离线播放。若要一次性下载全部多媒体文件,用 `m3u8Info.getLocalPathMap()`(密钥已自动下好,见 `getKeyLocalPathMap()`)。
```java
M3u8Info info = M3u8Parse.getM3u8Info(saveDir, m3u8Url, fetch);
M3u8Info.StreamInfo stream = info.getStreams().get(0);
// 1. 归纳时间段(正片/广告交替)
List segments = M3u8Editor.segments(info, stream);
for (M3u8Editor.AdSegment seg : segments) {
System.out.printf("段%d [%.1f~%.1f]s 时长%.1fs 片段数%d 广告可能性=%s%n",
seg.getIndex(), seg.getStartTime(), seg.getEndTime(),
seg.getDuration(), seg.getTsList().size(), seg.getAdLikelihood().getLabel());
// 需要时可先预览这段: seg.toM3u8()
}
// 2. 打标(判定权在你):把中间那段标记为广告
segments.get(1).setAd(true);
// 3. 去广告重塑
M3u8Editor.FilteredM3u8 filtered = M3u8Editor.filterAd(segments);
System.out.println("去广告后总时长: " + filtered.getTotalDuration());
String cleanM3u8 = filtered.toM3u8(); // 本地相对路径, 可离线播放
```
> `AdLikelihood` 是「广告可能性」档位(`未知 / 几乎不是 / 可能 / 较可能 / 极可能`),**仅为提示**:广告本身也是正常视频流,库不替你拍板是不是广告,最终由你确认并打标。
### 8. 附属工具:URL 本地化(`UrlLocalizer`)
`UrlLocalizer` 是一个**附属工具类(全静态、独立可选)**,把「清单里引用的网络资源 URL → `saveDir` 内本地落地路径」这段计算单独抽了出来。它就是解析核心 `M3u8Parse` 内部用来算 `localPathMap` 落点、并做目录穿越防护的**同一份实现**——抽出来是为了让**需要自己下载多媒体文件**的调用方能直接复用,保证落地规则与库内完全一致(同一份代码,不存在第二份实现漂移)。
**什么时候用它**:一般情况下你直接遍历 `m3u8Info.getLocalPathMap()`(value 就是安全落地路径)即可,用不到本类。仅当你**绕开 `localPathMap`、要为某些 URL 自行计算落地路径**时(例如自定义下载器、断点续传补算路径),才用它来算出与库一致、且**保证不越界**的本地路径。
```java
// 主清单目录前缀(命中它的资源会扁平落在 saveDir 下)
String prefix = UrlLocalizer.mainDirPrefix("http://host/a/b/index.m3u8");
// 一步得到某资源在 saveDir 内的【安全】落地绝对路径(越界会抛 M3u8Exception)
Path local = UrlLocalizer.localize(saveDir, "http://host/a/b/seg0.ts", prefix);
// 若你已自行拼好相对路径,也可只做目录穿越校验后落地
Path safe = UrlLocalizer.resolveWithinSaveDir(saveDir, "sub/seg1.ts");
```
**它能处理的问题与安全漏洞:**
- **目录穿越 / Zip-Slip**:归一化后强校验「必须落在 `saveDir` 之内」,任何 `../` 穿越、域根绝对路径逃逸、被消解到 `saveDir` 之外的 URI 一律抛 `M3u8Exception`。`startsWith` 按**路径元素**比较,故 `saveDir` 与兄弟目录 `saveDir-evil` 不会混淆;百分号编码的 `%2e%2e` 不会被当成 `..`,只会变成普通子目录名,仍在 `saveDir` 内。
- **同站跨目录 / 真跨域资源**:跳出主清单目录的资源(域根绝对路径指向的同站另一目录、或真正跨域外链)镜像到 `_ext/{目录短哈希}/文件名`,用调用方的同一个 `inputStreamFunction` 即可取回,不产生 `key == value`。
- **http 全路径 / 协议相对 URI**:子清单/片段写成完整 `https://host/...` 或 `//host/...` 时,从归一化后的绝对 URL 反推落点,避免把整条 URL 直接当本地路径(Windows 下 `:` 非法会崩溃)。
- **URL 上的鉴权参数与非法文件名字符**:落地前先剥 `?query` / `#fragment`(它们不属于文件名,且 `? * :` 在 Windows 下非法);下载用的完整 URL(含 token)不受影响。
- **超长深路径(Windows `MAX_PATH`)**:镜像目录用**短哈希**而非原始深层路径命名,规避 260 字符上限,且目录名可复现、同目录资源天然聚合。
> **不负责**的部分:本类只计算「落到哪个本地路径」并保证不越界,**不负责实际下载**,也不校验网络内容。若你拿到路径后**另行拼接**(例如直接用 URL 的 path 段去 `new File(dir, urlPath)`),则不在本类保护范围内——请直接使用它返回的路径。
---
## 示例截图
| 示例图 1 | 示例图 2 |
|---------|---------|
|  |  |
| 示例图 3 | 示例图 4 |
|---------|---------|
|  |  |
---
## 更新内容说明
### v1.4.2
- **修复变体子清单为「相对穿越路径 `../xxx`」时解析崩溃**:v1.4.1 只补上了「域根绝对 `/xxx`」,但子清单写成 `../xxx`、`../../xxx` 这类**相对穿越路径**(跳出主清单所在目录)时仍会崩溃——旧逻辑把普通相对 URI(含 `../`)原样落地,`../` 会逃出 `saveDir` 被目录穿越校验拒绝,导致**整个解析崩溃**。现已把子清单落点与片段**统一**:一律先把 URI 归一化成完整 URL 再计算落点——命中主清单目录则扁平落地,跳出目录(`../` 穿越 / 同站跨目录 / http 全路径一视同仁)镜像到 `/_ext/{目录短哈希}/文件名`。这样既不再崩溃,又让**子清单与它自己的片段按「完整 URL 目录」聚到同一 `_ext` 子目录**,本地化后清单内的相对引用不会断链。(说明:链接前无任何路径的裸文件名 `a.ts` 本就扁平落地;只要前面带路径,都会先归一化再按目录哈希落点,一劳永逸。片段与密钥本就先归一化,`../` 一直正常;本次收敛的是子清单这一条路径。)
### v1.4.1
- **修复变体子清单为「域根绝对路径 `/xxx`」时解析崩溃**:当主清单里的变体流(`#EXT-X-STREAM-INF`)或音轨/字幕轨(`#EXT-X-MEDIA`)的子清单地址写成**域根绝对路径**(单个前导 `/`,如 `/hls/720p/index.m3u8`)时,此前只识别 `http(s)://` 与协议相对 `//`,会把 `/xxx` 当普通相对路径原样落地——在 Linux 下 `/xxx` 是绝对路径、Windows 下变成盘符根,都会逃出 `saveDir` 而被目录穿越校验拒绝,导致**整个解析崩溃**。现已把「域根绝对路径」与「http 全路径」一视同仁:从归一化后的绝对 URL 反推落点(命中主清单目录前缀则扁平落地,否则镜像到 `_ext/`)。真正的 `../` 相对穿越仍按原样交由穿越校验拒绝,安全特性不变。(片段 URL 本就先归一化再计算落点,全是 `/xxx` 一直正常;本次修复的是子清单这一条路径,是 v1.3.3「全路径子清单」修复遗漏的分支。)
### v1.4.0
- **新增附属工具 `UrlLocalizer`(URL 本地化,全静态)**:把「网络资源 URL → `saveDir` 内本地落地路径」的落点计算与**目录穿越(Zip-Slip)防护**从解析核心抽出为独立公共工具类,供**需要自行下载多媒体文件**的调用方直接复用——与核心内部(`localPathMap` 落点、子清单落点、密钥落点)**共用同一份实现**,杜绝第二份规则漂移。它能处理:目录穿越 / 同站跨目录与真跨域镜像 / http 全路径与协议相对 URI / `?query`·`#fragment` 与 Windows 非法文件名字符 / 超长深路径(`MAX_PATH`)。详见「使用说明」第 8 节。
- **文档明确「核心与附属」边界**:核心只有 `M3u8Parse` 一个类(唯一入口,负责解析 + 本地化);`UrlLocalizer` / `M3u8Editor` / `AdLikelihood` / `Definition` / `PathUtils` 均为可按需取用的**附属工具**,不用也不影响核心解析。
- 解析行为与 API 无变化,纯重构 + 文档补充(`M3u8Parse` 现委托 `UrlLocalizer` 完成落点计算,对外结果逐字节一致)。
### v1.3.4
- **跨域外部资源也纳入本地化(`localPathMap` 不再有 key == value)**:此前只有「同站」资源会被本地化,**真正跨域**的外部 ts / init 段仍在 `localPathMap` 里保留 `key == value`(未本地化)。现已统一为:**凡跳出主清单目录的资源(同站跨目录 + 真跨域)一律镜像到 `/_ext/{目录短哈希}/文件名`**,用调用方的同一个 `inputStreamFunction` 下载、本地化清单据此改写为相对路径。跨域资源的目录 URL 含协议与域名,天然与同站资源落在不同的 `_ext` 子目录、互不撞名。至此 `localPathMap` 的 value **恒为 `` 内的本地路径**。
- **路径遍历(Zip-Slip)安全保证 + 不变量测试**:明确 `localPathMap` 的每个 value 都经 `normalize()` + 「必须落在 `` 之内」校验(越界即在解析期抛异常),调用方按「下载 key、写入 value」落盘不会产生目录穿越。新增回归用例 `localPathMapValuesStayWithinSaveDir` 把该不变量锁死。
### v1.3.3
- **修复变体子清单为「http 全路径」时解析崩溃**:当主清单里的变体流(`#EXT-X-STREAM-INF`)或音轨/字幕轨(`#EXT-X-MEDIA`)的子清单地址写成**完整 http 链接**(而非相对路径)时,此前会把整条 URL 直接当本地落地路径,在 Windows 下因 `:` 是非法字符抛 `InvalidPathException` 导致**整个解析崩溃**。现已改为:相对写法保持原有落点(并保留目录穿越拒绝),**全路径写法则从归一化后的绝对 URL 反推**——同站在清单目录内的扁平落地、同站跨目录镜像到 `_ext/`,与片段落地判据一致。(片段本就走归一化 URL,全路径 + 跨目录一直正常,本次修复的是子清单这一条路径。)
### v1.3.2
- **修复 `localPathMap` 同站跨目录资源未本地化**:当片段用**域根绝对路径**(如 `/video/adjump/time/xxx.ts`,点播插播广告最常见的写法)指向**与主清单同站、但跳出了清单所在目录**的资源时,此前会被误判为「跨域外链」而在 `localPathMap` 里留下 **key == value**(未本地化)。现已区分「真正跨域」与「同站跨目录」:同站资源改为镜像落到 `/_ext/{目录短哈希}/文件名` 下(同一目录的资源聚到同一子目录),本地化清单据此改写为相对路径;只有**真正跨域**的外链才继续保留原始网络链接(`key == value`)。
### v1.3.1
- **修复广告判断问题**:`M3u8Editor` 此前切分时间段**只按 `#EXT-X-CUE-OUT/IN` 广告标记**,遗漏了**仅用 `#EXT-X-DISCONTINUITY` 分隔、不带任何 CUE 标记**的广告(点播里最常见,如 `/adjump/` 短片段)。这类清单会被整条并成单一时间段,导致明显偏短的广告块拿不到独立段、广告可能性被判为「未知」而无法提示。现已把 `#EXT-X-DISCONTINUITY` 也作为切段边界,并将其作为广告判定的结构强信号:**不连续边界 + 时长偏短/片段偏少 → 判为「极可能」**(广告结束后正片续接同样带不连续点,但时长不短,仅作「可能」弱提示,避免误伤正片)。
### v1.3.0
- **密钥解析期自动下载**:解析时把清单里的所有加密密钥(`#EXT-X-KEY`,含段内被轮换的旧密钥、以及**跨域外部密钥**)直接下载到 `/key/` 下(同名密钥按顺序改名避免覆盖),整个目录拷到任意位置即可离线解密播放。
- **密钥映射独立**:新增 `m3u8Info.getKeyLocalPathMap()`(`<密钥网络链接, 本地路径>`)。密钥**不再出现在 `localPathMap`** 中——`localPathMap` 从此只覆盖 ts 片段与 fMP4 初始化段。
- **本地化清单指向本地密钥**:本地化后的清单及 `M3u8Editor` 输出,`#EXT-X-KEY` 的 URI 均改写为指向 `saveDir/key/` 的本地相对路径(此前跨域密钥无法本地化的限制随之解除)。
- **修复 `#EXTINF` 标题污染时长**:形如 `#EXTINF:9.5,广告` 的片段(标题为可选、真实清单常见),此前会把标题一并读入时长字段;现已在第一个 `,` 处截断,`time` 只取纯时长值。
- **健壮性边界明确**:清单须为 UTF-8 编码,非 UTF-8 内容会干净失败抛 `M3u8Exception` 而非产生乱码;纯头部 / 空媒体清单被安全解析为空结果而不抛异常;未识别标签(如 `#EXT-X-GAP`)被安全忽略、不影响前后片段解析。
### v1.2.0
- **`localPathMap` 完整映射**:`m3u8Info.getLocalPathMap()` 覆盖**除 m3u8 清单文件外的全部文件**——所有清晰度 / 音轨的 ts、密钥、fMP4 初始化段,含段内被轮换的旧密钥。仅跨域外部密钥(不在主清单目录树下)保留原始网络链接。
- **新增清晰度判断**:新增枚举 `Definition`(`流畅 LD / 标清 SD / 高清 HD / 超清 FHD / 蓝光2K QHD / 4K超高清 UHD_4K / 8K UHD_8K`,外加 `UNKNOWN`),`StreamInfo` 提供 `getDefinition()`。判定按 `RESOLUTION` 高度,兼容 `x/×/*` 分隔符、空格、`1080p` 高度写法,以及 `4K/超清/蓝光` 等中英文关键字命名;无法辨识则归 `UNKNOWN`。
- **新增可选工具 `M3u8Editor`(广告归纳 / 去广告重塑)**:读取解析期已存的清单原文重扫 `#EXT-X-CUE-OUT/IN`,按「连续相同广告状态」切成时间段 `AdSegment`(整段共享 `AdLikelihood` 广告可能性提示,段含 1..N 个片段);`segments(info)` 支持多清晰度独立时间轴。`AdSegment#toM3u8()` 可预览单段;用户 `setAd(true)` 打标后 `filterAd(...)` 得 `FilteredM3u8`,`toM3u8()` 输出去广告的本地相对路径清单(整目录拷走可离线播放)。可能性**仅为提示**,`filterAd` 只删用户显式打标的段,绝不自动过滤。
- **加解密逐片段(per-ts)**:`AdSegment#getTsList()` 返回 `List`,每片各自携带其生效的 `#EXT-X-KEY` / `#EXT-X-MAP`;即使段内密钥中途轮换也已正确绑定到每一片,`toM3u8()` 在密钥变化处逐一重发。
### v1.1.0
- **fMP4/CMAF 支持**:解析 `#EXT-X-MAP` 初始化段(含 `BYTERANGE` 子属性),`ExtInfo` 新增 `map` / `mapByteRange` 字段。
- **单文件分片支持**:解析 `#EXT-X-BYTERANGE`,`ExtInfoData` 新增 `byteRange` 字段;多个片段指向同一资源时 `localPathMap` 自动去重。
- **M3u8 本地化能力**:`M3u8LinkInfo` 扩展为 5 个字段(原版路径/内容 + 本地化路径/内容)。本地化版本把清单内部 URI(子清单 / 分段 / 密钥 / init 段)改写为本地相对路径,整个 `saveDir` 目录可整体拷贝到任意位置,用 `ffmpeg`/`ffplay` 直接播放。
- **文件头校验**:入口处校验 `#EXTM3U` 头,非清单内容(如 CDN 返回的 HTML 错误页)快速失败抛出异常。
- **安全防护**:对子清单相对路径做目录穿越(路径逃逸 / Zip-Slip)校验,越界写入抛异常。
- **`#EXT-X-DISCONTINUITY-SEQUENCE` 兼容**:与 `#EXT-X-DISCONTINUITY` 区分,不误切分段。
- **相对 URI 解析**:统一使用 `java.net.URI.resolve()`(RFC 3986)。
### v1.0.0
- 初始版本。支持 master / media 清单解析,覆盖 `#EXT-X-STREAM-INF`、`#EXT-X-MEDIA`、`#EXT-X-KEY`、`#EXTINF`、`#EXT-X-DISCONTINUITY` 等核心标签,提供 `<网络链接, 本地路径>` 映射。