test(canvas): [PKG-CANVAS-01] freeze published proxy and media adapter contracts

This commit is contained in:
Harvey Zhao committed 2026-09-12 12:19:25 +08:00
1 parent 20f062bc90
commit 5e26d2298d
17 files changed
+814 -7

No files matched your search

@@ -0,0 +1,109 @@
{
"schemaVersion": 1,
"task": "PKG-CANVAS-01",
"status": "done",
"parentCommit": "20f062bc901d525d0659807dceebd1e41535ca7d",
"checks": [
{
"command": "node --test refactor/scripts/canvas-contract.test.mjs refactor/scripts/contracts.test.mjs",
"result": "pass",
"tests": 20,
"canvas": 13,
"indexValidation": 7,
"scope": "two actual npm archives and frozen workspace; normal property forwarding, bitmap/fallback/callback/event/resize contracts, public export/declaration shapes and exact Git versus npm core distinction"
},
{
"command": "focused eslint --no-fix",
"result": "pass"
},
{
"command": "yarn ci:check",
"result": "pass",
"tests": {
"unit": 881,
"engineering": 14,
"baseline": 96,
"mainTotal": 991,
"repeatedContractExecutions": 44
},
"strictProductionTypeScriptFiles": 278
}
],
"archiveScope": {
"plugins": [
"npm 1.0.0",
"npm 1.1.0",
"workspace frozen at parentCommit"
],
"members": 12,
"workspaceFiles": 6,
"gitAssociatedCoreVersions": [
"5.1.7",
"5.3.1"
],
"supportedCoreRange": "not inferred from Git associations"
},
"contractIndex": {
"packages": 22,
"assignedFamilies": 264,
"workspaceVersionPoints": 22,
"publishedVersionPoints": 11,
"exactIndexedCases": 10,
"unindexedFamilyRows": 255
},
"inputs": [
{
"file": "refactor/baselines/canvas-release.json",
"sha256LF": "7f7b229a5c0350a5d02be101d0095548c7ef1e76ce95c3b7152b415b0117398c"
},
{
"file": "refactor/scripts/canvas-contract.mjs",
"sha256LF": "eb545d0ccb3438413980d292fd2e511342b680e03c5985e58006a1407c8f53aa"
},
{
"file": "refactor/scripts/canvas-contract.test.mjs",
"sha256LF": "65e5ea0c9a12f47bc7939840d6e4c25b2820e7fd5e6f32fe3bd9f35e7f3e6da6"
},
{
"file": "test/helpers/canvas.js",
"sha256LF": "cc617b6d1053a9ed9caa5dad03088abaaff4344f078e23d007ee1a935f2a6c3c"
},
{
"file": "refactor/contract-policy.json",
"sha256LF": "f450c792465b67ebdc92e6d7339fb6be4f6fb19ffbded6c512244d73a57eafd2"
},
{
"file": "refactor/scripts/contracts.test.mjs",
"sha256LF": "7cb6cfa5fe2cdfe3fd6f8dc5377ff21db2aa748bc7788f4660f61abb98bbf647"
},
{
"file": "refactor/baselines/vast-core.json",
"sha256LF": "bb703cf37591211f78719665aed0e0cb2524ec5618dad48e6b8e2dbfef132b8e"
}
],
"logs": [
{
"file": "refactor/.cache/canvas01-capture.log",
"sha256": "a8dd65ebf981668b0fb1f44a58cd044879ecce10847797622f6bf56ef5189ed4"
},
{
"file": "refactor/.cache/canvas01-targeted.log",
"sha256": "9ad62bd4c20fd501c197ec77eb19e7c66090fce356e7c26fadb972e5524e0c2c"
},
{
"file": "refactor/.cache/canvas01-lint-final.log",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"file": "refactor/.cache/canvas01-ci.log",
"sha256": "ca15ff3aaa561911ed0809b6833c3ad47013d683fab4bfbac258b6cdb3be3243"
}
],
"limitations": [
"Controlled DOM/RAF/microtasks prove normal adapter contracts, not native browser pixels, video codecs or all enumerable browser properties.",
"Pending bitmap, cleanup, error and delayed subscription concerns remain source-observed; reproduce them in task 02 and fix in task 03.",
"Declaration shape checks are not full installed type consumer validation; optional inference/export/result checks remain task 04.",
"Git commit labeled core 5.1.7 contains proxy support while actual npm 5.1.7 does not; do not merge these identities into a support range.",
"No production source or artifacts modified; no new browser/media claims, push, version bump or publication."
]
}
+57
View File
@@ -0,0 +1,57 @@
# Canvas代理发布与工作区契约
PKG-CANVAS-01;工作区冻结于20f062bc。registry观察为1.0.0/1.1.0,latest为1.1.0;
两份实际tarball各6成员,SHA512、SHA256、所有文件及manifest见canvas-release.json。
1.0.0包含src/index.js,1.1.0不带src、增加真实mjs;测试直接执行两版实际main产物和
Git冻结工作区,不能用现代构建重生成“旧包”。工作区1.1.0未来按REL-09升至2.0.0。
## 入口、属性和事件
- factory(callback)返回同步注册函数;注册依次创建canvas/video/2Dcontext,返回该canvas。
callback省略时正常;不添加插件name或公开destroy句柄。
- 先收集canvas可枚举方法并绑定canvas,然后仅转发video里可枚举且canvas上不存在的
属性。转发get/set为enumerable/configurable;函数读取时绑定video,属性值实时读取。
既有canvas属性、style和方法优先;同名addEventListener仍是canvas方法。
- 通过零延迟setTimeout,按Artplayer.config.events逐项用art.proxy监听内置video;
原生event对象原样传给art.emit('video:'+type)。这不等于对canvas dispatchEvent。
- loadedmetadata将输出width/height设为视频intrinsic尺寸;resize按容器比例缩放输出,
用padding居中。autoSize=true时跳过尺寸变更,但resize依旧会绘制。
- play取消旧RAF后立即触发异步draw,await后排下一RAF;pause/destroy只取消已存handle。
- 支持createImageBitmap时await其结果,drawImage至canvas完整输出buffer,close bitmap,
然后callback(ctx,video),再emit('artplayerProxyCanvas:draw',ctx,video)。无bitmap能力
时直接drawImage(video,...),callback/event顺序相同。错误发artplayerProxyCanvas:error。
上述是实际正常执行与源码事实;错误/竞争/终止行为由02继续复现,不自动作为应永久
保留的缺陷。属性枚举器用受控DOM描述符做断言,未宣称穷尽不同浏览器的全部属性。
## 类型与分发差异
| 维度 | npm1.0.0 | npm1.1.0/工作区 |
| --- | --- | --- |
| CJS | namespace.default | 可调用工厂 |
| ESM | 无独立mjs | mjs默认导出 |
| 类型导出 | export=与全局声明 | export default |
| callback参数声明 | 必填 | 可选 |
| callback形状 | (ctx:CanvasRenderingContext2D,video:HTMLVideoElement)=>void | 同左 |
| Result | HTMLCanvasElement | 同左,未描述转发媒体能力 |
04必须保留1.1.0的可选参数提取(Parameters包含undefined),不能照搬Ambilight的
“必填最后重载”。旧require.default、全局、现代ESM/legacy/声明路径需分别验证。
README仅demo链接;canvas.js示例省略callback并开启多项播放器功能,不构成这些功能
已在每个代理/核心/设备上验收的证明。03改实现,04补类型,05/06再验证实际消费。
## 历史核心关联的限制
1.0.0 registryGitHead对应Git核心5.1.7,1.1.0对应Git5.3.1。冻结的前者index/template
已有proxy配置及替换媒体元素逻辑,但实际npm5.1.7的option声明和此前浏览器证据都
无proxy。版本字符串相同也不能把未发布Git状态当作npm已发布能力;新增断言并固定
两份Git源码哈希防止误认。这里没有声明一个连续支持范围。
前一步Ambilight代理取色使用工作区Canvas源码+实际npm5.4.0/候选核心,另明确记录
npm5.1.7未启用代理;不是这两版npm代理的浏览器矩阵。来源见ambilight-proxy-validation.json。
## 后续风险
源码可见await bitmap完成后仍可能在pause/destroy后排帧、draw抛错可能遗漏bitmap.close、
延迟订阅与art.on缺少主动终止、空context/早期尺寸需要能力边界。01仅记录,02必须
以受控完成顺序和必要真实浏览器复现,再由03修复,不能为通过旧快照保留泄漏。
+142
View File
@@ -0,0 +1,142 @@
{
"schemaVersion": 1,
"task": "PKG-CANVAS-01",
"capturedAt": "2026-09-12T04:06:47.519Z",
"sourceCommit": "20f062bc901d525d0659807dceebd1e41535ca7d",
"registryObservation": {
"versions": [
"1.0.0",
"1.1.0"
],
"latest": "1.1.0",
"workspaceVersion": "1.1.0"
},
"release": {
"name": "artplayer-proxy-canvas",
"version": "1.1.0",
"metadataUrl": "https://registry.npmjs.org/artplayer-proxy-canvas/1.1.0",
"tarball": "https://registry.npmjs.org/artplayer-proxy-canvas/-/artplayer-proxy-canvas-1.1.0.tgz",
"integrity": "sha512-lOq22z+xX2DcpokDSMQrDTk4VGDwUaQm9SovQ0GlhgmvMBdxRBDlvv6QrVxFUFsiGUjXEdx9TmtzdiW38dlxiQ==",
"sha256": "348c1d6671f79d0e0dbf2bb15875a1e6d60d00b73cde833c018bec7e4be96c8e",
"archiveBytes": 2960,
"publishedAt": "2026-03-12T14:20:13.012Z",
"registryGitHead": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
"files": {
"package/README.md": "eb063f1381b4eea2e3f2bf6f7ae76d2979e807119ae53708bb3b2014c38a30d2",
"package/dist/artplayer-proxy-canvas.js": "ef489570e13f402f1a60eb256ce3900a0021a3d05699c2ba15adfaaeff184d4c",
"package/dist/artplayer-proxy-canvas.legacy.js": "7d25f21170c06d067aeaa1c8e79b15f2c0130a27cff1b410532d6c4ef4d2d45f",
"package/dist/artplayer-proxy-canvas.mjs": "4bb4a881787770093652a3262fc214a2529a1c514ef49affaf152b8abf0f4dc6",
"package/package.json": "4df3a0098730cfc12232fc57066fd1f7159bbd41dce87904fa51f39b82252eed",
"package/types/artplayer-proxy-canvas.d.ts": "2d7f4517ab9954ce8925739c2473c553977268bfa120b6420218470952567b47"
},
"manifest": {
"name": "artplayer-proxy-canvas",
"version": "1.1.0",
"description": "canvas 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-canvas.d.ts",
"import": "./dist/artplayer-proxy-canvas.mjs",
"require": "./dist/artplayer-proxy-canvas.js"
},
"./legacy": {
"types": "./types/artplayer-proxy-canvas.d.ts",
"import": "./dist/artplayer-proxy-canvas.legacy.js",
"require": "./dist/artplayer-proxy-canvas.legacy.js"
}
},
"main": "./dist/artplayer-proxy-canvas.js",
"module": "./dist/artplayer-proxy-canvas.mjs",
"types": "./types/artplayer-proxy-canvas.d.ts",
"legacy": "./dist/artplayer-proxy-canvas.legacy.js",
"browserslist": "last 1 Chrome version"
},
"historicalCore": {
"version": "5.3.1",
"commit": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
"file": "packages/artplayer/package.json",
"sha256LF": "862f3697876fe915dd0ada178da766ce823aae7563ed30b3e5b11b811202929c",
"scope": "Git association only, not a tested support range"
}
},
"previous": [
{
"name": "artplayer-proxy-canvas",
"version": "1.0.0",
"metadataUrl": "https://registry.npmjs.org/artplayer-proxy-canvas/1.0.0",
"tarball": "https://registry.npmjs.org/artplayer-proxy-canvas/-/artplayer-proxy-canvas-1.0.0.tgz",
"integrity": "sha512-Wp/vbobzVfGXWkFWhhyi07TTsO7mtOhJ0BEEh/DWCBbJU1S4ghWyUhB0T9PFMVopbJfY7X51ytrrDeC2vpT1Jg==",
"sha256": "728ba39534b3b8139bdc404ef15fe1ac5f76a95562a14e78c6973db2dcfa6f4f",
"archiveBytes": 8534,
"publishedAt": "2024-09-19T02:48:29.419Z",
"registryGitHead": "1605df29431abfe1fc77683e609a2c3c0fba428a",
"files": {
"package/README.md": "2d2c6283a028e1f3882e855d0abef28a15f67525b1268b5f21c00eeea6244e80",
"package/dist/artplayer-proxy-canvas.js": "b87a0032711457b4c364d628cac3ae41fe071de08ed530ec2bd4cebf24941ec5",
"package/dist/artplayer-proxy-canvas.legacy.js": "21a2509bc01be721bda037fb6cf24461c52326865362e21873c4c32bfbb162dd",
"package/package.json": "600274235ba6d20cd9fe2c34b18acc72eeb6fd3ff262d19bc72a9a4e24bb5d42",
"package/src/index.js": "24add6af4f030e94fe1bdc66ca684e21d8baed7055e1ed44e85437bbff787c39",
"package/types/artplayer-proxy-canvas.d.ts": "aaa5f7886ecdfe1f6948d296fa3436811c466ac6e4da7fcd2d43faaab6bebc4d"
},
"manifest": {
"license": "MIT",
"version": "1.0.0",
"name": "artplayer-proxy-canvas",
"types": "types/artplayer-proxy-canvas.d.ts",
"description": "canvas proxy for ArtPlayer",
"main": "dist/artplayer-proxy-canvas.js",
"legacy": "dist/artplayer-proxy-canvas.legacy.js",
"author": "Harvey Zack <laozhaochaguan@gmail.com>",
"browserslist": "last 1 Chrome version",
"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"
]
},
"historicalCore": {
"version": "5.1.7",
"commit": "1605df29431abfe1fc77683e609a2c3c0fba428a",
"file": "packages/artplayer/package.json",
"sha256LF": "8bd532103099c367fb2c289e0d410f144fdeb00b1b62cf850762a24b61e63c8a",
"scope": "Git association only, not a tested support range",
"proxySources": {
"packages/artplayer/src/index.js": "8c29c0d1c46461bf8ce0bb16f927637055fcdb9b76f979d4bf691c956b1bc8c0",
"packages/artplayer/src/template.js": "9f7e8708e79ce2f3a36c767b93721d40027fa85730eebbdd4dc807efea88f9e8"
}
}
}
],
"source": {
"packages/artplayer-proxy-canvas/src/index.js": "4b5922b05b46adf0d54ae92025b5c70456a5b8c87819c797ef91eb50e0c8f4d4",
"packages/artplayer-proxy-canvas/types/artplayer-proxy-canvas.d.ts": "2d7f4517ab9954ce8925739c2473c553977268bfa120b6420218470952567b47",
"packages/artplayer-proxy-canvas/package.json": "4df3a0098730cfc12232fc57066fd1f7159bbd41dce87904fa51f39b82252eed",
"packages/artplayer-proxy-canvas/README.md": "eb063f1381b4eea2e3f2bf6f7ae76d2979e807119ae53708bb3b2014c38a30d2",
"packages/artplayer-proxy-canvas/.npmignore": "46063058e98be55dba5cf5f33d379e73eca048f4126e7af75f8011bab7f09145",
"docs/assets/example/canvas.js": "95a971773e93cfb340a07eebcd49e333825f49ce0d167514ad98a188cd027799"
},
"limitation": "Historical core Git association is not compatibility verification; media/proxy/browser and candidate consumer checks remain later tasks."
}
+23
View File
@@ -0,0 +1,23 @@
# Canvas代理测试维护
先读 [实际契约](baselines/canvas-contract.md) 和canvas-release.json。来源包含两个真实
发布、当前Git冻结工作区,以及1.0.0关联核心Git源码,后者不等于同版本npm内容。
`node --test refactor/scripts/canvas-contract.test.mjs`运行13项:归档/导出/声明、Git与
实际npm核心能力区别、三实现属性转发和事件、正常bitmap与无bitmap绘制/resize。
测试已接入test:baseline;test/helpers/canvas.js控制元素属性、microtask、RAF、延迟
订阅和事件对象,后续02复用以构造乱序/销毁。假Canvas不证明真实像素、codec或设备。
13项正常契约通过以后,02必须分别复现过期bitmap完成、回调错误/close资源、重复
play/pause、destroy前后订阅、缺context和尺寸异常;保留历史失败与候选通过两套测试。
03拆媒体adapter/绘制调度/订阅和所有权,04明确公共类型(尤其可选参数及canvas自有
方法优先),05/06核验新旧核心、实际媒体/浏览器/设备/安装包和8082 demo。
当前已有的Ambilight代理颜色测试会直接加载proxy的src/index.js;代理迁移文件入口
或模块拆分时必须同步改为实际候选入口加载,不能用旧dist掩盖改名或遗漏模块。
Canvas本身的正式历史浏览器矩阵尚未建立;没有在01使用新浏览器结果作包整体验收。
01最终13项Canvas专项、7项契约索引检查与991项完整CI通过;证据见
[发布契约验证](baselines/canvas-contract-validation.json) 和
[实施记录](changes/2026-09-12-PKG-CANVAS-01-contract.md)。三个新增风险继续open,
生命周期仅source-observed,02需先复现再推进03源码修复。
@@ -0,0 +1,26 @@
# PKG-CANVAS-01 发布契约与基线
获取并固定npm1.0.0/1.1.0实际tarball共12成员,记录registry时点、integrity、逐成员
哈希与入口、Git工作区src/types/manifest/README/.npmignore/demo。验证器只读Git
冻结来源,未来迁移不会修改这些旧哈希。
真实执行三实现的同步canvas返回、属性/方法绑定、延迟媒体事件、callback/draw顺序、
bitmap.close及无bitmap路径、metadata/resize/autoSize。公开差异包括旧CJS.default与
新callable、export=与default、旧callback必填与新可选、Result尚未描述转发媒体面。
对应行为/类型/生命周期风险已登记,后续依次复现和修复。本步没有改生产源码。
registryGitHead的核心5.1.7有proxy源码,而实际npm5.1.7没有该配置。固定关联Git的
index/template哈希及实际旧核心声明对照,防止以相同版本号当作同样实现。5.3.1也是
Git关联而非已验证支持范围。旧代理浏览器/安装消费者尚未验收;13项为受控契约检查。
契约索引新增两个真实发布对照点,当前22工作区+11发布版本;保留255行尚未精确
索引的家族缺口,没有把本步运行自动计入每项全局契约通过。来源与验证说明见
[契约](../baselines/canvas-contract.md)、[维护入口](../canvas-validation.md)。
13项Canvas专项与7项索引反例共20项通过;专项lint和完整CI991项通过(881单元+
14工程+96基线),另44项契约重复观察不当作新增测试。278个生产TS文件严格检查;
本步没有新增生产TS文件或声称浏览器验收。结果、输入哈希与日志见
[验证证据](../baselines/canvas-contract-validation.json)。
01完成,独立本地commit并审计自身,不推送/发布。下一步PKG-CANVAS-02复现异步
bitmap/RAF及终止问题,03进入TS职责拆分;不能把源码观察直接升级为已修复问题。
+1 -1
View File
@@ -10,7 +10,7 @@ ENG-COVERAGE-01建立可校验的归属和证据索引,不把索引建成当
`contract-policy.json`维护逐包逐类责任、版本依据及稳定测试ID。版本用JSON pointer
指向冻结清单,核对包名、版本和发布archive integrity;工作区单独标识。当前22个
工作区、9个发布对照点(Ambilight新增两个),不由单个版本推断连续支持区间。
工作区、11个发布对照点(Canvas代理新增两个),不由单个版本推断连续支持区间。
`contract-runs.json`引用归档报告及LF规范化SHA-256。报告保存当时定义、实际事件、
输入指纹、命令、HEAD和Node/OS。其他tasks.json历史JSON证据列为recorded-not-normalized,
+2
View File
@@ -318,3 +318,5 @@
- PUB-artplayer-plugin-vast-1.0.0: artplayer-plugin-vast@1.0.0 (published-baseline);[来源](../refactor/baselines/vast-release.json) JSON pointer `/release`。
- PUB-artplayer-plugin-ambilight-1.1.0: artplayer-plugin-ambilight@1.1.0 (published-baseline);[来源](../refactor/baselines/ambilight-release.json) JSON pointer `/release`。
- PUB-artplayer-plugin-ambilight-1.0.0: artplayer-plugin-ambilight@1.0.0 (published-baseline);[来源](../refactor/baselines/ambilight-release.json) JSON pointer `/previous/0`。
- PUB-artplayer-proxy-canvas-1.1.0: artplayer-proxy-canvas@1.1.0 (published-baseline);[来源](../refactor/baselines/canvas-release.json) JSON pointer `/release`。
- PUB-artplayer-proxy-canvas-1.0.0: artplayer-proxy-canvas@1.0.0 (published-baseline);[来源](../refactor/baselines/canvas-release.json) JSON pointer `/previous/0`。
+21 -1
View File
@@ -451,7 +451,9 @@
"API-12": "PKG-CANVAS-02"
},
"versionIds": [
"WS-artplayer-proxy-canvas"
"WS-artplayer-proxy-canvas",
"PUB-artplayer-proxy-canvas-1.1.0",
"PUB-artplayer-proxy-canvas-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"
@@ -802,6 +804,24 @@
"file": "refactor/baselines/ambilight-release.json",
"pointer": "/previous/0",
"archiveSha256": "a95aa8c232ab48493e6b4d46b36edc8091230cbde5d18d7ec41912a4fa2b8956"
},
{
"id": "PUB-artplayer-proxy-canvas-1.1.0",
"package": "artplayer-proxy-canvas",
"kind": "published-baseline",
"version": "1.1.0",
"file": "refactor/baselines/canvas-release.json",
"pointer": "/release",
"archiveSha256": "348c1d6671f79d0e0dbf2bb15875a1e6d60d00b73cde833c018bec7e4be96c8e"
},
{
"id": "PUB-artplayer-proxy-canvas-1.0.0",
"package": "artplayer-proxy-canvas",
"kind": "published-baseline",
"version": "1.0.0",
"file": "refactor/baselines/canvas-release.json",
"pointer": "/previous/0",
"archiveSha256": "728ba39534b3b8139bdc404ef15fe1ac5f76a95562a14e78c6973db2dcfa6f4f"
}
],
"cases": [
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 221 项,范围 22 个包及工作区/示例。
状态:todo 126 / doing 6 / blocked 0 / done 89 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 125 / doing 6 / blocked 0 / done 90 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -318,7 +318,7 @@
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| PKG-CANVAS-01 | artplayer-proxy-canvas<br>核对包契约与历史用法 | BASE-05 | callback、canvas 原方法、转发 media 属性/方法/事件 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | todo |
| PKG-CANVAS-01 | artplayer-proxy-canvas<br>核对包契约与历史用法 | BASE-05 | callback、canvas 原方法、转发 media 属性/方法/事件 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
| PKG-CANVAS-02 | artplayer-proxy-canvas<br>建立特有行为与错误测试 | PKG-CANVAS-01, ENG-03, ENG-05 | ready/loadeddata/canplay、play/seek、回调、resize 与销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | todo |
| PKG-CANVAS-03 | artplayer-proxy-canvas<br>整理内部职责与资源 | PKG-CANVAS-02, CORE-06, CORE-11, CORE-16 | video adapter/原 canvas 方法/RAF 绘制/事件订阅分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
| PKG-CANVAS-04 | artplayer-proxy-canvas<br>迁移自有源码和公开类型 | PKG-CANVAS-03, ENG-04, ENG-06, CORE-07 | canvas 与媒体能力的精确组合类型,保持 Result | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
@@ -506,3 +506,4 @@
- PKG-VAST-01: [记录](baselines/vast-contract.md) [记录](baselines/vast-release.json) [记录](changes/2026-09-12-PKG-VAST-01-contract.md) [记录](baselines/vast-contract-validation.json)
- PKG-VAST-02: [记录](changes/2026-09-12-PKG-VAST-02-tests.md) [记录](baselines/vast-behavior-validation.json) [记录](baselines/vast-core.json) [记录](vast-validation.md)
- PKG-VAST-03: [记录](changes/2026-09-12-PKG-VAST-03-lifecycle.md) [记录](baselines/vast-lifecycle-validation.json) [记录](vast-compatibility-decision.md)
- PKG-CANVAS-01: [记录](changes/2026-09-12-PKG-CANVAS-01-contract.md) [记录](baselines/canvas-contract.md) [记录](baselines/canvas-release.json) [记录](canvas-validation.md) [记录](baselines/canvas-contract-validation.json)
+11
View File
@@ -1,5 +1,16 @@
# 进度与证据
## 最新完成:Canvas-01 发布契约与正常基线
冻结npm1.0.0/1.1.0共12成员及工作区6个Git输入,运行三实现的同步返回、媒体属性/
方法转发、延迟事件、bitmap关闭、回调顺序和resize。明确同版本Git核心5.1.7已有
proxy而实际npm5.1.7没有,不能混为支持范围。13项Canvas专项、7项索引检查与991项
完整CI通过,另44项重复观察;新增两个发布对照点,索引现为22工作区+11发布。
生命周期/类型/分发风险已登记,生产代理源码尚未修改。当前221项:90 done、6 doing、
125 todo。下一步Canvas-02复现异步资源和终止问题,Canvas-03开始源码TS拆分。
见 [实施记录](changes/2026-09-12-PKG-CANVAS-01-contract.md) 和
[验证证据](baselines/canvas-contract-validation.json)。独立本地提交,无推送/发布。
## 最新完成:Ambilight-PROXY-01 Canvas输出区域修复
组合测试复现Canvas缩放后仍按底层视频尺寸截取而取错九格颜色;内部明确video/canvas
+3
View File
@@ -155,3 +155,6 @@
| AMBILIGHT-TYPE-01 | resolved / 已复现 | Ambilight declarations disagree with optional factory argument and differ across published generations | PKG-AMBILIGHT-04, PKG-AMBILIGHT-06 |
| AMBILIGHT-DIST-01 | resolved / 已复现 | Ambilight 1.0.0 CommonJS default namespace differs from callable 1.1.0 | PKG-AMBILIGHT-04, PKG-AMBILIGHT-06 |
| AMBILIGHT-PROXY-01 | resolved / 已复现 | Canvas proxy output pixels differ from forwarded video dimensions | PKG-AMBILIGHT-PROXY-01, PKG-AMBILIGHT-05 |
| CANVAS-LIFE-01 | open / 源码/产物事实 | Canvas proxy pending draw, bitmap and delayed subscriptions lack terminal ownership | PKG-CANVAS-02, PKG-CANVAS-03, PKG-CANVAS-05 |
| CANVAS-TYPE-01 | open / 源码/产物事实 | Canvas proxy declarations change required callback across generations and omit forwarded media capabilities | PKG-CANVAS-04, PKG-CANVAS-06 |
| CANVAS-DIST-01 | open / 已复现 | Canvas proxy published CommonJS namespace differs from current callable export | PKG-CANVAS-04, PKG-CANVAS-06 |
+52
View File
@@ -3339,6 +3339,58 @@
"refactor/baselines/ambilight-proxy-validation.json"
],
"resolutionRationale": "Node output resize/zero-size and normal video tests pass; actual 5.4.0/candidate canvas proxy nine-color regions pass across three engines. Actual 5.1.7 lacks proxy option and is recorded as a native capability observation, not a canvas pass. Proxy-owned lifecycle and final device acceptance remain tasks CANVAS and AMBILIGHT-05."
},
{
"id": "CANVAS-LIFE-01",
"title": "Canvas proxy pending draw, bitmap and delayed subscriptions lack terminal ownership",
"confirmation": "source-observed",
"owners": [
"PKG-CANVAS-02",
"PKG-CANVAS-03",
"PKG-CANVAS-05"
],
"compatibleResolution": "Reproduce pending bitmap completion and rollback/late subscription before adding explicit draw scheduling and resource ownership.",
"closureCriteria": "Historical failures reproduced and candidate terminal/error/bitmap/real media lifecycle verified.",
"status": "open",
"evidence": [
"refactor/baselines/canvas-contract.md",
"refactor/baselines/canvas-release.json",
"refactor/changes/2026-09-12-PKG-CANVAS-01-contract.md"
]
},
{
"id": "CANVAS-TYPE-01",
"title": "Canvas proxy declarations change required callback across generations and omit forwarded media capabilities",
"confirmation": "source-observed",
"owners": [
"PKG-CANVAS-04",
"PKG-CANVAS-06"
],
"compatibleResolution": "Keep optional current callback inference and actual canvas return while validating historical consumers and any additive media surface types.",
"closureCriteria": "Strict old/current compiler consumers and installed callback/result/import variants verified without new inference narrowing.",
"status": "open",
"evidence": [
"refactor/baselines/canvas-contract.md",
"refactor/baselines/canvas-release.json",
"refactor/changes/2026-09-12-PKG-CANVAS-01-contract.md"
]
},
{
"id": "CANVAS-DIST-01",
"title": "Canvas proxy published CommonJS namespace differs from current callable export",
"confirmation": "reproduced",
"owners": [
"PKG-CANVAS-04",
"PKG-CANVAS-06"
],
"compatibleResolution": "Preserve both require.default and callable factory with actual global, legacy and ESM paths.",
"closureCriteria": "Packed runtime/type and real local demo/editor consumers pass.",
"status": "open",
"evidence": [
"refactor/baselines/canvas-contract.md",
"refactor/baselines/canvas-release.json",
"refactor/changes/2026-09-12-PKG-CANVAS-01-contract.md"
]
}
]
}
+49
View File
@@ -0,0 +1,49 @@
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 { archiveFiles, ensureArchive, hash, readMember, refactorDir } from './releases.mjs'
export async function verifyCanvasContract() {
const baseline = JSON.parse(fs.readFileSync(path.join(refactorDir, 'baselines/canvas-release.json'), 'utf8'))
const archives = new Map()
const coreSources = 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']) {
const entry = release.manifest[field]
if (entry)
assert(Object.hasOwn(release.files, `package/${entry.replace(/^\.\//, '')}`))
}
if (release.historicalCore) {
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)
for (const [file, expected] of Object.entries(core.proxySources || {})) {
const content = execFileSync('git', ['show', `${core.commit}:${file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
assert.equal(hash(content), expected)
coreSources.set(`${release.version}:${file}`, content)
}
}
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' }).replaceAll('\r\n', '\n')
assert.equal(hash(source), expected, `Canvas frozen source: ${file}`)
sources.set(file, source)
}
return { baseline, archives, sources, coreSources }
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const { baseline } = await verifyCanvasContract()
console.log(`Canvas proxy verified: ${[baseline.release, ...baseline.previous].map(release => release.version).join(', ')}; 12 members and frozen Git inputs`)
}
+154
View File
@@ -0,0 +1,154 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
// eslint-disable-next-line test/no-import-node-test -- Node baseline runner.
import test from 'node:test'
import vm from 'node:vm'
import { canvasEnvironment, canvasHistorical } from '../../test/helpers/canvas.js'
import { verifyCanvasContract } from './canvas-contract.mjs'
import { ensureArchive, hash, readMember } from './releases.mjs'
const contract = await verifyCanvasContract()
test('Canvas Git-associated core 5.1.7 has proxy support that the actual npm 5.1.7 declaration lacks', async () => {
const release = JSON.parse(fs.readFileSync('refactor/baselines/vast-core.json')).release
assert.equal(release.version, '5.1.7')
const file = 'package/types/option.d.ts'
const declaration = readMember(await ensureArchive(release), file)
assert.equal(hash(declaration), release.files[file])
assert.doesNotMatch(declaration.toString(), /proxy\??\s*:/)
assert.match(contract.coreSources.get('1.0.0:packages/artplayer/src/index.js'), /proxy:\s*undefined/)
assert.match(contract.coreSources.get('1.0.0:packages/artplayer/src/template.js'), /option\.proxy\.call\(this\.art, this\.art\)/)
})
test('Canvas actual archives and frozen workspace retain exact files, versions and Git associations', () => {
assert.equal(contract.baseline.release.version, '1.1.0')
assert.equal(contract.baseline.previous[0].version, '1.0.0')
assert.equal(contract.baseline.release.historicalCore.version, '5.3.1')
assert.equal(contract.baseline.previous[0].historicalCore.version, '5.1.7')
assert.equal([...contract.archives].length, 2)
assert.equal([...contract.sources].length, 6)
})
test('Canvas published CJS namespaces, callable exports, script globals and actual ESM stay distinct', async () => {
for (const release of [contract.baseline.release, ...contract.baseline.previous]) {
for (const field of ['main', 'legacy']) {
const source = readMember(contract.archives.get(release.version), `package/${release.manifest[field].replace(/^\.\//, '')}`).toString()
const module = { exports: {} }
vm.runInNewContext(source, { module, exports: module.exports, window: {} })
assert.equal(typeof module.exports, release.version === '1.0.0' ? 'object' : 'function')
assert.equal(typeof (module.exports.default || module.exports)(), 'function')
const global = { window: {} }
vm.runInNewContext(source, global)
assert.equal(typeof (global.artplayerProxyCanvas || global.window.artplayerProxyCanvas), 'function')
}
}
const source = readMember(contract.archives.get('1.1.0'), 'package/dist/artplayer-proxy-canvas.mjs').toString()
const namespace = await import(`data:text/javascript,${encodeURIComponent(source)}`)
assert.deepEqual(Object.keys(namespace), ['default'])
assert.equal(typeof namespace.default(), 'function')
})
test('Canvas published declarations preserve callback/result shape and expose the historical optional-argument change', () => {
const old = readMember(contract.archives.get('1.0.0'), 'package/types/artplayer-proxy-canvas.d.ts').toString()
const current = readMember(contract.archives.get('1.1.0'), 'package/types/artplayer-proxy-canvas.d.ts').toString()
assert.match(old, /export = artplayerProxyCanvas/)
assert.match(old, /\(option: Option\)/)
assert.match(current, /export default artplayerProxyCanvas/)
assert.match(current, /\(option\?: Option\)/)
for (const source of [old, current]) {
assert.match(source, /ctx: CanvasRenderingContext2D, video: HTMLVideoElement/)
assert.match(source, /type Result = HTMLCanvasElement/)
}
})
for (const implementation of await canvasHistorical()) {
test(`Canvas ${implementation.name}: bitmap closes before the public callback and draw event`, async () => {
const sequence = []
const bitmap = { close() {
sequence.push('close')
} }
const env = await canvasEnvironment(implementation, {
createImageBitmap(video) {
assert.equal(video, env.video)
sequence.push('bitmap')
return Promise.resolve(bitmap)
},
onDraw() { sequence.push('draw') },
})
env.factory(() => sequence.push('callback'))(env.art)
env.art.on('artplayerProxyCanvas:draw', () => sequence.push('event'))
env.art.emit('resize')
await env.flush()
assert.equal(env.draws[0][0], bitmap)
assert.deepEqual(sequence, ['bitmap', 'draw', 'close', 'callback', 'event'])
assert.equal(env.frames.size, 0)
})
test(`Canvas ${implementation.name}: synchronous canvas return, own methods and live bound media forwarding`, async () => {
const env = await canvasEnvironment(implementation)
const result = env.factory()(env.art)
assert.equal(result, env.canvas)
assert.equal(result.title, 'canvas-title')
result.src = 'sample.mp4'
result.currentTime = 3
assert.equal(env.video.src, 'sample.mp4')
assert.equal(env.video.currentTime, 3)
env.video.videoWidth = 999
assert.equal(result.videoWidth, 999)
const descriptor = Object.getOwnPropertyDescriptor(result, 'src')
assert.equal(descriptor.enumerable, true)
assert.equal(descriptor.configurable, true)
const play = result.play
await play('argument')
assert.equal(env.calls[0].receiver, env.video)
assert.deepEqual(env.calls[0].args, ['argument'])
const toDataURL = result.toDataURL
assert.equal(toDataURL('image/png'), 'data:canvas')
assert.equal(env.calls[1].receiver, env.canvas)
result.addEventListener('click', () => {})
assert.equal(env.calls[2].name, 'canvas-listener')
assert.equal(env.proxies.length, 0)
assert.equal(env.timers.size, 1)
env.flushTimers()
assert.equal(env.proxies.length, env.art.constructor.config.events.length)
const event = { type: 'seeked', target: env.video }
env.emitVideo(event)
assert.equal(env.emitted.at(-1).name, 'video:seeked')
assert.equal(env.emitted.at(-1).args[0], event)
})
test(`Canvas ${implementation.name}: callback precedes draw event, metadata/resize preserve aspect and autoSize rules`, async () => {
const env = await canvasEnvironment(implementation)
const sequence = []
env.factory((context, video) => {
assert.equal(context, env.context)
assert.equal(video, env.video)
sequence.push('callback')
})(env.art)
env.art.on('artplayerProxyCanvas:draw', (context, video) => {
assert.equal(context, env.context)
assert.equal(video, env.video)
sequence.push('event')
})
env.flushTimers()
env.emitVideo({ type: 'loadedmetadata' })
assert.deepEqual([env.canvas.width, env.canvas.height], [320, 180])
env.art.emit('resize')
await env.flush()
assert.deepEqual([env.canvas.width, env.canvas.height], [640, 360])
assert.equal(env.canvas.style.padding, '60px 0px')
assert.deepEqual(env.draws[0], [env.video, 0, 0, 640, 360])
assert.deepEqual(sequence, ['callback', 'event'])
env.art.option.autoSize = true
env.art.template.$player.clientWidth = 100
env.art.emit('resize')
await env.flush()
assert.equal(env.canvas.width, 640)
assert.equal(env.draws.length, 2, 'autoSize suppresses sizing, not the historical resize draw')
env.emitVideo({ type: 'play' })
await env.flush()
assert.equal(env.frames.size, 1)
env.emitVideo({ type: 'pause' })
assert.equal(env.frames.size, 0)
})
}
+1 -1
View File
@@ -44,7 +44,7 @@ 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, 9)
assert.equal(actual.policy.versions.filter(version => version.kind === 'published-baseline').length, 11)
assert.equal(actual.policy.versions.filter(version => version.kind === 'workspace-baseline').length, 22)
assert.equal(actual.policy.cases.length, 10)
const markdown = renderContracts(actual)
+8 -2
View File
@@ -3244,11 +3244,17 @@
"dependsOn": [
"BASE-05"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "callback、canvas 原方法、转发 media 属性/方法/事件",
"acceptance": "源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结",
"evidence": []
"evidence": [
"changes/2026-09-12-PKG-CANVAS-01-contract.md",
"baselines/canvas-contract.md",
"baselines/canvas-release.json",
"canvas-validation.md",
"baselines/canvas-contract-validation.json"
]
},
{
"id": "PKG-CANVAS-02",
+152
View File
@@ -0,0 +1,152 @@
import vm from 'node:vm'
import { transform } from 'esbuild'
import { verifyCanvasContract } from '../../refactor/scripts/canvas-contract.mjs'
import { readMember } from '../../refactor/scripts/releases.mjs'
export async function canvasHistorical() {
const contract = await verifyCanvasContract()
return [
...[contract.baseline.release, ...contract.baseline.previous].map(release => ({ name: `published-${release.version}`, source: readMember(contract.archives.get(release.version), `package/${release.manifest.main.replace(/^\.\//, '')}`).toString(), format: 'artifact' })),
{ name: 'frozen-workspace', source: contract.sources.get('packages/artplayer-proxy-canvas/src/index.js'), format: 'source' },
]
}
export async function canvasEnvironment(implementation, settings = {}) {
const calls = []
const draws = []
const listeners = new Map()
const proxies = []
const emitted = []
const timers = new Map()
const frames = new Map()
const cancelled = []
let nextTimer = 0
let nextFrame = settings.firstFrame ?? 0
const context = {
drawImage(...args) {
draws.push(args)
settings.onDraw?.(...args)
},
}
const canvas = {
nodeName: 'CANVAS',
width: 300,
height: 150,
title: 'canvas-title',
style: {},
getContext(kind) { return kind === '2d' && !settings.noContext ? context : null },
toDataURL(...args) {
calls.push({ name: 'toDataURL', receiver: this, args })
return 'data:canvas'
},
addEventListener(...args) { calls.push({ name: 'canvas-listener', receiver: this, args }) },
}
const video = {
nodeName: 'VIDEO',
videoWidth: 320,
videoHeight: 180,
width: 320,
height: 180,
title: 'video-title',
src: '',
currentTime: 0,
paused: true,
muted: false,
style: {},
play(...args) {
calls.push({ name: 'play', receiver: this, args })
this.paused = false
return Promise.resolve()
},
pause(...args) {
calls.push({ name: 'pause', receiver: this, args })
this.paused = true
},
addEventListener(...args) { calls.push({ name: 'video-listener', receiver: this, args }) },
}
const art = {
constructor: { config: { events: ['loadedmetadata', 'play', 'pause', 'seeked', 'error'] }, utils: { createElement: tag => tag === 'canvas' ? canvas : video } },
option: { autoSize: false },
template: { $player: { clientWidth: 640, clientHeight: 480 } },
isDestroy: false,
on(name, callback) {
if (!listeners.has(name))
listeners.set(name, new Set())
listeners.get(name).add(callback)
return art
},
off(name, callback) {
listeners.get(name)?.delete(callback)
return art
},
emit(name, ...args) {
emitted.push({ name, args })
for (const callback of [...(listeners.get(name) || [])]) callback(...args)
return art
},
proxy(target, name, callback) {
const entry = { target, name, callback }
proxies.push(entry)
return () => proxies.splice(proxies.indexOf(entry), 1)
},
destroy() {
art.isDestroy = true
art.emit('destroy')
},
}
const source = implementation.format === 'source' ? (await transform(implementation.source, { format: 'cjs', target: 'es2020' })).code : implementation.source
const module = { exports: {} }
vm.runInNewContext(source, {
module,
exports: module.exports,
window: {},
createImageBitmap: settings.createImageBitmap,
setTimeout(callback) {
const id = nextTimer++
timers.set(id, callback)
return id
},
clearTimeout(id) { timers.delete(id) },
requestAnimationFrame(callback) {
const id = nextFrame++
frames.set(id, callback)
return id
},
cancelAnimationFrame(id) {
cancelled.push(id)
frames.delete(id)
},
}, { timeout: 5000 })
return {
factory: module.exports.default || module.exports,
art,
canvas,
video,
context,
calls,
draws,
listeners,
proxies,
emitted,
timers,
frames,
cancelled,
flushTimers() {
const pending = [...timers.values()]
timers.clear()
for (const callback of pending) callback()
},
emitVideo(event) {
for (const entry of [...proxies]) {
if (entry.target === video && entry.name === event.type)
entry.callback(event)
}
},
async flush() {
// Resolve only the controlled draw/animation promise chain, without wall-clock sleeps.
await Promise.resolve()
await Promise.resolve()
await Promise.resolve()
},
}
}