test(mediabunny): [PKG-MB-01] freeze published proxy and dependency contracts

This commit is contained in:
Harvey Zhao committed 2026-09-12 17:05:55 +08:00
1 parent 1f93bb9ba1
commit 1cfd2e17f3
18 files changed
+1357 -7

No files matched your search

@@ -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."
}
]
}
+113
View File
@@ -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<Uint8Array> |
| 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继续关闭。
+186
View File
@@ -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 <laozhaochaguan@gmail.com>",
"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 <laozhaochaguan@gmail.com>",
"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"
}
}
+494
View File
@@ -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<Uint8Array>"
},
{
"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"
}
]
}
@@ -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/产物文件哈希与冻结输入一致,未以重建掩盖历史行为。
+2
View File
@@ -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`。
+21 -1
View File
@@ -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": [
+35
View File
@@ -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豁免不适用于本包。
+3 -2
View File
@@ -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<br>核对 shim 与媒体契约 | BASE-05 | 全部 option、art.mediabunny、canvas/shim 属性及事件表 | 公开暴露成员和实际发布依赖/资源范围明确;核实 third-party.json 所记录依赖 MPL-2.0 元数据与实际 bundle 的许可/通知范围,不从根 MIT 推断 | H | todo |
| PKG-MB-01 | artplayer-proxy-mediabunny<br>核对 shim 与媒体契约 | BASE-05 | 全部 option、art.mediabunny、canvas/shim 属性及事件表 | 公开暴露成员和实际发布依赖/资源范围明确;核实 third-party.json 所记录依赖 MPL-2.0 元数据与实际 bundle 的许可/通知范围,不从根 MIT 推断 | H | done |
| PKG-MB-02 | artplayer-proxy-mediabunny<br>建立真实媒体与事件基线 | PKG-MB-01, ENG-05, ENG-08 | MP4/WebM/HLS/Blob/Stream、seek/错误/轨道切换样本 | ready 事件顺序、音画同步、超时和释放可重跑 | H | todo |
| PKG-MB-03 | artplayer-proxy-mediabunny<br>整理 input 与加载取消 | PKG-MB-02, CORE-09 | input.js 源检测、输入资源、Range 与取消 | HLS 检测在 input 边界,旧来源类型和超时保持 | H | todo |
| PKG-MB-04 | artplayer-proxy-mediabunny<br>整理 VideoShim/EventTarget/Engine 协调 | PKG-MB-03, CORE-06, CORE-11 | shim 描述符、事件桥、加载状态和操作隔离 | 公开属性/同步异步返回与事件重入顺序保持 | H | todo |
@@ -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)
+10
View File
@@ -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场景编译对照,以及可选重载的独立反例。
+3
View File
@@ -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 |
+55
View File
@@ -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"
]
}
]
}
+2 -1
View File
@@ -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)
+45
View File
@@ -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`)
}
+208
View File
@@ -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<typeof factory>[0];
const replacement: typeof factory = (_option?: Option) => (_art: Artplayer) => document.createElement('canvas');
const result: ReturnType<ReturnType<typeof factory>> = 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')
})
}
+9 -2
View File
@@ -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",
+1 -1
View File
@@ -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"
},
{
+80
View File
@@ -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 }
}