mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-12 15:26:16 -08:00
test(hls): [PKG-HLS-SDK-01] checkpoint unresolved Firefox diagnostics
This commit is contained in:
1 parent
2f9bee4ee0
commit
26b2983daf
18 files changed
+17776
-8
No files matched your search
@@ -1,5 +1,6 @@
|
||||
* text=auto
|
||||
*.js text eol=lf
|
||||
test/browser/media/hls/*.m3u8 text eol=lf
|
||||
test/browser/fixtures/hls-grouped.m3u8 text eol=lf
|
||||
test/browser/media/hls/*.mpegts binary
|
||||
refactor/baselines/core-vendor/*.txt text eol=lf
|
||||
@@ -95,6 +95,7 @@ node --test test/hls-control.test.js
|
||||
node --test refactor/scripts/hls-types.test.mjs
|
||||
yarn test:browser hls-control.spec.js
|
||||
yarn test:browser hls-editor-types.spec.js
|
||||
yarn test:browser hls-sdk.spec.js
|
||||
yarn build artplayer-plugin-hls-control
|
||||
yarn build:ts
|
||||
yarn ci:check
|
||||
@@ -106,11 +107,23 @@ and test attachments record its bytes. These checks do not replace isolated tarb
|
||||
Use the local demo at `http://localhost:8082` with example `hls.control` during example work.
|
||||
|
||||
Windows Playwright WebKit lacks MSE; explicit capability/error cleanup runs there, while playback
|
||||
cases are marked skipped. Safari/native HLS, SDK workers, grouped track changes, SDK version range,
|
||||
updated example and isolated package verification remain PKG-HLS-05/06 work. See
|
||||
cases are marked skipped. The SDK suite uses frozen Hls.js 1.5.17/1.7.2, real native workers and local
|
||||
media to test replacement, detach/reattach and reordered audio groups with published/current core.
|
||||
Worker messages and termination are observed without substituting worker output. Grouped playlists
|
||||
reuse generated video/audio segments; the Commentary track intentionally reuses the English tone.
|
||||
The fixed releases are two tested points, not a supported version range or new minimum version.
|
||||
Safari/native HLS, external network/device evidence, updated example and isolated package verification
|
||||
remain PKG-HLS-05/06 work. See
|
||||
[refactor validation](../../refactor/hls-validation.md). Keep these gaps visible in release reviews.
|
||||
|
||||
Actual Hls.js 1.5.17 declaration consumers pass TS 5.9.3. TS 4.3.5 reports two existing SDK DOM type
|
||||
errors (MediaDecodingConfiguration and MediaCapabilitiesDecodingInfo), reproduced with the SDK alone.
|
||||
The plugin introduces no additional errors; do not hide those SDK diagnostics with skipLibCheck or
|
||||
claim every SDK/compiler combination passes. Standalone plugin consumers pass both compilers.
|
||||
|
||||
For unresolved Firefox grouped-stream diagnostics, use
|
||||
`node refactor/scripts/hls-sdk-diagnostic.mjs --host direct --transport route --iterations 5`.
|
||||
`--host published|candidate --plugin` adds an actual core and the candidate plugin;
|
||||
`--observe-workers` uses the same native-worker observer as the integration suite. Each run retains
|
||||
its own report and trace files. See the HLS-SDK-01 in-progress record: one target crash and a separate
|
||||
group-switch stall remain unclassified. Passing diagnostic repetitions are not release evidence.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "PKG-HLS-SDK-01",
|
||||
"capturedAt": "2026-09-11T16:52:02.281Z",
|
||||
"metadataUrl": "https://registry.npmjs.org/hls.js/1.7.2",
|
||||
"releases": [
|
||||
{
|
||||
"name": "hls.js",
|
||||
"version": "1.5.17",
|
||||
"tarball": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.17.tgz",
|
||||
"integrity": "sha512-wA66nnYFvQa1o4DO/BFgLNRKnBTVXpNeldGRBJ2Y0SvFtdwvFKCbqa9zhHoZLoxHhZ+jYsj3aIBkWQQCPNOhMw==",
|
||||
"sha256": "8f9e60001992f59dbfb1dbc397e145743c41bb6dde233ec8a7974013c8a031fe",
|
||||
"files": {
|
||||
"package/package.json": "f2172b9942a23ffb46edd4af815cf1ea161db088e7f63dc518d30e55c95d6ae9",
|
||||
"package/LICENSE": "ca8773cf798c7ed997d4dd7c8e23c348699f8d5b7462636694cc14de6cda12db",
|
||||
"package/dist/hls.min.js": "484054e8cd03d3f6d1781fb7f402bdc318d8a4c527f933a95c624e27cc9a9470",
|
||||
"package/src/hls.ts": "b3d3f797669e89bc8d05d2de2aae243ae110811d6738948f173d789cc1ac1f2d",
|
||||
"package/src/controller/audio-track-controller.ts": "ff34f3cb29b66aa5fcba87758bb20d129b052a053808c150ce779e0ceb1e4626"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "hls.js",
|
||||
"version": "1.7.2",
|
||||
"tarball": "https://registry.npmjs.org/hls.js/-/hls.js-1.7.2.tgz",
|
||||
"integrity": "sha512-CW/pPvSOFIRsosbwxrYaE9ERmpTo5fbTqL7wCvuCFlqW1Bmb1K5fXsY7yiH95rmlr4rVuA7UXHbM/cIXQ6AXwg==",
|
||||
"sha256": "14311c46a7aea04a7ffc373653b11d019bb41df8a693a2459eb3115a262aef53",
|
||||
"files": {
|
||||
"package/package.json": "49918bb59328a44f4533dcfb5ecde80893db85072bd52b52721901b4ce239d81",
|
||||
"package/LICENSE": "ca8773cf798c7ed997d4dd7c8e23c348699f8d5b7462636694cc14de6cda12db",
|
||||
"package/dist/hls.min.js": "afcde07437ec84b072fe8782e772ceb5046eac751b2719f73ae0d83d763bc3f5",
|
||||
"package/dist/hls.d.ts": "9571b0113a7103feb8e36e9fbc04c3baba07b5ab3e5c51166b50552ebbbf17f2",
|
||||
"package/src/hls.ts": "e86f0bb70e845814aa2e3a4ff6a9f1f658d1c98fbdc738b02f623c9ddcf6c72c",
|
||||
"package/src/controller/audio-track-controller.ts": "f7ec55e90cc2194e64432db1336d75d0309f6cddf8b55d7eac45b0ebba0517b1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"note": "Fixed integration test inputs only. Latest tag observed as 1.7.2; no range or minimum SDK compatibility claim and no runtime dependency."
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,100 @@
|
||||
# PKG-HLS-SDK-01:真实 worker 与分组轨道验证(进行中)
|
||||
|
||||
本子任务来自 PKG-HLS-05,负责可独立取证的桌面 SDK worker、分组轨道与重绑定。
|
||||
父任务增加对子任务的依赖,仍要求真实设备/SDK 证据,不因拆分而降低完成条件。
|
||||
当前子任务保持 doing,尚无完成提交;HLS-CRASH-01 未解释,不能继承为正常通过。
|
||||
|
||||
## 已实现
|
||||
|
||||
- 固定 Hls.js 1.5.17 和核实时 npm latest 为 1.7.2 的两个归档。SHA-512、SHA-256、
|
||||
选定源/运行/许可文件均核验;历史冻结对象保持相同。版本元数据 URL 及获取时间见
|
||||
baselines/hls-sdk-matrix.json。新版本来源为 https://registry.npmjs.org/hls.js/1.7.2,
|
||||
上游变更核对 https://github.com/video-dev/hls.js/releases,不执行浮动 latest 测试。
|
||||
- test/browser/hls-sdk.spec.js 使用原生 Worker 子类观察 init/transmuxComplete/error/
|
||||
terminate,所有操作委托实际 Worker。视频帧、媒体时钟和 enableWorker 保持启用共同验证,
|
||||
防止只设置开关或发生 SDK main-thread fallback 后仍冒充 worker 通过。
|
||||
- 新旧核心与插件四组合覆盖两个固定 SDK、真实播放/seek、换成无音轨来源和 worker 清理。
|
||||
候选插件另覆盖实际 detach/reattach、两档位不同音轨组、重编号/偏好保持和 UI 选择。
|
||||
Commentary 复用 English 正弦样本,只验证轨道拓扑,不声称真实语音或音频语言识别。
|
||||
- 新增 baseline runner 用例核验 SDK 归档、Apache-2.0 许可和未引入运行时依赖;现有
|
||||
ci:check 纳入它。生产代码/构建产物保持 HLS-04 提交 2f9bee4e,未因 SDK 测试升级生产依赖。
|
||||
|
||||
## 已执行与未解释失败
|
||||
|
||||
完整本地 CI 537 项通过(496 单元、11 工程、30 基线),254 个生产 TS 文件严格检查通过。
|
||||
首轮 Chromium 18 项通过。随后 main/legacy 完整矩阵各 38 passed、16 skipped。
|
||||
强化新实例 worker 启用/错误断言后,一次 main 运行出现 37 passed、16 skipped、1 failed:
|
||||
Firefox、Hls 1.5.17、候选核心在高低音轨组切换后的 destroy 附近出现 Target crashed。
|
||||
错误发生在实际 destroy 返回后读取 worker 终止状态,无法从已关闭页面获取诊断状态。
|
||||
目前没有证据将其归因于插件、核心、Hls、浏览器或内存压力,不称为“环境偶发已解决”。
|
||||
|
||||
随后同样源码的 legacy 完整矩阵和 main 完整复核各 38 passed、16 skipped;
|
||||
Firefox 两个 SDK × 新旧核心分组场景各重复三次,共 12 passed。这些结果不关闭首次失败。
|
||||
另进行 DEBUG=pw:browser + trace=on 的五次重复诊断,保留独立报告/trace 目录。
|
||||
该轮 20 项中 19 通过、1 失败,未复现首次崩溃,但出现另一种失败:Firefox、Hls 1.7.2、
|
||||
旧核心的组切换后音轨组已更新、视频仍为 90P,7 秒未达到 180P。SDK 有非致命 aborted
|
||||
记录,未收到对应 LEVEL_SWITCHED=1。此问题另记 HLS-PLAYBACK-01,不混称崩溃已在旧核心复现。
|
||||
有实际 SDK/浏览器状态及完整 trace,后续需直接 Hls 对照与时序取证;不扩大等待来掩盖。
|
||||
首次失败报告/错误栈保留;首次 trace 所在共享结果目录被后续套件覆盖,不能声称 trace 仍在。
|
||||
后续诊断复制完整 results 至专用 cache 路径,避免再丢失。
|
||||
|
||||
冻结的当前进度见 baselines/hls-sdk-validation.json,状态 partial-unresolved-crash。
|
||||
其中记录首次失败、后续重复、完整矩阵、输入指纹及原生 worker 消息计数,不能只引用绿灯摘要。
|
||||
浏览器报告中的 skipped 仅为 Windows WebKit 无 MSE;每个 SDK 的能力/失败清理实际执行。
|
||||
|
||||
## 剩余与接续
|
||||
|
||||
HLS-CRASH-01/HLS-PLAYBACK-01:继续保留失败并追查 Firefox 销毁崩溃和独立切组卡住;
|
||||
定位前不完成本子任务。
|
||||
适当的下一步是浏览器诊断日志/崩溃证据与直接 Hls+原生 video 对照,避免靠反复重跑宣布修复。
|
||||
父 HLS-05 还包括 Safari/native、真机和完整 SDK/网络范围;HLS-06 包含例子/隔离安装分发。
|
||||
本轮 1.7.2 只提供运行时验证,公开 SDK 类型消费仍以 HLS-04 的实际 1.5.17 为已验证范围。
|
||||
没有将两个 SDK 点版本称为整个区间或新的最低版本。SDK-01/HLS-ENV-01 继续 open。
|
||||
|
||||
当前任务可提交明确标注 checkpoint 的诊断进度,但状态仍为 doing,不计入完成数。
|
||||
满足原验收后仍须独立完成提交;checkpoint 不代表任一风险已关闭。
|
||||
没有 push/tag/publish/merge。全项目目标继续。
|
||||
|
||||
## 后续诊断入口与当前判断
|
||||
|
||||
新增 refactor/scripts/hls-sdk-diagnostic.mjs:固定 SDK、确定性媒体和本地随机端口服务器,
|
||||
可选择普通 HTTP/Playwright route、直接原生 video/发布核心/候选核心、是否安装候选插件、
|
||||
SDK 调试日志、worker 开关/观察器以及直接 video 的销毁顺序。日志包含 SDK/媒体事件、
|
||||
暂停/缓冲区/时钟与最终状态;每次运行使用专用目录保存每轮 trace 和报告。
|
||||
直接模式不加载 ArtPlayer/插件,报告检查其 global 为 undefined;三种宿主的输入字节均记录。
|
||||
此工具用于对照取证,不以诊断重复通过替代原场景或发布验收。
|
||||
|
||||
```sh
|
||||
node refactor/scripts/hls-sdk-diagnostic.mjs --version 1.7.2 --iterations 10 --transport route
|
||||
node refactor/scripts/hls-sdk-diagnostic.mjs --version 1.7.2 --iterations 10 --transport route --host published --plugin --observe-workers
|
||||
node refactor/scripts/hls-sdk-diagnostic.mjs --version 1.5.17 --iterations 5 --transport route --host candidate --plugin --observe-workers
|
||||
```
|
||||
|
||||
共享 test/helpers/worker-observer.js 保留原生 Worker 操作;grouped playlist 移到
|
||||
test/browser/fixtures/hls-grouped.m3u8,内容与先前内嵌字符串相同,避免两套样本漂移。
|
||||
原集成测试补充 paused、readyState、buffered 和 SDK 事件当时的媒体状态,保持原断言及超时。
|
||||
移动 helper 后的一次完整 main 矩阵仍为 38 passed、16 skipped,trace 独立保存。
|
||||
|
||||
初始直接 HTTP/route 及更接近真实切轨完成顺序的 direct 对照各十次通过;
|
||||
旧核心初始化在异步 customType 前暴露 video 尚未赋值,一轮新诊断脚本因此自身失败,
|
||||
已改为动态 getter 与就绪条件,这不是播放器缺陷,不能计入 SDK 通过/失败归因。
|
||||
其后带 SDK 日志的三宿主并行各 15 次通过,关闭 SDK 日志/采用相同条件轮询各十次通过,
|
||||
加入相同 Worker 观察器后各十次通过。没有证明任一宿主/日志/路由/观察器是原因。
|
||||
1.5.17 同样安排直接、旧核心、候选核心观察器对照各五次,结果与版本/指纹另存
|
||||
baselines/hls-sdk-diagnostics.json;旧失败文件 hls-sdk-validation.json 不覆盖。
|
||||
|
||||
1.5.17 对照确实再次捕获崩溃:直接原生 video 与候选核心均出现 page crashed;
|
||||
直接模式没有加载 ArtPlayer/插件(输入文件和 runner 分支可核对)。候选核心关闭 Worker
|
||||
观察器后也出现两次崩溃,所以既不能只归因于核心,也不能只归因于 Worker 观察器。
|
||||
直接模式关闭观察器、以及先销毁 SDK 再清理 video 的有限对照通过;尚不足以确定修复策略。
|
||||
|
||||
这些诊断还暴露早期 runner 的计数问题:操作断言完成后,最后状态读取失败或稍后 crash
|
||||
没有改写 passed。现在状态/trace/关闭错误及 pageerror/crash 都参与最终结果,结束前再次核对。
|
||||
冻结汇总保留原 recordedStatus,并按原始 errors/state.error 重算 effective status,
|
||||
不是通过删除失败来刷新统计。全部有效诊断共 184 passed、6 failed;另有前述十条脚本自身
|
||||
初始化失败单列排除。此处通过数不是发布矩阵通过数,未解释失败仍阻止本子任务完成。
|
||||
|
||||
目前不能从这些有限对照推出“已修复”或具体的“上游缺陷”。两风险保留 open,本子任务保持 doing。
|
||||
下一轮优先比较非插桩/非路由的 1.5.17 worker 销毁与禁用 worker 控制,并保留最后销毁前
|
||||
状态或原生崩溃证据。没有依据提前更改核心公开 destroy 顺序或消费者拥有的 SDK 配置。
|
||||
继续其他无依赖的包迁移时,HLS 的原生/设备/压力与复盘门槛仍必须回来完成。
|
||||
@@ -76,3 +76,26 @@ TS 4.3/5.9 严格校验;真实 Monaco 验证推断、错误提示及编译后
|
||||
MediaCapabilitiesDecodingInfo;仅 SDK 的消费者也出现相同两个诊断。插件无新增诊断,
|
||||
不注入虚假 DOM 定义或打开 skipLibCheck。独立插件五组类型模式均要求零诊断。
|
||||
PKG-HLS-05/06 继续完整 SDK、浏览器、示例及安装包验收,SDK-01/HLS-ENV-01 仍开放。
|
||||
|
||||
## PKG-HLS-SDK-01 桌面 SDK 扩展
|
||||
|
||||
本子任务作为 PKG-HLS-05 前置单独交付,不降低父任务的设备/SDK 验收条件。
|
||||
test/browser/hls-sdk.spec.js 使用冻结的 Hls.js 1.5.17/1.7.2 和真实 native Worker:
|
||||
监听 init/transmuxComplete 消息、错误与 terminate,并且同时验证视频帧、时间推进和销毁。
|
||||
只有 enableWorker=true 或仅创建 Worker 不算通过;实际转封装结果和清理是断言的一部分。
|
||||
这些探针委托原生操作,不注入模拟转封装输出。SDK 仍为测试归档,不进入生产依赖。
|
||||
|
||||
worker 切源/清理覆盖两个 SDK 与新旧核心/插件四组合;候选插件另测 detach/reattach、
|
||||
两档位不同音轨组、组内重新编号和实际 SDK 切轨完成事件、鼠标选择与 UI 清理。
|
||||
高档组有 French/English/Commentary 三项、低档组有 English/French 两项,顺序有意不同。
|
||||
音频是确定性正弦样本,Commentary 复用 English 样本,不冒充真实语音或音频语言识别。
|
||||
|
||||
版本来源、Apache-2.0 许可及成员哈希见 baselines/hls-sdk-matrix.json;新增版本来自
|
||||
[npm 固定 1.7.2 元数据](https://registry.npmjs.org/hls.js/1.7.2),
|
||||
[上游发布记录](https://github.com/video-dev/hls.js/releases) 仅用于核对版本变化。
|
||||
这里不测试所有中间版本或外部服务器,也没有将 1.7.2 的声明改动等同于类型矩阵全部通过。
|
||||
实际 TypeScript SDK 声明消费仍只有 PKG-HLS-04 明确记录的 1.5.17 范围。
|
||||
|
||||
本子任务暂未完成:一次 Firefox 分组切换后的销毁出现 Target crashed,HLS-CRASH-01
|
||||
仍待定位。后续完整矩阵和重复通过不能抹去首次失败;详见
|
||||
[进行中记录](changes/2026-09-12-PKG-HLS-SDK-01-integration.md)。
|
||||
+6
-4
@@ -2,9 +2,9 @@
|
||||
|
||||
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 215 项,范围 22 个包及工作区/示例。
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 216 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 150 / doing 0 / blocked 0 / done 65 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 150 / doing 1 / blocked 0 / done 65 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
| artplayer-plugin-danmuku-mask | 1.1.0 | PKG-MASK-01, PKG-MASK-02, PKG-MASK-03, PKG-MASK-04, PKG-MASK-05, PKG-MASK-06 |
|
||||
| artplayer-plugin-dash-control | 1.1.0 | PKG-DASH-01, PKG-DASH-02, PKG-DASH-03, PKG-DASH-04, PKG-DASH-05, PKG-DASH-06 |
|
||||
| artplayer-plugin-document-pip | 1.1.0 | PKG-DPIP-01, PKG-DPIP-02, PKG-DPIP-03, PKG-DPIP-04, PKG-DPIP-05, PKG-DPIP-06 |
|
||||
| artplayer-plugin-hls-control | 1.1.0 | PKG-HLS-01, PKG-HLS-02, PKG-HLS-03, PKG-HLS-04, PKG-HLS-05, PKG-HLS-06 |
|
||||
| artplayer-plugin-hls-control | 1.1.0 | PKG-HLS-01, PKG-HLS-02, PKG-HLS-03, PKG-HLS-04, PKG-HLS-SDK-01, PKG-HLS-05, PKG-HLS-06 |
|
||||
| artplayer-plugin-jassub | 1.1.0 | PKG-JASSUB-01, PKG-JASSUB-02, PKG-JASSUB-03, PKG-JASSUB-04, PKG-JASSUB-05, PKG-JASSUB-06 |
|
||||
| artplayer-plugin-multiple-subtitles | 1.2.0 | PKG-MULTI-SUB-01, PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-04, PKG-MULTI-SUB-05, PKG-MULTI-SUB-06 |
|
||||
| artplayer-plugin-vast | 1.2.0 | PKG-VAST-01, PKG-VAST-02, PKG-VAST-03, PKG-VAST-04, PKG-VAST-05, PKG-VAST-06 |
|
||||
@@ -192,7 +192,8 @@
|
||||
| PKG-HLS-02 | artplayer-plugin-hls-control<br>建立特有行为与错误测试 | PKG-HLS-01, ENG-03, ENG-05 | Auto/手动实际 level、音轨、去重/过滤和拓扑变化 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
|
||||
| PKG-HLS-03 | artplayer-plugin-hls-control<br>整理内部职责与资源 | PKG-HLS-02, CORE-11, CORE-14 | 映射计算与 selector 更新/删除、Hls 事件订阅分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
|
||||
| PKG-HLS-04 | artplayer-plugin-hls-control<br>迁移自有源码和公开类型 | PKG-HLS-03, ENG-04, ENG-06, CORE-07 | HLS 能力适配类型、回调上下文和旧 getName 参数 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done |
|
||||
| PKG-HLS-05 | artplayer-plugin-hls-control<br>验证新旧核心和组合 | PKG-HLS-04, CORE-22 | 本地多码率 HLS、换成无轨道来源、旧核心和最终核心 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
| PKG-HLS-SDK-01 | artplayer-plugin-hls-control<br>验证真实 SDK worker 与分组轨道组合 | PKG-HLS-04, CORE-22 | 固定历史及当前 SDK 归档、真实 worker/多音轨组、外部选择与重绑定浏览器证据 | 受支持桌面 MSE 引擎与新旧核心通过;无 worker 回退伪通过,设备缺口仍由 PKG-HLS-05 保持未完成 | H | doing |
|
||||
| PKG-HLS-05 | artplayer-plugin-hls-control<br>验证新旧核心和组合 | PKG-HLS-04, CORE-22, PKG-HLS-SDK-01 | 本地多码率 HLS、换成无轨道来源、旧核心和最终核心 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
| PKG-HLS-06 | artplayer-plugin-hls-control<br>验证分发并同步文档 | PKG-HLS-05, ENG-07 | hls.control.js、依赖范围与回退记录 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
|
||||
|
||||
## 5 包迁移:artplayer-plugin-dash-control
|
||||
@@ -469,3 +470,4 @@
|
||||
- PKG-HLS-02: [记录](hls-validation.md) [记录](baselines/hls-sdk.json) [记录](baselines/hls-validation.json) [记录](changes/2026-09-11-PKG-HLS-02-tests.md)
|
||||
- PKG-HLS-03: [记录](changes/2026-09-12-PKG-HLS-03-modules.md) [记录](baselines/hls-modules-validation.json) [记录](hls-validation.md)
|
||||
- PKG-HLS-04: [记录](changes/2026-09-12-PKG-HLS-04-types.md) [记录](baselines/hls-types-validation.json) [记录](hls-validation.md)
|
||||
- PKG-HLS-SDK-01: [记录](changes/2026-09-12-PKG-HLS-SDK-01-integration.md) [记录](baselines/hls-sdk-validation.json) [记录](baselines/hls-sdk-diagnostics.json)
|
||||
@@ -1,5 +1,34 @@
|
||||
# 进度与证据
|
||||
|
||||
## HLS 诊断 checkpoint:已缩小范围,尚未关闭风险
|
||||
|
||||
独立诊断 runner 已加入直接 Hls、发布核心、候选核心对照,以及 HTTP/route、worker/观察器、
|
||||
SDK 日志和直接 video 销毁顺序选项。1.5.17 的直接原生 video 对照也收到了 page crashed;
|
||||
候选核心关闭观察器仍复现,因此不是仅靠替换核心或移除观察器就能解释的问题。
|
||||
有效诊断重算为 184 passed、6 failed,另十条初期 runner 初始化失败独立排除;
|
||||
早期遗漏的末尾状态读取失败已纳入失败统计,没有删除原始证据。
|
||||
|
||||
见 [诊断记录](baselines/hls-sdk-diagnostics.json) 与 [进行中说明](changes/2026-09-12-PKG-HLS-SDK-01-integration.md)。
|
||||
本次保存明确标注 checkpoint 的进度提交,PKG-HLS-SDK-01 继续 doing,HLS-CRASH-01/
|
||||
HLS-PLAYBACK-01 继续 open,完成数仍为 65。完整 CI 537 项通过,最终 main 集成矩阵
|
||||
38 通过、16 无 MSE 跳过;这些通过结果不覆盖已保留的六次诊断失败。
|
||||
未修改生产源码、公开 API 或 destroy 顺序。可继续独立生态包迁移,HLS 的调查与发布门槛仍须完成。
|
||||
|
||||
## 当前实施:PKG-HLS-SDK-01 桌面 SDK 组合与崩溃取证
|
||||
|
||||
从 HLS-05 拆出可独立验证的 worker/分组轨道子任务,父任务仍保留全部设备门槛。
|
||||
Hls 1.5.17/1.7.2 的实际 worker、切源清理、分组音轨和 detach/reattach 测试已加入。
|
||||
完整 CI 537 项、254 个生产 TS 文件通过;main/legacy 完整复核各 38 通过、16 WebKit 无 MSE 跳过。
|
||||
一次 Firefox 在候选核心 + 1.5.17 分组切换后的销毁阶段 Target crashed,尚未归因;
|
||||
后续 12 项重复与完整复核通过不构成关闭证据。HLS-CRASH-01 保持 open,子任务仍 doing。
|
||||
带 trace 的 20 项重复另有 1 项旧核心 + Hls 1.7.2 切组后画面未切到 180P,
|
||||
登记 HLS-PLAYBACK-01;它不是同一种崩溃,完整 trace 已单独保存。
|
||||
|
||||
见 [进行中记录](changes/2026-09-12-PKG-HLS-SDK-01-integration.md) 与
|
||||
[部分证据](baselines/hls-sdk-validation.json)。带浏览器诊断和 trace 的重复另存专用目录;
|
||||
接续调查崩溃及直接 SDK 对照。当前 216 项、65 done、1 doing、150 todo;HLS-04 已独立提交
|
||||
2f9bee4e,本子任务尚未完成/提交,没有发布放行。
|
||||
|
||||
## 最新完成:PKG-HLS-04 严格 TS 与公开类型
|
||||
|
||||
HLS 五个自有模块已迁移严格 TS;公开声明保留旧 object formatter、Parameters 配置提取、
|
||||
|
||||
@@ -124,3 +124,5 @@
|
||||
| HLS-LIFE-01 | resolved / 已复现 | 过期 HLS 选择回调与销毁后 update 仍工作 | PKG-HLS-03 |
|
||||
| HLS-AUTO-01 | resolved / 已复现 | 真实 ABR 播放的 Auto 标签在 update 后丢失 | PKG-HLS-03, PKG-HLS-05 |
|
||||
| HLS-ENV-01 | open / 已复现 | Windows WebKit 缺失 MSE,HLS 播放矩阵尚缺 Safari 证据 | PKG-HLS-05, REL-09, REVIEW-02 |
|
||||
| HLS-CRASH-01 | open / 已复现 | Firefox Hls 1.5.17 worker 销毁可在原生 video 与候选核心复现崩溃 | PKG-HLS-SDK-01, PKG-HLS-05, REVIEW-02 |
|
||||
| HLS-PLAYBACK-01 | open / 已复现 | Firefox 旧核心与 Hls 1.7.2 切组后视频档位未完成切换 | PKG-HLS-SDK-01, PKG-HLS-05, REVIEW-02 |
|
||||
@@ -2606,6 +2606,43 @@
|
||||
],
|
||||
"compatibleResolution": "显式验证宿主不支持,保留 14 skipped,不作为播放通过;补有 MSE 的 Safari/macOS/iOS 环境。",
|
||||
"closureCriteria": "实际 Safari/MSE/native 路径证据齐全,不能仅凭 Windows 能力检查关闭。"
|
||||
},
|
||||
{
|
||||
"id": "HLS-CRASH-01",
|
||||
"title": "Firefox Hls 1.5.17 worker 销毁可在原生 video 与候选核心复现崩溃",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-HLS-SDK-01",
|
||||
"PKG-HLS-05",
|
||||
"REVIEW-02"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/hls-sdk.spec.js",
|
||||
"refactor/changes/2026-09-12-PKG-HLS-SDK-01-integration.md",
|
||||
"refactor/baselines/hls-sdk-diagnostics.json",
|
||||
"refactor/scripts/hls-sdk-diagnostic.mjs"
|
||||
],
|
||||
"compatibleResolution": "直接原生 video(无 ArtPlayer/插件)和候选核心已有 page crashed;候选核心无观察器也复现。继续区分 SDK、Firefox/Playwright 与媒体销毁,不静默改核心 destroy 顺序或调用方 SDK 配置。",
|
||||
"closureCriteria": "取得可解释的崩溃诊断或直接 SDK 对照;验证处置和新旧核心/worker/分组销毁矩阵,不能仅以再次通过关闭。"
|
||||
},
|
||||
{
|
||||
"id": "HLS-PLAYBACK-01",
|
||||
"title": "Firefox 旧核心与 Hls 1.7.2 切组后视频档位未完成切换",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-HLS-SDK-01",
|
||||
"PKG-HLS-05",
|
||||
"REVIEW-02"
|
||||
],
|
||||
"evidence": [
|
||||
"test/browser/hls-sdk.spec.js",
|
||||
"refactor/changes/2026-09-12-PKG-HLS-SDK-01-integration.md",
|
||||
"refactor/baselines/hls-sdk-validation.json"
|
||||
],
|
||||
"compatibleResolution": "区分 SDK、测试媒体/时序和宿主问题,保留真实档位/解码断言及 trace;不扩大等待或只验证菜单。",
|
||||
"closureCriteria": "直接 Hls 与新旧核心对照定位切换停滞,验证处置和正常/连续组切换,不能以重复通过关闭。"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import fs from 'node:fs'
|
||||
import http from 'node:http'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { expect, firefox } from '@playwright/test'
|
||||
import { observeWorkers } from '../../test/helpers/worker-observer.js'
|
||||
import { ensureArchive, hash, readMember, refactorDir } from './releases.mjs'
|
||||
|
||||
// Direct SDK control: no ArtPlayer or plugin is loaded, and no worker output is substituted.
|
||||
export async function diagnose({ version = '1.7.2', iterations = 5, transport = 'http', teardown = 'reset-first', worker = true, host = 'direct', plugin = false, sdkLogs = false, workerObserver = false } = {}) {
|
||||
const runnerSHA256 = hash(fs.readFileSync(new URL(import.meta.url)))
|
||||
const observerSHA256 = hash(fs.readFileSync(new URL('../../test/helpers/worker-observer.js', import.meta.url)))
|
||||
assert(Number.isInteger(iterations) && iterations > 0 && iterations <= 50)
|
||||
assert(['http', 'route'].includes(transport))
|
||||
assert(['reset-first', 'sdk-first'].includes(teardown))
|
||||
assert(['direct', 'published', 'candidate'].includes(host))
|
||||
assert(!plugin || host !== 'direct', 'Plugin comparison requires an ArtPlayer host')
|
||||
const matrix = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/hls-sdk-matrix.json')))
|
||||
const release = matrix.releases.find(item => item.version === version)
|
||||
assert(release, 'Use a frozen SDK version')
|
||||
const sdk = readMember(await ensureArchive(release), 'package/dist/hls.min.js')
|
||||
assert.equal(hash(sdk), release.files['package/dist/hls.min.js'])
|
||||
const media = path.join(refactorDir, '../test/browser/media/hls')
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(media, 'manifest.json')))
|
||||
const files = new Map()
|
||||
for (const [name, expected] of Object.entries(manifest.files)) {
|
||||
const bytes = fs.readFileSync(path.join(media, name))
|
||||
assert.equal(hash(bytes), expected.sha256)
|
||||
files.set(`/${name}`, bytes)
|
||||
}
|
||||
files.set('/grouped.m3u8', fs.readFileSync(path.join(refactorDir, '../test/browser/fixtures/hls-grouped.m3u8')))
|
||||
files.set('/hls.js', sdk)
|
||||
if (host === 'published') {
|
||||
const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/releases.json')))
|
||||
const core = baseline.releases.find(item => item.name === 'artplayer')
|
||||
const bytes = readMember(await ensureArchive(core), 'package/dist/artplayer.js')
|
||||
assert.equal(hash(bytes), core.files['package/dist/artplayer.js'])
|
||||
files.set('/artplayer.js', bytes)
|
||||
}
|
||||
if (host === 'candidate')
|
||||
files.set('/artplayer.js', fs.readFileSync(path.join(refactorDir, '../packages/artplayer/dist/artplayer.js')))
|
||||
if (plugin)
|
||||
files.set('/plugin.js', fs.readFileSync(path.join(refactorDir, '../packages/artplayer-plugin-hls-control/dist/artplayer-plugin-hls-control.js')))
|
||||
files.set('/', Buffer.from('<!doctype html><title>Hls host diagnostic</title><div class="player" style="width:640px;height:360px"></div>'))
|
||||
const contentType = name => name.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl' : name.endsWith('.mpegts') ? 'video/mp2t' : name.endsWith('.js') ? 'text/javascript' : 'text/html'
|
||||
const server = http.createServer((req, res) => {
|
||||
const name = new URL(req.url, 'http://localhost').pathname
|
||||
const bytes = files.get(name)
|
||||
res.writeHead(bytes ? 200 : 404, { 'Content-Type': contentType(name), 'Cache-Control': 'no-store' })
|
||||
res.end(bytes || 'Missing diagnostic fixture')
|
||||
})
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
|
||||
const origin = `http://127.0.0.1:${server.address().port}`
|
||||
const directory = fs.mkdtempSync(path.join(refactorDir, '.cache/hls-direct-'))
|
||||
const results = []
|
||||
let browser
|
||||
let browserVersion
|
||||
try {
|
||||
browser = await firefox.launch({ headless: true })
|
||||
browserVersion = browser.version()
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
const context = await browser.newContext()
|
||||
await context.tracing.start({ screenshots: true, snapshots: true, sources: true })
|
||||
const page = await context.newPage()
|
||||
const wait = predicate => expect.poll(() => page.evaluate(predicate), { timeout: 7000 }).toBe(true)
|
||||
const entry = { iteration, status: 'running', errors: [], requests: [] }
|
||||
results.push(entry)
|
||||
page.on('pageerror', error => entry.errors.push(error.message))
|
||||
page.on('crash', () => entry.errors.push('page crashed'))
|
||||
page.on('requestfailed', request => entry.requests.push({ url: request.url(), error: request.failure() }))
|
||||
if (transport === 'route') {
|
||||
await page.route('**/*.m3u8', route => serveRoute(route))
|
||||
await page.route('**/*.mpegts', route => serveRoute(route))
|
||||
}
|
||||
async function serveRoute(route) {
|
||||
const name = new URL(route.request().url()).pathname
|
||||
const bytes = files.get(name)
|
||||
await route.fulfill({ status: bytes ? 200 : 404, body: bytes || 'Missing fixture', contentType: contentType(name) })
|
||||
}
|
||||
try {
|
||||
await page.goto(origin)
|
||||
if (workerObserver)
|
||||
await page.evaluate(observeWorkers)
|
||||
await page.addScriptTag({ url: `${origin}/hls.js` })
|
||||
if (host !== 'direct')
|
||||
await page.addScriptTag({ url: `${origin}/artplayer.js` })
|
||||
if (plugin)
|
||||
await page.addScriptTag({ url: `${origin}/plugin.js` })
|
||||
await page.evaluate(({ worker, teardown, host, plugin, sdkLogs }) => {
|
||||
let video
|
||||
let hls
|
||||
let art
|
||||
const records = []
|
||||
const buffer = ranges => Array.from({ length: ranges.length }, (_, index) => [ranges.start(index), ranges.end(index)])
|
||||
const state = () => ({ time: video.currentTime, readyState: video.readyState, height: video.videoHeight, paused: video.paused, buffered: buffer(video.buffered), seekable: buffer(video.seekable) })
|
||||
const record = (event, data) => {
|
||||
records.push({ at: performance.now(), event, data, media: state() })
|
||||
if (records.length > 400)
|
||||
records.shift()
|
||||
}
|
||||
const logger = Object.fromEntries(['trace', 'debug', 'log', 'warn', 'info', 'error'].map(level => [level, (...args) => record(`log:${level}`, args.map(String))]))
|
||||
function attach(element) {
|
||||
video = element
|
||||
hls = new window.Hls({ enableWorker: worker, startLevel: 0, debug: sdkLogs ? logger : false })
|
||||
for (const key of ['ERROR', 'AUDIO_TRACKS_UPDATED', 'AUDIO_TRACK_SWITCHED', 'LEVEL_SWITCHED', 'FRAG_BUFFERED', 'BUFFER_FLUSHED']) {
|
||||
hls.on(window.Hls.Events[key], (_, data) => record(key, { id: data.id, level: data.level, fatal: data.fatal, details: data.details, frag: data.frag && { type: data.frag.type, sn: data.frag.sn, level: data.frag.level }, tracks: data.audioTracks?.map(track => ({ id: track.id, name: track.name, group: track.groupId })) }))
|
||||
}
|
||||
for (const name of ['waiting', 'stalled', 'seeking', 'seeked', 'playing', 'pause', 'ended', 'loadedmetadata', 'canplay', 'error'])
|
||||
video.addEventListener(name, () => record(name))
|
||||
hls.loadSource('/grouped.m3u8')
|
||||
hls.attachMedia(video)
|
||||
return hls
|
||||
}
|
||||
if (host === 'direct') {
|
||||
video = document.createElement('video')
|
||||
video.muted = true
|
||||
video.controls = true
|
||||
video.width = 640
|
||||
video.height = 360
|
||||
document.querySelector('.player').append(video)
|
||||
attach(video)
|
||||
}
|
||||
else {
|
||||
art = new window.Artplayer({
|
||||
container: '.player',
|
||||
url: '/grouped.m3u8',
|
||||
muted: true,
|
||||
setting: true,
|
||||
plugins: plugin ? [window.artplayerPluginHlsControl({ quality: { control: true, setting: true }, audio: { control: true, setting: true } })] : [],
|
||||
customType: { m3u8(video, _url, player) {
|
||||
player.hls = attach(video)
|
||||
} },
|
||||
})
|
||||
art.on('destroy', () => hls.destroy())
|
||||
}
|
||||
window.direct = {
|
||||
get hls() { return hls },
|
||||
get video() { return video },
|
||||
records,
|
||||
state,
|
||||
destroy() {
|
||||
record('destroy:start', teardown)
|
||||
if (art) {
|
||||
art.destroy()
|
||||
}
|
||||
else {
|
||||
if (teardown === 'reset-first') {
|
||||
video.removeAttribute('src')
|
||||
video.load()
|
||||
video.remove()
|
||||
}
|
||||
hls.destroy()
|
||||
if (teardown === 'sdk-first') {
|
||||
video.removeAttribute('src')
|
||||
video.load()
|
||||
video.remove()
|
||||
}
|
||||
}
|
||||
record('destroy:end')
|
||||
},
|
||||
}
|
||||
}, { worker, teardown, host, plugin, sdkLogs })
|
||||
await wait(() => window.direct.video?.readyState >= 3)
|
||||
await page.evaluate(() => window.direct.video.play())
|
||||
await wait(() => window.direct.video.currentTime > 0.3)
|
||||
await page.evaluate(() => {
|
||||
window.direct.hls.currentLevel = 0
|
||||
})
|
||||
await wait(() => window.direct.hls.audioTracks.length === 2 && window.direct.hls.audioTracks.every(track => track.groupId === 'low'))
|
||||
await page.evaluate(() => {
|
||||
window.direct.mark = performance.now()
|
||||
window.direct.hls.audioTrack = 1
|
||||
})
|
||||
await wait(() => window.direct.records.some(item => item.at >= window.direct.mark && item.event === 'AUDIO_TRACK_SWITCHED' && item.data.id === 1))
|
||||
await page.evaluate(() => {
|
||||
window.direct.hls.currentLevel = 1
|
||||
})
|
||||
await wait(() => window.direct.video.videoHeight === 180 && window.direct.hls.audioTrack === 0)
|
||||
await page.evaluate(() => {
|
||||
window.direct.hls.audioTrack = 2
|
||||
})
|
||||
await wait(() => window.direct.records.some(item => item.event === 'AUDIO_TRACK_SWITCHED' && item.data.id === 2))
|
||||
await page.evaluate(() => {
|
||||
window.direct.hls.currentLevel = 0
|
||||
})
|
||||
await wait(() => window.direct.hls.audioTracks.length === 2)
|
||||
await page.evaluate(() => window.direct.destroy())
|
||||
await page.evaluate(() => window.direct.state())
|
||||
assert.deepEqual(entry.errors, [])
|
||||
entry.status = 'passed'
|
||||
}
|
||||
catch (error) {
|
||||
entry.status = 'failed'
|
||||
entry.failure = error.message
|
||||
await page.screenshot({ path: path.join(directory, `${iteration}.png`) }).catch(() => {})
|
||||
}
|
||||
finally {
|
||||
entry.state = await page.evaluate(() => ({ coreLoaded: typeof window.Artplayer, pluginLoaded: typeof window.artplayerPluginHlsControl, workers: window.workerEvidence, media: window.direct?.state(), events: window.direct?.records, sdk: { version: window.Hls?.version, level: window.direct?.hls.currentLevel, audioTrack: window.direct?.hls.audioTrack } })).catch(error => ({ error: error.message }))
|
||||
await context.tracing.stop({ path: path.join(directory, `${iteration}.zip`) }).catch((error) => {
|
||||
entry.traceError = error.message
|
||||
})
|
||||
await context.close().catch((error) => {
|
||||
entry.closeError = error.message
|
||||
})
|
||||
if (entry.state.error || entry.errors.length || entry.traceError || entry.closeError) {
|
||||
entry.status = 'failed'
|
||||
entry.failure ||= entry.state.error || entry.traceError || entry.closeError || entry.errors.join('; ')
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ iteration, status: entry.status, failure: entry.failure, media: entry.state.media }))
|
||||
}
|
||||
}
|
||||
finally {
|
||||
await browser?.close()
|
||||
server.closeAllConnections()
|
||||
await new Promise(resolve => server.close(resolve))
|
||||
fs.writeFileSync(path.join(directory, 'report.json'), `${JSON.stringify({ version, iterations, transport, teardown, worker, host, plugin, sdkLogs, workerObserver, runnerSHA256, observerSHA256, browserVersion, release, inputs: Object.fromEntries([...files].map(([file, bytes]) => [file, hash(bytes)])), results }, null, 2)}\n`)
|
||||
console.log(`Diagnostic report: ${directory}`)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
const { values } = parseArgs({ options: { 'version': { type: 'string', default: '1.7.2' }, 'iterations': { type: 'string', default: '5' }, 'transport': { type: 'string', default: 'http' }, 'teardown': { type: 'string', default: 'reset-first' }, 'no-worker': { type: 'boolean', default: false }, 'host': { type: 'string', default: 'direct' }, 'plugin': { type: 'boolean', default: false }, 'sdk-logs': { type: 'boolean', default: false }, 'observe-workers': { type: 'boolean', default: false } } })
|
||||
diagnose({ version: values.version, iterations: Number(values.iterations), transport: values.transport, teardown: values.teardown, worker: !values['no-worker'], host: values.host, plugin: values.plugin, sdkLogs: values['sdk-logs'], workerObserver: values['observe-workers'] }).then((results) => {
|
||||
process.exitCode = results.some(result => result.status !== 'passed') ? 1 : 0
|
||||
}).catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Verify immutable integration inputs in the baseline runner.
|
||||
import test from 'node:test'
|
||||
import { ensureArchive, hash, readMember } from './releases.mjs'
|
||||
|
||||
test('HLS SDK integration matrix keeps historical provenance and verified standalone worker builds', async () => {
|
||||
const matrix = JSON.parse(fs.readFileSync(new URL('../baselines/hls-sdk-matrix.json', import.meta.url)))
|
||||
const historical = JSON.parse(fs.readFileSync(new URL('../baselines/hls-sdk.json', import.meta.url)))
|
||||
assert.deepEqual(matrix.releases.map(release => release.version), ['1.5.17', '1.7.2'])
|
||||
assert.deepEqual(matrix.releases[0], historical.release, 'Do not rewrite the historical SDK baseline')
|
||||
for (const release of matrix.releases) {
|
||||
const archive = await ensureArchive(release)
|
||||
for (const [member, expected] of Object.entries(release.files))
|
||||
assert.equal(hash(readMember(archive, member)), expected, `${release.version} ${member}`)
|
||||
const manifest = JSON.parse(readMember(archive, 'package/package.json'))
|
||||
assert.equal(manifest.name, 'hls.js')
|
||||
assert.equal(manifest.version, release.version)
|
||||
assert.equal(manifest.license, 'Apache-2.0')
|
||||
assert(release.files['package/dist/hls.min.js'])
|
||||
assert(release.files['package/LICENSE'])
|
||||
}
|
||||
const plugin = JSON.parse(fs.readFileSync(new URL('../../packages/artplayer-plugin-hls-control/package.json', import.meta.url)))
|
||||
assert(!plugin.dependencies?.['hls.js'], 'Integration testing must not impose a new runtime SDK dependency')
|
||||
})
|
||||
+23
-1
@@ -1860,6 +1860,27 @@
|
||||
"hls-validation.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-HLS-SDK-01",
|
||||
"phase": "5 包迁移:artplayer-plugin-hls-control",
|
||||
"title": "验证真实 SDK worker 与分组轨道组合",
|
||||
"scope": [
|
||||
"artplayer-plugin-hls-control"
|
||||
],
|
||||
"dependsOn": [
|
||||
"PKG-HLS-04",
|
||||
"CORE-22"
|
||||
],
|
||||
"status": "doing",
|
||||
"risk": "H",
|
||||
"deliverable": "固定历史及当前 SDK 归档、真实 worker/多音轨组、外部选择与重绑定浏览器证据",
|
||||
"acceptance": "受支持桌面 MSE 引擎与新旧核心通过;无 worker 回退伪通过,设备缺口仍由 PKG-HLS-05 保持未完成",
|
||||
"evidence": [
|
||||
"changes/2026-09-12-PKG-HLS-SDK-01-integration.md",
|
||||
"baselines/hls-sdk-validation.json",
|
||||
"baselines/hls-sdk-diagnostics.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-HLS-05",
|
||||
"phase": "5 包迁移:artplayer-plugin-hls-control",
|
||||
@@ -1869,7 +1890,8 @@
|
||||
],
|
||||
"dependsOn": [
|
||||
"PKG-HLS-04",
|
||||
"CORE-22"
|
||||
"CORE-22",
|
||||
"PKG-HLS-SDK-01"
|
||||
],
|
||||
"status": "todo",
|
||||
"risk": "H",
|
||||
|
||||
@@ -1034,7 +1034,7 @@
|
||||
"docs/assets/example/hls.control.js"
|
||||
],
|
||||
"contract": "Caller supplies art.hls; homepage demo loads hls.js 1.5.17. Preserve quality/audio selectors and old core integration.",
|
||||
"validationStatus": "PKG-HLS-03 verifies actual Hls.js 1.5.17 on Chromium/Firefox with source/published core and modern/legacy candidate builds. Auto/topology/lifecycle defects fixed; Windows WebKit has no MSE. Safari/native, detach/reattach, worker, grouped tracks, versions and isolated installation remain HLS-05/06 and EX-03.",
|
||||
"validationStatus": "PKG-HLS-SDK-01 tests pinned Hls.js 1.5.17/1.7.2 with native workers, grouped audio, replacement and detach/reattach across published/current core on desktop MSE engines. Inputs remain test-only archives. Windows WebKit lacks MSE; Safari/native, physical devices, external networks, full SDK types/version range and isolated installation remain HLS-05/06 and EX-03. One Firefox grouped teardown target crash remains unexplained (HLS-CRASH-01); passing subsequent runs are not closure evidence and SDK task remains doing.",
|
||||
"demoPathsSource": "refactor/baselines/demo-inventory.json"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="low",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,URI="english.m3u8"
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="low",NAME="French",LANGUAGE="fr",DEFAULT=NO,AUTOSELECT=YES,URI="french.m3u8"
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="high",NAME="French",LANGUAGE="fr",DEFAULT=NO,AUTOSELECT=YES,URI="french.m3u8"
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="high",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,URI="english.m3u8"
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="high",NAME="Commentary",LANGUAGE="en",DEFAULT=NO,AUTOSELECT=NO,URI="english.m3u8"
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=200000,RESOLUTION=160x90,CODECS="avc1.42c01e,mp4a.40.2",AUDIO="low"
|
||||
low.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=500000,RESOLUTION=320x180,CODECS="avc1.42c01e,mp4a.40.2",AUDIO="high"
|
||||
high.m3u8
|
||||
@@ -0,0 +1,202 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { compilePackage } from '../helpers/load.js'
|
||||
import { observeWorkers } from '../helpers/worker-observer.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const matrix = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/hls-sdk-matrix.json', import.meta.url)))
|
||||
const grouped = fs.readFileSync(new URL('./fixtures/hls-grouped.m3u8', import.meta.url), 'utf8')
|
||||
let candidate
|
||||
let published
|
||||
const sdks = new Map()
|
||||
const media = new Map()
|
||||
|
||||
test.beforeAll(async () => {
|
||||
assert(!process.env.ARTPLAYER_BROWSER_ARTIFACTS, 'HLS installed artifact map remains a separate HLS-06 gate')
|
||||
candidate = process.env.ARTPLAYER_HLS_ARTIFACT
|
||||
? fs.readFileSync(process.env.ARTPLAYER_HLS_ARTIFACT, 'utf8')
|
||||
: await compilePackage('artplayer-plugin-hls-control', 'umd')
|
||||
const baseline = JSON.parse(fs.readFileSync(new URL('../../refactor/baselines/hls-control-release.json', import.meta.url)))
|
||||
const bytes = readMember(await ensureArchive(baseline.release), 'package/dist/artplayer-plugin-hls-control.js')
|
||||
assert.equal(hash(bytes), baseline.release.files['package/dist/artplayer-plugin-hls-control.js'])
|
||||
published = bytes.toString()
|
||||
for (const release of matrix.releases) {
|
||||
const archive = await ensureArchive(release)
|
||||
for (const [member, digest] of Object.entries(release.files))
|
||||
assert.equal(hash(readMember(archive, member)), digest)
|
||||
sdks.set(release.version, readMember(archive, 'package/dist/hls.min.js').toString())
|
||||
}
|
||||
const manifest = JSON.parse(fs.readFileSync(new URL('./media/hls/manifest.json', import.meta.url)))
|
||||
for (const [name, expected] of Object.entries(manifest.files)) {
|
||||
const bytes = fs.readFileSync(new URL(`./media/hls/${name}`, import.meta.url))
|
||||
assert.equal(hash(bytes), expected.sha256)
|
||||
media.set(name, bytes)
|
||||
}
|
||||
media.set('grouped.m3u8', Buffer.from(grouped))
|
||||
})
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
const state = await page.evaluate(() => {
|
||||
const video = window.art?.video
|
||||
return {
|
||||
workers: window.workerEvidence,
|
||||
errors: window.sdkErrors,
|
||||
events: window.sdkEvents,
|
||||
supported: window.Hls?.isSupported(),
|
||||
destroyed: window.destroyedEngines,
|
||||
tracks: window.art?.hls?.audioTracks?.map(track => ({ id: track.id, name: track.name, groupId: track.groupId })),
|
||||
media: video && { time: video.currentTime, paused: video.paused, ended: video.ended, readyState: video.readyState, height: video.videoHeight, buffered: Array.from({ length: video.buffered.length }, (_, index) => [video.buffered.start(index), video.buffered.end(index)]) },
|
||||
}
|
||||
}).catch(error => ({ diagnosticError: error.message }))
|
||||
await testInfo.attach('hls-sdk-state', { contentType: 'application/json', body: JSON.stringify(state) })
|
||||
})
|
||||
|
||||
async function open(page, testInfo, release, core, plugin = 'candidate', manifest = 'master.m3u8') {
|
||||
await testInfo.attach('hls-sdk-inputs', { contentType: 'application/json', body: JSON.stringify({ sdk: release, core, plugin, pluginSHA256: hash(plugin === 'candidate' ? candidate : published), candidate: process.env.ARTPLAYER_HLS_ARTIFACT || 'workspace source build', media: Object.fromEntries([...media].map(([name, bytes]) => [name, hash(bytes)])), enableWorker: true }) })
|
||||
await page.route('**/hls-sdk-fixture/**', async (route) => {
|
||||
const name = new URL(route.request().url()).pathname.split('/').at(-1)
|
||||
const bytes = media.get(name)
|
||||
await route.fulfill({ status: bytes ? 200 : 404, body: bytes || 'Missing test fixture', contentType: name.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl' : 'video/mp2t' })
|
||||
})
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.evaluate(observeWorkers)
|
||||
await page.addScriptTag({ content: sdks.get(release.version) })
|
||||
await page.addScriptTag({ content: plugin === 'candidate' ? candidate : published })
|
||||
await page.evaluate((manifest) => {
|
||||
window.sdkErrors = []
|
||||
window.sdkEvents = []
|
||||
window.destroyedEngines = 0
|
||||
window.art = new window.Artplayer({
|
||||
container: '.player',
|
||||
url: `/hls-sdk-fixture/${manifest}`,
|
||||
muted: true,
|
||||
setting: true,
|
||||
plugins: [window.artplayerPluginHlsControl({ quality: { control: true, setting: true }, audio: { control: true, setting: true } })],
|
||||
customType: { m3u8(video, url, art) {
|
||||
art.hls?.destroy()
|
||||
const hls = new window.Hls({ enableWorker: true, startLevel: 0 })
|
||||
art.hls = hls
|
||||
const mediaState = () => ({ time: video.currentTime, paused: video.paused, readyState: video.readyState, height: video.videoHeight, buffered: Array.from({ length: video.buffered.length }, (_, index) => [video.buffered.start(index), video.buffered.end(index)]) })
|
||||
hls.on(window.Hls.Events.ERROR, (_, data) => window.sdkErrors.push({ type: data.type, details: data.details, fatal: data.fatal, media: mediaState() }))
|
||||
for (const key of ['AUDIO_TRACKS_UPDATED', 'AUDIO_TRACK_SWITCHED', 'LEVEL_SWITCHED', 'MEDIA_ATTACHED', 'MEDIA_DETACHED'])
|
||||
hls.on(window.Hls.Events[key], (_, data) => window.sdkEvents.push({ key, id: data.id, level: data.level, tracks: data.audioTracks?.map(track => ({ id: track.id, name: track.name, groupId: track.groupId })), media: mediaState() }))
|
||||
hls.on(window.Hls.Events.DESTROYING, () => window.destroyedEngines++)
|
||||
hls.loadSource(url)
|
||||
hls.attachMedia(video)
|
||||
} },
|
||||
})
|
||||
window.art.on('destroy', () => window.art.hls.destroy())
|
||||
document.querySelector('#play').onclick = () => window.art.play()
|
||||
document.querySelector('#pause').onclick = () => window.art.pause()
|
||||
}, manifest)
|
||||
}
|
||||
|
||||
async function playing(page) {
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(0.3)
|
||||
await expect.poll(() => page.evaluate(() => window.workerEvidence.some(worker => worker.messages.includes('transmuxComplete')))).toBe(true)
|
||||
expect(await page.evaluate(() => window.art.video.getVideoPlaybackQuality().totalVideoFrames)).toBeGreaterThan(0)
|
||||
expect(await page.evaluate(() => window.art.hls.config.enableWorker)).toBe(true)
|
||||
expect(await page.evaluate(() => window.workerEvidence.flatMap(worker => worker.errors))).toEqual([])
|
||||
}
|
||||
|
||||
async function destroyed(page) {
|
||||
expect(await page.evaluate(() => window.art.hls.config.enableWorker)).toBe(true)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
await expect.poll(() => page.evaluate(() => window.workerEvidence.filter(worker => worker.terminated !== 1).length)).toBe(0)
|
||||
expect(await page.evaluate(() => window.workerEvidence.flatMap(worker => worker.errors))).toEqual([])
|
||||
expect(await page.evaluate(() => ({ media: window.art.hls.media, children: document.querySelector('.player').childElementCount, errors: window.sdkErrors.filter(error => error.fatal) }))).toEqual({ media: null, children: 0, errors: [] })
|
||||
}
|
||||
|
||||
for (const release of matrix.releases) {
|
||||
test(`SDK ${release.version}: MSE capability and unsupported cleanup`, async ({ page, browserName }, testInfo) => {
|
||||
await open(page, testInfo, release, 'candidate')
|
||||
if (process.platform === 'win32' && browserName === 'webkit') {
|
||||
expect(await page.evaluate(() => ({ supported: window.Hls.isSupported(), mse: typeof window.MediaSource, managed: typeof window.ManagedMediaSource }))).toEqual({ supported: false, mse: 'undefined', managed: 'undefined' })
|
||||
await expect.poll(() => page.evaluate(() => window.sdkErrors.some(error => error.fatal))).toBe(true)
|
||||
await page.evaluate(() => window.art.destroy())
|
||||
expect(await page.evaluate(() => ({ media: window.art.hls.media, workers: window.workerEvidence.length, destroyed: window.destroyedEngines }))).toEqual({ media: null, workers: 0, destroyed: 1 })
|
||||
}
|
||||
else {
|
||||
await playing(page)
|
||||
await destroyed(page)
|
||||
}
|
||||
})
|
||||
test.describe(`SDK ${release.version} worker integration`, () => {
|
||||
test.skip(({ browserName }) => process.platform === 'win32' && browserName === 'webkit', 'Windows WebKit has no MSE; explicit capability/error cleanup is tested separately')
|
||||
for (const core of ['published', 'candidate']) {
|
||||
for (const plugin of ['published', 'candidate']) {
|
||||
test(`${core} core / ${plugin} plugin: actual worker playback, source replacement and cleanup`, async ({ page }, testInfo) => {
|
||||
await open(page, testInfo, release, core, plugin)
|
||||
await playing(page)
|
||||
expect(await page.evaluate(() => window.Hls.version)).toBe(release.version)
|
||||
await page.click('#pause')
|
||||
await page.evaluate(() => {
|
||||
window.art.seek = 4
|
||||
})
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(4.2)
|
||||
await page.evaluate(() => window.art.switchUrl('/hls-sdk-fixture/video-only.m3u8'))
|
||||
await expect.poll(() => page.evaluate(() => ({ height: window.art.video.videoHeight, tracks: window.art.hls.audioTracks.length }))).toEqual({ height: 90, tracks: 0 })
|
||||
await playing(page)
|
||||
expect(await page.evaluate(() => window.destroyedEngines)).toBe(1)
|
||||
await destroyed(page)
|
||||
expect(await page.evaluate(() => window.destroyedEngines)).toBe(2)
|
||||
})
|
||||
}
|
||||
test(`${core} core: group changes reindex actual audio tracks and UI`, async ({ page }, testInfo) => {
|
||||
await open(page, testInfo, release, core, 'candidate', 'grouped.m3u8')
|
||||
await playing(page)
|
||||
await page.evaluate(() => {
|
||||
window.art.hls.currentLevel = 0
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.hls.audioTracks.map(track => track.groupId))).toEqual(['low', 'low'])
|
||||
await page.evaluate(() => {
|
||||
window.art.hls.audioTrack = 1
|
||||
})
|
||||
await expect(page.locator('.art-control-hls-audio .art-selector-value')).toHaveText('French')
|
||||
await page.evaluate(() => {
|
||||
window.art.hls.currentLevel = 1
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.hls.audioTracks.map(track => ({ id: track.id, name: track.name, group: track.groupId })))).toEqual([{ id: 0, name: 'French', group: 'high' }, { id: 1, name: 'English', group: 'high' }, { id: 2, name: 'Commentary', group: 'high' }])
|
||||
await expect.poll(() => page.evaluate(() => ({ index: window.art.hls.audioTrack, height: window.art.video.videoHeight }))).toEqual({ index: 0, height: 180 })
|
||||
await expect(page.locator('.art-control-hls-audio .art-selector-value')).toHaveText('French')
|
||||
await page.evaluate(() => {
|
||||
window.art.controls.show = true
|
||||
})
|
||||
const control = page.locator('.art-control-hls-audio')
|
||||
await control.hover()
|
||||
await control.locator('.art-selector-item').filter({ hasText: /^Commentary$/ }).click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.hls.audioTrack)).toBe(2)
|
||||
await expect.poll(() => page.evaluate(() => window.sdkEvents.some(event => event.key === 'AUDIO_TRACK_SWITCHED' && event.id === 2))).toBe(true)
|
||||
await expect(control.locator('.art-selector-value')).toHaveText('Commentary')
|
||||
await page.evaluate(() => {
|
||||
window.art.hls.currentLevel = 0
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.hls.audioTracks.length)).toBe(2)
|
||||
await expect(control.locator('.art-selector-item')).toHaveCount(2)
|
||||
await destroyed(page)
|
||||
})
|
||||
test(`${core} core: actual SDK detaches and reattaches to the same media`, async ({ page }, testInfo) => {
|
||||
await open(page, testInfo, release, core)
|
||||
await playing(page)
|
||||
await page.evaluate(() => {
|
||||
window.art.pause()
|
||||
window.art.hls.detachMedia()
|
||||
window.art.hls.attachMedia(window.art.video)
|
||||
window.art.plugins.artplayerPluginHlsControl.update()
|
||||
})
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(0.3)
|
||||
await expect(page.locator('.art-control-hls-quality')).toHaveCount(1)
|
||||
await expect(page.locator('.art-control-hls-audio')).toHaveCount(1)
|
||||
expect(await page.evaluate(() => window.sdkEvents.filter(event => event.key === 'MEDIA_ATTACHED').length)).toBe(2)
|
||||
await destroyed(page)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Browser-only diagnostic observer; messages and termination still use native workers.
|
||||
export function observeWorkers() {
|
||||
const NativeWorker = window.Worker
|
||||
window.workerEvidence = []
|
||||
window.Worker = class extends NativeWorker {
|
||||
constructor(url, options) {
|
||||
super(url, options)
|
||||
const record = { url: String(url), messages: [], errors: [], terminated: 0 }
|
||||
window.workerEvidence.push(record)
|
||||
this.addEventListener('message', event => record.messages.push(event.data?.event))
|
||||
this.addEventListener('error', event => record.errors.push(event.message))
|
||||
const terminate = this.terminate.bind(this)
|
||||
this.terminate = () => {
|
||||
record.terminated++
|
||||
return terminate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user