mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
test(hls): [PKG-HLS-SDK-01] retain controller evidence across page crashes
This commit is contained in:
1 parent
ea3174daa2
commit
e0c5cf27de
11 files changed
+17131
-15
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,96 @@
|
||||
# PKG-HLS-SDK-01 控制器现场与 Firefox 诊断限制
|
||||
|
||||
来源 HEAD:`ea3174daa28e698af3db2ff36aecf4ef2da54470`。本次是进行中任务的检查点,
|
||||
不是插件验收完成;HLS-PLAYBACK-01 与 HLS-CRASH-01 继续 open。
|
||||
|
||||
## 取得的证据与下一步变化
|
||||
|
||||
未改动的分组集成用例在 Firefox 155.0 / Hls.js 1.7.2 / 发布核心 / 候选插件上
|
||||
连续执行 20 次均通过。这只能说明本轮未再现,不推翻先前 0.999146 秒停滞、
|
||||
空媒体缓冲及没有后续 high 视频请求的失败。不能把重复通过作为关闭风险的依据。
|
||||
|
||||
对固定 SDK 增加可选 `--controller-state`:在已有事件及 buffer/fragment 事件上
|
||||
读取主/音频控制器、独立 SourceBuffer、当前/上一片段、endList 和片段跟踪状态。
|
||||
仍用原来最多 400 条的事件队列,未插入 play/seek、关闭 Worker、改变 SDK 参数或
|
||||
扩大断言超时。未开启时不安装观察函数,也不增加这些观察事件。
|
||||
启用后还通过页面绑定异步复制事件到 Node 的独立400条队列,页面崩溃后仍保留已收到的
|
||||
记录;最后尚未送达的事件可能丢失,receivedDuring 仅表示宿主收到时的阶段。
|
||||
无穷大的 flush 终点写为字符串 Infinity,避免 JSON 把它变成 null 后产生歧义。
|
||||
|
||||
新的成功对照也出现过主控制器 ENDED、video.buffered 为空,但视频 SourceBuffer
|
||||
仍有 0–12 秒数据,音频 SourceBuffer 暂时为空的组合。其后正常恢复并双向切档实播。
|
||||
因此,单独看到 ENDED 和 video.buffered=[] 不足以认定根因;后续失败需要同时保留
|
||||
音频/视频缓冲、片段追踪和 flush 顺序,不能据此强行 reset 主控制器。
|
||||
|
||||
## 实现与验证
|
||||
|
||||
`test/helpers/hls-controller-state.js` 是可序列化进页面的只读快照函数,仅用于
|
||||
两个冻结 SDK。fragmentTracker 归属 stream/audio controller;1.5.17 的
|
||||
sourceBuffer 字典与 1.7.2 的 sourceBuffers 数组分别读取。loaded 对象会含循环
|
||||
引用,只记录是否存在,不导出整个对象。销毁后不读取失效的公共档位 getter。
|
||||
1.5.17 的 audio controller 从 networkControllers 中按 playlistType 识别;
|
||||
1.7.2 则可读取 audioStreamController,不能因字段缺失误报没有音频控制器。
|
||||
原生 range getter 失败时返回单独 diagnosticError,不覆盖播放器原失败。
|
||||
|
||||
新增四项 Node/独立 VM 测试覆盖页面序列化、循环引用、旧版缓冲布局、销毁 getter
|
||||
和采集错误;与既有固定 SDK 来源测试合计 5 通过,定向 lint 通过。
|
||||
|
||||
最终观察器实际运行:直接 native video 两 SDK 各一次,发布核心+候选插件两 SDK
|
||||
各一次;四次均通过且均取得非空独立缓冲、片段跟踪和 endList 记录,无采集错误。
|
||||
两版都有音频控制器状态,并验证 Node 收到记录与页面队列数量相同。
|
||||
原默认模式 Hls.js 1.7.2 另运行一次通过,未开启 controller/Worker observer 或 SDK logs。
|
||||
这五次不是全包矩阵。原集成测试的 20 次在新增观察器前执行,单独记录。
|
||||
|
||||
最初七次观察运行虽完成播放,但 tracker 路径错误导致字段缺失;修正归属后又做
|
||||
四次,确认 tracker 可读,不过 1.5.17 的独立缓冲字段仍未收集。补上旧缓冲布局的
|
||||
早期四次和默认一次也保存;之后补齐页面外保留及1.5音频定位,再做最终验证。
|
||||
所有这些日志分别保留,不能把早期部分附件当作完整控制器证据。
|
||||
lint 首轮三处 runner 说明/格式问题已修正;没有更新 SDK 或媒体来制造通过。
|
||||
|
||||
## 本轮再次捕获的真实崩溃
|
||||
|
||||
上述改进期间,直接 native video 的两个诊断序列各发生一次 Hls.js 1.5.17 页面崩溃。
|
||||
第一个序列为一失败一通过,首个1.5.17就在 high-group 阶段崩溃,无更早SDK、ArtPlayer
|
||||
或插件;当时只有页内队列,崩溃后读不到现场。第二个序列为三通过一失败,失败是第三项
|
||||
1.5.17,Node 在页面失联后仍保留97条已收到事件。两个序列退出码均为1,最终通过不覆盖它们。
|
||||
|
||||
第二次最后记录:播放位置0.381837、readyState=4、高度90、9帧;先发audio flush,
|
||||
再选择音轨、高组,最后发video flush。最后video SourceBuffer仍为0–8秒,audio为空,
|
||||
两者updating=true,main从PARSED进入IDLE,endList为空。此时尚未执行destroy。
|
||||
crash事件在宿主low-group阶段收到,后续high-group等待抛出Target crashed;保留两种
|
||||
阶段信息,不把宿主阶段标签当精确原生崩溃位置。最后收到的事件不一定是最后执行的事件。
|
||||
这些记录增强了独立SDK/Firefox路径证据,但没有定位原生模块、异常代码或堆栈;
|
||||
尤其不能把此页面崩溃与1.7.2的播放停滞合并成同一个已证明根因。
|
||||
|
||||
## Firefox 原生崩溃证据的限制
|
||||
|
||||
实际安装目录 firefox-1543/firefox/omni.ja 的
|
||||
`chrome/toolkit/content/global/buildconfig.html` 明确包含 `--disable-crashreporter`。
|
||||
同目录 playwright.cfg 第350行锁定 toolkit.crashreporter.enabled=false,且没有
|
||||
crashreporter/minidump-analyzer 可执行文件。自动导航 about:buildconfig 超时后,
|
||||
改为只读提取本地归档成员;并未改写浏览器、偏好或系统注册表。
|
||||
|
||||
[Mozilla 文档](https://firefox-source-docs.mozilla.org/toolkit/crashreporter/crashreporter/index.html)
|
||||
说明环境变量可控制已构建的 crash reporter,但不能让本机已禁用的编译功能出现。
|
||||
所以这里没有添加一个虚假的“设置变量即可收集 minidump”选项,也没有声称取得原生堆栈。
|
||||
后续需要针对本任务创建的准确进程收集 Windows 原生异常,或使用具备相应功能的独立
|
||||
浏览器对照。可选工具依据见
|
||||
[Microsoft ProcDump](https://learn.microsoft.com/en-us/sysinternals/downloads/procdump)。
|
||||
本轮仅查阅该工具说明,未安装、附加调试器、设置系统级崩溃捕获或上传报告。
|
||||
|
||||
## 重跑与交接
|
||||
|
||||
```sh
|
||||
yarn test:browser hls-sdk.spec.js --project=firefox --grep "1.7.2.*published core: group changes" --repeat-each=20 --workers=1
|
||||
node refactor/scripts/hls-sdk-diagnostic.mjs --sequence 1.5.17,1.7.2 --switch-boundary immediate --controller-state --capture-before-destroy
|
||||
node refactor/scripts/hls-sdk-diagnostic.mjs --sequence 1.5.17,1.7.2 --host published --plugin --switch-boundary selected --controller-state --capture-before-destroy
|
||||
node --test refactor/scripts/hls-controller-state.test.mjs refactor/scripts/hls-sdk-matrix.test.mjs
|
||||
```
|
||||
|
||||
Windows / Node 24.21.0 / Yarn Classic 1.22.22;两个 SDK 都保持 enableWorker=true。
|
||||
原分组停滞与独立原生页面崩溃仍未定位,后续优先获取失败现场或进程异常证据;
|
||||
不要再把无观察器的重复绿色结果作为新修复。来源、实际样本和日志指纹见
|
||||
[机器证据](../baselines/hls-controller-validation.json)。
|
||||
|
||||
回退本检查点仅撤销诊断采集、相应测试与说明。没有生产源码、公开 API、类型、
|
||||
依赖、锁文件或分发产物变化,没有推送、部署或发布。
|
||||
@@ -99,3 +99,13 @@ worker 切源/清理覆盖两个 SDK 与新旧核心/插件四组合;候选插
|
||||
本子任务暂未完成:一次 Firefox 分组切换后的销毁出现 Target crashed,HLS-CRASH-01
|
||||
仍待定位。后续完整矩阵和重复通过不能抹去首次失败;详见
|
||||
[进行中记录](changes/2026-09-12-PKG-HLS-SDK-01-integration.md)。
|
||||
|
||||
后续的分组停滞与原生崩溃保持独立风险。固定 SDK 诊断现在可加
|
||||
`--controller-state` 同时记录控制器、音视频 SourceBuffer 和片段追踪;该观察器
|
||||
位于 test/helpers/hls-controller-state.js,不进入生产插件。默认不开启,避免把
|
||||
额外观察导致的时序变化当修复。2026-09-15 的20次原测试通过仍不关闭风险;
|
||||
成功对照也能短暂出现 ENDED+空媒体缓冲,必须看独立缓冲和后续恢复。
|
||||
本机 Firefox 编译参数禁用了 crash reporter,不能承诺环境变量能生成 minidump。
|
||||
随后直接1.5.17两次切组崩溃均在destroy之前;可选观察器向Node复制事件后,
|
||||
其中一次页面失联仍保留97条事件。此处有最后收到的音视频flush状态,尚无原生堆栈。
|
||||
详情及后续取证方向见[控制器检查点](changes/2026-09-15-PKG-HLS-SDK-01-controller-state.md)。
|
||||
+1
-1
@@ -573,7 +573,7 @@
|
||||
- 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) [记录](changes/2026-09-14-PKG-HLS-SDK-01-http-teardown.md) [记录](baselines/hls-http-teardown-validation.json) [记录](changes/2026-09-15-PKG-HLS-SDK-01-switch-boundary.md) [记录](baselines/hls-switch-boundary-validation.json) [记录](changes/2026-09-15-PKG-HLS-SDK-01-sdk-sequence.md) [记录](baselines/hls-sdk-sequence-validation.json)
|
||||
- PKG-HLS-SDK-01: [记录](changes/2026-09-12-PKG-HLS-SDK-01-integration.md) [记录](baselines/hls-sdk-validation.json) [记录](baselines/hls-sdk-diagnostics.json) [记录](changes/2026-09-14-PKG-HLS-SDK-01-http-teardown.md) [记录](baselines/hls-http-teardown-validation.json) [记录](changes/2026-09-15-PKG-HLS-SDK-01-switch-boundary.md) [记录](baselines/hls-switch-boundary-validation.json) [记录](changes/2026-09-15-PKG-HLS-SDK-01-sdk-sequence.md) [记录](baselines/hls-sdk-sequence-validation.json) [记录](changes/2026-09-15-PKG-HLS-SDK-01-controller-state.md) [记录](baselines/hls-controller-validation.json)
|
||||
- PKG-DASH-01: [记录](changes/2026-09-12-PKG-DASH-01-contract.md) [记录](baselines/dash-control-contract.md) [记录](baselines/dash-control-release.json)
|
||||
- PKG-DASH-02: [记录](changes/2026-09-12-PKG-DASH-02-tests.md) [记录](dash-validation.md) [记录](baselines/dash-validation.json)
|
||||
- PKG-DASH-03: [记录](changes/2026-09-12-PKG-DASH-03-runtime.md) [记录](baselines/dash-runtime.json) [记录](dash-validation.md)
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-HLS-SDK-01 控制器与原生诊断检查点
|
||||
|
||||
原Firefox1.7.2切组测试20次通过,不关闭历史停滞。新增可选controller-state,
|
||||
读取固定SDK两版的控制器、独立音视频缓冲与片段跟踪,异步保留到Node队列。
|
||||
最终直接宿主/发布核心各两次通过并取得两版完整字段,默认模式一次通过;回归与
|
||||
SDK来源测试5项、lint通过。早期缺tracker/旧缓冲/音频字段的记录保留为部分证据。
|
||||
|
||||
本轮另两次直接1.5.17在destroy之前页面崩溃,无ArtPlayer/插件,其中一次是该
|
||||
浏览器序列首项;新增Node采集在第二次保住97条事件,最后处于audio/video flush
|
||||
与切高组阶段。仍无原生异常代码或堆栈,不将该崩溃和1.7.2停滞合并为同一根因。
|
||||
本机Firefox归档确认--disable-crashreporter,后续需限定任务进程的原生异常取证。
|
||||
详见[记录](changes/2026-09-15-PKG-HLS-SDK-01-controller-state.md)与
|
||||
[证据](baselines/hls-controller-validation.json)。
|
||||
|
||||
本任务继续doing,两项HLS风险继续open;207 done、21 doing、43 todo,总271不变。
|
||||
没有生产代码、公开API、依赖、版本或分发修改,没有推送或发布。
|
||||
|
||||
## SITE-DANMUKU-01 双语指南和示例验证
|
||||
|
||||
新增 Danmuku 英文指南/侧栏,按源码修正中文 Promise/owner、加载、回调、mount、
|
||||
|
||||
+9
-4
@@ -2815,11 +2815,13 @@
|
||||
"refactor/changes/2026-09-14-PKG-HLS-SDK-01-http-teardown.md",
|
||||
"refactor/baselines/hls-http-teardown-validation.json",
|
||||
"refactor/changes/2026-09-15-PKG-HLS-SDK-01-sdk-sequence.md",
|
||||
"refactor/baselines/hls-sdk-sequence-validation.json"
|
||||
"refactor/baselines/hls-sdk-sequence-validation.json",
|
||||
"refactor/changes/2026-09-15-PKG-HLS-SDK-01-controller-state.md",
|
||||
"refactor/baselines/hls-controller-validation.json"
|
||||
],
|
||||
"compatibleResolution": "直接原生 video(无 ArtPlayer/插件)和候选核心已有 page crashed;候选核心无观察器也复现。继续区分 SDK、Firefox/Playwright 与媒体销毁,不静默改核心 destroy 顺序或调用方 SDK 配置。",
|
||||
"closureCriteria": "取得可解释的崩溃诊断或直接 SDK 对照;验证处置和新旧核心/worker/分组销毁矩阵,不能仅以再次通过关闭。",
|
||||
"workspaceState": "2026-09-14 direct HTTP/no ArtPlayer/plugin/Worker observer:35 diagnostics,31 passed4 failed. Worker reset-first fails1/10 and1/5; no-worker10/10 and optional pre-destroy snapshot5/5 pass. SDK-first also fails2/5. Three phase-recorded crashes occur during final-state collection after destroy returned. Installed Firefox TargetRegistry observer receives oop-frameloader-crashed; no native stack/root cause established. Do not substitute no-worker/snapshot success or change core teardown order; risk remains open. 2026-09-15 sequence: direct native video first 1.5.17 case reports page crash in high-group phase before explicit destroy; no earlier SDK case, ArtPlayer, plugin, route, SDK logger or Worker observer. Three ordered sequences plus one default invocation total 8 passed/1 failed. Native cause remains unproven; do not restrict investigation to teardown or merge distinct signals into a proven shared cause."
|
||||
"workspaceState": "2026-09-14 direct HTTP/no ArtPlayer/plugin/Worker observer:35 diagnostics,31 passed4 failed. Worker reset-first fails1/10 and1/5; no-worker10/10 and optional pre-destroy snapshot5/5 pass. SDK-first also fails2/5. Three phase-recorded crashes occur during final-state collection after destroy returned. Installed Firefox TargetRegistry observer receives oop-frameloader-crashed; no native stack/root cause established. Do not substitute no-worker/snapshot success or change core teardown order; risk remains open. 2026-09-15 sequence: direct native video first 1.5.17 case reports page crash in high-group phase before explicit destroy; no earlier SDK case, ArtPlayer, plugin, route, SDK logger or Worker observer. Three ordered sequences plus one default invocation total 8 passed/1 failed. Native cause remains unproven; do not restrict investigation to teardown or merge distinct signals into a proven shared cause. 2026-09-15 installed Firefox 1543 local omni.ja buildconfig includes --disable-crashreporter; playwright.cfg locks crash reporter off. No native stack collected. Environment-variable-only capture cannot restore a compile-disabled component; next evidence needs scoped Windows native debugging or a separately validated browser build. Later in this checkpoint, direct 1.5.17 crashed twice before destroy (one first-in-sequence); no ArtPlayer/plugin. Optional Node event queue retained 97 events in the second crash, ending during overlapping audio/video flush and high selection at media time 0.381837. Crash receipt was low-group; following assertion failure was high-group. No native module/exception/stack established."
|
||||
},
|
||||
{
|
||||
"id": "HLS-PLAYBACK-01",
|
||||
@@ -2836,10 +2838,13 @@
|
||||
"refactor/changes/2026-09-12-PKG-HLS-SDK-01-integration.md",
|
||||
"refactor/baselines/hls-sdk-validation.json",
|
||||
"refactor/changes/2026-09-15-PKG-HLS-SDK-01-switch-boundary.md",
|
||||
"refactor/baselines/hls-switch-boundary-validation.json"
|
||||
"refactor/baselines/hls-switch-boundary-validation.json",
|
||||
"refactor/changes/2026-09-15-PKG-HLS-SDK-01-controller-state.md",
|
||||
"refactor/baselines/hls-controller-validation.json"
|
||||
],
|
||||
"compatibleResolution": "区分 SDK、测试媒体/时序和宿主问题,保留真实档位/解码断言及 trace;不扩大等待或只验证菜单。",
|
||||
"closureCriteria": "直接 Hls 与新旧核心对照定位切换停滞,验证处置和正常/连续组切换,不能以重复通过关闭。"
|
||||
"closureCriteria": "直接 Hls 与新旧核心对照定位切换停滞,验证处置和正常/连续组切换,不能以重复通过关闭。",
|
||||
"workspaceState": " 2026-09-15 unchanged integration 20/20 passes do not close historical failure. Optional --controller-state now captures fixed SDK controller/buffer/fragment tracker state. Final direct and published-core controls pass for both SDKs; successful cases can briefly show main ENDED and empty media ranges while video SourceBuffer remains populated. Original failure still needs those independent buffers and tracker state; no forced-reset workaround."
|
||||
},
|
||||
{
|
||||
"id": "AUDIO-LIFE-01",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import assert from 'node:assert/strict'
|
||||
// eslint-disable-next-line test/no-import-node-test -- The baseline runner verifies page-serialized diagnostics in an isolated Node VM.
|
||||
import { test } from 'node:test'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { snapshotHlsControllers } from '../../test/helpers/hls-controller-state.js'
|
||||
|
||||
test('fixed SDK controller observer serializes in a fresh realm and avoids cyclic loaded data', () => {
|
||||
const snapshot = runInNewContext(`(${snapshotHlsControllers.toString()})`)
|
||||
const source = { buffered: { length: 1, start: () => 0, end: () => 12 }, updating: false }
|
||||
const body = { sn: 5, level: 1, type: 'main', start: 10, duration: 2 }
|
||||
const loaded = { frag: body }
|
||||
body.loader = { loaded }
|
||||
const entity = { body, loaded, buffered: true }
|
||||
const fragmentTracker = Object.freeze({ endListFragments: { main: entity }, fragments: { main_1_5: entity } })
|
||||
const hls = Object.freeze({
|
||||
streamController: Object.freeze({ state: 'ENDED', level: 1, nextLoadPosition: 12, altAudio: 2, mediaBuffer: source, videoBuffer: source, fragPrevious: body, fragmentTracker }),
|
||||
bufferController: Object.freeze({ sourceBuffers: [['video', source]], mediaSource: { readyState: 'open' } }),
|
||||
})
|
||||
const result = JSON.parse(JSON.stringify(snapshot(hls)))
|
||||
assert.equal(result.main.state, 'ENDED')
|
||||
assert.equal(result.main.mediaBuffer, 'video')
|
||||
assert.deepEqual(result.main.buffered, [[0, 12]])
|
||||
assert.deepEqual(result.endList.main, { fragment: { sn: 5, level: 1, type: 'main', start: 10, duration: 2 }, buffered: true, loaded: true })
|
||||
assert.deepEqual(result.tracked, [{ key: 'main_1_5', buffered: true, loaded: true }])
|
||||
assert.equal(entity.loaded, loaded)
|
||||
assert.equal(source.buffered.length, 1)
|
||||
})
|
||||
|
||||
test('detached and destroyed SDK snapshots do not invoke invalid public level getters', () => {
|
||||
const hls = {
|
||||
streamController: null,
|
||||
bufferController: null,
|
||||
fragmentTracker: null,
|
||||
get currentLevel() {
|
||||
throw new Error('destroyed getter')
|
||||
},
|
||||
}
|
||||
assert.equal(snapshotHlsControllers(hls).diagnosticError, undefined)
|
||||
assert.equal(snapshotHlsControllers(undefined).diagnosticError, undefined)
|
||||
assert.equal(snapshotHlsControllers({ streamController: { state: 'STOPPED' } }).main.state, 'STOPPED')
|
||||
})
|
||||
|
||||
test('SDK 1.5 buffer dictionary and audio controller tracker remain readable', () => {
|
||||
const source = { updating: false, buffered: { length: 0 } }
|
||||
const result = snapshotHlsControllers({ bufferController: { sourceBuffer: { audio: source } }, networkControllers: [{ playlistType: 'main' }, { playlistType: 'audio', state: 'IDLE', fragmentTracker: { fragments: {}, endListFragments: {} } }] })
|
||||
assert.deepEqual(result.buffers, [{ type: 'audio', updating: false, buffered: [] }])
|
||||
assert.deepEqual(result.endList, {})
|
||||
assert.deepEqual(result.tracked, [])
|
||||
assert.equal(result.audio.state, 'IDLE')
|
||||
})
|
||||
|
||||
test('native range getter failure is recorded without replacing the player failure', () => {
|
||||
const hls = { streamController: {
|
||||
get mediaBuffer() {
|
||||
throw new Error('released buffer')
|
||||
},
|
||||
} }
|
||||
assert.equal(snapshotHlsControllers(hls).diagnosticError, 'Error: released buffer')
|
||||
})
|
||||
@@ -6,13 +6,15 @@ import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { expect, firefox } from '@playwright/test'
|
||||
import { snapshotHlsControllers } from '../../test/helpers/hls-controller-state.js'
|
||||
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, captureBeforeDestroy = false, switchBoundary = 'switched', prefill = false, sequence } = {}) {
|
||||
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, controllerState = false, captureBeforeDestroy = false, switchBoundary = 'switched', prefill = false, sequence } = {}) {
|
||||
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)))
|
||||
const controllerObserverSHA256 = hash(fs.readFileSync(new URL('../../test/helpers/hls-controller-state.js', import.meta.url)))
|
||||
assert(Number.isInteger(iterations) && iterations > 0 && iterations <= 50)
|
||||
assert(['http', 'route'].includes(transport))
|
||||
assert(['reset-first', 'sdk-first'].includes(teardown))
|
||||
@@ -76,6 +78,14 @@ export async function diagnose({ version = '1.7.2', iterations = 5, transport =
|
||||
const wait = predicate => expect.poll(() => page.evaluate(predicate), { timeout: 7000 }).toBe(true)
|
||||
const entry = { iteration, version: versions[iteration], status: 'running', errors: [], requests: [], phase: 'setup', crashes: [] }
|
||||
results.push(entry)
|
||||
if (controllerState) {
|
||||
entry.observedEvents = []
|
||||
await page.exposeFunction('__recordHlsControllerState', (event) => {
|
||||
entry.observedEvents.push({ receivedDuring: entry.phase, ...event })
|
||||
if (entry.observedEvents.length > 400)
|
||||
entry.observedEvents.shift()
|
||||
})
|
||||
}
|
||||
page.on('pageerror', error => entry.errors.push(error.message))
|
||||
page.on('crash', () => {
|
||||
entry.errors.push('page crashed')
|
||||
@@ -93,6 +103,8 @@ export async function diagnose({ version = '1.7.2', iterations = 5, transport =
|
||||
}
|
||||
try {
|
||||
await page.goto(origin)
|
||||
if (controllerState)
|
||||
await page.addScriptTag({ content: `window.snapshotHlsControllers = ${snapshotHlsControllers.toString()};` })
|
||||
if (workerObserver)
|
||||
await page.evaluate(observeWorkers)
|
||||
await page.addScriptTag({ url: `${origin}/hls-${entry.version}.js` })
|
||||
@@ -101,7 +113,7 @@ export async function diagnose({ version = '1.7.2', iterations = 5, transport =
|
||||
await page.addScriptTag({ url: `${origin}/artplayer.js` })
|
||||
if (plugin)
|
||||
await page.addScriptTag({ url: `${origin}/plugin.js` })
|
||||
await page.evaluate(({ worker, teardown, host, plugin, sdkLogs }) => {
|
||||
await page.evaluate(({ worker, teardown, host, plugin, sdkLogs, controllerState }) => {
|
||||
let video
|
||||
let hls
|
||||
let art
|
||||
@@ -109,7 +121,10 @@ export async function diagnose({ version = '1.7.2', iterations = 5, transport =
|
||||
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, frames: video.getVideoPlaybackQuality().totalVideoFrames, buffered: buffer(video.buffered), seekable: buffer(video.seekable) })
|
||||
const record = (event, data) => {
|
||||
records.push({ at: performance.now(), event, data, media: state() })
|
||||
const snapshot = { at: performance.now(), event, data, media: state(), ...(controllerState ? { controllers: window.snapshotHlsControllers(hls) } : {}) }
|
||||
records.push(snapshot)
|
||||
if (controllerState)
|
||||
window.__recordHlsControllerState(snapshot).catch(() => {})
|
||||
if (records.length > 400)
|
||||
records.shift()
|
||||
}
|
||||
@@ -117,8 +132,11 @@ export async function diagnose({ version = '1.7.2', iterations = 5, transport =
|
||||
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_SWITCHING', 'AUDIO_TRACK_SWITCHED', 'LEVEL_SWITCHING', 'LEVEL_SWITCHED', 'FRAG_BUFFERED', 'BUFFER_FLUSHED']) {
|
||||
hls.on(window.Hls.Events[key], (_, data) => record(key, { id: data.id, level: typeof data.level === 'number' ? data.level : undefined, fatal: data.fatal, details: typeof data.details === 'string' ? data.details : undefined, 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 })) }))
|
||||
const eventKeys = ['ERROR', 'AUDIO_TRACKS_UPDATED', 'AUDIO_TRACK_SWITCHING', 'AUDIO_TRACK_SWITCHED', 'LEVEL_SWITCHING', 'LEVEL_SWITCHED', 'FRAG_BUFFERED', 'BUFFER_FLUSHED']
|
||||
if (controllerState)
|
||||
eventKeys.push('BUFFER_FLUSHING', 'FRAG_LOADING', 'FRAG_LOADED', 'BUFFER_APPENDED')
|
||||
for (const key of eventKeys) {
|
||||
hls.on(window.Hls.Events[key], (_, data) => record(key, { id: data.id, type: data.type, startOffset: data.startOffset, endOffset: typeof data.endOffset === 'number' && !Number.isFinite(data.endOffset) ? String(data.endOffset) : data.endOffset, level: typeof data.level === 'number' ? data.level : undefined, fatal: data.fatal, details: typeof data.details === 'string' ? data.details : undefined, 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))
|
||||
@@ -175,7 +193,7 @@ export async function diagnose({ version = '1.7.2', iterations = 5, transport =
|
||||
record('destroy:end')
|
||||
},
|
||||
}
|
||||
}, { worker, teardown, host, plugin, sdkLogs })
|
||||
}, { worker, teardown, host, plugin, sdkLogs, controllerState })
|
||||
entry.phase = 'initial-playback'
|
||||
await wait(() => window.direct.video?.readyState >= 3)
|
||||
await page.evaluate(() => window.direct.video.play())
|
||||
@@ -273,7 +291,7 @@ export async function diagnose({ version = '1.7.2', iterations = 5, transport =
|
||||
finally {
|
||||
entry.failurePhase = entry.status === 'failed' ? entry.phase : undefined
|
||||
entry.phase = 'final-state'
|
||||
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, mainState: window.direct?.hls.streamController?.state, audioState: window.direct?.hls.audioStreamController?.state } })).catch(error => ({ error: error.message }))
|
||||
entry.state = await page.evaluate(() => ({ coreLoaded: typeof window.Artplayer, pluginLoaded: typeof window.artplayerPluginHlsControl, workers: window.workerEvidence, media: window.direct?.state(), events: window.direct?.records, controllers: window.snapshotHlsControllers?.(window.direct?.hls), sdk: { version: window.Hls?.version, level: window.direct?.hls.currentLevel, audioTrack: window.direct?.hls.audioTrack, mainState: window.direct?.hls.streamController?.state, audioState: window.direct?.hls.audioStreamController?.state } })).catch(error => ({ error: error.message }))
|
||||
entry.phase = 'trace-stop'
|
||||
await context.tracing.stop({ path: path.join(directory, `${iteration}.zip`) }).catch((error) => {
|
||||
entry.traceError = error.message
|
||||
@@ -295,7 +313,7 @@ export async function diagnose({ version = '1.7.2', iterations = 5, transport =
|
||||
await browser?.close()
|
||||
server.closeAllConnections()
|
||||
await new Promise(resolve => server.close(resolve))
|
||||
fs.writeFileSync(path.join(directory, 'report.json'), `${JSON.stringify({ version: sequence ? undefined : version, iterations: versions.length, sequence: versions, transport, teardown, worker, host, plugin, sdkLogs, workerObserver, captureBeforeDestroy, switchBoundary, prefill, runnerSHA256, observerSHA256, browserVersion, release: sequence ? undefined : releases[0], releases, inputs: Object.fromEntries([...files].map(([file, bytes]) => [file, hash(bytes)])), results }, null, 2)}\n`)
|
||||
fs.writeFileSync(path.join(directory, 'report.json'), `${JSON.stringify({ version: sequence ? undefined : version, iterations: versions.length, sequence: versions, transport, teardown, worker, host, plugin, sdkLogs, workerObserver, controllerState, captureBeforeDestroy, switchBoundary, prefill, runnerSHA256, observerSHA256, controllerObserverSHA256, browserVersion, release: sequence ? undefined : releases[0], releases, inputs: Object.fromEntries([...files].map(([file, bytes]) => [file, hash(bytes)])), results }, null, 2)}\n`)
|
||||
console.log(`Diagnostic report: ${directory}`)
|
||||
}
|
||||
return results
|
||||
@@ -314,13 +332,14 @@ const { values, tokens } = parseArgs({
|
||||
'plugin': { type: 'boolean', default: false },
|
||||
'sdk-logs': { type: 'boolean', default: false },
|
||||
'observe-workers': { type: 'boolean', default: false },
|
||||
'controller-state': { type: 'boolean', default: false },
|
||||
'capture-before-destroy': { type: 'boolean', default: false },
|
||||
'switch-boundary': { type: 'string', default: 'switched' },
|
||||
'prefill': { type: 'boolean', default: false },
|
||||
},
|
||||
})
|
||||
assert(!values.sequence || !tokens.some(token => token.kind === 'option' && ['version', 'iterations'].includes(token.name)), 'Use --sequence or --version/--iterations, not both')
|
||||
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'], captureBeforeDestroy: values['capture-before-destroy'], switchBoundary: values['switch-boundary'], prefill: values.prefill, sequence: values.sequence?.split(',') }).then((results) => {
|
||||
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'], controllerState: values['controller-state'], captureBeforeDestroy: values['capture-before-destroy'], switchBoundary: values['switch-boundary'], prefill: values.prefill, sequence: values.sequence?.split(',') }).then((results) => {
|
||||
process.exitCode = results.some(result => result.status !== 'passed') ? 1 : 0
|
||||
}).catch((error) => {
|
||||
console.error(error)
|
||||
|
||||
+3
-1
@@ -2154,7 +2154,9 @@
|
||||
"changes/2026-09-15-PKG-HLS-SDK-01-switch-boundary.md",
|
||||
"baselines/hls-switch-boundary-validation.json",
|
||||
"changes/2026-09-15-PKG-HLS-SDK-01-sdk-sequence.md",
|
||||
"baselines/hls-sdk-sequence-validation.json"
|
||||
"baselines/hls-sdk-sequence-validation.json",
|
||||
"changes/2026-09-15-PKG-HLS-SDK-01-controller-state.md",
|
||||
"baselines/hls-controller-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -146,6 +146,16 @@ one explicit capability/error-cleanup test covers that limitation and its 14 pla
|
||||
skipped with a reason. Chromium/Firefox run the full four-combination HLS matrix. This is not
|
||||
Safari playback approval; see [HLS validation](../../refactor/hls-validation.md) for evidence and open gates.
|
||||
|
||||
The standalone `refactor/scripts/hls-sdk-diagnostic.mjs --controller-state` option
|
||||
installs `helpers/hls-controller-state.js` only for a fixed-SDK diagnostic. It reads
|
||||
the 1.5.17 buffer dictionary or 1.7.2 buffer array and the controllers' shared
|
||||
fragment tracker, keeping cyclic loaded objects out of JSON. The 1.5 audio stream
|
||||
controller is located by playlistType in networkControllers. Observations are also
|
||||
copied asynchronously into a bounded Node queue, retaining received events after
|
||||
a page crash; the last undelivered events may be lost. Default diagnostics
|
||||
do not enable this observer. A missing or errored snapshot is incomplete evidence;
|
||||
neither transient empty media ranges nor repeated success resolves HLS-PLAYBACK-01.
|
||||
|
||||
`playback.spec.js` tests published/current core with current chapter, real decoded frame colors,
|
||||
media time advancing, pause, seek, source change to the existing docs video, destroy and real
|
||||
HTTP failure. These are initial smoke contracts, not full chapter or lifecycle coverage.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Self-contained so the fixed-SDK diagnostic can install this function in a page.
|
||||
// These private fields are evidence only, never dependencies of a player plugin.
|
||||
export function snapshotHlsControllers(hls) {
|
||||
const ranges = source => source && Array.from({ length: source.length }, (_, index) => [source.start(index), source.end(index)])
|
||||
const fragment = value => value && ({ sn: value.sn, level: value.level, type: value.type, start: value.start, duration: value.duration })
|
||||
const controller = value => value && ({
|
||||
state: value.state,
|
||||
level: value.level,
|
||||
nextLoadPosition: value.nextLoadPosition,
|
||||
altAudio: value.altAudio,
|
||||
mediaBuffer: value.mediaBuffer === value.videoBuffer && value.videoBuffer ? 'video' : value.mediaBuffer === value.media && value.media ? 'media' : 'other',
|
||||
buffered: ranges(value.mediaBuffer?.buffered),
|
||||
current: fragment(value.fragCurrent),
|
||||
previous: fragment(value.fragPrevious),
|
||||
})
|
||||
try {
|
||||
const audio = hls?.audioStreamController || hls?.networkControllers?.find(value => value.playlistType === 'audio')
|
||||
const tracker = hls?.streamController?.fragmentTracker || audio?.fragmentTracker
|
||||
const bufferController = hls?.bufferController
|
||||
const buffers = Array.isArray(bufferController?.sourceBuffers)
|
||||
? bufferController.sourceBuffers
|
||||
: bufferController?.sourceBuffer && Object.entries(bufferController.sourceBuffer)
|
||||
return {
|
||||
main: controller(hls?.streamController),
|
||||
audio: controller(audio),
|
||||
mediaSourceState: hls?.bufferController?.mediaSource?.readyState,
|
||||
buffers: buffers?.map(([type, source]) => ({ type, updating: source?.updating, buffered: ranges(source?.buffered) })),
|
||||
endList: tracker && Object.fromEntries(Object.entries(tracker.endListFragments || {}).map(([type, entity]) => [type, { fragment: fragment(entity.body), buffered: !!entity.buffered, loaded: !!entity.loaded }])),
|
||||
tracked: tracker && Object.entries(tracker.fragments || {}).map(([key, entity]) => ({ key, buffered: !!entity?.buffered, loaded: !!entity?.loaded })),
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
return { diagnosticError: String(error) }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user