From 1cfd2e17f319749ba5f2e73f4c54c21d858e9d7e Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Sat, 12 Sep 2026 17:05:55 +0800 Subject: [PATCH] test(mediabunny): [PKG-MB-01] freeze published proxy and dependency contracts --- .../baselines/mb-contract-validation.json | 65 +++ refactor/baselines/mb-contract.md | 113 ++++ refactor/baselines/mb-release.json | 186 +++++++ refactor/baselines/mb-surface.json | 494 ++++++++++++++++++ .../changes/2026-09-12-PKG-MB-01-contract.md | 25 + refactor/contract-index.md | 2 + refactor/contract-policy.json | 22 +- refactor/mb-validation.md | 35 ++ refactor/plan.md | 5 +- refactor/progress.md | 10 + refactor/risk-table.md | 3 + refactor/risks.json | 55 ++ refactor/scripts/contracts.test.mjs | 3 +- refactor/scripts/mb-contract.mjs | 45 ++ refactor/scripts/mb-contract.test.mjs | 208 ++++++++ refactor/tasks.json | 11 +- refactor/third-party.json | 2 +- test/helpers/mediabunny.js | 80 +++ 18 files changed, 1357 insertions(+), 7 deletions(-) create mode 100644 refactor/baselines/mb-contract-validation.json create mode 100644 refactor/baselines/mb-contract.md create mode 100644 refactor/baselines/mb-release.json create mode 100644 refactor/baselines/mb-surface.json create mode 100644 refactor/changes/2026-09-12-PKG-MB-01-contract.md create mode 100644 refactor/mb-validation.md create mode 100644 refactor/scripts/mb-contract.mjs create mode 100644 refactor/scripts/mb-contract.test.mjs create mode 100644 test/helpers/mediabunny.js diff --git a/refactor/baselines/mb-contract-validation.json b/refactor/baselines/mb-contract-validation.json new file mode 100644 index 000000000..546573457 --- /dev/null +++ b/refactor/baselines/mb-contract-validation.json @@ -0,0 +1,65 @@ +{ + "task": "PKG-MB-01", + "status": "done", + "scope": "Historical source/package/interface and dependency provenance, not real media or release acceptance", + "checks": [ + { + "command": "node --test refactor/scripts/mb-contract.test.mjs", + "result": "pass", + "tests": 19, + "historicalArchiveMembers": 12, + "frozenGitInputs": 17, + "declaredOptionPaths": 25, + "scope": "Historical package/interface, actual exports, type consumers and controlled media-method forwarding; not native media playback" + }, + { + "command": "focused eslint --no-fix", + "result": "pass", + "files": [ + "refactor/scripts/mb-contract.mjs", + "refactor/scripts/mb-contract.test.mjs", + "refactor/scripts/contracts.test.mjs", + "test/helpers/mediabunny.js" + ] + }, + { + "command": "yarn ci:check", + "result": "pass", + "tests": { + "unit": 1006, + "engineering": 14, + "baseline": 141, + "mainTotal": 1161, + "repeatedContractExecutions": 44 + }, + "strictProductionTypeScriptFiles": 290, + "log": "refactor/.cache/mb01-ci-verified.log" + }, + { + "command": "production/demo/artifact hashes compared with frozen source", + "result": "unchanged", + "files": 17 + } + ], + "limitations": [ + "Native MP4/WebM/HLS/Blob/Stream and AV synchronization remain MB-02/09.", + "Historical bundled SDK exact version unknown; current installed SDK is recorded separately.", + "License/source-notice packaging remains MB-10; no legal compliance claim.", + "Canvas/Ambilight factory decision remains pending and is not decided by this task." + ], + "parentCommit": "1f93bb9ba1f7329a348c1b0538473615e5278d7c", + "failedAttempts": [ + { + "log": "refactor/.cache/mb01-tests-first.log", + "resolution": "Exact historical readonly-write behavior and TypeScript-version diagnostic codes retained; VM exception checked by name." + }, + { + "log": "refactor/.cache/mb01-ci.log", + "resolution": "Generated contract-index.md was stale after adding two published versions; regenerated using contracts.mjs --write before the passing full CI." + }, + { + "log": "refactor/.cache/mb01-ci-final.log", + "resolution": "Contract model assertion still expected fifteen published versions; updated to seventeen and explicitly asserted the two MediaBunny release identities before the final passing CI." + } + ] +} diff --git a/refactor/baselines/mb-contract.md b/refactor/baselines/mb-contract.md new file mode 100644 index 000000000..62507a973 --- /dev/null +++ b/refactor/baselines/mb-contract.md @@ -0,0 +1,113 @@ +# MediaBunny proxy 历史契约 + +来源:[发布与 Git 哈希](mb-release.json)、[实际产物表面及全部配置声明](mb-surface.json)。 +PKG-MB-01 只冻结可观察接口、来源和依赖/资源范围;真实媒体时序、音画同步及长期释放由 +PKG-MB-02/09 验证。以下标为源码观察的顺序,不等于浏览器已经通过。 + +## 发布范围与入口 + +| 来源 | 运行时主入口 | 类型 | 扩展能力 | +| --- | --- | --- | --- | +| npm 1.0.0 | CommonJS namespace.default;原生 ESM default | 可选 Option → 同步 initializer → HTMLCanvasElement | 不创建 art.mediabunny,没有三个 HLS 方法 | +| npm 1.2.0 | CommonJS 直接工厂;原生 ESM default | 保持可选参数、Canvas 返回;增加 m3u8 | 创建 art.mediabunny,提供 HLS 状态与选轨 | +| 冻结工作区 | 与 1.2.0 同代,单独冻结字节 | 当前声明单独冻结 | 不是对历史 tarball 的替代 | + +两份实际 tarball 各6个文件:README、package.json、types 声明、main/legacy/ESM。 +没有独立 worker、WASM、source map 或许可证/通知成员;不据此推断 SDK 永远不会使用 +浏览器 worker 等能力。main/module/types/legacy、root/legacy exports 的原始值见 manifest。 +所有入口存在。1.0.0 关联 Git 核心5.3.1-beta.1,1.2.0关联5.4.1;这是提交关联, +不是已测支持范围,也不是已验证的 npm 核心组合。 + +## 配置 + +完整25条顶层/嵌套配置路径与回调结构已记录在 mb-surface.json.options。 + +| 配置 | 默认或语义 | 边界 | +| --- | --- | --- | +| loadTimeout | 0;只接受有限数字作为超时 | 非正值不启动超时;成功后 timeout timer 的所有权待02/03验证 | +| timeupdateInterval | 250 ms,使用 ?? | VideoEngine 发 timeupdate 的间隔 | +| avSyncTolerance | 0.12 s,使用 ?? | 视频相对音频时钟容差 | +| dropLateFrames | false,使用 ?? | 不因优化静默改变默认丢帧策略 | +| poster | 空字符串 | 异步 Image 绘制;后续 poster setter 改 option,不代表已同步重设 VideoEngine | +| source | 无 | 真值时优先,否则取 art.option.url;公开类型 string / Blob / ReadableStream | +| preflightRange | false | 字符串非HLS源 HEAD;无 accept-ranges 或 none 发 RangeNotSupported error 后返回false;网络异常警告后继续 | +| volume | 0.7,使用 ?? | 初始值不钳制;setter转数值并钳制0..1,同时取消muted | +| muted | false,布尔转换 | setter布尔转换,发volumechange | +| autoplay / loop | false | getter读option;setter为空实现,不能声称真正实现自动播放或循环 | +| crossOrigin | 空字符串 | setter为空实现,不据此推断 fetch 的 CORS/凭据策略 | +| m3u8 | 无,1.2.0新增 | 真值才安装loadedmetadata/restart监听 | +| m3u8.quality / audio | 各含control、setting、title、auto、getName | control/setting默认不显示,title默认Quality/Audio,auto默认Auto | + +工厂保留传入 option 对象的引用,没有克隆;同工厂初始化多个实例可共享该引用。 +quality.getName接收id/index/name/height/bitrate;audio.getName接收id/index/name/lang/language/bitrate。 +运行时状态还保留实际SDK track对象,当前公开配置回调声明未暴露track。 + +## Canvas 与 art.mediabunny + +公开Result严格为HTMLCanvasElement;不能直接重标成HTMLVideoElement。1.2.0的 +art.mediabunny指向VideoShim实例,destroy回调删除该字段并调用shim.destroy。 + +入口枚举 shim 实例自身和直接原型的全部属性,为 Canvas 中不存在的属性添加可配置、 +可枚举的getter/setter;方法每次读取重新bind(shim),因此`canvas.play !== canvas.play`。 +已有Canvas属性/方法优先保留;枚举到的原生方法再绑定原Canvas,不将原生事件接口替换成shim。 +其结果是`canvas.setAttribute('src', url)`不等于`canvas.src = url`,Canvas监听器也不会 +自动收到shim发给ArtPlayer的媒体事件。浏览器中的完整属性集合仍要在02验证。 + +| 分组 | 成员与行为 | +| --- | --- | +| 来源/时间 | src读写保存输入,真值触发engine.load;currentSrc同源;currentTime写入Number(t)或0并调用异步seek;duration只读 | +| 状态 | paused、playing、ended、seeking、readyState、networkState、error映射engine;playing由paused/ended计算 | +| 媒体范围 | buffered、played、seekable是合成范围;无有效duration或end<=0为空;非空start/end不校验下标,buffered不是实际下载进度 | +| 速度/音量 | playbackRate默认1,只拒绝NaN及<=0;volume setter钳制并取消静音;muted布尔转换;均转发engine并发对应事件 | +| 播放 | play返回engine的Promise;pause/load返回undefined;currentTime/src属性赋值不返回底层Promise | +| HLS(1.2.0) | getM3u8State返回Promise;switchM3u8Quality(id或'auto')、switchM3u8Audio(id或'auto')返回Promise | +| 尺寸 | videoWidth、videoHeight读取engine;Canvas width/height仍是原生成员 | +| 属性兼容 | poster可写修改option;autoplay/loop/crossOrigin只读option但setter不执行动作;controls=false、playsInline=true、preload='auto'、defaultMuted=false、defaultPlaybackRate=1,相应setter均无动作 | +| 能力/帧 | canPlayType无条件'maybe',不是codec支持检测;requestVideoFrameCallback只是单次RAF,presentedFrames=0;cancelVideoFrameCallback调用cancelAnimationFrame | +| 辅助方法 | setupEventForwarding、createTimeRanges、getBoundingClientRect、setAttribute、destroy;后两种DOM同名方法在Canvas侧仍原生优先 | +| 意外可观察字段 | art、canvas、option、events、engine、_src、_volume、_muted、_playbackRate同样被转发;完整own/prototype清单见JSON。重构不得仅因以下划线开头就默认可以删除 | + +readonly属性的Canvas转发setter存在,但底层shim无setter:实际1.0.0主产物静默忽略duration +写入,1.2.0/冻结工作区主产物抛TypeError。这是已发布代际差异,不能简单标为统一旧行为。 + +## 输入与 HLS(当前冻结源码观察) + +input.js集中检测`.m3u8`后缀,支持查询/片段且大小写不敏感;不嗅探无后缀URL内容。 +字符串/Blob/ReadableStream分别封装对应SDK Source,其余值原样传递,空值转null。 +Input选择HLS_FORMATS或ALL_FORMATS。先取主视频,再取其可配对主音频;无视频才取主音频。 +直播duration为Infinity;非直播优先metadata,null才computeDuration。SDK拒绝和无轨道路径待02测试。 + +HLS状态包含levels、audios、currentLevel、currentAudio、videoMode、audioMode;只有与当前视频 +可配对的音频进入状态列表。手动质量/音频通过SDK id选择,'auto'重新选主轨道;若旧搭档不能配对, +补选可配对轨道并将搭档mode置auto。不能把mode名称当作已证明持续ABR自适应。 + +控件/设置名为mediabunny-quality、mediabunny-audio;按实际current id标记default,名称去重, +质量按高度降序;音轨少于2隐藏。缺HLS状态或配置不启用时通过controls.remove/setting.remove清理。 +选中后await切轨,写notice,再await更新。并发更新/销毁后的晚回写留给07验证。 + +## 事件与异步边界(当前冻结源码观察) + +| 触发 | 可观察事件和状态 | +| --- | --- | +| shim事件 | 自有EventTarget构造Event,写detail;按Artplayer.constructor.config.events逐项桥接video:名称;非DOM dispatchEvent | +| load开始 | pause旧播放、dispose旧Input;networkState=2、readyState=0;两个零延迟timer分别发waiting、loadstart,不能假设始终早于异步metadata | +| 元数据齐备 | readyState=1;loadedmetadata → durationchange → progress;无轨道/异步callback的次数待02复现 | +| 加载完成 | readyState=4、networkState=1;loadeddata → canplay → canplaythrough → progress | +| load失败/超时 | error={code:4,message}、networkState=3,发error;外层和performLoad内层都有捕获,不据此推断所有Input已释放 | +| play/pause | play await audio后启动video,发play → playing;pause同步audio.pause/video.stop后发pause;ended重播先seek(0) | +| seek | seeking=true;seeking → waiting,暂停并等待audio/video.seek;seeking=false后seeked,必要时恢复play | +| 选轨 | 保留currentTime及是否播放,pause,seeking → waiting;重新load/seek后发metadata/duration/progress/data/canplay/canplaythrough/seeked,再恢复play | +| 帧/音频调度 | VideoEngine还可发timeupdate、ended、pause、canplay、playing、waiting;AudioEngine也可发canplay、playing。一次性/重复事件不能只从主Engine判断 | +| destroy | pause、disposeInput、audio.destroy、video.destroy;loadSeq未在destroy递增,未统一收回全部timer/RAF;准确竞态与释放证据待02 | + +## 依赖与许可范围 + +1.0.0声明mediabunny ^1.27.3,1.2.0/当前声明^1.43.1。当前Yarn锁和安装版本1.56.1, +其package.json为MPL-2.0,LICENSE为完整Mozilla Public License 2.0;冻结相应文件哈希。 +SDK清单另含两个@types依赖,不能把类型依赖自动计为播放产物中的JS。 + +实际历史ESM内含AudioBufferSink/CanvasSink实现,三个历史格式均未发现MPL/Source Code Form +通知标记;两份归档均无LICENSE/NOTICE成员,README和包清单仅标MIT。这个事实不构成 +完整法律判断,但候选发布必须补齐实际内嵌依赖的来源、许可/通知和可获得的对应源码说明。 +历史归档没有构建依赖版本证明,关联Git无锁文件,因此内嵌SDK精确版本记为unknown; +不能拿当前安装1.56.1冒充历史构建版本。由MB-LICENSE-01和PKG-MB-10继续关闭。 diff --git a/refactor/baselines/mb-release.json b/refactor/baselines/mb-release.json new file mode 100644 index 000000000..13498a675 --- /dev/null +++ b/refactor/baselines/mb-release.json @@ -0,0 +1,186 @@ +{ + "schemaVersion": 1, + "task": "PKG-MB-01", + "capturedAt": "2026-09-12T08:48:19.282Z", + "registry": { + "url": "https://registry.npmjs.org/artplayer-proxy-mediabunny", + "versions": [ + "1.0.0", + "1.2.0" + ], + "distTags": { + "latest": "1.2.0" + } + }, + "release": { + "name": "artplayer-proxy-mediabunny", + "version": "1.2.0", + "tarball": "https://registry.npmjs.org/artplayer-proxy-mediabunny/-/artplayer-proxy-mediabunny-1.2.0.tgz", + "integrity": "sha512-uSKkJFvd5Y2XxPPhwtPM5k9qq3xScbWnO/9fOfC8S+Liz8f8nT/iyA2eynU3maVcHZbJgA3PHZm0r6RRmSkefg==", + "sha256": "acb016166059183917a6de2e03a65ad77b09997fc9fb62513d751a24467ca76c", + "manifest": { + "name": "artplayer-proxy-mediabunny", + "version": "1.2.0", + "description": "mediabunny proxy for ArtPlayer", + "author": "Harvey Zhao ", + "license": "MIT", + "homepage": "https://artplayer.org", + "repository": { + "type": "git", + "url": "git+https://github.com/zhw2590582/ArtPlayer.git" + }, + "bugs": { + "url": "https://github.com/zhw2590582/ArtPlayer/issues" + }, + "keywords": [ + "html5", + "video", + "player" + ], + "exports": { + ".": { + "types": "./types/artplayer-proxy-mediabunny.d.ts", + "import": "./dist/artplayer-proxy-mediabunny.mjs", + "require": "./dist/artplayer-proxy-mediabunny.js" + }, + "./legacy": { + "types": "./types/artplayer-proxy-mediabunny.d.ts", + "import": "./dist/artplayer-proxy-mediabunny.legacy.js", + "require": "./dist/artplayer-proxy-mediabunny.legacy.js" + } + }, + "main": "./dist/artplayer-proxy-mediabunny.js", + "module": "./dist/artplayer-proxy-mediabunny.mjs", + "types": "./types/artplayer-proxy-mediabunny.d.ts", + "legacy": "./dist/artplayer-proxy-mediabunny.legacy.js", + "browserslist": "last 1 Chrome version", + "dependencies": { + "mediabunny": "^1.43.1" + } + }, + "files": { + "package/README.md": "32377da36a37d7ddc2b85031645b3b2ee49fea7185c3cef1d0b674b52367d58f", + "package/dist/artplayer-proxy-mediabunny.js": "7d088c0abe2f3c1e2857cb0547405ce1e5588630f28e484a64211fc675322bbd", + "package/dist/artplayer-proxy-mediabunny.legacy.js": "327da5f1be221af6887efb4175c1b92e4e0c28838cae8107acca652a76bddccb", + "package/dist/artplayer-proxy-mediabunny.mjs": "42f85d84456d1710a8b71640b1cb4f51203052692b84da9f401077f331dc057b", + "package/package.json": "96b09ce19ea7914485aa9780329a0cfc43b18e38232ea1dd92d2e6dc27310055", + "package/types/artplayer-proxy-mediabunny.d.ts": "af2083224d0760752d83eb2ac86b1dea5586455b81d47e4ba70f9f6a874a75d8" + }, + "gitHead": "fc90d4a0df8fe4efaf2172ca7b04dd059eed8144", + "publishedAt": "2026-05-04T05:27:32.743Z", + "historicalCore": { + "commit": "fc90d4a0df8fe4efaf2172ca7b04dd059eed8144", + "file": "packages/artplayer/package.json", + "version": "5.4.1", + "sha256LF": "06cbc0de3f944dfc215ea1c4e7c3376e6352ee5d6f1f459828f45025c62f57f0", + "scope": "Git association only, not a published core support range or proof of proxy capability" + }, + "bundledDependencyVersion": null, + "bundledDependencyVersionEvidence": "Published tarball does not provide dependency build provenance; Git tree contains no lockfile. The manifest range does not identify the embedded SDK version." + }, + "previous": [ + { + "name": "artplayer-proxy-mediabunny", + "version": "1.0.0", + "tarball": "https://registry.npmjs.org/artplayer-proxy-mediabunny/-/artplayer-proxy-mediabunny-1.0.0.tgz", + "integrity": "sha512-i9ot2ZbGqviO+BdcJJS0BqgfBmXBOCt2EuRJJBrp0QArCbXkjzOxRutSn4kIUfTWnLcazgego4on1HPkt5ZE9w==", + "sha256": "d62f3ad75d07abbc8ca597846e213f34abd15b3742f7e87f294800fdef2e8962", + "manifest": { + "name": "artplayer-proxy-mediabunny", + "version": "1.0.0", + "description": "mediabunny proxy for ArtPlayer", + "author": "Harvey Zack ", + "license": "MIT", + "homepage": "https://artplayer.org", + "repository": { + "type": "git", + "url": "git+https://github.com/zhw2590582/ArtPlayer.git" + }, + "bugs": { + "url": "https://github.com/zhw2590582/ArtPlayer/issues" + }, + "keywords": [ + "html5", + "video", + "player" + ], + "exports": { + ".": { + "types": "./types/artplayer-proxy-mediabunny.d.ts", + "import": "./dist/artplayer-proxy-mediabunny.mjs", + "require": "./dist/artplayer-proxy-mediabunny.js" + }, + "./legacy": { + "types": "./types/artplayer-proxy-mediabunny.d.ts", + "import": "./dist/artplayer-proxy-mediabunny.legacy.js", + "require": "./dist/artplayer-proxy-mediabunny.legacy.js" + } + }, + "main": "./dist/artplayer-proxy-mediabunny.js", + "module": "./dist/artplayer-proxy-mediabunny.mjs", + "types": "./types/artplayer-proxy-mediabunny.d.ts", + "legacy": "./dist/artplayer-proxy-mediabunny.legacy.js", + "browserslist": "last 1 Chrome version", + "dependencies": { + "mediabunny": "^1.27.3" + } + }, + "files": { + "package/README.md": "5cab1501e0a724fa43bd54ce4aa61eb6c43b01411484c370a6a2054accdd5145", + "package/dist/artplayer-proxy-mediabunny.js": "7331465d8c7a6575505b9d3fd45986c5d3d9cb3839b2620ad5a52c581199562b", + "package/dist/artplayer-proxy-mediabunny.legacy.js": "370b3340c2bc14912ea1f64de9e9d34aa917e6620f027733dd23e7dc0b6d4d05", + "package/dist/artplayer-proxy-mediabunny.mjs": "548adec34185ec1619bbc602c06cc8a8e0ed3c4752494f1f623aa0ec1cf2c2d7", + "package/package.json": "7a05c5110ee7468406ca2bdcff676f8c8c9837629310e008a746ab29523745b3", + "package/types/artplayer-proxy-mediabunny.d.ts": "d88bb80cd47603e35ca12bbf3311a7cd678b906d9cd977b73e89d7cb5a7d3075" + }, + "gitHead": "4a0fbda3c4cd2a1b223f3172697ac98a14ddb7b6", + "publishedAt": "2026-01-01T11:49:03.448Z", + "historicalCore": { + "commit": "4a0fbda3c4cd2a1b223f3172697ac98a14ddb7b6", + "file": "packages/artplayer/package.json", + "version": "5.3.1-beta.1", + "sha256LF": "e1a0a390ec10cd8c1c12006ac6515414ae98a964300dd3e9c288377def948497", + "scope": "Git association only, not a published core support range or proof of proxy capability" + }, + "bundledDependencyVersion": null, + "bundledDependencyVersionEvidence": "Published tarball does not provide dependency build provenance; Git tree contains no lockfile. The manifest range does not identify the embedded SDK version." + } + ], + "sourceCommit": "1f93bb9ba1f7329a348c1b0538473615e5278d7c", + "source": { + "packages/artplayer-proxy-mediabunny/src/AudioEngine.js": "910d6831372e4895d55dec78010f281a199bc88f99735630a5a3b621f192ac35", + "packages/artplayer-proxy-mediabunny/src/EventTarget.js": "c1854510af5a37301b4bd3b1fbcc28f7e0f105f50cb67324c5c05a76f2d054fb", + "packages/artplayer-proxy-mediabunny/src/index.js": "4e1be8937908ff6e6ef8a4e863e22b4468312cf31d66ac299322e475757773bb", + "packages/artplayer-proxy-mediabunny/src/input.js": "4341c27a8d8ebc77334ac96fc8cec11fc28e369e115eb690c6aeb0a352863d81", + "packages/artplayer-proxy-mediabunny/src/m3u8.js": "4639f09e0c743dc2b24078f48ca1e58eaaedaa3f5e6b863583cbd4bc4a7d3142", + "packages/artplayer-proxy-mediabunny/src/MediaBunnyEngine.js": "3f847bd1e8dfe5334e55def87862af7412af0204691c275293758cff95950189", + "packages/artplayer-proxy-mediabunny/src/VideoEngine.js": "6b73dd5867a5640b68f801b397ace36bb6289e50cca176bfc9288d71f67e6e0b", + "packages/artplayer-proxy-mediabunny/src/VideoShim.js": "f01c954e8a98fb70138dded78b99eb2765f5cd1e8a98dafbb35ffbf8f7741dbb", + "packages/artplayer-proxy-mediabunny/types/artplayer-proxy-mediabunny.d.ts": "af2083224d0760752d83eb2ac86b1dea5586455b81d47e4ba70f9f6a874a75d8", + "packages/artplayer-proxy-mediabunny/package.json": "96b09ce19ea7914485aa9780329a0cfc43b18e38232ea1dd92d2e6dc27310055", + "packages/artplayer-proxy-mediabunny/README.md": "32377da36a37d7ddc2b85031645b3b2ee49fea7185c3cef1d0b674b52367d58f", + "packages/artplayer-proxy-mediabunny/dist/artplayer-proxy-mediabunny.js": "7d088c0abe2f3c1e2857cb0547405ce1e5588630f28e484a64211fc675322bbd", + "packages/artplayer-proxy-mediabunny/dist/artplayer-proxy-mediabunny.legacy.js": "327da5f1be221af6887efb4175c1b92e4e0c28838cae8107acca652a76bddccb", + "packages/artplayer-proxy-mediabunny/dist/artplayer-proxy-mediabunny.mjs": "42f85d84456d1710a8b71640b1cb4f51203052692b84da9f401077f331dc057b", + "docs/assets/example/mediabunny.js": "dad4cecb76b2e5ce9164a4da715793911123e8fd2bc4b93f4bcc86a7053bad7d", + "packages/artplayer-plugin-hls-control/src/audio.svg": "dbee5ce7487c9f8ec927e5286188cf9439345bbe947b6c06c32c07193e01e0be", + "packages/artplayer-plugin-hls-control/src/quality.svg": "4666a4c01764422f9008e442f5a889ff9691bb0386083d673943a0718f4d680c" + }, + "dependency": { + "name": "mediabunny", + "version": "1.56.1", + "declaredRange": "^1.43.1", + "manifestLicense": "MPL-2.0", + "dependencies": { + "@types/dom-mediacapture-transform": "^0.1.11", + "@types/dom-webcodecs": "0.1.13" + }, + "files": { + "package.json": "809d14362055f6db3c17dd6dd43d7b913fc4daf4ac4f4179f29128eec866d868", + "LICENSE": "3f3d9e0024b1921b067d6f7f88deb4a60cbe7a78e76c64e3f1d7fc3b779b9d04", + "src/index.ts": "ba7672de9670183cd7c9965b844cbc4e134b9daa7aded6e0cf1efa0a5d736d78", + "dist/modules/src/index.js": "9236aa229772332a4f5ed04e453d8794a84311468012a9dd0de78e35301d39c0" + }, + "scope": "Installed and Yarn-locked dependency provenance for the upcoming candidate build only, never claimed as the SDK version embedded in historical bundles" + } +} diff --git a/refactor/baselines/mb-surface.json b/refactor/baselines/mb-surface.json new file mode 100644 index 000000000..8da4931aa --- /dev/null +++ b/refactor/baselines/mb-surface.json @@ -0,0 +1,494 @@ +{ + "task": "PKG-MB-01", + "scope": "Actual historical main bundles instantiated without a media source against a controlled canvas and ArtPlayer event host. Canvas native members win; full browser descriptor variations and native media events are MB-02/09, not this snapshot.", + "records": [ + { + "name": "published-1.2.0", + "exported": "function", + "artMediabunny": true, + "canvasKeys": [ + "width", + "height", + "style", + "getContext", + "addEventListener", + "removeEventListener", + "getBoundingClientRect", + "setAttribute", + "art", + "canvas", + "option", + "events", + "engine", + "_src", + "_volume", + "_muted", + "_playbackRate", + "setupEventForwarding", + "src", + "currentSrc", + "currentTime", + "duration", + "buffered", + "played", + "seekable", + "createTimeRanges", + "paused", + "playing", + "ended", + "seeking", + "readyState", + "networkState", + "error", + "playbackRate", + "volume", + "muted", + "play", + "pause", + "load", + "getM3u8State", + "switchM3u8Quality", + "switchM3u8Audio", + "videoWidth", + "videoHeight", + "poster", + "autoplay", + "loop", + "controls", + "playsInline", + "crossOrigin", + "preload", + "defaultMuted", + "defaultPlaybackRate", + "canPlayType", + "requestVideoFrameCallback", + "cancelVideoFrameCallback", + "destroy" + ], + "shimOwn": [ + "art", + "canvas", + "option", + "events", + "engine", + "_src", + "_volume", + "_muted", + "_playbackRate" + ], + "shimPrototype": [ + "constructor", + "setupEventForwarding", + "addEventListener", + "removeEventListener", + "src", + "currentSrc", + "currentTime", + "duration", + "buffered", + "played", + "seekable", + "createTimeRanges", + "paused", + "playing", + "ended", + "seeking", + "readyState", + "networkState", + "error", + "playbackRate", + "volume", + "muted", + "play", + "pause", + "load", + "getM3u8State", + "switchM3u8Quality", + "switchM3u8Audio", + "videoWidth", + "videoHeight", + "poster", + "autoplay", + "loop", + "controls", + "playsInline", + "crossOrigin", + "preload", + "defaultMuted", + "defaultPlaybackRate", + "canPlayType", + "getBoundingClientRect", + "requestVideoFrameCallback", + "cancelVideoFrameCallback", + "setAttribute", + "destroy" + ], + "defaults": { + "volume": 0.7, + "muted": false, + "playbackRate": 1, + "autoplay": false, + "loop": false, + "poster": "", + "crossOrigin": "", + "src": null, + "paused": true, + "readyState": 0, + "networkState": 0, + "preload": "auto", + "playsInline": true, + "controls": false, + "defaultMuted": false, + "defaultPlaybackRate": 1 + } + }, + { + "name": "published-1.0.0", + "exported": "object", + "artMediabunny": false, + "canvasKeys": [ + "width", + "height", + "style", + "getContext", + "addEventListener", + "removeEventListener", + "getBoundingClientRect", + "setAttribute", + "art", + "canvas", + "option", + "events", + "engine", + "_src", + "_volume", + "_muted", + "_playbackRate", + "setupEventForwarding", + "src", + "currentSrc", + "currentTime", + "duration", + "buffered", + "played", + "seekable", + "createTimeRanges", + "paused", + "playing", + "ended", + "seeking", + "readyState", + "networkState", + "error", + "playbackRate", + "volume", + "muted", + "play", + "pause", + "load", + "videoWidth", + "videoHeight", + "poster", + "autoplay", + "loop", + "controls", + "playsInline", + "crossOrigin", + "preload", + "defaultMuted", + "defaultPlaybackRate", + "canPlayType", + "requestVideoFrameCallback", + "cancelVideoFrameCallback", + "destroy" + ], + "shimOwn": null, + "shimPrototype": null, + "defaults": { + "volume": 0.7, + "muted": false, + "playbackRate": 1, + "autoplay": false, + "loop": false, + "poster": "", + "crossOrigin": "", + "src": null, + "paused": true, + "readyState": 0, + "networkState": 0, + "preload": "auto", + "playsInline": true, + "controls": false, + "defaultMuted": false, + "defaultPlaybackRate": 1 + } + }, + { + "name": "frozen-workspace", + "exported": "function", + "artMediabunny": true, + "canvasKeys": [ + "width", + "height", + "style", + "getContext", + "addEventListener", + "removeEventListener", + "getBoundingClientRect", + "setAttribute", + "art", + "canvas", + "option", + "events", + "engine", + "_src", + "_volume", + "_muted", + "_playbackRate", + "setupEventForwarding", + "src", + "currentSrc", + "currentTime", + "duration", + "buffered", + "played", + "seekable", + "createTimeRanges", + "paused", + "playing", + "ended", + "seeking", + "readyState", + "networkState", + "error", + "playbackRate", + "volume", + "muted", + "play", + "pause", + "load", + "getM3u8State", + "switchM3u8Quality", + "switchM3u8Audio", + "videoWidth", + "videoHeight", + "poster", + "autoplay", + "loop", + "controls", + "playsInline", + "crossOrigin", + "preload", + "defaultMuted", + "defaultPlaybackRate", + "canPlayType", + "requestVideoFrameCallback", + "cancelVideoFrameCallback", + "destroy" + ], + "shimOwn": [ + "art", + "canvas", + "option", + "events", + "engine", + "_src", + "_volume", + "_muted", + "_playbackRate" + ], + "shimPrototype": [ + "constructor", + "setupEventForwarding", + "addEventListener", + "removeEventListener", + "src", + "currentSrc", + "currentTime", + "duration", + "buffered", + "played", + "seekable", + "createTimeRanges", + "paused", + "playing", + "ended", + "seeking", + "readyState", + "networkState", + "error", + "playbackRate", + "volume", + "muted", + "play", + "pause", + "load", + "getM3u8State", + "switchM3u8Quality", + "switchM3u8Audio", + "videoWidth", + "videoHeight", + "poster", + "autoplay", + "loop", + "controls", + "playsInline", + "crossOrigin", + "preload", + "defaultMuted", + "defaultPlaybackRate", + "canPlayType", + "getBoundingClientRect", + "requestVideoFrameCallback", + "cancelVideoFrameCallback", + "setAttribute", + "destroy" + ], + "defaults": { + "volume": 0.7, + "muted": false, + "playbackRate": 1, + "autoplay": false, + "loop": false, + "poster": "", + "crossOrigin": "", + "src": null, + "paused": true, + "readyState": 0, + "networkState": 0, + "preload": "auto", + "playsInline": true, + "controls": false, + "defaultMuted": false, + "defaultPlaybackRate": 1 + } + } + ], + "options": [ + { + "name": "m3u8", + "optional": true, + "type": "{\n quality?: {\n control?: boolean\n setting?: boolean\n title?: string\n auto?: string\n getName?: (level: {\n id: number\n index: number\n name: null | string\n height: number\n bitrate: number\n }) => string\n }\n audio?: {\n control?: boolean\n setting?: boolean\n title?: string\n auto?: string\n getName?: (track: {\n id: number\n index: number\n name: null | string\n lang: string\n language: string\n bitrate: number\n }) => string\n }\n }" + }, + { + "name": "m3u8.quality", + "optional": true, + "type": "{\n control?: boolean\n setting?: boolean\n title?: string\n auto?: string\n getName?: (level: {\n id: number\n index: number\n name: null | string\n height: number\n bitrate: number\n }) => string\n }" + }, + { + "name": "m3u8.quality.control", + "optional": true, + "type": "boolean" + }, + { + "name": "m3u8.quality.setting", + "optional": true, + "type": "boolean" + }, + { + "name": "m3u8.quality.title", + "optional": true, + "type": "string" + }, + { + "name": "m3u8.quality.auto", + "optional": true, + "type": "string" + }, + { + "name": "m3u8.quality.getName", + "optional": true, + "type": "(level: {\n id: number\n index: number\n name: null | string\n height: number\n bitrate: number\n }) => string" + }, + { + "name": "m3u8.audio", + "optional": true, + "type": "{\n control?: boolean\n setting?: boolean\n title?: string\n auto?: string\n getName?: (track: {\n id: number\n index: number\n name: null | string\n lang: string\n language: string\n bitrate: number\n }) => string\n }" + }, + { + "name": "m3u8.audio.control", + "optional": true, + "type": "boolean" + }, + { + "name": "m3u8.audio.setting", + "optional": true, + "type": "boolean" + }, + { + "name": "m3u8.audio.title", + "optional": true, + "type": "string" + }, + { + "name": "m3u8.audio.auto", + "optional": true, + "type": "string" + }, + { + "name": "m3u8.audio.getName", + "optional": true, + "type": "(track: {\n id: number\n index: number\n name: null | string\n lang: string\n language: string\n bitrate: number\n }) => string" + }, + { + "name": "loadTimeout", + "optional": true, + "type": "number" + }, + { + "name": "timeupdateInterval", + "optional": true, + "type": "number" + }, + { + "name": "avSyncTolerance", + "optional": true, + "type": "number" + }, + { + "name": "dropLateFrames", + "optional": true, + "type": "boolean" + }, + { + "name": "poster", + "optional": true, + "type": "string" + }, + { + "name": "source", + "optional": true, + "type": "string | Blob | ReadableStream" + }, + { + "name": "preflightRange", + "optional": true, + "type": "boolean" + }, + { + "name": "volume", + "optional": true, + "type": "number" + }, + { + "name": "muted", + "optional": true, + "type": "boolean" + }, + { + "name": "autoplay", + "optional": true, + "type": "boolean" + }, + { + "name": "loop", + "optional": true, + "type": "boolean" + }, + { + "name": "crossOrigin", + "optional": true, + "type": "string" + } + ] +} diff --git a/refactor/changes/2026-09-12-PKG-MB-01-contract.md b/refactor/changes/2026-09-12-PKG-MB-01-contract.md new file mode 100644 index 000000000..c82e8b927 --- /dev/null +++ b/refactor/changes/2026-09-12-PKG-MB-01-contract.md @@ -0,0 +1,25 @@ +# PKG-MB-01 MediaBunny proxy 契约冻结 + +核实真实发布只有1.0.0、1.2.0,两包共12归档成员;冻结17个Git输入(8源码、声明/清单/README、 +三产物、demo和两份共享SVG),记录归档完整性及逐文件哈希。Git关联核心仅作来源关联,不当支持范围。 + +冻结3份实际主产物的可观察表面、25条声明配置路径,补充[完整契约](../baselines/mb-contract.md) +和[后续验证入口](../mb-validation.md)。1.0.0没有art.mediabunny/HLS方法;1.2.0新增这些能力。 +原生Canvas成员优先、动态bind、伪TimeRanges、RAF式frame callback、配置引用和setter的 +实际限制均有正常接口测试或明确源码观察标记。真实解码、时序、音画同步和清理仍由02以后验证。 + +19项专项测试覆盖归档/SDK来源、许可文件事实、两代编译器与完整工厂替换、实际main/legacy/global/ +原生ESM、三份主产物的表面、setter、事件桥、转发和RAF。首次观察保留在mb01-tests-first.log: +旧TS未知字段报2345而新TS报2353;VM错误需按name匹配;1.0.0只读写入静默而1.2.0抛TypeError。 +按版本建立精确对照,没有为通过测试改动历史实现。 + +历史包声明^1.27.3/^1.43.1;当前Yarn安装1.56.1有MPL-2.0清单和完整LICENSE。实际历史bundle包含 +SDK实现但无MPL通知标记/许可证成员,且不能确认历史内嵌SDK精确版本。记录MB-LICENSE-01, +不把根MIT或当前安装版本当作历史产物许可证明;分发阶段须补实际来源/通知并验证归档。 + +本步骤未修改生产源码、声明或产物,不需要重建/浏览器验收。最终检查和完成状态以 +[验证记录](../baselines/mb-contract-validation.json)及独立完成提交为准。 + +最终完整CI通过1161项(1006单元、14工程、141基线),另44重复契约;290生产TS严格检查。 +第一次CI发现新增发布版本后的生成契约索引过期,使用contracts.mjs --write更新后全量重跑通过。 +最终17个生产/demo/产物文件哈希与冻结输入一致,未以重建掩盖历史行为。 diff --git a/refactor/contract-index.md b/refactor/contract-index.md index e99cc26bf..8e90a28c6 100644 --- a/refactor/contract-index.md +++ b/refactor/contract-index.md @@ -324,3 +324,5 @@ - PUB-artplayer-plugin-document-pip-1.0.2: artplayer-plugin-document-pip@1.0.2 (published-baseline);[来源](../refactor/baselines/dpip-release.json) JSON pointer `/previous/0`。 - PUB-artplayer-plugin-document-pip-1.0.1: artplayer-plugin-document-pip@1.0.1 (published-baseline);[来源](../refactor/baselines/dpip-release.json) JSON pointer `/previous/1`。 - PUB-artplayer-plugin-document-pip-1.0.0: artplayer-plugin-document-pip@1.0.0 (published-baseline);[来源](../refactor/baselines/dpip-release.json) JSON pointer `/previous/2`。 +- PUB-artplayer-proxy-mediabunny-1.2.0: artplayer-proxy-mediabunny@1.2.0 (published-baseline);[来源](../refactor/baselines/mb-release.json) JSON pointer `/release`。 +- PUB-artplayer-proxy-mediabunny-1.0.0: artplayer-proxy-mediabunny@1.0.0 (published-baseline);[来源](../refactor/baselines/mb-release.json) JSON pointer `/previous/0`。 diff --git a/refactor/contract-policy.json b/refactor/contract-policy.json index 862fe7c27..6efac4e65 100644 --- a/refactor/contract-policy.json +++ b/refactor/contract-policy.json @@ -479,7 +479,9 @@ "API-12": "PKG-MB-02" }, "versionIds": [ - "WS-artplayer-proxy-mediabunny" + "WS-artplayer-proxy-mediabunny", + "PUB-artplayer-proxy-mediabunny-1.2.0", + "PUB-artplayer-proxy-mediabunny-1.0.0" ], "supportWindow": "not-inferred-from-baseline", "scopeReview": "Each public member, callback, option, event and capability requires package contract review; family ownership alone is not exhaustive coverage" @@ -862,6 +864,24 @@ "file": "refactor/baselines/dpip-release.json", "pointer": "/previous/2", "archiveSha256": "3b0164391171c817ab511f740205732e65db0e8f1d65d63f0d24d8eef4251573" + }, + { + "id": "PUB-artplayer-proxy-mediabunny-1.2.0", + "package": "artplayer-proxy-mediabunny", + "kind": "published-baseline", + "version": "1.2.0", + "file": "refactor/baselines/mb-release.json", + "pointer": "/release", + "archiveSha256": "acb016166059183917a6de2e03a65ad77b09997fc9fb62513d751a24467ca76c" + }, + { + "id": "PUB-artplayer-proxy-mediabunny-1.0.0", + "package": "artplayer-proxy-mediabunny", + "kind": "published-baseline", + "version": "1.0.0", + "file": "refactor/baselines/mb-release.json", + "pointer": "/previous/0", + "archiveSha256": "d62f3ad75d07abbc8ca597846e213f34abd15b3742f7e87f294800fdef2e8962" } ], "cases": [ diff --git a/refactor/mb-validation.md b/refactor/mb-validation.md new file mode 100644 index 000000000..2d52842e3 --- /dev/null +++ b/refactor/mb-validation.md @@ -0,0 +1,35 @@ +# MediaBunny proxy 验证入口 + +开始改动前读取[冻结契约](baselines/mb-contract.md)、[接口快照](baselines/mb-surface.json)、 +[归档/源码哈希](baselines/mb-release.json)。当前阶段为PKG-MB-01;不得把接口基线称为真实媒体验收。 + +## 可重复命令 + +使用仓库固定Node 24.21.0、Yarn 1.22.22: + +```sh +node refactor/scripts/mb-contract.mjs +node --test refactor/scripts/mb-contract.test.mjs +yarn ci:check +``` + +归档缺失时仅从固定npm URL下载,并验证SHA-512/SHA-256及逐文件哈希。冻结工作区从明确Git +提交读取,后续迁移不能修改冻结来源。已安装SDK校验来自Yarn锁及原始文件,仅描述候选工具链。 + +历史测试使用实际main/legacy/ESM;受控Canvas与ArtPlayer事件宿主只测公开接口、方法绑定、 +属性写入、默认值和事件桥。空媒体源不会构造真实AudioContext或解码,SDK存在不等于SDK播放已验收。 +play/load/seek转发测试刻意截获engine方法;不得宣传为真实播放测试。 + +## 后续顺序 + +1. MB-02建立真实MP4/WebM/HLS/Blob/Stream和失败样本;三浏览器能力分别记录。 + 复现pending load/destroy、超时后释放、无轨道、并发seek/选轨、metadata重复与晚帧/音频回调。 +2. MB-03输入/取消,MB-04事件/状态协调,MB-05帧调度,MB-06音频时钟,MB-07配对轨道/控件。 + 按已批准计划分步迁移,不等到MB-08才一次性堆积全部源码修改。 +3. MB-08完成严格TS和公开声明兼容矩阵;保留HTMLCanvasElement精确Result、可选工厂和全工厂 + 替换用例,避免重演FACTORY-TYPE-01。1.0.0 namespace.default与1.2.0直接导出分别验证。 +4. MB-09新旧核心、AV sync、长播放、原生DPiP和浏览器组合;MB-10实际打包安装、许可来源/通知和8082示例。 + +Chrome连接不可用时可使用内置浏览器;CLI自动化也按现有Playwright三引擎流程执行。 +保留report.json及整个results目录。受控输入不能替代真实解码,iframe不能替代原生Document PiP。 +VAST脚本VPN豁免不适用于本包。 diff --git a/refactor/plan.md b/refactor/plan.md index 564d5b548..efb4c31bb 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -4,7 +4,7 @@ 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 222 项,范围 22 个包及工作区/示例。 -状态:todo 118 / doing 7 / blocked 0 / done 97 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 117 / doing 7 / blocked 0 / done 98 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -329,7 +329,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 | todo | +| 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-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 | @@ -520,4 +520,5 @@ - PKG-CANVAS-02: [记录](changes/2026-09-12-PKG-CANVAS-02-tests.md) [记录](baselines/canvas-behavior-validation.json) [记录](canvas-validation.md) - PKG-CANVAS-03: [记录](changes/2026-09-12-PKG-CANVAS-03-lifecycle.md) [记录](baselines/canvas-lifecycle-validation.json) [记录](canvas-validation.md) - 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-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 5437520f6..1b1b1c077 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,15 @@ # 进度与证据 +## 最新完成:PKG-MB-01 MediaBunny proxy 契约基线 + +冻结实际npm1.0.0/1.2.0共12成员、17个Git输入、3份产物接口表面与25条配置路径; +19项专项通过。完整CI1161项(1006单元+14工程+141基线)通过,另44重复契约, +290生产TS文件保持严格检查;生产/demo/产物哈希均未改变。 +见[步骤记录](changes/2026-09-12-PKG-MB-01-contract.md)、[完整契约](baselines/mb-contract.md) +和[验证证据](baselines/mb-contract-validation.json)。历史内嵌SDK版本未知,与当前锁定1.56.1 +分开记录;MPL通知/来源缺口由MB-LICENSE-01跟进。真实媒体、取消、音画同步等仍待02以后。 +当前222项:98 done、7 doing、117 todo;Canvas/Ambilight类型取舍仍待确认。 + ## 当前检查点:PKG-FACTORY-01 类型兼容决策 Canvas/Ambilight 的工厂替换问题已增加72场景编译对照,以及可选重载的独立反例。 diff --git a/refactor/risk-table.md b/refactor/risk-table.md index e227a4559..3d2b4dd84 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -168,3 +168,6 @@ | DPIP-MEDIA-01 | open / 已复现 | WebKit reports styled width640 for width320 media in the controlled Document PiP iframe matrix | PKG-DPIP-05 | | 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-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 | diff --git a/refactor/risks.json b/refactor/risks.json index 93321b561..effb957ba 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -3618,6 +3618,61 @@ "refactor/baselines/factory-compatibility-proposals.json", "refactor/factory-compatibility-decision.md" ] + }, + { + "id": "MB-LICENSE-01", + "title": "Historical MediaBunny proxy bundles embed SDK code without MPL notice markers or separate license/source provenance", + "confirmation": "reproduced", + "owners": [ + "PKG-MB-10" + ], + "compatibleResolution": "Inventory the actual candidate bundle modules, preserve relevant license/notice text and corresponding source availability; never infer embedded historical SDK version from the current lock.", + "closureCriteria": "Final candidate archive includes verified dependency license/notice and source provenance, with historical unknowns documented; no root-MIT substitution.", + "status": "open", + "evidence": [ + "refactor/baselines/mb-release.json", + "refactor/baselines/mb-contract.md", + "refactor/changes/2026-09-12-PKG-MB-01-contract.md" + ] + }, + { + "id": "MB-LIFE-01", + "title": "MediaBunny load, track replacement, timeout, RAF and audio callbacks lack one shared terminal owner", + "confirmation": "source-observed", + "owners": [ + "PKG-MB-02", + "PKG-MB-03", + "PKG-MB-04", + "PKG-MB-05", + "PKG-MB-06", + "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.", + "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" + ] + }, + { + "id": "MB-TYPE-01", + "title": "MediaBunny proxy declares only Canvas while exposing a shim and changes CommonJS export generation between releases", + "confirmation": "reproduced", + "owners": [ + "PKG-MB-08", + "PKG-MB-10" + ], + "compatibleResolution": "Preserve optional whole-factory and exact Canvas assignment, model explicit shim capabilities separately and verify historical default namespace and callable consumers.", + "closureCriteria": "Actual installed old/candidate compiler and runtime export matrices pass with negative controls; any incompatible declaration generation requires an explicit decision.", + "status": "open", + "evidence": [ + "refactor/baselines/mb-release.json", + "refactor/baselines/mb-contract.md", + "refactor/changes/2026-09-12-PKG-MB-01-contract.md" + ] } ] } diff --git a/refactor/scripts/contracts.test.mjs b/refactor/scripts/contracts.test.mjs index 6e4d03c82..82e7a2278 100644 --- a/refactor/scripts/contracts.test.mjs +++ b/refactor/scripts/contracts.test.mjs @@ -44,7 +44,8 @@ function fixture(t) { test('Actual contract model assigns all 264 family rows and separates exact published versions from workspace snapshots', () => { assert.equal(actual.policy.packages.length, 22) assert.equal(actual.families.length, 12) - assert.equal(actual.policy.versions.filter(version => version.kind === 'published-baseline').length, 15) + assert.equal(actual.policy.versions.filter(version => version.kind === 'published-baseline').length, 17) + assert.deepEqual(actual.policy.versions.filter(version => version.kind === 'published-baseline' && version.package === 'artplayer-proxy-mediabunny').map(version => version.version).sort(), ['1.0.0', '1.2.0']) assert.equal(actual.policy.versions.filter(version => version.kind === 'workspace-baseline').length, 22) assert.equal(actual.policy.cases.length, 10) const markdown = renderContracts(actual) diff --git a/refactor/scripts/mb-contract.mjs b/refactor/scripts/mb-contract.mjs new file mode 100644 index 000000000..fdeddc95e --- /dev/null +++ b/refactor/scripts/mb-contract.mjs @@ -0,0 +1,45 @@ +import assert from 'node:assert/strict' +import { execFileSync } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' +import lockfile from '@yarnpkg/lockfile' +import { archiveFiles, ensureArchive, hash, readMember, refactorDir } from './releases.mjs' + +export async function verifyMbContract() { + const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/mb-release.json'), 'utf8')) + const archives = new Map() + for (const release of [baseline.release, ...baseline.previous]) { + const archive = await ensureArchive(release) + assert.deepEqual(archiveFiles(archive), Object.keys(release.files).sort()) + for (const [file, expected] of Object.entries(release.files)) + assert.equal(hash(readMember(archive, file)), expected, `${release.version}/${file}`) + assert.deepEqual(JSON.parse(readMember(archive, 'package/package.json')), release.manifest) + for (const field of ['main', 'module', 'types', 'legacy']) + assert(Object.hasOwn(release.files, `package/${release.manifest[field].replace(/^\.\//, '')}`)) + const core = release.historicalCore + const source = execFileSync('git', ['show', `${core.commit}:${core.file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n') + assert.equal(hash(source), core.sha256LF) + assert.equal(JSON.parse(source).version, core.version) + archives.set(release.version, archive) + } + const sources = new Map() + for (const [file, expected] of Object.entries(baseline.source)) { + const source = execFileSync('git', ['show', `${baseline.sourceCommit}:${file}`], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }).replaceAll('\r\n', '\n') + assert.equal(hash(source), expected, `MediaBunny frozen source: ${file}`) + sources.set(file, source) + } + const sdk = baseline.dependency + for (const [file, expected] of Object.entries(sdk.files)) + assert.equal(hash(fs.readFileSync(path.join(refactorDir, '../node_modules', sdk.name, file))), expected, `Installed SDK provenance changed: ${file}`) + const lock = lockfile.parse(fs.readFileSync(path.join(refactorDir, '../yarn.lock'), 'utf8')) + assert.equal(lock.type, 'success') + assert.equal(lock.object[`${sdk.name}@${sdk.declaredRange}`].version, sdk.version) + return { baseline, archives, sources } +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const { baseline } = await verifyMbContract() + console.log(`MediaBunny proxy verified: two releases, twelve archive members, ${Object.keys(baseline.source).length} frozen inputs; installed SDK ${baseline.dependency.version} provenance is separate from historical bundles`) +} diff --git a/refactor/scripts/mb-contract.test.mjs b/refactor/scripts/mb-contract.test.mjs new file mode 100644 index 000000000..6d4116017 --- /dev/null +++ b/refactor/scripts/mb-contract.test.mjs @@ -0,0 +1,208 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +// eslint-disable-next-line test/no-import-node-test -- Node frozen contract runner. +import test from 'node:test' +import vm from 'node:vm' +import ts from 'typescript' +import compat from 'typescript-compat' +import { mbEnvironment, mbHistorical } from '../../test/helpers/mediabunny.js' +import { diagnostics } from './factory-assignability.mjs' +import { verifyMbContract } from './mb-contract.mjs' +import { readMember } from './releases.mjs' + +const contract = await verifyMbContract() +const { baseline, archives } = contract +const surface = JSON.parse(fs.readFileSync('refactor/baselines/mb-surface.json', 'utf8')) + +test('MediaBunny freezes two real releases, twelve members, seventeen Git inputs and separately locked SDK provenance', () => { + assert.deepEqual([baseline.release.version, ...baseline.previous.map(item => item.version)], ['1.2.0', '1.0.0']) + assert.equal([baseline.release, ...baseline.previous].reduce((sum, item) => sum + Object.keys(item.files).length, 0), 12) + assert.equal(Object.keys(baseline.source).length, 17) + assert.equal(baseline.dependency.version, '1.56.1') + assert.equal(baseline.dependency.manifestLicense, 'MPL-2.0') + assert.deepEqual([baseline.release, ...baseline.previous].map(item => item.bundledDependencyVersion), [null, null]) +}) + +test('Historical proxy archives embed SDK code without separate license/notice members or MPL markers', () => { + for (const release of [baseline.release, ...baseline.previous]) { + const files = Object.keys(release.files) + assert(!files.some(file => /license|notice|\.wasm$|worker/i.test(file))) + assert.equal(release.manifest.license, 'MIT') + const archive = archives.get(release.version) + for (const field of ['main', 'legacy', 'module']) { + const code = readMember(archive, `package/${release.manifest[field].replace(/^\.\//, '')}`).toString() + assert.doesNotMatch(code, /Mozilla Public License|MPL-2\.0|This Source Code Form/) + if (field === 'module') { + assert.match(code, /AudioBufferSink/) + assert.match(code, /CanvasSink/) + } + } + } + assert.match(fs.readFileSync('node_modules/mediabunny/LICENSE', 'utf8'), /^Mozilla Public License Version 2\.0/) +}) + +test('Published declarations preserve optional factory replacements and exact Canvas results in both supported compiler generations', () => { + const source = `import Artplayer from 'artplayer'; import factory from './factory-history'; +type Option = Parameters[0]; +const replacement: typeof factory = (_option?: Option) => (_art: Artplayer) => document.createElement('canvas'); +const result: ReturnType> = document.createElement('canvas'); +factory(); factory({ volume: 0.7, source: new Blob([]), loadTimeout: 1000 }); void [replacement, result];` + for (const release of [baseline.release, ...baseline.previous]) { + const types = readMember(archives.get(release.version), `package/${release.manifest.types.replace(/^\.\//, '')}`).toString() + for (const compiler of [ts, compat]) { + assert.deepEqual(diagnostics(compiler, source, types), []) + assert.deepEqual(diagnostics(compiler, `${source} factory({ volume: 'loud' });`, types).map(item => item.code), [2322]) + assert.deepEqual(diagnostics(compiler, `${source} factory({ m3u8: {} });`, types).map(item => item.code), release.version === '1.0.0' ? [compiler === compat ? 2345 : 2353] : []) + } + } +}) + +test('Actual main, legacy, browser globals and native ESM retain historical export shapes', async () => { + for (const release of [baseline.release, ...baseline.previous]) { + for (const field of ['main', 'legacy']) { + const code = readMember(archives.get(release.version), `package/${release.manifest[field].replace(/^\.\//, '')}`).toString() + const env = mbEnvironment({ code }) + assert.equal(typeof env.exported, release.version === '1.0.0' ? 'object' : 'function') + assert.equal(typeof env.factory(), 'function') + const global = { ...env.globals } + delete global.module + delete global.exports + vm.runInNewContext(code, global) + const value = global.artplayerProxyMediabunny || global.window.artplayerProxyMediabunny + assert.equal(typeof (value.default || value), 'function') + } + const code = readMember(archives.get(release.version), `package/${release.manifest.module.replace(/^\.\//, '')}`).toString() + const module = await import(`data:text/javascript,${encodeURIComponent(code)}`) + assert.deepEqual(Object.keys(module), ['default']) + assert.equal(typeof module.default(), 'function') + } +}) + +for (const implementation of await mbHistorical()) { + test(`MediaBunny ${implementation.name}: exact observable canvas surface and media defaults`, () => { + const env = mbEnvironment(implementation) + const canvas = env.factory()(env.art) + const expected = surface.records.find(item => item.name === implementation.name) + assert.equal(canvas, env.canvas) + assert.deepEqual(Object.keys(canvas), expected.canvasKeys) + assert.equal(Boolean(env.art.mediabunny), expected.artMediabunny) + if (env.art.mediabunny) { + assert.deepEqual(Object.getOwnPropertyNames(env.art.mediabunny), expected.shimOwn) + assert.deepEqual(Object.getOwnPropertyNames(Object.getPrototypeOf(env.art.mediabunny)), expected.shimPrototype) + } + for (const [name, value] of Object.entries(expected.defaults)) assert.equal(canvas[name], value, name) + for (const name of ['src', 'currentTime', 'play', 'duration', 'engine']) { + const descriptor = Object.getOwnPropertyDescriptor(canvas, name) + assert.equal(descriptor.enumerable, true) + assert.equal(descriptor.configurable, true) + assert.equal(typeof descriptor.get, 'function') + assert.equal(typeof descriptor.set, 'function') + } + if (implementation.name === 'published-1.0.0') { + assert.equal(Reflect.set(canvas, 'duration', 12), true, 'Older non-strict forwarding silently ignores readonly writes') + assert(Number.isNaN(canvas.duration)) + } + else { + assert.throws(() => Reflect.set(canvas, 'duration', 12), { name: 'TypeError' }) + } + assert.notEqual(canvas.play, canvas.play, 'Forwarded methods are rebound on every read') + }) + + test(`MediaBunny ${implementation.name}: option reference, coercion and historical no-op setters`, () => { + const env = mbEnvironment(implementation) + const option = { volume: 2, muted: true, poster: 'old.png', autoplay: true, loop: true, crossOrigin: 'anonymous' } + const canvas = env.factory(option)(env.art) + assert.equal(canvas.option, option) + assert.equal(canvas.volume, 2, 'Initial volume bypasses setter clamp') + canvas.volume = '0.4' + assert.equal(canvas.volume, 0.4) + assert.equal(canvas.muted, false) + canvas.volume = -1 + assert.equal(canvas.volume, 0) + canvas.volume = 2 + assert.equal(canvas.volume, 1) + canvas.muted = 'yes' + assert.equal(canvas.muted, true) + canvas.playbackRate = 2 + canvas.playbackRate = 0 + canvas.playbackRate = Number.NaN + assert.equal(canvas.playbackRate, 2) + canvas.poster = 'new.png' + assert.equal(option.poster, 'new.png') + canvas.autoplay = false + canvas.loop = false + canvas.crossOrigin = 'use-credentials' + assert.equal(canvas.autoplay, true) + assert.equal(canvas.loop, true) + assert.equal(canvas.crossOrigin, 'anonymous') + assert.deepEqual(env.emitted.map(item => item.name), ['video:volumechange', 'video:volumechange', 'video:volumechange', 'video:volumechange', 'video:ratechange']) + }) + + test(`MediaBunny ${implementation.name}: native canvas methods win and configured shim events forward through ArtPlayer`, () => { + const env = mbEnvironment(implementation) + const canvas = env.factory()(env.art) + assert.equal(canvas.getContext('2d'), env.context) + assert.deepEqual(env.nativeCalls.at(-1), ['context', '2d', true]) + canvas.setAttribute('src', 'media.mp4') + assert.equal(canvas.src, null, 'Canvas native setAttribute is not shim setAttribute') + assert.deepEqual(env.nativeCalls.at(-1), ['attribute', 'src', 'media.mp4']) + let nativeEvents = 0 + canvas.addEventListener('volumechange', () => nativeEvents++) + for (const name of env.eventNames) { + const detail = { name } + canvas.events.emit(name, detail) + const event = env.emitted.at(-1) + assert.equal(event.name, `video:${name}`) + assert.equal(event.args[0].type, name) + assert.equal(event.args[0].detail, detail) + } + assert.equal(nativeEvents, 0) + env.art.emit('resize') + assert.deepEqual({ ...canvas.style }, { width: '100%', height: '100%', objectFit: 'contain' }) + }) + + test(`MediaBunny ${implementation.name}: sync and async forwarding, synthetic ranges and factory teardown`, async () => { + const env = mbEnvironment(implementation) + const canvas = env.factory()(env.art) + const calls = [] + const promise = Promise.resolve() + canvas.engine.play = () => promise + canvas.engine.pause = () => calls.push('pause') + canvas.engine.load = source => calls.push(['load', source]) + canvas.engine.seek = time => calls.push(['seek', time]) + canvas.engine.destroy = () => calls.push('destroy') + assert.equal(canvas.play(), promise) + assert.equal(canvas.pause(), undefined) + canvas.src = 'first.mp4' + assert.equal(canvas.load(), undefined) + canvas.currentTime = '3.5' + assert.equal(canvas.currentSrc, 'first.mp4') + assert.equal(canvas.buffered.length, 0) + Object.defineProperty(canvas.engine, 'duration', { value: 12, configurable: true }) + Object.defineProperty(canvas.engine, 'currentTime', { value: 3.5, configurable: true }) + assert.equal(canvas.buffered.end(99), 12, 'Synthetic range does not validate indices or measure downloaded bytes') + assert.equal(canvas.played.end(0), 3.5) + assert.equal(canvas.seekable.start(0), 0) + env.art.emit('destroy') + assert.equal(env.art.mediabunny, undefined) + assert.deepEqual(calls, ['pause', ['load', 'first.mp4'], ['load', 'first.mp4'], ['seek', 3.5], 'destroy']) + await promise + }) + + test(`MediaBunny ${implementation.name}: frame callback is one cancellable RAF, not decoded-frame evidence`, () => { + const env = mbEnvironment(implementation) + const canvas = env.factory()(env.art) + const calls = [] + const first = canvas.requestVideoFrameCallback((...args) => calls.push(args)) + assert.equal(first, 0) + canvas.cancelVideoFrameCallback(first) + assert.equal(env.frames.size, 0) + const second = canvas.requestVideoFrameCallback((...args) => calls.push(args)) + env.frames.get(second)(100) + assert.equal(calls.length, 1) + assert.equal(calls[0][0], 100) + assert.equal(calls[0][1].presentedFrames, 0) + assert.equal(calls[0][1].mediaTime, 0) + assert.equal(canvas.canPlayType('not/a-real-codec'), 'maybe') + }) +} diff --git a/refactor/tasks.json b/refactor/tasks.json index 0197a9c98..5a16794da 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -3391,11 +3391,18 @@ "dependsOn": [ "BASE-05" ], - "status": "todo", + "status": "done", "risk": "H", "deliverable": "全部 option、art.mediabunny、canvas/shim 属性及事件表", "acceptance": "公开暴露成员和实际发布依赖/资源范围明确;核实 third-party.json 所记录依赖 MPL-2.0 元数据与实际 bundle 的许可/通知范围,不从根 MIT 推断", - "evidence": [] + "evidence": [ + "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" + ] }, { "id": "PKG-MB-02", diff --git a/refactor/third-party.json b/refactor/third-party.json index 95bab3c67..7aeae9369 100644 --- a/refactor/third-party.json +++ b/refactor/third-party.json @@ -1176,7 +1176,7 @@ "packages/artplayer-proxy-mediabunny/package.json" ], "contract": "Preserve native-video-like events, HLS input and topology, codec capability failures and AV sync; actual library/codec environment still requires verification.", - "validationStatus": "Source integration observed; complete real SDK/service/device matrix not yet verified.", + "validationStatus": "PKG-MB-01 verifies actual 1.0.0/1.2.0 archives and installed 1.56.1 MPL-2.0 provenance, recorded in mb-release.json and mb-contract.md. Historical embedded SDK version is unknown; notices are MB-LICENSE-01/MB-10. Native codec, SDK media and AV sync remain MB-02/09.", "demoPathsSource": "refactor/baselines/demo-inventory.json" }, { diff --git a/test/helpers/mediabunny.js b/test/helpers/mediabunny.js new file mode 100644 index 000000000..9d164d553 --- /dev/null +++ b/test/helpers/mediabunny.js @@ -0,0 +1,80 @@ +import vm from 'node:vm' +import { verifyMbContract } from '../../refactor/scripts/mb-contract.mjs' +import { readMember } from '../../refactor/scripts/releases.mjs' + +export async function mbHistorical() { + const { baseline, archives, sources } = await verifyMbContract() + const name = baseline.release.name + return [ + ...[baseline.release, ...baseline.previous].map(release => ({ + name: `published-${release.version}`, + code: readMember(archives.get(release.version), `package/${release.manifest.main.replace(/^\.\//, '')}`).toString(), + })), + { name: 'frozen-workspace', code: sources.get(`packages/${name}/dist/${name}.js`) }, + ] +} + +export function mbEnvironment({ code }, option = {}) { + const emitted = [] + const handlers = new Map() + const frames = new Map() + const canvasListeners = new Map() + let nextFrame = 0 + const nativeCalls = [] + const context = { clearRect() {}, drawImage() {} } + const canvas = { + width: 300, + height: 150, + style: {}, + getContext(type) { + nativeCalls.push(['context', type, this === canvas]) + return context + }, + addEventListener(type, fn) { canvasListeners.set(type, fn) }, + removeEventListener(type) { canvasListeners.delete(type) }, + getBoundingClientRect() { return { width: 300, height: 150 } }, + setAttribute(name, value) { nativeCalls.push(['attribute', name, value]) }, + } + const eventNames = ['volumechange', 'ratechange', 'play', 'pause', 'loadedmetadata', 'error'] + const art = { + constructor: { utils: { createElement: () => canvas }, config: { events: eventNames } }, + option: { url: '', autoSize: false }, + template: { $player: {} }, + on(name, fn) { + const listeners = handlers.get(name) || [] + listeners.push(fn) + handlers.set(name, listeners) + return this + }, + emit(name, ...args) { + emitted.push({ name, args }) + for (const fn of handlers.get(name) || []) fn(...args) + return this + }, + } + const module = { exports: {} } + const globals = { + module, + exports: module.exports, + window: {}, + TextDecoder, + TextEncoder, + ReadableStream, + Blob, + Event, + URL, + console, + performance, + setTimeout, + clearTimeout, + requestAnimationFrame(fn) { + const id = nextFrame++ + frames.set(id, fn) + return id + }, + cancelAnimationFrame(id) { frames.delete(id) }, + } + vm.runInNewContext(code, globals) + const factory = module.exports.default || module.exports + return { factory, exported: module.exports, art, canvas, context, option, frames, nativeCalls, eventNames, handlers, emitted, canvasListeners, globals } +}