mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-08 19:06:15 -08:00
test(jassub): [PKG-JASSUB-02] establish lifecycle failures and native render baseline
This commit is contained in:
1 parent
0cf56b64d2
commit
944e2cc9d0
14 files changed
+4311
-11
No files matched your search
+1
-1
@@ -90,7 +90,7 @@
|
||||
"test:vtt-thumbnail-types-package": "node refactor/scripts/vtt-thumbnail-package-types.mjs",
|
||||
"test:multiple-subtitles": "node --test test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js refactor/scripts/multiple-subtitles-contract.test.mjs refactor/scripts/multiple-subtitles-runtime-types.test.mjs refactor/scripts/multiple-subtitles-types.test.mjs",
|
||||
"test:multiple-subtitles-types-package": "node refactor/scripts/multiple-subtitles-package-types.mjs",
|
||||
"test:jassub": "node --test test/jassub.test.js refactor/scripts/jassub-contract.test.mjs refactor/scripts/jassub-provenance.test.mjs",
|
||||
"test:jassub": "node --test test/jassub.test.js refactor/scripts/jassub-contract.test.mjs refactor/scripts/jassub-provenance.test.mjs refactor/scripts/jassub-failures.test.mjs",
|
||||
"check:ci": "node refactor/scripts/ci-workflow.mjs",
|
||||
"test:ci": "node --test test/ci-summary.test.js refactor/scripts/ci-workflow.test.mjs refactor/scripts/impact.test.mjs test/package-runtime.test.js",
|
||||
"test:package:runtime": "node scripts/package-runtime.mjs --canonical",
|
||||
|
||||
@@ -42,5 +42,16 @@ asset comparisons and fails on any request error, without substituting cached ev
|
||||
Set ARTPLAYER_JASSUB_CANDIDATE=1 for current source behavior; ARTPLAYER_JASSUB_ARTIFACT can point
|
||||
at a main/legacy artifact. Tests use controlled DOM, Worker and SIMD detection, with actual
|
||||
vendor JavaScript. They do not render ASS or execute worker WASM. Real browser, failures,
|
||||
repeated destroy and resource cleanup remain later steps. See
|
||||
repeated destroy and resource cleanup remain later steps. Frozen failure tests now
|
||||
cover repeated teardown, cross-parent setVideo, custom canvas, Worker construction
|
||||
failure, stale video frames and incorrect fallback ratechange payloads. They assert
|
||||
the old failures, not candidate fixes. PKG-JASSUB-03 owns adapter cleanup; 07 owns
|
||||
separately tracked vendor lifecycle/clock changes and 05 verifies native behavior.
|
||||
|
||||
`yarn test:browser test/browser/jassub-native.spec.js --workers=1` loads the actual
|
||||
published wrapper, Worker, WASM and default font with offscreenRender=false.
|
||||
Chromium/Firefox render and seek correctly; Windows WebKit's default frame-clock
|
||||
path remains a recorded failure. The onDemandRender=false diagnostic does not
|
||||
waive the default behavior gate. Native failure recovery, default offscreen mode,
|
||||
full devices and sustained resource release remain unverified. See
|
||||
`../../refactor/baselines/jassub-contract.md` for precise contract and provenance evidence.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,72 @@
|
||||
# PKG-JASSUB-02 行为、错误与原生渲染基线
|
||||
|
||||
本步骤建立迁移前基线,生产源码、第三方文件和资源未修改。新增的历史失败
|
||||
断言证明问题存在,不能当作修复通过;后续 03/07 负责修复,05 完成组合验收。
|
||||
|
||||
## 受控行为
|
||||
|
||||
七份真实发布/冻结实现各九个失败或保护场景,共 63 项;既有工厂、入口和
|
||||
来源测试加上事件时钟与切换字幕轨道,`yarn test:jassub` 联合 120 项通过。
|
||||
实际运行 vendor JS,DOM/Worker/RVFC 为受控平台,以下六类问题均已复现:
|
||||
|
||||
| 问题 | 具体结果与后续责任 |
|
||||
| --- | --- |
|
||||
| 直接 destroy 再宿主 destroy,或直接重复 destroy | 二次移除容器抛错;03 修宿主重复调用,07 保证公开实例重复销毁 |
|
||||
| setVideo 切到其他父容器再 destroy | 从新视频父节点删除旧容器失败,Worker 未终止、七监听器保留;07 |
|
||||
| 自定义 canvas | vendor 已创建 Worker,adapter 访问不存在的 _canvasParent.style 抛错,尚未注册宿主清理;03 |
|
||||
| Worker 构造抛 SecurityError | 原错误保留,但先前创建的容器和七监听器遗留;07 |
|
||||
| setVideo 后旧 RVFC 到达 | 实际已排队的旧回调以 time=123 驱动 currentTime=9 的新视频,再建立第二条回调链;07 |
|
||||
| 事件模式 ratechange | 直接 setRate(2) 正常;媒体 ratechange 却将 Event 当作 rate 传给 Worker,未使用 playbackRate=1.5;07 |
|
||||
|
||||
测试还保护已有正常行为:ready 前 destroy 会终止 Worker 并去监听器,虽然测试
|
||||
能观察到终止后仍尝试发 init;Worker error 原样分发,宿主销毁仍能清理;
|
||||
destroy 后遗留的单个 RVFC 到达会退出且不再排队。不能将这些观察夸大成
|
||||
“终止后 Worker 仍实际执行”或“销毁后永远循环”。受控 removeChild 抛的是
|
||||
ERR_ASSERTION,不冒充原生 DOMException;rate 错误负载尚非真实 clone 异常证据。
|
||||
|
||||
正常事件用例验证时间偏移、playing/seeking 状态,以及同步 setTrackByUrl、
|
||||
setTrack、freeTrack 的实际 Worker 消息顺序。保留公开 instance 与返回值。
|
||||
|
||||
## 真实浏览器
|
||||
|
||||
新 `jassub-native.spec.js` 加载真实 npm 1.1.0 主产物、原生 Worker、本地固定 WASM
|
||||
和 Liberation 字体,使用两条自编 ASS 字幕及实际视频。明确设置
|
||||
offscreenRender=false 以读取真实字幕 canvas 像素,其他默认路径未冒充验收。
|
||||
原生 Worker 子类只记录 postMessage/message/terminate,仍委托浏览器原实现。
|
||||
断言初始非透明像素、seek 后不同字幕像素、网页全屏尺寸和正常单次销毁。
|
||||
|
||||
Chromium/Firefox 各三个核心组合通过:候选、实际 npm 5.4.0、实际 5.3.1-beta.1。
|
||||
截图确认字幕已由真实 WASM 绘制。首次 Chromium 三例失败是测试服务器未提供
|
||||
application/wasm,补齐 MIME 后通过;没有改 SDK、替换编译器或放宽等待时间。
|
||||
|
||||
Windows WebKit 26.6 的三个默认 onDemandRender 场景均没有可见字幕。保留的
|
||||
进一步观测显示:资源 200、Worker ready,但媒体已到 6.4992094 秒仍只有 init/
|
||||
canvas 消息,没有 demand 请求;canvas 为零可见像素。显式设置旧选项
|
||||
onDemandRender=false 的候选核心诊断通过。这个诊断不能替代默认路径验收,
|
||||
也不足以断言所有 Safari/设备不支持 RVFC。新增 JASSUB-NATIVE-FRAME-01,后续
|
||||
07/05 继续验证原因与兼容处理。默认 WebKit 用例保持失败,不加 skip、重试或
|
||||
扩大超时。02 的完成只表示已建立成功/失败基线,不表示全浏览器绿色。
|
||||
|
||||
## 维护与验收边界
|
||||
|
||||
```sh
|
||||
yarn test:jassub
|
||||
yarn test:browser test/browser/jassub-native.spec.js --workers=1
|
||||
```
|
||||
|
||||
历史失败测试进入既有 baseline glob 和 test:jassub。设置
|
||||
ARTPLAYER_JASSUB_FAILURE_REPORT 可输出详细受控观察;默认测试不写报告。
|
||||
原生诊断可设置 ARTPLAYER_JASSUB_ON_DEMAND=false,报告保存该选择。按不同运行
|
||||
分别归档 report/results,等待进程终态后再跑下一轮。机器报告包含成功、失败和
|
||||
诊断结果,见 [验证](../baselines/jassub-failures-validation.json)。
|
||||
|
||||
自有 adapter 修复与 vendor 方法修复必须分开。新增 PKG-JASSUB-07,在 03 之后、
|
||||
05 之前处理有证据的 vendor 生命周期/时钟问题;保留原文件及来源基线,任何
|
||||
实际补丁都必须单列差异与候选正例,不得把未修改 vendor 宣称为已修复。
|
||||
完整设备、默认 OffscreenCanvas、真实资源失败、CSP、倍率、长时播放/内存和
|
||||
VideoFrame/ResizeObserver 回收仍属后续验证。源码/类型/发布门槛全部保留。
|
||||
|
||||
本次新增测试及测试服务器 MIME 修正,定向 lint、计划和风险检查通过;完整
|
||||
浏览器流程仍有上述 WebKit 失败,不宣称 CI 或后续组合验收通过。
|
||||
没有新依赖、锁文件或生产产物变化。独立本地提交,不推送或发布;回退本步骤
|
||||
仅移除新基线/测试与 MIME 支持,生产行为保持原样。
|
||||
+7
-5
@@ -2,9 +2,9 @@
|
||||
|
||||
> 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。
|
||||
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 231 项,范围 22 个包及工作区/示例。
|
||||
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 232 项,范围 22 个包及工作区/示例。
|
||||
|
||||
状态:todo 65 / doing 14 / blocked 0 / done 152 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
状态:todo 65 / doing 14 / blocked 0 / done 153 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
|
||||
|
||||
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
| artplayer-plugin-dash-control | 1.1.0 | PKG-DASH-01, PKG-DASH-02, PKG-DASH-03, PKG-DASH-04, PKG-DASH-05, PKG-DASH-06 |
|
||||
| artplayer-plugin-document-pip | 1.1.0 | PKG-DPIP-01, PKG-DPIP-02, PKG-DPIP-03, PKG-DPIP-04, PKG-DPIP-05, PKG-DPIP-06 |
|
||||
| artplayer-plugin-hls-control | 1.1.0 | PKG-HLS-01, PKG-HLS-02, PKG-HLS-03, PKG-HLS-04, PKG-HLS-SDK-01, PKG-HLS-05, PKG-HLS-06 |
|
||||
| artplayer-plugin-jassub | 1.1.0 | PKG-JASSUB-01, PKG-JASSUB-02, PKG-JASSUB-03, PKG-JASSUB-04, PKG-JASSUB-05, PKG-JASSUB-06 |
|
||||
| artplayer-plugin-jassub | 1.1.0 | PKG-JASSUB-01, PKG-JASSUB-02, PKG-JASSUB-03, PKG-JASSUB-04, PKG-JASSUB-05, PKG-JASSUB-06, PKG-JASSUB-07 |
|
||||
| artplayer-plugin-multiple-subtitles | 1.2.0 | PKG-MULTI-SUB-01, PKG-MULTI-SUB-02, PKG-MULTI-SUB-03, PKG-MULTI-SUB-04, PKG-MULTI-SUB-07, PKG-MULTI-SUB-05, PKG-MULTI-SUB-06 |
|
||||
| artplayer-plugin-vast | 1.2.0 | PKG-VAST-01, PKG-VAST-02, PKG-VAST-03, PKG-VAST-04, PKG-VAST-05, PKG-VAST-06 |
|
||||
| artplayer-plugin-vtt-thumbnail | 1.1.0 | PKG-VTT-THUMB-01, PKG-VTT-THUMB-02, PKG-VTT-THUMB-03, PKG-VTT-THUMB-04, PKG-VTT-THUMB-05, PKG-VTT-THUMB-06 |
|
||||
@@ -230,11 +230,12 @@
|
||||
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PKG-JASSUB-01 | artplayer-plugin-jassub<br>核对包契约与历史用法 | BASE-05 | 选项透传、result.instance、vendor 来源、worker/WASM/font 路径 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结;接续 VENDOR-04/05,核对 wrapper/worker/WASM/font 的独立来源与通知,保持资源路径和选项透传 | H | done |
|
||||
| PKG-JASSUB-02 | artplayer-plugin-jassub<br>建立特有行为与错误测试 | PKG-JASSUB-01, ENG-03, ENG-05 | ASS 字体、时钟/seek/倍率/resize、加载失败和销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | todo |
|
||||
| PKG-JASSUB-02 | artplayer-plugin-jassub<br>建立特有行为与错误测试 | PKG-JASSUB-01, ENG-03, ENG-05 | ASS 字体、时钟/seek/倍率/resize、加载失败和销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
|
||||
| PKG-JASSUB-03 | artplayer-plugin-jassub<br>整理内部职责与资源 | PKG-JASSUB-02, CORE-15, CORE-16 | 仅整理自有 adapter/销毁;保留第三方文件及来源 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
|
||||
| PKG-JASSUB-04 | artplayer-plugin-jassub<br>迁移自有源码和公开类型 | PKG-JASSUB-03, ENG-04, ENG-06, CORE-07 | JASSUB option/instance 的兼容类型包装,vendor JS 例外记录 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
|
||||
| PKG-JASSUB-05 | artplayer-plugin-jassub<br>验证新旧核心和组合 | PKG-JASSUB-04, CORE-22 | 真实 worker/WASM 字幕渲染与全屏、旧核心测试 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
| PKG-JASSUB-05 | artplayer-plugin-jassub<br>验证新旧核心和组合 | PKG-JASSUB-04, CORE-22, PKG-JASSUB-07 | 真实 worker/WASM 字幕渲染与全屏、旧核心测试 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
|
||||
| PKG-JASSUB-06 | artplayer-plugin-jassub<br>验证分发并同步文档 | PKG-JASSUB-05, ENG-07 | jassub.js、外部资源路径、许可和离线失败记录;接续 VENDOR-04/05 完整组件/字体通知、LGPL 源码分发核对及六份未明确字体的分发处置 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录;来源身份已由01冻结,完整通知和未明确字体的发布处理必须闭环,不能以来源相同替代许可结论 | H | todo |
|
||||
| PKG-JASSUB-07 | artplayer-plugin-jassub<br>修复 vendor 生命周期与字幕时钟 | PKG-JASSUB-03, PKG-JASSUB-02 | 独立记录 vendor 适配差异;幂等销毁、跨容器 setVideo、构造回滚、逐帧归属和倍率/能力边界 | 保持公开真实 instance、同步方法与资源路径;保留原 vendor 来源指纹并记录每项实际补丁;候选修复正例和真实字幕/失败/时钟验证通过,未证实设备不得冒充通过 | H | todo |
|
||||
|
||||
## 5 包迁移:artplayer-plugin-danmuku-mask
|
||||
|
||||
@@ -532,6 +533,7 @@
|
||||
- PKG-MULTI-SUB-04: [记录](baselines/multiple-subtitles-runtime-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-runtime-types.md) [记录](baselines/multiple-subtitles-public-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-public-types.md) [记录](type-compatibility-policy.md) [记录](baselines/multiple-subtitles-approved-types.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-04-approved-types.md)
|
||||
- PKG-MULTI-SUB-07: [记录](baselines/multiple-subtitles-timestamps.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md)
|
||||
- PKG-JASSUB-01: [记录](baselines/jassub-release.json) [记录](baselines/jassub-vendor.json) [记录](baselines/jassub-font-metadata.json) [记录](baselines/jassub-contract.md) [记录](baselines/jassub-contract-validation.json) [记录](changes/2026-09-13-PKG-JASSUB-01-baseline.md) [记录](baselines/jassub-provenance.json) [记录](baselines/jassub-provenance-validation.json) [记录](changes/2026-09-14-PKG-JASSUB-01-provenance.md)
|
||||
- PKG-JASSUB-02: [记录](changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md) [记录](baselines/jassub-failures-validation.json)
|
||||
- PKG-MASK-01: [记录](baselines/danmuku-mask-release.json) [记录](baselines/danmuku-mask-registry.json) [记录](baselines/danmuku-mask-contract.md) [记录](baselines/danmuku-mask-contract-validation.json) [记录](changes/2026-09-13-PKG-MASK-01-contract.md)
|
||||
- PKG-MASK-02: [记录](changes/2026-09-13-PKG-MASK-02-failures.md) [记录](baselines/danmuku-mask-failures-validation.json)
|
||||
- PKG-MASK-03: [记录](changes/2026-09-13-PKG-MASK-03-lifecycle.md) [记录](baselines/danmuku-mask-lifecycle-validation.json)
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# 进度与证据
|
||||
|
||||
## PKG-JASSUB-02 历史缺陷与原生渲染基线完成
|
||||
|
||||
联合 120 项通过,其中 63 项确认旧失败/已有保护;没有宣称修复。Chromium/Firefox
|
||||
实际 npm 插件和 WASM/字体六个核心组合通过,WebKit 默认逐帧路径仍失败,显式
|
||||
事件时钟诊断单独通过。测试 server 的 WASM MIME 错误已修正并保存首次失败。
|
||||
[基线记录](changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md)明确新增六项风险;
|
||||
03 处理自有 adapter,新增 07 专门修 vendor 生命周期/时钟且纳入 05 前置。生产文件
|
||||
未改,02 仅完成基线职责,全部兼容与发布门槛保留。
|
||||
|
||||
## PKG-JASSUB-01 来源及契约核对完成
|
||||
|
||||
两份 WASM 与 11 份字体的上游 blob 已精确匹配,nightly 对应源码、七子模块和
|
||||
|
||||
@@ -242,3 +242,9 @@
|
||||
| DANMUKU-TIMING-01 | resolved / 已复现 | Hidden Worker wait is deducted from visible lifetime and can recycle a comment immediately after visible | PKG-DANMUKU-11 |
|
||||
| DANMUKU-SAMPLING-01 | resolved / 已复现 | Serial asynchronous placement stops native frame eligibility sampling and delays existing visible expiry | PKG-DANMUKU-12 |
|
||||
| MASK-SCHEDULING-01 | open / 已复现 | Native Mask startup can coincide with a missed narrow Danmuku timestamp window | PKG-MASK-05 |
|
||||
| JASSUB-DESTROY-01 | open / 已复现 | Repeated destruction or cross-parent setVideo can throw before JASSUB cleanup | PKG-JASSUB-02, PKG-JASSUB-03, PKG-JASSUB-07 |
|
||||
| JASSUB-CANVAS-01 | open / 已复现 | Custom canvas registration throws after JASSUB Worker allocation | PKG-JASSUB-02, PKG-JASSUB-03 |
|
||||
| JASSUB-CONSTRUCTION-01 | open / 已复现 | Worker construction failure leaves previously allocated JASSUB DOM and listeners | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-VIDEO-FRAME-01 | open / 已复现 | Old video frame callbacks can drive replacement subtitle time and duplicate chains | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-RATE-01 | open / 已复现 | Fallback ratechange sends an Event instead of numeric playbackRate | PKG-JASSUB-02, PKG-JASSUB-07 |
|
||||
| JASSUB-NATIVE-FRAME-01 | open / 已复现 | Windows WebKit default frame-clock path does not produce subtitle demands in actual playback | PKG-JASSUB-02, PKG-JASSUB-07, PKG-JASSUB-05 |
|
||||
@@ -5425,6 +5425,104 @@
|
||||
],
|
||||
"compatibleResolution": "Retain startup sampling evidence and distinguish it from post-model-ready delivery; investigate actual startup scheduling without silently adding catch-up semantics.",
|
||||
"closureCriteria": "Identify the startup scheduling cause and assess a compatible mitigation or explicitly reviewed limitation using actual model/media evidence; post-ready success alone cannot close this risk."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-DESTROY-01",
|
||||
"title": "Repeated destruction or cross-parent setVideo can throw before JASSUB cleanup",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-03",
|
||||
"PKG-JASSUB-07"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md",
|
||||
"refactor/baselines/jassub-failures-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Use actual resource ownership and tolerate direct instance teardown before host teardown.",
|
||||
"closureCriteria": "Candidate direct/repeated/host teardown and cross-parent video replacement release container, Worker and listeners without throwing or removing user nodes."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-CANVAS-01",
|
||||
"title": "Custom canvas registration throws after JASSUB Worker allocation",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-03"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md",
|
||||
"refactor/baselines/jassub-failures-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Guard adapter-created parent styling; retain the supplied canvas and roll back failed registration.",
|
||||
"closureCriteria": "Custom canvas registration succeeds and cleanup releases owned work while preserving caller DOM; public instance and option override behavior remain compatible."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-CONSTRUCTION-01",
|
||||
"title": "Worker construction failure leaves previously allocated JASSUB DOM and listeners",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-07"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md",
|
||||
"refactor/baselines/jassub-failures-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Roll back each construction phase and rethrow the original error without assuming new returned an instance.",
|
||||
"closureCriteria": "Controlled and native construction failure do not retain owned DOM/listeners/callbacks; original error identity and successful construction remain intact."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-VIDEO-FRAME-01",
|
||||
"title": "Old video frame callbacks can drive replacement subtitle time and duplicate chains",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-07"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md",
|
||||
"refactor/baselines/jassub-failures-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Track video generation and callback ownership across setVideo and destroy.",
|
||||
"closureCriteria": "Actually queued old callbacks cannot deliver stale mediaTime or rearm; replacement video owns one loop, with native evidence."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-RATE-01",
|
||||
"title": "Fallback ratechange sends an Event instead of numeric playbackRate",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-07"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md",
|
||||
"refactor/baselines/jassub-failures-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Adapt event subscription to read the current video rate while retaining direct setRate(number).",
|
||||
"closureCriteria": "Real ratechange and direct calls send numeric values, preserve timing, and avoid stale listeners after source replacement."
|
||||
},
|
||||
{
|
||||
"id": "JASSUB-NATIVE-FRAME-01",
|
||||
"title": "Windows WebKit default frame-clock path does not produce subtitle demands in actual playback",
|
||||
"confirmation": "reproduced",
|
||||
"status": "open",
|
||||
"owners": [
|
||||
"PKG-JASSUB-02",
|
||||
"PKG-JASSUB-07",
|
||||
"PKG-JASSUB-05"
|
||||
],
|
||||
"evidence": [
|
||||
"refactor/changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md",
|
||||
"refactor/baselines/jassub-failures-validation.json"
|
||||
],
|
||||
"compatibleResolution": "Investigate native frame delivery and establish a compatible capability response; explicit event-clock success is diagnostic only.",
|
||||
"closureCriteria": "Default native behavior is verified or an explicit reviewed environment limitation is recorded; no silent global fallback or substitution of diagnostic mode for default acceptance."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
// eslint-disable-next-line test/no-import-node-test -- Frozen failure observations use the repository runner.
|
||||
import test from 'node:test'
|
||||
import { jassubEnvironment, jassubHistorical } from '../../test/helpers/jassub.js'
|
||||
|
||||
const observations = []
|
||||
const implementations = await jassubHistorical()
|
||||
|
||||
function environment(implementation) {
|
||||
// Expose only VM platform constructors for controlled faults; vendor methods stay unchanged.
|
||||
const code = `${implementation.code}\nObject.defineProperty(module.exports.default || module.exports, '__reviewPlatform', { value: { createCanvas: () => document.createElement('canvas'), setGlobal: (name, value) => { globalThis[name] = value }, ErrorEvent, Video: HTMLVideoElement } });`
|
||||
const env = jassubEnvironment({ ...implementation, code })
|
||||
env.platform = env.factory.__reviewPlatform
|
||||
return env
|
||||
}
|
||||
|
||||
function trackListeners(video) {
|
||||
const listeners = new Map()
|
||||
const add = video.addEventListener
|
||||
const remove = video.removeEventListener
|
||||
video.addEventListener = function (type, callback, ...options) {
|
||||
if (!listeners.has(type))
|
||||
listeners.set(type, new Set())
|
||||
listeners.get(type).add(callback)
|
||||
return add.call(this, type, callback, ...options)
|
||||
}
|
||||
video.removeEventListener = function (type, callback, ...options) {
|
||||
listeners.get(type)?.delete(callback)
|
||||
return remove.call(this, type, callback, ...options)
|
||||
}
|
||||
return () => [...listeners.values()].reduce((count, entries) => count + entries.size, 0)
|
||||
}
|
||||
|
||||
function frames(env) {
|
||||
const pending = new Map()
|
||||
const cancelled = []
|
||||
let next = 0
|
||||
env.platform.Video.prototype.requestVideoFrameCallback = function (callback) {
|
||||
pending.set(++next, { video: this, callback })
|
||||
return next
|
||||
}
|
||||
env.platform.Video.prototype.cancelVideoFrameCallback = function (id) {
|
||||
cancelled.push(id)
|
||||
pending.delete(id)
|
||||
}
|
||||
return {
|
||||
pending,
|
||||
cancelled,
|
||||
deliver(video, metadata) {
|
||||
const entry = [...pending].find(([, row]) => row.video === video)
|
||||
assert(entry, 'must deliver an actually queued frame callback')
|
||||
const [id, row] = entry
|
||||
pending.delete(id)
|
||||
row.callback(0, metadata)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for (const implementation of implementations) {
|
||||
test(`JASSUB failure baseline ${implementation.name}: fallback ratechange forwards the Event instead of the numeric media rate`, async () => {
|
||||
const env = environment(implementation)
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
instance.setRate(2)
|
||||
await env.flush()
|
||||
assert.equal(env.workers[0].messages.at(-1).rate, 2)
|
||||
env.art.video.playbackRate = 1.5
|
||||
const event = new Event('ratechange')
|
||||
env.art.video.dispatchEvent(event)
|
||||
await env.flush()
|
||||
const message = env.workers[0].messages.at(-1)
|
||||
assert.equal(message.target, 'video')
|
||||
assert.equal(message.rate, event)
|
||||
assert.notEqual(message.rate, env.art.video.playbackRate)
|
||||
observations.push({ implementation: implementation.name, case: 'rate-event-payload', mediaRate: 1.5, forwardedType: message.rate.type, directNumericRatePreserved: true })
|
||||
instance.destroy()
|
||||
})
|
||||
|
||||
test(`JASSUB failure baseline ${implementation.name}: direct destroy followed by art destroy throws on the removed container`, async () => {
|
||||
const env = environment(implementation)
|
||||
const active = trackListeners(env.art.video)
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
assert.equal(active(), 7)
|
||||
instance.destroy()
|
||||
assert.equal(active(), 0)
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
assert.equal(env.parent.children.length, 1)
|
||||
assert.throws(() => env.emit('destroy'), { code: 'ERR_ASSERTION' })
|
||||
assert.throws(() => instance.destroy(), { code: 'ERR_ASSERTION' })
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
observations.push({ implementation: implementation.name, case: 'repeated-destroy', throws: true, workerTerminations: 1 })
|
||||
})
|
||||
|
||||
test(`JASSUB failure baseline ${implementation.name}: setVideo across parents makes destroy throw before releasing the worker`, async () => {
|
||||
const env = environment(implementation)
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
const secondParent = new env.parent.constructor()
|
||||
const secondVideo = secondParent.appendChild(new env.platform.Video())
|
||||
const active = trackListeners(secondVideo)
|
||||
instance.setVideo(secondVideo)
|
||||
assert.equal(active(), 7)
|
||||
assert.throws(() => instance.destroy(), { code: 'ERR_ASSERTION' })
|
||||
assert.notEqual(instance._destroyed, true)
|
||||
assert.equal(env.workers[0].terminated, 0)
|
||||
assert.equal(env.parent.children.length, 2)
|
||||
assert.equal(active(), 7)
|
||||
observations.push({ implementation: implementation.name, case: 'set-video-parent', workerTerminations: 0, listenersRetained: active(), originalContainerRetained: true })
|
||||
instance.setVideo(env.art.video)
|
||||
instance.destroy()
|
||||
})
|
||||
|
||||
test(`JASSUB failure baseline ${implementation.name}: a supplied canvas makes the adapter throw after allocating the worker`, async () => {
|
||||
const env = environment(implementation)
|
||||
const active = trackListeners(env.art.video)
|
||||
const canvas = env.parent.appendChild(env.platform.createCanvas())
|
||||
assert.throws(() => env.factory({ canvas })(env.art), { name: 'TypeError', message: /reading 'style'/ })
|
||||
await env.flush()
|
||||
assert.equal(env.workers.length, 1)
|
||||
assert.equal(env.workers[0].terminated, 0)
|
||||
assert.equal(env.workers[0].messages[0].target, 'init')
|
||||
assert.equal(env.listeners.has('destroy'), false)
|
||||
assert.equal(canvas.parentNode, env.parent)
|
||||
assert.equal(active(), 7)
|
||||
observations.push({ implementation: implementation.name, case: 'custom-canvas', workerTerminations: 0, listenersRetained: active(), hostDestroyHandlerRegistered: false, initSent: true })
|
||||
env.workers[0].terminate()
|
||||
})
|
||||
|
||||
test(`JASSUB failure baseline ${implementation.name}: Worker construction failure leaves the already inserted container and video listeners`, async () => {
|
||||
const env = environment(implementation)
|
||||
const active = trackListeners(env.art.video)
|
||||
const failure = new DOMException('Controlled Worker construction failure', 'SecurityError')
|
||||
env.platform.setGlobal('Worker', class {
|
||||
constructor() { throw failure }
|
||||
})
|
||||
assert.throws(() => env.factory()(env.art), error => error === failure)
|
||||
await env.flush()
|
||||
assert.equal(env.workers.length, 0)
|
||||
assert.equal(env.listeners.has('destroy'), false)
|
||||
assert.equal(env.parent.children.length, 2)
|
||||
assert.equal(env.parent.children[1].className, 'JASSUB')
|
||||
assert.equal(active(), 7)
|
||||
observations.push({ implementation: implementation.name, case: 'worker-construction-failure', originalErrorPreserved: true, insertedContainerRetained: true, listenersRetained: active() })
|
||||
})
|
||||
|
||||
test(`JASSUB failure baseline ${implementation.name}: a previous video's queued frame drives the replacement video and creates a second loop`, async () => {
|
||||
const env = environment(implementation)
|
||||
const clock = frames(env)
|
||||
const oldVideo = env.art.video
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
assert.equal(clock.pending.size, 1)
|
||||
const newVideo = env.createVideo()
|
||||
newVideo.currentTime = 9
|
||||
instance.setVideo(newVideo)
|
||||
await env.flush()
|
||||
assert.equal(clock.pending.size, 2)
|
||||
assert.equal(clock.cancelled.length, 0)
|
||||
clock.deliver(oldVideo, { mediaTime: 123, width: 640, height: 360 })
|
||||
await env.flush()
|
||||
assert.equal(env.workers[0].messages.filter(item => item.target === 'demand').at(-1).time, 123)
|
||||
assert.equal([...clock.pending.values()].filter(row => row.video === newVideo).length, 2)
|
||||
observations.push({ implementation: implementation.name, case: 'old-video-frame', replacementCurrentTime: newVideo.currentTime, staleDemandTime: 123, replacementPendingCallbacks: 2, cancelledCallbacks: 0 })
|
||||
instance.destroy()
|
||||
})
|
||||
|
||||
test(`JASSUB containment baseline ${implementation.name}: destroy before ready terminates and detaches, but initialization continues after termination`, async () => {
|
||||
const env = environment(implementation)
|
||||
const active = trackListeners(env.art.video)
|
||||
const { instance } = env.factory()(env.art)
|
||||
const worker = env.workers[0]
|
||||
const post = worker.postMessage
|
||||
const afterTermination = []
|
||||
worker.postMessage = function (message) {
|
||||
if (this.terminated)
|
||||
afterTermination.push(message.target)
|
||||
return post.call(this, message)
|
||||
}
|
||||
let settled = false
|
||||
instance._loaded.then(() => {
|
||||
settled = true
|
||||
})
|
||||
instance.setTrack('[Script Info]')
|
||||
instance.destroy()
|
||||
await env.flush()
|
||||
assert.equal(worker.terminated, 1)
|
||||
assert.equal(env.parent.children.length, 1)
|
||||
assert.equal(active(), 0)
|
||||
assert.deepEqual(afterTermination, ['init'])
|
||||
assert.equal(settled, false)
|
||||
assert.equal(worker.messages.some(item => item.target === 'setTrack'), false)
|
||||
observations.push({ implementation: implementation.name, case: 'destroy-before-ready', workerTerminations: 1, listenersRetained: 0, postAfterTermination: afterTermination, loadedSettledWithinFixtureFlush: settled })
|
||||
})
|
||||
|
||||
test(`JASSUB containment baseline ${implementation.name}: Worker errors reach the instance and explicit host destroy still releases resources`, async () => {
|
||||
const env = environment(implementation)
|
||||
const active = trackListeners(env.art.video)
|
||||
const { instance } = env.factory()(env.art)
|
||||
const failure = new Error('Controlled Worker script load failure')
|
||||
const errors = []
|
||||
instance.addEventListener('error', event => errors.push(event.error))
|
||||
env.workers[0].onerror(new env.platform.ErrorEvent('error', { error: failure }))
|
||||
await env.flush()
|
||||
assert.deepEqual(errors, [failure])
|
||||
assert.equal(env.errors.at(-1), failure)
|
||||
assert.equal(env.workers[0].terminated, 0)
|
||||
assert.equal(active(), 7)
|
||||
env.emit('destroy')
|
||||
assert.equal(env.workers[0].terminated, 1)
|
||||
assert.equal(env.parent.children.length, 1)
|
||||
assert.equal(active(), 0)
|
||||
observations.push({ implementation: implementation.name, case: 'worker-error', exactErrorForwarded: true, automaticallyTerminated: false, explicitHostCleanup: true })
|
||||
})
|
||||
|
||||
test(`JASSUB containment baseline ${implementation.name}: the outstanding frame after destroy does no work and does not rearm`, async () => {
|
||||
const env = environment(implementation)
|
||||
const clock = frames(env)
|
||||
const { instance } = env.factory()(env.art)
|
||||
await env.ready()
|
||||
instance.destroy()
|
||||
await env.flush()
|
||||
assert.equal(clock.pending.size, 1)
|
||||
assert.equal(clock.cancelled.length, 0)
|
||||
const before = env.workers[0].messages.length
|
||||
clock.deliver(env.art.video, { mediaTime: 123, width: 640, height: 360 })
|
||||
await env.flush()
|
||||
assert.equal(env.workers[0].messages.length, before)
|
||||
assert.equal(clock.pending.size, 0)
|
||||
observations.push({ implementation: implementation.name, case: 'frame-after-destroy', activelyCancelled: false, callbackDoesNoWork: true, callbackRearms: false })
|
||||
})
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
if (process.env.ARTPLAYER_JASSUB_FAILURE_REPORT)
|
||||
fs.writeFileSync(process.env.ARTPLAYER_JASSUB_FAILURE_REPORT, `${JSON.stringify(observations, null, 2)}\n`)
|
||||
})
|
||||
+24
-3
@@ -2434,11 +2434,14 @@
|
||||
"ENG-03",
|
||||
"ENG-05"
|
||||
],
|
||||
"status": "todo",
|
||||
"status": "done",
|
||||
"risk": "H",
|
||||
"deliverable": "ASS 字体、时钟/seek/倍率/resize、加载失败和销毁",
|
||||
"acceptance": "旧版本行为可重跑,成功/失败/切源/销毁有必要断言",
|
||||
"evidence": []
|
||||
"evidence": [
|
||||
"changes/2026-09-14-PKG-JASSUB-02-failure-baseline.md",
|
||||
"baselines/jassub-failures-validation.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PKG-JASSUB-03",
|
||||
@@ -2486,7 +2489,8 @@
|
||||
],
|
||||
"dependsOn": [
|
||||
"PKG-JASSUB-04",
|
||||
"CORE-22"
|
||||
"CORE-22",
|
||||
"PKG-JASSUB-07"
|
||||
],
|
||||
"status": "todo",
|
||||
"risk": "H",
|
||||
@@ -2511,6 +2515,23 @@
|
||||
"acceptance": "tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录;来源身份已由01冻结,完整通知和未明确字体的发布处理必须闭环,不能以来源相同替代许可结论",
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "PKG-JASSUB-07",
|
||||
"phase": "5 包迁移:artplayer-plugin-jassub",
|
||||
"title": "修复 vendor 生命周期与字幕时钟",
|
||||
"scope": [
|
||||
"artplayer-plugin-jassub"
|
||||
],
|
||||
"dependsOn": [
|
||||
"PKG-JASSUB-03",
|
||||
"PKG-JASSUB-02"
|
||||
],
|
||||
"status": "todo",
|
||||
"risk": "H",
|
||||
"deliverable": "独立记录 vendor 适配差异;幂等销毁、跨容器 setVideo、构造回滚、逐帧归属和倍率/能力边界",
|
||||
"acceptance": "保持公开真实 instance、同步方法与资源路径;保留原 vendor 来源指纹并记录每项实际补丁;候选修复正例和真实字幕/失败/时钟验证通过,未证实设备不得冒充通过",
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"id": "PKG-MASK-01",
|
||||
"phase": "5 包迁移:artplayer-plugin-danmuku-mask",
|
||||
|
||||
@@ -169,3 +169,12 @@ The startup time=6 row remains diagnostic because initial model work can coincid
|
||||
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.
|
||||
|
||||
`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.
|
||||
Windows WebKit currently fails the default frame-clock path; the explicitly
|
||||
selected ARTPLAYER_JASSUB_ON_DEMAND=false diagnostic passes on the candidate core.
|
||||
This is a retained historical failure, not a waived browser gate. The server must
|
||||
serve WASM as application/wasm for native streaming compilation. Archive failed
|
||||
and successful runs independently. See the PKG-JASSUB-02 change record.
|
||||
@@ -0,0 +1,150 @@
|
||||
import fs from 'node:fs'
|
||||
import process from 'node:process'
|
||||
import { ensureArchive, hash, readMember } from '../../refactor/scripts/releases.mjs'
|
||||
import { expect, test } from './fixtures.js'
|
||||
|
||||
const baseline = JSON.parse(fs.readFileSync('refactor/baselines/jassub-release.json', 'utf8'))
|
||||
const release = baseline.release
|
||||
const archive = await ensureArchive(release)
|
||||
const member = `package/${release.manifest.main.replace(/^\.\//, '')}`
|
||||
const code = readMember(archive, member).toString()
|
||||
const onDemandRender = process.env.ARTPLAYER_JASSUB_ON_DEMAND !== 'false'
|
||||
const subtitles = `[Script Info]
|
||||
ScriptType: v4.00+
|
||||
PlayResX: 640
|
||||
PlayResY: 360
|
||||
[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: Default,Liberation Sans,36,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,20,1
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||
Dialogue: 0,0:00:00.00,0:00:04.00,Default,,0,0,0,,Before seek
|
||||
Dialogue: 0,0:00:05.00,0:00:30.00,Default,,0,0,0,,After seek with actual WASM
|
||||
`
|
||||
|
||||
for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
|
||||
test(`JASSUB published ${release.version} renders actual WASM subtitles through seek and layout with ${core} core`, async ({ page, context }, testInfo) => {
|
||||
const resources = []
|
||||
const external = []
|
||||
const origin = new URL(testInfo.project.use.baseURL).origin
|
||||
await context.route('**/*', (route) => {
|
||||
if (new URL(route.request().url()).origin !== origin) {
|
||||
external.push(route.request().url())
|
||||
return route.abort()
|
||||
}
|
||||
return route.continue()
|
||||
})
|
||||
context.on('response', (response) => {
|
||||
if (response.url().includes('/assets/jassub/'))
|
||||
resources.push({ url: response.url(), status: response.status(), contentType: response.headers()['content-type'] })
|
||||
})
|
||||
await page.goto(`/test/player.html?core=${core}`)
|
||||
await page.addScriptTag({ content: code })
|
||||
await page.evaluate(() => {
|
||||
window.jassubNative = { workers: [], snapshots: [], errors: [] }
|
||||
const NativeWorker = window.Worker
|
||||
window.Worker = class extends NativeWorker {
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
this.observation = { url: String(args[0]), terminated: 0, sent: [], received: [] }
|
||||
window.jassubNative.workers.push(this.observation)
|
||||
this.addEventListener('message', ({ data }) => this.observation.received.push({ target: data.target, images: data.images?.length, width: data.width, height: data.height }))
|
||||
}
|
||||
|
||||
postMessage(message, ...rest) {
|
||||
this.observation.sent.push({ target: message.target, time: message.time, width: message.width, height: message.height })
|
||||
return super.postMessage(message, ...rest)
|
||||
}
|
||||
|
||||
terminate() {
|
||||
this.observation.terminated++
|
||||
return super.terminate()
|
||||
}
|
||||
}
|
||||
window.createPlayer('/assets/sample/steve-jobs.mp4')
|
||||
})
|
||||
try {
|
||||
await expect.poll(() => page.evaluate(() => window.art.duration)).toBeGreaterThan(30)
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.1)
|
||||
await page.locator('#pause').click()
|
||||
await page.evaluate(async ({ subContent, onDemandRender }) => {
|
||||
const register = window.artplayerPluginJassub.default || window.artplayerPluginJassub
|
||||
window.art.plugins.add(register({
|
||||
subContent,
|
||||
workerUrl: '/assets/jassub/jassub-worker.js',
|
||||
wasmUrl: '/assets/jassub/jassub-worker.wasm',
|
||||
modernWasmUrl: '/assets/jassub/jassub-worker-modern.wasm',
|
||||
availableFonts: { 'liberation sans': '/assets/jassub/default.woff2' },
|
||||
fonts: ['/assets/jassub/default.woff2'],
|
||||
offscreenRender: false,
|
||||
onDemandRender,
|
||||
}))
|
||||
window.jassub = window.art.plugins.artplayerPluginJassub.instance
|
||||
window.jassub.addEventListener('error', event => window.jassubNative.errors.push(String(event.error)))
|
||||
await new Promise((resolve, reject) => {
|
||||
window.jassub.addEventListener('ready', resolve, { once: true })
|
||||
window.jassub.addEventListener('error', event => reject(event.error), { once: true })
|
||||
})
|
||||
window.jassubPixels = (phase) => {
|
||||
const canvas = window.jassub._canvas
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const pixels = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data
|
||||
let visible = 0
|
||||
let signature = 0
|
||||
for (let i = 3; i < pixels.length; i += 4) {
|
||||
if (pixels[i] > 0) {
|
||||
visible++
|
||||
signature = (signature + i * pixels[i]) >>> 0
|
||||
}
|
||||
}
|
||||
const state = { phase, visible, signature, width: canvas.width, height: canvas.height, cssWidth: rect.width, cssHeight: rect.height, time: window.art.currentTime, connected: canvas.isConnected }
|
||||
window.jassubNative.lastPixel = state
|
||||
if (phase)
|
||||
window.jassubNative.snapshots.push(state)
|
||||
return state
|
||||
}
|
||||
}, { subContent: subtitles, onDemandRender })
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)
|
||||
const before = await page.evaluate(() => window.jassubPixels('before-seek'))
|
||||
expect(before.connected).toBe(true)
|
||||
await page.locator('#pause').click()
|
||||
await page.evaluate(async () => {
|
||||
const seeked = new Promise(resolve => window.art.video.addEventListener('seeked', resolve, { once: true }))
|
||||
window.art.currentTime = 10
|
||||
await seeked
|
||||
})
|
||||
await page.locator('#play').click()
|
||||
await expect.poll(() => page.evaluate(() => window.jassubPixels().signature)).not.toBe(before.signature)
|
||||
await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)
|
||||
const after = await page.evaluate(() => window.jassubPixels('after-seek'))
|
||||
expect(after.time).toBeGreaterThanOrEqual(10)
|
||||
expect(after.signature).not.toBe(before.signature)
|
||||
await page.evaluate(() => {
|
||||
window.art.fullscreenWeb = true
|
||||
})
|
||||
await expect.poll(() => page.evaluate(() => window.jassubPixels().cssWidth)).toBeGreaterThan(before.cssWidth)
|
||||
await expect.poll(() => page.evaluate(() => window.jassubPixels().visible)).toBeGreaterThan(100)
|
||||
await page.evaluate(() => window.jassubPixels('fullscreen'))
|
||||
await testInfo.attach('actual-jassub-page', { body: await page.screenshot(), contentType: 'image/png' })
|
||||
await page.evaluate(() => window.art.destroy(false))
|
||||
expect(await page.evaluate(() => document.querySelectorAll('.JASSUB').length)).toBe(0)
|
||||
expect(await page.evaluate(() => window.jassubNative.workers.map(worker => worker.terminated))).toEqual([1])
|
||||
expect(await page.evaluate(() => window.jassubNative.errors)).toEqual([])
|
||||
expect(external).toEqual([])
|
||||
expect(resources.some(item => item.url.endsWith('.wasm') && item.status === 200 && item.contentType === 'application/wasm')).toBe(true)
|
||||
expect(resources.some(item => item.url.endsWith('.woff2') && item.status === 200)).toBe(true)
|
||||
}
|
||||
finally {
|
||||
const state = await page.evaluate(() => window.jassubNative).catch(error => ({ unavailable: error.message }))
|
||||
await testInfo.attach('actual-jassub-evidence', { body: JSON.stringify({ core, onDemandRender, release: { version: release.version, integrity: release.integrity, member, sha256: hash(code) }, subtitleSha256: hash(subtitles), resources, external, state, limitation: 'Actual published plugin, worker, WASM and local font with offscreenRender=false. No default offscreen path, failure recovery, repeated destruction or device acceptance.' }, null, 2), contentType: 'application/json' })
|
||||
if (!page.isClosed()) {
|
||||
await page.evaluate(() => {
|
||||
if (window.art && !window.art.isDestroy)
|
||||
window.art.destroy()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -83,7 +83,7 @@ add('/test/audio-tone.m4a', fs.readFileSync(path.join(workspace, 'test/browser/m
|
||||
add('/test/thumbnail-pattern.mp4', fs.readFileSync(path.join(workspace, 'test/browser/media/thumbnail-pattern.mp4')), { kind: 'generated-media', file: 'test/browser/media/thumbnail-pattern.mp4' })
|
||||
add('/test/auto-thumbnail-timeline.mp4', fs.readFileSync(path.join(workspace, 'test/browser/media/auto-thumbnail-timeline.mp4')), { kind: 'generated-media', file: 'test/browser/media/auto-thumbnail-timeline.mp4' })
|
||||
|
||||
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.mp4': 'video/mp4', '.m4a': 'audio/mp4', '.webm': 'video/webm', '.vtt': 'text/vtt', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg' }
|
||||
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.mp4': 'video/mp4', '.m4a': 'audio/mp4', '.webm': 'video/webm', '.vtt': 'text/vtt', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.wasm': 'application/wasm', '.woff2': 'font/woff2' }
|
||||
add('/test/declaration-cues.vtt', fs.readFileSync(path.join(workspace, 'test/browser/media/declaration-cues.vtt')), { kind: 'test-subtitles', file: 'test/browser/media/declaration-cues.vtt' })
|
||||
add('/test/thumbnail-grid.svg', fs.readFileSync(path.join(workspace, 'test/browser/media/thumbnail-grid.svg')), { kind: 'test-thumbnail-grid', file: 'test/browser/media/thumbnail-grid.svg' })
|
||||
add('/test/legacy-safe-area.js', fs.readFileSync(path.join(workspace, 'test/helpers/legacy-safe-area.js')), { kind: 'frozen-own-source', file: 'test/helpers/legacy-safe-area.js', commit: 'ccf77c4e' })
|
||||
|
||||
@@ -76,4 +76,30 @@ for (const implementation of implementations) {
|
||||
assert.equal(init.availableFonts['liberation sans'], './default.woff2')
|
||||
result.instance.destroy()
|
||||
})
|
||||
test(`JASSUB ${implementation.name}: fallback clock keeps offsets and synchronous track replacement preserves message order`, async () => {
|
||||
const env = jassubEnvironment(implementation)
|
||||
const { instance } = env.factory({ timeOffset: -0.25 })(env.art)
|
||||
await env.ready()
|
||||
env.art.video.currentTime = 12
|
||||
env.art.video.paused = false
|
||||
env.art.video.dispatchEvent(new Event('playing'))
|
||||
await env.flush()
|
||||
const playing = env.workers[0].messages.at(-1)
|
||||
assert.equal(playing.target, 'video')
|
||||
assert.equal(playing.currentTime, 11.75)
|
||||
assert.equal(playing.isPaused, false)
|
||||
env.art.video.dispatchEvent(new Event('seeking'))
|
||||
await env.flush()
|
||||
assert.equal(env.workers[0].messages.at(-1).isPaused, true)
|
||||
const start = env.workers[0].messages.length
|
||||
assert.equal(instance.setTrackByUrl('/replacement.ass'), undefined)
|
||||
assert.equal(instance.setTrack('[Script Info]\nTitle: Replacement'), undefined)
|
||||
assert.equal(instance.freeTrack(), undefined)
|
||||
await env.flush()
|
||||
const messages = env.workers[0].messages.slice(start)
|
||||
assert.deepEqual(messages.map(message => message.target), ['setTrackByUrl', 'setTrack', 'freeTrack'])
|
||||
assert.equal(messages[0].url, '/replacement.ass')
|
||||
assert.equal(messages[1].content, '[Script Info]\nTitle: Replacement')
|
||||
instance.destroy()
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user