mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
test(danmuku): [PKG-DANMUKU-08] cover old plugin combinations and expose model load failures
This commit is contained in:
1 parent
30363a5346
commit
475d38043e
13 files changed
+11199
-282
No files matched your search
@@ -1,5 +1,12 @@
|
||||
# Danmuku Mask maintenance map
|
||||
|
||||
The native combination suite includes both frozen/current Danmuku with three core
|
||||
versions and a bounded 20 rows/s load after model readiness. Model output must
|
||||
continue during that load. Current Chromium/WebKit delivery failures block the
|
||||
combination gate under PKG-DANMUKU-MASK-LOAD-01; Firefox passes do not close them.
|
||||
See `refactor/changes/2026-09-14-PKG-DANMUKU-08-combined-load.md` for exact source
|
||||
inputs, historical observations and the still-unproven performance cause.
|
||||
|
||||
The compatibility baseline is the actual npm 1.1.0 package and the earlier
|
||||
1.0.0 export shape. Sources and historical failure probes are recorded in
|
||||
`refactor/baselines/danmuku-mask-contract.md`, `danmuku-mask-release.json` and
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
# Danmuku maintenance map
|
||||
|
||||
`test/browser/danmuku-dpip.spec.js` exercises the candidate with old/current cores
|
||||
`test/browser/danmuku-dpip.spec.js` exercises frozen/current plugins with old/current cores
|
||||
in two sequential native Document PiP windows. It checks timestamp-delivered text,
|
||||
adopted layer and heatmap nodes, stylesheet layout, one surviving track Worker,
|
||||
close/native-close/destroy cleanup, and no retained scheduling operation. Native
|
||||
API absence is reported separately; it does not simulate a popup or count as
|
||||
playback evidence. This is not a background-throttling or device benchmark.
|
||||
playback evidence. Frozen 5.3.0 retains renderer/Setting nodes on destroy(false);
|
||||
its explicit historical outcome is not candidate cleanup acceptance. The load
|
||||
case runs 120 timestamped rows in each of two windows, observing native RAF in
|
||||
the player's owning window. This is not a background-throttling or device benchmark.
|
||||
Set `ARTPLAYER_DANMUKU_ARTIFACT` and `ARTPLAYER_DPIP_ARTIFACT` together to check
|
||||
the corresponding built formats; both loaded hashes appear in each attachment.
|
||||
|
||||
@@ -15,6 +18,12 @@ both plugins from source by default. `ARTPLAYER_MASK_ARTIFACT` and
|
||||
Do not substitute a stale dist file for source checks. Installation, remaining
|
||||
old-plugin combinations, combined-load performance and devices remain task 08/09.
|
||||
|
||||
The actual Mask combination now also tests frozen 5.3.0 and a 20 rows/s post-model
|
||||
load. `test/helpers/danmuku-combination-load.js` owns only test RAF/listeners and
|
||||
retains observations after failed assertions. Candidate complete delivery remains
|
||||
mandatory. Chromium/WebKit failures are tracked by DANMUKU-MASK-LOAD-01 and
|
||||
PKG-DANMUKU-MASK-LOAD-01; ordinary playback or Firefox passes cannot close them.
|
||||
|
||||
`danmuku-fullscreen.spec.js` tests all old/current core and plugin pairs in real
|
||||
document and CSS web fullscreen, with three fresh player lifetimes per case.
|
||||
Each lifetime must show a timestamp-delivered row, restore layout and release its
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,77 @@
|
||||
# PKG-DANMUKU-08 旧插件组合与真实组合负载
|
||||
|
||||
状态:仍 doing,存在未关闭失败。起点30363a5346e8d6b227651f224dd4d411289a9971。
|
||||
本批仅扩展测试和证据,不修改生产实现、声明、默认配置或依赖。下一修复由独立
|
||||
PKG-DANMUKU-MASK-LOAD-01负责。风险继续阻止弹幕08与Mask05终验;尚未启动的
|
||||
弹幕09与Mask06显式依赖该修复,确保它不能被分发阶段绕过。
|
||||
|
||||
## 真实输入与新增覆盖
|
||||
|
||||
画中画及Mask用例现在都加载冻结npm弹幕5.3.0或当前源码,校验tarball成员SHA。
|
||||
显式ARTPLAYER_DANMUKU_ARTIFACT仍只替换候选输入,不能覆盖冻结发布输入。
|
||||
画中画使用当前Document PiP,核心为5.4.0/当前源码;Mask使用当前插件及固定
|
||||
本地MediaPipe模型,另覆盖实际5.3.1-beta.1核心。没有修改模型或SDK字节。
|
||||
|
||||
`test/helpers/danmuku-combination-load.js`通过旧config/load调用设置speed=1、
|
||||
antiOverlap=false,6秒内每秒20条,观察到媒体推进8秒后核验120条。真实RAF来自
|
||||
播放器所属window,原生媒体时钟和Worker不被替换。临时可见/错误监听器及RAF在
|
||||
finally释放;失败后仍保存load快照。候选要求每行恰好可见一次、最终wait、无引用、
|
||||
节点池身份唯一;冻结旧插件仅观察其实际漏显,不能混称为完整交付通过。
|
||||
|
||||
Mask负载发生在真实模型就绪、pause/seek/网页全屏/hide-show验证之后,并检查
|
||||
负载期间模型确实继续产生输出。新画中画load用例在两个真实PiP窗口各运行一轮,
|
||||
单项45秒只为新增16秒媒体推进留预算;原有close/native-close/destroy超时不变。
|
||||
这不是三小时耐久、后台节流保证、物理设备验收或私有GPU/WASM泄漏证明。
|
||||
|
||||
## 画中画结果与旧清理缺陷
|
||||
|
||||
Windows Chromium153.0.8010.12、Firefox155.0、WebKit26.6;Node24.21.0/Yarn1.22.22。
|
||||
新增旧插件首轮18项为6个API不可用记录、12个原生退出后零DOM断言失败。
|
||||
12个支持用例均已完成两次真实开窗、弹幕显示、热力图/设置迁移、关闭和Worker终止;
|
||||
冻结5.3.0在destroy(false)后保留3个renderer节点及1个Setting,两个核心完全一致。
|
||||
旧destroy实现不移除这些节点,候选在05已修复其所有权清理;参考
|
||||
[资源修复](2026-09-13-PKG-DANMUKU-05-resources.md)。
|
||||
|
||||
因此旧分支单独断言该确定的历史残留,outcome明确为
|
||||
native-playback-with-published-retained-dom;候选仍要求零DOM与空scheduler。
|
||||
两行固定fixture精确断言3/1,load fixture按销毁前实际节点数验证旧保留行为。
|
||||
保留原12个失败报告;这不要求永久保留旧插件缺陷,也不豁免候选清理。
|
||||
|
||||
最终普通PiP矩阵36项退出0:12候选原生通过、12旧插件原生播放及残留观察、
|
||||
12 WebKit API不可用记录。新增load矩阵12项退出0:4候选原生、4旧插件原生、
|
||||
4 API不可用;每个候选原生用例两个窗口分别120行完整显示/回收,最终Worker终止。
|
||||
普通矩阵54.25秒、load矩阵94.81秒,均无重试。源码PiP测试不冒充最终安装分发。
|
||||
|
||||
## Mask负载失败:不能计入验收完成
|
||||
|
||||
18项组合为12 passed / 6 failed,运行退出1。12个通过中9项为旧插件观察、
|
||||
3项为Firefox候选完整交付;不能称12项候选负载通过。
|
||||
|
||||
| 引擎 | 候选负载显示 | 旧插件负载显示 | 结论 |
|
||||
| --- | --- | --- | --- |
|
||||
| Chromium | 49/120(当前核心)、48/120(5.4.0) | 34–49/120 | 候选仍有活动/待放置工作,串行放置积压需分析 |
|
||||
| Chromium / beta核心 | 未进入负载 | 45/120 | 模型就绪后Ready mask combination一行未显示,原7秒断言失败 |
|
||||
| Firefox | 三核心各120/120 | 三核心各120/120 | 本负载完整显示/回收 |
|
||||
| WebKit | 三核心109/109/116条,共需各120 | 104/111/113条 | 有未显示行,最终已无行节点引用;检查未采样时间窗口 |
|
||||
|
||||
本轮候选RAF帧间隔P95:Chromium约214–219ms,Firefox78–85ms,WebKit227–238ms。
|
||||
这些数据表明与普通播放器相比主线程推进明显变慢,但尚不能将模型、PNG编码、
|
||||
Worker往返或某个模块单独定为根因。旧插件也漏显;目前没有证据证明候选比旧插件
|
||||
回归。候选目标仍不达标,不能降低20条/秒、移除完整性断言或只跑Firefox。
|
||||
原始模型、源码/核心哈希、行与RAF/媒体时间快照均保留在报告中;机器摘要见
|
||||
[组合负载证据](../baselines/danmuku-combined-load-validation.json)。
|
||||
|
||||
## 修复接续与重跑
|
||||
|
||||
PKG-DANMUKU-MASK-LOAD-01首先区分未采样行与已采样待放置行,分析实际模型各阶段
|
||||
及Worker请求/回复成本,建立可控回归后修复。保持公开readys/回调串行顺序、
|
||||
seek/pause/hide/reset取消和显示寿命契约;涉及真实兼容冲突时明确决策。
|
||||
不得通过在调试夹具中停止模型、降低视频尺寸或改变媒体时钟作为正式通过结果。
|
||||
若某条诊断改变负载输入,必须保持原始失败并明确其反事实用途。
|
||||
|
||||
重跑:`yarn test:browser:source test/browser/danmuku-dpip.spec.js --workers=2`,
|
||||
只选新增负载加`--grep 'PiP load'`;Mask用
|
||||
`yarn test:browser:source test/browser/danmuku-mask-native.spec.js
|
||||
--grep 'actual model and Danmuku' --workers=2`。每次前归档前一份报告。
|
||||
三个修改文件的只读ESLint通过。任务、风险和生成文档同步校验后独立本地提交,
|
||||
再运行提交审计。没有push、部署或发布;回退本检查点只撤销覆盖,不修复生产问题。
|
||||
+14
-7
@@ -2,9 +2,9 @@
|
||||
|
||||
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 261 项,范围 22 个包及工作区/示例。
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 262 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 48 / doing 21 / blocked 0 / done 192 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 49 / doing 21 / blocked 0 / done 192 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
| artplayer-plugin-auto-thumbnail | 1.1.0 | PKG-AUTO-THUMB-01, PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-04, PKG-AUTO-THUMB-05, PKG-AUTO-THUMB-06, PKG-AUTO-THUMB-07, PKG-AUTO-THUMB-08, PKG-AUTO-THUMB-09, PKG-AUTO-THUMB-10, PKG-AUTO-THUMB-11 |
|
||||
| artplayer-plugin-chapter | 1.1.0 | PILOT-01, PKG-CHAPTER-01, PKG-CHAPTER-02, PKG-CHAPTER-03, PKG-CHAPTER-04, PKG-CHAPTER-05, PKG-CHAPTER-06, PKG-CHAPTER-HOVER-01 |
|
||||
| artplayer-plugin-chromecast | 1.1.0 | PKG-CAST-01, PKG-CAST-02, PKG-CAST-03, PKG-CAST-04, PKG-CAST-05, PKG-CAST-06 |
|
||||
| artplayer-plugin-danmuku | 5.3.0 | PKG-DANMUKU-01, PKG-DANMUKU-02, PKG-DANMUKU-03, PKG-DANMUKU-04, PKG-DANMUKU-05, PKG-DANMUKU-06, PKG-DANMUKU-07, PKG-DANMUKU-08, PKG-DANMUKU-09, PKG-DANMUKU-10, PKG-DANMUKU-11, PKG-DANMUKU-12 |
|
||||
| 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, ENG-LINT-01 |
|
||||
| artplayer-plugin-danmuku | 5.3.0 | PKG-DANMUKU-01, PKG-DANMUKU-02, PKG-DANMUKU-03, PKG-DANMUKU-04, PKG-DANMUKU-05, PKG-DANMUKU-06, PKG-DANMUKU-07, PKG-DANMUKU-08, PKG-DANMUKU-09, PKG-DANMUKU-10, PKG-DANMUKU-11, PKG-DANMUKU-12, PKG-DANMUKU-MASK-LOAD-01 |
|
||||
| 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, ENG-LINT-01, PKG-DANMUKU-MASK-LOAD-01 |
|
||||
| artplayer-plugin-dash-control | 1.1.0 | PKG-DASH-01, PKG-DASH-02, PKG-DASH-03, PKG-DASH-04, PKG-DASH-SEEK-01, PKG-DASH-MENU-01, 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-SDK-01, PKG-HLS-05, PKG-HLS-06 |
|
||||
@@ -263,7 +263,7 @@
|
||||
| PKG-MASK-03 | artplayer-plugin-danmuku-mask<br>整理内部职责与资源 | PKG-MASK-02, CORE-18, PKG-DANMUKU-07 | 模型加载/推理/画布输出分离,阻止重叠推理与过期写入 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
|
||||
| PKG-MASK-04 | artplayer-plugin-danmuku-mask<br>迁移自有源码和公开类型 | PKG-MASK-03, ENG-04, ENG-06, CORE-07 | 模型 adapter、canvas 和选项的精确类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done |
|
||||
| PKG-MASK-05 | artplayer-plugin-danmuku-mask<br>验证新旧核心和组合 | PKG-MASK-04, CORE-22 | 真实模型和 danmuku/seek/全屏组合,GPU 资源释放 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | doing |
|
||||
| PKG-MASK-06 | artplayer-plugin-danmuku-mask<br>验证分发并同步文档 | PKG-MASK-05, ENG-07 | danmuku.mask.js、资源版本/许可、CPU fallback 和包体积证据 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
|
||||
| PKG-MASK-06 | artplayer-plugin-danmuku-mask<br>验证分发并同步文档 | PKG-MASK-05, ENG-07, PKG-DANMUKU-MASK-LOAD-01 | danmuku.mask.js、资源版本/许可、CPU fallback 和包体积证据 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
|
||||
|
||||
## 5 包迁移:artplayer-plugin-asr
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
| PKG-DANMUKU-06 | artplayer-plugin-danmuku<br>迁移 TS 与公开声明 | PKG-DANMUKU-05, ENG-04, ENG-06 | 自有模块、worker 消息、option/item/result 类型 | 旧参数、扩展字段和事件消费通过,vendored 边界清楚 | H | done |
|
||||
| PKG-DANMUKU-07 | artplayer-plugin-danmuku<br>弹幕能力稳定性验收 | PKG-DANMUKU-06, PKG-DANMUKU-10, PKG-DANMUKU-11, PKG-DANMUKU-12 | 负载/渲染/资源对比及 mask 可依赖的稳定边界 | 在试点核心中旧插件 API 与关键帧路径通过 | H | done |
|
||||
| PKG-DANMUKU-08 | artplayer-plugin-danmuku<br>完成新旧核心与组合验收 | PKG-DANMUKU-07, CORE-22 | 最终核心/旧核心、mask/fullscreen/PiP 组合报告 | 性能无未解释退化,反复装卸无累计资源 | H | doing |
|
||||
| PKG-DANMUKU-09 | artplayer-plugin-danmuku<br>完成分发、示例与文档 | PKG-DANMUKU-08, ENG-07 | danmuku.js、README、声明、worker 与产物 | tarball 和静态 icons 等旧调用通过,有独立回退版本 | H | todo |
|
||||
| PKG-DANMUKU-09 | artplayer-plugin-danmuku<br>完成分发、示例与文档 | PKG-DANMUKU-08, ENG-07, PKG-DANMUKU-MASK-LOAD-01 | danmuku.js、README、声明、worker 与产物 | tarball 和静态 icons 等旧调用通过,有独立回退版本 | H | todo |
|
||||
| PKG-DANMUKU-10 | artplayer-plugin-danmuku<br>修复 issue958 密集热力图过高与曲线裁平 | PKG-DANMUKU-05 | 自动密度缩放与曲线边界、历史自定义坐标兼容及真实浏览器红绿证据 | 16000条三种分布在两核心三引擎和source/main/legacy不遮挡;显式轴和自定义points保持旧解释,有单独commit | M | done |
|
||||
| PKG-DANMUKU-11 | artplayer-plugin-danmuku<br>修复 Worker 隐藏等待吞掉可见寿命 | PKG-DANMUKU-06 | 成功放置时的可见计时与延迟回复回归 | 真实 Worker 延迟下完整显示寿命;保留暂停/回收/取消和公开契约 | M | done |
|
||||
| PKG-DANMUKU-12 | artplayer-plugin-danmuku<br>保持异步放置期间的原生帧采样与寿命维护 | PKG-DANMUKU-11 | 每帧采样与串行dispatcher、代际buffer及回归 | 异步等待不丢已到时采样、不延迟既有寿命维护;保留顺序、取消与公开readys窗口 | H | done |
|
||||
@@ -463,6 +463,12 @@
|
||||
| ENG-LINT-01 | artplayer-plugin-danmuku-mask, workspace<br>修复 Mask manifest 排序并恢复全仓 lint | PKG-MASK-04 | 仅调整 manifest 字段顺序,独立验证全仓只读 lint 与元数据语义不变 | 根 lint 通过;解析后的 manifest 深度相等;不改包版本、依赖或公开入口 | L | done |
|
||||
| ENG-13 | workspace<br>保证性能资源探针的最小观察时长 | ENG-08 | 性能页面按实测时钟补足既有350ms观察窗口,保留冻结基线和原校验门槛;时钟边界测试和实际安装包配对证据 | 349ms仍被拒绝;早到定时器补足观察、正常超时保留实际值;冻结计时样本逻辑未变,三浏览器实际350ms观察及资源校验通过 | M | done |
|
||||
|
||||
## 5 包组合:Danmuku / Mask
|
||||
|
||||
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PKG-DANMUKU-MASK-LOAD-01 | artplayer-plugin-danmuku, artplayer-plugin-danmuku-mask<br>修复真实Mask组合下的弹幕未采样与放置积压 | PKG-DANMUKU-12, PKG-MASK-04, CORE-22 | 分阶段性能/采样诊断、兼容实现修复及新旧核心/产物回归 | 关闭真实模型组合5项密集负载失败和1项负载前漏显,保持回调顺序、显示寿命与生命周期取消;不能以降低负载或关闭模型制造通过 | H | todo |
|
||||
|
||||
## 完成证据与阻塞
|
||||
|
||||
- DOC-01: [记录](progress.md)
|
||||
@@ -611,7 +617,7 @@
|
||||
- PKG-DANMUKU-05: [记录](changes/2026-09-13-PKG-DANMUKU-05-resources.md) [记录](baselines/danmuku-resources-validation.json)
|
||||
- PKG-DANMUKU-06: [记录](changes/2026-09-13-PKG-DANMUKU-06-types.md) [记录](baselines/danmuku-types-validation.json)
|
||||
- PKG-DANMUKU-07: [记录](changes/2026-09-13-PKG-DANMUKU-07-stability.md) [记录](baselines/danmuku-stability-validation.json) [记录](danmuku-stability.md)
|
||||
- PKG-DANMUKU-08: [记录](changes/2026-09-14-PKG-DANMUKU-08-native-combinations.md) [记录](baselines/danmuku-combination-checkpoint.json) [记录](changes/2026-09-14-PKG-DANMUKU-08-fullscreen-lifetimes.md) [记录](baselines/danmuku-fullscreen-validation.json) [记录](baselines/danmuku-old-core-load-validation.json)
|
||||
- PKG-DANMUKU-08: [记录](changes/2026-09-14-PKG-DANMUKU-08-native-combinations.md) [记录](baselines/danmuku-combination-checkpoint.json) [记录](changes/2026-09-14-PKG-DANMUKU-08-fullscreen-lifetimes.md) [记录](baselines/danmuku-fullscreen-validation.json) [记录](baselines/danmuku-old-core-load-validation.json) [记录](changes/2026-09-14-PKG-DANMUKU-08-combined-load.md) [记录](baselines/danmuku-combined-load-validation.json)
|
||||
- PKG-CANVAS-01: [记录](changes/2026-09-12-PKG-CANVAS-01-contract.md) [记录](baselines/canvas-contract.md) [记录](baselines/canvas-release.json) [记录](canvas-validation.md) [记录](baselines/canvas-contract-validation.json)
|
||||
- PKG-CANVAS-02: [记录](changes/2026-09-12-PKG-CANVAS-02-tests.md) [记录](baselines/canvas-behavior-validation.json) [记录](canvas-validation.md)
|
||||
- PKG-CANVAS-03: [记录](changes/2026-09-12-PKG-CANVAS-03-lifecycle.md) [记录](baselines/canvas-lifecycle-validation.json) [记录](canvas-validation.md)
|
||||
@@ -678,3 +684,4 @@
|
||||
- CI-BROWSER-01: [记录](changes/2026-09-14-CI-BROWSER-01-scopes.md) [记录](baselines/browser-scope-validation.json)
|
||||
- PKG-CHAPTER-HOVER-01: [记录](changes/2026-09-14-PKG-CHAPTER-HOVER-01-geometry.md) [记录](baselines/chapter-hover-validation.json)
|
||||
- PKG-CANVAS-SUBTITLE-01: [记录](changes/2026-09-14-PKG-CANVAS-SUBTITLE-01-track.md) [记录](baselines/canvas-subtitles-validation.json)
|
||||
- PKG-DANMUKU-MASK-LOAD-01: [记录](changes/2026-09-14-PKG-DANMUKU-08-combined-load.md) [记录](baselines/danmuku-combined-load-validation.json)
|
||||
@@ -1,5 +1,26 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-DANMUKU-08 旧插件组合与真实负载:新增必须修复项
|
||||
|
||||
扩展PiP/Mask测试加载校验后的npm弹幕5.3.0及候选源码。普通PiP矩阵36项退出0:
|
||||
24原生播放(12候选清理、12旧插件残留观察),12WebKit API不可用;新load矩阵
|
||||
12项退出0:8原生(4候选/4旧插件)、4不可用。候选每个原生用例在两个窗口
|
||||
各完整显示/回收120行,Worker终止。旧插件destroy(false)残留renderer/Setting的
|
||||
12个首轮失败单独保留,并有明确历史outcome;候选零残留要求不变。
|
||||
|
||||
真实Mask模型保持运行并施加20条/秒负载,18项12通过6失败(退出1):候选
|
||||
5项负载显示不完整,另1项Chromium beta核心负载前行未显示。Firefox候选3项
|
||||
完整交付;旧插件在Chromium/WebKit也漏显,尚不能认定重构回归或单一根因。
|
||||
完整的行、帧间隔、输入哈希和原失败报告见
|
||||
[记录](changes/2026-09-14-PKG-DANMUKU-08-combined-load.md)及
|
||||
[证据](baselines/danmuku-combined-load-validation.json)。没有降低负载、扩大等待或
|
||||
关闭模型把该失败改为通过。原先只含稀疏行的Mask通过报告不能关闭新增负载要求。
|
||||
|
||||
新增PKG-DANMUKU-MASK-LOAD-01及DANMUKU-MASK-LOAD-01,接续区分未采样与已采样
|
||||
放置积压,分析真实模型/绘制/Worker往返成本后兼容修复。08/Mask05保持doing,
|
||||
09/Mask06显式依赖修复。262项:192 done、21 doing、49 todo。专项lint和任务/风险
|
||||
校验通过;浏览器仍有上述6失败。本检查点独立本地提交并核验审计;无push/发布。
|
||||
|
||||
## PKG-DANMUKU-08 全屏生命周期与旧核心负载检查点(仍 doing)
|
||||
|
||||
三引擎/新旧核心/新旧弹幕的网页及原生document全屏源码24项通过,候选main与
|
||||
|
||||
@@ -275,3 +275,4 @@
|
||||
| CANVAS-SUBTITLE-01 | resolved / 已复现 | Canvas底层video缺初始字幕轨道且普通Canvas挂载不能加载原生字幕 | PKG-CANVAS-SUBTITLE-01, PKG-CANVAS-05 |
|
||||
| DPIP-INSTALLED-PROVENANCE-01 | resolved / 已复现 | Document PiP测试加载器忽略显式安装map并回退到源码 | PKG-CANVAS-05, CI-01 |
|
||||
| DASH-MENU-01 | resolved / 已复现 | SDK 5.2.1 播放中设置质量选项在点击时变为不可见 | PKG-DASH-MENU-01, PKG-DASH-05, REVIEW-02 |
|
||||
| DANMUKU-MASK-LOAD-01 | open / 已复现 | 真实Mask模型运行时弹幕存在未采样窗口及串行放置积压 | PKG-DANMUKU-MASK-LOAD-01, PKG-DANMUKU-08, PKG-MASK-05 |
|
||||
@@ -6294,6 +6294,26 @@
|
||||
"test/dash-events.test.js"
|
||||
],
|
||||
"resolutionRationale": "原trace显示设置仍打开但导航退回根面板;真实SDK刷新在四组核心/SDK组合复现相同失败。修复后源码92、main设置32、legacy完整候选92通过,新旧核心/两SDK/两引擎覆盖质量及音轨,原失败与未加force/延长超时的回归均保留。Node446验证三格式及重入/销毁边界;DASH其他SDK/设备/发布风险仍独立开放。"
|
||||
},
|
||||
{
|
||||
"id": "DANMUKU-MASK-LOAD-01",
|
||||
"title": "真实Mask模型运行时弹幕存在未采样窗口及串行放置积压",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-DANMUKU-MASK-LOAD-01",
|
||||
"PKG-DANMUKU-08",
|
||||
"PKG-MASK-05"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/changes/2026-09-14-PKG-DANMUKU-08-combined-load.md",
|
||||
"refactor/baselines/danmuku-combined-load-validation.json",
|
||||
"test/browser/danmuku-mask-native.spec.js",
|
||||
"test/helpers/danmuku-combination-load.js"
|
||||
],
|
||||
"compatibleResolution": "分析真实模型/绘制/Worker往返成本和未采样、待放置行,保持公共readys与回调顺序、显示寿命及生命周期取消;用独立回归验证兼容修复。",
|
||||
"closureCriteria": "原始失败保留并有可解释修复;真实模型下120行完整交付、无活动引用积压,新旧核心和main/legacy以及暂停/seek/hide/destroy回归通过;不降低负载、停模型或扩大等待伪造结果。",
|
||||
"workspaceState": "源码18项12通过6失败:5项候选密集负载不完整,1项Chromium beta核心负载前行未显示。旧插件同环境也漏显,尚未证明是重构回归或定位单一瓶颈。Firefox候选和原生PiP负载通过。"
|
||||
}
|
||||
]
|
||||
}
|
||||
+29
-3
@@ -2764,7 +2764,8 @@
|
||||
],
|
||||
"dependsOn": [
|
||||
"PKG-MASK-05",
|
||||
"ENG-07"
|
||||
"ENG-07",
|
||||
"PKG-DANMUKU-MASK-LOAD-01"
|
||||
],
|
||||
"status": "todo",
|
||||
"risk": "H",
|
||||
@@ -3565,7 +3566,9 @@
|
||||
"baselines/danmuku-combination-checkpoint.json",
|
||||
"changes/2026-09-14-PKG-DANMUKU-08-fullscreen-lifetimes.md",
|
||||
"baselines/danmuku-fullscreen-validation.json",
|
||||
"baselines/danmuku-old-core-load-validation.json"
|
||||
"baselines/danmuku-old-core-load-validation.json",
|
||||
"changes/2026-09-14-PKG-DANMUKU-08-combined-load.md",
|
||||
"baselines/danmuku-combined-load-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -3577,7 +3580,8 @@
|
||||
],
|
||||
"dependsOn": [
|
||||
"PKG-DANMUKU-08",
|
||||
"ENG-07"
|
||||
"ENG-07",
|
||||
"PKG-DANMUKU-MASK-LOAD-01"
|
||||
],
|
||||
"status": "todo",
|
||||
"risk": "H",
|
||||
@@ -5465,6 +5469,28 @@
|
||||
"changes/2026-09-14-PKG-CANVAS-SUBTITLE-01-track.md",
|
||||
"baselines/canvas-subtitles-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-DANMUKU-MASK-LOAD-01",
|
||||
"phase": "5 包组合:Danmuku / Mask",
|
||||
"title": "修复真实Mask组合下的弹幕未采样与放置积压",
|
||||
"scope": [
|
||||
"artplayer-plugin-danmuku",
|
||||
"artplayer-plugin-danmuku-mask"
|
||||
],
|
||||
"dependsOn": [
|
||||
"PKG-DANMUKU-12",
|
||||
"PKG-MASK-04",
|
||||
"CORE-22"
|
||||
],
|
||||
"status": "todo",
|
||||
"risk": "H",
|
||||
"deliverable": "分阶段性能/采样诊断、兼容实现修复及新旧核心/产物回归",
|
||||
"acceptance": "关闭真实模型组合5项密集负载失败和1项负载前漏显,保持回调顺序、显示寿命与生命周期取消;不能以降低负载或关闭模型制造通过",
|
||||
"evidence": [
|
||||
"changes/2026-09-14-PKG-DANMUKU-08-combined-load.md",
|
||||
"baselines/danmuku-combined-load-validation.json"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -250,6 +250,14 @@ with a missed narrow timestamp window. A passing post-ready combination is not
|
||||
proof of startup losslessness, private GPU/WASM closure, OS fullscreen or devices.
|
||||
See refactor/changes/2026-09-14-PKG-MASK-05-native-checkpoint.md for exact limits.
|
||||
|
||||
Its combination cases now include frozen/current Danmuku and a bounded 120-row
|
||||
load while the actual model continues. `danmuku-combination-load.js` observes
|
||||
real media/RAF and cleans up its test listeners; it does not seed eligible rows.
|
||||
Candidate completeness failures remain failing under PKG-DANMUKU-MASK-LOAD-01.
|
||||
`danmuku-dpip.spec.js` also includes two-window load cases and both plugin versions.
|
||||
Frozen-plugin retained DOM, candidate cleanup and unsupported API outcomes remain
|
||||
distinct; the old plugin's observed loss is not complete-delivery acceptance.
|
||||
|
||||
`jassub-native.spec.js` runs actual published JASSUB 1.1.0, its native Worker,
|
||||
fixed local WASM/font and authored ASS cues with offscreenRender=false. It checks
|
||||
real canvas pixels before/after seek, CSS web fullscreen and normal destruction.
|
||||
|
||||
+167
-139
@@ -1,161 +1,189 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { observeDanmukuLoad } from '../helpers/danmuku-combination-load.js'
|
||||
import { dpipCandidate } from '../helpers/dpip.js'
|
||||
import { compilePackage } from '../helpers/load.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const dpip = await dpipCandidate()
|
||||
let danmuku
|
||||
const implementations = new Map()
|
||||
test.beforeAll(async () => {
|
||||
danmuku = process.env.ARTPLAYER_DANMUKU_ARTIFACT
|
||||
const release = JSON.parse(fs.readFileSync('refactor/baselines/danmuku-release.json', 'utf8')).release
|
||||
const member = 'package/dist/artplayer-plugin-danmuku.js'
|
||||
const published = readMember(await ensureArchive(release), member)
|
||||
assert.equal(hash(published), release.files[member])
|
||||
implementations.set('published', { input: `npm ${release.version}`, code: published.toString() })
|
||||
const candidate = process.env.ARTPLAYER_DANMUKU_ARTIFACT
|
||||
? fs.readFileSync(process.env.ARTPLAYER_DANMUKU_ARTIFACT, 'utf8')
|
||||
: await compilePackage('artplayer-plugin-danmuku', 'umd')
|
||||
implementations.set('candidate', { input: process.env.ARTPLAYER_DANMUKU_ARTIFACT || 'source UMD', code: candidate })
|
||||
})
|
||||
|
||||
for (const core of ['published', 'candidate']) {
|
||||
for (const ending of ['close', 'native-close', 'destroy']) {
|
||||
test(`${core}: Danmuku, heatmap and Worker survive native Document PiP ${ending}`, async ({ page, context }, testInfo) => {
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
const supported = await page.evaluate(() => typeof window.documentPictureInPicture?.requestWindow === 'function')
|
||||
const evidence = { core, ending, supported, danmuku: { input: process.env.ARTPLAYER_DANMUKU_ARTIFACT || 'source UMD', sha256: hash(danmuku) }, dpip: { input: dpip.name, sha256: hash(dpip.code) }, outcome: 'incomplete', rounds: [] }
|
||||
if (!supported) {
|
||||
evidence.outcome = 'native-document-pip-unavailable'
|
||||
await testInfo.attach('danmuku-dpip', { contentType: 'application/json', body: JSON.stringify(evidence) })
|
||||
return
|
||||
}
|
||||
await page.evaluate(() => {
|
||||
window.combinationWorkers = []
|
||||
const NativeWorker = window.Worker
|
||||
window.Worker = class extends NativeWorker {
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
this.record = { terminated: 0, replies: 0 }
|
||||
window.combinationWorkers.push(this.record)
|
||||
this.addEventListener('message', () => this.record.replies++)
|
||||
}
|
||||
|
||||
terminate() {
|
||||
this.record.terminated++
|
||||
return super.terminate()
|
||||
}
|
||||
for (const implementation of ['published', 'candidate']) {
|
||||
for (const ending of ['close', 'native-close', 'destroy', 'load']) {
|
||||
test(`${core}: ${implementation} Danmuku, heatmap and Worker survive native Document PiP ${ending}`, async ({ page, context }, testInfo) => {
|
||||
if (ending === 'load')
|
||||
test.setTimeout(45000) // Two native PiP loads add 16 seconds of actual media progression.
|
||||
const input = implementations.get(implementation)
|
||||
const danmuku = input.code
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
const supported = await page.evaluate(() => typeof window.documentPictureInPicture?.requestWindow === 'function')
|
||||
const evidence = { core, implementation, ending, supported, danmuku: { input: input.input, sha256: hash(danmuku) }, dpip: { input: dpip.name, sha256: hash(dpip.code) }, outcome: 'incomplete', rounds: [], loads: [] }
|
||||
if (!supported) {
|
||||
evidence.outcome = 'native-document-pip-unavailable'
|
||||
await testInfo.attach('danmuku-dpip', { contentType: 'application/json', body: JSON.stringify(evidence) })
|
||||
return
|
||||
}
|
||||
})
|
||||
await page.addScriptTag({ content: danmuku })
|
||||
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${dpip.code}; window.dpipFactory = module.exports.default || module.exports; })();` })
|
||||
await page.evaluate(() => {
|
||||
window.combinationVisible = []
|
||||
window.combinationErrors = []
|
||||
window.combinationEvents = []
|
||||
window.art = new window.Artplayer({
|
||||
container: '.player',
|
||||
url: '/assets/sample/video.mp4',
|
||||
muted: true,
|
||||
plugins: [window.artplayerPluginDanmuku({ danmuku: [], heatmap: true, emitter: true, speed: 4 }), window.dpipFactory({ fallbackToVideoPiP: false })],
|
||||
})
|
||||
window.danmuku = window.art.plugins.artplayerPluginDanmuku
|
||||
window.danmukuOwner = window.danmuku.config({})
|
||||
window.pip = window.art.plugins.artplayerPluginDocumentPip
|
||||
window.combinationPlayer = window.art.template.$player
|
||||
window.combinationLayer = window.art.template.$danmuku
|
||||
window.combinationParent = window.combinationPlayer.parentNode
|
||||
window.art.on('document-pip', value => window.combinationEvents.push(value))
|
||||
window.art.on('artplayerPluginDanmuku:error', error => window.combinationErrors.push(String(error)))
|
||||
window.art.on('artplayerPluginDanmuku:visible', row => window.combinationVisible.push({ text: row.text, ref: row.$ref, time: window.art.currentTime }))
|
||||
document.querySelector('#play').onclick = () => window.art.play()
|
||||
const open = document.createElement('button')
|
||||
open.id = 'danmuku-open-pip'
|
||||
open.textContent = 'Open Danmuku PiP'
|
||||
open.onclick = () => {
|
||||
window.openingDanmukuPip = window.pip.open()
|
||||
}
|
||||
document.body.append(open)
|
||||
})
|
||||
try {
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
for (let round = 0; round < 2; round++) {
|
||||
const popupPromise = context.waitForEvent('page', { timeout: 8000 })
|
||||
await page.click('#danmuku-open-pip')
|
||||
const popup = await popupPromise
|
||||
await page.evaluate(() => window.openingDanmukuPip)
|
||||
await expect(popup.locator('video.art-video')).toBeVisible()
|
||||
await expect(popup.locator('.artplayer-plugin-danmuku')).toHaveCount(1)
|
||||
const text = `PiP row ${round}`
|
||||
await page.evaluate(async (text) => {
|
||||
await window.danmuku.load([{ text, time: window.art.currentTime + 0.5, mode: 1 }])
|
||||
}, text)
|
||||
await expect.poll(() => page.evaluate(text => window.combinationVisible.some(row => row.text === text), text)).toBe(true)
|
||||
await expect(popup.getByText(text, { exact: true })).toBeVisible()
|
||||
const snapshot = await page.evaluate((text) => {
|
||||
const row = window.combinationVisible.find(row => row.text === text)
|
||||
const rect = row.ref.getBoundingClientRect()
|
||||
const popup = window.documentPictureInPicture.window
|
||||
return {
|
||||
time: window.art.currentTime,
|
||||
rowTime: row.time,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
adopted: row.ref.ownerDocument === popup.document && window.combinationLayer.ownerDocument === popup.document,
|
||||
inLayer: window.combinationLayer.contains(row.ref),
|
||||
position: popup.getComputedStyle(row.ref).position,
|
||||
player: window.combinationPlayer.ownerDocument === popup.document,
|
||||
heatmap: window.art.controls.heatmap.ownerDocument === popup.document && !!window.art.controls.heatmap.querySelector('svg'),
|
||||
workers: window.combinationWorkers.map(worker => ({ ...worker })),
|
||||
await page.evaluate(() => {
|
||||
window.combinationWorkers = []
|
||||
const NativeWorker = window.Worker
|
||||
window.Worker = class extends NativeWorker {
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
this.record = { terminated: 0, replies: 0 }
|
||||
window.combinationWorkers.push(this.record)
|
||||
this.addEventListener('message', () => this.record.replies++)
|
||||
}
|
||||
}, text)
|
||||
evidence.rounds.push(snapshot)
|
||||
expect(snapshot.adopted && snapshot.inLayer && snapshot.player && snapshot.heatmap).toBe(true)
|
||||
expect(snapshot.width).toBeGreaterThan(0)
|
||||
expect(snapshot.height).toBeGreaterThan(0)
|
||||
expect(snapshot.position).toBe('absolute')
|
||||
expect(snapshot.workers).toHaveLength(1)
|
||||
expect(snapshot.workers[0].replies).toBeGreaterThan(0)
|
||||
if (round === 0)
|
||||
await testInfo.attach('native-danmuku-page', { contentType: 'image/png', body: await popup.screenshot() })
|
||||
if (round === 1 && ending === 'destroy') {
|
||||
|
||||
terminate() {
|
||||
this.record.terminated++
|
||||
return super.terminate()
|
||||
}
|
||||
}
|
||||
})
|
||||
await page.addScriptTag({ content: danmuku })
|
||||
await page.addScriptTag({ content: `(() => { const module = { exports: {} }; const exports = module.exports; ${dpip.code}; window.dpipFactory = module.exports.default || module.exports; })();` })
|
||||
await page.evaluate(() => {
|
||||
window.combinationVisible = []
|
||||
window.combinationErrors = []
|
||||
window.combinationEvents = []
|
||||
window.art = new window.Artplayer({
|
||||
container: '.player',
|
||||
url: '/assets/sample/video.mp4',
|
||||
muted: true,
|
||||
plugins: [window.artplayerPluginDanmuku({ danmuku: [], heatmap: true, emitter: true, speed: 4 }), window.dpipFactory({ fallbackToVideoPiP: false })],
|
||||
})
|
||||
window.danmuku = window.art.plugins.artplayerPluginDanmuku
|
||||
window.danmukuOwner = window.danmuku.config({})
|
||||
window.pip = window.art.plugins.artplayerPluginDocumentPip
|
||||
window.combinationPlayer = window.art.template.$player
|
||||
window.combinationLayer = window.art.template.$danmuku
|
||||
window.combinationParent = window.combinationPlayer.parentNode
|
||||
window.art.on('document-pip', value => window.combinationEvents.push(value))
|
||||
window.art.on('artplayerPluginDanmuku:error', error => window.combinationErrors.push(String(error)))
|
||||
window.art.on('artplayerPluginDanmuku:visible', row => window.combinationVisible.push({ text: row.text, ref: row.$ref, time: window.art.currentTime }))
|
||||
document.querySelector('#play').onclick = () => window.art.play()
|
||||
const open = document.createElement('button')
|
||||
open.id = 'danmuku-open-pip'
|
||||
open.textContent = 'Open Danmuku PiP'
|
||||
open.onclick = () => {
|
||||
window.openingDanmukuPip = window.pip.open()
|
||||
}
|
||||
document.body.append(open)
|
||||
})
|
||||
try {
|
||||
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
|
||||
await page.click('#play')
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
for (let round = 0; round < 2; round++) {
|
||||
const popupPromise = context.waitForEvent('page', { timeout: 8000 })
|
||||
await page.click('#danmuku-open-pip')
|
||||
const popup = await popupPromise
|
||||
await page.evaluate(() => window.openingDanmukuPip)
|
||||
await expect(popup.locator('video.art-video')).toBeVisible()
|
||||
await expect(popup.locator('.artplayer-plugin-danmuku')).toHaveCount(1)
|
||||
const text = `PiP row ${round}`
|
||||
await page.evaluate(async (text) => {
|
||||
await window.danmuku.load([{ text, time: window.art.currentTime + 0.5, mode: 1 }])
|
||||
}, text)
|
||||
await expect.poll(() => page.evaluate(text => window.combinationVisible.some(row => row.text === text), text)).toBe(true)
|
||||
await expect(popup.getByText(text, { exact: true }).and(popup.locator(':visible'))).toBeVisible()
|
||||
const snapshot = await page.evaluate((text) => {
|
||||
const row = window.combinationVisible.find(row => row.text === text)
|
||||
const rect = row.ref.getBoundingClientRect()
|
||||
const popup = window.documentPictureInPicture.window
|
||||
return {
|
||||
time: window.art.currentTime,
|
||||
rowTime: row.time,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
adopted: row.ref.ownerDocument === popup.document && window.combinationLayer.ownerDocument === popup.document,
|
||||
inLayer: window.combinationLayer.contains(row.ref),
|
||||
position: popup.getComputedStyle(row.ref).position,
|
||||
player: window.combinationPlayer.ownerDocument === popup.document,
|
||||
heatmap: window.art.controls.heatmap.ownerDocument === popup.document && !!window.art.controls.heatmap.querySelector('svg'),
|
||||
workers: window.combinationWorkers.map(worker => ({ ...worker })),
|
||||
}
|
||||
}, text)
|
||||
evidence.rounds.push(snapshot)
|
||||
expect(snapshot.adopted && snapshot.inLayer && snapshot.player && snapshot.heatmap).toBe(true)
|
||||
expect(snapshot.width).toBeGreaterThan(0)
|
||||
expect(snapshot.height).toBeGreaterThan(0)
|
||||
expect(snapshot.position).toBe('absolute')
|
||||
expect(snapshot.workers).toHaveLength(1)
|
||||
expect(snapshot.workers[0].replies).toBeGreaterThan(0)
|
||||
if (round === 0)
|
||||
await testInfo.attach('native-danmuku-page', { contentType: 'image/png', body: await popup.screenshot() })
|
||||
if (ending === 'load')
|
||||
evidence.loads.push(await observeDanmukuLoad(page, { label: `PiP ${round}`, requireComplete: implementation === 'candidate' }))
|
||||
evidence.beforeDestroy = await page.evaluate(() => ({ nodes: window.combinationLayer.children.length }))
|
||||
if (round === 1 && ending === 'destroy') {
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
}
|
||||
else {
|
||||
await page.evaluate(ending => ending === 'native-close' ? window.documentPictureInPicture.window.close() : window.pip.close(), ending)
|
||||
await expect.poll(() => page.evaluate(() => window.pip.isActive)).toBe(false)
|
||||
expect(await page.evaluate(() => window.combinationPlayer.parentNode === window.combinationParent && window.combinationLayer.ownerDocument === document)).toBe(true)
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(snapshot.time + 0.1)
|
||||
}
|
||||
}
|
||||
if (ending !== 'destroy')
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
await expect.poll(() => page.evaluate(() => !window.documentPictureInPicture.window || window.documentPictureInPicture.window.closed)).toBe(true)
|
||||
evidence.final = await page.evaluate(() => ({
|
||||
active: window.pip.isActive,
|
||||
workers: window.combinationWorkers,
|
||||
errors: window.combinationErrors,
|
||||
events: window.combinationEvents,
|
||||
frame: window.danmukuOwner.scheduler?.frame,
|
||||
operation: window.danmukuOwner.scheduler?.operation,
|
||||
nodes: window.combinationLayer.children.length,
|
||||
settings: document.querySelectorAll('.artplayer-plugin-danmuku').length,
|
||||
placeholders: document.querySelectorAll('.artplayer-document-pip-placeholder').length,
|
||||
}))
|
||||
expect(evidence.final.active).toBe(false)
|
||||
expect(evidence.final.workers).toHaveLength(1)
|
||||
expect(evidence.final.workers[0].terminated).toBe(1)
|
||||
expect(evidence.final.errors).toEqual([])
|
||||
// Destroy suppresses further public events, as in the existing native PiP contract.
|
||||
expect(evidence.final.events).toEqual(ending === 'destroy' ? [true, false, true] : [true, false, true, false])
|
||||
if (implementation === 'candidate') {
|
||||
expect(evidence.final.frame).toBeNull()
|
||||
expect(evidence.final.operation).toBeNull()
|
||||
expect(evidence.final.nodes + evidence.final.settings + evidence.final.placeholders).toBe(0)
|
||||
evidence.outcome = 'native-playback-danmuku-and-cleanup-passed'
|
||||
}
|
||||
else {
|
||||
await page.evaluate(ending => ending === 'native-close' ? window.documentPictureInPicture.window.close() : window.pip.close(), ending)
|
||||
await expect.poll(() => page.evaluate(() => window.pip.isActive)).toBe(false)
|
||||
expect(await page.evaluate(() => window.combinationPlayer.parentNode === window.combinationParent && window.combinationLayer.ownerDocument === document)).toBe(true)
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(snapshot.time + 0.1)
|
||||
// Frozen npm 5.3.0 destroy(false) retains its renderer and Setting nodes.
|
||||
expect(evidence.final.nodes).toBe(ending === 'load' ? evidence.beforeDestroy.nodes : 3)
|
||||
expect(evidence.final.nodes).toBeGreaterThan(0)
|
||||
expect(evidence.final.settings).toBe(1)
|
||||
expect(evidence.final.placeholders).toBe(0)
|
||||
evidence.outcome = 'native-playback-with-published-retained-dom'
|
||||
}
|
||||
}
|
||||
if (ending !== 'destroy')
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
await expect.poll(() => page.evaluate(() => !window.documentPictureInPicture.window || window.documentPictureInPicture.window.closed)).toBe(true)
|
||||
evidence.final = await page.evaluate(() => ({
|
||||
active: window.pip.isActive,
|
||||
workers: window.combinationWorkers,
|
||||
errors: window.combinationErrors,
|
||||
events: window.combinationEvents,
|
||||
frame: window.danmukuOwner.scheduler.frame,
|
||||
operation: window.danmukuOwner.scheduler.operation,
|
||||
nodes: window.combinationLayer.children.length,
|
||||
settings: document.querySelectorAll('.artplayer-plugin-danmuku').length,
|
||||
placeholders: document.querySelectorAll('.artplayer-document-pip-placeholder').length,
|
||||
}))
|
||||
expect(evidence.final.active).toBe(false)
|
||||
expect(evidence.final.workers).toHaveLength(1)
|
||||
expect(evidence.final.workers[0].terminated).toBe(1)
|
||||
expect(evidence.final.errors).toEqual([])
|
||||
// Destroy suppresses further public events, as in the existing native PiP contract.
|
||||
expect(evidence.final.events).toEqual(ending === 'destroy' ? [true, false, true] : [true, false, true, false])
|
||||
expect(evidence.final.frame).toBeNull()
|
||||
expect(evidence.final.operation).toBeNull()
|
||||
expect(evidence.final.nodes + evidence.final.settings + evidence.final.placeholders).toBe(0)
|
||||
evidence.outcome = 'native-playback-danmuku-and-cleanup-passed'
|
||||
}
|
||||
finally {
|
||||
await testInfo.attach('danmuku-dpip', { contentType: 'application/json', body: JSON.stringify(evidence) })
|
||||
await page.evaluate(() => {
|
||||
if (window.art && !window.art.isDestroy)
|
||||
window.art.destroy(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
finally {
|
||||
evidence.incompleteLoad = await page.evaluate(() => window.danmukuCombinationLoadResult)
|
||||
await testInfo.attach('danmuku-dpip', { contentType: 'application/json', body: JSON.stringify(evidence) })
|
||||
await page.evaluate(() => {
|
||||
if (window.art && !window.art.isDestroy)
|
||||
window.art.destroy(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import { hash } from '../../refactor/scripts/releases.mjs'
|
||||
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { observeDanmukuLoad } from '../helpers/danmuku-combination-load.js'
|
||||
import { compilePackage } from '../helpers/load.js'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
@@ -20,11 +21,18 @@ const assets = baseline.sdk.assets.map(({ file, sha256 }) => {
|
||||
const media = 'docs/assets/sample/steve-jobs.mp4'
|
||||
const danmukuFile = process.env.ARTPLAYER_DANMUKU_ARTIFACT || 'source UMD'
|
||||
let danmukuCode
|
||||
const danmukuImplementations = new Map()
|
||||
test.beforeAll(async () => {
|
||||
;[code, danmukuCode] = await Promise.all([
|
||||
process.env.ARTPLAYER_MASK_ARTIFACT ? fs.readFileSync(artifact, 'utf8') : compilePackage('artplayer-plugin-danmuku-mask', 'umd'),
|
||||
process.env.ARTPLAYER_DANMUKU_ARTIFACT ? fs.readFileSync(danmukuFile, 'utf8') : compilePackage('artplayer-plugin-danmuku', 'umd'),
|
||||
])
|
||||
const release = JSON.parse(fs.readFileSync('refactor/baselines/danmuku-release.json', 'utf8')).release
|
||||
const member = 'package/dist/artplayer-plugin-danmuku.js'
|
||||
const published = readMember(await ensureArchive(release), member)
|
||||
assert.equal(hash(published), release.files[member])
|
||||
danmukuImplementations.set('published', { file: `npm ${release.version}`, code: published.toString() })
|
||||
danmukuImplementations.set('candidate', { file: danmukuFile, code: danmukuCode })
|
||||
})
|
||||
|
||||
for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
@@ -132,138 +140,146 @@ for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
}
|
||||
})
|
||||
|
||||
test(`Mask actual model and Danmuku share the layer through pause, seek and web fullscreen with ${core} core`, async ({ page }, testInfo) => {
|
||||
test.setTimeout(90000)
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: danmukuCode })
|
||||
await page.addScriptTag({ content: code })
|
||||
const unexpected = []
|
||||
const origin = new URL(testInfo.project.use.baseURL).origin
|
||||
await page.route('**/*', (route) => {
|
||||
if (new URL(route.request().url()).origin !== origin) {
|
||||
unexpected.push(route.request().url())
|
||||
return route.abort()
|
||||
}
|
||||
return route.continue()
|
||||
})
|
||||
await page.evaluate(() => {
|
||||
window.maskCombination = { times: [], visible: [], snapshots: [], samples: [] }
|
||||
const encode = HTMLCanvasElement.prototype.toDataURL
|
||||
HTMLCanvasElement.prototype.toDataURL = function (...args) {
|
||||
const result = encode.apply(this, args)
|
||||
window.maskCombination.times.push(window.art?.currentTime)
|
||||
return result
|
||||
}
|
||||
window.art = new window.Artplayer({
|
||||
container: '.player',
|
||||
url: '/assets/sample/steve-jobs.mp4',
|
||||
muted: true,
|
||||
fullscreenWeb: true,
|
||||
plugins: [window.artplayerPluginDanmuku({ danmuku: [{ text: 'Native mask combination', time: 6, mode: 1 }], heatmap: false }), window.artplayerPluginDanmukuMask({ solutionPath: '/assets/@mediapipe/selfie_segmentation' })],
|
||||
for (const implementation of ['published', 'candidate']) {
|
||||
test(`Mask actual model and Danmuku share the layer through pause, seek and web fullscreen with ${core} core / ${implementation} plugin`, async ({ page }, testInfo) => {
|
||||
const danmuku = danmukuImplementations.get(implementation)
|
||||
test.setTimeout(90000)
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: danmuku.code })
|
||||
await page.addScriptTag({ content: code })
|
||||
const unexpected = []
|
||||
let load
|
||||
const origin = new URL(testInfo.project.use.baseURL).origin
|
||||
await page.route('**/*', (route) => {
|
||||
if (new URL(route.request().url()).origin !== origin) {
|
||||
unexpected.push(route.request().url())
|
||||
return route.abort()
|
||||
}
|
||||
return route.continue()
|
||||
})
|
||||
window.maskCombination.layer = window.art.template.$danmuku
|
||||
const owner = window.art.plugins.artplayerPluginDanmuku.config({})
|
||||
const ready = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(owner), 'readys').get
|
||||
Object.defineProperty(owner, 'readys', { configurable: true, get() {
|
||||
const rows = ready.call(this)
|
||||
window.maskCombination.samples.push({ time: window.art.currentTime, wall: performance.now(), rows: rows.map(row => ({ time: row.time, text: row.text })) })
|
||||
return rows
|
||||
} })
|
||||
window.art.on('artplayerPluginDanmuku:visible', (row) => {
|
||||
const node = row.$ref
|
||||
const rect = node?.getBoundingClientRect()
|
||||
window.maskCombination.visible.push({ text: row.text, connected: node?.isConnected, inLayer: node ? window.maskCombination.layer.contains(node) : false, width: rect?.width, height: rect?.height, display: node ? getComputedStyle(node).display : null })
|
||||
await page.evaluate(() => {
|
||||
window.maskCombination = { times: [], visible: [], snapshots: [], samples: [] }
|
||||
const encode = HTMLCanvasElement.prototype.toDataURL
|
||||
HTMLCanvasElement.prototype.toDataURL = function (...args) {
|
||||
const result = encode.apply(this, args)
|
||||
window.maskCombination.times.push(window.art?.currentTime)
|
||||
return result
|
||||
}
|
||||
window.art = new window.Artplayer({
|
||||
container: '.player',
|
||||
url: '/assets/sample/steve-jobs.mp4',
|
||||
muted: true,
|
||||
fullscreenWeb: true,
|
||||
plugins: [window.artplayerPluginDanmuku({ danmuku: [{ text: 'Native mask combination', time: 6, mode: 1 }], heatmap: false }), window.artplayerPluginDanmukuMask({ solutionPath: '/assets/@mediapipe/selfie_segmentation' })],
|
||||
})
|
||||
window.maskCombination.layer = window.art.template.$danmuku
|
||||
const owner = window.art.plugins.artplayerPluginDanmuku.config({})
|
||||
const ready = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(owner), 'readys').get
|
||||
Object.defineProperty(owner, 'readys', { configurable: true, get() {
|
||||
const rows = ready.call(this)
|
||||
window.maskCombination.samples.push({ time: window.art.currentTime, wall: performance.now(), rows: rows.map(row => ({ time: row.time, text: row.text })) })
|
||||
return rows
|
||||
} })
|
||||
window.art.on('artplayerPluginDanmuku:visible', (row) => {
|
||||
const node = row.$ref
|
||||
const rect = node?.getBoundingClientRect()
|
||||
window.maskCombination.visible.push({ text: row.text, connected: node?.isConnected, inLayer: node ? window.maskCombination.layer.contains(node) : false, width: rect?.width, height: rect?.height, display: node ? getComputedStyle(node).display : null })
|
||||
})
|
||||
document.querySelector('#play').onclick = () => window.art.play()
|
||||
document.querySelector('#pause').onclick = () => window.art.pause()
|
||||
window.maskSnapshot = (phase) => {
|
||||
const layer = window.art.template.$danmuku
|
||||
const rect = layer.getBoundingClientRect()
|
||||
const snapshot = { phase, time: window.art.currentTime, outputs: window.maskCombination.times.length, sameLayer: layer === window.maskCombination.layer, mask: layer.style.maskImage.startsWith('url('), mode: getComputedStyle(layer).maskMode, width: rect.width, height: rect.height, fullscreenWeb: window.art.fullscreenWeb }
|
||||
window.maskCombination.snapshots.push(snapshot)
|
||||
return snapshot
|
||||
}
|
||||
})
|
||||
document.querySelector('#play').onclick = () => window.art.play()
|
||||
document.querySelector('#pause').onclick = () => window.art.pause()
|
||||
window.maskSnapshot = (phase) => {
|
||||
const layer = window.art.template.$danmuku
|
||||
const rect = layer.getBoundingClientRect()
|
||||
const snapshot = { phase, time: window.art.currentTime, outputs: window.maskCombination.times.length, sameLayer: layer === window.maskCombination.layer, mask: layer.style.maskImage.startsWith('url('), mode: getComputedStyle(layer).maskMode, width: rect.width, height: rect.height, fullscreenWeb: window.art.fullscreenWeb }
|
||||
window.maskCombination.snapshots.push(snapshot)
|
||||
return snapshot
|
||||
try {
|
||||
await expect.poll(() => page.evaluate(() => window.art.duration)).toBeGreaterThan(20)
|
||||
await page.evaluate(() => {
|
||||
window.art.currentTime = 5
|
||||
})
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.maskCombination.times.length), { timeout: 30000 }).toBeGreaterThan(2)
|
||||
// Keep the startup row and sampling history as diagnostics: native model
|
||||
// initialization may block its narrow existing timestamp window. Exercise
|
||||
// the ongoing combination after the real model has become operational.
|
||||
await page.evaluate(async () => {
|
||||
const time = window.art.currentTime + 1
|
||||
window.maskCombination.scheduled = time
|
||||
await window.art.plugins.artplayerPluginDanmuku.load([{ text: 'Ready mask combination', time, mode: 1 }])
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.maskCombination.visible.map(row => row.text))).toContain('Ready mask combination')
|
||||
const row = await page.evaluate(() => window.maskCombination.visible.find(row => row.text === 'Ready mask combination'))
|
||||
expect(row.connected && row.inLayer).toBe(true)
|
||||
expect(row.width).toBeGreaterThan(0)
|
||||
expect(row.height).toBeGreaterThan(0)
|
||||
expect(row.display).not.toBe('none')
|
||||
const initial = await page.evaluate(() => window.maskSnapshot('playing'))
|
||||
expect(initial.mask).toBe(true)
|
||||
expect(initial.sameLayer).toBe(true)
|
||||
await page.locator('#pause').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.video.paused)).toBe(true)
|
||||
// Wait native frames for the already-running SDK frame to finish. Compare
|
||||
// its media timestamp, rather than forbidding one legitimate late encode.
|
||||
const paused = await page.evaluate(async () => {
|
||||
const time = window.art.currentTime
|
||||
const beforeOutputs = window.maskCombination.times.length
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
return { before: time, after: window.art.currentTime, extraOutputs: window.maskCombination.times.length - beforeOutputs, snapshot: window.maskSnapshot('paused') }
|
||||
})
|
||||
expect(Math.abs(paused.after - paused.before)).toBeLessThan(0.01)
|
||||
expect(paused.extraOutputs).toBeLessThanOrEqual(1)
|
||||
await page.evaluate(async () => {
|
||||
const seeked = new Promise(resolve => window.art.video.addEventListener('seeked', resolve, { once: true }))
|
||||
window.art.currentTime = 20
|
||||
window.art.fullscreenWeb = true
|
||||
await seeked
|
||||
window.maskCombination.seeked = { time: window.art.currentTime, outputs: window.maskCombination.times.length }
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThanOrEqual(19.9)
|
||||
const full = await page.evaluate(() => window.maskSnapshot('fullscreen-paused'))
|
||||
expect(full.sameLayer).toBe(true)
|
||||
expect(full.fullscreenWeb).toBe(true)
|
||||
expect(full.width).toBeGreaterThan(initial.width)
|
||||
await page.evaluate(() => window.art.play())
|
||||
await expect.poll(() => page.evaluate(() => window.maskCombination.times.length - window.maskCombination.seeked.outputs), { timeout: 30000 }).toBeGreaterThanOrEqual(2)
|
||||
expect(await page.evaluate(() => window.maskCombination.times.at(-1))).toBeGreaterThan(20)
|
||||
await page.evaluate(async () => {
|
||||
window.art.fullscreenWeb = false
|
||||
window.art.plugins.artplayerPluginDanmuku.hide()
|
||||
window.art.plugins.artplayerPluginDanmuku.show()
|
||||
await window.art.plugins.artplayerPluginDanmuku.load([{ text: 'After seek and visibility', time: window.art.currentTime + 1, mode: 1 }])
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.maskCombination.visible.map(row => row.text))).toContain('After seek and visibility')
|
||||
const afterRow = await page.evaluate(() => window.maskCombination.visible.find(row => row.text === 'After seek and visibility'))
|
||||
expect(afterRow.connected && afterRow.inLayer).toBe(true)
|
||||
expect(afterRow.width).toBeGreaterThan(0)
|
||||
expect(afterRow.height).toBeGreaterThan(0)
|
||||
expect(afterRow.display).not.toBe('none')
|
||||
const restored = await page.evaluate(() => window.maskSnapshot('restored'))
|
||||
expect(restored.sameLayer).toBe(true)
|
||||
expect(restored.mask).toBe(true)
|
||||
expect(restored.width).toBe(initial.width)
|
||||
expect(restored.mode).toBe('alpha')
|
||||
await testInfo.attach('actual-combination-page', { body: await page.screenshot(), contentType: 'image/png' })
|
||||
const outputsBeforeLoad = await page.evaluate(() => window.maskCombination.times.length)
|
||||
load = await observeDanmukuLoad(page, { label: 'Mask load', requireComplete: implementation === 'candidate' })
|
||||
load.modelOutputs = await page.evaluate(before => window.maskCombination.times.length - before, outputsBeforeLoad)
|
||||
expect(load.modelOutputs).toBeGreaterThan(1)
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
expect(await page.evaluate(() => window.maskCombination.layer.style.maskImage)).toBe('none')
|
||||
expect(unexpected).toEqual([])
|
||||
}
|
||||
finally {
|
||||
const state = await page.evaluate(() => ({ times: window.maskCombination.times, visible: window.maskCombination.visible, snapshots: window.maskCombination.snapshots, scheduled: window.maskCombination.scheduled, samples: window.maskCombination.samples, seeked: window.maskCombination.seeked, incompleteLoad: window.danmukuCombinationLoadResult }))
|
||||
await testInfo.attach('actual-combination-evidence', { body: JSON.stringify({ core, implementation, mask: { file: artifact, sha256: hash(code) }, danmuku: { file: danmuku.file, sha256: hash(danmuku.code) }, unexpected, state, load, limitation: 'Actual model, native media, timestamp-delivered Danmuku and CSS web fullscreen. Bounded 20 rows/s after model readiness. No OS fullscreen, GPU completion or device coverage claim.' }, null, 2), contentType: 'application/json' })
|
||||
await page.evaluate(() => {
|
||||
if (window.art && !window.art.isDestroy)
|
||||
window.art.destroy()
|
||||
})
|
||||
}
|
||||
})
|
||||
try {
|
||||
await expect.poll(() => page.evaluate(() => window.art.duration)).toBeGreaterThan(20)
|
||||
await page.evaluate(() => {
|
||||
window.art.currentTime = 5
|
||||
})
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.maskCombination.times.length), { timeout: 30000 }).toBeGreaterThan(2)
|
||||
// Keep the startup row and sampling history as diagnostics: native model
|
||||
// initialization may block its narrow existing timestamp window. Exercise
|
||||
// the ongoing combination after the real model has become operational.
|
||||
await page.evaluate(async () => {
|
||||
const time = window.art.currentTime + 1
|
||||
window.maskCombination.scheduled = time
|
||||
await window.art.plugins.artplayerPluginDanmuku.load([{ text: 'Ready mask combination', time, mode: 1 }])
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.maskCombination.visible.map(row => row.text))).toContain('Ready mask combination')
|
||||
const row = await page.evaluate(() => window.maskCombination.visible.find(row => row.text === 'Ready mask combination'))
|
||||
expect(row.connected && row.inLayer).toBe(true)
|
||||
expect(row.width).toBeGreaterThan(0)
|
||||
expect(row.height).toBeGreaterThan(0)
|
||||
expect(row.display).not.toBe('none')
|
||||
const initial = await page.evaluate(() => window.maskSnapshot('playing'))
|
||||
expect(initial.mask).toBe(true)
|
||||
expect(initial.sameLayer).toBe(true)
|
||||
await page.locator('#pause').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.video.paused)).toBe(true)
|
||||
// Wait native frames for the already-running SDK frame to finish. Compare
|
||||
// its media timestamp, rather than forbidding one legitimate late encode.
|
||||
const paused = await page.evaluate(async () => {
|
||||
const time = window.art.currentTime
|
||||
const beforeOutputs = window.maskCombination.times.length
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
return { before: time, after: window.art.currentTime, extraOutputs: window.maskCombination.times.length - beforeOutputs, snapshot: window.maskSnapshot('paused') }
|
||||
})
|
||||
expect(Math.abs(paused.after - paused.before)).toBeLessThan(0.01)
|
||||
expect(paused.extraOutputs).toBeLessThanOrEqual(1)
|
||||
await page.evaluate(async () => {
|
||||
const seeked = new Promise(resolve => window.art.video.addEventListener('seeked', resolve, { once: true }))
|
||||
window.art.currentTime = 20
|
||||
window.art.fullscreenWeb = true
|
||||
await seeked
|
||||
window.maskCombination.seeked = { time: window.art.currentTime, outputs: window.maskCombination.times.length }
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThanOrEqual(19.9)
|
||||
const full = await page.evaluate(() => window.maskSnapshot('fullscreen-paused'))
|
||||
expect(full.sameLayer).toBe(true)
|
||||
expect(full.fullscreenWeb).toBe(true)
|
||||
expect(full.width).toBeGreaterThan(initial.width)
|
||||
await page.evaluate(() => window.art.play())
|
||||
await expect.poll(() => page.evaluate(() => window.maskCombination.times.length - window.maskCombination.seeked.outputs), { timeout: 30000 }).toBeGreaterThanOrEqual(2)
|
||||
expect(await page.evaluate(() => window.maskCombination.times.at(-1))).toBeGreaterThan(20)
|
||||
await page.evaluate(async () => {
|
||||
window.art.fullscreenWeb = false
|
||||
window.art.plugins.artplayerPluginDanmuku.hide()
|
||||
window.art.plugins.artplayerPluginDanmuku.show()
|
||||
await window.art.plugins.artplayerPluginDanmuku.load([{ text: 'After seek and visibility', time: window.art.currentTime + 1, mode: 1 }])
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.maskCombination.visible.map(row => row.text))).toContain('After seek and visibility')
|
||||
const afterRow = await page.evaluate(() => window.maskCombination.visible.find(row => row.text === 'After seek and visibility'))
|
||||
expect(afterRow.connected && afterRow.inLayer).toBe(true)
|
||||
expect(afterRow.width).toBeGreaterThan(0)
|
||||
expect(afterRow.height).toBeGreaterThan(0)
|
||||
expect(afterRow.display).not.toBe('none')
|
||||
const restored = await page.evaluate(() => window.maskSnapshot('restored'))
|
||||
expect(restored.sameLayer).toBe(true)
|
||||
expect(restored.mask).toBe(true)
|
||||
expect(restored.width).toBe(initial.width)
|
||||
expect(restored.mode).toBe('alpha')
|
||||
await testInfo.attach('actual-combination-page', { body: await page.screenshot(), contentType: 'image/png' })
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
expect(await page.evaluate(() => window.maskCombination.layer.style.maskImage)).toBe('none')
|
||||
expect(unexpected).toEqual([])
|
||||
}
|
||||
finally {
|
||||
const state = await page.evaluate(() => ({ times: window.maskCombination.times, visible: window.maskCombination.visible, snapshots: window.maskCombination.snapshots, scheduled: window.maskCombination.scheduled, samples: window.maskCombination.samples, seeked: window.maskCombination.seeked }))
|
||||
await testInfo.attach('actual-combination-evidence', { body: JSON.stringify({ core, mask: { file: artifact, sha256: hash(code) }, danmuku: { file: danmukuFile, sha256: hash(danmukuCode) }, unexpected, state, limitation: 'Actual model, native media, timestamp-delivered Danmuku and CSS web fullscreen. No OS fullscreen, GPU completion or device coverage claim.' }, null, 2), contentType: 'application/json' })
|
||||
await page.evaluate(() => {
|
||||
if (window.art && !window.art.isDestroy)
|
||||
window.art.destroy()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
// Observe real media/RAF delivery after a companion plugin has become operational.
|
||||
export async function observeDanmukuLoad(page, { label, requireComplete, rate = 20, seconds = 6 }) {
|
||||
let result
|
||||
try {
|
||||
await page.evaluate(async ({ label, rate, seconds }) => {
|
||||
const art = window.art
|
||||
const plugin = art.plugins.artplayerPluginDanmuku
|
||||
const owner = plugin.config({ speed: 1, antiOverlap: false })
|
||||
const host = art.template.$player.ownerDocument.defaultView
|
||||
const base = art.currentTime
|
||||
const rows = Array.from({ length: rate * seconds }, (_, index) => ({
|
||||
id: `${label}-${index}`,
|
||||
text: `${label} ${index}`,
|
||||
time: base + 0.5 + index / rate,
|
||||
mode: index % 3,
|
||||
}))
|
||||
const ids = new Set(rows.map(row => row.id))
|
||||
const state = window.danmukuCombinationLoad = { base, end: base + seconds + 2, rows, visible: [], frames: [], errors: [], startWall: host.performance.now() }
|
||||
const visible = (row) => {
|
||||
if (ids.has(row.id))
|
||||
state.visible.push({ id: row.id, time: art.currentTime, wall: host.performance.now() })
|
||||
}
|
||||
const error = error => state.errors.push(String(error))
|
||||
let frame
|
||||
const sample = () => {
|
||||
state.frames.push({ time: art.currentTime, wall: host.performance.now() })
|
||||
frame = host.requestAnimationFrame(sample)
|
||||
}
|
||||
frame = host.requestAnimationFrame(sample)
|
||||
art.on('artplayerPluginDanmuku:visible', visible)
|
||||
art.on('artplayerPluginDanmuku:error', error)
|
||||
window.finishDanmukuCombinationLoad = () => {
|
||||
host.cancelAnimationFrame(frame)
|
||||
art.off('artplayerPluginDanmuku:visible', visible)
|
||||
art.off('artplayerPluginDanmuku:error', error)
|
||||
const loaded = owner.queue.filter(row => ids.has(row.id))
|
||||
const result = { ...state, endWall: host.performance.now(), time: art.currentTime, loaded: loaded.length, waiting: loaded.filter(row => row.$state === 'wait').length, referenced: loaded.filter(row => row.$ref).length, nodes: art.template.$danmuku.children.length, pooled: owner.$refs.length, uniquePool: new Set(owner.$refs).size, decodedWidth: art.video.videoWidth }
|
||||
window.danmukuCombinationLoadResult = result
|
||||
return result
|
||||
}
|
||||
await plugin.load(rows)
|
||||
}, { label, rate, seconds })
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime >= window.danmukuCombinationLoad.end), { timeout: 16000 }).toBe(true)
|
||||
}
|
||||
finally {
|
||||
result = await page.evaluate(() => window.finishDanmukuCombinationLoad?.())
|
||||
}
|
||||
expect(result.decodedWidth).toBeGreaterThan(0)
|
||||
expect(result.loaded).toBe(rate * seconds)
|
||||
expect(result.errors).toEqual([])
|
||||
if (requireComplete) {
|
||||
expect(result.visible.map(row => row.id).sort()).toEqual(result.rows.map(row => row.id).sort())
|
||||
expect(result.waiting).toBe(rate * seconds)
|
||||
expect(result.referenced).toBe(0)
|
||||
expect(result.uniquePool).toBe(result.pooled)
|
||||
}
|
||||
return { ...result, requireComplete, rate, seconds }
|
||||
}
|
||||
Reference in new issue
Block a user