test(hls): [PKG-HLS-SDK-01] checkpoint unresolved Firefox diagnostics

This commit is contained in:
Harvey Zhao committed 2026-09-12 01:42:47 +08:00
1 parent 2f9bee4ee0
commit 26b2983daf
18 files changed
+17776 -8

No files matched your search

+1
View File
@@ -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
+38
View File
@@ -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 的原生/设备/压力与复盘门槛仍必须回来完成。
+23
View File
@@ -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
View File
@@ -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)
+29
View File
@@ -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 配置提取、
+2
View File
@@ -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 |
+37
View File
@@ -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 与新旧核心对照定位切换停滞,验证处置和正常/连续组切换,不能以重复通过关闭。"
}
]
}
+232
View File
@@ -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
})
+25
View File
@@ -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
View File
@@ -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",
+1 -1
View File
@@ -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"
},
{
+11
View File
@@ -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
+202
View File
@@ -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)
})
}
})
}
+19
View File
@@ -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()
}
}
}
}