test(mask): [PKG-MASK-05] checkpoint native model and historical-core coverage

This commit is contained in:
Harvey Zhao committed 2026-09-14 00:36:06 +08:00
1 parent aca4bc1cd2
commit 8abd49bc9c
14 files changed
+5486 -15

No files matched your search

@@ -162,8 +162,13 @@ Two registrations in the same controlled SDK/RAF environment also verify that
stopping or destroying one leaves the other model, mask and scheduled frame owned
by its original player. Native shared TensorFlow backend state remains unverified.
Native acceptance still needs actual local model loading, new/old cores, real
Danmuku composition, pause/seek/source/layout changes, multiple players, failure
recovery and post-destroy resources. The default unversioned solutionPath can
`yarn test:browser test/browser/danmuku-mask-native.spec.js --workers=1` now covers
actual local model loading, bitmap alpha and owned canvas cleanup, new/old cores,
real Danmuku delivery after model readiness, pause/seek and CSS web fullscreen.
Set ARTPLAYER_MASK_ARTIFACT to the legacy global build for the second format.
Archive each report before another browser run. The startup timestamp diagnostic
can still miss a row; do not interpret the post-ready assertions as losslessness.
Native acceptance still needs source changes, multiple players, failure recovery,
devices and internal GPU/WASM resources. The default unversioned solutionPath can
load assets independently of Yarn's SDK resolution. PKG-MASK-05/06 retain the
real combination and distribution/release gates after source/type migration.
@@ -0,0 +1,484 @@
{
"schemaVersion": 1,
"task": "PKG-MASK-05",
"sharedWith": [
"PKG-JASSUB-05"
],
"sourceCommit": "aca4bc1cd26f3327236cc9801b331c40ac93e385",
"capturedAt": "2026-09-13T16:16:17.160Z",
"associationScope": "These versions are the core workspace manifests at the Git commits associated with real Mask/JASSUB releases. Archive existence is not a supported-version range or browser validation. Stable 5.3.1 and prerelease 5.3.1-beta.1 are distinct identities.",
"existingEvidence": [
"refactor/baselines/asr-core.json records prior unavailable 5.3.1 and adjacent published 5.3.0; it is not substituted here",
"refactor/baselines/vast-core.json records separate 5.1.7"
],
"associations": [
{
"baseline": "refactor/baselines/danmuku-mask-release.json",
"plugin": "artplayer-plugin-danmuku-mask",
"pluginVersion": "1.1.0",
"commit": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
"file": "packages/artplayer/package.json",
"version": "5.3.1",
"sha256LF": "862f3697876fe915dd0ada178da766ce823aae7563ed30b3e5b11b811202929c",
"sourceManifest": {
"name": "artplayer",
"version": "5.3.1",
"description": "ArtPlayer.js is a modern and full featured HTML5 video player",
"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.d.ts",
"import": "./dist/artplayer.mjs",
"require": "./dist/artplayer.js"
},
"./legacy": {
"types": "./types/artplayer.d.ts",
"import": "./dist/artplayer.legacy.js",
"require": "./dist/artplayer.legacy.js"
},
"./i18n/*": {
"types": "./types/i18n.d.ts",
"import": "./dist/i18n/*.mjs",
"require": "./dist/i18n/*.js"
}
},
"main": "./dist/artplayer.js",
"module": "./dist/artplayer.mjs",
"types": "./types/artplayer.d.ts",
"typesVersions": {
"*": {
"i18n/*": [
"types/i18n.d.ts"
],
"legacy": [
"types/artplayer.d.ts"
]
}
},
"legacy": "./dist/artplayer.legacy.js",
"browserslist": "last 1 Chrome version",
"dependencies": {
"option-validator": "^2.0.6"
}
}
},
{
"baseline": "refactor/baselines/danmuku-mask-release.json",
"plugin": "artplayer-plugin-danmuku-mask",
"pluginVersion": "1.0.0",
"commit": "b36f88db5f7fe0250285fd4f47229842231f402b",
"file": "packages/artplayer/package.json",
"version": "5.3.1-beta.1",
"sha256LF": "e1a0a390ec10cd8c1c12006ac6515414ae98a964300dd3e9c288377def948497",
"sourceManifest": {
"name": "artplayer",
"version": "5.3.1-beta.1",
"description": "ArtPlayer.js is a modern and full featured HTML5 video player",
"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.d.ts",
"import": "./dist/artplayer.mjs",
"require": "./dist/artplayer.js"
},
"./legacy": {
"types": "./types/artplayer.d.ts",
"import": "./dist/artplayer.legacy.js",
"require": "./dist/artplayer.legacy.js"
},
"./i18n/*": {
"types": "./types/i18n.d.ts",
"import": "./dist/i18n/*.mjs",
"require": "./dist/i18n/*.js"
}
},
"main": "./dist/artplayer.js",
"module": "./dist/artplayer.mjs",
"types": "./types/artplayer.d.ts",
"typesVersions": {
"*": {
"i18n/*": [
"types/i18n.d.ts"
],
"legacy": [
"types/artplayer.d.ts"
]
}
},
"legacy": "./dist/artplayer.legacy.js",
"browserslist": "last 1 Chrome version",
"dependencies": {
"option-validator": "^2.0.6"
}
}
},
{
"baseline": "refactor/baselines/jassub-release.json",
"plugin": "artplayer-plugin-jassub",
"pluginVersion": "1.1.0",
"commit": "daf133b22630b4a0eecfa3336bbddab0e9119d96",
"file": "packages/artplayer/package.json",
"version": "5.3.1",
"sha256LF": "862f3697876fe915dd0ada178da766ce823aae7563ed30b3e5b11b811202929c",
"scope": "Source-associated core version only, not verified supported core range.",
"sourceManifest": {
"name": "artplayer",
"version": "5.3.1",
"description": "ArtPlayer.js is a modern and full featured HTML5 video player",
"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.d.ts",
"import": "./dist/artplayer.mjs",
"require": "./dist/artplayer.js"
},
"./legacy": {
"types": "./types/artplayer.d.ts",
"import": "./dist/artplayer.legacy.js",
"require": "./dist/artplayer.legacy.js"
},
"./i18n/*": {
"types": "./types/i18n.d.ts",
"import": "./dist/i18n/*.mjs",
"require": "./dist/i18n/*.js"
}
},
"main": "./dist/artplayer.js",
"module": "./dist/artplayer.mjs",
"types": "./types/artplayer.d.ts",
"typesVersions": {
"*": {
"i18n/*": [
"types/i18n.d.ts"
],
"legacy": [
"types/artplayer.d.ts"
]
}
},
"legacy": "./dist/artplayer.legacy.js",
"browserslist": "last 1 Chrome version",
"dependencies": {
"option-validator": "^2.0.6"
}
}
},
{
"baseline": "refactor/baselines/jassub-release.json",
"plugin": "artplayer-plugin-jassub",
"pluginVersion": "1.0.0",
"commit": "b69d020c5a5d06e093c63f456ad7d0c7777b0b03",
"file": "packages/artplayer/package.json",
"version": "5.3.1-beta.1",
"sha256LF": "e1a0a390ec10cd8c1c12006ac6515414ae98a964300dd3e9c288377def948497",
"scope": "Source-associated core version only, not verified supported core range.",
"sourceManifest": {
"name": "artplayer",
"version": "5.3.1-beta.1",
"description": "ArtPlayer.js is a modern and full featured HTML5 video player",
"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.d.ts",
"import": "./dist/artplayer.mjs",
"require": "./dist/artplayer.js"
},
"./legacy": {
"types": "./types/artplayer.d.ts",
"import": "./dist/artplayer.legacy.js",
"require": "./dist/artplayer.legacy.js"
},
"./i18n/*": {
"types": "./types/i18n.d.ts",
"import": "./dist/i18n/*.mjs",
"require": "./dist/i18n/*.js"
}
},
"main": "./dist/artplayer.js",
"module": "./dist/artplayer.mjs",
"types": "./types/artplayer.d.ts",
"typesVersions": {
"*": {
"i18n/*": [
"types/i18n.d.ts"
],
"legacy": [
"types/artplayer.d.ts"
]
}
},
"legacy": "./dist/artplayer.legacy.js",
"browserslist": "last 1 Chrome version",
"dependencies": {
"option-validator": "^2.0.6"
}
}
}
],
"metadata": {
"checks": [
{
"checkedAt": "2026-09-13T16:16:11.693Z",
"url": "https://registry.npmjs.org/artplayer",
"method": "GET",
"status": 200,
"bodySha256": "560fa27d689284c6e1743835b765e790613b0b11d7255ea0390eb0017a7e9748",
"contentType": "application/json",
"containsRequestedVersions": {
"5.3.1": false,
"5.3.1-beta.1": true
},
"nearbyVersions": [
"5.2.0",
"5.2.1",
"5.2.2",
"5.2.3",
"5.2.4",
"5.2.5",
"5.3.0-beta.0",
"5.3.0-beta.1",
"5.3.0-beta.2",
"5.3.0-beta.3",
"5.3.0-beta.4",
"5.3.0",
"5.3.1-beta.0",
"5.3.1-beta.1",
"5.4.0"
],
"distTags": {
"latest": "5.4.0",
"beta": "5.3.1-beta.1"
}
},
{
"checkedAt": "2026-09-13T16:16:12.059Z",
"url": "https://registry.npmjs.org/artplayer/5.3.1",
"method": "GET",
"status": 404,
"bodySha256": "cb85b239f6d43e568f1a54eeac640ca05397450355d18994fe3719fed61dfd8b",
"contentType": "application/json",
"body": "\"version not found: 5.3.1\""
},
{
"checkedAt": "2026-09-13T16:16:12.880Z",
"url": "https://registry.npmjs.org/artplayer/-/artplayer-5.3.1.tgz",
"method": "HEAD",
"status": 404,
"bodySha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"contentType": "application/json",
"body": ""
},
{
"checkedAt": "2026-09-13T16:16:13.152Z",
"url": "https://registry.npmjs.org/artplayer/5.3.1-beta.1",
"method": "GET",
"status": 200,
"bodySha256": "c239f203392461685268b388af53eb1ff4cd1f72c2fb7c776181302edb58c074",
"contentType": "application/json"
},
{
"checkedAt": "2026-09-13T16:16:16.650Z",
"url": "https://registry.npmjs.org/artplayer/-/artplayer-5.3.1-beta.1.tgz",
"method": "GET",
"status": 200,
"byteLength": 152307,
"bodySha256": "335d4e042b92249aea5d9dfbc7ea4c9b5fd42c7c427b54c5d4b91ec6068a487e"
}
]
},
"entries": [
{
"requestedVersion": "5.3.1",
"status": "unavailable-in-npm-at-capture",
"release": null,
"archivePath": null,
"note": "Source-associated workspace version only; no npm tarball or integrity is available. No substitute version is selected."
},
{
"requestedVersion": "5.3.1-beta.1",
"status": "archive-verified",
"release": {
"name": "artplayer",
"version": "5.3.1-beta.1",
"metadataUrl": "https://registry.npmjs.org/artplayer/5.3.1-beta.1",
"tarball": "https://registry.npmjs.org/artplayer/-/artplayer-5.3.1-beta.1.tgz",
"integrity": "sha512-btjbQwXPl86U/Apja0WrCUF+AXLPacI60eUGAeJTx3EMuUKrVaOFrmWrIeeyMUi5aIzMSXM++tQjlSMJVnmhuQ==",
"sha256": "335d4e042b92249aea5d9dfbc7ea4c9b5fd42c7c427b54c5d4b91ec6068a487e",
"registryShasum": "15eb5cd86016ff22702d1c4fe69c75293c026baf",
"registryGitHead": "3f47356d2ce11a5ebf81fe4a5a30600f5e714d57",
"files": {
"package/README.md": "2070b94087839a80dc01616a3114cbe2d3a4950018188f433752880daf7843f8",
"package/dist/artplayer.js": "e91e64228c57bb97300c709789d5557ab07b133cab307267b4a2a4df3083c861",
"package/dist/artplayer.legacy.js": "da8180437fe7f65af62041ef95a5e6c40ef701560cf7d2619c2873bde38d20f0",
"package/dist/artplayer.mjs": "04896904fb8ff3c4757bcd790a88f1517c65dba718b8b96ecca347a81694b7e8",
"package/dist/i18n/ar.js": "44b94758cfc7c45179ee1a90ca730b91717fccb5a79d27afd691b61fdfdf51e0",
"package/dist/i18n/ar.mjs": "5dd3a7183c81350786e95a5f0431de9c64937a16627c2a44042af7cb33d3d646",
"package/dist/i18n/cs.js": "76353dbd1b97f52f3bcd7e71aa9117ca7d76d2b2cbf86426cf3dac64afd7abf2",
"package/dist/i18n/cs.mjs": "aa6cb5eaa1ce78fd7d4033affb4f48cff4fedd773bf55f88ccaf3d76dd822cb1",
"package/dist/i18n/es.js": "7e43252ba479d87c1edd5244a7c870126aae07c8791c365888fe0ba82dd6e85f",
"package/dist/i18n/es.mjs": "7a0aa9698ddcb191fc4f62d1fcd07ee004dbb00053b2920cfcf5fa81f71ea71e",
"package/dist/i18n/fa.js": "4b60d3d4f5d5e87686a3af179c9b671b4f432888c6f77ae92fb7025386a9910b",
"package/dist/i18n/fa.mjs": "261a81e46d40a1bcad64419d4954036fb49e9fea86682a89aaba277bd52d3c54",
"package/dist/i18n/fr.js": "410d9552ad387c191d168b6fff6d21538c57e74d07908ebab2c0dedc3577ed90",
"package/dist/i18n/fr.mjs": "d8adfe0ba5eb2208a5241675a084dbd1c4d20d8ac2979fcbcb72bef18827fbfd",
"package/dist/i18n/id.js": "4a8fc5d2393a5a19a8ba68429545073035c8803d9d3101b696fbc895d20b22b2",
"package/dist/i18n/id.mjs": "b3ac8f8c203a7a64374ac64931d7eb3d912c2e71c4a604ddc4651229cca64c07",
"package/dist/i18n/pl.js": "a7c421f0264cd054b8d289e553e77018c547417d8d437f24157ccb1a2cd8c4f6",
"package/dist/i18n/pl.mjs": "c58ce3f06411e837c28dbea3d2a68f38045da58de3b73ae5457468a01d526884",
"package/dist/i18n/ru.js": "5406ee8662a259af703a930c4e241874e12a811304f3e1d40dd00033e7ff0b52",
"package/dist/i18n/ru.mjs": "c17c830da2189d4584c816907b6e8626d8606c7a20565b383c4b768be00e6600",
"package/dist/i18n/tr.js": "f82cdbf5feefceff68f78466ca261e1ea1c4975c853543947fc91ffd8107ec46",
"package/dist/i18n/tr.mjs": "683b768eeea905793f63bf483d9870af10183a718aba84d8ee794934a8f07b22",
"package/dist/i18n/vi.js": "c4f0c224404ce73a807758661c9eaa46fb548eb4519a2ee11304396f268a6a0e",
"package/dist/i18n/vi.mjs": "68d10c0bb6e7fe4d82817bca883f04f514d73301ff5dfddf02a0a1e3b2bfbc0a",
"package/dist/i18n/zh-tw.js": "1241a6749c088192d1118f91ae36a894ecc198875d4b9bdfe2bbf617803e3806",
"package/dist/i18n/zh-tw.mjs": "b28d13355759e2b47d0553617a01ad6b3af76d975681b569bad2e99e48d095ff",
"package/package.json": "e1a0a390ec10cd8c1c12006ac6515414ae98a964300dd3e9c288377def948497",
"package/types/artplayer.d.ts": "28cb09ddffb6591372ab01cbde8ff3c3c0db903eea58851dc6561375e740cc3c",
"package/types/component.d.ts": "0118ec5dbb9aa48d735169c74bd65149b72c9f99db29dd635cd41ff69707b68d",
"package/types/config.d.ts": "bb40a0ca9d200c15bba7ecba0103d4ce8216e428d05c17ec43288d298223bc68",
"package/types/cssVar.d.ts": "2ff9827aa10bf326f60f6cda7556a94fdcabafba529185a1ceae46d865f33697",
"package/types/events.d.ts": "afd56b6939bedecc070eecd2b7440d0903ef8d3107fe41d376bfdb9e0ae3362d",
"package/types/i18n.d.ts": "01e7d7e7f775166eed6dbc6b86feb26306302fcbfe33fa01b7e363a14b3ef2b0",
"package/types/icons.d.ts": "61ce1b0cb9bc7488263b644419b5fd5a13e72de1493c8113eee81d5174f1234b",
"package/types/option.d.ts": "d689b55e3fbc0a9ca9c554b5c9f6c1ae5f1d81aed07c174a557c95633a18a276",
"package/types/player.d.ts": "8eb3e3b5822139379ad09f6aad8a0da360fb049bd0ebf4540d128cdd1fb5e994",
"package/types/quality.d.ts": "dcca8c0f5b5c25373f85825a274fc3ee8beeffd7c8b2a978468e039ff4946a9e",
"package/types/setting.d.ts": "cc87ff5ccb87901e12d2f367ec2e872afd5c029b9f6d0a22d01d7d44a63dfb33",
"package/types/subtitle.d.ts": "92f711ad0f7924e609ad4425cd26d19c73eb027cccddc6d89fcf9b8e334db065",
"package/types/template.d.ts": "22d64dc2c68f8bd90247755a12bcd233343bb67537591ab08e34900917e1723d",
"package/types/utils.d.ts": "65cbb66ab2f5d38085e0a1e5ebea57c320d16f566fb6dc8f8e2dac6cf096c004"
},
"manifest": {
"name": "artplayer",
"version": "5.3.1-beta.1",
"description": "ArtPlayer.js is a modern and full featured HTML5 video player",
"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.d.ts",
"import": "./dist/artplayer.mjs",
"require": "./dist/artplayer.js"
},
"./legacy": {
"types": "./types/artplayer.d.ts",
"import": "./dist/artplayer.legacy.js",
"require": "./dist/artplayer.legacy.js"
},
"./i18n/*": {
"types": "./types/i18n.d.ts",
"import": "./dist/i18n/*.mjs",
"require": "./dist/i18n/*.js"
}
},
"main": "./dist/artplayer.js",
"module": "./dist/artplayer.mjs",
"types": "./types/artplayer.d.ts",
"typesVersions": {
"*": {
"i18n/*": [
"types/i18n.d.ts"
],
"legacy": [
"types/artplayer.d.ts"
]
}
},
"legacy": "./dist/artplayer.legacy.js",
"browserslist": "last 1 Chrome version",
"dependencies": {
"option-validator": "^2.0.6"
}
}
},
"archivePath": "D:\\github\\ArtPlayer\\refactor\\.cache\\releases\\artplayer-5.3.1-beta.1.tgz",
"archiveRelativePath": "refactor/.cache/releases/artplayer-5.3.1-beta.1.tgz",
"cachedBefore": false,
"memberCount": 41,
"verification": [
"registry metadata agrees with packument",
"registry SHA-512 integrity",
"registry SHA-1 shasum",
"archive SHA-256",
"all member SHA-256 hashes",
"manifest identity and declared entrypoints"
]
}
],
"helperBoundary": "ensureArchive currently restricts versions to stable x.y.z. For the fixed beta identity this capture uses the same releases.mjs verifyIntegrity/archiveFiles/readMember primitives, registry-only download, expected registry SHA-512/SHA-1 verification before cache write, and normal cache filename. No shared helper was modified.",
"validationLimits": [
"No browser run",
"No plugin/core combination acceptance",
"No claim of all historical support"
]
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,83 @@
# PKG-MASK-05 真实模型与弹幕组合检查点
本次保持 doing,不增加完成任务数。生产源码、公开声明和构建产物没有修改。
新增可重跑的真实 MediaPipe/WASM 测试,固定历史核心来源并增强发布归档工具。
## 实际覆盖
`test/browser/danmuku-mask-native.spec.js` 使用正常构建的 Mask 与 Danmuku、仓库内
Steve Jobs 视频和 12 个固定模型资源,不替换 SDK、原生媒体时钟或 canvas 编码。
候选核心、实际发布核心和实际 5.3.1-beta.1 核心各有两例,分别验证:
- 模型加载、连续原生输出、PNG 中同时存在透明和不透明像素;stop 后重启、
destroy(false) 后画布清零,连续观察 500ms 没有新输出或 mask 复活。
- 模型就绪后的时间戳弹幕显示;真实节点仍在原根层且有尺寸;暂停时媒体时钟
停止,允许最多一次已在执行的输出;等待原生 seeked 后至少两次新输出,
网页全屏进出保持根节点;隐藏/显示后另一条新弹幕正常投递。
独立审查发现只检查编码时的 currentTime 会误认 seek 前已启动的推理已更新,
因此测试明确等待 seeked 并记录其输出计数。visible 事件也必须附有实际节点
连接、尺寸与归属证据。截图可见人物区域遮住弹幕,但不是精确分割质量评分。
自动夹具同时保存浏览器版本、资源指纹、媒体状态、请求和错误。
主产物与 legacy 各 18 项、合计 36 项通过,0 重试/跳过;精确环境见
[机器记录](../baselines/danmuku-mask-native-validation.json)。浏览器是 Windows 下的
Playwright Chromium/Firefox/WebKit;不是连接 Chrome、macOS Safari 或移动真机。
无需重新构建生产文件:04 已确认这些产物与 03 的输出一致。
## 历史失败与尚未修复的现象
最初收集失败来自 Windows checkout 把 README 换为 CRLF。现在仅文档、声明及
JSON 允许 LF 归一化比较,同时记录实际 SHA;执行 JS、WASM 和模型二进制仍需
逐字节匹配固定基线,没有修改资源文件。
首次组合测试以固定 time=6 弹幕作为启动验收,六例中五例没有投递该条。
后续 Chromium 诊断保留原条目和原 readys getter 的委托观测:发布核心一例
首次采样为 6.202297,已经超过原 6±0.1 秒窗口;模型就绪后新增的未来条目正常。
这是实际启动窗口遗漏,尚不足以断言具体哪段 CPU/SDK 操作造成它。首次报告在
执行期间有另一处尚未到达的暂停断言编辑,因此只作为固定启动断言的失败证据,
不作为全文件版本的最终验收。
最终用例把启动条目明确保留为诊断,成功断言覆盖模型就绪后的持续组合;
最终主产物 9 个组合中 8 个、legacy 9 个组合中 9 个未显示启动条目,这一现象
仍然存在;两轮各 9 个模型就绪后的组合断言均通过,不能混为启动验收通过。
没有扩大原弹幕资格窗口、添加追补语义或把遗漏标为已修复。启动性能与采样
影响保留在 MASK-SCHEDULING-01 / PKG-MASK-05,不能据此声称全时段零丢失。
## 历史核心与工具修改
[独立捕获记录](../baselines/danmuku-mask-historical-cores.json) 区分插件发布关联 Git
manifest 的核心版本与实际 npm 产物。5.3.1 未找到发布归档:metadata、packument
和 tarball 检查保留缺失事实;5.3.1-beta.1 则有实际 tarball、SHA-512/SHA-256、
manifest 与入口校验。不能把 prerelease 或相邻 5.3.0 当作 5.3.1。
共享 releases helper 现在接受受限的 prerelease 标识,仍拒绝路径穿越并保留
registry 源、归档完整性验证。新增危险版本反例与真实 beta 归档测试,共三项。
浏览器 server 从该归档提取固定主入口,manifest 记录其实际身份与 SHA。
原捕获记录中的 stable-only helper 限制是捕获时事实,本次才补齐支持。
## 重跑与剩余门槛
使用 .node-version 的 Node 与 Yarn 1.22.22:
```sh
yarn test:browser test/browser/danmuku-mask-native.spec.js --workers=1
node --test refactor/scripts/releases.test.mjs
```
第二轮设置 `ARTPLAYER_MASK_ARTIFACT` 为
`packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.legacy.js`。
每轮等待进程完成,保存 report.json/results 后再启动下一轮。当前加载方式是
script global,不支持把 .mjs 传入该 override;真实 ESM/安装包验证仍属 06。
尚缺多实例共享原生后端、切源/网络失败恢复、实际推理 fallback、OS 全屏、
设备矩阵、GPU/WASM 内部释放和持续资源测量。body-segmentation 1.0.2 的 dispose
没有返回内部 close Promise,画布归零不能证明 GPU 已释放。默认远端未固定
solutionPath 与许可/分发核对仍在 06;本次仅验证本地固定资源。
归档工具三项、定向 lint、plan/risk 检查与影响分析通过。共享测试 server/helper
使影响分析保守要求全生态 CI;这不是本地全套或远端 CI 已通过的声明。原生报告
无 pageerror/console error,媒体 abort/cache 请求诊断保留在机器记录中。
回退此检查点测试、server 路由与 prerelease 支持不影响生产接口。没有新增依赖、
修改锁文件、推送、发布或远端 CI 验收。
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 231 项,范围 22 个包及工作区/示例。
状态:todo 66 / doing 14 / blocked 0 / done 151 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 65 / doing 15 / blocked 0 / done 151 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -244,7 +244,7 @@
| PKG-MASK-02 | artplayer-plugin-danmuku-mask<br>建立特有行为与错误测试 | PKG-MASK-01, ENG-03, ENG-05 | 加载期间停止/销毁、重复启动、推理失败、WebGL/CPU 边界 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-MASK-03 | artplayer-plugin-danmuku-mask<br>整理内部职责与资源 | PKG-MASK-02, CORE-18, PKG-DANMUKU-07 | 模型加载/推理/画布输出分离,阻止重叠推理与过期写入 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-MASK-04 | artplayer-plugin-danmuku-mask<br>迁移自有源码和公开类型 | PKG-MASK-03, ENG-04, ENG-06, CORE-07 | 模型 adapter、canvas 和选项的精确类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done |
| PKG-MASK-05 | artplayer-plugin-danmuku-mask<br>验证新旧核心和组合 | PKG-MASK-04, CORE-22 | 真实模型和 danmuku/seek/全屏组合,GPU 资源释放 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
| PKG-MASK-05 | artplayer-plugin-danmuku-mask<br>验证新旧核心和组合 | PKG-MASK-04, CORE-22 | 真实模型和 danmuku/seek/全屏组合,GPU 资源释放 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | doing |
| PKG-MASK-06 | artplayer-plugin-danmuku-mask<br>验证分发并同步文档 | PKG-MASK-05, ENG-07 | danmuku.mask.js、资源版本/许可、CPU fallback 和包体积证据 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo |
## 5 包迁移:artplayer-plugin-asr
@@ -536,6 +536,7 @@
- PKG-MASK-02: [记录](changes/2026-09-13-PKG-MASK-02-failures.md) [记录](baselines/danmuku-mask-failures-validation.json)
- PKG-MASK-03: [记录](changes/2026-09-13-PKG-MASK-03-lifecycle.md) [记录](baselines/danmuku-mask-lifecycle-validation.json)
- PKG-MASK-04: [记录](changes/2026-09-14-PKG-MASK-04-types.md) [记录](baselines/danmuku-mask-types-validation.json)
- PKG-MASK-05: [记录](changes/2026-09-14-PKG-MASK-05-native-checkpoint.md) [记录](baselines/danmuku-mask-native-validation.json) [记录](baselines/danmuku-mask-historical-cores.json)
- PKG-ASR-01: [记录](changes/2026-09-13-PKG-ASR-01-contracts.md) [记录](baselines/asr-contract.md) [记录](baselines/asr-release.json)
- PKG-ASR-02: [记录](changes/2026-09-13-PKG-ASR-02-audio-baseline.md) [记录](baselines/asr-audio-validation.json)
- PKG-ASR-03: [记录](changes/2026-09-13-PKG-ASR-03-audio-ownership.md) [记录](baselines/asr-ownership-validation.json)
+8
View File
@@ -1,5 +1,13 @@
# 进度与证据
## PKG-MASK-05 真实模型组合检查点(仍 doing)
main/legacy 各 18 项,三个浏览器引擎和三个核心组合共 36 项通过。实际模型、PNG
透明像素、弹幕节点、暂停/seek/网页全屏和画布清理已有证据;启动窗口遗漏单独
登记 MASK-SCHEDULING-01,GPU/WASM/设备与其他剩余门槛未关闭。
[检查点](changes/2026-09-14-PKG-MASK-05-native-checkpoint.md)记录失败历史、独立审查、
历史 beta 归档、重跑和限制;没有生产修改或新增 done。
## PKG-MASK-04 严格TS及公开类型完成
五个自有模块迁TS且运行产物保持MASK03字节;真实核心宿主、SDK声明桥接和私有资源
+1
View File
@@ -241,3 +241,4 @@
| DANMUKU-HEATMAP-02 | resolved / 已复现 | Issue958 dense automatic heatmaps exceed the fixed Y domain and flatten across video | PKG-DANMUKU-10 |
| DANMUKU-TIMING-01 | resolved / 已复现 | Hidden Worker wait is deducted from visible lifetime and can recycle a comment immediately after visible | PKG-DANMUKU-11 |
| DANMUKU-SAMPLING-01 | resolved / 已复现 | Serial asynchronous placement stops native frame eligibility sampling and delays existing visible expiry | PKG-DANMUKU-12 |
| MASK-SCHEDULING-01 | open / 已复现 | Native Mask startup can coincide with a missed narrow Danmuku timestamp window | PKG-MASK-05 |
+25 -3
View File
@@ -754,7 +754,9 @@
"packages/artplayer-plugin-danmuku-mask/src/index.ts",
"refactor/baselines/danmuku-mask-contract.md",
"refactor/baselines/danmuku-mask-contract-validation.json",
"refactor/baselines/danmuku-mask-release.json"
"refactor/baselines/danmuku-mask-release.json",
"refactor/baselines/danmuku-mask-native-validation.json",
"refactor/changes/2026-09-14-PKG-MASK-05-native-checkpoint.md"
],
"compatibleResolution": "Npm dependency versions are separate from unversioned solutionPath CDN assets; keep option override, backend fallback, model result disposal and masks.",
"closureCriteria": "固定 SDK/外部资源版本和环境;成功/失败/切换/销毁有真实证据,设备和网络限制逐项记录。"
@@ -4922,7 +4924,9 @@
"test/danmuku-mask-failures.test.js",
"refactor/changes/2026-09-13-PKG-MASK-03-lifecycle.md",
"refactor/baselines/danmuku-mask-lifecycle-validation.json",
"test/danmuku-mask-lifecycle.test.js"
"test/danmuku-mask-lifecycle.test.js",
"refactor/baselines/danmuku-mask-native-validation.json",
"refactor/changes/2026-09-14-PKG-MASK-05-native-checkpoint.md"
],
"compatibleResolution": "Invalidate stale work while keeping restartable stop distinct from final destroy; release model and subscriptions. PKG-MASK-03 fixes controlled stale work, owned bitmaps and subscriptions; native SDK dispose completion is not exposed by body-segmentation 1.0.2 and remains a real-resource validation limit in PKG-MASK-05.",
"closureCriteria": "Pending initialization/inference cannot write masks or schedule work after stop/destroy; errors settle and owned resources are released."
@@ -5017,7 +5021,9 @@
"test/danmuku-mask-failures.test.js",
"refactor/changes/2026-09-13-PKG-MASK-03-lifecycle.md",
"refactor/baselines/danmuku-mask-lifecycle-validation.json",
"test/danmuku-mask-lifecycle.test.js"
"test/danmuku-mask-lifecycle.test.js",
"refactor/baselines/danmuku-mask-native-validation.json",
"refactor/changes/2026-09-14-PKG-MASK-05-native-checkpoint.md"
],
"compatibleResolution": "Handle missing dependencies and frame/context failures without stale writes or uncontrolled loops while preserving plugin ordering.",
"closureCriteria": "Missing Danmuku/context, CORS failure, source changes and ready/destroy timing have controlled and real browser evidence."
@@ -5395,6 +5401,22 @@
"refactor/changes/2026-09-13-PKG-DANMUKU-12-frame-sampling.md"
],
"resolutionRationale": "Controlled red/green tests prove continued observation and expiry with serial callback/Worker ownership, lifecycle clearing and ready priority. Three-format native media/RAF and existing two-core scheduling/lifetime tests pass. CPU-blocked unobserved timestamps and sustained load remain separately tracked."
},
{
"id": "MASK-SCHEDULING-01",
"title": "Native Mask startup can coincide with a missed narrow Danmuku timestamp window",
"confirmation": "reproduced",
"status": "open",
"owners": [
"PKG-MASK-05"
],
"evidence": [
"test/browser/danmuku-mask-native.spec.js",
"refactor/baselines/danmuku-mask-native-validation.json",
"refactor/changes/2026-09-14-PKG-MASK-05-native-checkpoint.md"
],
"compatibleResolution": "Retain startup sampling evidence and distinguish it from post-model-ready delivery; investigate actual startup scheduling without silently adding catch-up semantics.",
"closureCriteria": "Identify the startup scheduling cause and assess a compatible mitigation or explicitly reviewed limitation using actual model/media evidence; post-ready success alone cannot close this risk."
}
]
}
+6 -3
View File
@@ -1,8 +1,10 @@
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { Buffer } from 'node:buffer'
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
export const refactorDir = fileURLToPath(new URL('../', import.meta.url))
@@ -30,7 +32,7 @@ export function readMember(archive, member) {
}
export async function ensureArchive(release) {
assert(/^(?:@[a-z0-9]+(?:[.-][a-z0-9]+)*\/)?[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(release.name) && /^\d+\.\d+\.\d+$/.test(release.version), 'Invalid release identifier')
assert(/^(?:@[a-z0-9]+(?:[.-][a-z0-9]+)*\/)?[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(release.name) && /^\d+\.\d+\.\d+(?:-[a-z0-9]+(?:[.-][a-z0-9]+)*)?$/i.test(release.version), 'Invalid release identifier')
const archive = path.join(cacheDir, `${release.name.replace('/', '+')}-${release.version}.tgz`)
if (!fs.existsSync(archive)) {
const url = new URL(release.tarball)
@@ -59,7 +61,8 @@ export async function verifyReleases() {
assert.equal(manifest.name, release.name)
assert.equal(manifest.version, release.version)
for (const field of ['main', 'module', 'types', 'legacy']) {
if (manifest[field]) assert(names.includes(`package/${manifest[field].replace(/^\.\//, '')}`), `Missing ${field}: ${release.name}`)
if (manifest[field])
assert(names.includes(`package/${manifest[field].replace(/^\.\//, '')}`), `Missing ${field}: ${release.name}`)
}
console.log(`Verified ${release.name}@${release.version}: SHA-512, SHA-256, ${names.length} files and manifest entrypoints`)
}
+13 -1
View File
@@ -1,4 +1,7 @@
import assert from 'node:assert/strict'
import { Buffer } from 'node:buffer'
import fs from 'node:fs'
// eslint-disable-next-line test/no-import-node-test -- Archive verification uses the repository Node baseline runner.
import { test } from 'node:test'
import { ensureArchive, hash, verifyIntegrity } from './releases.mjs'
@@ -13,6 +16,15 @@ test('release integrity rejects corrupted bytes and inconsistent frozen digests'
test('release cache rejects unsafe package and version path identifiers before I/O', async () => {
for (const name of ['../hls.js', '.hls', 'hls..js', 'hls/js', 'hls\\js', 'hls.js/evil', '-hls', '@svta/../outside', '@../outside', '@svta/cml/request', '@svta\\cml-request', '@svta+cml-request', '@svta/', '@svta/.hidden'])
await assert.rejects(ensureArchive({ name, version: '1.5.17' }), /Invalid release identifier/)
for (const version of ['../1.5.17', '1.5.17/evil', 'latest'])
for (const version of ['../1.5.17', '1.5.17/evil', 'latest', '5.3.1-beta/1', '5.3.1-beta\\1', '5.3.1-beta..1', '5.3.1-../outside'])
await assert.rejects(ensureArchive({ name: 'hls.js', version }), /Invalid release identifier/)
})
test('release cache accepts the fixed historical prerelease without replacing its identity', async () => {
const baseline = JSON.parse(fs.readFileSync(new URL('../baselines/danmuku-mask-historical-cores.json', import.meta.url), 'utf8'))
const { release } = baseline.entries.find(entry => entry.requestedVersion === '5.3.1-beta.1')
assert.equal(release.version, '5.3.1-beta.1')
const archive = await ensureArchive(release)
assert(archive.endsWith('artplayer-5.3.1-beta.1.tgz'))
verifyIntegrity(fs.readFileSync(archive), release)
})
+6 -2
View File
@@ -2605,11 +2605,15 @@
"PKG-MASK-04",
"CORE-22"
],
"status": "todo",
"status": "doing",
"risk": "H",
"deliverable": "真实模型和 danmuku/seek/全屏组合,GPU 资源释放",
"acceptance": "最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成",
"evidence": []
"evidence": [
"changes/2026-09-14-PKG-MASK-05-native-checkpoint.md",
"baselines/danmuku-mask-native-validation.json",
"baselines/danmuku-mask-historical-cores.json"
]
},
{
"id": "PKG-MASK-06",
+11
View File
@@ -158,3 +158,14 @@ remain observations. No media/RAF clock is replaced or ready state seeded here.
across commands and destroy(false), with actual video and placement Worker. It
uses explicit ready rows for resource setup and a fixed SVG mask; it does not
load the Mask SDK/model or provide timestamp-delivery evidence.
`danmuku-mask-native.spec.js` loads the actual built Mask/SDK and fixed local
MediaPipe assets with native video. It verifies bitmap alpha, stop/restart/destroy,
then actual Danmuku delivery after model readiness, pause, seek and CSS web
fullscreen with candidate, published and actual 5.3.1-beta.1 cores. Set
`ARTPLAYER_MASK_ARTIFACT` to a main/legacy global artifact; ESM is not supported by
that override. Run formats sequentially and archive each completed report.
The startup time=6 row remains diagnostic because initial model work can coincide
with a missed narrow timestamp window. A passing post-ready combination is not
proof of startup losslessness, private GPU/WASM closure, OS fullscreen or devices.
See refactor/changes/2026-09-14-PKG-MASK-05-native-checkpoint.md for exact limits.
+262
View File
@@ -0,0 +1,262 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import process from 'node:process'
import { hash } from '../../refactor/scripts/releases.mjs'
import { expect, test } from './fixtures.js'
const artifact = process.env.ARTPLAYER_MASK_ARTIFACT || 'packages/artplayer-plugin-danmuku-mask/dist/artplayer-plugin-danmuku-mask.js'
const code = fs.readFileSync(artifact, 'utf8')
const baseline = JSON.parse(fs.readFileSync('refactor/baselines/danmuku-mask-release.json', 'utf8'))
const assets = baseline.sdk.assets.map(({ file, sha256 }) => {
const bytes = fs.readFileSync(file)
const actual = hash(bytes)
// Git checkout may convert documentation/declaration newlines. Executed model
// scripts and binaries must retain their exact fixed bytes.
const normalizedLF = /\.(?:md|d\.ts|json)$/.test(file) && actual !== sha256
assert.equal(normalizedLF ? hash(bytes.toString().replaceAll('\r\n', '\n')) : actual, sha256, `Model asset differs from fixed source: ${file}`)
return { file, sha256: actual, baselineSha256: sha256, normalizedLF }
})
const media = 'docs/assets/sample/steve-jobs.mp4'
const danmukuFile = 'packages/artplayer-plugin-danmuku/dist/artplayer-plugin-danmuku.js'
const danmukuCode = fs.readFileSync(danmukuFile, 'utf8')
for (const core of ['candidate', 'published', 'published-5.3.1-beta.1']) {
test(`Mask actual local MediaPipe model renders, stops and restarts with ${core} core`, async ({ page }, testInfo) => {
test.setTimeout(90000) // Includes two actual model initializations and native video decoding.
const external = []
const resources = []
const origin = new URL(testInfo.project.use.baseURL).origin
await page.route('**/*', (route) => {
if (new URL(route.request().url()).origin !== origin) {
external.push(route.request().url())
return route.abort()
}
return route.continue()
})
page.on('response', (response) => {
if (response.url().includes('/assets/@mediapipe/'))
resources.push({ url: response.url(), status: response.status() })
})
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: code })
await page.evaluate(() => {
window.maskNative = { outputs: [], snapshots: [] }
const encode = HTMLCanvasElement.prototype.toDataURL
HTMLCanvasElement.prototype.toDataURL = function (...args) {
const url = encode.apply(this, args)
window.maskNative.outputs.push({ canvas: this, url, time: window.art?.currentTime })
return url
}
window.createPlayer('/assets/sample/steve-jobs.mp4')
window.art.plugins.add(window.artplayerPluginDanmukuMask({ solutionPath: '/assets/@mediapipe/selfie_segmentation' }))
window.maskPlugin = window.art.plugins.artplayerPluginDanmukuMask
window.maskLayer = window.art.template.$danmuku
})
try {
await expect.poll(() => page.evaluate(() => window.art.duration)).toBeGreaterThan(1)
await page.evaluate(() => {
window.art.currentTime = Math.min(5, window.art.duration / 2)
})
await page.locator('#play').click()
await page.evaluate(() => window.maskPlugin.start())
await expect.poll(() => page.evaluate(() => window.maskNative.outputs.length), { timeout: 30000 }).toBeGreaterThan(1)
const first = await page.evaluate(async () => {
const records = window.maskNative.outputs
const output = records.at(-1)
const image = new Image()
image.src = output.url
await image.decode()
const canvas = document.createElement('canvas')
canvas.width = image.naturalWidth
canvas.height = image.naturalHeight
const ctx = canvas.getContext('2d')
ctx.drawImage(image, 0, 0)
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data
let transparent = 0
let opaque = 0
for (let i = 3; i < pixels.length; i += 4) {
if (pixels[i] === 0)
transparent++
if (pixels[i] === 255)
opaque++
}
const value = { time: window.art.currentTime, firstTime: records[0].time, lastTime: output.time, width: canvas.width, height: canvas.height, transparent, opaque, pixels: pixels.length / 4, maskImage: window.maskLayer.style.maskImage.slice(0, 45), outputs: records.length }
window.maskNative.snapshots.push(value)
return value
})
expect(first.width).toBeGreaterThan(0)
expect(first.height).toBeGreaterThan(0)
expect(first.lastTime).toBeGreaterThan(first.firstTime)
expect(first.transparent).toBeGreaterThan(0)
expect(first.opaque).toBeGreaterThan(0)
expect(first.maskImage).toContain('data:image/png')
await testInfo.attach('actual-model-page', { body: await page.screenshot(), contentType: 'image/png' })
await page.evaluate(() => {
window.maskPlugin.stop()
window.stoppedOutputCount = window.maskNative.outputs.length
})
await expect.poll(() => page.evaluate(() => window.maskNative.outputs.every(({ canvas }) => canvas.width === 0 && canvas.height === 0))).toBe(true)
expect(await page.evaluate(() => window.maskLayer.style.maskImage)).toBe('none')
await page.waitForTimeout(500) // Bounded native observation after output cleanup.
expect(await page.evaluate(() => window.maskNative.outputs.length)).toBe(await page.evaluate(() => window.stoppedOutputCount))
expect(await page.evaluate(() => window.maskLayer.style.maskImage)).toBe('none')
await page.evaluate(() => window.maskPlugin.start())
await expect.poll(() => page.evaluate(() => window.maskNative.outputs.length > window.stoppedOutputCount), { timeout: 30000 }).toBe(true)
await page.evaluate(() => {
window.art.destroy(false)
window.destroyedOutputCount = window.maskNative.outputs.length
})
await expect.poll(() => page.evaluate(() => window.maskNative.outputs.every(({ canvas }) => canvas.width === 0 && canvas.height === 0))).toBe(true)
expect(await page.evaluate(() => window.maskLayer.style.maskImage)).toBe('none')
await page.waitForTimeout(500)
expect(await page.evaluate(() => window.maskNative.outputs.length)).toBe(await page.evaluate(() => window.destroyedOutputCount))
expect(await page.evaluate(() => window.maskLayer.style.maskImage)).toBe('none')
expect(external).toEqual([])
expect(resources.some(item => item.url.endsWith('.wasm') && item.status === 200)).toBe(true)
expect(resources.every(item => item.status === 200)).toBe(true)
}
finally {
const state = await page.evaluate(() => ({ snapshots: window.maskNative?.snapshots, outputs: window.maskNative?.outputs.map(({ canvas, time }) => ({ width: canvas.width, height: canvas.height, time })), video: window.art ? { time: window.art.currentTime, duration: window.art.duration, paused: window.art.video.paused, size: [window.art.video.videoWidth, window.art.video.videoHeight] } : null, mask: window.maskLayer?.style.maskImage?.slice(0, 80) }))
await testInfo.attach('actual-model-evidence', { body: JSON.stringify({ artifact, sha256: hash(code), core, assets, media: { file: media, sha256: hash(fs.readFileSync(media)) }, external, resources, state, limitation: 'Actual SDK/model, native video and output bitmap lifecycle. No assertion of private MediaPipe/GPU closure or Danmuku layout composition.' }, null, 2), contentType: 'application/json' })
await page.evaluate(() => {
if (window.art && !window.art.isDestroy)
window.art.destroy()
})
}
})
test(`Mask actual model and Danmuku share the layer through pause, seek and web fullscreen with ${core} core`, async ({ page }, testInfo) => {
test.setTimeout(90000)
await page.goto(`/test/player.html?core=${core}`)
await page.addScriptTag({ content: danmukuCode })
await page.addScriptTag({ content: code })
const unexpected = []
const origin = new URL(testInfo.project.use.baseURL).origin
await page.route('**/*', (route) => {
if (new URL(route.request().url()).origin !== origin) {
unexpected.push(route.request().url())
return route.abort()
}
return route.continue()
})
await page.evaluate(() => {
window.maskCombination = { times: [], visible: [], snapshots: [], samples: [] }
const encode = HTMLCanvasElement.prototype.toDataURL
HTMLCanvasElement.prototype.toDataURL = function (...args) {
const result = encode.apply(this, args)
window.maskCombination.times.push(window.art?.currentTime)
return result
}
window.art = new window.Artplayer({
container: '.player',
url: '/assets/sample/steve-jobs.mp4',
muted: true,
fullscreenWeb: true,
plugins: [window.artplayerPluginDanmuku({ danmuku: [{ text: 'Native mask combination', time: 6, mode: 1 }], heatmap: false }), window.artplayerPluginDanmukuMask({ solutionPath: '/assets/@mediapipe/selfie_segmentation' })],
})
window.maskCombination.layer = window.art.template.$danmuku
const owner = window.art.plugins.artplayerPluginDanmuku.config({})
const ready = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(owner), 'readys').get
Object.defineProperty(owner, 'readys', { configurable: true, get() {
const rows = ready.call(this)
window.maskCombination.samples.push({ time: window.art.currentTime, wall: performance.now(), rows: rows.map(row => ({ time: row.time, text: row.text })) })
return rows
} })
window.art.on('artplayerPluginDanmuku:visible', (row) => {
const node = row.$ref
const rect = node?.getBoundingClientRect()
window.maskCombination.visible.push({ text: row.text, connected: node?.isConnected, inLayer: node ? window.maskCombination.layer.contains(node) : false, width: rect?.width, height: rect?.height, display: node ? getComputedStyle(node).display : null })
})
document.querySelector('#play').onclick = () => window.art.play()
document.querySelector('#pause').onclick = () => window.art.pause()
window.maskSnapshot = (phase) => {
const layer = window.art.template.$danmuku
const rect = layer.getBoundingClientRect()
const snapshot = { phase, time: window.art.currentTime, outputs: window.maskCombination.times.length, sameLayer: layer === window.maskCombination.layer, mask: layer.style.maskImage.startsWith('url('), mode: getComputedStyle(layer).maskMode, width: rect.width, height: rect.height, fullscreenWeb: window.art.fullscreenWeb }
window.maskCombination.snapshots.push(snapshot)
return snapshot
}
})
try {
await expect.poll(() => page.evaluate(() => window.art.duration)).toBeGreaterThan(20)
await page.evaluate(() => {
window.art.currentTime = 5
})
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.maskCombination.times.length), { timeout: 30000 }).toBeGreaterThan(2)
// Keep the startup row and sampling history as diagnostics: native model
// initialization may block its narrow existing timestamp window. Exercise
// the ongoing combination after the real model has become operational.
await page.evaluate(async () => {
const time = window.art.currentTime + 1
window.maskCombination.scheduled = time
await window.art.plugins.artplayerPluginDanmuku.load([{ text: 'Ready mask combination', time, mode: 1 }])
})
await expect.poll(() => page.evaluate(() => window.maskCombination.visible.map(row => row.text))).toContain('Ready mask combination')
const row = await page.evaluate(() => window.maskCombination.visible.find(row => row.text === 'Ready mask combination'))
expect(row.connected && row.inLayer).toBe(true)
expect(row.width).toBeGreaterThan(0)
expect(row.height).toBeGreaterThan(0)
expect(row.display).not.toBe('none')
const initial = await page.evaluate(() => window.maskSnapshot('playing'))
expect(initial.mask).toBe(true)
expect(initial.sameLayer).toBe(true)
await page.locator('#pause').click()
await expect.poll(() => page.evaluate(() => window.art.video.paused)).toBe(true)
// Wait native frames for the already-running SDK frame to finish. Compare
// its media timestamp, rather than forbidding one legitimate late encode.
const paused = await page.evaluate(async () => {
const time = window.art.currentTime
const beforeOutputs = window.maskCombination.times.length
await new Promise(resolve => setTimeout(resolve, 500))
return { before: time, after: window.art.currentTime, extraOutputs: window.maskCombination.times.length - beforeOutputs, snapshot: window.maskSnapshot('paused') }
})
expect(Math.abs(paused.after - paused.before)).toBeLessThan(0.01)
expect(paused.extraOutputs).toBeLessThanOrEqual(1)
await page.evaluate(async () => {
const seeked = new Promise(resolve => window.art.video.addEventListener('seeked', resolve, { once: true }))
window.art.currentTime = 20
window.art.fullscreenWeb = true
await seeked
window.maskCombination.seeked = { time: window.art.currentTime, outputs: window.maskCombination.times.length }
})
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThanOrEqual(19.9)
const full = await page.evaluate(() => window.maskSnapshot('fullscreen-paused'))
expect(full.sameLayer).toBe(true)
expect(full.fullscreenWeb).toBe(true)
expect(full.width).toBeGreaterThan(initial.width)
await page.evaluate(() => window.art.play())
await expect.poll(() => page.evaluate(() => window.maskCombination.times.length - window.maskCombination.seeked.outputs), { timeout: 30000 }).toBeGreaterThanOrEqual(2)
expect(await page.evaluate(() => window.maskCombination.times.at(-1))).toBeGreaterThan(20)
await page.evaluate(async () => {
window.art.fullscreenWeb = false
window.art.plugins.artplayerPluginDanmuku.hide()
window.art.plugins.artplayerPluginDanmuku.show()
await window.art.plugins.artplayerPluginDanmuku.load([{ text: 'After seek and visibility', time: window.art.currentTime + 1, mode: 1 }])
})
await expect.poll(() => page.evaluate(() => window.maskCombination.visible.map(row => row.text))).toContain('After seek and visibility')
const afterRow = await page.evaluate(() => window.maskCombination.visible.find(row => row.text === 'After seek and visibility'))
expect(afterRow.connected && afterRow.inLayer).toBe(true)
expect(afterRow.width).toBeGreaterThan(0)
expect(afterRow.height).toBeGreaterThan(0)
expect(afterRow.display).not.toBe('none')
const restored = await page.evaluate(() => window.maskSnapshot('restored'))
expect(restored.sameLayer).toBe(true)
expect(restored.mask).toBe(true)
expect(restored.width).toBe(initial.width)
expect(restored.mode).toBe('alpha')
await testInfo.attach('actual-combination-page', { body: await page.screenshot(), contentType: 'image/png' })
await page.evaluate(() => window.art.destroy(false))
expect(await page.evaluate(() => window.maskCombination.layer.style.maskImage)).toBe('none')
expect(unexpected).toEqual([])
}
finally {
const state = await page.evaluate(() => ({ times: window.maskCombination.times, visible: window.maskCombination.visible, snapshots: window.maskCombination.snapshots, scheduled: window.maskCombination.scheduled, samples: window.maskCombination.samples, seeked: window.maskCombination.seeked }))
await testInfo.attach('actual-combination-evidence', { body: JSON.stringify({ core, mask: { file: artifact, sha256: hash(code) }, danmuku: { file: danmukuFile, sha256: hash(danmukuCode) }, unexpected, state, limitation: 'Actual model, native media, timestamp-delivered Danmuku and CSS web fullscreen. No OS fullscreen, GPU completion or device coverage claim.' }, null, 2), contentType: 'application/json' })
await page.evaluate(() => {
if (window.art && !window.art.isDestroy)
window.art.destroy()
})
}
})
}
+7 -1
View File
@@ -88,6 +88,12 @@ add('/test/declaration-cues.vtt', fs.readFileSync(path.join(workspace, 'test/bro
add('/test/thumbnail-grid.svg', fs.readFileSync(path.join(workspace, 'test/browser/media/thumbnail-grid.svg')), { kind: 'test-thumbnail-grid', file: 'test/browser/media/thumbnail-grid.svg' })
add('/test/legacy-safe-area.js', fs.readFileSync(path.join(workspace, 'test/helpers/legacy-safe-area.js')), { kind: 'frozen-own-source', file: 'test/helpers/legacy-safe-area.js', commit: 'ccf77c4e' })
add('/test/iframe-boundary-child.html', fs.readFileSync(path.join(workspace, 'test/browser/iframe-boundary-child.html')), { kind: 'iframe-fixture', file: 'test/browser/iframe-boundary-child.html' })
const maskHistorical = JSON.parse(fs.readFileSync(path.join(workspace, 'refactor/baselines/danmuku-mask-historical-cores.json'), 'utf8'))
const maskCore = maskHistorical.entries.find(entry => entry.requestedVersion === '5.3.1-beta.1').release
const maskCoreMember = `package/${maskCore.manifest.main.replace(/^\.\//, '')}`
const maskCoreBytes = readMember(await ensureArchive(maskCore), maskCoreMember)
assert.equal(hash(maskCoreBytes), maskCore.files[maskCoreMember], 'Historical Mask core differs from frozen archive')
add('/published-5.3.1-beta.1/artplayer.js', maskCoreBytes, { kind: 'npm-release', version: maskCore.version, integrity: maskCore.integrity, member: maskCoreMember })
function send(req, res, bytes, filename) {
res.setHeader('Content-Type', mime[path.extname(filename)] || 'application/octet-stream')
res.setHeader('Cache-Control', 'no-store')
@@ -159,7 +165,7 @@ const server = http.createServer((req, res) => {
if (url.pathname === '/test/player.html') {
const core = url.searchParams.get('core') || 'candidate'
const chapter = url.searchParams.get('chapter') || 'candidate'
assert(['candidate', 'published', 'published-4.5.5', 'published-4.5.9', 'published-5.1.7', 'published-5.3.0'].includes(core) && ['candidate', 'published'].includes(chapter), 'Invalid test combination')
assert(['candidate', 'published', 'published-4.5.5', 'published-4.5.9', 'published-5.1.7', 'published-5.3.0', 'published-5.3.1-beta.1'].includes(core) && ['candidate', 'published'].includes(chapter), 'Invalid test combination')
const html = fs.readFileSync(path.join(workspace, 'test/browser/player.html'), 'utf8')
.replace('__CORE__', core)
.replace('__CHAPTER__', chapter)