test(mediabunny): [PKG-MB-02] checkpoint native playback and late load failures

This commit is contained in:
Harvey Zhao committed 2026-09-12 17:12:59 +08:00
1 parent 1cfd2e17f3
commit 63c8938dd7
10 files changed
+908 -10

No files matched your search

+713
View File
@@ -0,0 +1,713 @@
{
"task": "PKG-MB-02",
"status": "doing",
"parentCommit": "1cfd2e17f319749ba5f2e73f4c54c21d858e9d7e",
"node": {
"tests": 22,
"historicalFailuresReproduced": 3,
"scope": "19 existing contracts plus actual historical load/destroy with controlled pending performLoad and timers. Does not model full core destroy or real decode."
},
"browser": {
"report": "refactor/.cache/mb02-first-results/report.json",
"results": "refactor/.cache/mb02-first-results/results",
"tests": 6,
"nativePlaybackSuccesses": 4,
"verifiedCapabilityFailures": 2,
"skipped": 0,
"records": [
{
"project": "chromium",
"browser": "153.0.8010.12",
"platform": "win32",
"implementation": "published-1.2.0",
"sha256": "7d088c0abe2f3c1e2857cb0547405ce1e5588630f28e484a64211fc675322bbd",
"capabilities": {
"secure": true,
"VideoDecoder": "function",
"AudioDecoder": "function",
"AudioContext": "function",
"webkitAudioContext": "undefined",
"coreVersion": "5.4.0"
},
"outcome": "native-mp4-playback-observed",
"state": {
"events": [
{
"name": "waiting",
"time": 97.90000000596046,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadstart",
"time": 98,
"currentTime": 0,
"readyState": 0
},
{
"name": "waiting",
"time": 98.09999999403954,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadstart",
"time": 98.09999999403954,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadedmetadata",
"time": 157.80000001192093,
"currentTime": 0,
"readyState": 1
},
{
"name": "durationchange",
"time": 157.80000001192093,
"currentTime": 0,
"readyState": 1
},
{
"name": "progress",
"time": 157.90000000596046,
"currentTime": 0,
"readyState": 1
},
{
"name": "loadeddata",
"time": 171.09999999403954,
"currentTime": 0,
"readyState": 4
},
{
"name": "canplay",
"time": 171.09999999403954,
"currentTime": 0,
"readyState": 4
},
{
"name": "canplaythrough",
"time": 171.09999999403954,
"currentTime": 0,
"readyState": 4
},
{
"name": "progress",
"time": 171.09999999403954,
"currentTime": 0,
"readyState": 4
},
{
"name": "play",
"time": 256.30000001192093,
"currentTime": 0,
"readyState": 4
},
{
"name": "playing",
"time": 256.30000001192093,
"currentTime": 0,
"readyState": 4
},
{
"name": "timeupdate",
"time": 265.90000000596046,
"currentTime": 0.010666666666666665,
"readyState": 4
},
{
"name": "timeupdate",
"time": 531,
"currentTime": 0.28,
"readyState": 4
},
{
"name": "pause",
"time": 655.3000000119209,
"currentTime": 0.4,
"readyState": 4
}
],
"actions": [],
"ready": true,
"dimensions": [
320,
180,
320,
180
],
"paused": true,
"currentTime": 0.4,
"error": null,
"readyState": 4,
"audioState": "running"
},
"scope": "Two actual published proxies with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No candidate, HLS or AV-sync acceptance."
},
{
"project": "chromium",
"browser": "153.0.8010.12",
"platform": "win32",
"implementation": "published-1.0.0",
"sha256": "7331465d8c7a6575505b9d3fd45986c5d3d9cb3839b2620ad5a52c581199562b",
"capabilities": {
"secure": true,
"VideoDecoder": "function",
"AudioDecoder": "function",
"AudioContext": "function",
"webkitAudioContext": "undefined",
"coreVersion": "5.4.0"
},
"outcome": "native-mp4-playback-observed",
"state": {
"events": [
{
"name": "waiting",
"time": 103.19999998807907,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadstart",
"time": 103.39999997615814,
"currentTime": 0,
"readyState": 0
},
{
"name": "waiting",
"time": 103.39999997615814,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadstart",
"time": 103.39999997615814,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadedmetadata",
"time": 167.39999997615814,
"currentTime": 0,
"readyState": 1
},
{
"name": "durationchange",
"time": 167.39999997615814,
"currentTime": 0,
"readyState": 1
},
{
"name": "progress",
"time": 167.39999997615814,
"currentTime": 0,
"readyState": 1
},
{
"name": "loadeddata",
"time": 176.39999997615814,
"currentTime": 0,
"readyState": 1
},
{
"name": "loadeddata",
"time": 176.39999997615814,
"currentTime": 0,
"readyState": 4
},
{
"name": "canplay",
"time": 176.5,
"currentTime": 0,
"readyState": 4
},
{
"name": "canplaythrough",
"time": 176.5,
"currentTime": 0,
"readyState": 4
},
{
"name": "progress",
"time": 176.5,
"currentTime": 0,
"readyState": 4
},
{
"name": "play",
"time": 260.7999999821186,
"currentTime": 0,
"readyState": 4
},
{
"name": "playing",
"time": 260.89999997615814,
"currentTime": 0,
"readyState": 4
},
{
"name": "timeupdate",
"time": 270.2999999821186,
"currentTime": 0.010666666666666665,
"readyState": 4
},
{
"name": "timeupdate",
"time": 536,
"currentTime": 0.2693333333333333,
"readyState": 4
},
{
"name": "pause",
"time": 661.0999999940395,
"currentTime": 0.4,
"readyState": 4
}
],
"actions": [],
"ready": true,
"dimensions": [
320,
180,
320,
180
],
"paused": true,
"currentTime": 0.4,
"error": null,
"readyState": 4,
"audioState": "running"
},
"scope": "Two actual published proxies with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No candidate, HLS or AV-sync acceptance."
},
{
"project": "firefox",
"browser": "155.0",
"platform": "win32",
"implementation": "published-1.2.0",
"sha256": "7d088c0abe2f3c1e2857cb0547405ce1e5588630f28e484a64211fc675322bbd",
"capabilities": {
"secure": true,
"VideoDecoder": "function",
"AudioDecoder": "function",
"AudioContext": "function",
"webkitAudioContext": "undefined",
"coreVersion": "5.4.0"
},
"outcome": "native-mp4-playback-observed",
"state": {
"events": [
{
"name": "waiting",
"time": 408,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadstart",
"time": 408,
"currentTime": 0,
"readyState": 0
},
{
"name": "waiting",
"time": 408,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadstart",
"time": 408,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadedmetadata",
"time": 432,
"currentTime": 0,
"readyState": 1
},
{
"name": "durationchange",
"time": 432,
"currentTime": 0,
"readyState": 1
},
{
"name": "progress",
"time": 432,
"currentTime": 0,
"readyState": 1
},
{
"name": "loadeddata",
"time": 448,
"currentTime": 0,
"readyState": 4
},
{
"name": "canplay",
"time": 449,
"currentTime": 0,
"readyState": 4
},
{
"name": "canplaythrough",
"time": 449,
"currentTime": 0,
"readyState": 4
},
{
"name": "progress",
"time": 449,
"currentTime": 0,
"readyState": 4
},
{
"name": "play",
"time": 606,
"currentTime": 0,
"readyState": 4
},
{
"name": "playing",
"time": 606,
"currentTime": 0,
"readyState": 4
},
{
"name": "timeupdate",
"time": 613,
"currentTime": 0,
"readyState": 4
},
{
"name": "timeupdate",
"time": 862,
"currentTime": 0.2506666666666667,
"readyState": 4
},
{
"name": "pause",
"time": 1017,
"currentTime": 0.41066666666666674,
"readyState": 4
}
],
"actions": [],
"ready": true,
"dimensions": [
320,
180,
320,
180
],
"paused": true,
"currentTime": 0.41066666666666674,
"error": null,
"readyState": 4,
"audioState": "running"
},
"scope": "Two actual published proxies with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No candidate, HLS or AV-sync acceptance."
},
{
"project": "firefox",
"browser": "155.0",
"platform": "win32",
"implementation": "published-1.0.0",
"sha256": "7331465d8c7a6575505b9d3fd45986c5d3d9cb3839b2620ad5a52c581199562b",
"capabilities": {
"secure": true,
"VideoDecoder": "function",
"AudioDecoder": "function",
"AudioContext": "function",
"webkitAudioContext": "undefined",
"coreVersion": "5.4.0"
},
"outcome": "native-mp4-playback-observed",
"state": {
"events": [
{
"name": "waiting",
"time": 339,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadstart",
"time": 339,
"currentTime": 0,
"readyState": 0
},
{
"name": "waiting",
"time": 339,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadstart",
"time": 339,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadedmetadata",
"time": 369,
"currentTime": 0,
"readyState": 1
},
{
"name": "durationchange",
"time": 369,
"currentTime": 0,
"readyState": 1
},
{
"name": "progress",
"time": 369,
"currentTime": 0,
"readyState": 1
},
{
"name": "loadeddata",
"time": 384,
"currentTime": 0,
"readyState": 1
},
{
"name": "loadeddata",
"time": 385,
"currentTime": 0,
"readyState": 4
},
{
"name": "canplay",
"time": 385,
"currentTime": 0,
"readyState": 4
},
{
"name": "canplaythrough",
"time": 385,
"currentTime": 0,
"readyState": 4
},
{
"name": "progress",
"time": 385,
"currentTime": 0,
"readyState": 4
},
{
"name": "play",
"time": 535,
"currentTime": 0,
"readyState": 4
},
{
"name": "playing",
"time": 535,
"currentTime": 0,
"readyState": 4
},
{
"name": "timeupdate",
"time": 542,
"currentTime": 0.010666666666666658,
"readyState": 4
},
{
"name": "timeupdate",
"time": 808,
"currentTime": 0.28,
"readyState": 4
},
{
"name": "pause",
"time": 937,
"currentTime": 0.41066666666666674,
"readyState": 4
}
],
"actions": [],
"ready": true,
"dimensions": [
320,
180,
320,
180
],
"paused": true,
"currentTime": 0.41066666666666674,
"error": null,
"readyState": 4,
"audioState": "running"
},
"scope": "Two actual published proxies with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No candidate, HLS or AV-sync acceptance."
},
{
"project": "webkit",
"browser": "26.6",
"platform": "win32",
"implementation": "published-1.2.0",
"sha256": "7d088c0abe2f3c1e2857cb0547405ce1e5588630f28e484a64211fc675322bbd",
"capabilities": {
"secure": true,
"VideoDecoder": "undefined",
"AudioDecoder": "undefined",
"AudioContext": "undefined",
"webkitAudioContext": "undefined",
"coreVersion": "5.4.0"
},
"outcome": "verified-unsupported-capability-control",
"state": {
"events": [
{
"name": "waiting",
"time": 158,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadstart",
"time": 158,
"currentTime": 0,
"readyState": 0
},
{
"name": "waiting",
"time": 165,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadstart",
"time": 165,
"currentTime": 0,
"readyState": 0
},
{
"name": "error",
"time": 229,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadedmetadata",
"time": 232,
"currentTime": 0,
"readyState": 1
},
{
"name": "durationchange",
"time": 232,
"currentTime": 0,
"readyState": 1
},
{
"name": "progress",
"time": 232,
"currentTime": 0,
"readyState": 1
}
],
"actions": [],
"ready": false,
"dimensions": [
0,
0,
0,
0
],
"paused": true,
"currentTime": 0,
"error": {
"code": 4,
"message": "undefined is not a constructor (evaluating 'new t')"
},
"readyState": 1
},
"scope": "Two actual published proxies with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No candidate, HLS or AV-sync acceptance."
},
{
"project": "webkit",
"browser": "26.6",
"platform": "win32",
"implementation": "published-1.0.0",
"sha256": "7331465d8c7a6575505b9d3fd45986c5d3d9cb3839b2620ad5a52c581199562b",
"capabilities": {
"secure": true,
"VideoDecoder": "undefined",
"AudioDecoder": "undefined",
"AudioContext": "undefined",
"webkitAudioContext": "undefined",
"coreVersion": "5.4.0"
},
"outcome": "verified-unsupported-capability-control",
"state": {
"events": [
{
"name": "waiting",
"time": 149,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadstart",
"time": 149,
"currentTime": 0,
"readyState": 0
},
{
"name": "waiting",
"time": 151,
"currentTime": 0,
"readyState": 0
},
{
"name": "loadstart",
"time": 151,
"currentTime": 0,
"readyState": 0
},
{
"name": "error",
"time": 200,
"currentTime": 0,
"readyState": 0
}
],
"actions": [],
"ready": false,
"dimensions": [
0,
0,
0,
0
],
"paused": true,
"currentTime": 0,
"error": {
"code": 4,
"message": "undefined is not a constructor (evaluating 'new t')"
},
"readyState": 0
},
"scope": "Two actual published proxies with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No candidate, HLS or AV-sync acceptance."
}
]
},
"fixtureSha256": {
"test/browser/mediabunny.spec.js": "0d9af2fc2c47a9a78953e8c1fd69148ba553dc3b5e3f5ff0c141f537597ae9fe",
"test/mediabunny.test.js": "bc740a32d88cfb1291785d6ba20737d1e53276e9d43a3fc99ecc5423e5fa6a77",
"test/helpers/mediabunny.js": "3e25bd5274d58be4d0bb85cd5fb2a825848cae1bfdbfaf5c91177a0c2b03a87a"
},
"limitations": [
"No full CI rerun claimed for this checkpoint; MB-01 CI evidence belongs to its preceding completion commit.",
"WebM/HLS/Blob/Stream, seek and exact readiness/AV-sync/cleanup coverage still pending.",
"Windows WebKit missing APIs is an explicit negative control, not successful playback or a general Safari support verdict.",
"Candidate code has not yet been migrated or fixed."
]
}
@@ -0,0 +1,38 @@
# PKG-MB-02 首批媒体与销毁负例检查点
本任务进行中,不计为完成。MB-01完成提交为1cfd2e17,完整CI1161项属于该提交;
本检查点新增测试按影响范围执行22项Node(19基线+3历史缺陷)及6项浏览器,不将旧CI计为本步完整CI。
## 已复现的历史缺陷
两份真实npm主产物及冻结工作区均使用实际engine.load/destroy,替换performLoad为受控未完成请求。
destroy没有推进loadSeq,waiting/loadstart两个timer继续保留;晚到失败先发error并写networkState=3,
之后仍能执行waiting/loadstart。三个精确负例通过;它们证明旧问题存在,不代表候选已修复。
宿主为受控ArtPlayer事件对象,不能据此断言完整核心销毁后外部监听器仍收到事件。
## 真实浏览器结果
实际npm核心5.4.0 × proxy 1.0.0/1.2.0 × 三引擎,共6项:
- Chromium、Firefox的4项:真实MP4加载,播放时钟>0.15秒,320×180 Canvas,红/蓝采样像素及alpha,暂停状态通过。
- 当前Windows WebKit的2项:VideoDecoder、AudioDecoder、AudioContext和webkitAudioContext均不存在;
实际旧proxy出现code=4和构造器错误、未ready、尺寸0。测试验证此精确能力失败,没有skip,
但不能把这两项称为播放通过,不能扩大为所有Safari不支持。
- 所有项保留实际proxy字节哈希、核心资源manifest、浏览器版本/平台、媒体状态/事件、页面错误和请求诊断。
第一次探索等待ready时使用了Playwright默认30秒超时,保留6个截图与trace;正式测试已使用
expect.poll和明确的能力失败断言,不再靠超时或catch任意错误判通过。
正式报告与整个results目录归档于`.cache/mb02-first-results`;探索报告位于`.cache/mb02-native-probe-3ZcV99`。
## 复跑与剩余范围
```sh
node --test test/mediabunny.test.js refactor/scripts/mb-contract.test.mjs
yarn test:browser test/browser/mediabunny.spec.js
```
还缺:WebM、HLS、Blob/Stream、真实seek、无音轨/无视频/无轨道、超时和取消、并发选轨与
就绪事件次数、音画同步及资源释放。原生DPiP、长播放、新旧核心完整组合留MB-09。
MPL来源/通知仍MB-10,Canvas/Ambilight类型取舍仍未决定。
实现和用例影响由[机器记录](../baselines/mb-media-checkpoint.json)描述,当前PKG-MB-02保持doing。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。
状态:todo 117 / doing 7 / blocked 0 / done 98 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 116 / doing 8 / blocked 0 / done 98 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -330,7 +330,7 @@
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| PKG-MB-01 | artplayer-proxy-mediabunny<br>核对 shim 与媒体契约 | BASE-05 | 全部 option、art.mediabunny、canvas/shim 属性及事件表 | 公开暴露成员和实际发布依赖/资源范围明确;核实 third-party.json 所记录依赖 MPL-2.0 元数据与实际 bundle 的许可/通知范围,不从根 MIT 推断 | H | done |
| PKG-MB-02 | artplayer-proxy-mediabunny<br>建立真实媒体与事件基线 | PKG-MB-01, ENG-05, ENG-08 | MP4/WebM/HLS/Blob/Stream、seek/错误/轨道切换样本 | ready 事件顺序、音画同步、超时和释放可重跑 | H | todo |
| PKG-MB-02 | artplayer-proxy-mediabunny<br>建立真实媒体与事件基线 | PKG-MB-01, ENG-05, ENG-08 | MP4/WebM/HLS/Blob/Stream、seek/错误/轨道切换样本 | ready 事件顺序、音画同步、超时和释放可重跑 | H | doing |
| PKG-MB-03 | artplayer-proxy-mediabunny<br>整理 input 与加载取消 | PKG-MB-02, CORE-09 | input.js 源检测、输入资源、Range 与取消 | HLS 检测在 input 边界,旧来源类型和超时保持 | H | todo |
| PKG-MB-04 | artplayer-proxy-mediabunny<br>整理 VideoShim/EventTarget/Engine 协调 | PKG-MB-03, CORE-06, CORE-11 | shim 描述符、事件桥、加载状态和操作隔离 | 公开属性/同步异步返回与事件重入顺序保持 | H | todo |
| PKG-MB-05 | artplayer-proxy-mediabunny<br>整理视频解码、seek 和帧释放 | PKG-MB-04 | VideoEngine 调度、队列、晚帧处理和资源释放 | 暂停/seek/切源无旧帧覆盖,默认丢帧策略不变 | H | todo |
@@ -521,4 +521,5 @@
- PKG-CANVAS-03: [记录](changes/2026-09-12-PKG-CANVAS-03-lifecycle.md) [记录](baselines/canvas-lifecycle-validation.json) [记录](canvas-validation.md)
- PKG-CANVAS-04: [记录](changes/2026-09-12-PKG-CANVAS-04-types.md) [记录](baselines/canvas-types-validation.json) [记录](canvas-validation.md)
- PKG-MB-01: [记录](baselines/mb-release.json) [记录](baselines/mb-surface.json) [记录](baselines/mb-contract.md) [记录](baselines/mb-contract-validation.json) [记录](mb-validation.md) [记录](changes/2026-09-12-PKG-MB-01-contract.md)
- PKG-MB-02: [记录](changes/2026-09-12-PKG-MB-02-checkpoint.md) [记录](baselines/mb-media-checkpoint.json) [记录](mb-validation.md)
- PKG-FACTORY-01: [记录](baselines/factory-assignment-gaps.json) [记录](baselines/factory-compatibility-proposals.json) [记录](factory-compatibility-decision.md) [记录](changes/2026-09-12-PKG-FACTORY-01-decision.md)
+8
View File
@@ -1,5 +1,13 @@
# 进度与证据
## 当前检查点:PKG-MB-02 真实MP4与销毁负例
22项Node(含3个销毁后晚加载历史缺陷)通过;6项浏览器检查中,4项Chromium/Firefox
真实MP4像素/时钟/暂停成功,2项Windows WebKit明确能力失败对照。六项不是六次播放成功。
见[检查点](changes/2026-09-12-PKG-MB-02-checkpoint.md)和[证据](baselines/mb-media-checkpoint.json)。
MB-02仍doing,剩余WebM/HLS/Blob/Stream、seek、时序与释放;98 done、8 doing、116 todo。
本步没有修改生产源码。Canvas/Ambilight类型取舍仍待确认。
## 最新完成:PKG-MB-01 MediaBunny proxy 契约基线
冻结实际npm1.0.0/1.2.0共12成员、17个Git输入、3份产物接口表面与25条配置路径;
+2 -1
View File
@@ -169,5 +169,6 @@
| DPIP-RESOLVE-01 | resolved / 已复现 | Historical Document PiP exports hide declarations or infer an uncallable CommonJS namespace in modern resolution | PKG-DPIP-04, PKG-DPIP-06 |
| FACTORY-TYPE-01 | open / 已复现 | Canvas and Ambilight default self property narrows assignability of historical factory replacements | PKG-FACTORY-01 |
| MB-LICENSE-01 | open / 已复现 | Historical MediaBunny proxy bundles embed SDK code without MPL notice markers or separate license/source provenance | PKG-MB-10 |
| MB-LIFE-01 | open / 源码/产物事实 | MediaBunny load, track replacement, timeout, RAF and audio callbacks lack one shared terminal owner | PKG-MB-02, PKG-MB-03, PKG-MB-04, PKG-MB-05, PKG-MB-06, PKG-MB-07, PKG-MB-09 |
| MB-LIFE-01 | open / 已复现 | MediaBunny load, track replacement, timeout, RAF and audio callbacks lack one shared terminal owner | PKG-MB-02, PKG-MB-03, PKG-MB-04, PKG-MB-05, PKG-MB-06, PKG-MB-07, PKG-MB-09 |
| MB-TYPE-01 | open / 已复现 | MediaBunny proxy declares only Canvas while exposing a shim and changes CommonJS export generation between releases | PKG-MB-08, PKG-MB-10 |
| MB-CAP-01 | open / 已复现 | Current Windows WebKit lacks WebCodecs and Web Audio constructors needed by historical MediaBunny proxies | PKG-MB-02, PKG-MB-09 |
+21 -3
View File
@@ -3638,7 +3638,7 @@
{
"id": "MB-LIFE-01",
"title": "MediaBunny load, track replacement, timeout, RAF and audio callbacks lack one shared terminal owner",
"confirmation": "source-observed",
"confirmation": "reproduced",
"owners": [
"PKG-MB-02",
"PKG-MB-03",
@@ -3648,13 +3648,15 @@
"PKG-MB-07",
"PKG-MB-09"
],
"compatibleResolution": "Reproduce pending operations and destroy/source-switch races before migrating each owner; preserve public event and promise behavior while preventing stale resource use.",
"compatibleResolution": "Reproduce pending operations and destroy/source-switch races before migrating each owner; preserve public event and promise behavior while preventing stale resource use. MB-02 now reproduces late load rejection and uncancelled waiting/loadstart timers after destroy in all three historical implementations using a controlled host. Other interleavings and full-core native behavior are not yet verified.",
"closureCriteria": "Historical failing interleavings and candidate cancellation/cleanup pass with real media and event evidence, including long playback and track topology.",
"status": "open",
"evidence": [
"refactor/baselines/mb-release.json",
"refactor/baselines/mb-contract.md",
"refactor/changes/2026-09-12-PKG-MB-01-contract.md"
"refactor/changes/2026-09-12-PKG-MB-01-contract.md",
"refactor/changes/2026-09-12-PKG-MB-02-checkpoint.md",
"refactor/baselines/mb-media-checkpoint.json"
]
},
{
@@ -3673,6 +3675,22 @@
"refactor/baselines/mb-contract.md",
"refactor/changes/2026-09-12-PKG-MB-01-contract.md"
]
},
{
"id": "MB-CAP-01",
"title": "Current Windows WebKit lacks WebCodecs and Web Audio constructors needed by historical MediaBunny proxies",
"status": "open",
"confirmation": "reproduced",
"owners": [
"PKG-MB-02",
"PKG-MB-09"
],
"compatibleResolution": "Keep exact capability negative controls and validate usable browsers/codecs separately; improve unsupported-capability reporting during migration without fabricating decoder support.",
"closureCriteria": "Per-browser native media/support matrix and predictable unsupported-capability behavior verified, including target Safari evidence where support is claimed.",
"evidence": [
"refactor/changes/2026-09-12-PKG-MB-02-checkpoint.md",
"refactor/baselines/mb-media-checkpoint.json"
]
}
]
}
+6 -2
View File
@@ -3416,11 +3416,15 @@
"ENG-05",
"ENG-08"
],
"status": "todo",
"status": "doing",
"risk": "H",
"deliverable": "MP4/WebM/HLS/Blob/Stream、seek/错误/轨道切换样本",
"acceptance": "ready 事件顺序、音画同步、超时和释放可重跑",
"evidence": []
"evidence": [
"changes/2026-09-12-PKG-MB-02-checkpoint.md",
"baselines/mb-media-checkpoint.json",
"mb-validation.md"
]
},
{
"id": "PKG-MB-03",
+72
View File
@@ -0,0 +1,72 @@
import { hash } from '../../refactor/scripts/releases.mjs'
import { mbHistorical } from '../helpers/mediabunny.js'
import { expect, test } from './fixtures.js'
const implementations = (await mbHistorical()).filter(item => item.name.startsWith('published'))
for (const implementation of implementations) {
test(`MediaBunny ${implementation.name}: native MP4 playback or exact Windows WebKit capability failure`, async ({ page, browserName }, testInfo) => {
await page.goto('/test/player.html?core=published')
await page.addScriptTag({ content: `(() => {
const module = { exports: {} }; const exports = module.exports;
${implementation.code};
window.mbFactory = module.exports.default || module.exports;
})();` })
await page.evaluate(() => {
window.mbEvents = []
window.mbActions = []
window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, proxy: window.mbFactory() })
for (const name of window.Artplayer.config.events) {
window.art.on(`video:${name}`, () => window.mbEvents.push({ name, time: performance.now(), currentTime: window.art.video.currentTime, readyState: window.art.video.readyState }))
}
document.querySelector('#play').onclick = () => window.art.play().catch(error => window.mbActions.push({ name: error.name, message: error.message }))
})
const capabilities = await page.evaluate(() => ({
secure: isSecureContext,
VideoDecoder: typeof VideoDecoder,
AudioDecoder: typeof AudioDecoder,
AudioContext: typeof AudioContext,
webkitAudioContext: typeof window.webkitAudioContext,
coreVersion: window.Artplayer.version,
}))
let outcome = 'unverified'
try {
expect(capabilities.secure).toBe(true)
expect(capabilities.coreVersion).toBe('5.4.0')
if (capabilities.VideoDecoder === 'undefined' || (capabilities.AudioContext === 'undefined' && capabilities.webkitAudioContext === 'undefined')) {
expect(browserName).toBe('webkit')
expect(capabilities).toMatchObject({ VideoDecoder: 'undefined', AudioDecoder: 'undefined', AudioContext: 'undefined', webkitAudioContext: 'undefined' })
await expect.poll(() => page.evaluate(() => window.art.video.error?.code)).toBe(4)
expect(await page.evaluate(() => window.art.video.error.message)).toMatch(/not a constructor/)
expect(await page.evaluate(() => ({ ready: window.art.isReady, width: window.art.video.videoWidth, height: window.art.video.videoHeight }))).toEqual({ ready: false, width: 0, height: 0 })
outcome = 'verified-unsupported-capability-control'
}
else {
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await page.click('#play')
await expect.poll(() => page.evaluate(() => window.art.video.currentTime)).toBeGreaterThan(0.15)
const pixels = await page.evaluate(() => {
const canvas = window.art.video
const context = canvas.getContext('2d')
return { size: [canvas.width, canvas.height, canvas.videoWidth, canvas.videoHeight], corner: [...context.getImageData(0, 0, 1, 1).data], center: [...context.getImageData(160, 90, 1, 1).data] }
})
expect(pixels.size).toEqual([320, 180, 320, 180])
expect(pixels.corner[0]).toBeGreaterThan(200)
expect(pixels.center[2]).toBeGreaterThan(200)
expect([pixels.corner[3], pixels.center[3]]).toEqual([255, 255])
await page.evaluate(() => window.art.video.pause())
expect(await page.evaluate(() => window.art.video.paused)).toBe(true)
expect(await page.evaluate(() => window.mbActions)).toEqual([])
outcome = 'native-mp4-playback-observed'
}
}
finally {
const state = await page.evaluate(() => {
const canvas = window.art.video
return { events: window.mbEvents, actions: window.mbActions, ready: window.art.isReady, dimensions: [canvas.width, canvas.height, canvas.videoWidth, canvas.videoHeight], paused: canvas.paused, currentTime: canvas.currentTime, error: canvas.error, readyState: canvas.readyState, audioState: canvas.engine?.audio?.audioContext?.state }
})
await testInfo.attach('mediabunny-input', { contentType: 'application/json', body: JSON.stringify({ implementation: implementation.name, sha256: hash(implementation.code), capabilities, outcome, state, scope: 'Two actual published proxies with actual core5.4.0; native MP4 pixels/clock or explicitly unsupported Windows WebKit APIs. No candidate, HLS or AV-sync acceptance.' }) })
await page.evaluate(() => window.art.destroy(false))
}
})
}
+2 -2
View File
@@ -65,8 +65,8 @@ export function mbEnvironment({ code }, option = {}) {
URL,
console,
performance,
setTimeout,
clearTimeout,
setTimeout: option.setTimeout || setTimeout,
clearTimeout: option.clearTimeout || clearTimeout,
requestAnimationFrame(fn) {
const id = nextFrame++
frames.set(id, fn)
+43
View File
@@ -0,0 +1,43 @@
import assert from 'node:assert/strict'
// eslint-disable-next-line test/no-import-node-test -- Freeze historical lifecycle failures before candidate fixes.
import test from 'node:test'
import { mbEnvironment, mbHistorical } from './helpers/mediabunny.js'
for (const implementation of await mbHistorical()) {
test(`MediaBunny historical ${implementation.name}: destroy still permits late load errors and scheduled events`, async () => {
const timers = new Map()
let nextTimer = 0
const env = mbEnvironment(implementation, {
setTimeout(fn) {
const id = nextTimer++
timers.set(id, fn)
return id
},
clearTimeout(id) { timers.delete(id) },
})
const canvas = env.factory()(env.art)
const engine = canvas.engine
const events = []
for (const name of ['waiting', 'loadstart', 'error'])
canvas.events.addEventListener(name, () => events.push(name))
let reject
const pending = new Promise((resolve, fail) => {
reject = fail
})
engine.performLoad = () => pending
const loading = engine.load('controlled-input')
const generation = engine.loadSeq
env.art.emit('destroy')
assert.equal(engine.loadSeq, generation, 'Destroy does not invalidate the load generation')
assert.equal(env.art.mediabunny, undefined)
assert.equal(timers.size, 2, 'Waiting and loadstart timers still exist after destroy')
reject(new Error('late controlled rejection'))
await loading
for (const fn of timers.values()) fn()
timers.clear()
assert.deepEqual(events, ['error', 'waiting', 'loadstart'])
assert.equal(env.emitted.at(-1).name, 'video:error')
assert.deepEqual({ ...engine.error }, { code: 4, message: 'late controlled rejection' })
assert.equal(engine.networkState, 3)
})
}