diff --git a/refactor/baselines/mb-media-checkpoint.json b/refactor/baselines/mb-media-checkpoint.json
new file mode 100644
index 000000000..42581eabd
--- /dev/null
+++ b/refactor/baselines/mb-media-checkpoint.json
@@ -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."
+ ]
+}
diff --git a/refactor/changes/2026-09-12-PKG-MB-02-checkpoint.md b/refactor/changes/2026-09-12-PKG-MB-02-checkpoint.md
new file mode 100644
index 000000000..71104be8c
--- /dev/null
+++ b/refactor/changes/2026-09-12-PKG-MB-02-checkpoint.md
@@ -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。
diff --git a/refactor/plan.md b/refactor/plan.md
index efb4c31bb..d4bb1a00b 100644
--- a/refactor/plan.md
+++ b/refactor/plan.md
@@ -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
核对 shim 与媒体契约 | BASE-05 | 全部 option、art.mediabunny、canvas/shim 属性及事件表 | 公开暴露成员和实际发布依赖/资源范围明确;核实 third-party.json 所记录依赖 MPL-2.0 元数据与实际 bundle 的许可/通知范围,不从根 MIT 推断 | H | done |
-| PKG-MB-02 | artplayer-proxy-mediabunny
建立真实媒体与事件基线 | PKG-MB-01, ENG-05, ENG-08 | MP4/WebM/HLS/Blob/Stream、seek/错误/轨道切换样本 | ready 事件顺序、音画同步、超时和释放可重跑 | H | todo |
+| PKG-MB-02 | artplayer-proxy-mediabunny
建立真实媒体与事件基线 | PKG-MB-01, ENG-05, ENG-08 | MP4/WebM/HLS/Blob/Stream、seek/错误/轨道切换样本 | ready 事件顺序、音画同步、超时和释放可重跑 | H | doing |
| PKG-MB-03 | artplayer-proxy-mediabunny
整理 input 与加载取消 | PKG-MB-02, CORE-09 | input.js 源检测、输入资源、Range 与取消 | HLS 检测在 input 边界,旧来源类型和超时保持 | H | todo |
| PKG-MB-04 | artplayer-proxy-mediabunny
整理 VideoShim/EventTarget/Engine 协调 | PKG-MB-03, CORE-06, CORE-11 | shim 描述符、事件桥、加载状态和操作隔离 | 公开属性/同步异步返回与事件重入顺序保持 | H | todo |
| PKG-MB-05 | artplayer-proxy-mediabunny
整理视频解码、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)
diff --git a/refactor/progress.md b/refactor/progress.md
index 1b1b1c077..e59fe95e9 100644
--- a/refactor/progress.md
+++ b/refactor/progress.md
@@ -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条配置路径;
diff --git a/refactor/risk-table.md b/refactor/risk-table.md
index 3d2b4dd84..59fc2ac7f 100644
--- a/refactor/risk-table.md
+++ b/refactor/risk-table.md
@@ -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 |
diff --git a/refactor/risks.json b/refactor/risks.json
index effb957ba..846950375 100644
--- a/refactor/risks.json
+++ b/refactor/risks.json
@@ -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"
+ ]
}
]
}
diff --git a/refactor/tasks.json b/refactor/tasks.json
index 5a16794da..cf796d1ae 100644
--- a/refactor/tasks.json
+++ b/refactor/tasks.json
@@ -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",
diff --git a/test/browser/mediabunny.spec.js b/test/browser/mediabunny.spec.js
new file mode 100644
index 000000000..6fd0fdc59
--- /dev/null
+++ b/test/browser/mediabunny.spec.js
@@ -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))
+ }
+ })
+}
diff --git a/test/helpers/mediabunny.js b/test/helpers/mediabunny.js
index 9d164d553..1dd38d0d5 100644
--- a/test/helpers/mediabunny.js
+++ b/test/helpers/mediabunny.js
@@ -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)
diff --git a/test/mediabunny.test.js b/test/mediabunny.test.js
new file mode 100644
index 000000000..0cdde8907
--- /dev/null
+++ b/test/mediabunny.test.js
@@ -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)
+ })
+}