From 63c8938dd768539cba701a6ef171c17ddd877955 Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Sat, 12 Sep 2026 17:12:59 +0800 Subject: [PATCH] test(mediabunny): [PKG-MB-02] checkpoint native playback and late load failures --- refactor/baselines/mb-media-checkpoint.json | 713 ++++++++++++++++++ .../2026-09-12-PKG-MB-02-checkpoint.md | 38 + refactor/plan.md | 5 +- refactor/progress.md | 8 + refactor/risk-table.md | 3 +- refactor/risks.json | 24 +- refactor/tasks.json | 8 +- test/browser/mediabunny.spec.js | 72 ++ test/helpers/mediabunny.js | 4 +- test/mediabunny.test.js | 43 ++ 10 files changed, 908 insertions(+), 10 deletions(-) create mode 100644 refactor/baselines/mb-media-checkpoint.json create mode 100644 refactor/changes/2026-09-12-PKG-MB-02-checkpoint.md create mode 100644 test/browser/mediabunny.spec.js create mode 100644 test/mediabunny.test.js 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) + }) +}